Broadcast API
Read the public surface and lifecycle rules.
Publish one live audio/video feed to many viewers without building a peer mesh
A normal OpenRTC media connection is a two-way call between peers. A broadcast is different: each host uploads one copy of each video quality to a managed distribution relay, and many viewers read from that relay. Viewers never join the host's peer mesh and do not receive the host's IP address, Iroh ticket, device ID, or endpoint ID.
The browser package installs the broadcast runtime and reuses OpenRTC's normal
WebCodecs/MediaStream edge. openrtc/native exposes the same API as a thin IPC
facade: grant verification, provider credentials, publication signing, media
queues, and lifecycle stay in the Tauri Rust host rather than a second WASM
runtime. broadcasts.open() still fails closed unless your developer app and
native host are admitted to the managed preview control plane and relay. It
never falls back to a viewer mesh.
openrtc/external-broadcast lets an application backend issue signed,
device-bound grants and choose an SFU control endpoint or MoQ relay. The
browser still uses OpenRTC's Rust/WASM grant verification, publication
signing, session generations, media framing, and carrier adapters. This
entrypoint does not enable OpenRTC-managed allocation or its billing and
usage enforcement. The application owns issuer-key custody, grant admission,
relay access, audience limits, provider spending, and operational monitoring.
The application passes an exact HTTPS serviceOrigin and a stable key
namespace to createExternalBroadcastClient. Its resolve(grant, bindingPublicJwk, publicationVerifyingKey) callback calls the application's
trusted access endpoint and returns the issuer's 32-byte public key plus
short-lived relay access. The SFU control URL must remain on that trusted
origin. The app server, not browser code, holds the issuer private key and
provider secret. Use externalBroadcastBindingPublicJwk before requesting an
installation-bound grant; the client returns the same binding through
bindingPublicJwk() after creation. Do not share a grant between viewers.
This path currently targets a small application-owned browser preview, such as OpenRTC Live. Native OBS ingest uses the separate local WHIP bridge and its outbound connection to the app service; the broadcaster does not open an inbound port or configure a tunnel. The app-owned path is not evidence that OpenRTC-managed production hosting is ready.
A Twitch-style page needs a publisher, a distribution path, and an audience page. Keep the video source separate from chat and page state:
camera/microphone or OBS → one publisher → selected broadcast relay → N viewers
app backend → grants, access, chat, page data
The publisher uploads one copy of each selected quality. The relay fans it out;
each viewer has its own subscriber grant and receives a decoded MediaStream.
This avoids one peer connection and one upload per viewer at the publisher. The
relay still delivers roughly one copy to each viewer, so audience size and
bitrate both affect delivery and budget. maxSubscribers is a hard admission
ceiling, not a promise that a particular relay has been capacity tested for N
viewers.
Install the published package with pnpm add openrtc, create an OpenRTC
developer app, and use that app's public API key in the browser. Keep its
matching server secret and any identity-provider secret on your trusted
backend. Normal consumer apps use the managed production OpenRTC service;
an app's development or staging lane does not select an internal OpenRTC
platform lane. See environments.
Give the page a player, a live/offline label, a description, and an optional
chat panel. Your app owns the URL (for example /live/alex), broadcaster
account, description, moderation, and chat persistence. OpenRTC owns broadcast
grants and media delivery; a broadcast grant is not a chat login. A viewer can
watch without signing in if your app allows anonymous grant requests. Require
your app's authentication before accepting a chat write.
Only the authenticated broadcaster should be able to start, stop, or invite
another publisher. Your backend must enforce that policy and limit anonymous
viewer-grant requests per broadcast and caller. Do not put an owner grant,
subscriber grant, or invitation in the page HTML, a URL, or a public log.
For the SDK's auth option, adapt your identity provider to OpenRTC's
AuthProvider; a Clerk browser session alone is not an
OpenRTC assertion.
For a camera or microphone source, the owner's page can prepare a sign-only
publisher, create a bounded owner grant, and add capture tracks. The following
is the admitted-preview SDK shape; create() fails closed for an app that
has not been admitted to managed hosting:
import { OpenRTC } from 'openrtc';
const ownerRtc = OpenRTC({
apiKey: import.meta.env.VITE_OPENRTC_API_KEY,
auth: broadcasterAuthProvider, // your app's authenticated owner session
});
const publisher = await ownerRtc.broadcasts.preparePublisher();
const ownerGrant = await ownerRtc.broadcasts.create({
publisher,
sourceSlot: 'main',
maxPublishers: 1,
maxSubscribers: 100,
maxBitrateBps: 2_000_000,
maxDurationSeconds: 3_600,
maxEgressBytes: 90_000_000_000,
publisherBitrateBps: 2_000_000,
reservedEgressBytes: 90_000_000_000,
});
const session = await publisher.open(ownerGrant);
const capture = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
});
for (const track of capture.getTracks()) await session.media.addTrack(track);
The values above illustrate a ceiling, not a recommended capacity or a
reservation guaranteed to be affordable. Choose caps from the duration,
bitrate, and audience you actually intend to support. Keep ownerGrant only
in the owner's installation and let the app backend keep the broadcast page's
public status. If capture or publication fails, close the session and stop
the captured tracks. Use session.getStats() and session.state to show actual
delivery and failure states, rather than marking the page live just because a
button was clicked.
A viewer needs a grant for that viewer's installation. The owner creates a single-use subscriber invitation; the viewer accepts it with its own OpenRTC client. Your app's trusted endpoint should authorize the request, enforce the viewer cap, and deliver each invitation only to its intended viewer. Anonymous watching means the app does not require a user account; it does not mean a shared grant or an unlimited invitation endpoint.
One simple invitation flow is: (1) the viewer asks your backend for access to the broadcast page; (2) the backend checks the page policy and remaining capacity; (3) the online owner session creates one subscriber invitation; (4) the backend delivers it to that waiting viewer; (5) the viewer accepts it. The owner session must stay available to issue invitations in this sketch. If your application uses a trusted service to issue grants instead, that service must enforce the same role, installation binding, and hard limits.
// Owner-side flow, called once per admitted viewer request.
const invitation = await ownerRtc.broadcasts.invite(ownerGrant, {
role: 'subscriber',
bitrateBps: 2_000_000,
durationSeconds: 3_600,
reservedEgressBytes: 900_000_000,
});
// Deliver invitation through your app's private, bounded response.
// Viewer-side flow. `invitation` is a one-use value supplied by your app.
const viewerRtc = OpenRTC({ apiKey: import.meta.env.VITE_OPENRTC_API_KEY });
const viewerGrant = await viewerRtc.broadcasts.accept(invitation);
const viewerSession = await viewerRtc.broadcasts.subscribe(viewerGrant);
const playback = new MediaStream();
const video = document.querySelector('video[data-live-player]') as HTMLVideoElement;
video.srcObject = playback;
video.playsInline = true;
const stopListening = viewerSession.media.onTrack(({ track }) => {
playback.addTrack(track); // keep both audio and video tracks in one stream
void video.play().catch(() => showPlayButton(video));
});
showPlayButton is your UI's user-gesture fallback for autoplay restrictions.
When the viewer leaves, unsubscribe with stopListening(), close
viewerSession, and call viewerRtc.broadcasts.leave(viewerGrant) if that role
should be retired. A late viewer repeats the same invitation and subscribe
flow; it must receive the current source generation rather than a cached media
URL. Show an offline or retrying state when no tracks arrive. Do not substitute
a looping local preview for proof that a viewer received live media.
Treat chat as an app service with its own authentication, rate limits, storage,
and moderation. A simple design allows anonymous GET/event-stream reads and
requires a signed-in user for POST; neither operation needs a publisher
grant. If you let broadcasters upload compiled page bundles, serve them from a
separate, cookie-free origin and bridge only the player and permitted chat
actions into the page. Do not give custom page scripts owner grants, identity
provider sessions, or relay credentials. A fixed starter page should work even
when no custom bundle has been published.
The referenced OpenRTC Live prototype demonstrates this shape with a Clerk
owner, anonymous viewers, authenticated chat writes, an optional compiled
TypeScript page, and one source slot named main. Those page, auth, and bundle
features belong to the application, not to BroadcastSession.
OBS does not call the browser getUserMedia() API. The Live prototype uses a
local native WHIP bridge: OBS sends H.264/Opus to a loopback WHIP endpoint;
the authenticated owner pairs the bridge once; the bridge publishes signed
OpenRTC media objects into the same main broadcast. Its tested local demo
used 720p30, about 2 Mb/s, and frequent keyframes so late viewers could start
decoding. That bridge and pairing UI are example application code, not a
public hosted OBS ingest endpoint or a feature provided by pnpm add openrtc.
Do not expose a loopback bridge directly to the Internet.
The prototype exercised a local MoQ relay and a Cloudflare SFU carrier in separate short runs, including three initial viewers and a late fourth. The SFU run used signed OpenRTC objects over a DataChannel. Those runs demonstrate the architecture at small scale; they do not establish hosted availability, sustained reliability, or a tested N-viewer capacity for the public service.
For one 2 Mb/s source, the rough delivered-media estimate is
2,000,000 / 8 × viewer-seconds. At 100 viewers watching for one hour, that
is about 90 decimal GB before transport overhead and retries. Choose
maxSubscribers, maxDurationSeconds, maxBitrateBps, and maxEgressBytes
as independent bounds; monitor actual delivered bytes and close the broadcast
when the event ends. Start with a small audience and measure a late join,
refresh, reconnect, and the selected relay's egress before raising the cap.
The pricing page describes the public customer credit
quantity; it is not a capacity or availability guarantee.
The signed grant decides whether a client is an owner, publisher, or
subscriber; browser options cannot promote a viewer. OpenRTC verifies the
source signature, grant generation, publication generation, replay window, and
egress budget before an encoded object reaches the browser decoder. See the
Broadcasts API for invitation, renewal, revocation,
leave, and close methods. Public production hosting remains disabled while
the preview release gates are incomplete.
Give each co-host a separate publisher grant and source slot. Revoking one grant closes that source generation without closing the other hosts. Never share the owner's grant. Your app may show display names, but OpenRTC's media envelope uses anonymous source slots.
The shared Rust core owns role checks, grant and publication generations, source signatures, replay rejection, queue limits, one bounded retry, close, and hard usage limits on native and browser/WASM. Browser code and Tauri IPC only capture/render media and execute private relay commands.
Publisher grants contain only the public verification key. The private media signing key remains in an opaque native Rust or Rust/WASM signer. A copied grant cannot publish without that bound runtime signer. The relay allocation ID in a grant is a non-secret lookup label; it is not a relay URL or credential and is unusable without the verified grant and private host resolution.
The existing moq transport option still means the custom Iroh packet carrier
for a peer connection. Broadcast distribution is separate and has no public
provider, WebTransport, MoQ draft, SDP, ICE, or relay-token API.
The broadcast path hides host network and durable OpenRTC device details from viewers. It does not hide network metadata from OpenRTC, the relay operator, your identity provider, billing systems, or lawful operator access. A separate direct chat or backstage connection can reveal more network information.
The main customer quantity is delivered data: selected video/audio bitrate
times viewer time, plus measured protocol overhead. OpenRTC shows customer
credits and remaining hard budget. The starting public value is $0.10 of
customer credit per decimal GB delivered to viewers. Set limits for viewers,
bitrate, publishers, end time, and delivered data when your backend creates the
broadcast.
You can estimate the public delivery credit before creating a grant. units
is the projected delivered data in decimal GB and may be fractional:
const projectedDeliveredBytes = 750_000_000;
const estimate = rtc.usage.estimate({
operation: 'broadcast.delivery',
units: projectedDeliveredBytes / 1_000_000_000,
});
console.log(estimate.creditsUsd); // 0.075
The server ledger and your portal remain authoritative; this local estimate is for budgets and UI previews.
Read the public surface and lifecycle rules.
Use two-way logical peer media for calls and small groups.