Skip to main content

moq_rtc/
lib.rs

1//! WebRTC ↔ MoQ gateway.
2//!
3//! Bridges WHIP (RFC 9725) and WHEP between WebRTC peers and
4//! [`moq_net`] broadcasts. The crate is split along two orthogonal axes
5//! so all four combinations can land independently:
6//!
7//! | | RTP-in (ingest into MoQ) | RTP-out (egress from MoQ) |
8//! |---|---|---|
9//! | HTTP server | [`Server::publish_router`] (WHIP server) | [`Server::subscribe_router`] (WHEP server) |
10//! | HTTP client | [`Client::subscribe`] (WHEP client) | [`Client::publish`] (WHIP client) |
11//!
12//! The two HTTP-client paths and the two HTTP-server paths share a single
13//! internal session driver and the same per-codec adapters; the per-direction
14//! split lives in the (crate-private) ingest and egress sources.
15//!
16//! ## Embedding
17//!
18//! Build a [`Server`] and pass your own origin handles when merging
19//! [`Server::publish_router`] / [`Server::subscribe_router`] into your own axum
20//! app, or dial out with [`Client`]. A command-line interface is provided by the
21//! `moq-cli` binary, on top of this library.
22//!
23//! The bundled routers are unauthenticated: they derive the broadcast name from
24//! the request path. To own the HTTP route and authorize requests yourself
25//! (resolving the broadcast name from a verified token), skip the routers and
26//! call [`whip::accept`] (ingest) / [`whep::accept`] (egress) from your own
27//! handler. Return the [`Response::answer`] in your HTTP response, then run
28//! [`Response::run`] to drive the media session for its lifetime. The routers and
29//! the `axum` re-export sit behind the default `server` feature, so such an
30//! embedder can drop them with `default-features = false`.
31//!
32//! ## Bitstream gotcha
33//!
34//! The WebRTC ↔ MoQ shape conversion for H.264 and H.265 is handled by
35//! `moq-mux` importers: str0m hands us Annex-B (start-code NALs with inline
36//! parameter sets) and that's exactly what the importers want. AV1 uses the
37//! shared OBU splitter/importer. Opus, VP8, and VP9 pass through.
38
39#![warn(missing_docs)]
40
41pub mod client;
42pub mod server;
43
44// Implementation detail modules: these carry the WebRTC/str0m plumbing (str0m
45// `Rtc`, `Mid`/`Pt`, tokio channels, raw packet buffers) and are deliberately
46// crate-private, so the public surface stays `Client`, `Server`,
47// `whip`/`whep::accept`, and `Response`.
48mod codec;
49mod egress;
50mod error;
51mod ingest;
52mod net;
53mod sdp;
54mod session;
55
56/// Re-export of the HTTP router stack, so consumers can merge the [`axum::Router`]
57/// returned by [`Server::publish_router`] / [`Server::subscribe_router`] (and by
58/// [`whip::router`] / [`whep::router`]) into their own app without adding their own
59/// axum dependency (and risking a version mismatch). A major axum bump is therefore
60/// a breaking change for this crate. Only with the `server` feature.
61#[cfg(feature = "server")]
62pub use axum;
63
64/// Re-export of the URL type, so consumers can build the [`url::Url`] that
65/// [`Client::subscribe`] / [`Client::publish`] dial without adding their own url
66/// dependency (and risking a version mismatch). A major url bump is therefore a
67/// breaking change for this crate.
68pub use url;
69
70pub use client::Client;
71pub use error::*;
72pub use server::{Response, Server, whep, whip};
73
74#[cfg(all(test, feature = "server"))]
75mod tests {
76	use std::time::Duration;
77
78	use axum::Router;
79	use bytes::Bytes;
80
81	use crate::codec::{Bridge, Frame, Track};
82	use crate::{Client, Server, client, server};
83
84	const TIMEOUT: Duration = Duration::from_secs(10);
85	const OPUS_PACKET: &[u8] = &[0xfc, 0xff, 0xfe];
86
87	#[tokio::test]
88	async fn whip_and_whep_round_trip_opus() {
89		let source_origin = moq_tokio::origin::spawn();
90		let source_consumer = source_origin.consume();
91		let mut announcements = source_consumer.announced();
92		let mut source = source_origin
93			.create_broadcast("source")
94			.expect("create source broadcast");
95		source
96			.announce(moq_net::origin::Route::default())
97			.expect("announce source broadcast");
98		let catalog = moq_mux::catalog::Producer::new(&mut source, moq_mux::catalog::Config::default())
99			.expect("create source catalog");
100		let mut opus = crate::codec::opus::Bridge::new(source, catalog, 48_000, 2).expect("create Opus bridge");
101		Bridge::push(
102			&mut opus,
103			Frame {
104				timestamp_us: 20_000,
105				payload: Bytes::from_static(OPUS_PACKET),
106			},
107		)
108		.expect("publish source packet");
109		let announcement = tokio::time::timeout(TIMEOUT, announcements.next())
110			.await
111			.expect("source announcement timed out")
112			.expect("source origin closed");
113		assert_eq!(announcement.prefix.as_str(), "source");
114		assert!(announcement.kind.is_active(), "source was unannounced");
115		drop(announcements);
116
117		let server_origin = moq_tokio::origin::spawn();
118		let server = Server::new(server::Config::default());
119		let app = Router::new()
120			.nest("/whip", server.publish_router(server_origin.clone()))
121			.nest("/whep", server.subscribe_router(server_origin.consume()));
122		let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
123			.await
124			.expect("bind HTTP listener");
125		let address = listener.local_addr().expect("HTTP listener address");
126		let http = tokio::spawn(async move { axum::serve(listener, app).await.expect("serve HTTP") });
127
128		let client = Client::new(client::Config::default());
129		let whip = format!("http://{address}/whip/ingested").parse().expect("WHIP URL");
130		tokio::time::timeout(TIMEOUT, client.publish(whip, source_consumer, "source"))
131			.await
132			.expect("WHIP negotiation timed out")
133			.expect("WHIP negotiation failed");
134
135		let output_origin = moq_tokio::origin::spawn();
136		let output = output_origin
137			.create_broadcast("output")
138			.expect("create output broadcast");
139		let output_consumer = output.consume();
140		let whep = format!("http://{address}/whep/ingested").parse().expect("WHEP URL");
141		tokio::time::timeout(TIMEOUT, client.subscribe(whep, output))
142			.await
143			.expect("WHEP negotiation timed out")
144			.expect("WHEP negotiation failed");
145
146		let catalog_track = output_consumer
147			.track(hang::Catalog::DEFAULT_NAME)
148			.expect("output catalog track")
149			.subscribe(hang::Catalog::default_subscription())
150			.await
151			.expect("subscribe to output catalog");
152		let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(catalog_track);
153		let catalog = tokio::time::timeout(TIMEOUT, catalogs.next())
154			.await
155			.expect("output catalog timed out")
156			.expect("read output catalog")
157			.expect("output catalog ended");
158		let track_name = catalog.audio.renditions.keys().next().expect("output Opus rendition");
159		let track = output_consumer
160			.track(track_name)
161			.expect("output Opus track")
162			.subscribe(None)
163			.await
164			.expect("subscribe to output Opus track");
165		let mut opus = Track::opus(track);
166		let frame = tokio::time::timeout(TIMEOUT, opus.next())
167			.await
168			.expect("output Opus packet timed out")
169			.expect("read output Opus packet")
170			.expect("output Opus track ended");
171		assert_eq!(frame.payload.as_ref(), OPUS_PACKET);
172
173		http.abort();
174	}
175}