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 planeBroadcastPublisher and BroadcastViewer, which move CMAF segments over the persistent MoQT connection.

Import

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

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.
const streams = new Streams({
  baseUrl: "https://app.clutchcall.dev",
  apiKey:  process.env.CLUTCHCALL_CREDENTIALS!,
  orgId:   "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 hands out scoped helpers — you never call the transport directly:
HelperTSPython
Live inputsstreams.liveInputsstreams.live_inputs
Signing keysstreams.signingKeysstreams.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 cleartext streamKey is returned 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). ttlSeconds is server-clamped 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 (captured once). Python: rotate_stream_key().

signingKeys

The org’s playback signing keys. Playback JWTs are signed with one of these; 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.
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.

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).
// 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();

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 the relay expects (stream/<org>/<input>).
stop()
void
Stop recording, close the track, and tear down 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
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).
close(reason?)
None
Close the publisher. reasonclosed_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).
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();

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 (subscribes the .catalog track in-band, then media tracks); pb_* plays VOD over HTTPS from the catalog.
stop()
void
Tear down 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 has is_init=True. chunk is a BroadcastChunk(data, timestamp_us, priority, is_init).
close()
None
Close the session; 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 has begun — first segment buffered.
onError / on_error
callback
Connection or decode error. If no handler is set, errors throw from play() / publish().
on_close(reason, detail)
callback (Python)
Terminal close. reasoncomplete | auth_failed | network | closed_by_caller. An expired/invalid playback JWT yields auth_failed.
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

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
}
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;
}
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).