Gateway: a challenge store (issued per endpoint or session, consumed, kept for replay detection), joule_moq::grant_proof::check_proof on every request that carries a grant (POST, and PATCH, DELETE, GET and HEAD on session URLs, GET and HEAD on endpoints when a grant is carried), a fresh WAI-Room-Challenge on every response to such a request, the two fields in CORS, admission's single-use check only on a request that opens a session. Receipts tracks renamed *_receipts. Tests: an Auth helper that proves possession; whip_publish, whep_play and codecs moved onto it. auth_and_ice.rs is still the pre-proof version and does not build against the new helpers; room_corpus does not yet run the corpus's proofs. Not built, not tested, not pushed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
171 lines
6.4 KiB
Rust
171 lines
6.4 KiB
Rust
//! VP8, VP9 and AV1 through WHIP: each a video-only publisher, its frames depacketised by
|
|
//! the WebRTC stack, packaged in its capability's framing (an IVF frame for VP8 and VP9,
|
|
//! SPEC §5's AV1 framing for AV1), received at the relay with every receipt verified,
|
|
//! and the same bytes the publisher sent.
|
|
|
|
mod common;
|
|
|
|
use common::*;
|
|
use joule_moq_ingest::frame::{self, Codec};
|
|
use std::time::Duration;
|
|
use str0m::media::MediaKind;
|
|
use tokio::time::timeout;
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn vp8_frames_arrive_whole_in_ivf() {
|
|
timeout(Duration::from_secs(120), video_only(Codec::Vp8, (640, 360))).await.expect("the test finishes in time");
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn vp9_frames_arrive_whole_in_ivf() {
|
|
timeout(Duration::from_secs(120), video_only(Codec::Vp9, (1280, 720))).await.expect("the test finishes in time");
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn av1_temporal_units_arrive_whole() {
|
|
timeout(Duration::from_secs(120), video_only(Codec::Av1, (1280, 720))).await.expect("the test finishes in time");
|
|
}
|
|
|
|
fn body(seed: u64, n: usize) -> Vec<u8> {
|
|
(0..n).map(|j| ((seed.wrapping_mul(31).wrapping_add(j as u64 * 7)) % 255 + 1) as u8).collect()
|
|
}
|
|
|
|
/// Bits, most significant first.
|
|
struct W(Vec<bool>);
|
|
impl W {
|
|
fn put(&mut self, v: u64, n: usize) {
|
|
for i in (0..n).rev() {
|
|
self.0.push((v >> i) & 1 == 1);
|
|
}
|
|
}
|
|
fn bytes(mut self) -> Vec<u8> {
|
|
while !self.0.len().is_multiple_of(8) {
|
|
self.0.push(false);
|
|
}
|
|
self.0.chunks(8).map(|c| c.iter().fold(0u8, |a, &b| (a << 1) | b as u8)).collect()
|
|
}
|
|
}
|
|
|
|
fn leb128(mut v: usize) -> Vec<u8> {
|
|
let mut o = Vec::new();
|
|
loop {
|
|
let b = (v & 0x7f) as u8;
|
|
v >>= 7;
|
|
if v == 0 {
|
|
o.push(b);
|
|
return o;
|
|
}
|
|
o.push(b | 0x80);
|
|
}
|
|
}
|
|
|
|
fn obu(ty: u8, payload: &[u8]) -> Vec<u8> {
|
|
let mut o = vec![(ty << 3) | 0x02];
|
|
o.extend(leb128(payload.len()));
|
|
o.extend_from_slice(payload);
|
|
o
|
|
}
|
|
|
|
/// Frame `i` of `codec`: a keyframe when `key`, with a header that states `size`.
|
|
fn video(codec: Codec, i: u64, key: bool, size: (u32, u32)) -> Vec<u8> {
|
|
let n = if key { 2500 } else { 600 + (i % 4) as usize * 50 };
|
|
match codec {
|
|
Codec::Vp8 => {
|
|
// RFC 6386 §9.1: the frame tag (bit 0 clear on a keyframe, show_frame set), and
|
|
// on a keyframe the start code and the size.
|
|
let mut f = vec![if key { 0x10 } else { 0x11 }, 0x02, 0x00];
|
|
if key {
|
|
f.extend_from_slice(&[0x9d, 0x01, 0x2a]);
|
|
f.extend_from_slice(&(size.0 as u16).to_le_bytes());
|
|
f.extend_from_slice(&(size.1 as u16).to_le_bytes());
|
|
}
|
|
f.extend(body(i, n));
|
|
f
|
|
}
|
|
Codec::Vp9 => {
|
|
// The uncompressed header: frame_marker, profile 0, show_existing_frame 0,
|
|
// frame_type, show_frame 1, error_resilient_mode 0; a keyframe's sync code,
|
|
// color_config (BT.601, studio range) and frame_size.
|
|
let mut w = W(Vec::new());
|
|
w.put(2, 2);
|
|
w.put(0, 2);
|
|
w.put(0, 1);
|
|
w.put(if key { 0 } else { 1 }, 1);
|
|
w.put(1, 1);
|
|
w.put(0, 1);
|
|
if key {
|
|
w.put(0x49_83_42, 24);
|
|
w.put(1, 3);
|
|
w.put(0, 1);
|
|
w.put(size.0 as u64 - 1, 16);
|
|
w.put(size.1 as u64 - 1, 16);
|
|
}
|
|
let mut f = w.bytes();
|
|
f.extend(body(i, n));
|
|
f
|
|
}
|
|
Codec::Av1 => {
|
|
let mut tu = Vec::new();
|
|
if key {
|
|
// A sequence header: profile 0, one operating point at level 8 tier 0,
|
|
// and the largest frame `size`.
|
|
let mut w = W(Vec::new());
|
|
w.put(0, 3);
|
|
w.put(0, 1);
|
|
w.put(0, 1);
|
|
w.put(0, 1);
|
|
w.put(0, 1);
|
|
w.put(0, 5);
|
|
w.put(0, 12);
|
|
w.put(8, 5);
|
|
w.put(0, 1);
|
|
w.put(10, 4);
|
|
w.put(10, 4);
|
|
w.put(size.0 as u64 - 1, 11);
|
|
w.put(size.1 as u64 - 1, 11);
|
|
tu.extend(obu(1, &w.bytes()));
|
|
}
|
|
tu.extend(obu(6, &body(i, n)));
|
|
tu
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
async fn video_only(codec: Codec, size: (u32, u32)) {
|
|
let w = World::start(|_| {}).await;
|
|
let auth = w.auth(&w.publisher, 0);
|
|
let p = publish_with(&w, &auth, &[codec], false, true).await;
|
|
let watcher = watch_kinds(&w, &[2]).await;
|
|
let start = std::time::Instant::now();
|
|
let mut sent = Vec::new();
|
|
for i in 0..45u64 {
|
|
tokio::time::sleep_until(tokio::time::Instant::from_std(start + Duration::from_micros(i * 33_333))).await;
|
|
let f = video(codec, i, i % 15 == 0, size);
|
|
p.write(MediaKind::Video, codec, f.clone(), 1_000 + i * 3000);
|
|
sent.push(f);
|
|
}
|
|
assert!(received(&w, &p, sent.len()).await, "every frame reached the gateway");
|
|
assert_eq!(delete(&w, &p.location, &auth).await.status, 200);
|
|
assert!(until(STEP, || watcher.ended.lock().unwrap().len() == 1).await, "PUBLISH_DONE");
|
|
settle(&watcher, Duration::from_millis(500)).await;
|
|
let t = &watcher.tracks[0];
|
|
let (frames, groups) = verified(&watcher, t, &w.gateway_key, codec);
|
|
same(&frames, &sent, "every frame, byte for byte");
|
|
assert_eq!(groups, 3, "one group per keyframe");
|
|
// Each keyframe's payload states its size: the IVF header for VP8 and VP9, the AV1
|
|
// framing's height and width.
|
|
let env = envelopes(&watcher, t);
|
|
for (i, e) in env.iter().enumerate().filter(|(i, _)| i % 15 == 0) {
|
|
assert!(e.key, "object {i} is a keyframe");
|
|
let (pw, ph) = match codec {
|
|
Codec::Vp8 | Codec::Vp9 => (u16::from_le_bytes([e.payload[12], e.payload[13]]) as u32, u16::from_le_bytes([e.payload[14], e.payload[15]]) as u32),
|
|
_ => (u32::from_le_bytes(e.payload[8..12].try_into().unwrap()), u32::from_le_bytes(e.payload[4..8].try_into().unwrap())),
|
|
};
|
|
assert_eq!((pw, ph), size, "the size {codec:?}'s framing states");
|
|
}
|
|
assert_eq!(env[0].manifest["model_requirement"]["capability"], codec.capability());
|
|
assert!(frame::unframe(codec, &env[1].payload).is_some());
|
|
p.close();
|
|
w.stop().await;
|
|
}
|