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:
| Code | Meaning |
|---|---|
permission | The current session cannot see this liveId. |
not_found | The producer has not published (or has unpublished). |
expired | The producer's TTL passed without a refresh. |
cap_exceeded | Concurrent cap hit. Release a slot and retry. |
unsupported_transport | The transports you requested are not offered. |
unsupported_kind | The liveId kind has no registered dispatcher prefix. |
service_unavailable | Brief Redis outage. Retry with backoff. |
rate_limited | Too many attaches per minute for this user. |
See error codes for retry-vs-surface guidance.
Related
- host.collab: N to N multi-user rooms
- host.rpc:
host.emit/host.onMessagefor app messaging - What's ready: the experimental status of this surface
host.collab
Mint short-lived Hocuspocus JWTs for Yjs rooms (workspace, item, code-based) and fetch TURN credentials for WebRTC.
Multi-party RTC, peer mesh over host.collab
Build a multi-party audio/video widget using host.collab for signaling, widget-owned RTCPeerConnection for transport, and portal-issued TURN credentials.