Skip to main content

Overlay Streaming

Overlay streaming composites a live web page — a scoreboard, a lower-third, a sponsor bar, any HTML — on top of a live video stream, and publishes the combined result as its own playable stream. The video comes in over RTMP or WebRTC; Native Frame renders your web page to images with the html2img plugin and burns them onto the video; viewers play back the composited output.

This guide drives the whole flow from the API. To set it up from the dashboard with no code, see Overlay Streaming (Dashboard).

New to streams and programs?

Overlay streaming builds on the v2 stream model. If "program", "stream", and "service-account token" are new, skim the Program API reference first — this guide assumes you can already create a program and a stream.

How it works

An overlay is two streams under the same program:

  • A source stream (kind: "source") — the raw video you ingest into.
  • An overlay stream (kind: "filtergraph") — the composited output. It points back at the source via derivedFromId, and it's the stream your viewers play.

Every stream response carries two fields that describe this relationship:

FieldTypeDescription
kindstring"source" for a regular stream, or "filtergraph" for an overlay. Defaults to "source" when omitted on create.
derivedFromIdstring | nullFor an overlay (filtergraph) stream, the id of the source stream it composites on top of. null for source streams.

The platform identifies an overlay by these two fields — not by name. An Overlay- name prefix is optional and just a human-readable label.

Step 1 — Create the streams

Create a program, then a source stream under it, then an overlay stream under the same program.

Create the source stream — a normal POST /program/api/v2/streams with kind omitted (defaults to source):

POST /program/api/v2/streams
Authorization: Bearer <serviceAccountJWT>
Content-Type: application/json

{
"streamName": "My Stream",
"authKey": "my-auth-key",
"authType": "public",
"transcode": true,
"programID": "{programId}",
"programSlug": "my-stream",
"apiVersion": "v2",
"appData": "{}",
"mode": "vf"
}

The create response includes the source stream's id (you'll need it next), plus vfIngress and vfManifestUrl (see Step 2 and Step 4).

Create the overlay stream — same program, with kind: "filtergraph" and derivedFromId set to the source stream's id:

POST /program/api/v2/streams
Authorization: Bearer <serviceAccountJWT>
Content-Type: application/json

{
"streamName": "Overlay-My Stream",
"authKey": "my-overlay-auth-key",
"authType": "public",
"transcode": true,
"programID": "{programId}",
"programSlug": "my-stream",
"apiVersion": "v2",
"appData": "{}",
"mode": "vf",
"kind": "filtergraph",
"derivedFromId": "{sourceStreamId}"
}

Use the same programID/programSlug as the source, and the source stream's id as derivedFromId. The overlay stream is the one you'll play back to see the composited result.

Find the overlay for a source

You can look up the overlay sibling of any source stream directly:

GET /program/api/v2/streams?derivedFromId={sourceStreamId}

Returns the filtergraph stream(s) derived from that source, or an empty array [] (HTTP 200) if none exists.

Step 2 — Build the RTMP ingest URL

You ingest video into the source stream over RTMP. The ingest URL is a base URL plus a stream key.

Base URL — take the rtmp value from vfIngress in the stream response and trim it to the /origin path:

rtmp://{host}/origin

Stream key — the create-stream response returns a streamKey (e.g. live_stream-uuid?authKey=my-auth-key). Append the namespace parameter ns, which is your project ID:

{streamKey}&ns={projectId}

Enable the overlay — to tell the platform to composite the overlay and push the result to the overlay stream, append a URL-encoded appdata parameter pointing at the overlay stream:

{streamKey}&ns={projectId}&appdata={urlEncodedAppData}

where appdata is a URL-encoded JSON object:

{
"authKey": "{overlayStreamAuthKey}",
"overlay_tag": "{overlayStreamId}"
}
FieldDescription
authKeyThe authKey of the overlay stream.
overlay_tagThe id of the overlay stream from Step 1.

If the stream has no overlay, omit the appdata parameter entirely and you get a plain (non-composited) ingest.

function buildRtmpUrl({ vfIngressRtmp, streamKey, projectId, overlayStreamId, overlayAuthKey }) {
// Trim the vfIngress rtmp URL to its /origin base
const baseUrl = `${vfIngressRtmp.match(/^rtmp:\/\/[^/]+/)[0]}/origin`;
let key = `${streamKey}&ns=${projectId}`;

if (overlayStreamId && overlayAuthKey) {
const appdata = JSON.stringify({ authKey: overlayAuthKey, overlay_tag: overlayStreamId });
key += `&appdata=${encodeURIComponent(appdata)}`;
}
return { baseUrl, streamKey: key };
}
WebRTC ingest (WHIP)

You can also ingest over WebRTC instead of RTMP — for example from the Aperture Live mobile app — by generating a WHIP link. The overlay is enabled the same way, by including appData with the overlay stream's authKey (as _authKey) and overlay_tag. See the Program API reference for the WHIP link endpoint.

Step 3 — Set the render config

The render config tells the platform what to render as the overlay. Configure it on the overlay stream, against your project's Media Assets host (returned as mediaAssetsHost from the dashboard public-config):

POST {mediaAssetsHost}/mediaassets/api/v1/render/config?ns={projectId}&tag={overlayStreamId}
Authorization: Bearer <serviceAccountJWT>
Content-Type: application/json

{
"renderType": "html2img",
"args": ["-u", "https://example.com/my-overlay"]
}
Query paramRequiredDescription
nsYesNamespace — your project ID.
tagYesThe overlay stream's id from Step 1.
retentionNoHow long to keep the config active, in seconds. Defaults to 30 days (2592000).
Body fieldRequiredDescription
renderTypeYesRender plugin. Currently only "html2img" is supported.
argsYesCommand-line arguments to html2img. At minimum, -u followed by the target URL.

html2img arguments

The args array is passed straight to the html2img renderer. -u (the target URL to capture) is required; the rest are optional:

FlagDefaultDescription
-u, --urlRequired. Target URL to capture.
-W, --width1920Viewport width.
-H, --height1080Viewport height.
-i, --interval800Capture interval (ms).
-c, --chromaChroma-key removal: auto, hex (#00B140), or RGB (0,177,64).
-D, --dom-watchoffCapture on DOM mutations (use with --selector).
--resizeResize output, e.g. 960x540 (or 960x0 to preserve aspect).
-A, --animationsoffEnable CSS animations/transitions (off by default for consistent captures).
POST {mediaAssetsHost}/mediaassets/api/v1/render/config?ns={projectId}&tag={overlayStreamId}&retention=15552000
Content-Type: application/json

{
"renderType": "html2img",
"args": ["-u", "https://example.com/my-overlay", "-W", "1280", "-H", "720", "-i", "500"]
}

Use GET (same URL) to read the current config and whether the render process is running (processRunning), and DELETE to remove it (stops the process if running; idempotent).

Step 4 — Play back the overlay

Play back the overlay stream — not the source — to see the composited result. Build its manifest URL from the overlay stream's id and vfManifestUrl:

https://{manifestHost}/manifest/api/v1/live/{overlayStreamId}.m3u8

where manifestHost is the host from the overlay stream's vfManifestUrl. Replace .m3u8 with .json for the JSON manifest. For video-foundation streams, always prefer vfIngress/vfManifestUrl over ingress/manifestUrl.

Putting it all together

  1. Create the streams — a program, a source stream, and an overlay stream (kind: "filtergraph", derivedFromId = source id) under the same program.
  2. Set the render config on the overlay stream (renderType: "html2img", args with at least -u <url>).
  3. Start streaming — build the source stream's RTMP URL (Step 2) with the appdata parameter pointing at the overlay, and start sending video. (Or generate a WHIP link for WebRTC ingest.)
  4. Play back the overlay stream's manifest to watch the composited output.

Program API reference

The stream operations above are part of the v2 Program Service API — see the Program API reference. The render config endpoints live on the Media Assets host ({mediaAssetsHost}/mediaassets/api/v1/render/config). Every call requires a Bearer service-account JWT; the project is taken from the token's projectID claim.

Related