open-standards/wai/wai-pack/src/plan.rs
Transaction Science 411966b672 wai-pack: the packer and its verifier, with a supplied AV1 floor, CMAF, and HLS, DASH, MoQ and stage-view catalogs
- av1: the low-overhead OBU stream split into temporal units, shown key frames, the
  sequence header's fields, the codecs string and the av1C record.
- floor: a supplied floor checked against its pinned listing and the keyframe plan;
  the reference AV1 framing of a floor object's payload.
- cmaf: init.mp4 (cmfc, av01 + av1C) and one fragment per piece, pure functions of
  the samples.
- objects: base and floor envelopes (derived_from on the floor), named by content
  hash.
- plan: moments, rules and the class plan (max_layer, rules, max_kbps, floor only).
- catalog: on http-delivery's Mode N, the classical master and floor playlist, each
  class's master (one added SESSION-DATA line), its WAI playlist with the codecs
  union and X-WAI-FLOOR, its WAI media playlist with #WAI-LAYER lines and
  #EXT-X-GAP for floor-only pieces; classical and per-class WAI MPDs; the MoQ
  catalog; stage views.
- pack: one video component, an int_motion base whose step choose_q picks per
  piece, a supplied floor; refuses by name what this revision does not pack.
- verify: LISTING, the receipt and policy, the indexes, every object and prefix
  digest, the floor's CMAF rebuilt, the catalogs re-rendered, the measurement claim.
- CLI: plan writes the keyframe plan; pack, verify, listing.
- fixture video-base-01: three shots and a bumper played twice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-06 05:52:22 -04:00

128 lines
5.7 KiB
Rust

//! What a piece is packed as, and what each class lists of it ([packaging] §8.1).
//!
//! - **Moments.** A piece of `[start, start + duration)` timebase units spans
//! `[s_ms, e_ms)` with `s_ms = floor(start · den · 1000 / num)` and `e_ms` the same of
//! its end; it lies in moment `i` when `s_ms < to_ms` and `e_ms > from_ms`.
//! - **Rules.** Rule `i` matches a piece when each condition it states holds: its kind is
//! in `when.piece_kind`, and a moment the piece lies in has a tag in `when.moment_tag`.
//! Matching rules fold in document order, a later `then` member overriding an earlier
//! one (§3.4).
//! - **Class plan.** For class `c` and piece `p`: a floor-only class lists the floor
//! (`0xFF`). Otherwise `listed = min(c.max_layer, the rules' max_layer, the piece's
//! highest layer)`; under `max_kbps`, while the listed prefix needs more than it,
//! `ceil(8 · bytes · num / (1000 · duration · den))` kilobits per second, the top
//! layer is dropped; a base that alone needs more makes the piece floor-only for the
//! class.
//!
//! [packaging]: ../../../extensions/packaging.md
use crate::classify::PieceKind;
use crate::policy::{Class, Policy, Timebase};
use wai::stage_index::FLOOR_ONLY;
/// A span in timebase units as milliseconds, rounded down.
pub fn units_to_ms(units: u64, tb: Timebase) -> u64 {
u64::try_from(units as u128 * tb.den as u128 * 1000 / tb.num as u128).unwrap_or(u64::MAX)
}
/// What the rules decide for one piece.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PiecePlan {
/// Bit `i`: rule `i` matched.
pub rules: u32,
/// Bit `i`: the piece lies in moment `i`.
pub moments: u32,
pub max_layer: Option<u64>,
pub deadline_ms: u64,
pub ladder: Option<Vec<String>>,
}
/// The plan of a piece of `[start, start + duration)` units of kind `kind`.
pub fn plan_piece(p: &Policy, tb: Timebase, start: u64, duration: u64, kind: PieceKind) -> PiecePlan {
let (s_ms, e_ms) = (units_to_ms(start, tb), units_to_ms(start.saturating_add(duration), tb));
let mut out = PiecePlan { deadline_ms: p.deadline_ms, ..PiecePlan::default() };
let mut tags: Vec<&str> = Vec::new();
for (i, m) in p.moments.iter().enumerate() {
if s_ms < m.to_ms && e_ms > m.from_ms {
out.moments |= 1 << i;
tags.extend(m.tags.iter().map(String::as_str));
}
}
for (i, r) in p.rules.iter().enumerate() {
let kind_ok = r.when.piece_kind.as_ref().is_none_or(|ks| ks.iter().any(|k| k == kind.label()));
let tag_ok = r.when.moment_tag.as_ref().is_none_or(|ts| ts.iter().any(|t| tags.contains(&t.as_str())));
if kind_ok && tag_ok {
out.rules |= 1 << i;
if let Some(m) = r.then.max_layer {
out.max_layer = Some(m);
}
if let Some(d) = r.then.deadline_ms {
out.deadline_ms = d;
}
if let Some(l) = &r.then.ladder {
out.ladder = Some(l.clone());
}
}
}
out
}
/// The layer class `c` lists for a piece: `layer_bytes[k]` is the bytes of the piece's
/// layer-`k` object(s), `rule_max` the rules' `max_layer`, and the piece lasts `duration`
/// units of `tb`.
pub fn class_plan(c: &Class, layer_bytes: &[u64], rule_max: Option<u64>, duration: u64, tb: Timebase) -> u8 {
if c.floor_only || layer_bytes.is_empty() {
return FLOOR_ONLY;
}
let available = (layer_bytes.len() - 1) as u64;
let mut listed = c.max_layer.min(available).min(rule_max.unwrap_or(u64::MAX));
if let Some(max) = c.max_kbps {
loop {
let bytes: u128 = layer_bytes[..=listed as usize].iter().map(|&b| b as u128).sum();
let need = (8 * bytes * tb.num as u128).div_ceil(1000 * duration as u128 * tb.den as u128);
if need <= max as u128 {
break;
}
if listed == 0 {
return FLOOR_ONLY;
}
listed -= 1;
}
}
listed as u8
}
#[cfg(test)]
mod tests {
use super::*;
fn class(max_layer: u64, max_kbps: Option<u64>, floor_only: bool) -> Class {
Class { id: "c".into(), max_layer, max_kbps, floor_only }
}
const TB: Timebase = Timebase { num: 25, den: 1 };
#[test]
fn the_class_plan_bounds_layers_by_class_rules_and_rate() {
// A 2 s piece (50 frames): 1000 bytes = 4 kbps.
assert_eq!(class_plan(&class(2, None, false), &[1000, 2000, 3000], None, 50, TB), 2);
assert_eq!(class_plan(&class(1, None, false), &[1000, 2000, 3000], None, 50, TB), 1);
assert_eq!(class_plan(&class(2, None, false), &[1000, 2000, 3000], Some(0), 50, TB), 0);
assert_eq!(class_plan(&class(5, None, false), &[1000], None, 50, TB), 0);
assert_eq!(class_plan(&class(2, None, true), &[1000, 2000], None, 50, TB), FLOOR_ONLY);
// 1000 + 2000 bytes over 2 s = 12 kbps; the base alone 4 kbps.
assert_eq!(class_plan(&class(2, Some(12), false), &[1000, 2000, 3000], None, 50, TB), 1);
assert_eq!(class_plan(&class(2, Some(11), false), &[1000, 2000, 3000], None, 50, TB), 0);
assert_eq!(class_plan(&class(2, Some(4), false), &[1000, 2000], None, 50, TB), 0);
assert_eq!(class_plan(&class(2, Some(3), false), &[1000, 2000], None, 50, TB), FLOOR_ONLY);
// Rounded up: 1001 bytes over 2 s is 4.004 kbps.
assert_eq!(class_plan(&class(0, Some(4), false), &[1001], None, 50, TB), FLOOR_ONLY);
}
#[test]
fn units_convert_to_milliseconds_rounding_down() {
assert_eq!(units_to_ms(50, TB), 2000);
assert_eq!(units_to_ms(1, Timebase { num: 30000, den: 1001 }), 33);
assert_eq!(units_to_ms(48000, Timebase { num: 48000, den: 1 }), 1000);
}
}