# Cookbook

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

This page gives short answers that you can copy for common streams tasks.
Each example assumes two steps: you imported from `clutchcall_connect/streams`
(TS) or `clutchcall.streams` (Python), and you constructed a
control-plane `Streams` client with `baseUrl`/`apiKey`/`orgId`.

## Create a live input

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

**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 API returns the cleartext stream key only here. Store 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.firstbyte.fast/live/<external_input_id>?sk=<stream_key>
SRT:   srt://ingest.firstbyte.fast:<port>?streamid=<external_input_id>:<stream_key>
```

Paste the RTMP URL + key into OBS (Settings → Stream → Custom) or into your
hardware encoder. Then 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.firstbyte.fast/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. It fragments a
`MediaStream` to CMAF and sends it over MoQT directly.

```ts
const dataPlane = new Streams({ relayUrl: "https://relay.firstbyte.fast" });
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. On completion, the engine
packages the file to CMAF.

```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, call
> the TUS2 endpoint directly with the same bearer key.

## Mint a signed playback URL

The playback URL carries a short-lived JWT. The relay checks 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_connect/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. It returns a fresh cleartext
key.

**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). Deactivate it later 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

The server clamps playback tokens (≤ 24 h). For a long session, re-mint and
re-open before `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

The control-plane analytics surface exposes overview KPIs, viewer
time-series, and glass-to-glass latency.

```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**.
