Longer, end-to-end walkthroughs that combine the control plane and data plane into realistic flows. Each assumes a control-plane Streams client constructed with baseUrl/apiKey/orgId.

Recipe 1 — Go live from the browser, watch with a signed URL

A complete “create input → publish a webcam → watch” loop, entirely in the browser, using the SDK’s MoQT publisher and viewer.
1

Create a live input

import { Streams } from "@clutchcall/sdk/streams";

const streams = new Streams({
  baseUrl: "https://app.clutchcall.dev",
  apiKey:  process.env.CLUTCHCALL_CREDENTIALS!,
  orgId:   "org_abc",
});

const { input, streamKey } = await streams.liveInputs.create({
  name: "Live Demo", ingest: "fmp4",
});
// Persist streamKey now — it is never returned again.
2

Publish a webcam over MoQT

const dataPlane = new Streams({ relayUrl: "https://relay.clutchcall.dev" });
const pub  = dataPlane.publisher({ timesliceMs: 1000 });
const cam  = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });

await pub.publish(
  input.external_input_id,
  streamKey,
  cam,
  `stream/org_abc/${input.external_input_id}`,
);
3

Mint a signed playback URL for viewers

const { url } = await input.signedPlaybackUrl({ ttlSeconds: 3600 });
// Hand `url` (or just the input id + a token) to each viewer.
4

Play it into a <video>

const viewerStreams = new Streams({ apiBase: "", relayUrl: "https://relay.clutchcall.dev" });
const viewer = viewerStreams.viewer({
  onStarted: () => console.log("live"),
  onError:   (e) => console.error("playback error", e),
});
viewer.attach(document.querySelector("video")!);
await viewer.play(input.external_input_id);   // lv_… → live over MoQT
5

Tear down

pub.stop();
viewer.stop();
On a warm same-region path this loop is sub-second glass-to-glass. Across an internet RTT, latency is roughly one RTT plus one MoQT group time — so keep the publisher’s timesliceMs short (250–500 ms) for tighter latency at the cost of slightly higher overhead.

Recipe 2 — RTMP event to thousands of viewers

A hardware/OBS encoder contributes over RTMP; the engine transcodes and fans the stream out to every viewer over MoQT. The control plane mints per-viewer signed URLs.
1

Provision the input and rotate a fresh key

const { input } = await streams.liveInputs.create({
  name: "Keynote 2026", ingest: "rtmp",
});
const { streamKey } = await input.rotateStreamKey();  // fresh key for the event
2

Point the encoder at the RTMP endpoint

Server:     rtmp://ingest.clutchcall.dev/live/<external_input_id>
Stream key: <streamKey>
The engine ingests, transcodes to multiple ABR renditions, and publishes the catalog in-band on the .catalog track.
3

Issue a signed URL per viewer (server side)

// In your app server, behind your own auth:
async function playbackForViewer(): Promise<string> {
  const inp = await streams.liveInputs.get({ id: "li_keynote" });
  const { url } = await inp.signedPlaybackUrl({ ttlSeconds: 900 });
  return url;   // 15-minute token; client re-fetches before expiry
}
4

Each browser plays the URL

const dataPlane = new Streams({ apiBase: "", token: tokenFromUrl });
const viewer = dataPlane.viewer();
viewer.attach(videoEl);
await viewer.play("lv_keynote");
Late joiners start at the most recent group boundary (keyframe); the relay drops by priority under loss rather than blocking the stream.
Because the relay mesh fans out each group, the encoder uploads once no matter how many viewers attach — fan-out cost lives at the edge POPs, not the contribution link.

Recipe 3 — TUS2 upload to VOD playback

Upload a recorded file resumably, let the engine package it to CMAF, then play the resulting asset from the same viewer the live path uses.
1

Start a resumable upload

// Preview surface: streams.assets.* — until the typed helper ships, drive
// the TUS2 endpoint with the same bearer key.
const { uploadUrl, assetId } = await streams.assets.createUpload({
  name: "Session-Recording.mp4",
});
2

Upload in resumable chunks

async function tusUpload(uploadUrl: string, bytes: Uint8Array, chunk = 8 << 20) {
  // On reconnect, HEAD uploadUrl to read Upload-Offset and resume there.
  let offset = Number(
    (await fetch(uploadUrl, { method: "HEAD", headers: { "tus-resumable": "2.0.0" } }))
      .headers.get("upload-offset") ?? 0,
  );
  while (offset < bytes.byteLength) {
    const end = Math.min(offset + chunk, bytes.byteLength);
    const res = await fetch(uploadUrl, {
      method: "PATCH",
      headers: {
        "tus-resumable": "2.0.0",
        "upload-offset": String(offset),
        "content-type": "application/offset+octet-stream",
      },
      body: bytes.slice(offset, end),
    });
    offset = Number(res.headers.get("upload-offset") ?? end);
  }
}
3

Wait for packaging, then play the asset

On completion the engine packages the file to CMAF/fMP4 and assigns a playback id (pb_…). The same viewer plays VOD over HTTPS from the catalog:
const dataPlane = new Streams({ apiBase: "" });
const viewer = dataPlane.viewer();
viewer.attach(videoEl);
await viewer.play("pb_def456");   // pb_… → VOD: catalog.json + CMAF over HTTPS
VOD reuses the live stream/<org>/… namespace and the same catalog format, so a single player component handles both live (lv_…) and on-demand (pb_…) ids — the only difference is the delivery framing (MoQT vs HTTPS), which the viewer picks by id prefix.

Recipe 4 — Headless capture of a live stream (Python)

Record a live broadcast to disk server-side, with signing-key rotation handled ahead of token expiry.
import os, time
from clutchcall.streams import Streams, BroadcastViewer

streams = Streams(
    base_url="https://app.clutchcall.dev",
    api_key=os.environ["CLUTCHCALL_CREDENTIALS"],
    org_id="org_abc",
)

inp    = streams.live_inputs.get(id="li_xyz")
ticket = inp.signed_playback_url(ttl_seconds=3600)

out = open("broadcast.mp4", "wb")

def on_chunk(is_init, chunk):
    # first chunk is the CMAF init segment; the rest are media segments
    out.write(chunk.data)

def on_close(reason, detail):
    out.close()
    print("capture closed:", reason, detail)

viewer = BroadcastViewer.open(ticket.url, on_chunk=on_chunk, on_close=on_close)

# Run until the broadcast ends or you stop it.
try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    viewer.close()
A bad or expired playback JWT closes the session with auth_failed (surfaced through on_close). For captures longer than the token TTL, re-mint with inp.signed_playback_url(...) and re-open the viewer before expires_at.