The packer writes refinement layers, chooses each piece's representation by its ladder, packs live, and simulates delivery over MoQ; the corpus gains a staged golden that the independent verifier re-derives byte for byte. - Refinements (packaging §5.4): for each refinement rung of a piece's ladder, a wai.video.int_refine object over the int_motion base, written by wai-rs's refinement encoder (staged::wir1::encode) with the manifest `wai stage refine` writes; every layer applied to the decoded base by the reference sink before it is kept. The index states each prefix's tier, digest, SSE and deadline. - A piece's ladder and stills (§5.7, plan::piece_ladder): a rule's then.ladder, or the component's first base rung, its refinement chain and its floors. §3.4 item 10 now requires a rule's ladder to name exactly one base, every refinement over a rung it names and every floor. A wai.image.png base is a still, the piece's first frame held for the piece, written as a PNG whose every byte is stated (png.rs; the verifier's reader inflates with miniz_oxide, the one new dependency). - One coder (pack::Coder) for the on-demand and the live packer; the verifier holds each piece to its ladder and its plan's deadline, and decodes every prefix (a still held, each layer through the sink). - Catalogs: the MoQ catalog is a stage set of moq-streaming-format §4.1, written by staged::moq (wai_staged, a receipts track and an alias per media track, depends on a track of every layer below); `priority` (MOQT's, §8.5) and `wai_layer` leave it, and the registry drops `wai_layer`. HTTP views carry the component's default base capability only: a still's piece plays from the floor there (http-delivery §4.2), and its stage view and MoQ tracks list it. - Live (§9.2, live.rs, `wai-pack live`): frames pushed one at a time, pieces coded as the segmenter closes them, windows sealed by version-2 receipts chained live-window; its plan, bases and refinements are the on-demand package's byte for byte (`wai-pack listing --primary`). - Delivery (§8.9, delivery.rs, `wai-pack sim`): a thin consumer of stage_measure::sim — per-track timeouts, §8.5's priorities, the subscriber's cap, group receipts on each track's chain; delivered objects accepted by Merkle proof, dropped ones held by inclusion only. - Corpus: the golden video-staged-01 (fixture, policy, supplied floor and supplied_floor.py, which states how it was written), staged-mutations/, delivery/; video-base-01 keeps its objects, index and receipt (its MoQ catalog and LISTING change). - pack_verify.py re-derives every layer and still from the source (its own rANS coder and deflate), decodes every prefix (staged_verify.py's WIR1 and PNG readers), checks the MoQ catalog with staged_verify.py's §4.1 check, renders every catalog, and re-derives each delivery case through sim_verify.py's simulator. stage_diff.py runs layers, stills, piece plans and deliveries differentially. - Tests 5, 7, 12 and 13 over the staged golden (tests/staged.rs). Co-Authored-By: Claude Opus 5.5 <[email protected]>
146 lines
6.7 KiB
Rust
146 lines
6.7 KiB
Rust
//! The base encode's choice of step ([packaging] §5.6): the function the packer's base
|
||
//! encode of a `wai.video.int_motion` rung calls for each piece.
|
||
//!
|
||
//! `q_candidates` is read as a set. The piece is encoded at each candidate from the
|
||
//! largest down, and the first whose `floor(SSE · 1000 / (3·w·h·n)) ≤
|
||
//! max_sse_px_milli` is chosen, `SSE` being its reconstruction's against the source and
|
||
//! `n` the piece's frames: the largest step that meets the bound, so candidates that meet
|
||
//! it equally are decided by their step. Where none meets it, the smallest candidate is
|
||
//! chosen.
|
||
//!
|
||
//! [packaging]: ../../../extensions/packaging.md
|
||
|
||
use crate::policy::Rung;
|
||
use wai::int_payload::{IntPayloadError, Refusal};
|
||
use wai::int_video_enc::{encode, EncParams, Encoded};
|
||
|
||
/// The parameters of a `wai.video.int_motion` rung that fix its encode.
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct MotionParams {
|
||
pub block: u32,
|
||
pub search: u32,
|
||
pub mv_precision: u8,
|
||
pub q_candidates: Vec<u32>,
|
||
pub max_sse_px_milli: u64,
|
||
}
|
||
|
||
impl MotionParams {
|
||
/// A rung's parameters, every default written in by the policy (§3.3); `None` for a
|
||
/// rung of another capability.
|
||
pub fn of(rung: &Rung) -> Option<MotionParams> {
|
||
if rung.capability != "wai.video.int_motion" {
|
||
return None;
|
||
}
|
||
Some(MotionParams {
|
||
block: rung.param_u64("block")? as u32,
|
||
search: rung.param_u64("search")? as u32,
|
||
mv_precision: rung.param_u64("mv_precision")? as u8,
|
||
q_candidates: rung.param_u64_list("q_candidates")?.into_iter().map(|q| q as u32).collect(),
|
||
max_sse_px_milli: rung.param_u64("max_sse_px_milli")?,
|
||
})
|
||
}
|
||
}
|
||
|
||
/// A piece's step and its encode.
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct QChoice {
|
||
pub q: u32,
|
||
pub encoded: Encoded,
|
||
}
|
||
|
||
/// `floor(sse · 1000 / (3·w·h·n))`: the SSE per sample, in thousandths.
|
||
pub fn sse_px_milli(sse: u64, h: u32, w: u32, n: usize) -> u64 {
|
||
let samples = 3 * h as u128 * w as u128 * n as u128;
|
||
(sse as u128 * 1000 / samples.max(1)) as u64
|
||
}
|
||
|
||
/// Choose the step for a piece of `frames` (each `h × w` RGB8) under `p`, and encode it.
|
||
pub fn choose_q(frames: &[Vec<u8>], h: u32, w: u32, p: &MotionParams) -> Result<QChoice, IntPayloadError> {
|
||
let mut qs: Vec<u32> = p.q_candidates.clone();
|
||
qs.sort_unstable_by(|a, b| b.cmp(a));
|
||
qs.dedup();
|
||
let smallest = *qs.last().ok_or_else(|| IntPayloadError::new(Refusal::FieldRange, "no step to choose from"))?;
|
||
for &q in &qs {
|
||
let e = encode(frames, h, w, &EncParams { block: p.block, search: p.search, mv_precision: p.mv_precision, q })?;
|
||
if sse_px_milli(e.sse, h, w, frames.len()) <= p.max_sse_px_milli || q == smallest {
|
||
return Ok(QChoice { q, encoded: e });
|
||
}
|
||
}
|
||
unreachable!("the smallest step is always chosen when it is reached")
|
||
}
|
||
|
||
/// The sum, over every sample, of the squared difference between two clips of RGB8
|
||
/// frames (§6.6).
|
||
pub fn sse(a: &[Vec<u8>], b: &[Vec<u8>]) -> u64 {
|
||
a.iter().zip(b).flat_map(|(x, y)| x.iter().zip(y)).map(|(&x, &y)| (x as i64 - y as i64).unsigned_abs().pow(2)).sum()
|
||
}
|
||
|
||
/// One refinement layer of a piece (§5.4): a `WIR1` of unit kind RGB8, one unit per frame,
|
||
/// the identity predictor, one table, at most 4095 table symbols, that brings each frame
|
||
/// of `below` (the stage the layer refines) toward its source frame in `src`, at step
|
||
/// `q`. This is `wai-rs`'s refinement encoder (`staged::wir1::encode`), the one `wai stage
|
||
/// refine` runs: the residual `rdiv(source − below, q)`, its table, its streams. Returns
|
||
/// the payload and the frames a sink reconstructs, which the encoder has decoded back.
|
||
pub fn encode_refinement(below: &[Vec<u8>], src: &[Vec<u8>], h: u32, w: u32, q: u32) -> Result<(Vec<u8>, Vec<Vec<u8>>), String> {
|
||
use wai::staged::wir1::{encode, Below, Next, Predictor, RefineParams, UnitKind};
|
||
let belows: Vec<Below> = below.iter().map(|f| Below::U8(f)).collect();
|
||
let targets: Vec<Below> = src.iter().map(|f| Below::U8(f)).collect();
|
||
let p = RefineParams { q, predictor: Predictor::Identity, rate: (0, 0), table_per_unit: false, max_symbols: 4095 };
|
||
let (payload, next) = encode(UnitKind::Rgb8, &belows, (h, w, 3), &targets, (h, w, 3), &p).map_err(|e| e.0)?;
|
||
let frames = next
|
||
.into_iter()
|
||
.map(|n| match n {
|
||
Next::U8(v) => Ok(v),
|
||
Next::I16(_) => Err("an RGB8 layer decoded to PCM".to_string()),
|
||
})
|
||
.collect::<Result<Vec<_>, _>>()?;
|
||
Ok((payload, frames))
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// A layer brings every frame closer to its source, and a finer step closer still.
|
||
#[test]
|
||
fn a_refinement_reduces_the_error() {
|
||
let src = clip();
|
||
let base = encode(&src, 10, 12, &EncParams { block: 4, search: 1, mv_precision: 1, q: 24 }).unwrap();
|
||
let (p1, r1) = encode_refinement(&base.recon, &src, 10, 12, 8).unwrap();
|
||
let (_, r2) = encode_refinement(&r1, &src, 10, 12, 2).unwrap();
|
||
assert_eq!(&p1[..4], b"WIR1");
|
||
assert_eq!(sse(&base.recon, &src), base.sse);
|
||
assert!(sse(&r1, &src) < base.sse && sse(&r2, &src) < sse(&r1, &src), "{} {} {}", base.sse, sse(&r1, &src), sse(&r2, &src));
|
||
// Step 1 reconstructs the source exactly.
|
||
assert_eq!(encode_refinement(&r1, &src, 10, 12, 1).unwrap().1, src);
|
||
}
|
||
|
||
fn clip() -> Vec<Vec<u8>> {
|
||
(0..4).map(|t| (0..12 * 10 * 3).map(|i| ((i * 37 + t * 11) % 251) as u8).collect()).collect()
|
||
}
|
||
|
||
fn params(q_candidates: Vec<u32>, max_sse_px_milli: u64) -> MotionParams {
|
||
MotionParams { block: 4, search: 1, mv_precision: 1, q_candidates, max_sse_px_milli }
|
||
}
|
||
|
||
fn metric(q: u32) -> u64 {
|
||
let e = encode(&clip(), 10, 12, &EncParams { block: 4, search: 1, mv_precision: 1, q }).unwrap();
|
||
sse_px_milli(e.sse, 10, 12, 4)
|
||
}
|
||
|
||
/// The largest step that meets the bound; the smallest when none does; candidates
|
||
/// read as a set, in any order.
|
||
#[test]
|
||
fn the_largest_step_meeting_the_bound() {
|
||
let (m16, m8, m2) = (metric(16), metric(8), metric(2));
|
||
assert!(m16 > m8 && m8 > m2, "{m16} {m8} {m2}");
|
||
let pick = |c: Vec<u32>, bound| choose_q(&clip(), 10, 12, ¶ms(c, bound)).unwrap().q;
|
||
assert_eq!(pick(vec![2, 16, 8], m16), 16);
|
||
assert_eq!(pick(vec![2, 16, 8], m16 - 1), 8);
|
||
assert_eq!(pick(vec![8, 2, 16, 8], m8), 8);
|
||
assert_eq!(pick(vec![16, 8, 2], m2), 2);
|
||
assert_eq!(pick(vec![16, 8, 2], 0), 2, "none meets it: the smallest");
|
||
let c = choose_q(&clip(), 10, 12, ¶ms(vec![16, 8], m8)).unwrap();
|
||
assert_eq!(c.encoded, encode(&clip(), 10, 12, &EncParams { block: 4, search: 1, mv_precision: 1, q: 8 }).unwrap());
|
||
}
|
||
}
|