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.
29//!
30//! ## Bitstream gotcha
31//!
32//! The WebRTC ↔ MoQ shape conversion for H.264 and H.265 is handled by
33//! `moq-mux` importers: str0m hands us Annex-B (start-code NALs with inline
34//! parameter sets) and that's exactly what the importers want. AV1 uses the
35//! shared OBU splitter/importer. Opus, VP8, and VP9 pass through.
36
37#![warn(missing_docs)]
38
39pub mod client;
40pub mod server;
41
42// Implementation detail modules: these carry the WebRTC/str0m plumbing (str0m
43// `Rtc`, `Mid`/`Pt`, tokio channels, raw packet buffers) and are deliberately
44// crate-private, so the public surface stays `Client`, `Server`,
45// `whip`/`whep::accept`, and `Response`.
46mod codec;
47mod egress;
48mod error;
49mod ingest;
50mod net;
51mod sdp;
52mod session;
53
54/// Re-export of the HTTP router stack, so consumers can merge the [`axum::Router`]
55/// returned by [`Server::publish_router`] / [`Server::subscribe_router`] (and by
56/// [`whip::router`] / [`whep::router`]) into their own app without adding their own
57/// axum dependency (and risking a version mismatch). A major axum bump is therefore
58/// a breaking change for this crate.
59pub use axum;
60
61/// Re-export of the URL type, so consumers can build the [`url::Url`] that
62/// [`Client::subscribe`] / [`Client::publish`] dial without adding their own url
63/// dependency (and risking a version mismatch). A major url bump is therefore a
64/// breaking change for this crate.
65pub use url;
66
67pub use client::Client;
68pub use error::*;
69pub use server::{Response, Server, whep, whip};
70
71#[cfg(test)]
72mod tests {
73	use std::time::Duration;
74
75	use axum::Router;
76	use bytes::Bytes;
77
78	use crate::codec::{Bridge, Frame, Track};
79	use crate::{Client, Server, client, server};
80
81	const TIMEOUT: Duration = Duration::from_secs(10);
82	const OPUS_PACKET: &[u8] = &[0xfc, 0xff, 0xfe];
83
84	#[tokio::test]
85	async fn whip_and_whep_round_trip_opus() {
86		let source_origin = moq_tokio::origin::spawn();
87		let source_consumer = source_origin.consume();
88		let mut announcements = source_consumer.announced();
89		let mut source = source_origin
90			.create_broadcast("source")
91			.expect("create source broadcast");
92		source
93			.announce(moq_net::origin::Route::default())
94			.expect("announce source broadcast");
95		let catalog = moq_mux::catalog::Producer::new(&mut source, moq_mux::catalog::Config::default())
96			.expect("create source catalog");
97		let mut opus = crate::codec::opus::Bridge::new(source, catalog, 48_000, 2).expect("create Opus bridge");
98		Bridge::push(
99			&mut opus,
100			Frame {
101				timestamp_us: 20_000,
102				payload: Bytes::from_static(OPUS_PACKET),
103			},
104		)
105		.expect("publish source packet");
106		let announcement = tokio::time::timeout(TIMEOUT, announcements.next())
107			.await
108			.expect("source announcement timed out")
109			.expect("source origin closed");
110		assert_eq!(announcement.prefix.as_str(), "source");
111		assert!(announcement.kind.is_active(), "source was unannounced");
112		drop(announcements);
113
114		let server_origin = moq_tokio::origin::spawn();
115		let server = Server::new(server::Config::default());
116		let app = Router::new()
117			.nest("/whip", server.publish_router(server_origin.clone()))
118			.nest("/whep", server.subscribe_router(server_origin.consume()));
119		let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
120			.await
121			.expect("bind HTTP listener");
122		let address = listener.local_addr().expect("HTTP listener address");
123		let http = tokio::spawn(async move { axum::serve(listener, app).await.expect("serve HTTP") });
124
125		let client = Client::new(client::Config::default());
126		let whip = format!("http://{address}/whip/ingested").parse().expect("WHIP URL");
127		tokio::time::timeout(TIMEOUT, client.publish(whip, source_consumer, "source"))
128			.await
129			.expect("WHIP negotiation timed out")
130			.expect("WHIP negotiation failed");
131
132		let output_origin = moq_tokio::origin::spawn();
133		let output = output_origin
134			.create_broadcast("output")
135			.expect("create output broadcast");
136		let output_consumer = output.consume();
137		let whep = format!("http://{address}/whep/ingested").parse().expect("WHEP URL");
138		tokio::time::timeout(TIMEOUT, client.subscribe(whep, output))
139			.await
140			.expect("WHEP negotiation timed out")
141			.expect("WHEP negotiation failed");
142
143		let catalog_track = output_consumer
144			.track(hang::Catalog::DEFAULT_NAME)
145			.expect("output catalog track")
146			.subscribe(hang::Catalog::default_subscription())
147			.await
148			.expect("subscribe to output catalog");
149		let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(catalog_track);
150		let catalog = tokio::time::timeout(TIMEOUT, catalogs.next())
151			.await
152			.expect("output catalog timed out")
153			.expect("read output catalog")
154			.expect("output catalog ended");
155		let track_name = catalog.audio.renditions.keys().next().expect("output Opus rendition");
156		let track = output_consumer
157			.track(track_name)
158			.expect("output Opus track")
159			.subscribe(None)
160			.await
161			.expect("subscribe to output Opus track");
162		let mut opus = Track::opus(track);
163		let frame = tokio::time::timeout(TIMEOUT, opus.next())
164			.await
165			.expect("output Opus packet timed out")
166			.expect("read output Opus packet")
167			.expect("output Opus track ended");
168		assert_eq!(frame.payload.as_ref(), OPUS_PACKET);
169
170		http.abort();
171	}
172}