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.