← All use cases

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.

<base>_subs.vtt Priya: we ship the migration Thursday
1280×720 · 25 fps · HLS + WebVTT → S3 A screen share holds the main region because it outranks active_speaker in mainPriority. The transcript is not in the pixels — HLS publishes it as its own WebVTT rendition.

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}"