Robutler

host.live

Experimental. host.live (realtime audio and video primitives) is early. The API shape below is stable enough to build against, but treat it as a preview. See What's ready.

host.live is the App SDK's primitive for 1 to N broadcast streams: one producer publishes, many consumers attach. The host enforces concurrent caps and TTLs on your behalf and hands you a transport descriptor; the wire itself is your choice (iframe-url, portal-relay, or webrtc).

For N to N multi-user rooms use host.collab. For point-to-point app messaging use host.emit / host.onMessage.

API

interface LiveNamespace {
  publish(meta: LivePublishMeta): Promise<{ ok: true }>;        // producer: announce
  unpublish(liveId: string): Promise<{ ok: true }>;             // producer: end
  attach(liveId: string, opts?: LiveAttachOpts): Promise<LiveAttachHandle>; // consumer: claim a slot
  release(attachId: string): Promise<{ ok: true }>;             // consumer: explicit release
  heartbeat(attachId: string): Promise<{ ok: true; expiresAt: string }>;    // consumer: refresh TTL
  url: { resolve(liveId: string): Promise<LiveResolveResult> }; // consumer: resolve current transport
}

type LiveTransport =
  | { kind: 'iframe-url'; url: string; allow?: string }
  | { kind: 'portal-relay' }
  | { kind: 'webrtc'; signalingChannelId: string };

interface LivePublishMeta {
  liveId: string;
  transports: LiveTransport[];
  widgetId?: string;
  expiresAt?: string;          // ISO-8601
  meta?: Record<string, unknown>;
}

interface LiveAttachHandle {
  attachId: string;
  transport: LiveTransport;
  widgetId?: string;
  expiresAt: string;           // ISO-8601
  release: () => Promise<void>; // idempotent; SDK also auto-releases on pagehide
}

interface LiveAttachOpts {
  transports?: Array<LiveTransport['kind']>; // preferred order; default iframe-url > portal-relay > webrtc
  modalities?: { audioIn: boolean; audioOut: boolean }; // realtime-voice direction; ignored by non-voice producers
}

Producer side

await host.ready();

const liveId = `content:${myContentId}`;
await host.live.publish({
  liveId,
  transports: [
    { kind: 'iframe-url', url: `https://example.com/stream/${myContentId}` },
    { kind: 'portal-relay' },
  ],
  widgetId: 'browser-stream-viewer',                       // optional consumer view
  expiresAt: new Date(Date.now() + 60_000).toISOString(),  // optional explicit TTL
});

// When the stream ends:
await host.live.unpublish(liveId);

Refresh the publish entry roughly every 25 seconds while the stream is active (re-publish, or set an expiresAt and re-publish on schedule). Stale entries auto-expire after about 30 seconds.

Consumer side

await host.ready();

const handle = await host.live.attach('content:abc-123');
console.log(handle.transport); // { kind: 'iframe-url', url: '...' }

// Open the wire yourself, or render via the standard browser-stream-viewer app.

// Keep the slot alive for long sessions:
const hb = setInterval(() => host.live.heartbeat(handle.attachId).catch(() => {}), 20_000);

// Release when done. The SDK also auto-releases on pagehide via navigator.sendBeacon.
clearInterval(hb);
await handle.release();

When attach resolves, you hold the slot until you release() or the TTL expires. The host enforces concurrent caps per (scope, kind). Typical caps are 4 simultaneous browser sessions and 8 app attaches per user.

Transport selection

Pass opts.transports to bias the negotiation; the bridge picks the highest-capability entry the producer can serve and the host supports. When omitted, the order is iframe-url > portal-relay > webrtc. For realtime voice, opts.modalities sets per-session media direction (omit for full duplex).

Re-resolving a transport

host.live.url.resolve(liveId) returns the producer's current transport without claiming a slot, useful for a preview chip:

const res = await host.live.url.resolve('content:abc-123');
if (res.ok) renderPreview(res.transport);

Errors

attach may reject with a WidgetError whose .code is one of:

CodeMeaning
permissionThe current session cannot see this liveId.
not_foundThe producer has not published (or has unpublished).
expiredThe producer's TTL passed without a refresh.
cap_exceededConcurrent cap hit. Release a slot and retry.
unsupported_transportThe transports you requested are not offered.
unsupported_kindThe liveId kind has no registered dispatcher prefix.
service_unavailableBrief Redis outage. Retry with backoff.
rate_limitedToo many attaches per minute for this user.

See error codes for retry-vs-surface guidance.

  • host.collab: N to N multi-user rooms
  • host.rpc: host.emit / host.onMessage for app messaging
  • What's ready: the experimental status of this surface

On this page