# Streams — SDK Methods

> The typed streams surface: control-plane Streams client, BroadcastPublisher, BroadcastViewer, signing keys, signed playback URLs.

The streams modality exposes two surfaces from the same subpath import:

- **Control plane** — a `Streams` client over HTTPS that manages live inputs,
  signing keys, and (preview) assets/API-keys/analytics.
- **Data plane** — `BroadcastPublisher` and `BroadcastViewer`, which move CMAF
  segments over the persistent MoQT connection.

## Import

**TypeScript:**
```ts
import {
  Streams,
  BroadcastPublisher,
  BroadcastViewer,
} from "@clutchcall/sdk/streams";
```

**Python:**
```python
from clutchcall.streams import (
    Streams,
    BroadcastPublisher,
    BroadcastViewer,
)
```

## Control plane — `Streams`

The control-plane client is stateless HTTPS. Every call is one query or
mutation against the control-plane API; the persistent QUIC connection lives in
the publisher/viewer, not here.

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

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

<ParamField path="baseUrl" type="string" required>
  Control-plane origin (no trailing slash needed). Python: `base_url`.
</ParamField>
<ParamField path="apiKey" type="string" required>
  API key with `streams:*` scopes, sent as `Authorization: Bearer`. Python:
  `api_key`.
</ParamField>
<ParamField path="orgId" type="string">
  Default org/tenant id for every tenant-scoped call. Required for
  `liveInputs.*` and `signingKeys.*`. Python: `org_id`.
</ParamField>
<ParamField path="fetch" type="typeof fetch">
  (TS only) Override `fetch` for test fakes or custom transports. Defaults to
  `globalThis.fetch`.
</ParamField>

The client hands out scoped helpers — you never call the transport directly:

| Helper | TS | Python |
| ------ | -- | ------ |
| Live inputs | `streams.liveInputs` | `streams.live_inputs` |
| Signing keys | `streams.signingKeys` | `streams.signing_keys` |

### `liveInputs`

<ParamField path="create({ name, ingest? })" type="Promise<LiveInputWithSecret>">
  Create a live input. `ingest` is one of `fmp4` (default) | `whip` | `rtmp` |
  `srt`. Returns `{ input, streamKey }` — the cleartext `streamKey` is returned
  **only here** (and on `rotateStreamKey`). Python: `create(name=, ingest=)`
  returning `LiveInputWithSecret(input, stream_key)`.
</ParamField>
<ParamField path="get({ id })" type="Promise<LiveInput>">
  Fetch a live input by id. Python: `get(id=)`.
</ParamField>
<ParamField path="list({ page?, perPage? })" type="Promise<LiveInput[]>">
  List inputs (default `page=1`, `perPage=50`). Python: `list(page=, per_page=)`.
</ParamField>

A `LiveInput` handle carries snapshot fields and bound methods:

<ParamField path="input.external_input_id" type="string">
  The path segment used in publish/playback URLs (the `<input_id>`).
</ParamField>
<ParamField path="input.status" type="'idle' | 'live' | 'errored'">
  Current ingest state.
</ParamField>
<ParamField path="input.signedPlaybackUrl({ ttlSeconds? })" type="Promise<SignedPlaybackUrl>">
  Mint a signed playback URL (`?tok=<jwt>` attached). `ttlSeconds` is
  server-clamped to 30 s – 24 h (default 3600). Returns `{ url, kid, alg,
  expiresAt }`. Python: `signed_playback_url(ttl_seconds=)`.
</ParamField>
<ParamField path="input.rotateStreamKey()" type="Promise<LiveInputWithSecret>">
  Rotate the stream key. Returns a fresh cleartext `streamKey` (captured once).
  Python: `rotate_stream_key()`.
</ParamField>

### `signingKeys`

The org's playback signing keys. Playback JWTs are signed with one of these; the
`kid` header on each token names the key.

<ParamField path="create({ alg?, use? })" type="Promise<SigningKey>">
  Create a signing key. `alg` is `Ed25519` (default) | `RS256`; `use` is
  `playback`. Returns a `SigningKey` with `{ id, alg, publicKeyPem, status }`.
  Python: `create(alg=, use=)`.
</ParamField>
<ParamField path="list()" type="Promise<SigningKey[]>">
  List the org's signing keys.
</ParamField>
<ParamField path="key.deactivate()" type="Promise<void>">
  Deactivate a key. Existing tokens minted from it stop verifying.
</ParamField>

<Accordion title="Preview: assets, apiKeys, and analytics">
The control-plane API also exposes asset management, API-key administration, and
viewer analytics (overview KPIs, viewer time-series, glass-to-glass latency
histograms, edge-POP health). These ride the same `streams.*` route shape as the
typed helpers above. Dedicated typed helpers — `streams.assets.*`,
`streams.apiKeys.*`, `streams.analytics.*` — are rolling out; until they land in
your SDK version, reach them through the control-plane API directly with the same
`CLUTCHCALL_CREDENTIALS` bearer key. Treat these surfaces as **Preview**.
</Accordion>

## Data plane — `BroadcastPublisher`

Push a broadcast into a live input over MoQT, authorized by the per-input stream
key. The TypeScript publisher fragments a browser `MediaStream` to CMAF; the
Python publisher takes raw CMAF chunks you supply (server-side packaging).

  <Tab title="TypeScript (browser MediaStream)">
```ts
// Get a data-plane handle from the control-plane import path. The data-plane
// Streams takes { apiBase, relayUrl, token, serverCertificateHash }.
const dataPlane = new Streams({ relayUrl: "https://relay.clutchcall.dev" });
const pub = dataPlane.publisher({ timesliceMs: 1000 });

await pub.publish(
  input.external_input_id,   // <input_id>
  streamKey,                 // cleartext stream key
  cameraStream,              // a MediaStream from getUserMedia / getDisplayMedia
  `stream/org_abc/${input.external_input_id}`, // MoQ namespace
);
// …later
pub.stop();
```
  </Tab>
  <Tab title="Python (raw CMAF chunks)">
```python
pub = BroadcastPublisher.open(
    input_id=inp.external_input_id,
    stream_key=stream_key,
    codecs=PublisherCodecs(video="avc1.42E01F", audio="opus"),
)
pub.write(fmp4_init)        # CMAF init segment FIRST
pub.write(fmp4_segment)     # then media segments
pub.close()                 # default reason "closed_by_caller"
```
  </Tab>

### TypeScript surface

<ParamField path="dataPlane.publisher(opts?)" type="BroadcastPublisher">
  Construct a publisher. `opts`: `mimeType?` (MediaRecorder mime, default
  `video/mp4; codecs="avc1.42E01E,mp4a.40.2"`), `timesliceMs?` (fragment cadence,
  default 1000), `serverCertificateHash?`, `webTransport?`, `onError?`.
</ParamField>
<ParamField path="publish(input, streamKey, media, namespace)" type="Promise<void>">
  Connect to `/publish/<input>?sk=<streamKey>`, announce the track, and stream
  CMAF fragments from `media` (a `MediaStream`). `namespace` is the MoQ namespace
  the relay expects (`stream/<org>/<input>`).
</ParamField>
<ParamField path="stop()" type="void">
  Stop recording, close the track, and tear down the session.
</ParamField>

### Python surface

<ParamField path="BroadcastPublisher.open(...)" type="BroadcastPublisher">
  Open a publisher. Args: `input_id`, `stream_key`, `relay_host?`, `codecs?`
  (`PublisherCodecs(video=, audio=)`), `on_close?`.
</ParamField>
<ParamField path="write(chunk, timestamp_us?)" type="None">
  Push one CMAF chunk. The **first** chunk is the init segment (priority 0, opens
  a group); subsequent chunks are media (priority 1). Pass `timestamp_us` to
  override the monotonic timestamp (replay/tape-sync).
</ParamField>
<ParamField path="close(reason?)" type="None">
  Close the publisher. `reason` ∈ `closed_by_caller` (default) | `auth_failed` |
  `network` | `finished`.
</ParamField>

## Data plane — `BroadcastViewer`

Play a stream from a playback id (TS, browser `<video>`) or from a signed
playback URL (Python, chunk callback).

  <Tab title="TypeScript (into a <video>)">
```ts
const dataPlane = new Streams({ apiBase: "", token: jwt });
const viewer = dataPlane.viewer({
  onStarted: () => console.log("playing"),
  onError:   (e) => console.error(e),
});
viewer.attach(document.querySelector("video")!);
await viewer.play("lv_abc123");   // lv_ → live over MoQT, pb_ → VOD over HTTPS
// …later
viewer.stop();
```
  </Tab>
  <Tab title="Python (chunk callback)">
```python
ticket = inp.signed_playback_url(ttl_seconds=3600)
viewer = BroadcastViewer.open(
    ticket.url,
    on_chunk=lambda is_init, chunk: pipe.write(chunk.data),
    on_close=lambda reason, detail: print("closed", reason, detail),
)
# …later
viewer.close()
```
  </Tab>

### TypeScript surface

<ParamField path="dataPlane.viewer(extra?)" type="BroadcastViewer">
  Construct a viewer. Data-plane `Streams` opts: `apiBase?` (resolve/catalog
  base), `relayUrl?`, `token?` (signed JWT, carried as `?tok=`),
  `serverCertificateHash?`. Per-viewer `extra`: `onStarted?`, `onError?`,
  `webTransport?`, `fetchImpl?`.
</ParamField>
<ParamField path="attach(video)" type="this">
  Attach the `<video>` element the stream renders into. Chainable.
</ParamField>
<ParamField path="play(playbackId)" type="Promise<void>">
  Resolve and play. `lv_*` plays **live** over MoQT (subscribes the `.catalog`
  track in-band, then media tracks); `pb_*` plays **VOD** over HTTPS from the
  catalog.
</ParamField>
<ParamField path="stop()" type="void">
  Tear down every track subscription and end the `MediaSource`.
</ParamField>

### Python surface

<ParamField path="BroadcastViewer.open(url, on_chunk, on_close?)" type="BroadcastViewer">
  Connect to a signed `moq://…/playback/<input>?tok=<jwt>` URL and forward
  chunks. `on_chunk(is_init, chunk)` fires per segment; the first has
  `is_init=True`. `chunk` is a `BroadcastChunk(data, timestamp_us, priority,
  is_init)`.
</ParamField>
<ParamField path="close()" type="None">
  Close the session; fires `on_close("closed_by_caller", None)`.
</ParamField>

## Events & close reasons

Both surfaces report terminal state through callbacks rather than an emitter:

<ResponseField name="onStarted / on_started" type="callback">
  (Viewer) Playback has begun — first segment buffered.
</ResponseField>
<ResponseField name="onError / on_error" type="callback">
  Connection or decode error. If no handler is set, errors throw from `play()` /
  `publish()`.
</ResponseField>
<ResponseField name="on_close(reason, detail)" type="callback (Python)">
  Terminal close. `reason` ∈ `complete` | `auth_failed` | `network` |
  `closed_by_caller`. An expired/invalid playback JWT yields `auth_failed`.
</ResponseField>

> **NOTE:**
> **Other languages.** The streams surface mirrors across the polyglot SDKs — the
> control-plane `Streams` client, `BroadcastPublisher`, and `BroadcastViewer` keep
> the same method names (camelCase in TS, snake_case in Python and the others). The
> TypeScript and Python signatures above are the reference shapes.

## Types reference

<AccordionGroup>
  <Accordion title="LiveInputData / IngestKind / LiveInputStatus">
    ```ts
    type IngestKind      = "fmp4" | "whip" | "rtmp" | "srt";
    type LiveInputStatus = "idle" | "live" | "errored";
    interface LiveInputData {
      id: string;
      external_input_id: string;   // path segment in publish/playback URLs
      name: string;
      status: LiveInputStatus;
      ingest: IngestKind;
      createdAt: string;           // ISO-8601
    }
    ```
  </Accordion>
  <Accordion title="SignedPlaybackUrl / SigningKeyData">
    ```ts
    interface SignedPlaybackUrl {
      url: string;        // …/playback/<input_id>?tok=<jwt>
      kid: string;        // signing key id (JWT kid header)
      alg: string;        // "Ed25519" | "RS256"
      expiresAt: number;  // unix epoch seconds
    }
    interface SigningKeyData {
      id: string;         // the JWT kid
      alg: "Ed25519" | "RS256";
      use: "playback";
      publicKeyPem: string;
      status: "active" | "inactive";
      createdAt: string;
    }
    ```
  </Accordion>
  <Accordion title="Catalog / CatalogTrack">
    ```ts
    type TrackKind = "video" | "audio";
    interface CatalogTrack {
      name: string;       // MoQ track name, e.g. "video/720p", "audio"
      kind: TrackKind;
      codec: string;      // RFC 6381, e.g. "avc1.42E01F" / "mp4a.40.2"
      mimeType?: string;
      initData?: string;  // base64 CMAF init (VOD); live sends init as 1st object
      width?: number; height?: number; bitrate?: number;
      sampleRate?: number; channels?: number;
    }
    interface Catalog { version: number; live?: boolean; tracks: CatalogTrack[]; }
    ```
    Helpers: `parseCatalog(json)`, `mseType(track)`, `videoTrack(cat)`,
    `audioTrack(cat)`.
  </Accordion>
</AccordionGroup>
