Some checks failed
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 34s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 38s
wai-app and Create / wai-app · does this change touch it (push) Successful in 25s
wai release binaries build on Windows / the seven release binaries · windows (push) Has been cancelled
WAI examples / WAI examples · does this change touch them (push) Has been cancelled
WAI examples / WAI examples · portable · linux (push) Has been cancelled
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Has been cancelled
WAI examples / WAI examples · android-app · linux-arm64 (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (push) Has been cancelled
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (push) Has been cancelled
The ST 2110-20 receive path in joule-moq-ingest no longer makes a system call and an allocation and a wake-up a datagram on the gateway's runtime (D66's finding at 1920 x 1080 and 60, about 218 000 datagrams a second). st2110::rx reads each stream's socket on a thread that does nothing else, into the session's one bounded queue: on Linux one recvmmsg up to 64 buffers with UDP_GRO (a run of datagrams of one size cut back at the size the kernel states), the kernel's arrival stamps (SO_TIMESTAMPNS) and its count of buffers a full receive buffer dropped (SO_RXQ_OVFL); elsewhere one recvmsg a datagram with SO_TIMESTAMP, and on macOS the thread asks for QOS_CLASS_USER_INTERACTIVE. The system calls are in st2110::sock on libc's structures, each with its SAFETY note. The readers are untouched: the reorder, loss and completeness rules, the corpus (--check) and every earlier test pass as they were. st2110::timing reads a video stream's arrivals against the Network Compatibility Model of the sender type its description names (ST 2110-21:2022 §6.6.1, C_MAX of §7.1), counting a frame outside only where C_INST passed C_MAX by more than one, since the drain grid's phase is the gateway's; it is a reading, never the sender's compliance (sender_type_checked stays "no"), and the Virtual Receiver Buffer Model is not read. st2110::pace is the test sender on ST 2110-21's linear schedule, four packets at an instant and never sooner than the model drains them, by sendmmsg with UDP_SEGMENT on Linux. tests/essence_rx.rs holds the way in to broadcast rates on request; tests/essence_rt.rs sends with the pacer and takes WAI_RT_MAXUDP (ST 2110-10's Extended UDP Size Limit, stated as MAXUDP). Planted faults, each caught: a batch that drops its last datagram; coalesced datagrams cut a size off either way; a sender bursting whole frames against a description of a narrow sender (through a session, and over computed arrivals). WAI essence-contribution §10.10 (new), §3, §10.1, §10.7, §10.9, §12. Co-Authored-By: Claude Opus 5.5 <[email protected]>
551 lines
30 KiB
Rust
551 lines
30 KiB
Rust
//! A contribution of uncompressed essence held to its frame rate, end to end on loopback
|
||
//! (WAI `extensions/essence-contribution.md` §10.3 and §12): the gateway's real session, a
|
||
//! real-time encoder behind it, this crate's own RTP sender pacing whole 4:2:2 10-bit frames
|
||
//! of a broadcast size at the stream's rate on ST 2110-21's linear schedule, four packets at an
|
||
//! instant and several a system call where the host can (`st2110::pace`), and a subscriber
|
||
//! at the relay that verifies every receipt. `tests/essence_rx.rs` measures the way in alone.
|
||
//!
|
||
//! **Slow is a failure here, not a note.** For each real-time encoder this host has
|
||
//! (`va`, `svt-av1`), the test sends 1280 × 720 and then 1920 × 1080 at 30 frames a second
|
||
//! for 20 seconds and fails unless
|
||
//!
|
||
//! - every frame sent reached the encode stage whole (none lost on the way in: that would
|
||
//! be a measurement of the sender or the socket, and is reported as such),
|
||
//! - the stage dropped none (`frames_not_encoded_queue_full` is 0, and the session counted
|
||
//! no frame behind the encoder),
|
||
//! - every frame was published, in order, each object verified against the gateway's
|
||
//! receipts, and every keyframe within the video floor,
|
||
//! - and, where the host has libdav1d, every temporal unit decoded there to a picture of
|
||
//! the stream's depth that is the picture sent (its luma within a stated distance of the
|
||
//! source's), coded at the picture's size or at the next the engine's alignment gives,
|
||
//! with the picture's size stated. The first frames are decoded a second time by this
|
||
//! repository's own decoder (`wai-av1`) and compared sample for sample with the first
|
||
//! decode: what it says is printed, and is not what the test passes or fails on.
|
||
//!
|
||
//! **A loaded host is not measured.** The test reads the host's load first and refuses to
|
||
//! measure above `WAI_RT_MAX_LOAD_PER_CORE` (default 0.15 of a core's worth a core over the
|
||
//! last minute), saying so: with `WAI_RT_REQUIRE=1` that refusal fails the test, otherwise
|
||
//! it is printed and the test ends without a result. Nothing here hides load.
|
||
//!
|
||
//! These tests are ignored unless asked for: they need a real-time encoder, 20 seconds a
|
||
//! size and a quiet host. Every figure they print is that host's, that day's.
|
||
//!
|
||
//! ```text
|
||
//! WAI_SVT_AV1_LIBRARY=/path/libSvtAv1Enc.so.4 cargo test --release -p joule-moq-ingest \
|
||
//! --features st2110 --test essence_rt -- --ignored --nocapture --test-threads 1
|
||
//! ```
|
||
//!
|
||
//! `WAI_RT_SOURCE` names a YUV4MPEG2 file of 8-bit 4:2:0 pictures at least as large as the
|
||
//! stream's, whose centre is sent (at 10 bits, its chroma rows doubled to 4:2:2); without
|
||
//! it the pictures are computed: a panning texture with a little noise, which no encoder
|
||
//! finds easy. `WAI_RT_SECONDS` (default 20) is how long each size is sent.
|
||
//!
|
||
//! `WAI_RT_MAXUDP` (default 1460, ST 2110-10's Standard UDP Size Limit) is the largest UDP
|
||
//! datagram sent, header and RTP header included; above 1460, up to the Extended UDP Size
|
||
//! Limit of 8960 (ST 2110-10 §6.4), the description states it as `MAXUDP` (§8.6). A run at
|
||
//! the extended size sends about a sixth of the datagrams: it measures the encoder where a
|
||
//! host's loopback cannot carry the standard size's datagram rate, and says so.
|
||
|
||
mod common;
|
||
|
||
use common::dav1d::Dav1d;
|
||
use common::*;
|
||
use joule_moq_ingest::frame::Codec;
|
||
use joule_moq_ingest::room::media;
|
||
use joule_moq_ingest::st2110::encode::{self, Kind};
|
||
use joule_moq_ingest::st2110::pace::Pacer;
|
||
use joule_moq_ingest::st2110::sdp::{Sampling, VideoFormat};
|
||
use joule_moq_ingest::st2110::send::{description, video_format, video_packets, Clocks, Media, Out};
|
||
use serde_json::{json, Value};
|
||
use std::sync::Arc;
|
||
use std::time::{Duration, Instant};
|
||
|
||
/// The host's one-minute load and its logical processors.
|
||
fn load() -> Option<(f64, usize)> {
|
||
let cores = std::thread::available_parallelism().ok()?.get();
|
||
let text = match std::fs::read_to_string("/proc/loadavg") {
|
||
Ok(t) => t,
|
||
Err(_) => String::from_utf8(std::process::Command::new("sysctl").args(["-n", "vm.loadavg"]).output().ok()?.stdout).ok()?,
|
||
};
|
||
let one = text.split_whitespace().find_map(|t| t.parse::<f64>().ok())?;
|
||
Some((one, cores))
|
||
}
|
||
|
||
fn env_or<T: std::str::FromStr>(name: &str, default: T) -> T {
|
||
std::env::var(name).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||
}
|
||
|
||
/// `Ok` with the load when the host is quiet enough to measure; `Err` with why not.
|
||
fn quiet() -> Result<String, String> {
|
||
let limit: f64 = env_or("WAI_RT_MAX_LOAD_PER_CORE", 0.15);
|
||
let (one, cores) = load().ok_or("this host's load cannot be read")?;
|
||
let said = format!("one-minute load {one:.2} on {cores} logical processors");
|
||
if one > limit * cores as f64 { Err(format!("{said}, above {limit} a processor: not measured")) } else { Ok(said) }
|
||
}
|
||
|
||
fn host() -> String {
|
||
let name = std::process::Command::new("hostname").output().ok().and_then(|o| String::from_utf8(o.stdout).ok()).unwrap_or_default();
|
||
let cpu = std::fs::read_to_string("/proc/cpuinfo").ok().and_then(|c| c.lines().find(|l| l.starts_with("model name")).and_then(|l| l.split(':').nth(1)).map(|s| s.trim().to_string())).unwrap_or_else(|| std::env::consts::ARCH.to_string());
|
||
format!("{} ({cpu}, {})", name.trim(), std::env::consts::OS)
|
||
}
|
||
|
||
/// Today's date, from the clock.
|
||
fn today() -> String {
|
||
joule_moq_ingest::package::rfc3339(now_us())[..10].to_string()
|
||
}
|
||
|
||
/// A computed picture no encoder finds easy: a texture that pans two samples a picture,
|
||
/// with a little noise that changes every picture, in the narrow range at 10 bits.
|
||
fn computed(f: &VideoFormat, n: u32) -> [Vec<u16>; 3] {
|
||
let (w, h, cw) = (f.width, f.height, f.width.div_ceil(2));
|
||
let noise = |x: u32, y: u32, k: u32| {
|
||
let mut v = x.wrapping_mul(0x9e37_79b1) ^ y.wrapping_mul(0x85eb_ca6b) ^ k.wrapping_mul(0xc2b2_ae35);
|
||
v ^= v >> 15;
|
||
v = v.wrapping_mul(0x2c1b_3c6d);
|
||
v ^ (v >> 12)
|
||
};
|
||
let mut y = Vec::with_capacity((w * h) as usize);
|
||
for py in 0..h {
|
||
for px in 0..w {
|
||
let (sx, sy) = (px + 2 * n, py + n);
|
||
let texture = ((sx / 4) ^ (sy / 4)) & 0x7f;
|
||
let ramp = (sx + sy) / 8 % 256;
|
||
y.push((100 + texture * 3 + ramp + (noise(px, py, n) & 15)) as u16);
|
||
}
|
||
}
|
||
let chroma = |seed: u32| -> Vec<u16> {
|
||
let mut c = Vec::with_capacity((cw * h) as usize);
|
||
for py in 0..h {
|
||
for px in 0..cw {
|
||
let (sx, sy) = (px + n, py + n);
|
||
c.push((300 + ((sx / 16 + seed * 7) ^ (sy / 16)) % 64 * 6 + (noise(px, py, n + seed) & 7)) as u16);
|
||
}
|
||
}
|
||
c
|
||
};
|
||
[y, chroma(1), chroma(2)]
|
||
}
|
||
|
||
/// The pictures to send: from `WAI_RT_SOURCE` where it is set, computed otherwise. With
|
||
/// what they are, for the record.
|
||
fn pictures(f: &VideoFormat, count: usize) -> (Vec<[Vec<u16>; 3]>, String) {
|
||
let Ok(path) = std::env::var("WAI_RT_SOURCE") else {
|
||
return ((0..count as u32).map(|n| computed(f, n)).collect(), "computed pictures (a panning texture with noise)".into());
|
||
};
|
||
let bytes = std::fs::read(&path).unwrap_or_else(|e| panic!("WAI_RT_SOURCE {path}: {e}"));
|
||
let end = bytes.iter().position(|b| *b == b'\n').expect("a YUV4MPEG2 header");
|
||
let head = String::from_utf8_lossy(&bytes[..end]).into_owned();
|
||
assert!(head.starts_with("YUV4MPEG2 "), "{path}: not YUV4MPEG2");
|
||
let field = |c: char| head.split(' ').find_map(|t| t.strip_prefix(c).map(str::to_string));
|
||
let (sw, sh): (usize, usize) = (field('W').unwrap().parse().unwrap(), field('H').unwrap().parse().unwrap());
|
||
let colour = field('C').unwrap_or_else(|| "420".into());
|
||
assert!(colour.starts_with("420") && !colour.starts_with("420p"), "{path}: {colour}, and 8-bit 4:2:0 is read");
|
||
let (w, h) = (f.width as usize, f.height as usize);
|
||
assert!(sw >= w && sh >= h && sw.is_multiple_of(2) && sh.is_multiple_of(2), "{path}: {sw} x {sh} for a stream of {w} x {h}");
|
||
let (x0, y0) = (((sw - w) / 2) & !1, ((sh - h) / 2) & !1);
|
||
let (mut at, mut out) = (end + 1, Vec::new());
|
||
while out.len() < count {
|
||
let Some(nl) = bytes[at..].iter().position(|b| *b == b'\n') else { break };
|
||
at += nl + 1;
|
||
if at + sw * sh * 3 / 2 > bytes.len() {
|
||
break;
|
||
}
|
||
let (ys, us, vs) = (&bytes[at..at + sw * sh], &bytes[at + sw * sh..at + sw * sh * 5 / 4], &bytes[at + sw * sh * 5 / 4..at + sw * sh * 3 / 2]);
|
||
at += sw * sh * 3 / 2;
|
||
let mut y = Vec::with_capacity(w * h);
|
||
for r in 0..h {
|
||
y.extend(ys[(y0 + r) * sw + x0..][..w].iter().map(|v| (*v as u16) << 2));
|
||
}
|
||
// Each 4:2:0 chroma row stands for two rows of 4:2:2.
|
||
let chroma = |p: &[u8]| -> Vec<u16> {
|
||
let mut c = Vec::with_capacity(w / 2 * h);
|
||
for r in 0..h {
|
||
c.extend(p[(y0 + r) / 2 * (sw / 2) + x0 / 2..][..w.div_ceil(2)].iter().map(|v| (*v as u16) << 2));
|
||
}
|
||
c
|
||
};
|
||
out.push([y, chroma(us), chroma(vs)]);
|
||
}
|
||
assert!(out.len() >= 2, "{path}: fewer than two pictures");
|
||
let said = format!("{} pictures of {} ({sw} x {sh}, its centre {w} x {h}, at 10 bits with its chroma rows doubled)", out.len(), std::path::Path::new(&path).file_name().unwrap().to_string_lossy());
|
||
(out, said)
|
||
}
|
||
|
||
/// What a paced sender did.
|
||
struct Sent {
|
||
frames: u32,
|
||
packets: u64,
|
||
/// How far behind its own schedule the sender ran at worst.
|
||
late_us: u64,
|
||
/// The system calls that wrote, and how.
|
||
calls: u64,
|
||
mechanism: &'static str,
|
||
}
|
||
|
||
/// Send `frames` frames of `packed` (in order, then back, so no cut is made where they
|
||
/// repeat) at `fps` on ST 2110-21's linear schedule, four packets at an instant
|
||
/// (`st2110::pace`), with the sequence numbers and timestamps of one stream.
|
||
fn send_paced(sock: std::net::UdpSocket, packed: Arc<Vec<Vec<Vec<u8>>>>, frames: u32, fps: (u32, u32), go: Instant) -> Sent {
|
||
let ticks = 90_000 * fps.1 / fps.0;
|
||
let (k, mut seq) = (packed.len(), 65_000u32);
|
||
let mut pacer = Pacer::new(sock, 4);
|
||
let mechanism = pacer.mechanism();
|
||
let p = pacer
|
||
.run(frames, fps, go, |n, data, lens| {
|
||
let at = (n as usize) % (2 * k - 2).max(1);
|
||
for p in &packed[if at < k { at } else { 2 * k - 2 - at }] {
|
||
let s = data.len();
|
||
data.extend_from_slice(p);
|
||
data[s + 2..s + 4].copy_from_slice(&(seq as u16).to_be_bytes());
|
||
data[s + 4..s + 8].copy_from_slice(&(1_000u32.wrapping_add(n.wrapping_mul(ticks))).to_be_bytes());
|
||
data[s + 12..s + 14].copy_from_slice(&((seq >> 16) as u16).to_be_bytes());
|
||
seq = seq.wrapping_add(1);
|
||
lens.push(p.len());
|
||
}
|
||
})
|
||
.expect("the sender writes");
|
||
Sent { frames: p.frames, packets: p.packets, late_us: p.late_us, calls: p.calls, mechanism }
|
||
}
|
||
|
||
/// The mean squared distance of a decoded luma plane from the luma sent, as a PSNR in
|
||
/// tenths of a decibel at the stream's depth.
|
||
fn psnr_tenths(sent: &[u16], got: &[u16], depth: u32) -> u64 {
|
||
let sse: u64 = sent.iter().zip(got).map(|(a, b)| (*a as i64 - *b as i64).pow(2) as u64).sum();
|
||
if sse == 0 {
|
||
return 999;
|
||
}
|
||
let peak = ((1u64 << depth) - 1) as f64;
|
||
(100.0 * (peak * peak * sent.len() as f64 / sse as f64).log10()) as u64
|
||
}
|
||
|
||
/// How many frames the second decoder is asked for: it is written for exactness, not speed.
|
||
const SECOND_DECODER_FRAMES: usize = 8;
|
||
|
||
/// The repository's own AV1 decoder (`wai/wai-av1`, written from the specification's text)
|
||
/// over the first temporal units, held sample for sample to what libdav1d made of them.
|
||
/// What it says is stated, not required: it refuses a coding tool no committed stream holds
|
||
/// it to, and is then asked again with those tools allowed, for which it claims nothing.
|
||
fn second_decoder(units: &[Vec<u8>], reference: &[Vec<u16>]) -> String {
|
||
let run = |allow_unverified: bool| -> Result<(usize, Vec<&'static str>), wai_av1::Error> {
|
||
let mut d = wai_av1::Decoder::new(wai_av1::Options { allow_unverified, ..wai_av1::Options::default() });
|
||
let mut same = 0;
|
||
for (u, want) in units.iter().zip(reference) {
|
||
// A temporal unit begins with its delimiter, which the track's framing carries.
|
||
let shown = d.decode_temporal_unit(&[&[0x12, 0x00][..], u].concat())?;
|
||
let luma: Vec<u16> = shown.first().map(|p| (0..p.planes[0].height).flat_map(|y| p.planes[0].data[y * p.planes[0].stride..][..p.planes[0].width].to_vec()).collect()).unwrap_or_default();
|
||
same += (shown.len() == 1 && luma == *want) as usize;
|
||
}
|
||
Ok((same, d.tools_used().iter().copied().filter(|t| !wai_av1::frame::VERIFIED_TOOLS.contains(t)).collect()))
|
||
};
|
||
let n = units.len();
|
||
match run(false) {
|
||
Ok((same, _)) if same == n => format!("wai-av1 decoded the first {n} to the same luma, sample for sample"),
|
||
Ok((same, _)) => format!("wai-av1 DISAGREES with libdav1d on {} of the first {n} frames", n - same),
|
||
Err(e) if e.code == "refused_unverified_tool" => match run(true) {
|
||
Ok((same, tools)) if same == n => format!("wai-av1 refuses the stream by default ({}) and, with the tools it holds to no reference allowed ({}), decoded the first {n} to the same luma, sample for sample", e.detail, tools.join(", ")),
|
||
Ok((same, tools)) => format!("wai-av1 refuses the stream by default ({}); with its unverified tools allowed ({}) it DISAGREES with libdav1d on {} of the first {n} frames", e.detail, tools.join(", "), n - same),
|
||
Err(e2) => format!("wai-av1 refuses the stream by default ({}), and with its unverified tools allowed: {}: {}", e.detail, e2.code, e2.detail),
|
||
},
|
||
Err(e) => format!("wai-av1 does not decode the stream: {}: {}", e.code, e.detail),
|
||
}
|
||
}
|
||
|
||
/// One measurement: a session of `w` × `h` at `fps` through `kind` for `seconds`. `Ok`
|
||
/// with the line to record when the rate was held; `Err` with what was not.
|
||
async fn hold(kind: Kind, w: u32, h: u32, fps: (u32, u32), seconds: u32) -> Result<String, String> {
|
||
let before = quiet()?;
|
||
let frames = seconds * fps.0 / fps.1;
|
||
let f = video_format(Sampling::YCbCr422, 10, w, h, fps);
|
||
// The pictures, packed once before anything is timed.
|
||
// The datagram size: the UDP header (8 octets) and the RTP header (12) are in it.
|
||
let maxudp: usize = env_or("WAI_RT_MAXUDP", 1_460usize).clamp(1_460, 8_960);
|
||
let (pics, source) = tokio::task::spawn_blocking({
|
||
let f = f.clone();
|
||
move || pictures(&f, 48)
|
||
})
|
||
.await
|
||
.unwrap();
|
||
let packed: Arc<Vec<Vec<Vec<u8>>>> = Arc::new(
|
||
tokio::task::spawn_blocking({
|
||
let (f, pics) = (f.clone(), pics.clone());
|
||
move || pics.iter().map(|p| video_packets(&f, p, 0, &mut Out::new(96, 0x7274_0001, 0), maxudp - 20)).collect()
|
||
})
|
||
.await
|
||
.unwrap(),
|
||
);
|
||
let world = World::start(|c| {
|
||
c.st2110.enabled = true;
|
||
c.st2110.bind_ip = "127.0.0.1".parse().unwrap();
|
||
c.st2110.encoder = kind.name().into();
|
||
c.st2110.queue_packets = 65_536;
|
||
encode::from_env(&mut c.st2110);
|
||
c.contrib.idle_ms = 1_500;
|
||
// A frame of 1920 x 1080 may be large at a keyframe; the session's own limits stand.
|
||
})
|
||
.await;
|
||
let chosen = world.gateway.gateway.essence_encoder().map_err(|e| format!("no encoder: {e}"))?;
|
||
let auth = world.auth(&world.publisher, 0);
|
||
let mut text = description(&[Media::Video(f.clone())], "127.0.0.1", &[30_000], Clocks::Local, None);
|
||
if maxudp > 1_460 {
|
||
text = text.replace("; TP=2110TPW", &format!("; TP=2110TPW; MAXUDP={maxudp}"));
|
||
}
|
||
let r = auth.send("POST", &format!("{}/st2110", world.base()), &[("content-type", "application/json")], &serde_json::to_vec(&json!({"sdp": text})).unwrap()).await;
|
||
if r.status != 201 {
|
||
return Err(format!("POST /st2110: {} {}", r.status, r.text()));
|
||
}
|
||
let s: Value = serde_json::from_slice(&r.body).unwrap();
|
||
assert_eq!((s["encoder"]["backend"].as_str(), s["encoder"]["fallback"].is_null()), (Some(kind.name()), true), "the encoder asked for encodes, under its own name");
|
||
let id = s["session"].as_str().unwrap().to_string();
|
||
assert!(contrib_live(&world, &id).await, "the session publishes its tracks");
|
||
let watcher = watch_kinds(&world, &[media::VIDEO]).await;
|
||
let sock = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
|
||
sock.connect(("127.0.0.1", s["streams"][0]["port"].as_u64().unwrap() as u16)).unwrap();
|
||
let go = Instant::now() + Duration::from_millis(200);
|
||
let sent = tokio::task::spawn_blocking({
|
||
let packed = packed.clone();
|
||
move || send_paced(sock, packed, frames, fps, go)
|
||
})
|
||
.await
|
||
.unwrap();
|
||
let during = load().map(|(l, _)| l).unwrap_or(0.0);
|
||
if !until(Duration::from_secs(60), || world.gateway.gateway.records().iter().any(|r| r.record.protocol == "st2110")).await {
|
||
return Err("the session signed no record".into());
|
||
}
|
||
let rec = world.gateway.gateway.records().into_iter().find(|r| r.record.protocol == "st2110").unwrap();
|
||
rec.verify().unwrap();
|
||
assert!(until(STEP, || watcher.ended.lock().unwrap().len() == 1).await, "PUBLISH_DONE on the video track");
|
||
settle(&watcher, Duration::from_millis(500)).await;
|
||
let d = &rec.record.detail;
|
||
let (sv, stage) = (&d["streams"][0], &d["encoder"]["stage"]);
|
||
let n = |v: &Value| v.as_u64().unwrap_or(0);
|
||
let size = if maxudp > 1_460 { format!("datagrams of at most {maxudp} octets, the extended UDP size limit's, stated as MAXUDP") } else { format!("datagrams of at most {maxudp} octets") };
|
||
let what = format!("{} {} ({}), {w} x {h} at {}/{} for {seconds} s, {} frames in {} {size}, {source}", kind.name(), chosen.identity.version, stage["settings"].as_str().unwrap_or(""), fps.0, fps.1, sent.frames, sent.packets);
|
||
|
||
// The way in: every packet arrived and every frame reached the stage whole. Otherwise
|
||
// this run measured the sender or the socket, not the encoder.
|
||
let whole = n(&sv["read"]["frames"]) == frames as u64 && n(&sv["read"]["frames_incomplete"]) == 0 && n(&sv["lost"]) == 0 && n(&d["packets_dropped_queue_full"]) == 0;
|
||
// How the way in went: the sender's writes, the receive thread's reads, the arrivals
|
||
// against the description's sender type.
|
||
let (rx, at) = (&sv["receive"], &sv["arrival_timing"]);
|
||
let way_in = format!(
|
||
"sent by {} in {} calls, at worst {} ms behind its schedule; read by {} in {} reads ({} coalesced buffers), the receive thread {} ms of processor time, {} buffers dropped by the kernel at a full receive buffer of {} octets ({} ms of the stream); arrivals against a {} sender's model: C_MAX {}, C_INST at most {}, {} of {} frames outside",
|
||
sent.mechanism,
|
||
sent.calls,
|
||
sent.late_us / 1000,
|
||
rx["mechanism"].as_str().unwrap_or("?"),
|
||
rx["reads"],
|
||
rx["coalesced_buffers"],
|
||
n(&rx["thread_cpu_us"]) / 1000,
|
||
rx["kernel_dropped_buffers"].as_u64().map_or("(a count the host does not give)".to_string(), |v| v.to_string()),
|
||
n(&sv["receive_buffer_octets"]),
|
||
n(&rx["buffer_us"]) / 1000,
|
||
at["sender_type"].as_str().unwrap_or("?"),
|
||
at["cmax"],
|
||
at["cinst_max"],
|
||
at["frames_outside"],
|
||
at["frames_read"],
|
||
);
|
||
if !whole || sent.late_us > 20_000 {
|
||
return Err(format!(
|
||
"{what}: the way in did not hold, so the encoder was not measured: {} of {} packets counted, {} lost, {} frames read, {} incomplete, {} datagrams found the session's queue full; {way_in}",
|
||
n(&sv["packets"]),
|
||
sent.packets,
|
||
n(&sv["lost"]),
|
||
n(&sv["read"]["frames"]),
|
||
n(&sv["read"]["frames_incomplete"]),
|
||
n(&d["packets_dropped_queue_full"]),
|
||
));
|
||
}
|
||
|
||
// The encoder: every frame offered, none dropped, every one published and verified.
|
||
let video = watcher.tracks.iter().find(|t| t.kind == media::VIDEO).unwrap();
|
||
let (units, groups) = verified(&watcher, video, &world.gateway_key, Codec::Av1);
|
||
let env = envelopes(&watcher, video);
|
||
let dropped = n(&stage["frames_not_encoded_queue_full"]);
|
||
let behind = n(&sv["not_published"]["frames_encoder_behind_not_published"]);
|
||
let figures = format!(
|
||
"{} frames encoded of {} offered, {dropped} dropped behind the encoder, {} published in {groups} groups; a frame took {:.1} ms on average and {:.1} at most of {:.1}; {} frames over their interval; queue {} deep at most of {}; rungs {} to {} of {} ({} moves faster, {} slower); {} kbit/s made",
|
||
n(&stage["frames_encoded"]),
|
||
n(&stage["frames_offered"]),
|
||
units.len(),
|
||
n(&stage["us_a_frame"]["encode_mean"]) as f64 / 1000.0,
|
||
n(&stage["us_a_frame"]["encode_max"]) as f64 / 1000.0,
|
||
n(&stage["frame_interval_us"]) as f64 / 1000.0,
|
||
n(&stage["frames_over_their_interval"]),
|
||
n(&stage["queue_depth_max"]),
|
||
n(&stage["queue"]),
|
||
n(&stage["rungs"]["first"]),
|
||
n(&stage["rungs"]["last"]),
|
||
n(&stage["rungs"]["count"]),
|
||
n(&stage["rungs"]["moved_faster"]),
|
||
n(&stage["rungs"]["moved_slower"]),
|
||
n(&stage["octets"]) * 8 * fps.0 as u64 / (fps.1 as u64 * frames as u64 * 1000),
|
||
);
|
||
if n(&stage["frames_offered"]) != frames as u64 || dropped != 0 || behind != 0 || n(&stage["frames_encoded"]) != frames as u64 || units.len() != frames as usize {
|
||
return Err(format!("{what}: NOT HELD: {figures}; ended {}", rec.record.ended));
|
||
}
|
||
if rec.record.ended != "idle" {
|
||
return Err(format!("{what}: the session ended {}", rec.record.ended));
|
||
}
|
||
|
||
// The floor, the order and the times: as every essence session's.
|
||
assert!(env[0].key, "a group begins at a keyframe");
|
||
for e in env.iter().filter(|e| e.key) {
|
||
joule_moq_ingest::video_floor::check_payload(&e.payload).unwrap_or_else(|o| panic!("an object outside the video floor: {}", o.detail()));
|
||
}
|
||
let seq = joule_moq_ingest::frame::av1_obus(&units[0]).unwrap().into_iter().find(|o| o.0 == 1).map(|o| joule_moq_ingest::video_floor::read_sequence_header(o.2).unwrap()).expect("a sequence header");
|
||
assert_eq!(seq.seq_profile, 0);
|
||
assert!(seq.operating_points.iter().all(|(level, tier)| *level <= 9 && *tier == 0), "{:?}", seq.operating_points);
|
||
// The picture every keyframe states is the stream's. The frames are coded at that size,
|
||
// or at the next the engine's block alignment gives, and the record says which.
|
||
let sizes = encode::check_unit(&units[0], w, h, fps.0, fps.1).expect("a keyframe within the floor");
|
||
let (cw, ch) = sizes.coded;
|
||
assert_eq!(sizes.render, (w, h));
|
||
assert_eq!(stage["coded_frame"], json!([cw, ch]));
|
||
assert_eq!(stage["picture"], json!([w, h]));
|
||
for (e, u) in env.iter().zip(&units).filter(|(e, _)| e.key) {
|
||
assert_eq!(encode::check_unit(u, w, h, fps.0, fps.1), Ok(sizes), "every keyframe");
|
||
let dim = |i: usize| u32::from_le_bytes(e.payload[4 * i..4 * i + 4].try_into().unwrap());
|
||
assert_eq!((dim(1), dim(2)), (ch, cw), "the framing states the size the frames are coded at");
|
||
}
|
||
let rtps: Vec<u64> = env.iter().map(|e| e.manifest["live"]["rtp"].as_u64().unwrap()).collect();
|
||
let ticks = 90_000 * fps.1 as u64 / fps.0 as u64;
|
||
assert!(rtps.windows(2).all(|x| x[1] == x[0] + ticks), "every frame is published, in order, with its own timestamp");
|
||
assert_eq!(world.relay.relay().stats().refused_at_door, 0, "the relay's door verified every object");
|
||
|
||
// The oracle: every temporal unit decodes with libdav1d to the picture sent.
|
||
let oracle = match Dav1d::open() {
|
||
Ok(mut dec) => {
|
||
let (mut worst, mut first) = (999u64, 0u64);
|
||
// The first frames as libdav1d decoded them, for the second decoder below.
|
||
let mut reference: Vec<Vec<u16>> = Vec::new();
|
||
for (i, u) in units.iter().enumerate() {
|
||
let shown = dec.decode(u).unwrap_or_else(|e| panic!("libdav1d refuses temporal unit {i}: {e}"));
|
||
assert_eq!(shown.len(), 1, "temporal unit {i} shows one picture");
|
||
let p = &shown[0];
|
||
assert_eq!((p.w, p.h, p.bpc, p.layout), (cw, ch, 10, common::dav1d::LAYOUT_I420), "temporal unit {i}");
|
||
if reference.len() < SECOND_DECODER_FRAMES {
|
||
reference.push(p.luma.clone());
|
||
}
|
||
let at = i % (2 * pics.len() - 2);
|
||
let src = &pics[if at < pics.len() { at } else { 2 * pics.len() - 2 - at }];
|
||
// The picture is the frame's first `h` rows of `w` samples.
|
||
let picture: Vec<u16> = p.luma.chunks_exact(cw as usize).take(h as usize).flat_map(|row| row[..w as usize].iter().copied()).collect();
|
||
let q = psnr_tenths(&src[0], &picture, 10);
|
||
worst = worst.min(q);
|
||
if i == 0 {
|
||
first = q;
|
||
}
|
||
}
|
||
// A picture of the wrong frame, or one with its planes or its samples misplaced,
|
||
// lies below 15 dB of the picture sent; an encode of it at these rates far above.
|
||
assert!(worst >= 200, "a decoded picture is not the picture sent: {:.1} dB at worst", worst as f64 / 10.0);
|
||
let second = tokio::task::spawn_blocking({
|
||
let units: Vec<Vec<u8>> = units.iter().take(SECOND_DECODER_FRAMES).cloned().collect();
|
||
move || second_decoder(&units, &reference)
|
||
})
|
||
.await
|
||
.unwrap();
|
||
format!("libdav1d {} decoded all {} temporal units to {cw} x {ch} 4:2:0 at 10 bits (the picture {w} x {h}), luma PSNR {:.1} dB for the first and {:.1} dB at worst; {second}", dec.version, units.len(), first as f64 / 10.0, worst as f64 / 10.0)
|
||
}
|
||
Err(why) => {
|
||
assert!(std::env::var_os("WAI_RT_REQUIRE_DAV1D").is_none(), "{why}");
|
||
format!("not decoded here: {why}")
|
||
}
|
||
};
|
||
world.stop().await;
|
||
let after = load().map(|(l, _)| l).unwrap_or(0.0);
|
||
Ok(format!("HELD {} on {}: {what}: {figures}; {oracle}; the way in: {way_in}; {before} before, {during:.2} as the sender ended, {after:.2} after", today(), host()))
|
||
}
|
||
|
||
/// The real-time encoders this host has, by their probes.
|
||
fn here() -> Vec<(Kind, Result<String, String>)> {
|
||
let mut c = joule_moq_ingest::config::St2110Config { calibrate_ms: 0, ..Default::default() };
|
||
encode::from_env(&mut c);
|
||
[Kind::Va, Kind::SvtAv1].into_iter().map(|k| (k, encode::probe(k, &c).map(|i| format!("{} {}", i.version, i.detail)))).collect()
|
||
}
|
||
|
||
async fn sizes(list: &[(u32, u32, (u32, u32))]) {
|
||
let seconds: u32 = env_or("WAI_RT_SECONDS", 20);
|
||
let require = std::env::var_os("WAI_RT_REQUIRE").is_some();
|
||
let only = std::env::var("WAI_RT_ENCODER").ok();
|
||
let (mut ran, mut failed) = (0, Vec::new());
|
||
for (kind, probe) in tokio::task::spawn_blocking(here).await.unwrap() {
|
||
if only.as_deref().is_some_and(|o| o != kind.name()) {
|
||
continue;
|
||
}
|
||
match probe {
|
||
Err(why) => println!("essence_rt: {} is not on this host: {why}", kind.name()),
|
||
Ok(id) => {
|
||
println!("essence_rt: {} is here: {id}", kind.name());
|
||
for (w, h, fps) in list {
|
||
ran += 1;
|
||
match hold(kind, *w, *h, *fps, seconds).await {
|
||
Ok(line) => println!("essence_rt: {line}"),
|
||
Err(why) if why.ends_with("not measured") => {
|
||
println!("essence_rt: REFUSED: {} {w} x {h}: {why}", kind.name());
|
||
if require {
|
||
failed.push(why);
|
||
}
|
||
}
|
||
Err(why) => {
|
||
println!("essence_rt: FAILED: {why}");
|
||
failed.push(why);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
assert!(ran > 0 || !require, "no real-time encoder is on this host: only the fallback, which holds no broadcast size");
|
||
assert!(failed.is_empty(), "{} of {ran} not held:\n{}", failed.len(), failed.join("\n"));
|
||
}
|
||
|
||
/// 1280 × 720 and 1920 × 1080 at 30 frames a second, 20 seconds each, with every real-time
|
||
/// encoder this host has: no frame dropped behind the encoder, or the test fails.
|
||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||
#[ignore = "20 seconds a size on a quiet host with a real-time encoder: run on request"]
|
||
async fn each_real_time_encoder_here_holds_hd_at_30_frames_a_second() {
|
||
sizes(&[(1280, 720, (30, 1)), (1920, 1080, (30, 1))]).await;
|
||
}
|
||
|
||
/// 1920 × 1080 at 60 frames a second, the top of level 4.1: measured and held to the
|
||
/// same, where it is asked for.
|
||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||
#[ignore = "a measurement at the top of the floor: run on request"]
|
||
async fn hd_at_60_frames_a_second_as_measured() {
|
||
sizes(&[(1920, 1080, (60, 1))]).await;
|
||
}
|
||
|
||
/// The fallback encoder at a broadcast size, as measured: it is not held to the rate (it
|
||
/// does not hold it on the hosts measured), but to the count: every frame it did not
|
||
/// encode is counted, and the rest are published.
|
||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||
#[ignore = "a measurement of the fallback: run on request"]
|
||
async fn the_fallback_at_hd_drops_and_counts() {
|
||
let seconds: u32 = env_or("WAI_RT_SECONDS", 10);
|
||
match hold(Kind::Rav1e, 1280, 720, (30, 1), seconds).await {
|
||
Ok(line) => println!("essence_rt: the fallback held it: {line}"),
|
||
Err(why) if why.contains("NOT HELD") => println!("essence_rt: the fallback, as measured on {} on {}: {why}", host(), today()),
|
||
Err(why) => println!("essence_rt: the fallback was not measured: {why}"),
|
||
}
|
||
}
|
||
|
||
/// 3840 × 2160 is past level 4.1's picture size at any rate: a session of it is refused
|
||
/// before any encoder is asked, so no encoder here is measured at it.
|
||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||
async fn ultra_high_definition_is_outside_the_floor_at_any_rate() {
|
||
let world = World::start(|c| {
|
||
c.st2110.enabled = true;
|
||
c.st2110.bind_ip = "127.0.0.1".parse().unwrap();
|
||
})
|
||
.await;
|
||
for (i, fps) in [(30u32, 1u32), (25, 1), (60, 1)].into_iter().enumerate() {
|
||
let who = ed25519_dalek::SigningKey::from_bytes(&[0x90 + i as u8; 32]);
|
||
let text = description(&[Media::Video(video_format(Sampling::YCbCr422, 10, 3840, 2160, fps))], "127.0.0.1", &[30_000], Clocks::Local, None);
|
||
let r = world.auth(&who, 0).send("POST", &format!("{}/st2110", world.base()), &[("content-type", "application/json")], &serde_json::to_vec(&json!({"sdp": text})).unwrap()).await;
|
||
let v: Value = serde_json::from_slice(&r.body).unwrap_or_default();
|
||
assert_eq!((r.status, v["reason"].as_str()), (422, Some("essence_outside_floor")), "{}", r.text());
|
||
}
|
||
world.stop().await;
|
||
}
|