The private name list over joule-code/ flagged 42 lines outside wai/. By kind: - joule-moq-ingest (33 lines, src/whep.rs, src/whip.rs, tests/codecs.rs, tests/common/mod.rs, tests/whep_caps.rs, tests/whep_play.rs): the WebRTC stack's media-kind type, a functional identifier. Each file imports it once under a local alias, `RtcKind`, on a line marked `allow-name` as the str0m crate's own name; every other use is the alias. No behaviour changes. - Prose renamed: joule-code-receipt's src/intoto.rs:6 names source forges' build attestations; the quiche recipe's build.sh:43 names the source archives its modules name; the xquic recipe's build.sh:2 drops the owner's name. - vocab/v1.ttl:47-49: three agent-vendor values of the published v1 vocabulary, which receipts carry and the SHACL shape enumerates; kept, each marked `allow-name`. - CHANGELOG.md:1125, an earlier entry's line: unedited, marked `allow-name: history`. - vendor/quinn-proto bbr/min_max.rs:2 and vendor/quinn/README.md:53: upstream text, left unedited. check-public-names.sh did not exclude any file; it now skips any file under a directory named `vendor`, says why in its header (the upstream's text, kept as its licence asks, not ours to rename, and a marker would edit it) and counts what it skips in its result line. Its default scope, wai/, holds no such directory, so CI's run reads what it read before. test-public-text-checks.sh holds the rule both ways: a vendored file is skipped and reported, ours beside it is still a hit, `vendors/` and `vendor-notes/` are ours and read, and a missing vendored file still fails closed. Planted: the match widened to `*vendor*` fails two cases; the skip removed fails two others. With the private list: joule-code/ clean (854 files read, 149 vendored skipped), wai/ clean (7001 files). joule-code/CHANGELOG.md records it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
95 lines
4.5 KiB
Rust
95 lines
4.5 KiB
Rust
//! A WHEP player (str0m as the client) watches a publisher's tracks through the gateway:
|
|
//! it gets the same frames the publisher sent, from the start of the group it joined in,
|
|
//! each verified against its receipt at the gateway before it was forwarded. Before the
|
|
//! publisher's catalog exists the player is told to come back (409, Retry-After).
|
|
|
|
mod common;
|
|
|
|
use common::*;
|
|
use ed25519_dalek::SigningKey;
|
|
use joule_moq_ingest::frame::Codec;
|
|
use std::time::Duration;
|
|
use str0m::media::{Direction, MediaKind as RtcKind}; // allow-name: the str0m crate's own type name, imported once under a local alias
|
|
use tokio::time::timeout;
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn a_whep_player_gets_the_frames_the_publisher_sent() {
|
|
timeout(Duration::from_secs(120), run()).await.expect("the test finishes in time");
|
|
}
|
|
|
|
async fn run() {
|
|
let w = World::start(|_| {}).await;
|
|
let viewer = SigningKey::from_bytes(&[0x61; 32]);
|
|
let va = w.auth(&viewer, 1);
|
|
|
|
// No publisher yet: 409, with Retry-After (WHEP §4.3.9).
|
|
let o = offer(&[Codec::Avc, Codec::Opus], Direction::RecvOnly, true, true).await;
|
|
let r = va.send("POST", &w.whep_url(), &[("content-type", "application/sdp")], o.offer.as_bytes()).await;
|
|
assert_eq!(r.status, 409, "{}", r.text());
|
|
assert!(r.header("retry-after").is_some());
|
|
|
|
let auth = w.auth(&w.publisher, 0);
|
|
let p = publish(&w, &auth).await;
|
|
let mut sent = Sent::default();
|
|
send(&p, &mut sent, 1, 15).await;
|
|
|
|
// The player, once the publisher's catalog is there.
|
|
let deadline = std::time::Instant::now() + STEP;
|
|
let (o, resp) = loop {
|
|
let o = offer(&[Codec::Avc, Codec::Opus], Direction::RecvOnly, true, true).await;
|
|
let r = va.send("POST", &w.whep_url(), &[("content-type", "application/sdp")], o.offer.as_bytes()).await;
|
|
if r.status == 201 {
|
|
break (o, r);
|
|
}
|
|
assert_eq!(r.status, 409, "{}", r.text());
|
|
assert!(std::time::Instant::now() < deadline, "the catalog arrives");
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
};
|
|
let player = o.start(&resp);
|
|
player.connected().await;
|
|
|
|
send(&p, &mut sent, 2, 15).await;
|
|
// A last keyframe closes the group before it, so every frame before it is verified
|
|
// and forwarded while the publisher is still live.
|
|
// And, after a pause longer than an audio group, a last audio packet closes the audio
|
|
// group before it the same way.
|
|
let last_key = sent.video.len();
|
|
let last_frame = sent.video.last().cloned();
|
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
p.write(RtcKind::Video, Codec::Avc, avc(last_key as u64, true), 1_000 + last_key as u64 * 3000 + 27_000);
|
|
let a = sent.audio.len() as u64;
|
|
p.write(RtcKind::Audio, Codec::Opus, opus(a), 5_000 + a * 960 + 14_400);
|
|
assert!(
|
|
until(STEP, || {
|
|
let got = player.media(RtcKind::Video);
|
|
!got.is_empty() && got.last() == last_frame.as_ref()
|
|
})
|
|
.await,
|
|
"the player gets every frame up to the last closed group"
|
|
);
|
|
let gv = player.media(RtcKind::Video);
|
|
let j = run_of(&sent.video, &gv);
|
|
assert_eq!(j % 15, 0, "the player joins at a group's first frame, a keyframe");
|
|
assert_eq!(j + gv.len(), last_key, "every frame of every group from the join");
|
|
let last_audio = sent.audio.last().cloned();
|
|
assert!(until(STEP, || player.media(RtcKind::Audio).last() == last_audio.as_ref()).await, "the player gets every audio packet up to the last closed group");
|
|
let ga = player.media(RtcKind::Audio);
|
|
let ja = run_of(&sent.audio, &ga);
|
|
assert_eq!(ja + ga.len(), sent.audio.len(), "every audio packet of every group from the join");
|
|
|
|
// The publisher leaves: the WHEP session ends with the tracks.
|
|
assert_eq!(delete(&w, &p.location, &auth).await.status, 200);
|
|
assert!(until(STEP, || w.gateway.gateway.records().iter().any(|r| r.record.protocol == "whep")).await, "the WHEP session ends with the tracks");
|
|
let rec = w.gateway.gateway.records().into_iter().find(|r| r.record.protocol == "whep").unwrap();
|
|
rec.verify().unwrap();
|
|
assert!(rec.record.ended.starts_with("track_ended"), "{}", rec.record.ended);
|
|
let refused: u64 = rec.record.tracks.iter().map(|t| t.dropped_refused).sum();
|
|
assert_eq!(refused, 0, "nothing failed its receipt");
|
|
let out: u64 = rec.record.tracks.iter().filter(|t| t.dir == 2).map(|t| t.objects).sum();
|
|
assert!(out as usize >= gv.len() + ga.len());
|
|
assert_eq!(rec.record.sessions, 1);
|
|
|
|
p.close();
|
|
player.close();
|
|
w.stop().await;
|
|
}
|