- 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]>
128 lines
5.7 KiB
Rust
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);
|
|
}
|
|
}
|