This quickstart walks the streams modality end to end: the control plane creates a live input, mints a signed playback URL, and the data plane opens a browser viewer that yields CMAF chunks. Replace streams with any other modality (voice, robotics, games, data) — they all follow the same shape.

1. Install

npm install @clutchcall/sdk

2. Server-side: create a live input

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

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

const { input, streamKey } = await streams.liveInputs.create({
  name: "My First Stream",
});

// streamKey is returned ONCE — store it; the control-plane API only keeps the hash.
console.log({ inputId: input.external_input_id, streamKey });

// Mint a 1-hour signed playback URL to hand to the browser.
const { url } = await streams.liveInputs
  .get({ id: input.external_input_id })
  .then(i => i.signedPlaybackUrl({ ttlSeconds: 3600 }));
console.log("playback URL:", url);

3. Browser-side: open a viewer

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

const mediaSource = new MediaSource();
videoElement.src  = URL.createObjectURL(mediaSource);
await new Promise(r => mediaSource.addEventListener("sourceopen", r));

const buffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01F,mp4a.40.2"');

const viewer = await BroadcastViewer.open(playbackUrl, {
  onChunk: (init, chunk) => buffer.appendBuffer(chunk.data),
  onClose: reason => console.log("closed:", reason),
});

4. Push a broadcast (if you’re publishing too)

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

const pub = await BroadcastPublisher.open({
  inputId:   input.external_input_id,
  streamKey,
  codecs:    { video: "avc1.42E01F", audio: "opus" },
});

pub.write(fmp4Init);             // CMAF init segment first
pub.write(fmp4Segment);          // media segments
await pub.close("finished");

What just happened

  1. The control-plane API (tRPC) procedure created the live input and returned the cleartext stream key once. The control-plane API only keeps a hash.
  2. signedPlaybackUrl minted a relay token scoped to playback/<input_id> for an hour.
  3. BroadcastViewer.open dialled MoQT over WebTransport on relay.clutchcall.dev:443, presented the token, and started receiving the CMAF init + media segments live.
  4. BroadcastPublisher.open opened a publish track on publish/<input_id> and the relay fanned it to your viewer.

Other modalities

The other four follow the same construct → control-plane call → data-plane publish/subscribe shape:

Voice

Voice.calls.originate + Voice.audioBridge.attach

Robotics

Robotics.publishTelemetry + subscribeCommand

Games

Games.publishState + subscribeInputs

Data

Data.publish + subscribe with MQTT-style filters