Broadcasts API
Non-peer live publishing and subscription API
For an application-owned broadcast, import createExternalBroadcastClient
and externalBroadcastBindingPublicJwk from openrtc/external-broadcast.
The application server issues a signed grant for each installation and
returns its public issuer key with scoped SFU or MoQ relay access through the
client's resolve callback. preparePublisher(), open(grant), and
subscribe(grant) then use the same Rust/WASM session and media path shown
below. Managed create(), invite(), accept(), renew(), revoke(),
leave(), and close(grant) are unavailable through this entrypoint; the
application must implement its own grant lifecycle and provider budget.
const publisher = await client.broadcasts.preparePublisher();
const ownerGrant = await client.broadcasts.create({
publisher,
maxPublishers: 3,
maxSubscribers: 1_000,
maxBitrateBps: 6_000_000,
maxDurationSeconds: 3_600,
maxEgressBytes: 20_000_000_000,
publisherBitrateBps: 2_000_000,
reservedEgressBytes: 1_000_000_000,
});
const live = await publisher.open(ownerGrant);
const capture = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
for (const track of capture.getTracks()) await live.media.addTrack(track);
The example above runs only with an admitted preview/local relay allocation. It is not evidence for production Cloudflare availability, price, SLA, or scale.
grant is an opaque BroadcastGrant created for one installation by the
managed control plane. Do not log, persist, copy, parse, or put it in a URL.
BroadcastSession exposes read-only id, role, and state; familiar media
methods; bounded delivery/drop/retry stats; and close(). The roles are
owner, publisher, subscriber, and processor. The grant, not caller
options, chooses the role and anonymous source slots.
A subscriber can open its own grant with subscribe(grant) (or open(grant)).
A publisher first asks the runtime for a sign-only identity. Managed create()
binds only the public verification key and reserves every hard limit before
provider work:
const publisher = await client.broadcasts.preparePublisher();
const grant = await client.broadcasts.create({
publisher,
maxPublishers: 3,
maxSubscribers: 1_000,
maxBitrateBps: 6_000_000,
maxDurationSeconds: 3_600,
maxEgressBytes: 20_000_000_000,
publisherBitrateBps: 2_000_000,
reservedEgressBytes: 1_000_000_000,
});
const live = await publisher.open(grant);
For another viewer or co-host, make a single-use invitation. Capacity is not reserved until the intended installation accepts it. A co-host prepares its own signer and sends only the public verification key through your app:
const cohost = await cohostClient.broadcasts.preparePublisher();
const invite = await ownerClient.broadcasts.invite(ownerGrant, {
role: 'publisher',
publicationVerificationKey: cohost.verificationKey,
sourceSlot: 'host-b',
bitrateBps: 2_000_000,
durationSeconds: 3_600,
reservedEgressBytes: 1_000_000_000,
});
const cohostGrant = await cohostClient.broadcasts.accept(invite);
const cohostLive = await cohost.open(cohostGrant);
An invitation is a short-lived bearer secret. Send it only to its recipient;
acceptance binds the resulting role grant to that installation. Revoking the
co-host grant retires host-b without changing other source slots.
Renewal returns a new fenced grant generation. Revocation and close are owner operations; they do not reveal the provider allocation:
const renewed = await client.broadcasts.renew(grant);
await ownerClient.broadcasts.revoke(ownerGrant, { invite }); // retire only that invited role/source
await cohostClient.broadcasts.leave(renewed); // self-retire this installation's role
await client.broadcasts.close(ownerGrant); // end the whole broadcast
The publisher's private signing key stays inside native Rust or Rust/WASM. It is not in the grant and JavaScript cannot export it or call a raw signing API. Close the prepared publisher if you decide not to use it.
Native Rust
Native applications do not need TypeScript:
use openrtc::broadcast::{
BroadcastAdapterAction,
BroadcastPublisherSigner,
};
let publisher = BroadcastPublisherSigner::generate()?;
let public_key = publisher.verifying_key();
let token = app_backend.create_publisher_grant(public_key).await?;
let broadcasts = client.broadcasts();
let challenge = broadcasts.prepare_grant_verification(
&token, &platform_issuer_key, now_ms,
)?;
// The device private key stays behind this native sign-only boundary.
let proof = platform_device_signer.sign(&challenge.signing_bytes)?;
let grant = broadcasts.complete_grant_verification(
&token, &platform_issuer_key, &challenge.handle, &proof, now_ms,
)?;
let live = client
.broadcasts()
.open_publisher(grant, &publisher, now_ms)?;
for command in live.take_actions(16) {
private_relay_adapter.execute(command).await?;
}
Production adapters are private OpenRTC internals. openrtc/native exposes the
same client.broadcasts methods through a thin IPC facade. Tauri keeps the
device and publication signers, provider credentials, grant verification,
session, adapter command queue, source verification, and usage counters in
Rust through opaque handles. Only verified portable media crosses IPC for the
WebKit capture/render edge; native TypeScript never initializes WASM. Browser
JavaScript may pass public one-shot challenge bytes to a sign-only device-key
callback, but no private key is exported. Hosted browser/native A/V evidence
and provider enforcement remain developer-preview release gates.