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`] over your own
19//! [`OriginProducer`](moq_net::origin::Producer) /
20//! [`OriginConsumer`](moq_net::origin::Consumer) and merge
21//! [`Server::publish_router`] / [`Server::subscribe_router`] into your own axum
22//! app, or dial out with [`Client`]. A command-line interface is provided by the
23//! `moq-cli` binary, on top of this library.
24//!
25//! The bundled routers are unauthenticated: they derive the broadcast name from
26//! the request path. To own the HTTP route and authorize requests yourself
27//! (resolving the broadcast name from a verified token), skip the routers and
28//! call [`whip::accept`] (ingest) / [`whep::accept`] (egress) from your own
29//! handler. Return the [`Response::answer`] in your HTTP response, then run
30//! [`Response::run`] to drive the media session for its lifetime.
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 underlying WebRTC stack, so consumers can name the str0m
57/// types that surface through [`Error::Rtc`] / [`Error::RtcInput`] without adding
58/// their own str0m dependency (and risking a version mismatch). A major str0m
59/// bump is therefore a breaking change for this crate.
60pub use str0m;
61
62/// Re-export of the HTTP router stack, so consumers can merge the [`axum::Router`]
63/// returned by [`Server::publish_router`] / [`Server::subscribe_router`] (and by
64/// [`whip::router`] / [`whep::router`]) into their own app without adding their own
65/// axum dependency (and risking a version mismatch). A major axum bump is therefore
66/// a breaking change for this crate.
67pub use axum;
68
69/// Re-export of the URL type, so consumers can build the [`url::Url`] that
70/// [`Client::subscribe`] / [`Client::publish`] dial without adding their own url
71/// dependency (and risking a version mismatch). A major url bump is therefore a
72/// breaking change for this crate.
73pub use url;
74
75pub use client::Client;
76pub use error::*;
77pub use server::{Response, Server, whep, whip};
78
79#[cfg(test)]
80mod tests {
81	use std::time::Duration;
82
83	use axum::Router;
84	use bytes::Bytes;
85
86	use crate::codec::{Bridge, Frame, Track};
87	use crate::{Client, Server, client, server};
88
89	const TIMEOUT: Duration = Duration::from_secs(10);
90	const OPUS_PACKET: &[u8] = &[0xfc, 0xff, 0xfe];
91
92	#[tokio::test]
93	async fn whip_and_whep_round_trip_opus() {
94		let source_origin = moq_net::Origin::random().produce();
95		let source_consumer = source_origin.consume();
96		let mut announcements = source_consumer.announced();
97		let mut source = source_origin
98			.create_broadcast("source", moq_net::broadcast::Route::new().with_announce(true))
99			.expect("create source broadcast");
100		let catalog = moq_mux::catalog::Producer::new(&mut source).expect("create source catalog");
101		let mut opus = crate::codec::opus::Bridge::new(source, catalog, 48_000, 2).expect("create Opus bridge");
102		Bridge::push(
103			&mut opus,
104			Frame {
105				timestamp_us: 20_000,
106				payload: Bytes::from_static(OPUS_PACKET),
107			},
108		)
109		.expect("publish source packet");
110		let announcement = tokio::time::timeout(TIMEOUT, announcements.next())
111			.await
112			.expect("source announcement timed out")
113			.expect("source origin closed");
114		assert_eq!(announcement.path.as_str(), "source");
115		assert!(announcement.broadcast.is_some(), "source was unannounced");
116		drop(announcements);
117
118		let server_origin = moq_net::Origin::random().produce();
119		let server = Server::new(
120			server::Config::default(),
121			server_origin.clone(),
122			server_origin.consume(),
123		);
124		let app = Router::new()
125			.nest("/whip", server.publish_router())
126			.nest("/whep", server.subscribe_router());
127		let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
128			.await
129			.expect("bind HTTP listener");
130		let address = listener.local_addr().expect("HTTP listener address");
131		let http = tokio::spawn(async move { axum::serve(listener, app).await.expect("serve HTTP") });
132
133		let client = Client::new(client::Config::default());
134		let whip = format!("http://{address}/whip/ingested").parse().expect("WHIP URL");
135		tokio::time::timeout(TIMEOUT, client.publish(whip, source_consumer, "source"))
136			.await
137			.expect("WHIP negotiation timed out")
138			.expect("WHIP negotiation failed");
139
140		let output_origin = moq_net::Origin::random().produce();
141		let output = output_origin
142			.create_broadcast("output", moq_net::broadcast::Route::new().with_announce(true))
143			.expect("create output broadcast");
144		let output_consumer = output.consume();
145		let whep = format!("http://{address}/whep/ingested").parse().expect("WHEP URL");
146		tokio::time::timeout(TIMEOUT, client.subscribe(whep, output))
147			.await
148			.expect("WHEP negotiation timed out")
149			.expect("WHEP negotiation failed");
150
151		let catalog_track = output_consumer
152			.track(hang::Catalog::DEFAULT_NAME)
153			.expect("output catalog track")
154			.subscribe(hang::Catalog::default_subscription())
155			.await
156			.expect("subscribe to output catalog");
157		let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(catalog_track);
158		let catalog = tokio::time::timeout(TIMEOUT, catalogs.next())
159			.await
160			.expect("output catalog timed out")
161			.expect("read output catalog")
162			.expect("output catalog ended");
163		let track_name = catalog.audio.renditions.keys().next().expect("output Opus rendition");
164		let track = output_consumer
165			.track(track_name)
166			.expect("output Opus track")
167			.subscribe(None)
168			.await
169			.expect("subscribe to output Opus track");
170		let mut opus = Track::opus(track);
171		let frame = tokio::time::timeout(TIMEOUT, opus.next())
172			.await
173			.expect("output Opus packet timed out")
174			.expect("read output Opus packet")
175			.expect("output Opus track ended");
176		assert_eq!(frame.payload.as_ref(), OPUS_PACKET);
177
178		http.abort();
179	}
180}