Skip to content

RFC: host-mediated realtime media sessions - #839

Open
replghost wants to merge 30 commits into
mainfrom
rfc/realtime-media-sessions
Open

replghost wants to merge 30 commits into
mainfrom
rfc/realtime-media-sessions

Conversation

@replghost

Copy link
Copy Markdown
Contributor

Summary

Proposes a Media service: a product runs an audio or video call, with screen
sharing, without running a realtime stack in its own code. The host owns the
connections, capture devices, codecs, audio route and the pictures on screen;
the product carries opaque signalling over a channel it already has, places the
rectangles the host draws into, and is told how the call is going.

One file, docs/rfcs/realtime-media-sessions.md. No code.

Tracking issue: #838.

What it decides

  • Media never enters a product. No frames, tracks or device handles; the
    host composites each incoming picture into rectangles the product places.
  • Signalling is sealed, and the first message carries no addresses. Keys are
    per session, so there is no long-lived identity to publish, rotate or
    correlate across products.
  • Devices belong to the host — which microphone and camera, gain, echo
    cancellation, routing, and the switching affordance. A product states intent
    and may express a preference; it is told the kind of device actually in use,
    never a name, model or list.
  • Consent is per call. Starting, joining and answering all take a decision
    before anything reaches the network; withdrawing camera or microphone access
    ends the call.
  • Group calls and audio-only calls are both ordinary. How a host connects a
    group is deliberately unspecified, and a small call needs no new server.
  • Relay versus direct is a host policy, invisible to the product.

The first implementation is WebRTC. Nothing a product sees says so.

Out of scope

Recording, product-chosen codecs, large calls needing conference
infrastructure, and waking a product that is not running for an incoming call —
the last is media-neutral and will be handled separately.

Base

Off main, parallel to #540 rather than stacked on it, so it can be combined
with #540 and #709 in an integration branch for testing.

Open questions

Carried in the RFC: whether per-call consent reuses RemotePermission::WebRtc;
whether a call may start before a surface is visible, which would let a
background worker begin an audio-only call; whether the coarse quality level is
worth sending; and whether a host could prove a key came from it, given the
product is the signalling channel.

RemotePermission::WebRtc is scoped to the browser sandbox and enforced by
removing RTCPeerConnection from a web realm, so a PolkaVM product cannot
place a call at all today. RFC 0029 proposes a Media service where the host
owns the peer connection, capture devices, codecs, audio route and video
compositing, while the product carries opaque host-sealed signalling over
its own channel and positions rectangles the host draws.

Frames never enter the product: the PolkaVM surface is already a host-owned
view in a host-owned container, so a composited video sibling needs no new
texture path, whereas delivering decoded frames to the guest would put
camera output inside the product.
The host owns candidate gathering, STUN, TURN credential minting and the
selected pair, so the product never learns either party's address: sealed
signalling, session state, coarse quality and remote-track flags carry no
candidate, selected-pair or relay information.

Address privacy between the two devices is a separate host policy, because
ICE with a default transport policy still trades candidates end to end:
relay-only hides both addresses at the cost of latency and egress, while
direct-preferred lets the devices see each other. The host chooses, the
product cannot detect or override it, and a messaging host should default
to relay-only.
…acking draft

An incoming call is not an input-modality dispatch. The input modality
decides which product handles input the user just produced here; an
invitation arrives from a remote party on a product's own channel, so the
product is already fixed by channel ownership and the only hard question is
whether that party may interrupt the user — which the product answers,
because it authenticates the channel.

Background delivery needs a wake contract (which executable, which
capabilities, how long, how often a peer may trigger it). That is shared with
every product reacting to a remote event, so it is deferred to its own RFC;
this API works without it and gains background ringing when it lands.

Adds the tracking-issue draft in the shape of #550.
…nt routes

The worker is part of the product, so it sits inside the same boundary as the
surface. An invitation is sealed to the host's media key, whose public half the
product publishes itself, so the worker stores and forwards bytes it cannot
open and the host unseals only when the surface answers. No call hands a worker
an address, candidate, SDP fragment, relay identity or device id, and the
session stream is unavailable to it; the worker knows only which of the
product's own contacts is calling, which it already knew.

RFC 0030 adds the missing inbound primitive: a durable statement-store topic
route the host matches while the product is not running, which forms a Worker
Lifecycle reference, wakes the worker, delivers matched statements with a
durable cursor, and is rate-limited and user-revocable because it lets a remote
party start code and interrupt the user. Topics stay opaque, so the host learns
nothing about the peer. A call invitation becomes one statement on one route,
so RFC 0029 needs no media-specific inbound path.
Matches the house tracking-issue shape: Problem, Goal, Requirements, core
implementation scope, implementation references, tasks. Scope limits move into
Goal and the open decisions become tasks, so the issue asserts the contract
without restating the RFC's reasoning.

Drops permission-internals framing, type names, and transport jargon, and
states the problem as products each shipping their own stack with media and
peer addresses inside product code, leaving the user no single place to see or
stop a call. Nothing is runtime-specific: the gap applies to web products and
to every other kind.
Calls are no longer limited to two people: the goal, requirements and scope
now speak about participants, per-participant video rectangles, participants
joining and leaving, and conference topology as a Host choice alongside direct
and relayed carriage. Screen sharing and recording stay out.

Removes the named-host reference, which asserted an engine and relay
deployment that this issue does not need to claim.
Both RFCs move to unnumbered filenames and leave the index, matching the
repository's existing unnumbered drafts; a number can be assigned on
acceptance rather than claimed while the text is still moving.

The tracking issue now allows calls between more than two people without
prescribing how, and states that no new server infrastructure is in scope.
Cuts the draft from 373 to 161 lines: the method signatures and wire-id block
move to the tracking issue, and the input-modality table, worker walkthrough,
quoted lifecycle passages and separate obligations sections collapse into the
sections that needed them.

A session now holds a set of participants rather than one peer, with per
participant signalling, rectangles and track reporting; how a host connects a
group is deliberately unspecified and needs no new server. The rendering
argument no longer depends on one runtime: every host already composites the
product's surface, whether that is a canvas, a web view or a native view.
Drops the internal prior-art paragraph, the quoted lifecycle passages, the
filter bound citations and the numbered delivery steps, keeping the concept:
a durable topic route the host matches while a product is closed, a bounded
wake with the matched statements, and a capability the user can revoke.
The previous text had the product name participants with its own identifiers,
which claimed a privacy property while creating a leak: product identities are
contacts, usernames and account keys, and handing them to the host puts them in
host memory, logs and crash reports for no benefit.

A participant is now a host-minted handle, meaningless outside its session and
carrying no product-side identity. It exists only to correlate a signalling
message, an arriving track and a video rectangle with one peer, which is all the
transport needs; identity stays product-side, where the callee's media key was
already obtained.
Offering and answering were properties of the session, which only holds for two
people: in a group call a product may be answering one peer while inviting
another. add_participant now takes an optional received invitation, so the host
answers it or produces one, and returns a participant handle;
remove_participant drops a peer without ending the call.

Consent follows from that: starting, joining and answering all pass through
create_session and take one decision there, while inviting a further peer into a
call the user is already in does not ask again.

The tracking issue gains the service table, the participant-handle guarantee,
the consent wording, and the honest group-call ceiling.
The previous problem statement opened with a missing API and described a web
product as bringing its own stack, which it does not: the browser engine is
already there, and the real complaint is that the camera and microphone feed and
both sides' addresses pass through product code.

It now opens with what cannot be done — a messaging product holds a conversation
with a contact and cannot turn it into a call — then why the only route today is
wrong, then the consequence: one implementation per product, and no Host-level
way to show a call is running or stop it.
Products do not call people. Both documents now open with what the user cannot
do — make a call from a product — and what the product consequently cannot
offer, rather than personifying the product.
Replaces two vague phrases in the problem statement: an ambiguous pronoun in a
sentence holding both a user and a product, and the euphemisms 'offer to ring'
and 'carries a live call'. Both documents now say the product cannot start a
voice or video call because no Host service carries real-time audio or video.
The routes and wake draft is not published, so neither the RFC nor the tracking
issue points at its path. Both now say background delivery to a product that is
not running is out of scope and handled separately, and the RFC keeps the reason
it is separate: every product reacting to a remote event needs the same thing.
The issue no longer restates the problem and goal in its own words. It opens
with the RFC's summary, says plainly that it tracks the implementation, and
names WebRTC as the first one while keeping the service transport-neutral.

Scope limits that had their own paragraph — group size, screen sharing,
recording, codecs, background wake — become requirements, and the reference
flow drops decline, which is the product not answering rather than a method.
Replacing WebRTC would need both ends of a call to agree, so it is a
host-to-host compatibility question rather than something a product-facing API
can promise, and neither document defines transports coexisting.

Both now state only what is true today: the first implementation is WebRTC, and
nothing a product sees says so, because it handles sealed messages and
host-minted handles rather than transport detail.
Audio-only calls are ordinary rather than a degenerate case: video is optional
per participant and per direction, and a product places a rectangle only for a
participant actually sending video.

The tracking issue lists the products this is for: DIM2 and Chat as SPAs, T3ams
for audio and video calling, Meet, and anything else needing peer-to-peer audio,
video or sharing.
Meet and T3ams already share screens over WebRTC, so excluding it would leave
both unable to move onto this service. Screen is a third local track beside
microphone and camera: create_session may declare it, set_local_tracks starts
and stops it, and an incoming shared screen is a picture the host draws into a
rectangle like a camera.

Consent differs from a device permission, so the host runs its own picker: the
user chooses what is shared and the product never names a window or a display.
Ending a share stops that track and leaves the call running. Recording and
product-chosen codecs stay out.
A product-level audio route implied the product picks earpiece or speaker and
names a camera, which the OS overrides anyway: iOS moves input and route when a
headset appears, and another app can take the camera. set_local_tracks now
states intent — microphone, camera, screen, and a preference such as the
front-facing camera or a speakerphone-style call — while the host owns device
choice, gain, echo cancellation, routing and the switching affordance, and
reports what is live. Products never enumerate devices, which would be a
fingerprinting surface.

The tracking issue loses the service table: the method set is what RFC review
will change, so pinning it outside the RFC creates a second source of truth,
and the requirements already state every capability.
Live or not live is too thin for a call UI: a product showing a speaker button
needs to know the call is on speaker, and one showing a camera flip needs to
know which camera is sending. The subscription now reports the kind of device
the host chose — earpiece, speaker, headset, front or rear camera — alongside
what is actually live, while names, models and lists stay out as a
fingerprinting surface.
The media key existed so a caller could encrypt an invitation before the callee
had a session. An invitation does not need to be secret: it needs to carry no
addresses. The host now puts its per-session public key and negotiation terms
in it and nothing that locates the device, candidates follow sealed to the pair
of per-session keys, and the long-lived identity disappears along with its
rotation, revocation and cross-product linkability.

It also bought less than it looked: the product distributes any key it
publishes, so a product willing to substitute one could read what follows
either way. That limit is now stated as a trade-off, with the trust-root
question as an open one.

Also fixes terminology left over from screen sharing — pictures rather than
video where a shared screen is included — and stops calling screen capture a
device permission.
The first paragraph narrated around the problem. It now names it: a product has
no way to make a call without running the WebRTC stack in its own code, which a
web product can do and no other kind can. The second paragraph says why that
place is wrong — feed and addresses inside the product, host-owned devices and
codecs, one stack and one set of bugs per product.

Also drops the claim that nothing in the transport needs to know who anyone is,
which overstated the design: the host sees who is called, when, and with how
many people.
The parenthetical about which permission ungates RTCPeerConnection was
mechanism inside a sentence about what a product cannot do.
The routes and wake draft is unreviewed and unreferenced, so it belongs on its
own branch rather than in this PR. The tracking-issue draft has served its
purpose and now lives as #838; keeping a copy in tree
would only let the two drift.
@replghost
replghost requested a review from a team September 17, 2026 22:10
@github-actions github-actions Bot added documentation Improvements or additions to documentation rfc labels Sep 17, 2026
@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown

CI Status: 19 required jobs green, 12 passed and 7 skipped by path filter.

All job results
job result
android-bindings skipped
changes success
changeset-guard skipped
cli-package skipped
codegen success
e2e skipped
explorer success
ios-bindings success
ios-swift skipped
licenses success
playground success
provider-android-bindings skipped
release-guard success
rust success
ts-client success
ts-debugger success
ts-host success
wasm-provider success
workflow-lint skipped

Commit 1a16b3d9 · run log

Adopts the model Epoca already ships: the host offers, answers and exchanges
candidates over its own channel, and the product only names a peer. Nothing to
seal, no key to distribute, and no session description or candidate in product
code — so the invitation, per-session keys and the key-substitution trade-off
all go, replaced by a host obligation to have a channel at all.

Consent follows RFC 0002 instead of inventing a per-call prompt: calling is one
permission, asked once and remembered, with allow-once as prompt behaviour
rather than a second kind of grant. What keeps a remembered grant from becoming
an invisible call is host control — an unsuppressable in-call indicator the user
can end the call from, teardown when camera or microphone access is withdrawn,
and teardown when the calling permission is revoked.

States that there is no statistics call: candidate pairs carry the addresses
this design keeps from products, and a coarse quality level covers what a call
UI can act on.

Placement is rectangles on the wire for every product, with an element-based
attach helper named as SDK sugar over the same contract, which is what the
shipped implementation already does by reporting element geometry.

Incoming calls now arrive through the session stream from the host channel
rather than as a product-carried invitation, and the product still decides
whether the caller may interrupt the user.
Addresses were argued in the concepts, the transport section and the
trade-offs, and the absent statistics call was stated twice. The transport
section now carries the relay policy alone, the device paragraph absorbs the
statistics point in one clause, the rectangle concept and its SDK helper are
one paragraph, and the rendering section is two.
Settles the open question rather than keeping both options: the host offers,
answers and exchanges candidates over the statement store, owns that
subscription, and calling therefore needs no statement permission of its own.
The trade-off is stated as the tie to that transport, and the shipped
implementation already signals this way.
Calling gets its own permission rather than reusing the WebRTC grant, whose
load-time resolution exists so a browser realm can be locked down before page
script runs and says nothing about placing a call.

A call needs no visible surface: audio connects while the product is still
opening, so a background worker can answer and the picture arrives once there
is somewhere to draw it.

Quality reporting is cut. A product cannot act on a bitrate or a round-trip
time, candidate pairs carry what this design keeps out of product code, and a
coarse signal can be added later without breaking anything.

One open question remains: the group size a host attempts before refusing.
Six is what a shared game needs and what a mesh carries comfortably, so a host
must support at least that many. Above it a host may refuse, and the refusal is
reported so a product can grey out its control instead of failing mid-call —
which leaves the ceiling a host policy rather than a number this RFC has to
guess. No open questions remain.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation rfc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant