# 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_connect/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://streams.firstbyte.fast",
  apiKey:  process.env.CLUTCHCALL_CREDENTIALS!,
  orgId:   "org_abc",
});
```

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

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

The client gives you 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`

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

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

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

### `signingKeys`

The org's playback signing keys. One of these keys signs each playback JWT.
The `kid` header on each token names the key.

- **`create({ alg?, use? })`** (`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=)`.
- **`list()`** (`Promise<SigningKey[]>`) — List the org's signing keys.
- **`key.deactivate()`** (`Promise<void>`) — Deactivate a key. Existing tokens minted from it stop verifying.

**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 use 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, call the control-plane API directly
with the same `CLUTCHCALL_CREDENTIALS` bearer key. Treat these surfaces as
**Preview**.

## Data plane — `BroadcastPublisher`

Send a broadcast into a live input over MoQT. The per-input stream key
authorizes it. The TypeScript publisher fragments a browser `MediaStream` to
CMAF. The Python publisher takes raw CMAF chunks that you supply
(server-side packaging).

  
```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.firstbyte.fast" });
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();
```
  
  
```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"
```
  

### TypeScript surface

- **`dataPlane.publisher(opts?)`** (`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?`.
- **`publish(input, streamKey, media, namespace)`** (`Promise<void>`) — Connect to `/publish/<input>?sk=<streamKey>`, announce the track, and stream   CMAF fragments from `media` (a `MediaStream`). `namespace` is the MoQ   namespace that the relay expects (`stream/<org>/<input>`).
- **`stop()`** (`void`) — Stop recording, close the track, and close the session.

### Python surface

- **`BroadcastPublisher.open(...)`** (`BroadcastPublisher`) — Open a publisher. Args: `input_id`, `stream_key`, `relay_host?`, `codecs?`   (`PublisherCodecs(video=, audio=)`), `on_close?`.
- **`write(chunk, timestamp_us?)`** (`None`) — Send 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).
- **`close(reason?)`** (`None`) — Close the publisher. `reason` ∈ `closed_by_caller` (default) | `auth_failed` |   `network` | `finished`.

## Data plane — `BroadcastViewer`

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

  )">
```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();
```
  
  
```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()
```
  

### TypeScript surface

- **`dataPlane.viewer(extra?)`** (`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?`.
- **`attach(video)`** (`this`) — Attach the `<video>` element the stream renders into. Chainable.
- **`play(playbackId)`** (`Promise<void>`) — Resolve and play. `lv_*` plays **live** over MoQT. It subscribes to the   `.catalog` track in-band, then to media tracks. `pb_*` plays **VOD** over   HTTPS from the catalog.
- **`stop()`** (`void`) — Close every track subscription and end the `MediaSource`.

### Python surface

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

## Events & close reasons

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

- **`onStarted / on_started`** (`callback`) — (Viewer) Playback started. The first segment is buffered.
- **`onError / on_error`** (`callback`) — Connection or decode error. If you set no handler, errors throw from   `play()` / `publish()`.
- **`on_close(reason, detail)`** (`callback (Python)`) — Terminal close. `reason` ∈ `complete` | `auth_failed` | `network` |   `closed_by_caller`. An expired/invalid playback JWT yields `auth_failed`.

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

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

  **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;
    }
    ```

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