//! What the gateway's end-to-end tests share: a relay and a gateway on loopback, a room //! and its grants, a WebRTC client on str0m (the WHIP publisher and the WHEP player), an //! HTTP client, synthetic frames, and a relay subscriber that verifies every receipt. //! //! Every address is loopback and every wait is bounded. #![allow(dead_code)] #[cfg(feature = "st2110")] pub mod dav1d; #[cfg(feature = "telephony")] pub mod telephony; pub mod rtmp; use base64::Engine; use bytes::Bytes; use ed25519_dalek::SigningKey; use http_body_util::{BodyExt, Full}; use joule_moq::carriage::ReceiptBundle; use joule_moq::draft22::data::FetchEntry; use joule_moq::draft22::{ControlMessage, Parameters, StreamHeader, UniEvent}; use joule_moq::receipt::ReceiptVerifier; use joule_moq::{GroupId, ObjectId, TrackName, TrackNamespace}; use joule_moq_ingest::catalog; use joule_moq_ingest::frame::{self, Codec}; use joule_moq_ingest::room::{build, Descriptor, Grant}; use joule_moq_ingest::{sdp, GatewayConfig, GatewayServer}; use joule_moq_relay::client::{Client, ClientOptions}; use joule_moq_relay::transport::quic; use joule_moq_relay::wire::{ReqRx, ReqTx}; use joule_moq_relay::{RelayConfig, RelayServer}; use std::collections::{BTreeMap, HashMap}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use str0m::change::{SdpAnswer, SdpPendingOffer}; use str0m::media::{Direction, Frequency, MediaKind as RtcKind, MediaTime, Mid}; // allow-name: the str0m crate's own type name, imported once under a local alias use str0m::net::{Protocol, Receive}; use str0m::{Candidate, Event, IceConnectionState, Input, Output, Rtc, RtcConfig}; use tokio::net::UdpSocket; use tokio::sync::{mpsc, oneshot}; use tokio::time::timeout; /// The bound on any one wait. pub const STEP: Duration = Duration::from_secs(20); /// A port below the range a system hands out for port 0 (from 32768 on Linux, 49152 on /// macOS) that `free` finds free now. A port handed out for port 0 can go to any socket on the /// host the moment it is let go, so a test that lets a port go and checks at once that it is /// free, or binds it again, takes one of these instead. Each call starts its search at a place /// of its own, so tests of one binary running at once are not handed the same port. Even ports /// only, so a pair (P, P + 1) can be asked for. pub fn low_port(free: impl Fn(u16) -> bool) -> u16 { static CALLS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); let first = std::process::id() % 2_000 + 64 * CALLS.fetch_add(1, std::sync::atomic::Ordering::SeqCst); (0..6_000u32) .map(|i| (20_000 + 2 * ((first + i) % 6_000)) as u16) .find(|&p| free(p)) .expect("a free port below the range handed out for port 0") } /// A TCP port on loopback below the range handed out for port 0 ([`low_port`]). pub fn low_tcp() -> u16 { low_port(|p| std::net::TcpListener::bind(("127.0.0.1", p)).is_ok()) } /// A UDP port on loopback below the range handed out for port 0 ([`low_port`]). pub fn low_udp() -> u16 { low_port(|p| std::net::UdpSocket::bind(("127.0.0.1", p)).is_ok()) } /// An even UDP port P on loopback, P and P + 1 both free, below the range handed out for port /// 0 ([`low_port`]): a RIST Simple Profile listener's pair. pub fn low_pair() -> u16 { low_port(|p| std::net::UdpSocket::bind(("127.0.0.1", p)).is_ok() && std::net::UdpSocket::bind(("127.0.0.1", p + 1)).is_ok()) } pub fn now_us() -> u64 { SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_micros() as u64 } // ── The room, the relay and the gateway ──────────────────────────────────────────── pub struct World { pub relay: RelayServer, pub gateway: GatewayServer, pub authority: SigningKey, pub descriptor: Descriptor, pub room: [u8; 32], pub publisher: SigningKey, pub gateway_key: SigningKey, } impl World { /// A relay whose door checks every receipt of the room's tracks under the gateway's /// key, and a gateway that publishes to it, with `f` adjusting the gateway's /// configuration. pub async fn start(f: impl FnOnce(&mut GatewayConfig)) -> World { World::start_with(|_, _, _| {}, f).await } /// As [`World::start`], with `fr` adjusting the relay's configuration once its door /// rule is in place. pub async fn start_with(fr: impl FnOnce(&mut RelayConfig, &[u8; 32], &str), f: impl FnOnce(&mut GatewayConfig)) -> World { World::start_room(|_| {}, fr, f).await } /// As [`World::start_with`], with `fd` adjusting the room's descriptor before the /// authority signs it. pub async fn start_room(fd: impl FnOnce(&mut Descriptor), fr: impl FnOnce(&mut RelayConfig, &[u8; 32], &str), f: impl FnOnce(&mut GatewayConfig)) -> World { let authority = SigningKey::from_bytes(&[0x51; 32]); let publisher = SigningKey::from_bytes(&[0x52; 32]); let gateway_key = SigningKey::from_bytes(&[0x53; 32]); let mut descriptor = build::room(&authority, now_us() - 60_000_000); fd(&mut descriptor); let descriptor = descriptor.sign(&authority); let room = descriptor.room_id(); let gk = hex::encode(gateway_key.verifying_key().to_bytes()); let mut rc = RelayConfig::loopback(); rc.receipts.door = vec![joule_moq_relay::config::DoorRule { prefix: vec![hex::encode(room)], trust: vec![gk.clone()], trust_field: None, hold_ms: 10_000 }]; fr(&mut rc, &room, &gk); let relay = RelayServer::start(rc).await.expect("the relay starts"); let mut gc = GatewayConfig::loopback(relay.quic_addr.expect("a UDP endpoint")); gc.relay.trust_der = vec![relay.identity.chain[0].to_vec()]; gc.signing.key_seed = Some(gateway_key.to_bytes()); gc.rooms = vec![joule_moq_ingest::config::RoomConfig { descriptor_path: None, revoked: Vec::new(), descriptor: Some(descriptor.to_bytes()) }]; f(&mut gc); let gateway = GatewayServer::start(gc).await.expect("the gateway starts"); World { relay, gateway, authority, descriptor, room, publisher, gateway_key } } /// A grant for `who` under role `role` (0 publisher, 1 viewer, 2 observer), good now. pub fn grant(&self, who: &SigningKey, role: u8) -> Grant { let s = now_us() / 1_000_000; build::grant(&self.descriptor, &self.authority, &who.verifying_key().to_bytes(), role, s - 60, s + 3600) } pub fn token(g: &Grant) -> String { base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(g.to_bytes()) } /// `who`'s grant under `role`, with its key, for requests that prove possession. pub fn auth(&self, who: &SigningKey, role: u8) -> Auth { Auth::new(self.grant(who, role), who) } pub fn base(&self) -> String { format!("http://{}", self.gateway.addr) } pub fn owner(&self) -> String { hex::encode(self.publisher.verifying_key().to_bytes()) } pub fn whep_url(&self) -> String { format!("{}/whep/{}/{}", self.base(), hex::encode(self.room), self.owner()) } pub async fn stop(self) { self.gateway.shutdown().await; self.relay.shutdown().await; } } // ── HTTP ─────────────────────────────────────────────────────────────────────────── #[derive(Debug)] pub struct Resp { pub status: u16, pub headers: HashMap, pub body: Vec, } impl Resp { pub fn header(&self, k: &str) -> Option<&str> { self.headers.get(&k.to_ascii_lowercase()).map(String::as_str) } pub fn text(&self) -> String { String::from_utf8_lossy(&self.body).into_owned() } } /// One HTTP/1.1 request on a connection of its own. pub async fn http(method: &str, url: &str, headers: &[(&str, &str)], body: &[u8]) -> Resp { let rest = url.strip_prefix("http://").expect("an http URL"); let (host, path) = rest.split_at(rest.find('/').unwrap_or(rest.len())); let path = if path.is_empty() { "/" } else { path }; let stream = timeout(STEP, tokio::net::TcpStream::connect(host)).await.expect("connect in time").expect("connect"); let (mut sender, conn) = hyper::client::conn::http1::handshake(hyper_util::rt::TokioIo::new(stream)).await.expect("handshake"); tokio::spawn(conn); let mut b = hyper::Request::builder().method(method).uri(path).header("host", host); for (k, v) in headers { b = b.header(*k, *v); } let req = b.body(Full::new(Bytes::copy_from_slice(body))).unwrap(); let resp = timeout(STEP, sender.send_request(req)).await.expect("a response in time").expect("a response"); let status = resp.status().as_u16(); let headers = resp.headers().iter().map(|(k, v)| (k.as_str().to_ascii_lowercase(), v.to_str().unwrap_or("").to_string())).collect(); let body = timeout(STEP, resp.into_body().collect()).await.expect("a body in time").expect("a body").to_bytes().to_vec(); Resp { status, headers, body } } pub fn bearer(token: &str) -> String { format!("Bearer {token}") } /// A principal's grant and key, and the last challenge the gateway gave it: what a client /// presents over HTTP (live-rooms §10.5.3). pub struct Auth { pub grant: Grant, pub key: SigningKey, challenge: Mutex>, } /// `METHOD //authority/path` of a URL, without its query (live-rooms §10.5.1). pub fn context_of(method: &str, url: &str) -> String { let rest = url.strip_prefix("http://").expect("an http URL"); let (host, path) = rest.split_at(rest.find('/').unwrap_or(rest.len())); joule_moq::grant_proof::context(method, host, if path.is_empty() { "/" } else { path }) } impl Auth { pub fn new(grant: Grant, key: &SigningKey) -> Auth { Auth { grant, key: key.clone(), challenge: Mutex::new(None) } } pub fn token(&self) -> String { World::token(&self.grant) } pub fn challenge(&self) -> Option<[u8; 32]> { *self.challenge.lock().unwrap() } /// A proof by `key` over `challenge` for `method` at `url`, in base64url. pub fn proof(&self, key: &SigningKey, challenge: [u8; 32], method: &str, url: &str) -> String { let p = joule_moq::grant_proof::GrantProof::sign(self.grant.digest(), key, challenge, &context_of(method, url)).expect("a proof"); joule_moq::grant_proof::base64url(&p.to_bytes()) } fn keep(&self, r: &Resp) { if let Some(c) = r.header("wai-room-challenge").and_then(joule_moq::grant_proof::from_base64url).and_then(|b| <[u8; 32]>::try_from(b).ok()) { *self.challenge.lock().unwrap() = Some(c); } } /// The request as given: the grant as the Bearer token and `proof`, if any. The /// response's challenge is kept. pub async fn send_raw(&self, method: &str, url: &str, headers: &[(&str, &str)], body: &[u8], proof: Option) -> Resp { let auth = bearer(&self.token()); let mut h: Vec<(&str, &str)> = headers.to_vec(); h.push(("authorization", &auth)); if let Some(p) = &proof { h.push(("wai-room-proof", p)); } let r = http(method, url, &h, body).await; self.keep(&r); r } /// The request with a proof over the last challenge; and, where it is answered `401` /// with a fresh challenge, once more over that one, as a client does that has none yet. pub async fn send(&self, method: &str, url: &str, headers: &[(&str, &str)], body: &[u8]) -> Resp { let proof = self.challenge().map(|c| self.proof(&self.key, c, method, url)); let r = self.send_raw(method, url, headers, body, proof).await; if r.status != 401 || r.header("wai-room-challenge").is_none() || !r.text().contains("pop_") { return r; } let c = self.challenge().expect("a challenge"); let proof = self.proof(&self.key, c, method, url); self.send_raw(method, url, headers, body, Some(proof)).await } } // ── A WebRTC client on str0m ─────────────────────────────────────────────────────── #[derive(Debug, Clone)] pub enum PeerEvent { Connected, Ice(IceConnectionState), /// A frame received: its kind, its codec (where the gateway carries it), its bytes and /// its RTP time. Media { kind: RtcKind, codec: Option, data: Vec, rtp: u64 }, } enum PeerCmd { Write { kind: RtcKind, codec: Codec, data: Vec, ticks: u64 }, /// Start an ICE restart: the new offer, whose ICE attributes go in the PATCH. Restart { reply: oneshot::Sender }, /// Finish it with the answer fragment the gateway returned. Restarted { frag: String, reply: oneshot::Sender> }, Close, } /// A WebRTC endpoint driven on its own task, on a loopback UDP socket. pub struct Peer { cmd: mpsc::UnboundedSender, pub events: Arc>>, pub offer: String, pub answer: String, pub location: String, pub etag: String, } fn client_config(codecs: &[Codec]) -> RtcConfig { let mut c = RtcConfig::new().clear_codecs().set_crypto_provider(Arc::new(str0m::crypto::from_feature_flags())); for codec in codecs { c = match codec { Codec::Avc => c.enable_h264(true), Codec::Vp8 => c.enable_vp8(true), Codec::Vp9 => c.enable_vp9(true), Codec::Av1 => c.enable_av1(true), Codec::Opus => c.enable_opus(true, false), _ => c, }; } c } /// An offer for one audio and one video section in `dir`, on a new endpoint; the /// endpoint's run loop starts once its answer is applied. pub struct Offering { rtc: Rtc, socket: UdpSocket, pending: SdpPendingOffer, /// The sections this end added: str0m raises no MediaAdded for its own. mids: HashMap, pub offer: String, } pub async fn offer(codecs: &[Codec], dir: Direction, audio: bool, video: bool) -> Offering { let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let local = socket.local_addr().unwrap(); let mut rtc = client_config(codecs).build(Instant::now()); rtc.add_local_candidate(Candidate::host(local, "udp").unwrap()); let mut api = rtc.sdp_api(); let mut mids = HashMap::new(); if audio { mids.insert(RtcKind::Audio, api.add_media(RtcKind::Audio, dir, Some("stream".into()), Some("audio".into()), None)); } if video { mids.insert(RtcKind::Video, api.add_media(RtcKind::Video, dir, Some("stream".into()), Some("video".into()), None)); } let (o, pending) = api.apply().expect("an offer"); Offering { rtc, socket, pending, mids, offer: o.to_sdp_string() } } impl Offering { /// Apply the gateway's answer and start the endpoint. pub fn start(mut self, resp: &Resp) -> Peer { let answer = resp.text(); let a = SdpAnswer::from_sdp_string(&answer).expect("the answer parses"); self.rtc.sdp_api().accept_answer(self.pending, a).expect("the answer applies"); let (tx, rx) = mpsc::unbounded_channel(); let events = Arc::new(Mutex::new(Vec::new())); tokio::spawn(run_peer(self.rtc, self.socket, rx, events.clone(), answer.clone(), self.mids)); let location = resp.header("location").expect("a Location").to_string(); let etag = resp.header("etag").expect("an ETag").trim_matches('"').to_string(); Peer { cmd: tx, events, offer: self.offer, answer, location, etag } } } async fn run_peer(mut rtc: Rtc, socket: UdpSocket, mut rx: mpsc::UnboundedReceiver, events: Arc>>, mut answer: String, mut mids: HashMap) { let local = socket.local_addr().unwrap(); let mut buf = vec![0u8; 2048]; let mut restart: Option = None; loop { let next = loop { match rtc.poll_output() { Ok(Output::Timeout(t)) => break t, Ok(Output::Transmit(t)) => { let _ = socket.try_send_to(&t.contents, t.destination); } Ok(Output::Event(e)) => { let mut ev = events.lock().unwrap(); match e { Event::Connected => ev.push(PeerEvent::Connected), Event::IceConnectionStateChange(s) => ev.push(PeerEvent::Ice(s)), Event::MediaAdded(m) => { mids.insert(m.kind, m.mid); } Event::MediaData(d) => { let kind = if d.params.spec().codec.is_audio() { RtcKind::Audio } else { RtcKind::Video }; ev.push(PeerEvent::Media { kind, codec: Codec::from_str0m(d.params.spec().codec), data: d.data.to_vec(), rtp: d.time.numer() }); } _ => {} } } Err(_) => return, } }; let sleep = tokio::time::sleep_until(tokio::time::Instant::from_std(next.max(Instant::now()))); tokio::select! { r = socket.recv_from(&mut buf) => { if let Ok((n, from)) = r && let Ok(r) = Receive::new(Protocol::Udp, from, local, &buf[..n]) { let _ = rtc.handle_input(Input::Receive(Instant::now(), r)); } } _ = sleep => { let _ = rtc.handle_input(Input::Timeout(Instant::now())); } c = rx.recv() => match c { None | Some(PeerCmd::Close) => { rtc.disconnect(); let _ = rtc.poll_output(); return; } Some(PeerCmd::Write { kind, codec, data, ticks }) => { let Some(&mid) = mids.get(&kind) else { continue }; let Some(pt) = rtc.writer(mid).and_then(|w| w.payload_params().find(|p| Some(p.spec().codec) == codec.to_str0m()).map(|p| p.pt())) else { continue }; let freq = if kind == RtcKind::Audio { Frequency::FORTY_EIGHT_KHZ } else { Frequency::NINETY_KHZ }; if let Some(w) = rtc.writer(mid) { let _ = w.write(pt, Instant::now(), MediaTime::new(ticks, freq), data); } } Some(PeerCmd::Restart { reply }) => { let mut api = rtc.sdp_api(); api.ice_restart(true); let (o, pending) = api.apply().expect("an ICE restart offer"); restart = Some(pending); let _ = reply.send(o.to_sdp_string()); } Some(PeerCmd::Restarted { frag, reply }) => { let r = (|| { let f = sdp::parse_frag(&frag).ok_or("the fragment reads")?; let (u, p) = (f.ufrag.ok_or("an ice-ufrag")?, f.pwd.ok_or("an ice-pwd")?); let a = sdp::restart_answer(&answer, &u, &p, &f.candidates); let parsed = SdpAnswer::from_sdp_string(&a).map_err(|e| e.to_string())?; let pending = restart.take().ok_or("a restart pending")?; rtc.sdp_api().accept_answer(pending, parsed).map_err(|e| e.to_string())?; answer = a; Ok::<(), String>(()) })(); let _ = reply.send(r.map_err(|e| e.to_string())); } } } } } impl Peer { pub fn write(&self, kind: RtcKind, codec: Codec, data: Vec, ticks: u64) { let _ = self.cmd.send(PeerCmd::Write { kind, codec, data, ticks }); } pub async fn restart_offer(&self) -> String { let (tx, rx) = oneshot::channel(); self.cmd.send(PeerCmd::Restart { reply: tx }).unwrap(); timeout(STEP, rx).await.expect("in time").expect("an offer") } pub async fn restarted(&self, frag: String) -> Result<(), String> { let (tx, rx) = oneshot::channel(); self.cmd.send(PeerCmd::Restarted { frag, reply: tx }).unwrap(); timeout(STEP, rx).await.expect("in time").expect("a result") } pub fn close(&self) { let _ = self.cmd.send(PeerCmd::Close); } pub fn count(&self, f: impl Fn(&PeerEvent) -> bool) -> usize { self.events.lock().unwrap().iter().filter(|e| f(e)).count() } pub async fn connected(&self) { assert!(until(STEP, || self.count(|e| matches!(e, PeerEvent::Connected)) > 0).await, "the WebRTC session connects"); } pub fn media(&self, kind: RtcKind) -> Vec> { self.events.lock().unwrap().iter().filter_map(|e| if let PeerEvent::Media { kind: k, data, .. } = e { (*k == kind).then(|| data.clone()) } else { None }).collect() } /// The codecs the frames of `kind` received came in, each once, in the order first seen. pub fn codecs(&self, kind: RtcKind) -> Vec> { let mut out = Vec::new(); for e in self.events.lock().unwrap().iter() { if let PeerEvent::Media { kind: k, codec, .. } = e && *k == kind && !out.contains(codec) { out.push(*codec); } } out } } /// Wait, bounded, until `f` holds. pub async fn until(limit: Duration, mut f: impl FnMut() -> bool) -> bool { let deadline = Instant::now() + limit; while !f() { if Instant::now() >= deadline { return false; } tokio::time::sleep(Duration::from_millis(20)).await; } true } /// POST a WHIP offer with `token` and start the publisher. pub async fn publish(w: &World, auth: &Auth) -> Peer { publish_with(w, auth, &[Codec::Avc, Codec::Opus], true, true).await } /// As [`publish`], offering `codecs`, and the sections asked for. pub async fn publish_with(w: &World, auth: &Auth, codecs: &[Codec], audio: bool, video: bool) -> Peer { let o = offer(codecs, Direction::SendOnly, audio, video).await; let resp = auth.send("POST", &format!("{}/whip", w.base()), &[("content-type", "application/sdp")], o.offer.as_bytes()).await; assert_eq!(resp.status, 201, "WHIP POST: {}", resp.text()); assert_eq!(resp.header("content-type"), Some("application/sdp")); assert!(resp.header("wai-room-challenge").is_some(), "a challenge for the session's next request"); let p = o.start(&resp); p.connected().await; p } // ── Synthetic media ──────────────────────────────────────────────────────────────── /// A body of `n` bytes, none zero, so no start code appears inside a NAL unit. fn body(seed: u64, n: usize) -> Vec { (0..n).map(|j| ((seed.wrapping_mul(31).wrapping_add(j as u64 * 7)) % 255 + 1) as u8).collect() } /// H.264 access unit `i`: an IDR with its SPS and PPS when `key`, a non-IDR slice /// otherwise, each NAL unit after a four-byte start code. Large enough to need FU-A. pub fn avc(i: u64, key: bool) -> Vec { let mut f = Vec::new(); if key { f.extend_from_slice(&[0, 0, 0, 1, 0x67, 0x42, 0xe0, 0x1f, 0x8c, 0x8d, 0x40]); f.extend_from_slice(&[0, 0, 0, 1, 0x68, 0xce, 0x3c, 0x80]); f.extend_from_slice(&[0, 0, 0, 1, 0x65]); f.extend_from_slice(&body(i, 3000)); } else { f.extend_from_slice(&[0, 0, 0, 1, 0x41]); f.extend_from_slice(&body(i, 700 + (i % 5) as usize * 100)); } f } /// Opus packet `i`: a fullband CELT 20 ms stereo TOC, then bytes. pub fn opus(i: u64) -> Vec { let mut f = vec![0xfc]; f.extend_from_slice(&body(i + 7_000, 80 + (i % 7) as usize)); f } /// What a publisher sent, in order. #[derive(Default, Clone)] pub struct Sent { pub video: Vec>, pub audio: Vec>, } /// Send `seconds` of media in real time: 30 video frames a second, a keyframe every /// `gop`, and 50 audio packets a second. Continues from what `sent` holds. pub async fn send(p: &Peer, sent: &mut Sent, seconds: u64, gop: u64) { let start = Instant::now(); let v0 = sent.video.len() as u64; let a0 = sent.audio.len() as u64; let (nv, na) = (30 * seconds, 50 * seconds); let (mut v, mut a) = (0, 0); while v < nv || a < na { let tv = Duration::from_micros(v * 33_333); let ta = Duration::from_micros(a * 20_000); let next = tv.min(ta); tokio::time::sleep_until(tokio::time::Instant::from_std(start + next)).await; if v < nv && tv <= ta { let i = v0 + v; let f = avc(i, i.is_multiple_of(gop)); p.write(RtcKind::Video, Codec::Avc, f.clone(), 1_000 + i * 3000); sent.video.push(f); v += 1; } else { let i = a0 + a; let f = opus(i); p.write(RtcKind::Audio, Codec::Opus, f.clone(), 5_000 + i * 960); sent.audio.push(f); a += 1; } } } // ── A subscriber at the relay that verifies every receipt ───────────────────────── pub async fn relay_client(w: &World) -> (Client, quinn::Endpoint) { let ep = quic::client_endpoint("127.0.0.1:0".parse().unwrap(), vec![w.relay.identity.chain[0].clone()], vec![joule_moq::draft22::ALPN.to_vec()], None).unwrap(); let conn = timeout(STEP, quic::connect(&ep, w.relay.quic_addr.unwrap(), "localhost")).await.expect("in time").expect("a QUIC session"); let c = Client::connect(Arc::new(conn), ClientOptions::default()).await.expect("SETUP"); (c, ep) } /// One subscribed media track and its receipts track, as the subscriber reads them. pub struct Watched { pub kind: u8, /// The layer: the Track Name, where it is a number (a layer); 0 for a track of /// another name, such as the link track. pub layer: u8, pub media_alias: u64, pub receipts_alias: u64, /// The request streams, held so the subscriptions stand. pub held: Vec, pub receipts_rx: ReqRx, } /// What a subscriber read: each object by (Track Alias, group, object). pub type Objects = Arc>>>; pub struct Watcher { pub client: Arc, pub _ep: quinn::Endpoint, pub tracks: Vec, pub objects: Objects, pub ended: Arc>>, } /// Subscribe at the relay to the owner's audio and video tracks and their receipts /// tracks, from their start, reading every stream concurrently. pub async fn watch(w: &World) -> Watcher { watch_kinds(w, &[1, 2]).await } /// As [`watch`], for the media kinds `kinds` (1 audio, 2 video). pub async fn watch_kinds(w: &World, kinds: &[u8]) -> Watcher { let t: Vec<(u8, u8)> = kinds.iter().map(|&k| (k, 0)).collect(); watch_layers(w, &t).await } /// As [`watch`], for the (media kind, layer) tracks `tracks_wanted`. pub async fn watch_layers(w: &World, tracks_wanted: &[(u8, u8)]) -> Watcher { let owner = w.owner(); let specs = tracks_wanted.iter().map(|&(k, layer)| (k, catalog::media_namespace(&w.room, &owner, k), layer.to_string())).collect(); watch_tracks(w, specs).await } /// Subscribe at the relay to each (kind, namespace, name) and its receipts track, from /// their start, reading every stream concurrently. pub async fn watch_tracks(w: &World, specs: Vec<(u8, TrackNamespace, String)>) -> Watcher { let (c, ep) = relay_client(w).await; let c = Arc::new(c); let mut tracks = Vec::new(); let ended = Arc::new(Mutex::new(Vec::new())); for (kind, ns, name) in specs { let layer: u8 = name.parse().unwrap_or(0); let (m, mtx, mrx) = c.subscribe(ns.clone(), TrackName::from_str(&name), Parameters::new()).await.expect("SUBSCRIBE the media track"); let (r, rtx, rrx) = c.subscribe(ns.clone(), catalog::receipts_name(&name), Parameters::new()).await.expect("SUBSCRIBE the receipts track"); // PUBLISH_DONE on the media track: the track ended. let ended = ended.clone(); let alias = m.track_alias; let mut mrx = mrx; tokio::spawn(async move { while let Ok(Some(msg)) = mrx.next().await { if let ControlMessage::PublishDone(_) = msg { ended.lock().unwrap().push(alias); return; } } }); tracks.push(Watched { kind, layer, media_alias: m.track_alias, receipts_alias: r.track_alias, held: vec![mtx, rtx], receipts_rx: rrx }); } let objects: Objects = Arc::default(); let (c2, o2) = (c.clone(), objects.clone()); tokio::spawn(async move { loop { let Some(mut r) = c2.next_stream(Duration::from_secs(1)).await else { if Arc::strong_count(&o2) == 1 { return; } continue; }; let o2 = o2.clone(); tokio::spawn(async move { let Ok(Ok(h)) = timeout(STEP, r.start()).await else { return }; while let Ok(Ok(Some(e))) = timeout(STEP, r.next()).await { match (&h, e) { (StreamHeader::Subgroup(sh), UniEvent::SubgroupObject(o)) if !o.payload.is_empty() => { o2.lock().unwrap().insert((sh.track_alias, sh.group_id, o.object_id), o.payload); } (StreamHeader::Fetch(_), UniEvent::FetchEntry(FetchEntry::Object(_))) => {} _ => {} } } }); } }); Watcher { client: c, _ep: ep, tracks, objects, ended } } /// Every media object of `t` verified against its receipts under `key`: returns the /// frames, in group and object order, and the number of groups whose receipts verified. pub fn verified(wt: &Watcher, t: &Watched, key: &SigningKey, codec: Codec) -> (Vec>, usize) { let v = ReceiptVerifier::with_anchors(vec![key.verifying_key()]); let objs = wt.objects.lock().unwrap(); let mut bundles = BTreeMap::new(); for ((alias, g, _), p) in objs.iter() { if *alias == t.receipts_alias { let b = ReceiptBundle::from_payload(p).expect("a receipts bundle"); let vg = v.verify_group(&b.group).expect("the group receipt verifies under the gateway's key"); assert_eq!(b.group.group_id, GroupId(*g), "a receipt names its MOQT group"); bundles.insert(*g, (b, vg)); } } let mut frames = Vec::new(); let mut prev: Option<(u64, [u8; 32])> = None; for (g, (b, _)) in &bundles { if let Some((pg, link)) = prev && pg + 1 == *g { assert_eq!(b.group.parent_receipt_hash, Some(link), "group {g} chains to group {pg}"); } prev = Some((*g, b.group.link_hash())); } for ((alias, g, o), p) in objs.iter() { if *alias != t.media_alias { continue; } let (b, vg) = bundles.get(g).unwrap_or_else(|| panic!("the receipts of group {g} arrived")); let r = &b.objects[*o as usize]; r.check_moqt(b.group.track_alias, GroupId(*g), ObjectId(*o)).expect("the receipt names its object"); v.verify_payload(p, r, vg).expect("every object verifies against its signed root"); let opened = frame::open(p).expect("an object is one WAI1 envelope"); assert_eq!(opened.codec, codec); frames.push(frame::unframe(codec, &opened.payload).expect("one frame in its framing")); } // Every object every receipt names arrived. let named: usize = bundles.values().map(|(b, _)| b.objects.len()).sum(); assert_eq!(named, frames.len(), "every receipted object arrived"); (frames, bundles.len()) } pub fn ns_of(w: &World, kind: u8) -> TrackNamespace { catalog::media_namespace(&w.room, &w.owner(), kind) } /// Wait, bounded, until the subscriber has read nothing new for `quiet`. pub async fn settle(wt: &Watcher, quiet: Duration) { let deadline = Instant::now() + STEP; let mut last = (wt.objects.lock().unwrap().len(), Instant::now()); while Instant::now() < deadline { tokio::time::sleep(Duration::from_millis(50)).await; let n = wt.objects.lock().unwrap().len(); if n != last.0 { last = (n, Instant::now()); } else if last.1.elapsed() >= quiet { return; } } } /// Wait, bounded, until the gateway's WHIP session has taken `n` frames: everything sent /// has arrived before the session ends. pub async fn received(w: &World, p: &Peer, n: usize) -> bool { let id = p.location.rsplit('/').next().unwrap().to_string(); until(STEP, || w.gateway.gateway.session_frames(false, &id).is_some_and(|f| f as usize >= n)).await } /// `got` is a run of `sent`, from where its first element is: that index. pub fn run_of(sent: &[Vec], got: &[Vec]) -> usize { assert!(!got.is_empty(), "something arrived"); let j = sent.iter().position(|f| *f == got[0]).expect("the first frame received is one that was sent"); assert!(j + got.len() <= sent.len(), "more frames received than were sent after the first: {} from {} of {}", got.len(), j, sent.len()); same(got, &sent[j..j + got.len()], "the frames received are the frames sent, in order, byte for byte, from the first received"); j } /// The payloads of track `t`'s media objects, in group and object order, as envelopes. pub fn envelopes(wt: &Watcher, t: &Watched) -> Vec { wt.objects.lock().unwrap().iter().filter(|(k, _)| k.0 == t.media_alias).map(|(_, p)| frame::open(p).expect("an envelope")).collect() } /// `got` equals `want`, frame for frame; on failure, says where they part, not every byte. pub fn same(got: &[Vec], want: &[Vec], what: &str) { if got == want { return; } let at = got.iter().zip(want).position(|(a, b)| a != b); panic!("{what}: {} received, {} sent, first difference at {:?}", got.len(), want.len(), at); } /// DELETE a session's resource with `token`. pub async fn delete(w: &World, location: &str, auth: &Auth) -> Resp { auth.send("DELETE", &format!("{}{}", w.base(), location), &[], b"").await } // ── A transport stream over SRT or RIST ─────────────────────────────────────────── /// An ADTS frame of AAC-LC at 48 kHz, stereo: a header and `i`-shaped bytes. pub fn adts(i: u64) -> Vec { let body: Vec = (0..(150 + i % 40)).map(|j| ((i * 7 + j) % 200 + 11) as u8).collect(); let len = body.len() + 7; let mut f = vec![0xff, 0xf1, (1 << 6) | (3 << 2), (2 << 6) | ((len >> 11) as u8 & 0x3), (len >> 3) as u8, ((len as u8 & 0x7) << 5) | 0x1f, 0xfc]; f.extend(body); f } /// An HEVC access unit: an IDR (type 19) with a VPS, SPS and PPS before it when `key`, a /// TRAIL_R (type 1) otherwise. pub fn hevc(i: u64, key: bool) -> Vec { let mut f = Vec::new(); if key { for t in [32u8, 33, 34] { f.extend_from_slice(&[0, 0, 0, 1, t << 1, 0x01, 0x0c, 0x01]); } f.extend_from_slice(&[0, 0, 0, 1, 19 << 1, 0x01]); } else { f.extend_from_slice(&[0, 0, 0, 1, 1 << 1, 0x01]); } f.extend((0..(400 + i % 9 * 30)).map(|j| ((i * 31 + j * 3) % 251 + 1) as u8)); f } pub struct SentTs { pub video: Vec>, pub audio: Vec>, } /// Send `seconds` of a program in real time on `link`: 30 video access units a second /// (`video_type`, a keyframe every `gop`) and 25 PES of two ADTS frames, in TS packets /// seven to a payload. pub async fn send_ts(link: &joule_contrib::link::Link, seconds: u64, gop: u64, video_type: u8) -> SentTs { let tx = link.send.clone().expect("a sending link"); let mut m = joule_contrib::ts::Mux::new(video_type); let mut pending = Vec::new(); let mut sent = SentTs { video: Vec::new(), audio: Vec::new() }; let start = tokio::time::Instant::now(); let mut a = 0u64; for i in 0..seconds * 30 { tokio::time::sleep_until(start + Duration::from_micros(i * 33_333)).await; let key = i % gop == 0; let au = if video_type == joule_contrib::ts::ST_HEVC { hevc(i, key) } else { avc(i, key) }; pending.extend(m.pes(true, 900_000 + i * 3000, &au, key)); sent.video.push(au); while a * 3840 <= i * 3000 { let (f0, f1) = (adts(2 * a), adts(2 * a + 1)); pending.extend(m.pes(false, 900_000 + a * 3840, &[f0.clone(), f1.clone()].concat(), false)); sent.audio.push(f0); sent.audio.push(f1); a += 1; } while pending.len() >= 1316 { let c: Vec = pending.drain(..1316).collect(); tx.send(c).await.expect("the link takes the payload"); } } // What is left, and the last PES ended by a packet of the PAT. pending.extend(m.psi()); for c in pending.chunks(1316) { tx.send(c.to_vec()).await.unwrap(); } sent } /// Wait, bounded, until the gateway's contribution session `id` has published its tracks. pub async fn contrib_live(w: &World, id: &str) -> bool { until(STEP, || w.gateway.gateway.contrib.sessions.lock().unwrap().contains_key(id)).await }