← All use cases

Per-participant moderation

Review every participant separately, so a finding points at a person rather than a composite.

Live social and creator platforms that have to answer "who did this, and when" — for trust and safety, for a takedown request, or for a regulator. This is the one pipeline here that deliberately never mixes.

no canvas · no mixer · streams stay separate Every other sink would force a mixer first and hand you one composited frame. image and websocket are the only n:n sinks, so a finding still points at a participant.

The pipeline

Sources

  • livekit

Nodes

  • audio_resample (16 kHz mono)

Sinks

  • image → S3
  • websocket
livekit(room) ─┬→ image ────────────────────────→ S3 (one jpeg per participant)
               └→ audio_resample → websocket ────→ your service (one socket per participant)

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.

These are the only two sinks that do not force a mixer

image and websocket are n:n — they keep one output per upstream stream. Every other sink is 1:1, so feeding it a room means composing first and losing track of who is who. segment is 1:1 on purpose: it would otherwise run a full encoder per participant.

Per-participant audio cannot go through an encoder

An audio_encoder is 1:1, so livekit → audio_encoder is rejected at submit time with a hint to insert a mixer — which is exactly what you are trying to avoid. Per-participant audio therefore leaves as PCM. audio_resample is the one n:n audio node, so it is what stands between the room and the socket.

Resampling is a cost decision, not just a format one

PCM is billed as egress. At 48 kHz stereo each participant is ~1.5 Mbit/s; at 16 kHz mono it is ~0.26 Mbit/s — about 6× cheaper, and already the format speech models want. For a ten-person room that difference is most of the bill.

Attribution comes from the object key

The pathPrefix is templated, so moderation/live-show/{identity} gives every participant their own folder and a finding does not depend on parsing a filename.

The image sink has no webhook

It uploads to s3, gcp, or azure and nothing else, so the loop is AVFlow → bucket → your bucket event → your service. With intervalSec at 10 this is sampling for evidence and audit, not a real-time gate. The audio socket is the low-latency half.

Neither sink carries captions

Routing an asr node into image is rejected — it does not support subtitles. Transcript-based moderation means running speech-to-text on the PCM you receive, or adding a separate asr → livekit path for data events.

The Job

Replace tokens, URLs, and storage credentials, then submit it. This is the same file /examples/25-moderation-per-participant.json serves.

{
  "name": "moderation-review",
  "sources": [
    {
      "name": "room",
      "type": "livekit",
      "config": {
        "serverUrl": "wss://your-project.livekit.cloud",
        "token": "<subscribe-token>",
        "select": {
          "mediaTypes": [
            "audio",
            "video"
          ]
        }
      }
    }
  ],
  "nodes": [
    {
      "name": "for_review",
      "type": "audio_resample",
      "inputs": [
        {
          "name": "room",
          "select": {
            "mediaTypes": [
              "audio"
            ]
          }
        }
      ],
      "config": {
        "sampleRate": 16000,
        "channels": 1
      }
    }
  ],
  "sinks": [
    {
      "name": "frames",
      "type": "image",
      "inputs": [
        {
          "name": "room",
          "select": {
            "mediaTypes": [
              "video"
            ]
          }
        }
      ],
      "config": {
        "storageType": "s3",
        "storageConfig": {
          "bucket": "your-bucket",
          "region": "us-east-1",
          "accessKeyId": "<access-key-id>",
          "secretAccessKey": "<secret-access-key>",
          "pathPrefix": "moderation/live-show/{identity}"
        },
        "format": "jpeg",
        "intervalSec": 10,
        "quality": 80,
        "width": 640,
        "height": 360
      }
    },
    {
      "name": "audio_review",
      "type": "websocket",
      "inputs": [
        "for_review"
      ],
      "config": {
        "url": "wss://moderation.your-app.example.com/audio",
        "headers": {
          "Authorization": "Bearer <moderation-service-token>"
        }
      }
    }
  ],
  "policies": {
    "maxDurationSec": 14400,
    "idleTimeoutSec": 300
  }
}

Submit it

curl -X POST "https://api.avflow.dev/v1/jobs" \
  -H "Authorization: Bearer ${AVFLOW_API_KEY}" \
  -H "Content-Type: application/json" \
  -d @25-moderation-per-participant.json

# check status
curl "https://api.avflow.dev/v1/jobs/moderation-review" \
  -H "Authorization: Bearer ${AVFLOW_API_KEY}"

# stop
curl -X DELETE "https://api.avflow.dev/v1/jobs/moderation-review" \
  -H "Authorization: Bearer ${AVFLOW_API_KEY}"