Meeting recording & notes
Record a call in speaker layout to your own storage, and get a transcript you can summarise.
Conferencing products that ship a "record this call" button and email a summary with action items afterwards. One Job produces the composited video, the mixed audio, and the transcript together.
The pipeline
Sources
- livekit
Nodes
- video_mixer (speaker)
- audio_mixer
- asr
Sinks
- segment → S3 (HLS + WebVTT)
livekit(room) ─┬→ video_mixer (speaker, screen share wins) ─┐
├→ audio_mixer ─────────────────────────────┤→ segment (S3: HLS + WebVTT)
└→ asr ─────────────────────────────────────┘ What makes it work
The parts that are not obvious from the JSON — usually because the naive approach does not do what you would expect.
Screen shares promote themselves
With mainPriority: ["screen_share", "active_speaker"] the mixer moves a screen share into the main region automatically and falls back to the loudest speaker otherwise, pushing everyone else into the thumbnail rail — no application logic and no re-submit.
segment is the only sink that carries captions without video
Every other sink transports captions inside the encoded video, as SEI on RTMP and SRT or as data messages on RTC. HLS is the exception because it publishes an independent WebVTT rendition, which is also what makes the transcript readable after the call.
Summarising happens in your app, not in AVFlow
AVFlow writes <base>_subs.vtt beside the recording. Turning that into notes is ordinary application work — read the object and send it to an LLM. HLS finalises the VTT when the job stops, so stop the job before summarising.
The Job
Replace tokens, URLs, and storage credentials, then submit it. This is the same file /examples/20-meeting-recording-notes.json serves.
{
"name": "meeting-recording",
"sources": [
{
"name": "room",
"type": "livekit",
"config": {
"serverUrl": "wss://your-project.livekit.cloud",
"token": "<subscribe-token>",
"select": {
"mediaTypes": [
"audio",
"video"
]
}
}
}
],
"nodes": [
{
"name": "stage",
"type": "video_mixer",
"inputs": [
"room"
],
"config": {
"canvas": {
"width": 1280,
"height": 720,
"fps": 25,
"backgroundColor": "#0b1120"
},
"layout": {
"mode": "speaker",
"common": {
"borderRadius": 12
},
"speaker": {
"mainPriority": [
"screen_share",
"active_speaker"
],
"mainRatio": 0.76,
"maxThumbnails": 4,
"thumbnailPosition": "right"
}
}
}
},
{
"name": "floor",
"type": "audio_mixer",
"inputs": [
"room"
],
"config": {}
},
{
"name": "transcript",
"type": "asr",
"inputs": [
"room"
],
"config": {
"language": "multi"
}
}
],
"sinks": [
{
"name": "recording",
"type": "segment",
"inputs": [
"stage",
"floor",
"transcript"
],
"config": {
"storageType": "s3",
"storageConfig": {
"bucket": "your-bucket",
"region": "us-east-1",
"accessKeyId": "<access-key-id>",
"secretAccessKey": "<secret-access-key>",
"pathPrefix": "meetings/standup"
},
"format": "hls",
"segmentDurationSec": 4,
"caption": {
"showSpeaker": true
},
"encoding": {
"videoCodec": "h264",
"audioCodec": "aac",
"keyframeIntervalSec": 2
}
}
}
],
"policies": {
"maxDurationSec": 14400,
"idleTimeoutSec": 180
}
} Submit it
curl -X POST "https://api.avflow.dev/v1/jobs" \
-H "Authorization: Bearer ${AVFLOW_API_KEY}" \
-H "Content-Type: application/json" \
-d @20-meeting-recording-notes.json
# check status
curl "https://api.avflow.dev/v1/jobs/meeting-recording" \
-H "Authorization: Bearer ${AVFLOW_API_KEY}"
# stop
curl -X DELETE "https://api.avflow.dev/v1/jobs/meeting-recording" \
-H "Authorization: Bearer ${AVFLOW_API_KEY}"