proto4webrtc
v1.11.2
Published
mediasoup runtime for proto4webrtc-annotated protofiles: SFU (signaling, transports, in-process data-stream subscriptions) and browser consumer client
Readme
proto4webrtc (TypeScript runtime)
Two entry points:
proto4webrtc— the mediasoup SFU side (Node): signaling, Worker/Router/transport lifecycle, and in-process access to a data stream by label — no browser, no WebRTC.proto4webrtc/client— the browser consumer: one call for signaling, Device load, receive transport, and ICE config, plus existing-and-future-producer subscriptions per stream.
The codegen counterpart is the npm package protoc-gen-proto4webrtc-ts,
which generates typed wrappers per stream on top of both.
npm install proto4webrtcimport { Proto4WebrtcSfu } from "proto4webrtc";
import { TelemetryStream } from "./gen/proto4webrtc"; // generated, per project
const sfu = new Proto4WebrtcSfu(); // sane defaults; every field overridable
// e.g. api/sfu/route.ts (next-ws), or any node `ws` server:
export async function UPGRADE(client: import("ws").WebSocket) {
sfu.handleWSClient(client);
}
// e.g. api/status/route.ts:
export function GET() {
return Response.json(sfu.getStatus());
}
// anywhere else in the same server process — no websocket, no browser:
const unsubscribe = TelemetryStream.subscribe(sfu, (msg) => {
console.log(msg.stamp, msg.value0);
});Real WebRTC media/video consumption is unchanged — browsers still connect to
handleWSClient's signaling endpoint with real mediasoup-client.
subscribe() is the new capability: an app process gets data-stream
messages directly, via a mediasoup DirectTransport (no ICE/DTLS). It's
safe to call before the matching producer connects, and keeps working across
producer reconnects.
connectToSfu() explicitly warms up the Worker/Router; both
handleWSClient() and subscribe() do this internally too, so call order
never matters.
Config
Zero config works out of the box: the defaults are env-driven, and the announced address is auto-detected (first non-internal IPv4) when a wildcard listen has none — peers on the same machine or LAN just connect.
| Env var | Meaning | Default |
|---|---|---|
| MEDIASOUP_LISTEN_IP | bind address | 0.0.0.0 |
| MEDIASOUP_ANNOUNCED_IP | public address peers connect to (fallback: PUBLIC_IP, then auto-detect) | auto-detect |
| MEDIASOUP_RTC_MIN_PORT / MEDIASOUP_RTC_MAX_PORT | RTC UDP/TCP port range (publish + open in firewall) | 40000–40049 |
| TURN_URLS | comma-separated TURN urls, appended to the STUN default | unset |
| TURN_USERNAME / TURN_CREDENTIAL | TURN credentials | unset |
Code-level config overrides the env-derived defaults. It's merged shallowly,
per top-level section (worker/router/webRtcTransport/iceServers) —
overriding a section means repeating any nested fields (e.g. listenInfos)
you still want from the default:
const sfu = new Proto4WebrtcSfu({
webRtcTransport: {
listenInfos: [{ protocol: "udp", ip: "0.0.0.0", announcedAddress: process.env.PUBLIC_IP }],
enableUdp: true,
},
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: process.env.TURN_URL!, username: process.env.TURN_USERNAME, credential: process.env.TURN_CREDENTIAL },
],
});iceServers defaults to a public STUN server. The resolved list is served
to browser consumers automatically in the createTransport signaling reply
(anyone who can reach the signaling endpoint gets it, TURN credentials
included — scope credentials accordingly). sfu.getIceServers() returns the
same list for manual wiring.
Browser consumer (proto4webrtc/client)
Prefer the generated connectToSfu() from protoc-gen-proto4webrtc-ts
output — it returns this client extended with a handle per stream
(client.<label>.setCallback(...) / .setSubscribed(true)). The raw client
underneath:
import { connectToSfu } from "proto4webrtc/client";
const client = await connectToSfu({
url: "ws://localhost:3000/api/sfu", // default: ws(s)://<location.host>/api/sfu
onConnectionState: (state) => console.log(state),
});
// The returned function unsubscribes, closing the consumer at the SFU as well.
const stop = client.subscribe("telemetry", (data) => { /* raw Uint8Array */ });
// Third argument picks one stream out of the producers of that kind; without
// it, every video producer matches (fine only when there is exactly one).
client.onMedia("video", (track) => { videoEl.srcObject = new MediaStream([track]); }, "camera");
client.onProducerClosed((label) => { /* a producer went away */ });
client.close();onProducerClosed receives the closed producer's label — with several robot
processes behind one SFU, filter by label to track each process's liveness
independently (label is undefined when it was never announced, and other
browsers' rpc request channels appear as "<service label>/requests").
subscribe()/onMedia() cover the producer already online at call time and
any that (re)appears later, and create consumers only for what was asked —
the SFU never sends this peer unrequested streams.
Streams declared lazy are not produced until a consumer asks: the generated
subscribe() handles that for you, and raw subscribe()/onMedia() callers
do it with client.keepAlive(label), which pings "<label>-pings" until the
returned release function is called. Proto4WebrtcSfu.keepAlive() is the same
thing for in-process subscribers.
Rpc (services annotated (proto4webrtc.rpc_service)): the generated
client.rpc.<method>() wrappers are the intended surface; underneath sits
client.callRpc(label, method, payload) — it lazily creates a send
transport, produces the service's "<label>/requests" data channel, and
matches responses from "<label>/responses" by client id (10 s default
timeout, per-call override).
Full docs: https://github.com/Emil1483/proto4webrtc
