# Streams — Cookbook

> Task-oriented streams snippets: create inputs, ingest via RTMP/SRT/WHIP/TUS2, mint signed playback, publish and view, rotate keys.

Short, copy-pasteable answers to "how do I X" for the streams modality. Each
assumes you've imported from `@clutchcall/sdk/streams` (TS) or
`clutchcall.streams` (Python) and constructed a control-plane `Streams`
client with `baseUrl`/`apiKey`/`orgId`.

## Create a live input

A live input is the destination an encoder or browser publishes into. Pick the
`ingest` kind your source speaks.

**TypeScript:**
```ts
const { input, streamKey } = await streams.liveInputs.create({
  name:   "Saturday Show",
  ingest: "rtmp",            // "rtmp" | "srt" | "whip" | "fmp4"
});
console.log(input.external_input_id, streamKey); // save streamKey NOW
```

**Python:**
```python
res = streams.live_inputs.create(name="Saturday Show", ingest="rtmp")
print(res.input.external_input_id, res.stream_key)  # save stream_key NOW
```

> **WARNING:**
> The cleartext stream key is returned only here. Persist it immediately.

## Build the RTMP / SRT publish endpoint

RTMP and SRT encoders ingest with the per-input stream key. The path segment is
the input's `external_input_id`.

```text
RTMP:  rtmp://ingest.clutchcall.dev/live/<external_input_id>?sk=<stream_key>
SRT:   srt://ingest.clutchcall.dev:<port>?streamid=<external_input_id>:<stream_key>
```

Paste the RTMP URL + key into OBS (Settings → Stream → Custom) or your hardware
encoder and go live.

## Go live from the browser with WHIP

WHIP is the WebRTC-HTTP ingest path — capture the camera and POST an SDP offer.
Create the input with `ingest: "whip"`, then publish with a standard WHIP client.

```ts
const { input, streamKey } = await streams.liveInputs.create({
  name: "Webcam", ingest: "whip",
});
const cam = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
const pc  = new RTCPeerConnection();
cam.getTracks().forEach((t) => pc.addTrack(t, cam));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

const answer = await fetch(
  `https://ingest.clutchcall.dev/whip/${input.external_input_id}`,
  { method: "POST", headers: {
      "content-type": "application/sdp",
      "authorization": `Bearer ${streamKey}`,
    }, body: offer.sdp },
).then((r) => r.text());
await pc.setRemoteDescription({ type: "answer", sdp: answer });
```

## Publish a browser MediaStream over MoQT

The SDK's `BroadcastPublisher` is a WHIP-style alternative that fragments a
`MediaStream` to CMAF and pushes it over MoQT directly.

```ts
const dataPlane = new Streams({ relayUrl: "https://relay.clutchcall.dev" });
const pub = dataPlane.publisher({ timesliceMs: 1000 });
const screen = await navigator.mediaDevices.getDisplayMedia({ video: true });

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

## Upload a VOD file with TUS2 (resumable)

VOD uploads use TUS2 so large files survive network drops. Create the upload,
PATCH chunks at the confirmed offset, and the engine packages to CMAF on
completion.

```ts
// 1) create the resumable upload, get a location to PATCH into
const { uploadUrl } = await streams.assets.createUpload({ name: "Keynote.mp4" });

// 2) send chunks; on reconnect, HEAD to learn the offset, then resume
let offset = 0;
const file = await fileHandle.arrayBuffer();
while (offset < file.byteLength) {
  const end = Math.min(offset + CHUNK, file.byteLength);
  await fetch(uploadUrl, {
    method: "PATCH",
    headers: {
      "tus-resumable": "2.0.0",
      "upload-offset": String(offset),
      "content-type": "application/offset+octet-stream",
    },
    body: file.slice(offset, end),
  });
  offset = end;
}
```

> **NOTE:**
> `streams.assets.*` is **Preview** — until the typed helper ships, drive the
> TUS2 endpoint directly with the same bearer key.

## Mint a signed playback URL

The playback URL carries a short-lived JWT. The relay verifies it — the URL is
the auth.

**TypeScript:**
```ts
const input = await streams.liveInputs.get({ id: "li_xyz" });
const { url, expiresAt } = await input.signedPlaybackUrl({ ttlSeconds: 3600 });
```

**Python:**
```python
inp = streams.live_inputs.get(id="li_xyz")
ticket = inp.signed_playback_url(ttl_seconds=3600)
```

## Play a live stream into a `<video>`

`lv_*` ids play live over MoQT; `pb_*` ids play VOD over HTTPS — the same viewer
handles both.

```ts
const dataPlane = new Streams({ apiBase: "", token: jwt });
const viewer = dataPlane.viewer({ onError: (e) => console.error(e) });
viewer.attach(document.querySelector("video")!);
await viewer.play("lv_abc123");
```

## Consume playback chunks headless (Python)

For server-side recording or re-muxing, take chunks via callback instead of a
`<video>` element.

```python
ticket = inp.signed_playback_url(ttl_seconds=600)
out = open("capture.mp4", "wb")
viewer = BroadcastViewer.open(
    ticket.url,
    on_chunk=lambda is_init, c: out.write(c.data),
    on_close=lambda reason, detail: out.close(),
)
```

## Read the catalog before playing

The catalog enumerates the available renditions so you can pick or label them.

```ts
import { parseCatalog, videoTrack, audioTrack } from "@clutchcall/sdk/streams";

const cat = parseCatalog(await fetch(catalogJsonUrl).then((r) => r.json()));
console.log("video:", videoTrack(cat)?.name, videoTrack(cat)?.bitrate);
console.log("audio:", audioTrack(cat)?.codec);
```

## Rotate a stream key

Rotation invalidates the old key immediately and returns a fresh cleartext one.

**TypeScript:**
```ts
const { streamKey } = await input.rotateStreamKey();
```

**Python:**
```python
res = inp.rotate_stream_key()
new_key = res.stream_key
```

## Manage signing keys

Create a signing key (Ed25519 by default), then deactivate it to revoke every
token minted from it.

```ts
const key = await streams.signingKeys.create({ alg: "Ed25519" });
console.log(key.id, key.publicKeyPem);
// revoke later
await key.deactivate();
```

## Re-mint before a long viewer expires

Playback tokens are server-clamped (≤ 24 h). For a long session, re-mint and
re-open ahead of `expiresAt`.

```ts
async function keepAlive(input, viewer, videoEl) {
  let { url, expiresAt } = await input.signedPlaybackUrl({ ttlSeconds: 3600 });
  setInterval(async () => {
    if (Date.now() / 1000 > expiresAt - 60) {
      ({ url, expiresAt } = await input.signedPlaybackUrl({ ttlSeconds: 3600 }));
      viewer.stop();
      await viewer.attach(videoEl).play(input.external_input_id);
    }
  }, 30_000);
}
```

## List live inputs

Page through the org's inputs to render a dashboard.

```ts
const inputs = await streams.liveInputs.list({ page: 1, perPage: 50 });
for (const i of inputs) console.log(i.external_input_id, i.status, i.ingest);
```

## Pull viewer analytics

Overview KPIs, viewer time-series, and glass-to-glass latency are exposed by the
control-plane analytics surface.

```ts
// Preview: streams.analytics.* — reach the control-plane analytics route
// directly with the bearer key until the typed helper ships in your SDK version.
```

> **NOTE:**
> `streams.analytics.*` is **Preview**.
