← All use cases

Vertical co-host switching

A 1080×1920 guest stream whose layout you retarget mid-broadcast without dropping the output.

Live shopping and creator apps where a host brings guests on and off screen. The audience sees the layout change; they never see the stream reconnect.

1080×1920 · 30 fps · RTMP The inset sits at offsetX 0.6 / offsetY 0.64 because that is what the Job says. Re-submit with different numbers and it moves without the stream reconnecting.

The pipeline

Sources

  • livekit

Nodes

  • video_mixer (custom, 9:16)
  • audio_mixer

Sinks

  • rtmp_push
livekit(room) ─┬→ video_mixer (custom, 1080x1920) ─┐
               └→ audio_mixer ──────────────────── ┴→ rtmp_push

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.

Switching layouts is an update, not a restart

There is no layout-only endpoint. Submit the Job again with the same name and different regions to update the running mixer in place. The RTMP output keeps flowing.

Regions match participants, not slots

A region names its occupant with source — by identity, trackSource, or index. In ratio coordinates the same numbers describe any canvas size, and a placeholder covers the region until its participant shows up.

Two limits to design around

The canvas caps at 1920 on the long edge and 1080 on the short edge, so 1080×1920 is the largest vertical frame. And zIndex is paint order only — there is no alpha blending, so an inset tile fully covers what sits beneath it.

The Job

Replace tokens, URLs, and storage credentials, then submit it. This is the same file /examples/21-cohost-vertical-switch.json serves.

{
  "name": "cohost-vertical",
  "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": 1080,
          "height": 1920,
          "fps": 30,
          "backgroundColor": "#05070f",
          "coordinateSystem": "ratio"
        },
        "layout": {
          "mode": "custom",
          "common": {
            "borderRadius": 20
          },
          "custom": {
            "regions": [
              {
                "name": "host",
                "source": {
                  "identity": "host"
                },
                "width": 1,
                "height": 1,
                "fit": "cover",
                "placeholder": {
                  "text": {
                    "content": "Waiting for the host…",
                    "position": "bottom",
                    "color": "#94a3b8",
                    "fontSize": 40
                  }
                }
              },
              {
                "name": "guest",
                "source": {
                  "identity": "guest"
                },
                "offsetX": 0.6,
                "offsetY": 0.64,
                "width": 0.36,
                "height": 0.24,
                "zIndex": 2,
                "fit": "cover",
                "style": {
                  "backgroundColor": "#111a2e",
                  "borderColor": "#38bdf8",
                  "borderWidth": 4,
                  "borderRadius": 28
                },
                "placeholder": {
                  "text": {
                    "content": "Waiting for a guest…",
                    "position": "bottom",
                    "color": "#94a3b8",
                    "fontSize": 40
                  }
                }
              }
            ]
          }
        }
      }
    },
    {
      "name": "mix",
      "type": "audio_mixer",
      "inputs": [
        "room"
      ],
      "config": {}
    }
  ],
  "sinks": [
    {
      "name": "live",
      "type": "rtmp_push",
      "inputs": [
        "stage",
        "mix"
      ],
      "config": {
        "urls": [
          "rtmp://ingest.example.com/live/<stream-key>"
        ],
        "encoding": {
          "videoCodec": "h264",
          "audioCodec": "aac",
          "videoBitrateBps": 4000000,
          "audioBitrateBps": 128000,
          "keyframeIntervalSec": 2
        }
      }
    }
  ],
  "policies": {
    "maxDurationSec": 14400,
    "idleTimeoutSec": 120
  }
}

Submit it

curl -X POST "https://api.avflow.dev/v1/jobs" \
  -H "Authorization: Bearer ${AVFLOW_API_KEY}" \
  -H "Content-Type: application/json" \
  -d @21-cohost-vertical-switch.json

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

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