Media
Add microphone, camera, and screen tracks to an OpenRTC connection
OpenRTC gives every peer connection a WebRTC-familiar media API. Your app asks
the browser or native platform for a microphone or camera, then adds those
tracks to connection.media. OpenRTC handles the encoded media protocol,
encryption, framing, recovery, and carrier changes.
camera or microphone
-> OpenRTC media track
-> protected logical Iroh connection
-> iroh-quic, webrtc, or moq carrier
-> remote media track
The carrier is an internal path, not a second connection. Your app does not create SDP, exchange ICE candidates, open a MoQ session, or add tracks again when OpenRTC changes carriers.
Browser quickstart
Start with the rtc client from the Quickstart, then
join a room for the call:
<video id="local-video" autoplay muted playsinline></video>
<video id="remote-video" autoplay playsinline></video>
const room = await rtc.rooms.join('call-123');
const localVideo = document.querySelector<HTMLVideoElement>('#local-video')!;
const remoteVideo = document.querySelector<HTMLVideoElement>('#remote-video')!;
const localMedia = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
});
localVideo.srcObject = localMedia;
// This simple example is a one-to-one call. Use one MediaStream per peer for a
// group call.
const remoteMedia = new MediaStream();
remoteVideo.srcObject = remoteMedia;
const stopConnections = room.onConnection(async (connection) => {
const stopTracks = connection.media.onTrack(({ track }) => {
for (const current of remoteMedia.getTracks()) {
if (current.kind === track.kind) remoteMedia.removeTrack(current);
}
remoteMedia.addTrack(track);
});
connection.onClose(stopTracks);
for (const track of localMedia.getTracks()) {
await connection.media.addTrack(track);
}
});
onConnection() reports the stable logical peer. If a physical Iroh path is
replaced, OpenRTC reopens the active publications after the new generation is
ready. The callback does not become a reconnect owner.
When the user ends the call, release the browser devices and the OpenRTC room:
stopConnections();
localMedia.getTracks().forEach((track) => track.stop());
await room.leave();
await rtc.close();
Call controls
Inside an onConnection() callback, addTrack() returns a sender. Keep it when
the UI needs mute, camera switching, screen sharing, statistics, or an explicit
stop:
const cameraTrack = localMedia.getVideoTracks()[0];
const camera = await connection.media.addTrack(cameraTrack);
await camera.setEnabled(false); // mute this outgoing track
const display = await navigator.mediaDevices.getDisplayMedia({ video: true });
const screenTrack = display.getVideoTracks()[0];
await camera.replaceTrack(screenTrack); // keep the publication, change its source
console.log(await camera.getStats());
await camera.stop();
display.getTracks().forEach((track) => track.stop());
Use connection.media.getStats() for all senders and receivers on the peer.
The latency values are bounded local pipeline measurements, not
glass-to-glass latency between devices.
Browser support
Camera and microphone access requires HTTPS or localhost plus user permission.
Browser media also needs WebCodecs and a supported track processing/rendering
edge. OpenRTC uses worker and Web Audio fallbacks where the browser exposes
equivalent APIs. An unsupported browser fails clearly instead of silently
creating a separate RTCPeerConnection media lifecycle.
Native Rust
The openrtc crate works without TypeScript. Native applications provide an
encoded MediaSource for capture and encoding, and a MediaSink for decoding
and rendering. Those platform edges never open sockets or choose carriers:
use openrtc::media::MediaSource;
use std::sync::Arc;
let media = room.media_connection(Arc::clone(&client), peer_id)?;
let source: Box<dyn MediaSource> = Box::new(MyEncodedCamera::new()?);
let mut sender = media.add_track(source).await?;
while sender.send_next().await? {
// The source controls capture pacing. OpenRTC owns delivery and recovery.
}
The app's one incoming-stream dispatcher passes the selected media stream to
MediaConnection::accept_incoming(). Calling render_next() validates the
generation, integrity, and replay state before the host's MediaSink sees a
chunk.
In Tauri, import OpenRTC from openrtc/native and use the
openrtc-tauri IPC bridge. The WebView keeps the same TypeScript media surface,
while the native Rust client owns the endpoint and peer lifecycle; the Tauri
path does not start a second WASM connection manager.
For a complete multi-participant setup and a live, origin-gated room, see Build a video meeting. The demo uses actual OpenRTC peers and media tracks; it does not simulate other participants in the browser.
See Advanced native for setup and Media, streams, and datagrams for every method, statistics field, codec rule, and the lower-level stream APIs.