diff --git a/.github/workflows/wai-pack.yml b/.github/workflows/wai-pack.yml index d1eca1656..ca27f97a1 100644 --- a/.github/workflows/wai-pack.yml +++ b/.github/workflows/wai-pack.yml @@ -20,7 +20,8 @@ name: wai-pack # # Push filters match the push's delta, so every path a build or a verifier reads is # listed: the crate, the wai crate it builds on, the corpus, the BLAKE3, integer payload, -# staged-delivery and staged-measurement verifiers and the registry export pack_verify.py +# staged-delivery and staged-measurement verifiers, the claim verifier (a metered +# package's claim) and the OER verifier it imports, the registry export pack_verify.py # reads, and this file. concurrency: @@ -35,6 +36,8 @@ on: - 'wai/pack-conformance/**' - 'wai/claim-conformance/_blake3.py' - 'wai/claim-conformance/_ed25519.py' + - 'wai/claim-conformance/claim_verify.py' + - 'wai/oer-conformance/oer2_verify.py' - 'wai/int-payload-conformance/int_payload_verify.py' - 'wai/staged-conformance/staged_verify.py' - 'wai/staged-measure-conformance/sim_verify.py' @@ -127,10 +130,15 @@ jobs: [ "$n" -ge "$floor" ] || { echo "::error::$target lists $n tests, under its floor of $floor"; fail=1; } done <<'EOF' --lib 34 + --test=cmaf 4 --test=conformance 1 - --test=packs 7 + --test=floor_only 2 + --test=ladder 3 + --test=pack_alloc 1 + --test=packs 8 --test=review 9 - --test=refusals 5 + --test=refusals 6 + --test=repack 3 --test=scenecut 3 --test=source_alloc 1 --test=staged 10 @@ -163,6 +171,12 @@ jobs: working-directory: wai shell: bash run: python3 pack-conformance/pack_verify.py pack-conformance + # The same checks with the hashes and decodes run ahead in worker processes + # (--jobs): every check and its reason are the serial run's. + - name: pack_verify.py --jobs 2 + working-directory: wai + shell: bash + run: python3 pack-conformance/pack_verify.py --jobs 2 pack-conformance - name: Install Rust 1.98.0 shell: bash run: | @@ -207,6 +221,13 @@ jobs: set -euo pipefail python3 ../pack-conformance/stage_diff.py target/debug/wai-pack 1 1000 python3 ../pack-conformance/stage_diff.py target/release/wai-pack 2 3000 + # The supplied CMAF reader differentially (packaging §7.7): seeded alterations of a + # media muxer's segments and of §7.6's writer's, every verdict and accepted segment's + # samples of wai-pack's reader equal to pack_verify.py's. + - name: cmaf_diff.py, wai-pack against pack_verify.py + shell: bash + working-directory: wai/wai-pack + run: python3 ../pack-conformance/cmaf_diff.py target/release/wai-pack 1 3000 # A still's reader differentially: the zlib library's own streams, dynamic blocks # built on each of RFC 1951's and zlib's code-set rules, and their alterations, every # verdict of wai-pack's reader equal to pack_verify.py's, which inflates with @@ -232,27 +253,31 @@ jobs: SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo" $SUDO apt-get update -qq $SUDO apt-get install -y -qq ffmpeg - # Test 14: the golden's init.mp4 and its fragments, in its floor playlist's order, - # read as one AV1 track of every frame of the source, with a key frame at each piece - # start of the plan and nowhere else, and decode without an error. + # Test 14: each golden floor's init.mp4 and its fragments, in its floor playlist's + # order, read as one AV1 track of every frame of the source, with a key frame at each + # piece start of the plan and nowhere else, and decode without an error; a ladder + # golden has one floor playlist per floor rung (packaging §7.1), each read so. - name: The goldens' floors read and decode shell: bash working-directory: wai/pack-conformance/packs run: | set -euo pipefail - for g in video-base-01 video-staged-01; do + for g in video-base-01 video-staged-01 video-ladder-01 video-cmaf-01 video-floor-only-02; do ( cd "$g/package" - tmp=$(mktemp -d) - cat floor/v/init.mp4 $(grep -v '^#' hls/classic/floor-v.m3u8 | sed 's#^\.\./\.\./##') > "$tmp/floor.mp4" - ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=codec_name,width,height,nb_read_frames -of default=nw=1 "$tmp/floor.mp4" | tee "$tmp/probe.txt" - grep -qx 'codec_name=av1' "$tmp/probe.txt" - grep -qx 'nb_read_frames=200' "$tmp/probe.txt" - keys=$(ffprobe -v error -select_streams v:0 -show_entries packet=pts,flags -of csv=p=0 "$tmp/floor.mp4" | awk -F, '$2 ~ /K/ {print $1}' | tr '\n' ' ') - want=$(tr '\n' ' ' < plan-v.txt) - echo "$g key frames: $keys" - [ "$keys" = "$want" ] || { echo "::error::$g: key frames at $keys; the plan says $want"; exit 1; } - ffmpeg -v error -xerror -i "$tmp/floor.mp4" -f null - - echo "$g: the floor reads as one AV1 track of 200 frames, a key frame at each piece start" ) + for pl in hls/classic/floor-*.m3u8; do + f=$(basename "$pl" .m3u8); f=${f#floor-} + tmp=$(mktemp -d) + cat "floor/$f/init.mp4" $(grep -v '^#' "$pl" | sed 's#^\.\./\.\./##') > "$tmp/floor.mp4" + ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=codec_name,width,height,nb_read_frames -of default=nw=1 "$tmp/floor.mp4" | tee "$tmp/probe.txt" + grep -qx 'codec_name=av1' "$tmp/probe.txt" + grep -qx 'nb_read_frames=200' "$tmp/probe.txt" + keys=$(ffprobe -v error -select_streams v:0 -show_entries packet=pts,flags -of csv=p=0 "$tmp/floor.mp4" | awk -F, '$2 ~ /K/ {print $1}' | tr '\n' ' ') + want=$(tr '\n' ' ' < plan-v.txt) + echo "$g $f key frames: $keys" + [ "$keys" = "$want" ] || { echo "::error::$g $f: key frames at $keys; the plan says $want"; exit 1; } + ffmpeg -v error -xerror -i "$tmp/floor.mp4" -f null - + echo "$g $f: the floor reads as one AV1 track of 200 frames, a key frame at each piece start" + done ) done licences: diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 97fe014fd..8606f8934 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,89 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### Packaging: a title's whole ladder in one package + +- **Several floor rungs per package** ([packaging](extensions/packaging.md) §7.1). A + component may have any number of floor rungs; each piece then has one floor object per + floor rung, in ladder order, all over one base, so the base of a ladder is coded once. + Each floor rung has a **floor name**: the component's id when it has one floor rung (so + existing packages are unchanged, byte for byte), the rung's id when it has several. The + name names the supplied stream (`.obu`, §7.3), the CMAF files (`floor//`, + §7.6), the floor playlist (`hls/classic/floor-.m3u8`), the DASH representation + (`-floor`) and the MoQ track (`/floor`); a floor name that cannot be one path + segment, or two equal in any case, is refused (`pack-id`). The classical master lists one + variant per floor rung; with several, `dash/classic.mpd` carries each representation's + codecs and size on the representation. The verifier holds the *k*-th floor entry of each + piece to the *k*-th floor rung. +- **A re-pack from a parent** (§11.5). A packer may take every piece's base and + refinements from an earlier package instead of coding them (`wai-pack pack + --base-from `), so a title's integer base is coded once and taken by every + package made from the same source after it. It refuses a parent that does not verify + (`repack-parent-invalid`), one whose pieces, kinds, rungs, parameters or deadlines are + not this pack's (`repack-parent-mismatch`), and one whose prefixes do not reconstruct + this source with the SSE their entries state (`repack-source-mismatch`). Its receipt + names the parent with parent kind `re-pack`, and its other files are those of a pack + that codes. A verifier holding the parent checks the re-pack against it (`wai-pack + verify --parent`). +- **A floor that is the ladder's own CMAF segments** (§7.7). With + `floor.supplied_form = "cmaf"` (absent, the OBU form; existing policy digests are + unchanged), a supplied floor is an existing ladder's `init.mp4` and media segments, one + segment per piece, read box by box: the floor's temporal units are the samples, each + with a temporal delimiter before it. The package's floor files are the ladder's files, + byte for byte, named by their SHA-256, and the package carries the supplied floor's + listing (`floor.listing`), whose SHA-256 the policy pins, so the receipt binds every + byte of the ladder. A server can hold those files once for the classic site and the + package. New refusals: `floor-cmaf-malformed`, `floor-cmaf-timing`, `floor-cmaf-pieces`. + `wai-pack pack --link-supplied` writes them as hard links to the supplied files. +- **A floor-only profile** (§7.8). A component of `profile = "floor-only"` is its floor + rungs alone: no integer base, no refinement, no source (`policy-floor-only` + otherwise). Its pieces are its first floor rung's media segments (the CMAF form) or + shown key frames (the OBU form), each at most `max_ms` (`floor-piece-length`); each piece + is its floor objects, which present no base and so carry no `derived_from`, and its kind + is `none` (stage index kind 6). It attests delivery: the receipt binds every floor + object and the supplied floor's bytes, and the verifier holds them to the AV1 stream's + structure and the CMAF files; it states no decoded output, no fidelity to a source and + no primary. The stage index now requires a floor's `derived_from` exactly when its piece + has a base (`floor-derived-from`). +- **The independent verifier at scale** (`pack_verify.py`). Each object is hashed once, + and `http_verify.resolve_uri` finds an object by its content id rather than by hashing + every object for every URI. `--jobs N` runs every object's hash and every base's decode + and layers in N worker processes, piece by piece, ahead of checks that are unchanged in + order and reason. `--native-blake3` takes BLAKE3 from the `blake3` module after it + agrees with the pure implementation on the published vectors (opt-in: that module is + the implementation wai-rs links). `--package ` verifies one package without its + sources or its key. **The measurement claim is checked** (§9.4): the claim corpus's + verifier reads it, and it is held to the receipt's figure, class, evidence and signer, so + a correctly metered package passes; `package-mutations/` gains a metered package (`ok`), + a claim about another figure (`measurement-claim-invalid`) and none + (`measurement-claim-missing`). +- **§7.3: key-frame sequence headers** (an informative note). A floor taken from an + existing ladder needs a sequence header in every key-frame unit; a producer that puts + one back must put back the stream's own first sequence header, byte for byte, not the + sample entry's `av1C` header, which can differ in its tool flags and is refused + (`floor-av1-malformed`). `tests/refusals.rs` holds the rule. +- **The DASH renderer** (`wai::http_binding::dash`) writes a representation's own + `@codecs`, `@width` and `@height` when given, and an adaptation set's `@codecs` only when + every representation shares it. +- **A streaming on-demand packer** (§11.4). The packer reads the source twice, a frame + at a time, once to cut it and once to code it, and each floor stream a temporal unit at + a time, and `wai-pack pack` writes each file into its directory as it is made, reading + objects back for the catalogs. What it holds no longer grows with the title, but for a + record per piece and per entry and a byte per frame per floor; a refusal removes what it + wrote. `--jobs ` codes n pieces at once, each alone, so the package is the same for + any n. A source that does not read the same twice is refused (`pack-source-changed`). + `tests/pack_alloc.rs` packs a source eight times as long in the same peak memory. +- **Conformance.** `packs/video-ladder-01/` (two floors, one at half size, over + `video-base-01`'s bases, which are the same objects), `ladder-mutations/`, + `packs/video-repack-01/` (the ladder re-packed from `video-base-01`), + `packs/video-cmaf-01/` and `cmaf-mutations/` (the ladder's streams as a muxer's fMP4 + segments), `packs/video-floor-only-01/` and `-02/` (both forms, no base), index cases of + a piece of floors alone, `tests/ladder.rs`, `tests/repack.rs`, `tests/cmaf.rs`, + `tests/floor_only.rs`, and the independent + verifier `pack_verify.py`, with a box reader of its own, which re-derives every golden + from its sources and checks the re-pack against its parent. `supplied_floor.py` writes + one stream per floor rung; `supplied_cmaf.py` the CMAF form. + ### `wai.media.exploration`: archives that replay, default-distance, a homogenization meter, and taste as a declared, owned objective - **The extension** ([exploration](extensions/exploration.md)) registers the record diff --git a/wai/extensions/packaging.md b/wai/extensions/packaging.md index 287b0aae7..6705aa135 100644 --- a/wai/extensions/packaging.md +++ b/wai/extensions/packaging.md @@ -4,11 +4,14 @@ > this extension fixes what a verifier recomputes. This revision defines the policy, > its digest and what it may ask a packager to emit (§3), how a source is cut into > pieces (§4), the objects a packager writes, refinements and stills among them, and a -> piece's ladder (§5), the stage index (§6), the floor (§7), what each class is offered +> piece's ladder (§5), the stage index (§6), the floor, one or several, supplied as OBU +> streams or as an existing ladder's CMAF files (§7), what each class is offered > over HTTP and MoQ and how its delivery is simulated (§8), the package receipt and live -> windows (§9), and the package and its verification (§11). Reference impl: `wai-pack` — +> windows (§9), and the package, its verification and a re-pack from a parent (§11). +> Reference impl: `wai-pack` — > `policy`, `capcheck`, `source`, `scenecut`, `classify`, `plan`, `encode`, `png`, -> `floor`, `cmaf`, `catalog`, `pack`, `live`, `delivery`, `verify`; in `wai-rs`, the +> `floor`, `cmaf`, `cmaf_read`, `catalog`, `files`, `pack`, `live`, `delivery`, `verify`; +> in `wai-rs`, the > encoder `int_video_enc`, the refinement encoder `staged::wir1`, the stage index > `stage_index`, the receipt `package` and the delivery simulator `stage_measure::sim`; > the corpus [`pack-conformance/`](../pack-conformance/). Keywords **MUST**, **MUST NOT**, @@ -32,6 +35,9 @@ where it names them. - **Class**: a delivery profile the policy names. - **Moment**: a declared span of presentation time with tags. - **Floor**: a rendition in a standard codec, for sinks that lack the primary. +- **Floor name**: the name a floor rung's files and catalog entries carry (§7.1): its + component's id when the component has one floor rung, the rung's id when it has + several. - **Timebase** of a component: `num / den` units per second. For video one unit is one frame; for audio, one sample. @@ -81,6 +87,7 @@ absent table reads as empty. | `component.media` | string | required | `video`, `image`, `audio`, `world`, `feed`, `avatar`, `film` | | `component.source` | string | absent | | | `component.timebase` | table | required | members `num`, `den`, each 1 to `2^32 − 1`, required | +| `component.profile` | string | absent | `floor-only` (§7.8); absent, the standard profile | | `segmentation.target_ms` | integer | 2000 | | | `segmentation.min_ms` | integer | 1000 | | | `segmentation.max_ms` | integer | 4000 | | @@ -99,6 +106,7 @@ absent table reads as empty. | `ladder.params` | table | empty | by capability, below | | `floor.supplied_listing` | string | absent | 64 lowercase hexadecimal digits | | `floor.extra_keyframes` | boolean | `false` | | +| `floor.supplied_form` | string | absent | `cmaf` (§7.7); absent, the OBU form (§7.3) | | `live.layer_lag_ms` | array of integers | one 0 per layer | §3.4 | | `class` | array of tables | empty | | | `class.id` | string | required | 1 to 255 bytes | @@ -157,15 +165,17 @@ first that fails is the refusal: chain of `refines` returns to a rung it passed (`policy-refines-cycle`); every chain stays in its component and ends at a base (`policy-refines-base`); no rung is refined twice (`policy-refines-branch`). -8. Every component has a base rung (`policy-no-base`). +8. Every component has a base rung (`policy-no-base`), but a floor-only one (§7.8), which + has no base and no refinement rung, no `source`, and at least one floor rung + (`policy-floor-only`). 9. A rung's **layer** is 0 for a base or a floor, and one more than the rung it refines for a refinement; the policy has one more layer than its highest rung layer. No class's and no rule's `max_layer` exceeds the highest (`policy-class-layer`). 10. A rule's `then.ladder` names known rungs, all of one component, each once: - exactly one base, every refinement over a rung it names (so its layers are a chain - from that base), and every floor rung of the component (every piece has a floor, - §7.1) (`policy-rule-ladder`). + exactly one base (none, for a floor-only component), every refinement over a rung it + names (so its layers are a chain from that base), and every floor rung of the + component (every piece has a floor, §7.1) (`policy-rule-ladder`). 11. `live.layer_lag_ms` has one entry per layer, the first 0, the entries non-decreasing (`policy-layer-lag`). 12. The deadline values (`defaults.deadline_ms` and every rule's `then.deadline_ms`) @@ -205,8 +215,9 @@ no reference outside the piece, and no later object of the package references in it. **4.2** Piece boundaries are a pure function of the source frames and the effective -policy. Where the policy names `wai.pack.scenecut/1`, the boundaries **MUST** be those -§4.3 computes. The list of piece starts, in timebase units, is the **plan**, and its +policy; a floor-only component's, which has no source, of its first floor rung (§7.8). +Where the policy names `wai.pack.scenecut/1`, the boundaries of a component with a +source **MUST** be those §4.3 computes. The list of piece starts, in timebase units, is the **plan**, and its digest is BLAKE3("wai:pack-plan\x01" ‖ u64 LE n ‖ n × u64 LE start) @@ -475,7 +486,7 @@ bytes are zero. | 8 | 4 | `duration`, in timebase units | | 12 | 4 | `first_entry`, window-relative | | 16 | 2 | `n_entries` | -| 18 | 1 | `kind`: 0 natural, 1 static, 2 synthetic, 3 motion_high (§4.6), 4 world segment, 5 audio | +| 18 | 1 | `kind`: 0 natural, 1 static, 2 synthetic, 3 motion_high (§4.6), 4 world segment, 5 audio, 6 none (a floor-only component's piece, which no source classifies, §7.8) | | 19 | 1 | flags: bit 0 scene cut, bit 1 forced at `max` (§4.3), bit 2 started by a cue, bit 3 aligned to the lead (§4.5) | | 20 | 4 | `rules`: bit *i* set when rule *i* matched the piece (§8.1) | | 24 | 4 | `moments`: bit *i* set when the piece lies in moment *i* (§8.1) | @@ -562,7 +573,9 @@ of these, checked in this order: (`layer-gap`); its tier (§6.4: `tier-entropy-passthrough`, `tier-weakest`, then `tier-mismatch`); a non-zero `prefix_digest` with a tier other than decode-equivalence (`digest-without-tier`); a deadline on an entry that is not a - refinement (`deadline-role`); a floor without flag bit 2 (`floor-derived-from`); a + refinement (`deadline-role`); a floor whose flag bit 2 is not set exactly when its + piece has a base, so a floor presents its piece's base and a piece of floors alone + presents none (`floor-derived-from`); a content hash an earlier entry has at another length (`dup-length`); the duplicate flag other than set exactly when an earlier entry has the content hash (`dup-flag`). 3. Class by class: a flag with no meaning (`class-flags`); a plan value other than @@ -615,12 +628,18 @@ and one without the source cannot. A floor's `sse` is `2^64 − 1`. **7.1** Every piece of a video, image or audio component **MUST** have a floor rendition in a capability of SPEC §5's mandatory floor or recommended set; a packager refuses a video component with no floor rung (`floor-missing`). A film or world package SHOULD -have one. This revision packs a supplied AV1 floor (`wai.video.av1` or -`wai.video.av1.lossless`, `source = "supplied"`). +have one. A component may have several floor rungs: each piece then has one floor object +per floor rung, in ladder order (§5.7), so one package carries a ladder of standard-codec +renditions over one base, which is coded once. Each floor rung has a floor name (§2): the +component's id when the component has one floor rung, the rung's id when it has several. +A floor name names files (§7.3, §7.6, §8.3), so one that cannot be one path segment of a +listing, or two floor names of a package equal once `A`–`Z` are mapped to `a`–`z`, are +refused (`pack-id`, §11.1). This revision packs supplied AV1 floors (`wai.video.av1` or +`wai.video.av1.lossless`, `source = "supplied"`), one stream per floor rung. **7.2** *Floor objects.* Each piece's floor is one object of the floor rung's capability -(§5.2), whose manifest carries `derived_from` (SPEC §7.1, *Derived renditions*) naming -the piece's base: `motif` the base's content hash, `motif_capability` its capability, +(§5.2), whose manifest carries `derived_from` (SPEC §7.1, *Derived renditions*), but in a +floor-only component (§7.8), naming the piece's base: `motif` the base's content hash, `motif_capability` its capability, `renderer` the floor's maker, and `settings` `BLAKE3("wai:render-settings\x01" ‖ JCS(settings))` (§3.6). For a supplied floor, `renderer` is `supplied@` and the settings are `{"source": "supplied", "supplied_listing": @@ -633,13 +652,15 @@ sequence header's maximum frame height and width, the component's timebase `num` `den`), then each temporal unit as a `u32` little-endian length and its bytes as the stream holds them. -**7.3** *A supplied floor* is a directory holding, per video component, `.obu`: an -AV1 stream in the low-overhead OBU format (every OBU with its size field), one temporal -unit per source frame. Its listing (SPEC §3.1) of every file is pinned by the policy's -`floor.supplied_listing`. A packager **MUST** refuse, before it writes anything: +**7.3** *A supplied floor* is a directory holding, per floor rung of a video component, +`.obu` (§7.1): an AV1 stream in the low-overhead OBU format (every OBU with +its size field), one temporal unit per source frame. Its listing (SPEC §3.1) of every +file is pinned by the policy's `floor.supplied_listing`. A packager **MUST** refuse, +before it writes anything, the directory and then each floor rung's stream in ladder +order: - a directory whose listing does not hash to `floor.supplied_listing` - (`floor-listing-mismatch`), and a component with no stream in it + (`floor-listing-mismatch`), and a floor rung with no stream in it (`floor-stream-missing`); - a stream that does not split into temporal units each holding a frame, whose first unit holds no sequence header, with a sequence header whose bytes differ from the @@ -652,6 +673,18 @@ unit per source frame. Its listing (SPEC §3.1) of every file is pinned by the p `KEY_FRAME` and `show_frame` 1, or the sequence uses the reduced still-picture header; a unit that shows an existing frame does not. +*Key-frame sequence headers (informative).* A floor taken from an existing ladder without +re-encoding (its CMAF samples demuxed to this form, each with its temporal delimiter put +back) needs a sequence header in every unit that starts at a shown key frame. Where a +ladder's key-frame samples lack one, a producer that puts one back must put back the +stream's own first sequence header OBU, byte for byte, after each such unit's temporal +delimiter: §7.3 refuses any other bytes (`floor-av1-malformed`). The sequence header in +the ladder's sample entry (`av1C`) is not always the stream's: an encoder may write it +before it settles its coding tools, so that it differs from the in-band one in its tool +flags, and a stream with that header put back is refused. The packager writes its own +`av1C` from the in-band header (§7.6). A ladder whose key frames carry no in-band header +cannot be taken in the CMAF form (§7.7), whose units are its samples as they are. + The plan a supplied floor is made against is the **keyframe plan**, the file `plan-.txt`: each piece's first frame in decimal, one per line, each line ended by a line feed. Its pieces' starts are the plan of §4.2, and `wai-pack plan` writes it from the @@ -663,10 +696,10 @@ no tier or `replicate` claim covers it. **7.5** A floor produced by an encoder that is not a pure function of its input (§11) **MUST** name that encoder and its version in `derived_from.renderer`. -**7.6** *CMAF.* The floor of a video component is also written as CMAF files, which no -stage index lists: `floor//init.mp4`, and one fragment per piece, -`floor//.m4s`. Box layouts are ISO/IEC 14496-12's, with the -AV1 binding's sample entry; every integer is big-endian and every time 0: +**7.6** *CMAF.* Each floor rung's floor is also written as CMAF files, which no stage +index lists: `floor//init.mp4`, and one fragment per piece, +`floor//.m4s`. Box layouts are ISO/IEC 14496-12's, +with the AV1 binding's sample entry; every integer is big-endian and every time 0: - `init.mp4`: `ftyp` (major brand `cmfc`, minor version 0, compatible `cmfc`, `iso6`, `av01`), then `moov` holding `mvhd` (version 0, timescale `tb_num`, rate 1, volume 1, @@ -692,6 +725,100 @@ AV1 binding's sample entry; every integer is big-endian and every time 0: The codecs string of the floor is the AV1 binding's short form, `av01...`. +**7.7** *A supplied CMAF floor.* With `floor.supplied_form = "cmaf"`, a supplied floor is +an existing ladder's CMAF files rather than OBU streams: per floor rung of floor name *f* +(§7.1), a directory `/` holding `init.mp4` and the floor's media segments, which are +every other file of `/`, each named `*.m4s` and in no directory below it, in byte order +of their names, at least one. The floor's temporal units are the segments' samples, in +order, each a temporal unit without its temporal delimiter: a unit is the two bytes +`0x12 0x00` (a temporal delimiter OBU with its size field and no extension) followed by the +sample. Boxes are ISO/IEC 14496-12's; a box's 32-bit size is at least 8, a size of 1 is the +64-bit size that follows its type (at least 16), and a size of 0 is refused; a box's +children tile its body: + +- `init.mp4`: its top-level boxes tile the file; one `moov`, holding one `trak` and one + `mvex`; the `trak`'s `tkhd` names the track, its `mdia`'s `mdhd` gives the timescale + *T*, its `hdlr` is of handler type `vide`, and its `stsd` holds one sample entry, of type + `av01`; an `edts`, where the `trak` has one, holds an `elst` of one entry, at media + time 0 and rate 1; and the `mvex` holds one `trex`, of the track, whose defaults a + sample takes. +- a media segment: its top-level boxes tile the file: boxes of the types `styp`, `sidx`, + `prft`, `emsg`, `free` and `skip`, then one or more `moof`, each followed by an `mdat`, + and nothing else. A `moof` holds an `mfhd` and one `traf`, and nothing else; the `traf` + holds one `tfhd` of the track, one `tfdt` and one or more `trun`, and nothing else; the + `tfhd`'s flags are among `0x2`, `0x8`, `0x10`, `0x20` and `0x20000` (no base data + offset, no empty duration); each `trun`'s are among `0x1`, `0x4`, `0x100`, `0x200`, + `0x400` and `0x800`, `0x1` (its data offset) among them. A sample's duration, size and + flags are its `trun`'s (the first sample's flags its first-sample flags where the `trun` + gives them), else the `tfhd`'s defaults, else the `trex`'s; its bytes start at the + `moof`'s first byte plus its `trun`'s data offset, a run's samples one after another, + and lie in the `mdat` after the `moof`. + +A packager **MUST** refuse, before it writes anything, after the directory's listing +(§7.3), floor rung by floor rung in ladder order and segment by segment in order: + +1. a floor rung with no `/init.mp4` (`floor-stream-missing`); another file of `/` + that is not a media segment, or no media segment (`floor-cmaf-malformed`); +2. an `init.mp4` that is not the above (`floor-cmaf-malformed`), or whose timescale is 0 + or whose edit list is another (`floor-cmaf-timing`); +3. a media segment that is not the above, or that holds a sample of no bytes + (`floor-cmaf-malformed`), or a sample with a composition offset other than 0 or a + `moof` whose decode time is not where the one before it ended (`floor-cmaf-timing`), + each where it is read; then one whose first sample is not a sync sample + (`sample_is_non_sync_sample` set) (`floor-cmaf-malformed`); then a sample that does + not last one frame (`duration · num = T · den`), or a segment whose first decode time + is not the frame where the segment before it ended, the first at frame 0 + (`dts · num = frame · T · den`) (`floor-cmaf-timing`); then its units, as §7.3 reads a + stream's (`floor-av1-malformed`, a unit's own faults held to the end); +4. then, as §7.3: a unit count other than the source's frames (`floor-frame-count`), and + the faults held back (`floor-av1-malformed`); +5. other than one media segment per piece of the plan, each holding exactly its piece's + frames (`floor-cmaf-pieces`); +6. a key frame §7.3 refuses (`floor-keyframe-mismatch`). + +The floor's CMAF files are then the supplied ones, byte for byte: `floor//init.mp4` +the supplied `init.mp4`, and per piece `floor//.m4s` its media segment (§7.6's +writer is not used), which §8.3's playlists and MPDs list. The package also carries the +supplied floor's listing as `floor.listing`: its SHA-256 is `floor.supplied_listing`, which +the policy digest, and so the receipt, binds, and it names each of the ladder's files by +its SHA-256, so the receipt binds the ladder's bytes. A server may hold those files once, +for the ladder and the package alike (by a hard link, or by their content address): they +are the same bytes. The floor objects carry the units as §7.2 frames them, as in the OBU +form. + +**7.8** *The floor-only profile.* A component of `profile = "floor-only"` is its floor +rungs alone: no base, no refinement and no source (§3.4 item 8). It is for a library that +delivers its standard-codec renditions as WAI objects, each named by its content hash, +listed in a signed stage index and bound to a signed receipt, without the integer lane. + +- *Its pieces* are its first floor rung's: in the CMAF form (§7.7), one piece per media + segment; in the OBU form (§7.3), one piece from each of the stream's shown key frames + to the next, the stream's first unit one (`floor-keyframe-mismatch` otherwise), and the + first floor rung's unit count is the component's frame count. A piece of more than + `max_ms` (§4.4) is refused (`floor-piece-length`); `min_ms` and `target_ms` do not + apply, and the scene-cut segmenter does not run. Every other floor rung is held to those + pieces as §7.3 and §7.7 hold a floor to a plan. A piece's kind is `none` (§6.2), its + flags 0; a rule that states `when.piece_kind` matches none of them, and its moments are + §8.1's. +- *Its objects*: each piece has one floor object per floor rung (§7.1) and nothing else. + A floor object presents no base, so its manifest is §5.2's for its capability with no + `derived_from`, and its entry's flag bit 2 is clear (§6.3); everything else is §7.2's. + Every class lists the floor only (§8.1): no catalog lists a WAI object, the MoQ catalog + is the floors' tracks, and the package has no WAI playlist or MPD. +- *What it attests*: the receipt signs the policy, so the supplied floor's listing, and + the stage index, so every floor object's content hash, length and capability; a + verifier recomputes the dedup counts, holds every object to its hash and to §7.2's + framing, its units to the AV1 stream's structure (temporal units, one per frame, + sequence headers, shown key frames at the piece starts), and the CMAF files to the + units (§7.6) or, in the CMAF form, to the ladder's bytes through `floor.listing` + (§7.7). A sink that checks an object's hash against the index receives the bytes the + signer packed, or refuses them. +- *What it does not attest*: a floor states no tier (§7.4): no decoded-output digest is + stated or checked, so nothing says what a decoder makes of the units; there is no + source, so no `sse` and no fidelity to anything; there is no primary, so no + `replicate` claim; and the pieces are not classified. It attests delivery, not + pictures. + ## 8. Classes, catalogs and delivery **8.1** *What a class lists.* The stages listed for a class are a pure function of the @@ -704,7 +831,8 @@ catalog bytes. `e_ms > from_ms`. Rule *i* matches it when each condition the rule states holds: its kind is in `when.piece_kind`, and a tag of a moment it lies in is in `when.moment_tag`. Matching rules fold in document order (§3.4). -- A floor-only class lists the floor (`0xFF`). Otherwise the class lists +- A floor-only class lists the floor (`0xFF`), and every class lists only the floor of a + piece with no base, a floor-only component's (§7.8). Otherwise the class lists `min(max_layer, the rules' then.max_layer, the piece's highest layer)`; under `max_kbps`, while the listed prefix needs more than `max_kbps`, that is `ceil(8 · bytes · num / (1000 · duration · den))` kilobits per second over the bytes of @@ -737,18 +865,20 @@ second; a variant's `BANDWIDTH` is the largest over the pieces it lists and `AVERAGE-BANDWIDTH` the rate of all their bytes over all their durations. A floor variant's bytes per piece are its fragment's; a WAI variant's are the listed bases' and refinements', over the pieces its HTTP view lists (above). Every playlist -line ends with a line feed. For component *c*, of frame size `W×H`, whose floor has -codecs string *F* and maximum frame size `FW×FH`: +line ends with a line feed. For component *c*, of frame size `W×H`, and each of its +floor rungs, in ladder order, of floor name *f* (§7.1), codecs string *F* and maximum +frame size `FW×FH`: - `hls/classic/master.m3u8`: `#EXTM3U`, `#EXT-X-VERSION:7`, - `#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-STREAM-INF:BANDWIDTH=,AVERAGE-BANDWIDTH=,CODECS="F",RESOLUTION=FWxFH`, - `../classic/floor-.m3u8`: a URI that names the same playlist from every class's + `#EXT-X-INDEPENDENT-SEGMENTS`, then per floor rung + `#EXT-X-STREAM-INF:BANDWIDTH=,AVERAGE-BANDWIDTH=,CODECS="F",RESOLUTION=FWxFH` and + `../classic/floor-.m3u8`: a URI that names the same playlist from every class's directory, so each class's master differs from this one only by the session-data line, as [http-delivery](http-delivery.md) §11.1 and §11.8 require. -- `hls/classic/floor-.m3u8`: `#EXTM3U`, `#EXT-X-VERSION:7`, +- `hls/classic/floor-.m3u8`, per floor rung: `#EXTM3U`, `#EXT-X-VERSION:7`, `#EXT-X-TARGETDURATION:`, `#EXT-X-MEDIA-SEQUENCE:0`, `#EXT-X-PLAYLIST-TYPE:VOD`, - `#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-MAP:URI="../../floor//init.mp4"`, then per - piece `#EXTINF:,` and `../../floor//.m4s`, then `#EXT-X-ENDLIST`. + `#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-MAP:URI="../../floor//init.mp4"`, then per + piece `#EXTINF:,` and `../../floor//.m4s`, then `#EXT-X-ENDLIST`. - `hls//master.m3u8`: the classical master, byte for byte, with, when the class lists a WAI object over HTTP, `#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8"` after `#EXT-X-INDEPENDENT-SEGMENTS`, and no other change. @@ -759,8 +889,9 @@ codecs string *F* and maximum frame size `FW×FH`: `#EXT-X-STREAM-INF:BANDWIDTH=,AVERAGE-BANDWIDTH=,CODECS="",X-WAI-FLOOR="",RESOLUTION=WxH`, `wai-.m3u8`. The union and the floor are [http-delivery](http-delivery.md) §4.2's over the objects the class lists. -- `hls//wai-.m3u8`: the floor playlist's lines without `#EXT-X-MAP`, adjacent - to it ([http-delivery](http-delivery.md) §11.2); per piece, `#EXT-X-GAP` when the +- `hls//wai-.m3u8`: the first floor rung's playlist's lines without + `#EXT-X-MAP`, adjacent to it ([http-delivery](http-delivery.md) §11.2) and so to every + floor rung's, which list the same pieces; per piece, `#EXT-X-GAP` when the class's HTTP view lists nothing of it (a floor-only piece, or one whose base is not of the default base capability), `#EXTINF:,`, each refinement the class lists, in entry order, as `#WAI-LAYER:N=,URI="../../objects/.wai#n="`, @@ -778,10 +909,13 @@ over the component's whole duration and its target duration, `` and ``, indented by two spaces a level: - `dash/classic.mpd`: `` - holding `` with - ``: ``, + holding, per floor rung, `` with + ``: ``, a `SegmentTimeline` of one `` per - piece, and one `` per piece. + piece, and one `` per piece. With + several floor rungs, the `AdaptationSet` carries no `codecs`, `width` or `height`, and + each `Representation` carries its own after `bandwidth`: + ``. - `dash/.mpd`, when the class's HTTP view lists a WAI object: `` holding ``, @@ -819,9 +953,10 @@ does not hold. A sink receives every track a track depends on (moq-streaming-for §4.1), so a dependency is all of them. A refinement track carries `wai_delivery_timeout_ms` (§8.6), a catalog field this extension defines under the `wai_` prefix of moq-streaming-format §4. The base track has `altGroup` 1 when the component has -a floor: the floor is a track `/floor` of `packaging` `"cmaf"`, in the same namespace, -with `altGroup` 1, `renderGroup` 1, `codec`, `width` and `height`, and a sink that -selects the floor subscribes to no layer track. The receipts tracks follow, one per media +a floor: each floor rung's floor, in ladder order, is a track `/floor` for its floor +name *f* (§7.1), of `packaging` `"cmaf"`, in the same namespace, with `altGroup` 1, +`renderGroup` 1, `codec`, `width` and `height`, and a sink that selects a floor +subscribes to no layer track. The receipts tracks follow, one per media track in its order, each `{"isLive": false, "name": "/receipts", "namespace", "packaging": "wai-receipts"}`. The catalog passes moq-streaming-format §4.1's check, and a packager writes no other. A package holds no receipt: the publisher signs each track's @@ -956,7 +1091,8 @@ figure and class that do not cohere, or a legacy label (`receipt-energy`); a sig that does not verify (`receipt-signature`). **9.2** Live windows chain by the parent link with parent kind `live-window`. A re-pack -of an earlier package **MAY** name it with parent kind `re-pack`. +of an earlier package **MAY** name it with parent kind `re-pack`, and one that takes its +bases and refinements from it **MUST** (§11.5). The reference live packer (`wai-pack live`, `wai_pack::live`) takes a source a frame at a time, cuts it with the segmenter of §4.3, which decides a frame once `lookahead_frames` @@ -1041,23 +1177,28 @@ royalty-free by its standards body"; it is not a freedom-to-operate opinion. policy.toml policy.json plan-.txt index-.wsix package.receipt.json objects/.wai … - floor//init.mp4 floor//.m4s … + floor//init.mp4 floor//.m4s … (per floor rung, of floor name f) + floor.listing (a supplied CMAF floor's listing, §7.7) hls/classic/… hls//… dash/… moq/catalog.json stages/.json claims/.measurement.json (a metered package) LISTING `policy.toml` is the policy as given; `policy.json` its effective canonical JSON (§3.2). `LISTING` is the listing (SPEC §3.1) of every other file, and its SHA-256 is the -**pack digest**. A component or class id that cannot be one path segment of a listing, -two class ids equal once `A`–`Z` are mapped to `a`–`z`, and a class named `classic` in -any case (ids name paths, and paths fold case) are refused (`pack-id`). +**pack digest**. A component or class id or a floor name (§7.1) that cannot be one path +segment of a listing, two class ids or two floor names equal once `A`–`Z` are mapped to +`a`–`z`, and a class named `classic` in any case (ids name paths, and paths fold case) +are refused (`pack-id`). **11.2** *Re-pack equivalence.* Given the same source bytes, effective policy, supplied floor, signing key and packager identity, a conforming reference packager **MUST** produce byte-identical package files on every machine, except the energy figure and measurement claim of a metered package and floor objects §7.5 covers. A verifier **MAY** pack again and compare listings. A packager whose identity differs is not held -to this. +to this. A re-pack from a parent (§11.5) is held to it with the parent among its inputs; +and where the parent was packed by a conforming packager from the same source under base +and refinement rungs of the same capabilities and parameters, its files are those of a +pack that codes every piece, but for its receipt's parent and so `LISTING`. **11.3** *Verification.* A verifier of a package, without its sources, applies these checks in this order and refuses at the first that fails; a check that reads files a @@ -1077,7 +1218,7 @@ failed check names is not applied: effective canonical JSON, `royalty_free_only` forced on where `policy.json` says `true` (`policy-json-mismatch`; an override never turns it off, §3.2, so a `policy.json` that says `false` over a `policy.toml` that says `true` is refused - here); its ids can name files (`pack-id`, §11.1); and under that effective policy it + here); its ids and floor names can name files (`pack-id`, §11.1); and under that effective policy it asks for nothing a packager may not emit (§3.6's reason; under `royalty_free_only`, a patent-pool floor is `license-refused`). 5. The receipt's components are the policy's, by id, in order, each of the policy's @@ -1089,18 +1230,20 @@ failed check names is not applied: class are the component's; its string table is the component's id, the class ids in policy order, then each capability at its first use; its class records are the policy's classes, in order, with their `max_layer`, floor-only flag and `max_kbps`; - every piece's kind is one a video piece has; its rules and moments are §8.1's from + every piece's kind is one a video piece has, `none` exactly in a floor-only + component (§7.8); its rules and moments are §8.1's from its kind and span; its entries are the rungs of its ladder (§5.7), by role, - capability and layer: its base, each refinement in layer order, each floor; each + capability and layer: its base, each refinement in layer order, each floor (a + floor-only component's, its floors alone); each refinement's deadline is its plan's (§8.6); and its class plan is §8.1's from its entries; - the plan file holds the index's starts and its digest is the header's (`plan-mismatch`); - every entry's object is present (`object-missing`) and keeps the object rules of §6.3, its counted bytes included; - - before any frame is decoded, each piece's floor object, where the policy has a floor - rung and the object's payload reads as §7.2's framing, holds one unit per frame of - its piece (`floor-object-mismatch`); + - before any frame is decoded, each piece's floor objects, where the policy has floor + rungs and an object's payload reads as §7.2's framing, hold one unit per frame of + their piece (`floor-object-mismatch`); - every base and refinement, in entry order: a base decodes by its capability (a still is a PNG §5.7 reads, its one frame held for the piece), or it is `prefix-digest`; its manifest is the one §5.2 states for its piece's frames and its @@ -1111,15 +1254,24 @@ failed check names is not applied: does not apply, ends the check; - then every decode-equivalent prefix decodes to its `prefix_digest` (`prefix-digest`); - - when all of that holds, the floor: per piece, one floor object of the floor rung's - capability whose renderer, settings, framing header, unit count and target are - §7.2's (`floor-object-mismatch`) and which presents the piece's base - (`floor-motif-mismatch`); its units each one temporal unit holding a frame, the - first of the first piece carrying a sequence header, every sequence header the - first's bytes, every shown key frame carrying one (`floor-av1-malformed`); each - piece opening on a shown key frame, and no other unit being one unless the policy - allows extra key frames (`floor-keyframe-mismatch`); and the CMAF files are the - ones its temporal units give (§7.6, `floor-cmaf-mismatch`); + - when all of that holds, the floors, floor rung by floor rung in ladder order: per + piece, one floor object per floor rung, the rung's the *k*-th floor entry for the + *k*-th floor rung, of the rung's capability, whose renderer, settings, framing + header, unit count and target are §7.2's (`floor-object-mismatch`) and which + presents the piece's base (`floor-motif-mismatch`), or, in a floor-only component, + which carries no `derived_from` (`floor-object-mismatch`); its units each one temporal unit + holding a frame, the first of the rung's first piece carrying a sequence header, + every sequence header the first's bytes, every shown key frame carrying one + (`floor-av1-malformed`); each piece opening on a shown key frame, and no other unit + being one unless the policy allows extra key frames (`floor-keyframe-mismatch`); and + the rung's CMAF files are the ones its temporal units give (§7.6, + `floor-cmaf-mismatch`); or, for a supplied floor in the CMAF form (§7.7), `floor.listing` + is a listing that hashes to `floor.supplied_listing`, `floor//init.mp4` is the file + of its line `/init.mp4` and reads as §7.7's initialization segment, and the + listing's media segments of `/`, in its order, are one per piece, piece *k*'s + `floor//.m4s`, reading as §7.7's media segment, opening on a sync + sample, each sample one frame and the first at the piece's start, its samples the + piece's floor units without their temporal delimiters (`floor-cmaf-mismatch`); - then the catalogs are the ones the index, the objects and the floor give, and an index no catalog can be written for (a start past `2^53 − 1`) is refused (`catalog-mismatch`). @@ -1131,7 +1283,9 @@ failed check names is not applied: **11.4** *What this revision packs.* The reference packager packs one video component: bases of `wai.video.int_motion` (§5.5, §5.6) and stills of `wai.image.png` (§5.7); refinements of `wai.video.int_refine` over an `int_motion` base (§5.4); each piece by its -ladder, a rule's ladder subset included (§5.7); and a supplied AV1 floor (§7.3). Its live +ladder, a rule's ladder subset included (§5.7); and a supplied AV1 floor per floor rung, +one or several (§7.1), in the OBU form (§7.3) or the CMAF form (§7.7); and floor-only +components (§7.8). Its live packer packs the same pieces in windows, without a floor (§9.2). It refuses by name, before writing anything, what it does not pack yet: another medium (`pack-media-unsupported`), a floor to encode (`pack-floor-encode-unsupported`), and a rung @@ -1139,33 +1293,79 @@ of a capability it does not encode, or a refinement over a base it does not refi (`pack-capability-unsupported:`, every one naming the capability of the rung it does not pack); and a component with no source (`pack-source-missing`) or whose source's frame rate is not its timebase (`pack-source-rate`). Where its own work fails it -refuses by name too, and writes nothing: an encode the encoder refuses, such as a piece +refuses by name too, and leaves nothing written (a file it wrote before the refusal is +removed): an encode the encoder refuses, such as a piece past the samples it holds, or an object it wrote that does not count (`pack-encode`); a piece past `2^32 − 1` units (`pack-piece-length`); catalogs it cannot write, the case §11.3 refuses as `catalog-mismatch` (`pack-catalog`); a meter that was asked for and gives no figure, a span too short to reach a microjoule or a figure past `2^64 − 1` (`pack-meter`, never an unmetered package in its place); a measurement claim it cannot -write (`pack-measurement-claim`); and a listing it cannot write (`pack-listing`). It +write (`pack-measurement-claim`); a listing it cannot write (`pack-listing`); and a source +that does not read the same twice (`pack-source-changed`) or a supplied floor stream that +does not (`floor-listing-mismatch`): it reads each a frame or a temporal unit at a time, +once to cut and check it and once to code it, and holds neither whole. What it holds is +not a function of the source's length but for a record per piece and per entry and a byte +per frame of each floor (whether it starts at a key frame): the frames of the pieces it +codes at once, which it chooses, each piece coded alone (§4.1), so the package is the same +for any number; one piece of each floor; and, for the catalogs, one object at a time, +read back from where it wrote it. Each file is written whole as it is made. It writes a package only into a directory that is new or holds nothing, and refuses any other before it packs (`pack-out-not-empty`): written among another package's files, it would leave behind the ones it does not write. +**11.5** *Re-pack from a parent.* A packager **MAY** take every piece's base and +refinements from an earlier package, its **parent**, instead of coding them, so a +component's integer base is coded once and taken by every package made from the same +source after it. It reads the source as for a pack that codes (§4: its pieces and their +kinds, §4.3, §4.6, are this source's), and then, before it writes anything, **MUST** +refuse: + +1. a parent that does not verify as §11.3 verifies a package (`repack-parent-invalid`, + with the reason the parent fails for); +2. a parent with no component of the component's id, media and timebase, or whose pieces + are not this source's, piece by piece: another number of pieces, or a piece of + another start, duration, flags or kind, or whose ladder (§5.7, under the parent's + policy) codes its base and refinements in rungs of other capabilities or other + parameters (§3.3) than this policy's ladder for the piece, in layer order, or with + another deadline (§8.6) (`repack-parent-mismatch`); +3. a piece whose prefixes, each the parent's base decoded and its refinements applied in + layer order by a staged-delivery §4 sink, do not reconstruct this source with the + `sse` the parent's entry states (§6.6) (`repack-source-mismatch`): a parent packed + from another source. + +It then writes, for each piece, the parent's base and refinement objects and their +entries' fields (§6.2: content hash, lengths, capability, role, layer, tier, prefix +digest, `sse`, deadline) in place of coding them, and the floors as it would; its +receipt names the parent's receipt with parent kind `re-pack` (§9.1). A verifier holding +the parent **MAY** check a re-pack against it: the parent verifies +(`repack-parent-invalid`), the re-pack's receipt names the parent's receipt with parent +kind `re-pack`, and each component's pieces are the parent's, piece by piece, of the same +start, duration, kind and flags, whose base and refinement entries are the parent's +entries, every field the same but the entry's place in its index: the entry it refines +counted from its piece's first entry, and the duplicate flag, which an index's own +entries decide (`repack-parent-mismatch`). A re-pack holds no source of its parent's +bases; what binds a reused base to this source is the third check, and what binds it to +the parent is the receipt. + ## 12. Reasons | Reason | Where | |---|---| | `policy-toml` (not UTF-8, a byte-order mark, not TOML 1.0, nesting past 32), `policy-float`, `policy-datetime`, `policy-negative`, `policy-integer-range` | §3.2 | | `policy-unknown-key`, `policy-missing-key`, `policy-type`, `policy-value`, `policy-schema` | §3.3 | -| `policy-too-many-components`, `policy-too-many-rungs`, `policy-too-many-classes`, `policy-too-many-rules`, `policy-too-many-moments`, `policy-duplicate-id`, `policy-lead`, `policy-open-gop`, `policy-piece-bounds`, `policy-ladder-component`, `policy-refines-role`, `policy-dangling-refines`, `policy-refines-cycle`, `policy-refines-base`, `policy-refines-branch`, `policy-no-base`, `policy-class-layer`, `policy-rule-ladder`, `policy-layer-lag`, `policy-too-many-deadlines`, `policy-deadline-unreachable` | §3.4 | +| `policy-too-many-components`, `policy-too-many-rungs`, `policy-too-many-classes`, `policy-too-many-rules`, `policy-too-many-moments`, `policy-duplicate-id`, `policy-lead`, `policy-open-gop`, `policy-piece-bounds`, `policy-ladder-component`, `policy-refines-role`, `policy-dangling-refines`, `policy-refines-cycle`, `policy-refines-base`, `policy-refines-branch`, `policy-no-base`, `policy-floor-only`, `policy-class-layer`, `policy-rule-ladder`, `policy-layer-lag`, `policy-too-many-deadlines`, `policy-deadline-unreachable` | §3.4 | | `capability-unknown`, `capability-not-payload`, `capability-candidate`, `capability-role`, `license-unclassified`, `license-refused`, each naming the capability (and the licence label) | §3.6 | | `y4m-header`, `y4m-interlaced`, `y4m-colorspace`, `y4m-size-change`, `y4m-truncated`, `wav-format`, `wav-truncated` | §4.7 | | `index-magic`, `index-version`, `index-truncated`, `index-reserved`, `index-media-class`, `index-role`, `index-tier`, `index-string`, `index-trailing` | §6.2 | | `index-timebase`, `window-start`, `piece-order`, `piece-entries`, `piece-kind`, `entry-flags`, `entry-order`, `layer-role`, `refines-outside-window`, `refines-mismatch`, `layer-gap`, `tier-entropy-passthrough`, `tier-weakest`, `tier-mismatch`, `digest-without-tier`, `deadline-role`, `floor-derived-from`, `dup-length`, `dup-flag`, `class-plan`, `class-flags` | §6.3, §6.4 | | `object-length`, `object-hash`, `non-envelope-entry`, `object-lengths`, `object-capability`, `object-refines`, `object-derived-from` | §6.3 | | `floor-missing`, `floor-listing-mismatch`, `floor-stream-missing`, `floor-av1-malformed`, `floor-frame-count`, `floor-keyframe-mismatch` | §7.1, §7.3 | +| `floor-stream-missing`, `floor-cmaf-malformed`, `floor-cmaf-timing`, `floor-cmaf-pieces` | §7.7 | +| `floor-keyframe-mismatch`, `floor-piece-length` | §7.8 | | `floor-object-mismatch`, `floor-motif-mismatch`, `floor-av1-malformed`, `floor-keyframe-mismatch`, `floor-cmaf-mismatch`, against a package | §11.3 | | `receipt-json`, `receipt-version`, `receipt-index-count`, `receipt-index-digest`, `receipt-stage-root`, `receipt-component`, `receipt-policy-digest`, `receipt-counts`, `receipt-parent-kind`, `receipt-energy`, `receipt-signature` | §9.1 | | `measurement-claim-missing`, `measurement-claim-invalid` | §9.4 | | `unexpected-file`, `listing-missing`, `listing-mismatch`, `policy-digest-mismatch`, `policy-json-mismatch`, `policy-component-mismatch`, `index-missing`, `index-policy-mismatch`, `plan-mismatch`, `object-missing`, `object-manifest`, `prefix-digest`, `catalog-mismatch`, `catalog-unexpected`, `object-unlisted` | §11.3 | | `pack-id`, `pack-source-missing`, `pack-source-rate`, `pack-media-unsupported`, `pack-floor-encode-unsupported`, `pack-capability-unsupported` (naming the capability) | §11.1, §11.4 | -| `pack-encode`, `pack-piece-length`, `pack-catalog`, `pack-meter`, `pack-measurement-claim`, `pack-listing`, `pack-out-not-empty` | §11.4 | +| `pack-encode`, `pack-piece-length`, `pack-catalog`, `pack-meter`, `pack-measurement-claim`, `pack-listing`, `pack-source-changed`, `pack-out-not-empty` | §11.4 | +| `repack-parent-invalid`, `repack-parent-mismatch`, `repack-source-mismatch` | §11.5 | diff --git a/wai/http-conformance/http_verify.py b/wai/http-conformance/http_verify.py index e28253049..a463b92bb 100644 --- a/wai/http-conformance/http_verify.py +++ b/wai/http-conformance/http_verify.py @@ -70,8 +70,26 @@ def blake3(b): return B3.blake3(b) +# Each object's content hash, computed once: an object resolved many times (resolve_uri, +# once per URI that names it) is hashed once. A caller that has checked an object's hash +# against its own record may seed it (seed_content_hash). +_CONTENT_HASH = {} + + def content_hash(env): - return blake3(DOMAIN + env) + h = _CONTENT_HASH.get(env) + if h is None: + h = _CONTENT_HASH[env] = blake3(DOMAIN + env) + return h + + +def seed_content_hash(env, h): + """Record an object's content hash that the caller has computed and checked.""" + _CONTENT_HASH[env] = h + + +def forget_content_hashes(): + _CONTENT_HASH.clear() # ------------------------------------------------------------------ the registry @@ -3035,7 +3053,11 @@ def resolve_uri(objs, uri): name = base.rsplit("/", 1)[-1] if name.endswith(".wai") and re.fullmatch(r"[0-9a-f]{64}", name[:-4]): cid = name[:-4] - b = next((b for b in objs.values() if cid is not None and content_hash(b).hex() == cid), None) + # An object held under its own content id is found by name, its hash checked; any + # other by its content hash, each object hashed once (content_hash). + b = objs.get(f"{cid}.wai") if cid is not None else None + if b is None or content_hash(b).hex() != cid: + b = next((b for b in objs.values() if cid is not None and content_hash(b).hex() == cid), None) if b is None: raise SystemExit("http_verify: %s names no corpus object" % uri) return b diff --git a/wai/pack-conformance/README.md b/wai/pack-conformance/README.md index 375ff2408..2f26ee8c0 100644 --- a/wai/pack-conformance/README.md +++ b/wai/pack-conformance/README.md @@ -57,6 +57,43 @@ packages (§6–§11), and their delivery over a simulated link (§8.9). written by `supplied_floor.py`. The bumper's base, layers and floor are one object each; the still's piece is carried over HTTP by the floor (§8.3) and over MoQ on the base track. +- `packs/video-ladder-01/` — a golden package of two floor rungs over one base (§7.1): + the fixture and base rung of `video-base-01`, so its bases are that package's objects, + and two supplied AV1 streams, `floor/hi.obu` at the fixture's size and `floor/lo.obu` + at half its width and height, each written by `supplied_floor.py` (`--scale lo=2`). + Each piece has its base and one floor object per floor rung; each floor has its own + CMAF files under its floor name (`floor/hi/`, `floor/lo/`), its own floor playlist and + MoQ track, and the classical master, every class's master and `dash/classic.mpd` list + both. +- `packs/video-repack-01/` — a re-pack golden (§11.5): `video-ladder-01`'s policy under + another asset id and its supplied floor, packed with every piece's base taken from the + parent its file `parent` names, `video-base-01`, rather than coded. Its objects are + `video-ladder-01`'s; its receipt names `video-base-01`'s receipt as the package it + re-packs (parent kind `re-pack`); and each piece's base entry is the parent's, field for + field. Both verifiers check it against its parent. +- `packs/video-cmaf-01/` — a golden whose supplied floor is in the CMAF form (§7.7): + `video-ladder-01`'s policy with `floor.supplied_form = "cmaf"`, and its two streams as a + media muxer wrote them, `floor//init.mp4` and `floor//seg_.m4s`, one + segment per piece, by `supplied_cmaf.py` (ffmpeg's HLS muxer, `-c copy`). The package's + floor files are those files, byte for byte, under their SHA-256; `floor.listing` is the + supplied floor's listing; and each floor object holds its piece's samples, which are the + OBU form's temporal units of the same streams. +- `cmaf-mutations/` — the CMAF golden altered, in `package-mutations/`'s layout: itself + (`ok`); a segment replaced by one of the same samples, a `free` box before it, under its + own SHA-256; `floor.listing` altered; and without it (each `floor-cmaf-mismatch`). +- `packs/video-floor-only-01/` and `packs/video-floor-only-02/` — floor-only goldens + (§7.8): a component of `profile = "floor-only"` with the ladder's two floor rungs and no + base or source, the floor in the CMAF form (`video-cmaf-01`'s files) and in the OBU form + (`video-ladder-01`'s streams). Their pieces are the first floor's segments or shown key + frames, the ladder golden's plan; each piece is its two floor objects, which carry no + `derived_from` and hold the ladder golden's units, of kind `none`; no class lists a WAI + object. +- `ladder-mutations/` — packages a verifier must refuse or accept (§11.3), each the + ladder golden altered (re-sealed with its key where the receipt or `LISTING` alone + would catch it), in `package-mutations/`'s layout: the golden itself (`ok`); piece 2's + two floor objects swapped (`floor-object-mismatch`); the second floor's CMAF files + under the component's name (`floor-cmaf-mismatch`); and a classical master listing one + floor of two (`catalog-mismatch`). - `mutations/` — stage indexes, objects and version-2 receipts, and `expected.json`: each case's reason (§6.2–§6.4, §9.1), a well-formed case of each kind among them, and three receipts carrying a signature only a verifier that checks the equation @@ -86,7 +123,9 @@ packages (§6–§11), and their delivery over a simulated link (§8.9). window past 32 KiB, bytes after the zlib stream, and its Adler-32. And one dynamic block of literals twice: with a lone distance code of one bit, read (the one incomplete code set the zlib library takes), and of two bits, refused. -- `staged-mutations/` — the staged golden altered and re-sealed with its key, in the +- `staged-mutations/` — the staged golden itself (`ok`: verified without its sources, + every layer's prefix digest held to it), and the staged golden altered and re-sealed + with its key, in the layout of `package-mutations/`: a refinement on another deadline than its plan's, a piece missing its top layer, a still where the policy's rule now gives the piece the default ladder (each `index-policy-mismatch`), a top layer coded again at another step @@ -115,7 +154,10 @@ packages (§6–§11), and their delivery over a simulated link (§8.9). off among them), a missing or corrupted object, a live window no catalog can be written for, floor objects that do not open on a key frame or are not §7.2's, files and directories a strict read finds, and a receipt file that is not its canonical - form; and one the verifier must accept (an extra key frame the policy allows). + form; and those the verifier must accept (an extra key frame the policy allows, and a + metered package, §9.4: the golden re-sealed with a 500 mW model meter over one second + and its measurement claim, every member fixed), with a claim about another figure + (`measurement-claim-invalid`) and none (`measurement-claim-missing`). The fixtures are not committed. `wai-pack fixture gen ` writes each from its definition (`wai-pack/src/fixture.rs`), every byte an integer function of the frame diff --git a/wai/pack-conformance/cmaf-mutations/expected.json b/wai/pack-conformance/cmaf-mutations/expected.json new file mode 100644 index 000000000..b6544e5fb --- /dev/null +++ b/wai/pack-conformance/cmaf-mutations/expected.json @@ -0,0 +1,30 @@ +{ + "pack": "video-cmaf-01", + "cases": [ + { + "name": "ok", + "reason": "ok", + "remove": [] + }, + { + "name": "segment-not-the-supplied-one", + "reason": "floor-cmaf-mismatch", + "remove": [ + "floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s", + "hls/classic/floor-hi.m3u8" + ] + }, + { + "name": "floor-listing-altered", + "reason": "floor-cmaf-mismatch", + "remove": [] + }, + { + "name": "floor-listing-missing", + "reason": "floor-cmaf-mismatch", + "remove": [ + "floor.listing" + ] + } + ] +} diff --git a/wai/pack-conformance/cmaf-mutations/floor-listing-altered/LISTING b/wai/pack-conformance/cmaf-mutations/floor-listing-altered/LISTING new file mode 100644 index 000000000..cd376b57e --- /dev/null +++ b/wai/pack-conformance/cmaf-mutations/floor-listing-altered/LISTING @@ -0,0 +1,47 @@ +688cb303ccb16fcdd21c8cb681f936ba813829e71fbf15df0ccb28b04c134447 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +4e4d6d55b352c17da028bcbf8d314cdd1f9bb1406367008710788665727dcffa floor.listing +14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889 floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s +759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1 floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s +d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s +e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s +eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8 floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s +a8a19ef9cd4d540037ef298097bbc5317de6a99690c8c4a745cc867c034208d6 floor/hi/init.mp4 +0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s +10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8 floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s +2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73 floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s +a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8 floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s +c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s +6dd501e27365ce9ef90576771aad73de4d200362e6974551847afc3c90604344 floor/lo/init.mp4 +ebdc7e40aecbe8b61124ac78f233b0dfaf2a8c5674d3fc9bb023935de1d9b6cb hls/classic/floor-hi.m3u8 +66f5eb8b89b80e7da839b23f39665eb85480c6a725fa29d93556efee673c1b9e hls/classic/floor-lo.m3u8 +d793737b69231eface4951001e65b3c52cba864f231246a41e64d49c77ebdc97 hls/classic/master.m3u8 +d793737b69231eface4951001e65b3c52cba864f231246a41e64d49c77ebdc97 hls/legacy/master.m3u8 +8c3b5e2b18192b383c8df8675acd435b861a2585bc6eaf0c09292754f44a8cef hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +8c3b5e2b18192b383c8df8675acd435b861a2585bc6eaf0c09292754f44a8cef hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +649330bcfaaee54f802f6b4ddf6e459daca8bb0e8df86c87a9d6bd0d65cd09ea index-v.wsix +68f00abff5ef19f074d510f8cce4592afc2a1d4981a87663965a46dc8bcbe1d3 moq/catalog.json +9f381e2ab764b5e14770b00503c1c90b9c22ba22f87464b2869a70644af11939 objects/08797901ff55d0bb4afba5cffe95df64b7e216f3a0c9ba82006133bb9b6d7f16.wai +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +d5f9cdd996920e07339b79093b9e4e5cb4237f1780f268220cc6f89c79aea90a objects/399048a078b0f2983149266a44eaf207cfa63f490212b69389fba4d72f62c60f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +d03f0813c266294f84483cfe4db177d094c06b95ab3a8471d6b2ec53401f05a7 objects/a236e9070d10fd09342cafdbf6973edd35e5266cf850fe2f38f77536ac803912.wai +2316616549f7f92dc29e34fe656c332c9f4a1e20f4be26396976407acc792ba4 objects/a3ae43406f9966487b6c5f5a4d56e9dca50c327423fcfd2a98f4c342d1dbfaf9.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +6d665f1bef3784cbb428abc28724f74fa83c8f2bc812828621c93c5514b6dc0e objects/c5253efe2949669bb9df01d9f60c01f2570ba4be1c0af26105e092dfccf90d30.wai +0acecba00acc28e313805189fd826ad3a9fab1fe69727ef2d5999fc543426289 objects/c81171fa53998cfe94207ad66ab9b53cfbd4b3f058730e62512ca9f0f6ef2ea3.wai +f3bd9cbe3c2518c997a4d1d3a32145a04f55ef53e0b5760a6ce6aa5e14fe06a7 objects/dea0f6415f639fca99d91a0341aba164078a1bd4780e21d72cbf2234928272ec.wai +d22c4f8ac8a5de43bbec6be04ea8b6da72f2bd133f8f57a31607362670178c68 objects/f467872fe8d3cd1f60f832c997cc14fd00abbf601c61286a612089b4a53a195a.wai +f1f4e32ba9f76e36431d97e0fd52b9025075f553bf6b9f63c484d60573c8ea90 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +428e24f9e1c62fc5590ce3d3a6239a5c274dcfc1391a2ea2abe8dc0562da7d20 policy.json +9d2b4c15ac17ee074f646d0b1ed11ef56557f633ff67a3e6a6d0eb4dd1596548 policy.toml +2f5807b891b8eb0a44b76c4303487be1dcf19e111f06f6c642e14b81c85d7c4d stages/legacy.json +8695fff48ca86321c5198e3a8cf660ccba208df4f40fd1907e76c64a52674cf5 stages/mobile.json +6ae56077cfc368a59489233eeee6ee7fd97437821822637ac62096ab93c85286 stages/tv.json diff --git a/wai/pack-conformance/cmaf-mutations/floor-listing-altered/floor.listing b/wai/pack-conformance/cmaf-mutations/floor-listing-altered/floor.listing new file mode 100644 index 000000000..1875b8922 --- /dev/null +++ b/wai/pack-conformance/cmaf-mutations/floor-listing-altered/floor.listing @@ -0,0 +1,12 @@ +a8a19ef9cd4d540037ef298097bbc5317de6a99690c8c4a745cc867c034208d6 hi/init.mp4 +14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889 hi/seg_0000.m4s +e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e hi/seg_0001.m4s +d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d hi/seg_0002.m4s +eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8 hi/seg_0003.m4s +759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1 hi/seg_0004.m4s +6dd501e27365ce9ef90576771aad73de4d200362e6974551847afc3c90604344 lo/init.mp4 +0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af lo/seg_0000.m4s +c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc lo/seg_0001.m4s +10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8 lo/seg_0002.m4s +a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8 lo/seg_0003.m4s +20484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73 lo/seg_0004.m4s diff --git a/wai/pack-conformance/cmaf-mutations/floor-listing-missing/LISTING b/wai/pack-conformance/cmaf-mutations/floor-listing-missing/LISTING new file mode 100644 index 000000000..3efd7408d --- /dev/null +++ b/wai/pack-conformance/cmaf-mutations/floor-listing-missing/LISTING @@ -0,0 +1,46 @@ +688cb303ccb16fcdd21c8cb681f936ba813829e71fbf15df0ccb28b04c134447 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889 floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s +759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1 floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s +d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s +e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s +eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8 floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s +a8a19ef9cd4d540037ef298097bbc5317de6a99690c8c4a745cc867c034208d6 floor/hi/init.mp4 +0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s +10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8 floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s +2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73 floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s +a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8 floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s +c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s +6dd501e27365ce9ef90576771aad73de4d200362e6974551847afc3c90604344 floor/lo/init.mp4 +ebdc7e40aecbe8b61124ac78f233b0dfaf2a8c5674d3fc9bb023935de1d9b6cb hls/classic/floor-hi.m3u8 +66f5eb8b89b80e7da839b23f39665eb85480c6a725fa29d93556efee673c1b9e hls/classic/floor-lo.m3u8 +d793737b69231eface4951001e65b3c52cba864f231246a41e64d49c77ebdc97 hls/classic/master.m3u8 +d793737b69231eface4951001e65b3c52cba864f231246a41e64d49c77ebdc97 hls/legacy/master.m3u8 +8c3b5e2b18192b383c8df8675acd435b861a2585bc6eaf0c09292754f44a8cef hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +8c3b5e2b18192b383c8df8675acd435b861a2585bc6eaf0c09292754f44a8cef hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +649330bcfaaee54f802f6b4ddf6e459daca8bb0e8df86c87a9d6bd0d65cd09ea index-v.wsix +68f00abff5ef19f074d510f8cce4592afc2a1d4981a87663965a46dc8bcbe1d3 moq/catalog.json +9f381e2ab764b5e14770b00503c1c90b9c22ba22f87464b2869a70644af11939 objects/08797901ff55d0bb4afba5cffe95df64b7e216f3a0c9ba82006133bb9b6d7f16.wai +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +d5f9cdd996920e07339b79093b9e4e5cb4237f1780f268220cc6f89c79aea90a objects/399048a078b0f2983149266a44eaf207cfa63f490212b69389fba4d72f62c60f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +d03f0813c266294f84483cfe4db177d094c06b95ab3a8471d6b2ec53401f05a7 objects/a236e9070d10fd09342cafdbf6973edd35e5266cf850fe2f38f77536ac803912.wai +2316616549f7f92dc29e34fe656c332c9f4a1e20f4be26396976407acc792ba4 objects/a3ae43406f9966487b6c5f5a4d56e9dca50c327423fcfd2a98f4c342d1dbfaf9.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +6d665f1bef3784cbb428abc28724f74fa83c8f2bc812828621c93c5514b6dc0e objects/c5253efe2949669bb9df01d9f60c01f2570ba4be1c0af26105e092dfccf90d30.wai +0acecba00acc28e313805189fd826ad3a9fab1fe69727ef2d5999fc543426289 objects/c81171fa53998cfe94207ad66ab9b53cfbd4b3f058730e62512ca9f0f6ef2ea3.wai +f3bd9cbe3c2518c997a4d1d3a32145a04f55ef53e0b5760a6ce6aa5e14fe06a7 objects/dea0f6415f639fca99d91a0341aba164078a1bd4780e21d72cbf2234928272ec.wai +d22c4f8ac8a5de43bbec6be04ea8b6da72f2bd133f8f57a31607362670178c68 objects/f467872fe8d3cd1f60f832c997cc14fd00abbf601c61286a612089b4a53a195a.wai +f1f4e32ba9f76e36431d97e0fd52b9025075f553bf6b9f63c484d60573c8ea90 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +428e24f9e1c62fc5590ce3d3a6239a5c274dcfc1391a2ea2abe8dc0562da7d20 policy.json +9d2b4c15ac17ee074f646d0b1ed11ef56557f633ff67a3e6a6d0eb4dd1596548 policy.toml +2f5807b891b8eb0a44b76c4303487be1dcf19e111f06f6c642e14b81c85d7c4d stages/legacy.json +8695fff48ca86321c5198e3a8cf660ccba208df4f40fd1907e76c64a52674cf5 stages/mobile.json +6ae56077cfc368a59489233eeee6ee7fd97437821822637ac62096ab93c85286 stages/tv.json diff --git a/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/LISTING b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/LISTING new file mode 100644 index 000000000..2da60b651 --- /dev/null +++ b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/LISTING @@ -0,0 +1,46 @@ +489923f27fa22f927813026d34acb69177b5eb042a513e8855b6bfaca1c80a1a dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +03ab06e026880875581952f91232405077be9a522dbf739b6a9d093458369710 floor.listing +103573aca98afc526a45518dc6b93d0fcd58511bb34550b2f4d7b29431df9ad9 floor/hi/103573aca98afc526a45518dc6b93d0fcd58511bb34550b2f4d7b29431df9ad9.m4s +759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1 floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s +d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s +e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s +eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8 floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s +a8a19ef9cd4d540037ef298097bbc5317de6a99690c8c4a745cc867c034208d6 floor/hi/init.mp4 +0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s +10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8 floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s +2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73 floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s +a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8 floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s +c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s +6dd501e27365ce9ef90576771aad73de4d200362e6974551847afc3c90604344 floor/lo/init.mp4 +66f5eb8b89b80e7da839b23f39665eb85480c6a725fa29d93556efee673c1b9e hls/classic/floor-lo.m3u8 +9329fd005fc0e4926a3bc3d19f35b67fb6990c916b8a39cde128d8cf8b3dcf25 hls/classic/master.m3u8 +9329fd005fc0e4926a3bc3d19f35b67fb6990c916b8a39cde128d8cf8b3dcf25 hls/legacy/master.m3u8 +b650f6ffe5e31f29507e563fe104d5d45bf063fe553021047c35a6e0e35e9333 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +b650f6ffe5e31f29507e563fe104d5d45bf063fe553021047c35a6e0e35e9333 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +649330bcfaaee54f802f6b4ddf6e459daca8bb0e8df86c87a9d6bd0d65cd09ea index-v.wsix +f56f7b8a585c4e85c4382eb163d8d6639284ddb2e7f2799715fa63120e7c0036 moq/catalog.json +9f381e2ab764b5e14770b00503c1c90b9c22ba22f87464b2869a70644af11939 objects/08797901ff55d0bb4afba5cffe95df64b7e216f3a0c9ba82006133bb9b6d7f16.wai +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +d5f9cdd996920e07339b79093b9e4e5cb4237f1780f268220cc6f89c79aea90a objects/399048a078b0f2983149266a44eaf207cfa63f490212b69389fba4d72f62c60f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +d03f0813c266294f84483cfe4db177d094c06b95ab3a8471d6b2ec53401f05a7 objects/a236e9070d10fd09342cafdbf6973edd35e5266cf850fe2f38f77536ac803912.wai +2316616549f7f92dc29e34fe656c332c9f4a1e20f4be26396976407acc792ba4 objects/a3ae43406f9966487b6c5f5a4d56e9dca50c327423fcfd2a98f4c342d1dbfaf9.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +6d665f1bef3784cbb428abc28724f74fa83c8f2bc812828621c93c5514b6dc0e objects/c5253efe2949669bb9df01d9f60c01f2570ba4be1c0af26105e092dfccf90d30.wai +0acecba00acc28e313805189fd826ad3a9fab1fe69727ef2d5999fc543426289 objects/c81171fa53998cfe94207ad66ab9b53cfbd4b3f058730e62512ca9f0f6ef2ea3.wai +f3bd9cbe3c2518c997a4d1d3a32145a04f55ef53e0b5760a6ce6aa5e14fe06a7 objects/dea0f6415f639fca99d91a0341aba164078a1bd4780e21d72cbf2234928272ec.wai +d22c4f8ac8a5de43bbec6be04ea8b6da72f2bd133f8f57a31607362670178c68 objects/f467872fe8d3cd1f60f832c997cc14fd00abbf601c61286a612089b4a53a195a.wai +f1f4e32ba9f76e36431d97e0fd52b9025075f553bf6b9f63c484d60573c8ea90 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +428e24f9e1c62fc5590ce3d3a6239a5c274dcfc1391a2ea2abe8dc0562da7d20 policy.json +9d2b4c15ac17ee074f646d0b1ed11ef56557f633ff67a3e6a6d0eb4dd1596548 policy.toml +2f5807b891b8eb0a44b76c4303487be1dcf19e111f06f6c642e14b81c85d7c4d stages/legacy.json +8695fff48ca86321c5198e3a8cf660ccba208df4f40fd1907e76c64a52674cf5 stages/mobile.json +6ae56077cfc368a59489233eeee6ee7fd97437821822637ac62096ab93c85286 stages/tv.json diff --git a/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/dash/classic.mpd b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/dash/classic.mpd new file mode 100644 index 000000000..844c42e97 --- /dev/null +++ b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/dash/classic.mpd @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/floor/hi/103573aca98afc526a45518dc6b93d0fcd58511bb34550b2f4d7b29431df9ad9.m4s b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/floor/hi/103573aca98afc526a45518dc6b93d0fcd58511bb34550b2f4d7b29431df9ad9.m4s new file mode 100644 index 000000000..ed7fdd945 Binary files /dev/null and b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/floor/hi/103573aca98afc526a45518dc6b93d0fcd58511bb34550b2f4d7b29431df9ad9.m4s differ diff --git a/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/hls/classic/master.m3u8 b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/hls/classic/master.m3u8 new file mode 100644 index 000000000..3bd645250 --- /dev/null +++ b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/hls/classic/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=8152,AVERAGE-BANDWIDTH=7583,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/hls/legacy/master.m3u8 b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/hls/legacy/master.m3u8 new file mode 100644 index 000000000..3bd645250 --- /dev/null +++ b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/hls/legacy/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=8152,AVERAGE-BANDWIDTH=7583,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/hls/mobile/master.m3u8 b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/hls/mobile/master.m3u8 new file mode 100644 index 000000000..560e7e8ed --- /dev/null +++ b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/hls/mobile/master.m3u8 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=8152,AVERAGE-BANDWIDTH=7583,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/hls/tv/master.m3u8 b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/hls/tv/master.m3u8 new file mode 100644 index 000000000..560e7e8ed --- /dev/null +++ b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/hls/tv/master.m3u8 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=8152,AVERAGE-BANDWIDTH=7583,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/moq/catalog.json b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/moq/catalog.json new file mode 100644 index 000000000..0c04f421b --- /dev/null +++ b/wai/pack-conformance/cmaf-mutations/segment-not-the-supplied-one/moq/catalog.json @@ -0,0 +1 @@ +{"tracks":[{"altGroup":1,"isLive":false,"name":"v/l0","namespace":"video-cmaf-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_motion"],"wai_determinism":"decode-equivalence","wai_receipt_alias":2923025889890445,"wai_receipts":true,"wai_receipts_track":"v/l0/receipts","wai_staged":{"layer":0,"lead_ms":null,"set":"v"}},{"altGroup":1,"codec":"av01.0.00M.08","height":18,"name":"lo/floor","namespace":"video-cmaf-01","packaging":"cmaf","renderGroup":1,"width":32},{"isLive":false,"name":"v/l0/receipts","namespace":"video-cmaf-01","packaging":"wai-receipts"}],"version":1} diff --git a/wai/pack-conformance/cmaf_diff.py b/wai/pack-conformance/cmaf_diff.py new file mode 100644 index 000000000..6bbaa1572 --- /dev/null +++ b/wai/pack-conformance/cmaf_diff.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""The supplied CMAF reader differentially (extensions/packaging.md §7.7): wai-pack's +(`wai-pack cmaf-verdict`) against pack_verify.py's, which shares no code with it, on +seeded alterations of real files. The files are the CMAF golden's floor (a media muxer's +initialization and media segments, packs/video-cmaf-01/floor/) and §7.6's writer's (the +ladder golden's package, packs/video-ladder-01/package/floor/). Each case alters the +initialization segment or one media segment: a byte flipped in its first boxes, a 32-bit +field overwritten at a box boundary, the file cut short, a box's size changed, a box +inserted at the top level or in a moof, the init's timescale or edit list altered, or a +segment's moof and mdat repeated. Every verdict (ok, floor-cmaf-malformed or +floor-cmaf-timing), and for an accepted segment its samples (offset, length, duration, +flags, decode time), must agree, and no reader may panic. + + python3 cmaf_diff.py +""" +import hashlib +import os +import random +import subprocess +import sys +import tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import pack_verify as PV # noqa: E402 + + +def py_init(b): + try: + return "ok", PV.cmaf_init(b) + except PV.CmafFault as x: + return x.code, None + except Exception as x: # a reader must refuse, never fail otherwise + return f"CRASH {type(x).__name__}", None + + +def py_segment(b, init): + try: + s = PV.cmaf_segment(b, init) + except PV.CmafFault as x: + return (x.code, 0, "-") + except Exception as x: + return (f"CRASH {type(x).__name__}", 0, "-") + text = "".join(f"{a},{n},{d},{f},{t};" for a, n, d, f, t in s) + return ("ok", len(s), hashlib.sha256(text.encode()).hexdigest()) + + +def boxes(b, start, end): + try: + return PV.bmff_boxes(b, start, end) + except PV.CmafFault: + return [] + + +def alter(rng, b): + b = bytearray(b) + top = boxes(b, 0, len(b)) + k = rng.randrange(8) + if k == 6: + # The times: an init's timescale or its edit list's media time, at their offsets. + for kind, off in ((b"mdhd", 20), (b"elst", 20), (b"elst", 16)): + at = bytes(b).find(kind) + if at >= 0 and rng.random() < 0.5 and at + off + 4 <= len(b): + b[at + off:at + off + 4] = rng.choice([0, 1, 2, rng.randrange(1 << 32)]).to_bytes(4, "big") + return bytes(b) + k = 0 + if k == 7: + # A media segment's moof and mdat repeated: the second's decode time is the first's. + pairs = [i for i, x in enumerate(top) if x[0] == b"moof" and i + 1 < len(top) and top[i + 1][0] == b"mdat"] + if pairs: + i = rng.choice(pairs) + b += b[top[i][1]:top[i + 1][3]] + return bytes(b) + k = 0 + if k == 0 and b: + b[rng.randrange(min(len(b), 400))] ^= 1 << rng.randrange(8) + elif k == 1 and top: + x = rng.choice(top) + inner = boxes(b, x[2], x[3]) if x[0] in (b"moov", b"moof", b"trak", b"traf", b"mdia", b"minf", b"stbl", b"mvex") else [] + y = rng.choice(inner) if inner and rng.random() < 0.7 else x + at = y[1] + rng.choice([8, 12, 16, 20, 24]) + if at + 4 <= len(b): + b[at:at + 4] = rng.choice([0, 1, 7, 0xFFFFFFFF, rng.randrange(1 << 32)]).to_bytes(4, "big") + elif k == 2 and len(b) > 8: + del b[rng.randrange(8, len(b)):] + elif k == 3 and top: + x = rng.choice(top) + b[x[1]:x[1] + 4] = max(0, (x[3] - x[1]) + rng.choice([-9, -1, 1, 4, 8, -(x[3] - x[1])])).to_bytes(4, "big") + elif k == 4 and top: + kind = rng.choice([b"free", b"skip", b"uuid", b"styp", b"mdat", b"moof"]) + box = (8 + 4).to_bytes(4, "big") + kind + bytes(4) + at = rng.choice([x[1] for x in top] + [len(b)]) + b[at:at] = box + elif top: + moofs = [x for x in top if x[0] == b"moof"] + if moofs: + x = rng.choice(moofs) + inner = boxes(b, x[2], x[3]) + at = rng.choice([y[1] for y in inner] + [x[3]]) + kind = rng.choice([b"sbgp", b"free", b"tfdt", b"trun"]) + box = (8 + 8).to_bytes(4, "big") + kind + bytes(8) + b[at:at] = box + b[x[1]:x[1] + 4] = (x[3] - x[1] + len(box)).to_bytes(4, "big") + return bytes(b) + + +def main(argv): + if len(argv) != 4: + print(__doc__) + return 2 + wai_pack, seed, n = argv[1], int(argv[2]), int(argv[3]) + rng = random.Random(seed) + root = os.path.join(HERE, "packs") + sets = [] + for d in (os.path.join(root, "video-cmaf-01", "floor", "hi"), os.path.join(root, "video-cmaf-01", "floor", "lo"), + os.path.join(root, "video-ladder-01", "package", "floor", "hi")): + names = sorted(os.listdir(d)) + sets.append((PV.load(os.path.join(d, "init.mp4")), [PV.load(os.path.join(d, x)) for x in names if x.endswith(".m4s")])) + bad, seen = 0, {} + with tempfile.TemporaryDirectory() as tmp: + for c in range(n): + init, segs = rng.choice(sets) + seg = rng.choice(segs) + if rng.random() < 0.25: + init = alter(rng, init) + else: + seg = alter(rng, seg) + ip, sp = os.path.join(tmp, "init.mp4"), os.path.join(tmp, "seg.m4s") + open(ip, "wb").write(init) + open(sp, "wb").write(seg) + out = subprocess.run([wai_pack, "cmaf-verdict", ip, sp], capture_output=True, text=True).stdout.splitlines() + iv, pi = py_init(init) + want = [f"init\t{iv}"] + if iv == "ok": + v = py_segment(seg, pi) + want.append(f"{sp}\t{v[0]}\t{v[1]}\t{v[2]}") + verdict = want[-1].split("\t")[1] + seen[verdict] = seen.get(verdict, 0) + 1 + if out != want or any("PANIC" in x or "CRASH" in x for x in out + want): + bad += 1 + if bad <= 10: + print(f"case {c}: wai-pack {out}, pack_verify {want}") + print(f"cmaf_diff: {n - bad}/{n} cases agree; verdicts {dict(sorted(seen.items()))}") + return 0 if bad == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/wai/pack-conformance/ladder-mutations/expected.json b/wai/pack-conformance/ladder-mutations/expected.json new file mode 100644 index 000000000..459c1583e --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/expected.json @@ -0,0 +1,35 @@ +{ + "pack": "video-ladder-01", + "cases": [ + { + "name": "ok", + "reason": "ok", + "remove": [] + }, + { + "name": "floors-swapped", + "reason": "floor-object-mismatch", + "remove": [ + "floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s", + "floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s" + ] + }, + { + "name": "floor-under-the-component-s-name", + "reason": "floor-cmaf-mismatch", + "remove": [ + "floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s", + "floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s", + "floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s", + "floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s", + "floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s", + "floor/lo/init.mp4" + ] + }, + { + "name": "master-lists-one-floor", + "reason": "catalog-mismatch", + "remove": [] + } + ] +} diff --git a/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/LISTING b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/LISTING new file mode 100644 index 000000000..3f9313343 --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/LISTING @@ -0,0 +1,46 @@ +c5203585fea0962de941d258789157a0590e18b62a2e5d1facbd11e1cfd1ddb9 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/hi/init.mp4 +589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f floor/v/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s +6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db floor/v/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s +acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74 floor/v/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s +d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833 floor/v/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s +d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9 floor/v/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s +640cd81c83baefbe13e8ccc2bf577395c6602885cc73e201ca34dc267e7f0a48 floor/v/init.mp4 +bfa98255d682517f58df4073bc71fdb3fcca120fc33d16e4aca873392476c586 hls/classic/floor-hi.m3u8 +ad81674ac359096ebed255a26a40e82858e53e9754702429e59d72898df33812 hls/classic/floor-lo.m3u8 +3fd88f9dcf835edf0fcca8dff8371d2b4e897ac097fc85901477c950c0da7996 hls/classic/master.m3u8 +3fd88f9dcf835edf0fcca8dff8371d2b4e897ac097fc85901477c950c0da7996 hls/legacy/master.m3u8 +09e75f55cb7ea829a4e630404d2ab540e8be0dbb1c4365dc30802359b658aed9 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +09e75f55cb7ea829a4e630404d2ab540e8be0dbb1c4365dc30802359b658aed9 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +a5140df0bf1f82b88335c13789502298a952b350ad45dfcfcd249b2b9a263a71 index-v.wsix +c310c5a4ae14df691a1a37a6c34e90243ed72be1e8f725d38f1babe1f6866b21 moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +2f5bd830d841eb122c63ae57a15442075478fd522ce55b6f9bcbe41acd51d8ae objects/482fbbb9b39bdb9cbf00515f94269ea591df681ced90b05d97e0df106b49b08e.wai +0057633252a2c2ed5f71fe14ca78075c8e89fc6675a08335ac90f0e56032b825 objects/4ab8f747d181632f342a4887b0a8bb86c174409cb625157784e18c5e5559a381.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +1a6f30fdc000f258c948470386d84beef2ba5a84e922c355093837ae21fd573c objects/6ce13c9ef6872caeb7fbe71661ca88bcc463ceac47f109c8c155e34bb1a6f37d.wai +b773dac63c9d2d9980d0d249ea956e03065c1ae40d17c512df29b690c0c820ef objects/b7bf0717e968d7b6be473785c9f66cbb814cde679bceb1ae37f0765308ddee72.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +cc8a6fc59be29bd33d96fb30ea9e5a30a5aa367a82da9c5784cbadce63396f62 objects/cf842ef3058d3c54d56c0ea3e075efdc77163602b4a1dabcff080e5ea28b9946.wai +01b596b672de3352adb3e01b34fcddfeff975f4ee1d0b88dde160ac10c9bf929 objects/d382ba093b0595c44a43dc3aad22fc0bed42baeb3488f00dfe3567056a7009c5.wai +9d9ae296558ce7f7d644ad0ca3af65eb4fc98d03b0bd6d7a8fc821fc952e86c1 objects/f525b4450c29153dde0b43e4d143dfb76fcc3884c607b744c8a53fa6a1728580.wai +3d573ee482f8187b0e3bc839aeea0f51a67144588d6d207e8466fea91a0c5b34 objects/f7f7f4d6af777bfcce09cdda5ecb754ce6bd8dcd2e03ecfc41359cad989b15f7.wai +bd6086051d0452449da8a90f426f454fee368b4794f7c294218a410ded8d9088 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +d1cf1bd28a06f3a9d695dd64331cb56cf793818e9e9e6cec620183d9dce37b2d policy.json +4ba48d04e5c8e72336e32307b27b4371c96f20f7abf959ac1c1d344ccb9a8106 policy.toml +813329d5521ab5891bb86d29b362e448fae4247c949384b0687d66966546c4fe stages/legacy.json +ca8e0059a23311d7690a0519f85dfb48883721c817cd8fe1cd481cabe14b0032 stages/mobile.json +8bc9c9da674b4765ead0e41d54852ac105161e9638e7ae705e3982847b069e4c stages/tv.json diff --git a/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s new file mode 100644 index 000000000..78b5c8866 Binary files /dev/null and b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s differ diff --git a/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s new file mode 100644 index 000000000..0cb031621 Binary files /dev/null and b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s differ diff --git a/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s new file mode 100644 index 000000000..8747edc9f Binary files /dev/null and b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s differ diff --git a/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s new file mode 100644 index 000000000..6adf8fdfd Binary files /dev/null and b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s differ diff --git a/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s new file mode 100644 index 000000000..5c06ef47a Binary files /dev/null and b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s differ diff --git a/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/init.mp4 b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/init.mp4 new file mode 100644 index 000000000..d419e7893 Binary files /dev/null and b/wai/pack-conformance/ladder-mutations/floor-under-the-component-s-name/floor/v/init.mp4 differ diff --git a/wai/pack-conformance/ladder-mutations/floors-swapped/LISTING b/wai/pack-conformance/ladder-mutations/floors-swapped/LISTING new file mode 100644 index 000000000..9fe69e397 --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/floors-swapped/LISTING @@ -0,0 +1,46 @@ +00ebf8e21ba74af068445c05138d2523c801d9d27d364e1377b518540bc8b34d dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s +acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74 floor/hi/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/hi/init.mp4 +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/lo/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s +6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s +d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833 floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s +d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9 floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s +640cd81c83baefbe13e8ccc2bf577395c6602885cc73e201ca34dc267e7f0a48 floor/lo/init.mp4 +49c1a6c22b80524c40e9509fd3c93ab4a7c1a470f4ff6df188aefe656a940a88 hls/classic/floor-hi.m3u8 +cdb43220dbeebd408a3a221829c819ca5fb7c38682f3e63ebe2039648d4ce34d hls/classic/floor-lo.m3u8 +cf34fb5f72042d09e651c8f21b68affb966bdbe55422e70c0d9d66d1a228a0af hls/classic/master.m3u8 +cf34fb5f72042d09e651c8f21b68affb966bdbe55422e70c0d9d66d1a228a0af hls/legacy/master.m3u8 +d40a6303c71682889899b2044f2d321dfd1f42b268abed0671109c61e0a67ad1 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +d40a6303c71682889899b2044f2d321dfd1f42b268abed0671109c61e0a67ad1 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +555648a512bc2da29e24dd6c2c4c2c245fb65d05a1228a8185dbb36d94299b46 index-v.wsix +c310c5a4ae14df691a1a37a6c34e90243ed72be1e8f725d38f1babe1f6866b21 moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +2f5bd830d841eb122c63ae57a15442075478fd522ce55b6f9bcbe41acd51d8ae objects/482fbbb9b39bdb9cbf00515f94269ea591df681ced90b05d97e0df106b49b08e.wai +0057633252a2c2ed5f71fe14ca78075c8e89fc6675a08335ac90f0e56032b825 objects/4ab8f747d181632f342a4887b0a8bb86c174409cb625157784e18c5e5559a381.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +1a6f30fdc000f258c948470386d84beef2ba5a84e922c355093837ae21fd573c objects/6ce13c9ef6872caeb7fbe71661ca88bcc463ceac47f109c8c155e34bb1a6f37d.wai +b773dac63c9d2d9980d0d249ea956e03065c1ae40d17c512df29b690c0c820ef objects/b7bf0717e968d7b6be473785c9f66cbb814cde679bceb1ae37f0765308ddee72.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +cc8a6fc59be29bd33d96fb30ea9e5a30a5aa367a82da9c5784cbadce63396f62 objects/cf842ef3058d3c54d56c0ea3e075efdc77163602b4a1dabcff080e5ea28b9946.wai +01b596b672de3352adb3e01b34fcddfeff975f4ee1d0b88dde160ac10c9bf929 objects/d382ba093b0595c44a43dc3aad22fc0bed42baeb3488f00dfe3567056a7009c5.wai +9d9ae296558ce7f7d644ad0ca3af65eb4fc98d03b0bd6d7a8fc821fc952e86c1 objects/f525b4450c29153dde0b43e4d143dfb76fcc3884c607b744c8a53fa6a1728580.wai +3d573ee482f8187b0e3bc839aeea0f51a67144588d6d207e8466fea91a0c5b34 objects/f7f7f4d6af777bfcce09cdda5ecb754ce6bd8dcd2e03ecfc41359cad989b15f7.wai +96b7f835515e95018d614ac326d03538b055a9ac2d3a52fac97e9f2dc29dc63b package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +d1cf1bd28a06f3a9d695dd64331cb56cf793818e9e9e6cec620183d9dce37b2d policy.json +4ba48d04e5c8e72336e32307b27b4371c96f20f7abf959ac1c1d344ccb9a8106 policy.toml +813329d5521ab5891bb86d29b362e448fae4247c949384b0687d66966546c4fe stages/legacy.json +ca8e0059a23311d7690a0519f85dfb48883721c817cd8fe1cd481cabe14b0032 stages/mobile.json +8bc9c9da674b4765ead0e41d54852ac105161e9638e7ae705e3982847b069e4c stages/tv.json diff --git a/wai/pack-conformance/ladder-mutations/floors-swapped/dash/classic.mpd b/wai/pack-conformance/ladder-mutations/floors-swapped/dash/classic.mpd new file mode 100644 index 000000000..1e82eeab4 --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/floors-swapped/dash/classic.mpd @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/ladder-mutations/floors-swapped/floor/hi/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s b/wai/pack-conformance/ladder-mutations/floors-swapped/floor/hi/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s new file mode 100644 index 000000000..8747edc9f Binary files /dev/null and b/wai/pack-conformance/ladder-mutations/floors-swapped/floor/hi/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s differ diff --git a/wai/pack-conformance/ladder-mutations/floors-swapped/floor/lo/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s b/wai/pack-conformance/ladder-mutations/floors-swapped/floor/lo/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s new file mode 100644 index 000000000..ec55da7e8 Binary files /dev/null and b/wai/pack-conformance/ladder-mutations/floors-swapped/floor/lo/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s differ diff --git a/wai/pack-conformance/ladder-mutations/floors-swapped/hls/classic/floor-hi.m3u8 b/wai/pack-conformance/ladder-mutations/floors-swapped/hls/classic/floor-hi.m3u8 new file mode 100644 index 000000000..40aecdff8 --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/floors-swapped/hls/classic/floor-hi.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/hi/init.mp4" +#EXTINF:2.400, +../../floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +#EXTINF:1.000, +../../floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +#EXTINF:2.000, +../../floor/hi/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s +#EXTINF:1.000, +../../floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +#EXTINF:1.600, +../../floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/ladder-mutations/floors-swapped/hls/classic/floor-lo.m3u8 b/wai/pack-conformance/ladder-mutations/floors-swapped/hls/classic/floor-lo.m3u8 new file mode 100644 index 000000000..aebf60714 --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/floors-swapped/hls/classic/floor-lo.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/lo/init.mp4" +#EXTINF:2.400, +../../floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s +#EXTINF:1.000, +../../floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s +#EXTINF:2.000, +../../floor/lo/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +#EXTINF:1.000, +../../floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s +#EXTINF:1.600, +../../floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/ladder-mutations/floors-swapped/hls/classic/master.m3u8 b/wai/pack-conformance/ladder-mutations/floors-swapped/hls/classic/master.m3u8 new file mode 100644 index 000000000..02be4ba12 --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/floors-swapped/hls/classic/master.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=8903,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9600,AVERAGE-BANDWIDTH=8875,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/ladder-mutations/floors-swapped/hls/legacy/master.m3u8 b/wai/pack-conformance/ladder-mutations/floors-swapped/hls/legacy/master.m3u8 new file mode 100644 index 000000000..02be4ba12 --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/floors-swapped/hls/legacy/master.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=8903,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9600,AVERAGE-BANDWIDTH=8875,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/ladder-mutations/floors-swapped/hls/mobile/master.m3u8 b/wai/pack-conformance/ladder-mutations/floors-swapped/hls/mobile/master.m3u8 new file mode 100644 index 000000000..9cdb3e5eb --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/floors-swapped/hls/mobile/master.m3u8 @@ -0,0 +1,8 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=8903,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9600,AVERAGE-BANDWIDTH=8875,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/ladder-mutations/floors-swapped/hls/tv/master.m3u8 b/wai/pack-conformance/ladder-mutations/floors-swapped/hls/tv/master.m3u8 new file mode 100644 index 000000000..9cdb3e5eb --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/floors-swapped/hls/tv/master.m3u8 @@ -0,0 +1,8 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=8903,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9600,AVERAGE-BANDWIDTH=8875,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/ladder-mutations/floors-swapped/index-v.wsix b/wai/pack-conformance/ladder-mutations/floors-swapped/index-v.wsix new file mode 100644 index 000000000..77547f145 Binary files /dev/null and b/wai/pack-conformance/ladder-mutations/floors-swapped/index-v.wsix differ diff --git a/wai/pack-conformance/ladder-mutations/floors-swapped/package.receipt.json b/wai/pack-conformance/ladder-mutations/floors-swapped/package.receipt.json new file mode 100644 index 000000000..5fb9a0c0f --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/floors-swapped/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-ladder-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"fc97f47085f5d79c5ac12f3ce965a6523929b3161091256f545bf6b520cb3017","media_class":"video","stage_root":"c9825fa6105cd5a41a142e11be3d6bf85421ca1547f580315cb5542f1a2de268"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"6938df61d1274e43680652af682498bd5c7f1505c5f51b08c770143643c6388e","receipt_hash":"42c2905ad0aabfe3c1d00caa9ecc73a089e289cd01f356ed5c60ce38395663e6","sig":"2f39ed2b68c18fce1278711711bc0cde4634403ae38b2c65dd1f64944acbbdcdd723490f017376c8c017871ef64b6c7a4d6e394851fe4ebed38dd2c84309a602","signer_id":"did:key:z6MkqahniHbtvRDna9jmc9dSMo88FCuKnuujLKxbWyK4682w","signer_pubkey":"a558bae731bbba29b0b2308493514f6ec8a51ab40409cce9e3afafecca787224","stored_bytes":127966,"total_bytes":140530,"total_entries":15,"unique_objects":12,"version":2,"work_units":15} \ No newline at end of file diff --git a/wai/pack-conformance/ladder-mutations/master-lists-one-floor/LISTING b/wai/pack-conformance/ladder-mutations/master-lists-one-floor/LISTING new file mode 100644 index 000000000..3497529b9 --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/master-lists-one-floor/LISTING @@ -0,0 +1,46 @@ +c5203585fea0962de941d258789157a0590e18b62a2e5d1facbd11e1cfd1ddb9 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/hi/init.mp4 +589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s +6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s +acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74 floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s +d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833 floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s +d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9 floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s +640cd81c83baefbe13e8ccc2bf577395c6602885cc73e201ca34dc267e7f0a48 floor/lo/init.mp4 +bfa98255d682517f58df4073bc71fdb3fcca120fc33d16e4aca873392476c586 hls/classic/floor-hi.m3u8 +ad81674ac359096ebed255a26a40e82858e53e9754702429e59d72898df33812 hls/classic/floor-lo.m3u8 +3d8ca6988d54790388e062f4ed37b3f73fd6904aefa5ed88014125e51aaf5429 hls/classic/master.m3u8 +3fd88f9dcf835edf0fcca8dff8371d2b4e897ac097fc85901477c950c0da7996 hls/legacy/master.m3u8 +09e75f55cb7ea829a4e630404d2ab540e8be0dbb1c4365dc30802359b658aed9 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +09e75f55cb7ea829a4e630404d2ab540e8be0dbb1c4365dc30802359b658aed9 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +a5140df0bf1f82b88335c13789502298a952b350ad45dfcfcd249b2b9a263a71 index-v.wsix +c310c5a4ae14df691a1a37a6c34e90243ed72be1e8f725d38f1babe1f6866b21 moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +2f5bd830d841eb122c63ae57a15442075478fd522ce55b6f9bcbe41acd51d8ae objects/482fbbb9b39bdb9cbf00515f94269ea591df681ced90b05d97e0df106b49b08e.wai +0057633252a2c2ed5f71fe14ca78075c8e89fc6675a08335ac90f0e56032b825 objects/4ab8f747d181632f342a4887b0a8bb86c174409cb625157784e18c5e5559a381.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +1a6f30fdc000f258c948470386d84beef2ba5a84e922c355093837ae21fd573c objects/6ce13c9ef6872caeb7fbe71661ca88bcc463ceac47f109c8c155e34bb1a6f37d.wai +b773dac63c9d2d9980d0d249ea956e03065c1ae40d17c512df29b690c0c820ef objects/b7bf0717e968d7b6be473785c9f66cbb814cde679bceb1ae37f0765308ddee72.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +cc8a6fc59be29bd33d96fb30ea9e5a30a5aa367a82da9c5784cbadce63396f62 objects/cf842ef3058d3c54d56c0ea3e075efdc77163602b4a1dabcff080e5ea28b9946.wai +01b596b672de3352adb3e01b34fcddfeff975f4ee1d0b88dde160ac10c9bf929 objects/d382ba093b0595c44a43dc3aad22fc0bed42baeb3488f00dfe3567056a7009c5.wai +9d9ae296558ce7f7d644ad0ca3af65eb4fc98d03b0bd6d7a8fc821fc952e86c1 objects/f525b4450c29153dde0b43e4d143dfb76fcc3884c607b744c8a53fa6a1728580.wai +3d573ee482f8187b0e3bc839aeea0f51a67144588d6d207e8466fea91a0c5b34 objects/f7f7f4d6af777bfcce09cdda5ecb754ce6bd8dcd2e03ecfc41359cad989b15f7.wai +bd6086051d0452449da8a90f426f454fee368b4794f7c294218a410ded8d9088 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +d1cf1bd28a06f3a9d695dd64331cb56cf793818e9e9e6cec620183d9dce37b2d policy.json +4ba48d04e5c8e72336e32307b27b4371c96f20f7abf959ac1c1d344ccb9a8106 policy.toml +813329d5521ab5891bb86d29b362e448fae4247c949384b0687d66966546c4fe stages/legacy.json +ca8e0059a23311d7690a0519f85dfb48883721c817cd8fe1cd481cabe14b0032 stages/mobile.json +8bc9c9da674b4765ead0e41d54852ac105161e9638e7ae705e3982847b069e4c stages/tv.json diff --git a/wai/pack-conformance/ladder-mutations/master-lists-one-floor/hls/classic/master.m3u8 b/wai/pack-conformance/ladder-mutations/master-lists-one-floor/hls/classic/master.m3u8 new file mode 100644 index 000000000..1742325fb --- /dev/null +++ b/wai/pack-conformance/ladder-mutations/master-lists-one-floor/hls/classic/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=9055,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 diff --git a/wai/pack-conformance/mutations/expected.json b/wai/pack-conformance/mutations/expected.json index 2d9e302a9..ad22a5645 100644 --- a/wai/pack-conformance/mutations/expected.json +++ b/wai/pack-conformance/mutations/expected.json @@ -96,6 +96,24 @@ "index": "index/floor-without-derived-from.wsix", "reason": "floor-derived-from" }, + { + "name": "floor-only-piece", + "kind": "index", + "index": "index/floor-only-piece.wsix", + "reason": "ok" + }, + { + "name": "floor-only-piece-with-derived-from", + "kind": "index", + "index": "index/floor-only-piece-with-derived-from.wsix", + "reason": "floor-derived-from" + }, + { + "name": "piece-kind-none", + "kind": "index", + "index": "index/piece-kind-none.wsix", + "reason": "ok" + }, { "name": "entry-flag-unknown", "kind": "index", diff --git a/wai/pack-conformance/mutations/index/floor-only-piece-with-derived-from.wsix b/wai/pack-conformance/mutations/index/floor-only-piece-with-derived-from.wsix new file mode 100644 index 000000000..965257d42 Binary files /dev/null and b/wai/pack-conformance/mutations/index/floor-only-piece-with-derived-from.wsix differ diff --git a/wai/pack-conformance/mutations/index/floor-only-piece.wsix b/wai/pack-conformance/mutations/index/floor-only-piece.wsix new file mode 100644 index 000000000..b3e502adb Binary files /dev/null and b/wai/pack-conformance/mutations/index/floor-only-piece.wsix differ diff --git a/wai/pack-conformance/mutations/index/piece-kind-none.wsix b/wai/pack-conformance/mutations/index/piece-kind-none.wsix new file mode 100644 index 000000000..55aecd700 Binary files /dev/null and b/wai/pack-conformance/mutations/index/piece-kind-none.wsix differ diff --git a/wai/pack-conformance/pack_verify.py b/wai/pack-conformance/pack_verify.py index 3e637f56d..50103e2aa 100644 --- a/wai/pack-conformance/pack_verify.py +++ b/wai/pack-conformance/pack_verify.py @@ -57,9 +57,27 @@ from their specifications, reads the capability registry from its published expo verified as above without its inputs, and refused with the reason the corpus gives it (the first check that fails), or accepted where the corpus says `ok`. - python3 pack_verify.py + python3 pack_verify.py [--jobs N] [--native-blake3] + python3 pack_verify.py [--jobs N] [--native-blake3] --package Exit status 0 only if every vector verifies. Python 3.11 or later (tomllib). + +At scale (a package of a full title), two options make the same checks faster: + +- `--jobs N` hashes every object and decodes every base and its layers in N worker + processes, each piece alone, before the checks run; the checks, their order and their + reasons are the serial run's, reading the workers' content hashes and prefix digests + where the serial run computes them. Each object is hashed once, and each URI a playlist + names is resolved by content id (http_verify.resolve_uri), not by hashing every object. +- `--native-blake3` takes BLAKE3 from the `blake3` module, which binds the BLAKE3 team's + Rust implementation, the one wai-rs links, so the hashes are no longer computed by code + independent of wai-rs; it is taken only after it agrees with the pure implementation + here on the published BLAKE3 vectors (../http-conformance/blake3_vectors.json) and on + generated inputs of every chunk boundary, and refused otherwise. + +`--package ` verifies one package as §11.3 does, without its sources, as a +publisher's verifier does (the receipt's signature by its own key, §9.1, since no +`key.hex` sits beside it). """ import hashlib import itertools @@ -87,6 +105,52 @@ sys.path.insert(0, os.path.join(HERE, "..", "http-conformance")) import http_verify as HV # noqa: E402 HV.load_registry() +# --jobs: worker processes for the hashes and decodes run ahead of the checks; 1 runs none. +JOBS = 1 +NATIVE = False +_POOL = None + + +def _worker_init(native): + if native: + use_native_blake3() + + +def pool(): + """The worker processes (--jobs), each with this run's BLAKE3.""" + global _POOL + if _POOL is None and JOBS > 1: + import multiprocessing + _POOL = multiprocessing.get_context("spawn").Pool(JOBS, initializer=_worker_init, initargs=(NATIVE,)) + return _POOL + + +def use_native_blake3(): + """--native-blake3: the blake3 module, only where it agrees with the pure BLAKE3 on the + published vectors and on inputs at and around every chunk and block boundary.""" + global blake3 + try: + import blake3 as nb + except ImportError: + raise SystemExit("pack_verify: --native-blake3, but no blake3 module is installed") + + def native(b): + return nb.blake3(b).digest() + vec = json.load(open(os.path.join(HERE, "..", "http-conformance", "blake3_vectors.json"))) + for c in vec["cases"]: + data = bytes(i % 251 for i in range(c["input_len"])) + if native(data).hex() != c["hash"][:64] or blake3(data).hex() != c["hash"][:64]: + raise SystemExit(f"pack_verify: the native BLAKE3 disagrees on the {c['input_len']}-byte vector") + for n in [0, 1, 63, 64, 65, 1023, 1024, 1025, 2048, 4095, 4096, 4097, 16385, 65537]: + data = bytes((i * 131 + n) % 256 for i in range(n)) + if native(data) != blake3(data): + raise SystemExit(f"pack_verify: the native BLAKE3 disagrees on a {n}-byte input") + import _blake3 + global NATIVE + _blake3.blake3 = native + blake3 = native + NATIVE = True + MAX_INTEGER = (1 << 53) - 1 # The longest manifest a reader must accept (SPEC §2, *Reader limits*). MANIFEST_MAX = 1 << 27 @@ -318,7 +382,7 @@ def read_policy(doc): raise top.err("policy-missing-key", "component") comps = [] for c in top.tables("component"): - c.keys(["id", "media", "source", "timebase"]) + c.keys(["id", "media", "source", "timebase", "profile"]) m = {"id": c.ident("id")} media = c.string_req("media") if media not in MEDIA: @@ -330,6 +394,12 @@ def read_policy(doc): tb = c.table_req("timebase") tb.keys(["num", "den"]) m["timebase"] = {"num": tb.uint_in("num", 1, U32, None), "den": tb.uint_in("den", 1, U32, None)} + # §7.8: "floor-only", a component of floor rungs alone; absent, the standard profile. + pf = c.string("profile") + if pf is not None: + if pf != "floor-only": + raise c.err("policy-value", "profile") + m["profile"] = pf comps.append(m) P["component"] = comps s = top.table("segmentation") @@ -364,7 +434,7 @@ def read_policy(doc): ladder.append(m) P["ladder"] = ladder f = top.table("floor") - f.keys(["supplied_listing", "extra_keyframes"]) + f.keys(["supplied_listing", "extra_keyframes", "supplied_form"]) fl = {} sl = f.string("supplied_listing") if sl is not None: @@ -372,6 +442,12 @@ def read_policy(doc): raise f.err("policy-value", "supplied_listing") fl["supplied_listing"] = sl fl["extra_keyframes"] = f.boolean("extra_keyframes", False) + # §7.7: "cmaf", the supplied floor in the CMAF form; absent, the OBU form (§7.3). + sf = f.string("supplied_form") + if sf is not None: + if sf != "cmaf": + raise f.err("policy-value", "supplied_form") + fl["supplied_form"] = sf P["floor"] = fl lv = top.table("live") lv.keys(["layer_lag_ms"]) @@ -514,7 +590,13 @@ def constrain(P, lag): if first_dup(r["refines"] for r in P["ladder"] if "refines" in r) is not None: no("policy-refines-branch") for c in P["component"]: - if not any(r["component"] == c["id"] and r["role"] == "base" for r in P["ladder"]): + def has(role, c=c): + return any(r["component"] == c["id"] and r["role"] == role for r in P["ladder"]) + if c.get("profile") == "floor-only": + # §7.8: floor rungs alone, and no source. + if has("base") or has("refinement") or "source" in c or not has("floor"): + no("policy-floor-only") + elif not has("base"): no("policy-no-base") n = max(layers(P).values(), default=0) + 1 if any(c["max_layer"] > n - 1 for c in P["class"]): @@ -527,8 +609,10 @@ def constrain(P, lag): rs = [by.get(i) for i in ids] # One component, each rung once, exactly one base, every refinement over a # rung it names, and every floor rung of its component (§3.4 item 10). + fo = rs and all(rs) and any(c["id"] == rs[0]["component"] and c.get("profile") == "floor-only" for c in P["component"]) + # A floor-only component's ladder is its floor rungs, every one (§7.8). ok = rs and all(rs) and all(x["component"] == rs[0]["component"] for x in rs) \ - and len(set(ids)) == len(ids) and sum(x["role"] == "base" for x in rs) == 1 \ + and len(set(ids)) == len(ids) and sum(x["role"] == "base" for x in rs) == (0 if fo else 1) \ and all(x.get("refines", ids[0]) in ids for x in rs) \ and all(f["id"] in ids for f in P["ladder"] if f["component"] == rs[0]["component"] and f["role"] == "floor") if not ok: @@ -1173,7 +1257,8 @@ MEDIA_CLASSES = ["video", "image", "audio", "world", "feed", "avatar", "film"] ROLES = ["base", "refinement", "floor", "snapshot", "ops", "receipt", "asset"] BASE, REFINEMENT, FLOOR = 0, 1, 2 NO_REFINES = 0xFFFFFFFF -PIECE_KINDS = KINDS + ["world", "audio"] +# §6.2: 6, none, is a floor-only component's piece, which no source classifies (§7.8). +PIECE_KINDS = KINDS + ["world", "audio", "none"] ENTRY_DUP, ENTRY_DERIVED = 2, 4 TIER_NAMES = [None, "entropy-consistency", "decode-equivalence"] @@ -1335,7 +1420,10 @@ def index_rules(ix): raise Refused("digest-without-tier") if e["deadline"] and not refinement: raise Refused("deadline-role") - if e["role"] == FLOOR and not e["flags"] & ENTRY_DERIVED: + # A floor presents its piece's base; a piece with no base has floors that present + # nothing (§6.3, §7.8). + has_base = any(x["role"] == BASE for x in E[pc["first_entry"]:pc["first_entry"] + pc["n_entries"]]) + if e["role"] == FLOOR and bool(e["flags"] & ENTRY_DERIVED) != has_base: raise Refused("floor-derived-from") earlier = [x for x in E[:i] if x["hash"] == e["hash"]] if any(x["byte_len"] != e["byte_len"] for x in earlier): @@ -1633,12 +1721,13 @@ def refines_ok(ix, i, rf): return all(p in below for p in producers) and E[e["refines"]]["hash"].hex() in producers -def object_rules(ix, i, data): - """§6.3, with the objects in hand: entry i's object, the first rule broken is the refusal.""" +def object_rules(ix, i, data, hashed=None): + """§6.3, with the objects in hand: entry i's object, the first rule broken is the refusal. + `hashed` is its content hash where a worker computed it (--jobs).""" e = ix["entries"][i] if len(data) != e["byte_len"]: raise Refused("object-length") - if content_hash(data) != e["hash"]: + if (hashed if hashed is not None else content_hash(data)) != e["hash"]: raise Refused("object-hash") env = read_envelope(data) if env is None: @@ -2495,6 +2584,219 @@ def without_delimiters(obu_list): return b"".join(whole for t, whole, _ in obu_list if t != OBU_TD) +# ---------------------------------------------------------------- a supplied CMAF floor (§7.7) + + +class CmafFault(Exception): + """§7.7: floor-cmaf-malformed (its boxes) or floor-cmaf-timing (its times).""" + def __init__(self, code, why): + super().__init__(f"{why} ({code})") + self.code = code + + +def cm_bad(why): + return CmafFault("floor-cmaf-malformed", why) + + +def bmff_boxes(b, start, end): + """ISO/IEC 14496-12's boxes tiling b[start:end]: (type, box start, body start, box end). + A size of 0 and a size below its header are refused; a size of 1 is the 64-bit size.""" + out, at = [], start + while at < end: + if at + 8 > end: + raise cm_bad(f"a box header runs past the end at byte {at}") + size, kind, head = int.from_bytes(b[at:at + 4], "big"), b[at + 4:at + 8], 8 + if size == 1: + if at + 16 > end: + raise cm_bad("a 64-bit box size runs past the end") + size, head = int.from_bytes(b[at + 8:at + 16], "big"), 16 + if size < 16: + raise cm_bad(f"a 64-bit box size below 16 at byte {at}") + elif size == 0: + raise cm_bad(f"a box of size 0 at byte {at}") + elif size < 8: + raise cm_bad(f"a box size below 8 at byte {at}") + if at + size > end: + raise cm_bad(f"a box runs past its parent at byte {at}") + out.append((kind, at, at + head, at + size)) + at += size + return out + + +def bmff_one(bs, kind, within): + v = [x for x in bs if x[0] == kind] + if len(v) != 1: + raise cm_bad(f"{within} holds {len(v)} {kind.decode('latin-1')} boxes, not one") + return v[0] + + +def be_at(b, at, n, end): + if at + n > end: + raise cm_bad(f"a field runs past its box at byte {at}") + return int.from_bytes(b[at:at + n], "big") + + +def version(b, body, end): + """A full box's version, its first byte, read inside the box.""" + return be_at(b, body, 4, end) >> 24 + + +def cmaf_init(b): + """§7.7's init.mp4: one moov of one trak and an mvex; tkhd's track, mdhd's timescale (at + least 1), a vide handler, one av01 sample entry, an edit list (if any) of one entry at + media time 0 and rate 1, and trex's defaults for that track. Returns (track, timescale, + (duration, size, flags)).""" + top = bmff_boxes(b, 0, len(b)) + _, _, mb, me = bmff_one(top, b"moov", "init.mp4") + mv = bmff_boxes(b, mb, me) + _, _, tb, te = bmff_one(mv, b"trak", "moov") + _, _, xb, xe = bmff_one(mv, b"mvex", "moov") + tk = bmff_boxes(b, tb, te) + _, _, hb, he = bmff_one(tk, b"tkhd", "trak") + track = be_at(b, hb + 4 + (16 if version(b, hb, he) == 1 else 8), 4, he) + ed = [x for x in tk if x[0] == b"edts"] + if ed: + _, _, eb, ee = bmff_one(bmff_boxes(b, ed[0][2], ed[0][3]), b"elst", "edts") + n = be_at(b, eb + 4, 4, ee) + if version(b, eb, ee) == 1: + mt, rate = be_at(b, eb + 16, 8, ee), be_at(b, eb + 24, 4, ee) + mt = mt - (1 << 64) if mt >> 63 else mt + else: + mt, rate = be_at(b, eb + 12, 4, ee), be_at(b, eb + 16, 4, ee) + mt = mt - (1 << 32) if mt >> 31 else mt + if n != 1 or mt != 0 or rate != 0x00010000: + raise CmafFault("floor-cmaf-timing", "an edit list other than one entry at media time 0 and rate 1") + _, _, db, de = bmff_one(tk, b"mdia", "trak") + md = bmff_boxes(b, db, de) + _, _, kb, ke = bmff_one(md, b"mdhd", "mdia") + ts = be_at(b, kb + 4 + (16 if version(b, kb, ke) == 1 else 8), 4, ke) + if ts == 0: + raise CmafFault("floor-cmaf-timing", "a timescale of 0") + _, _, lb, le_ = bmff_one(md, b"hdlr", "mdia") + if lb + 12 > le_ or b[lb + 8:lb + 12] != b"vide": + raise cm_bad("the track's handler is not vide") + _, _, ib, ie = bmff_one(md, b"minf", "mdia") + _, _, sb, se = bmff_one(bmff_boxes(b, ib, ie), b"stbl", "minf") + _, _, db2, de2 = bmff_one(bmff_boxes(b, sb, se), b"stsd", "stbl") + if be_at(b, db2 + 4, 4, de2) != 1: + raise cm_bad("a sample description of other than one entry") + entries = bmff_boxes(b, db2 + 8, de2) + if len(entries) != 1 or entries[0][0] != b"av01": + raise cm_bad("the sample description is not one av01 entry") + _, _, rb, re_ = bmff_one(bmff_boxes(b, xb, xe), b"trex", "mvex") + version(b, rb, re_) + if be_at(b, rb + 4, 4, re_) != track: + raise cm_bad("trex names another track than tkhd") + return track, ts, (be_at(b, rb + 12, 4, re_), be_at(b, rb + 16, 4, re_), be_at(b, rb + 20, 4, re_)) + + +PRELUDE = (b"styp", b"sidx", b"prft", b"emsg", b"free", b"skip") + + +def cmaf_segment(b, init): + """§7.7's media segment: prelude boxes, then moof/mdat pairs; each moof an mfhd and one + traf of tfhd (the init's track, no base data offset or empty duration), tfdt and truns + with data offsets; each sample's duration, size and flags its trun's, else tfhd's, else + trex's, its bytes in the mdat after its moof, no composition offset but 0, no sample of + no bytes; each moof's decode time where the one before it ended. Returns the samples as + (offset, length, duration, flags, decode time).""" + track, _, (dd, ds, df) = init + top = bmff_boxes(b, 0, len(b)) + first = next((i for i, x in enumerate(top) if x[0] == b"moof"), None) + if first is None: + raise cm_bad("a segment with no moof") + if any(x[0] not in PRELUDE for x in top[:first]): + raise cm_bad("a box before the first moof that is not a prelude box") + rest = top[first:] + if len(rest) % 2 or any(rest[i][0] != b"moof" or rest[i + 1][0] != b"mdat" for i in range(0, len(rest), 2)): + raise cm_bad("after the first moof, other than moof and mdat pairs") + out, nxt = [], None + for i in range(0, len(rest), 2): + (_, ms, mb, me), (_, _, ab, ae) = rest[i], rest[i + 1] + mo = bmff_boxes(b, mb, me) + bmff_one(mo, b"mfhd", "moof") + if any(x[0] not in (b"mfhd", b"traf") for x in mo): + raise cm_bad("a moof holding other than mfhd and traf") + _, _, fb, fe = bmff_one(mo, b"traf", "moof") + tr = bmff_boxes(b, fb, fe) + if any(x[0] not in (b"tfhd", b"tfdt", b"trun") for x in tr): + raise cm_bad("a traf holding other than tfhd, tfdt and trun") + _, _, hb, he = bmff_one(tr, b"tfhd", "traf") + tf = be_at(b, hb, 4, he) & 0xFFFFFF + if tf & ~(0x02 | 0x08 | 0x10 | 0x20 | 0x020000): + raise cm_bad(f"tfhd flags {tf:#x}") + if be_at(b, hb + 4, 4, he) != track: + raise cm_bad("a tfhd of another track") + at = hb + 8 + vals = {} + for bit, name in ((0x02, "sdi"), (0x08, "dur"), (0x10, "size"), (0x20, "flags")): + if tf & bit: + vals[name] = be_at(b, at, 4, he) + at += 4 + d_dur, d_size, d_flags = vals.get("dur", dd), vals.get("size", ds), vals.get("flags", df) + _, _, tb, te = bmff_one(tr, b"tfdt", "traf") + dts = be_at(b, tb + 4, 8 if version(b, tb, te) == 1 else 4, te) + if nxt is not None and nxt != dts: + raise CmafFault("floor-cmaf-timing", "a moof whose decode time is not where the one before it ended") + runs = [x for x in tr if x[0] == b"trun"] + if not runs: + raise cm_bad("a traf with no trun") + for _, _, rb, re_ in runs: + rf = be_at(b, rb, 4, re_) & 0xFFFFFF + if rf & ~(0x01 | 0x04 | 0x100 | 0x200 | 0x400 | 0x800) or not rf & 0x01: + raise cm_bad(f"trun flags {rf:#x}") + n = be_at(b, rb + 4, 4, re_) + off = be_at(b, rb + 8, 4, re_) + off = off - (1 << 32) if off >> 31 else off + at = rb + 12 + first_flags = None + if rf & 0x04: + first_flags, at = be_at(b, at, 4, re_), at + 4 + per = sum(1 for bit in (0x100, 0x200, 0x400, 0x800) if rf & bit) + if at + 4 * per * n > re_: + raise cm_bad("trun shorter than its samples") + data = ms + off + for k in range(n): + def f(bit): + nonlocal at + if not rf & bit: + return None + v = be_at(b, at, 4, re_) + at += 4 + return v + dur, size, own, cto = f(0x100), f(0x200), f(0x400), f(0x800) + dur = d_dur if dur is None else dur + size = d_size if size is None else size + flags = (first_flags if first_flags is not None else own) if k == 0 else own + flags = d_flags if flags is None else flags + if cto: + raise CmafFault("floor-cmaf-timing", "a sample whose composition time is not its decode time") + if size == 0: + raise cm_bad("a sample of no bytes") + if data < ab or data + size > ae: + raise cm_bad("a sample's bytes outside the mdat after its moof") + out.append((data, size, dur, flags, dts)) + data += size + dts += dur + nxt = dts + if not out: + raise cm_bad("a segment with no sample") + return out + + +def cmaf_times(samples, timescale, num, den, start): + """§7.7: every sample one frame long, the first at frame `start`'s decode time.""" + if any(s[2] * num != timescale * den for s in samples): + raise CmafFault("floor-cmaf-timing", "a sample that does not last one frame") + if samples[0][4] * num != start * timescale * den: + raise CmafFault("floor-cmaf-timing", f"a segment that does not start at frame {start}") + + +def cmaf_units(b, samples): + """§7.7: each sample a temporal unit without its temporal delimiter.""" + return [b"\x12\x00" + b[at:at + n] for at, n, _, _, _ in samples] + + # ---------------------------------------------------------------- classes and catalogs (§8) @@ -2554,6 +2856,13 @@ def piece_ladder(P, cid, then): return base, chain, [r for r in mine if r["role"] == "floor"] +def floor_names(P, cid): + """§7.1: the component's floor rungs, in ladder order, each with its floor name: the + component's id when it has one floor rung, the rung's id when it has several.""" + rungs = [r for r in P["ladder"] if r["component"] == cid and r["role"] == "floor"] + return [(r, cid if len(rungs) == 1 else r["id"]) for r in rungs] + + def default_base_cap(P, cid): """§5.7: the capability of a component's first base rung, which its HTTP views carry.""" return next((r["capability"] for r in P["ladder"] if r["component"] == cid and r["role"] == "base"), "") @@ -2623,13 +2932,15 @@ def codecs_value(objects): return "wai1." + "|".join(alt(*a) for a in union), "wai1." + "|".join(alt(*a) for a in floor) -def render_catalogs(P, ix, c, floor, size, policy_hex): +def render_catalogs(P, ix, c, floors, size, policy_hex): """§8.3–§8.8 for one component c: every catalog file as the index, the objects and the - floor give it. floor: (codecs, width, height, [(fragment name, bytes)]); size: (W, H).""" + floors give it. floors: per floor rung in ladder order, (floor name, codecs, width, + height, [(fragment name, bytes)]); size: (W, H).""" num, den = ix["tb"] pieces, plan = ix["pieces"], ix["plan"] - F, FW, FH, frags = floor - W, H = size + # A floor-only component has no base and no frame size of its own (§7.8); only a WAI + # view, which lists none of it, names that size. + W, H = size or (0, 0) first_piece, first_start = ix["first_piece"], pieces[0]["start"] total = sum(pc["duration"] for pc in pieces) tgt = max(-(-pc["duration"] * den // num) for pc in pieces) @@ -2642,18 +2953,24 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): """(BANDWIDTH, AVERAGE-BANDWIDTH) over (bytes, units) pairs.""" return (max(bps(n, d, num, den) for n, d in per), bps(sum(n for n, _ in per), sum(d for _, d in per), num, den)) - fb, fa = rates([(len(fr), pc["duration"]) for (_, fr), pc in zip(frags, pieces)]) head = ["#EXTM3U", "#EXT-X-VERSION:7", "#EXT-X-INDEPENDENT-SEGMENTS"] - inf = f'#EXT-X-STREAM-INF:BANDWIDTH={fb},AVERAGE-BANDWIDTH={fa},CODECS="{F}",RESOLUTION={FW}x{FH}' - # The floor variant's URI resolves from hls/classic/ and from every class's directory - # alike, so a class's master is the classical one with at most one line added. - out["hls/classic/master.m3u8"] = text(head + [inf, f"../classic/floor-{c}.m3u8"]) + # One variant per floor rung, in ladder order. A variant's URI resolves from + # hls/classic/ and from every class's directory alike, so a class's master is the + # classical one with at most one line added. + variants, fbw = [], {} + for fname, F, FW, FH, frags in floors: + fb, fa = rates([(len(fr), pc["duration"]) for (_, fr), pc in zip(frags, pieces)]) + fbw[fname] = fb + variants += [f'#EXT-X-STREAM-INF:BANDWIDTH={fb},AVERAGE-BANDWIDTH={fa},CODECS="{F}",RESOLUTION={FW}x{FH}', + f"../classic/floor-{fname}.m3u8"] + out["hls/classic/master.m3u8"] = text(head + variants) media = ["#EXTM3U", "#EXT-X-VERSION:7", f"#EXT-X-TARGETDURATION:{tgt}", "#EXT-X-MEDIA-SEQUENCE:0", "#EXT-X-PLAYLIST-TYPE:VOD", "#EXT-X-INDEPENDENT-SEGMENTS"] - lines = media + [f'#EXT-X-MAP:URI="../../floor/{c}/init.mp4"'] - for (name, _), pc in zip(frags, pieces): - lines += [f"#EXTINF:{ms_text(pc['duration'], num, den)},", f"../../floor/{c}/{name}"] - out[f"hls/classic/floor-{c}.m3u8"] = text(lines + ["#EXT-X-ENDLIST"]) + for fname, _, _, _, frags in floors: + lines = media + [f'#EXT-X-MAP:URI="../../floor/{fname}/init.mp4"'] + for (name, _), pc in zip(frags, pieces): + lines += [f"#EXTINF:{ms_text(pc['duration'], num, den)},", f"../../floor/{fname}/{name}"] + out[f"hls/classic/floor-{fname}.m3u8"] = text(lines + ["#EXT-X-ENDLIST"]) mpd_head = ['', '', @@ -2664,27 +2981,36 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): return [" "] + \ [f' ' for k in ks] + [" "] - lines = mpd_head + [ - f' ', - f' ', f' ', - f' '] + timeline(range(len(pieces))) - lines += [f' ' for name, _ in frags] - out["dash/classic.mpd"] = text(lines + [" ", " "] + mpd_tail) + # One Representation per floor rung: with one, its codecs and size are the set's; with + # several, each Representation's. + one = len(floors) == 1 + _, F0, FW0, FH0, _ = floors[0] + own_attrs = f' codecs="{F0}" width="{FW0}" height="{FH0}"' if one else "" + lines = mpd_head + [f' '] + for fname, F, FW, FH, frags in floors: + attrs = "" if one else f' codecs="{F}" width="{FW}" height="{FH}"' + lines += [f' ', + f' ', + f' '] + timeline(range(len(pieces))) + lines += [f' ' for name, _ in frags] + lines += [" ", " "] + out["dash/classic.mpd"] = text(lines + mpd_tail) E = ix["entries"] # §8.3: an HTTP view lists a piece only where its base is of the component's default # base capability (http-delivery §4.2: no variant mixes bases with nothing in common). base_cap = default_base_cap(P, c) - bases = [next(e for e in E[pc["first_entry"]:pc["first_entry"] + pc["n_entries"]] if e["role"] == BASE) for pc in pieces] + # A floor-only piece has no base (§7.8), and an HTTP view lists nothing of it. + bases = [next((e for e in E[pc["first_entry"]:pc["first_entry"] + pc["n_entries"]] if e["role"] == BASE), None) for pc in pieces] for ci, cl in enumerate(P["class"]): name, row = cl["id"], plan[ci] lists = [listed(ix, k, row[k]) for k in range(len(pieces))] - http = [lists[k] if bases[k]["cap"] == base_cap else [] for k in range(len(pieces))] + http = [lists[k] if bases[k] is not None and bases[k]["cap"] == base_cap else [] for k in range(len(pieces))] wai = any(http) lines = head[:] if wai: lines.append('#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8"') - out[f"hls/{name}/master.m3u8"] = text(lines + [inf, f"../classic/floor-{c}.m3u8"]) + out[f"hls/{name}/master.m3u8"] = text(lines + variants) views = [] for k, pc in enumerate(pieces): v = {"dur": pc["duration"], "p": first_piece + k, "start": pc["start"], "stages": []} @@ -2750,7 +3076,7 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): out[f"dash/{name}.mpd"] = text(lines + mpd_tail) has_floor = any(e["role"] == FLOOR for e in E) out["moq/catalog.json"] = render_moq(c, E, P["live"]["layer_lag_ms"], P["defaults"]["deadline_ms"], - (F, FW, FH) if has_floor else None, P["asset_id"]) + [(f[0], f[1], f[2], f[3]) for f in floors] if has_floor else [], P["asset_id"]) return out @@ -2760,14 +3086,14 @@ def receipt_alias(ns, name): return int.from_bytes(blake3(b"wai:pack-receipt-alias\x01" + ns.encode() + b"\x00" + name.encode())[:8], "little") & MAX_INTEGER -def render_moq(c, E, lag, default_deadline, floor, ns): +def render_moq(c, E, lag, default_deadline, floors, ns): """§8.4: moq/catalog.json, the component's bases and refinements as a stage set of moq-streaming-format §4.1. One track per (layer, deadline), by layer, then deadline ascending, of namespace `ns`; each depends on every track carrying an entry below one of its own down its chain; each with wai_staged, receipts declared with its alias and a - receipts track of its own; then the floor (cmaf, altGroup 1 with the base track), then - the receipts tracks. floor: (codecs, width, height) or None. The catalog must pass - §4.1's check (staged_verify.py's).""" + receipts track of its own; then each floor (cmaf, `/floor`, altGroup 1 with + the base track), then the receipts tracks. floors: (floor name, codecs, width, height) + per floor rung in ladder order. The catalog must pass §4.1's check (staged_verify.py's).""" keys = sorted({(e["layer"], e["deadline"]) for e in E if e["role"] in (BASE, REFINEMENT)}) def name(key): @@ -2800,12 +3126,12 @@ def render_moq(c, E, lag, default_deadline, floor, ns): t["wai_determinism"] = TIER_NAMES[weakest] if layer: t["wai_delivery_timeout_ms"] = max(0, deadline - (lag[layer] if layer < len(lag) else 0)) - elif floor is not None: + elif floors: t["altGroup"] = 1 tracks.append(t) - if floor is not None: - tracks.append({"altGroup": 1, "codec": floor[0], "height": floor[2], "name": f"{c}/floor", "namespace": ns, - "packaging": "cmaf", "renderGroup": 1, "width": floor[1]}) + for fname, codec, fw, fh in floors: + tracks.append({"altGroup": 1, "codec": codec, "height": fh, "name": f"{fname}/floor", "namespace": ns, + "packaging": "cmaf", "renderGroup": 1, "width": fw}) tracks += [{"namespace": ns, "name": f"{name(k)}/receipts", "packaging": "wai-receipts", "isLive": False} for k in keys] if len({receipt_alias(ns, name(k)) for k in keys}) != len(keys): raise ValueError("two tracks' receipt aliases are equal") @@ -2891,9 +3217,21 @@ class Held: return itertools.repeat(self.frame, self.n) +class Decoded: + """A prefix a worker decoded (--jobs): its clip digest (§6.5) and its frame count, + not its frames.""" + def __init__(self, digest, n): + self.digest, self.n = digest, n + + def __len__(self): + return self.n + + def prefix_digest_of(frames): """SPEC §7's clip digest, each frame's SHA-256 fed in turn: a still's frame, held, is hashed once and its digest fed for every frame, a block of them at a time (§5.7).""" + if isinstance(frames, Decoded): + return frames.digest h = hashlib.sha256() if isinstance(frames, Held): d = hashlib.sha256(frames.frame).digest() @@ -2913,10 +3251,48 @@ def clip_digest(frames): return hashlib.sha256(b"".join(hashlib.sha256(f).digest() for f in frames)).digest() +def work_hash(path): + """A worker's object (--jobs): its content hash (jwp-receipts §2.1).""" + return content_hash(load(path)) + + +def work_chain(task): + """A worker's piece (--jobs): its base decoded, or a still held, and each layer applied + in entry order, every prefix's clip digest, as the serial run computes them; at the + first that fails, which (the base, or the j-th layer) and why, in the serial run's words. + task: (capability, frames, [(path, manifest length)], base first).""" + cap, n, objs = task + payloads = [load(p)[12 + ml:] for p, ml in objs] + try: + if cap == "wai.image.png": + pw, ph, rgb = png_frame(payloads[0]) + frames = Held(rgb, n) + else: + wv = IPV.parse_wiv1(payloads[0], 0) + frames, pw, ph = IPV.decode_wiv1_k0(wv), wv["w"], wv["h"] + except (IPV.Refused, ValueError, TypeError, IndexError, struct.error, zlib.error) as x: + return ("base", str(x)) + digests = [prefix_digest_of(frames)] + for p in payloads[1:]: + try: + frames = wir1_apply(p, frames) + except (SV.Refused, KeyError, IndexError, struct.error) as x: + return ("layer", len(digests), str(x), pw, ph, digests) + digests.append(prefix_digest_of(frames)) + return ("ok", pw, ph, digests) + + def verify_pack(rep, d, name, pkg=None, inputs=True): """packs//: package/ (or `pkg`) verified in §11.3's order, without the source; then, with `inputs`, the package's inputs: policy.toml as given, and what only the source decides (the plan, each base's sse and step) and the supplied floor.""" + try: + verify_pack_once(rep, d, name, pkg, inputs) + finally: + HV.forget_content_hashes() + + +def verify_pack_once(rep, d, name, pkg, inputs): root = os.path.join(d, "packs", name) pkg = pkg or os.path.join(root, "package") tag = f"packs/{name}" @@ -2977,6 +3353,11 @@ def verify_pack(rep, d, name, pkg=None, inputs=True): cls = [c["id"].lower() for c in P["class"]] ok = all(segment_ok(i) for i in ids) and len(set(cls)) == len(cls) and "classic" not in cls rep.check(ok, f"{tag}: component and class ids are path segments, class ids distinct in any case, none `classic` (pack-id)") + if not ok: + return + names = [n for c in P["component"] for _, n in floor_names(P, c["id"])] + ok = all(segment_ok(n) for n in names) and len({n.lower() for n in names}) == len(names) + rep.check(ok, f"{tag}: floor names (§7.1) are path segments, distinct in any case (pack-id)") if not ok: return # §3.6 under the effective policy: royalty_free_only as policy.json states it. @@ -3004,10 +3385,21 @@ def verify_pack(rep, d, name, pkg=None, inputs=True): except Refused as e: rep.check(False, f"{tag}: the receipt against its indexes ({e.code})") return - seed = bytes.fromhex(load(os.path.join(root, "key.hex")).decode().strip()) - pk, sig = ed25519_from_seed(seed, r["payload"]) - rep.check(pk.hex() == j["signer_pubkey"], f"{tag}: the receipt's signer is the package key") - rep.check(sig.hex() == j["sig"], f"{tag}: the signature is the package key's over the payload (§11.2)") + # A golden's own key re-signs its payload (§11.2); a package verified without its key + # (--package) has its signature checked by verify_receipt_v2 alone (§9.1). + if os.path.exists(os.path.join(root, "key.hex")): + seed = bytes.fromhex(load(os.path.join(root, "key.hex")).decode().strip()) + pk, sig = ed25519_from_seed(seed, r["payload"]) + rep.check(pk.hex() == j["signer_pubkey"], f"{tag}: the receipt's signer is the package key") + rep.check(sig.hex() == j["sig"], f"{tag}: the signature is the package key's over the payload (§11.2)") + # A re-pack golden names the golden it takes its bases from (§11.5); every other golden + # has no parent. + if inputs: + parent = os.path.join(root, "parent") + if os.path.exists(parent): + verify_repack(rep, d, tag, j, ixs, load(parent).decode().strip()) + else: + rep.check(j["parent_kind"] == "none", f"{tag}: the golden names no parent (§9.2)") named = {"LISTING", "policy.toml", "policy.json", "package.receipt.json"} | {c["index"] for c in j["components"]} for c, ix in zip(j["components"], ixs): @@ -3020,9 +3412,12 @@ def verify_pack(rep, d, name, pkg=None, inputs=True): # §9.4: a metered package publishes its measurement claim; an unmetered one, none. claim = f"claims/{j['receipt_hash']}.measurement.json" - if j["joules_micro"]: + if j["energy_provenance"] is not None: rep.check(claim in files, f"{tag}: a metered package's measurement claim (measurement-claim-missing)") - rep.check(False, f"{tag}: this verifier does not check a measurement claim (measurement-claim-invalid)") + if claim in files: + why = measurement_claim_fault(load(os.path.join(pkg, claim)), j) + rep.check(why is None, f"{tag}: the measurement claim verifies and is about the receipt's figure: {why} " + f"(measurement-claim-invalid)") named.add(claim) else: rep.check(not any(p.startswith("claims/") for p in others), @@ -3036,6 +3431,67 @@ def verify_pack(rep, d, name, pkg=None, inputs=True): rep.check(not other, f"{tag}: other files: {other} (unexpected-file)") +def measurement_claim_fault(b, j): + """§9.4: a metered package's claim, `claims/.measurement.json`, read by the + claim corpus's own verifier (../claim-conformance/claim_verify.py: jwp-receipts §3.1 + steps 1-4, its signature among them, and step 5, a first claim naming no parent), then + checked against the receipt as jwp-receipts §2.4.8 and energy-measurement §6.1 check a + measurement claim against the figure it names (step 6): a `measurement` claim about the + receipt (subject type receipt, subject the receipt's hash), of figure profile + `wai:video-package-receipt-id`, which has no figure index and no group, whose figure + is the receipt's (joules, class and evidence) and whose signer is the receipt's. + None when it holds; else what fails.""" + import claim_verify as CV + try: + c = CV.verify_claim(b.decode("utf-8")) + CV.check_chain(c, None) + except (CV.Reject, UnicodeError) as x: + return f"the claim does not verify ({x})" + f = c["body"] + if c["class"] != "measurement" or c["subject_type"] != "receipt" or c["subject"] != j["receipt_hash"]: + return "not a measurement claim about this receipt" + if f["figure_profile"] != "wai:video-package-receipt-id" or f["figure_index"] is not None or f["group"] is not None: + return "not of the package receipt's figure profile, or with an index or a group" + if f["joules_micro"] != j["joules_micro"]: + return f"a figure of {f['joules_micro']} uJ, the receipt's {j['joules_micro']}" + if f["energy_provenance"] != j["energy_provenance"] or any(f.get(k) != j.get(k) for k in EVIDENCE_MEMBERS): + return "another acquisition class or evidence than the receipt's" + if c["signer_pubkey"] != j["signer_pubkey"]: + return "signed by another key than the receipt's" + return None + + +def repack_fields(ix, k): + """§11.5: piece k's base and refinement entries, every field but the entry's place in its + index: the entry it refines counted from its piece's first, its flags but the duplicate + flag.""" + pc = ix["pieces"][k] + out = [] + for e in ix["entries"][pc["first_entry"]:pc["first_entry"] + pc["n_entries"]]: + if e["role"] not in (BASE, REFINEMENT): + continue + out.append((e["hash"], e["byte_len"], e["cap"], e["role"], e["layer"], e["tier"], e["flags"] & ~ENTRY_DUP, + e["deadline"], e["prefix"], e["sse"], e["man_len"], e["payload_len"], + None if e["refines"] == NO_REFINES else e["refines"] - pc["first_entry"])) + return out + + +def verify_repack(rep, d, tag, j, ixs, parent): + """§11.5: a re-pack against its parent, the golden packs//: its receipt names the + parent's receipt as the package it re-packs, and each component's pieces are the + parent's, piece by piece, their base and refinement entries the parent's.""" + pdir = os.path.join(d, "packs", parent, "package") + pr = read_receipt(load(os.path.join(pdir, "package.receipt.json")))["json"] + rep.check(j["parent_kind"] == "re-pack" and j["parent_receipt_hash"] == pr["receipt_hash"], + f"{tag}: the receipt names packs/{parent}'s receipt as the package it re-packs (§11.5) (repack-parent-mismatch)") + for c, ix in zip(j["components"], ixs): + pix = parse_index(load(os.path.join(pdir, c["index"]))) + same = len(ix["pieces"]) == len(pix["pieces"]) and all( + (a["start"], a["duration"], a["kind"], a["flags"]) == (b["start"], b["duration"], b["kind"], b["flags"]) + and repack_fields(ix, k) == repack_fields(pix, k) for k, (a, b) in enumerate(zip(ix["pieces"], pix["pieces"]))) + rep.check(same, f"{tag} {c['id']}: every piece's base and refinements are packs/{parent}'s (§11.5) (repack-parent-mismatch)") + + class Capture(Report): """A report that keeps what fails, in order, and prints nothing.""" def __init__(self): @@ -3135,15 +3591,20 @@ def index_against_policy(P, comp, c, ix): long = next((k for k, pc in enumerate(ix["pieces"]) if pc["duration"] > mx), None) if long is not None: return f"piece {long}: past the policy's max_ms" + floor_only = comp.get("profile") == "floor-only" for k, pc in enumerate(ix["pieces"]): - if pc["kind"] >= len(KINDS): - return f"piece {k}: a kind a video piece does not have" + # A floor-only component's pieces are of kind none, which no other piece is (§7.8). + if (pc["kind"] == PIECE_KINDS.index("none")) != floor_only or (not floor_only and pc["kind"] >= len(KINDS)): + return f"piece {k}: a kind a piece of this component does not have" rb, mb, then = piece_then(P, ix, k) if (rb, mb) != (pc["rules"], pc["moments"]): return f"piece {k}: the rules and moments" - base, chain, floors = piece_ladder(P, c["id"], then) - want = [(BASE, base["capability"], 0)] + [(REFINEMENT, r["capability"], j + 1) for j, r in enumerate(chain)] \ - + [(FLOOR, r["capability"], 0) for r in floors] + if floor_only: + want = [(FLOOR, r["capability"], 0) for r, _ in floor_names(P, c["id"])] + else: + base, chain, floors = piece_ladder(P, c["id"], then) + want = [(BASE, base["capability"], 0)] + [(REFINEMENT, r["capability"], j + 1) for j, r in enumerate(chain)] \ + + [(FLOOR, r["capability"], 0) for r in floors] mine = E[pc["first_entry"]:pc["first_entry"] + pc["n_entries"]] if [(e["role"], e["cap"], e["layer"]) for e in mine] != want: return f"piece {k}: its entries are not the rungs of its ladder" @@ -3185,6 +3646,9 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix, inputs=True): # Every entry's object (§6.3), the decode-equivalent prefixes (§6.5). objs, mans, decoded = {}, {}, {} + # --jobs: every object's content hash computed ahead, once per file, in the workers. + present = sorted({f"objects/{e['hash'].hex()}.wai" for e in E} & set(files)) + hashes = dict(zip(present, pool().map(work_hash, [os.path.join(pkg, p) for p in present], 1))) if pool() else {} for i, e in enumerate(E): path = f"objects/{e['hash'].hex()}.wai" named.add(path) @@ -3193,7 +3657,9 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix, inputs=True): continue objs[i] = load(os.path.join(pkg, path)) try: - mans[i] = object_rules(ix, i, objs[i]) + mans[i] = object_rules(ix, i, objs[i], hashes.get(path)) + # The object's hash is its entry's: http_verify resolves it without hashing it. + HV.seed_content_hash(objs[i], e["hash"]) rep.check(True, f"{t}: entry {i}: the object rules (§6.3)") except Refused as x: rep.check(False, f"{t}: entry {i}: the object rules (§6.3) ({x.code})") @@ -3204,21 +3670,44 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix, inputs=True): if any(r["component"] == c["id"] and r["role"] == "floor" for r in P["ladder"]): own = owners(ix) for k, pc in enumerate(pieces): - fl = [i for i in range(len(E)) if own[i] == k and E[i]["role"] == FLOOR] - fp = floor_payload(objs[fl[0]][12 + E[fl[0]]["man_len"]:]) if len(fl) == 1 else None - if fp is not None and len(fp[5]) != pc["duration"]: - rep.check(False, f"{t}: piece {k}: {len(fp[5])} floor units for {pc['duration']} frames, before any frame " - f"is decoded (floor-object-mismatch)") - return named | unreached(files, c['id']) + for i in [i for i in range(len(E)) if own[i] == k and E[i]["role"] == FLOOR]: + fp = floor_payload(objs[i][12 + E[i]["man_len"]:]) + if fp is not None and len(fp[5]) != pc["duration"]: + rep.check(False, f"{t}: piece {k}: {len(fp[5])} floor units for {pc['duration']} frames, before any " + f"frame is decoded (floor-object-mismatch)") + return named | unreached(files, c['id']) size = None own = owners(ix) stage = {} # entry index → the frames of the prefix it completes, decoded + # --jobs, without the sources (which re-derive from the frames themselves): each piece's + # base decoded and its layers applied in the workers, ahead, piece by piece; the checks + # below read their digests in the serial run's order. + chains = {} + if pool() and not inputs: + tasks = {} + for k, pc in enumerate(pieces): + mine = range(pc["first_entry"], pc["first_entry"] + pc["n_entries"]) + b = [i for i in mine if E[i]["role"] == BASE] + if b: + chain = [b[0]] + [i for i in mine if E[i]["role"] == REFINEMENT] + tasks[k] = (E[b[0]]["cap"], pc["duration"], [(os.path.join(pkg, f"objects/{E[i]['hash'].hex()}.wai"), E[i]["man_len"]) for i in chain]) + chains = dict(zip(tasks, pool().map(work_chain, list(tasks.values()), 1))) + layer_of = {} for i, e in enumerate(E): ml = e["man_len"] payload = objs[i][12 + ml:] k = own[i] n = pieces[k]["duration"] - if e["role"] == BASE: + if e["role"] == BASE and k in chains: + res = chains[k] + if res[0] == "base": + rep.check(False, f"{t}: entry {i}: the base does not decode ({res[1]}) (prefix-digest)") + return named | unreached(files, c['id']) + pw, ph, digests = (res[1], res[2], res[3]) if res[0] == "ok" else (res[3], res[4], res[5]) + frames = Decoded(digests[0], n) + stage[i] = frames + layer_of[k] = 0 + elif e["role"] == BASE: try: if e["cap"] == "wai.image.png": # §5.7: a still, its one frame held for the piece: the frame and its @@ -3233,6 +3722,7 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix, inputs=True): return named | unreached(files, c['id']) decoded.setdefault(e["hash"], (wv, frames)) stage[i] = frames + if e["role"] == BASE: size = size or (pw, ph) # §5.2: the reference packager's base manifest, as SPEC §3 orders it. want = {"wai": "1.0", "media": comp["media"], "intent": "replicate", @@ -3249,6 +3739,14 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix, inputs=True): "target": None} rep.check(objs[i][8:8 + ml] == json.dumps(want, separators=(",", ":")).encode(), f"{t}: entry {i}: the refinement's manifest is §5.4's (object-manifest)") + if k in chains: + res = chains[k] + layer_of[k] += 1 + if res[0] == "layer" and res[1] == layer_of[k]: + rep.check(False, f"{t}: entry {i}: the layer does not apply to the stage below it ({res[2]}) (prefix-digest)") + return named | unreached(files, c['id']) + stage[i] = Decoded((res[3] if res[0] == "ok" else res[5])[layer_of[k]], n) + continue try: stage[i] = wir1_apply(payload, stage[e["refines"]]) except (SV.Refused, KeyError, IndexError, struct.error) as x: @@ -3259,75 +3757,100 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix, inputs=True): rep.check(prefix_digest_of(stage[i]) == e["prefix"], f"{t}: entry {i}: the prefix decodes to its prefix_digest (prefix-digest)") - # The floor objects (§7.2) and the CMAF files their temporal units give (§7.6). - rung = next((r for r in P["ladder"] if r["component"] == c["id"] and r["role"] == "floor"), None) + # The floor objects (§7.2) and the CMAF files their temporal units give (§7.6), floor + # rung by floor rung in ladder order: the k-th floor entry of each piece is rung k's. sup = P["floor"].get("supplied_listing") settings = blake3(b"wai:render-settings\x01" + jcs({"source": "supplied", "supplied_listing": sup}).encode()).hex() \ if sup else None - seq = seq_obu = None - units_of, frags = [], [] + rungs = floor_names(P, c["id"]) own = owners(ix) - for k, pc in enumerate(pieces): - mine = [i for i in range(len(E)) if own[i] == k] - bases = [i for i in mine if E[i]["role"] == BASE] - fl = [i for i in mine if E[i]["role"] == FLOOR] - rep.check(len(bases) == 1 and rung is not None and len(fl) == 1 and E[fl[0]]["cap"] == rung["capability"], - f"{t}: piece {k}: one base, and one floor object of the floor rung's capability (§5, §7.2) (floor-object-mismatch)") - if len(bases) != 1 or len(fl) != 1: - return named | unreached(files, c['id']) - base, i = bases[0], fl[0] - df = mans[i]["derived_from"] - rep.check(df["motif"] == E[base]["hash"].hex() and df["motif_capability"] == E[base]["cap"], - f"{t}: piece {k}: the floor presents its piece's base (floor-motif-mismatch)") - rep.check(sup is not None and df["renderer"] == f"supplied@{sup}" and df["settings"] == settings, - f"{t}: piece {k}: a supplied floor's renderer and settings (§7.2) (floor-object-mismatch)") - fp = floor_payload(objs[i][12 + E[i]["man_len"]:]) - if fp is None: - rep.check(False, f"{t}: piece {k}: the floor payload is not §7.2's framing (floor-object-mismatch)") - return named | unreached(files, c['id']) - rep.check(len(fp[5]) == pc["duration"], f"{t}: piece {k}: one temporal unit per frame (floor-object-mismatch)") - try: - units = [obus(u) for u in fp[5]] - if any(not o or o[0][0] != OBU_TD or sum(x[0] == OBU_TD for x in o) != 1 or unit_frame(o) is None for o in units): - raise Malformed("a unit that is not one temporal unit holding a frame") - if seq is None: - seq_obu = next((whole for typ, whole, _ in units[0] if typ == OBU_SEQ), None) if units else None - if seq_obu is None: - raise Malformed("the first unit holds no sequence header") - seq = sequence_header(obus(seq_obu)[0][2]) - # §7.3 against the package: every sequence header the first's bytes, a shown key - # frame with one. - for o in units: - if any(typ == OBU_SEQ and whole != seq_obu for typ, whole, _ in o): - raise Malformed("a sequence header other than the first") - if shown_key(o, seq) and not any(typ == OBU_SEQ for typ, _, _ in o): - raise Malformed("a shown key frame without a sequence header") - except Malformed as x: - rep.check(False, f"{t}: piece {k}: the floor payload ({x}) (floor-av1-malformed)") - return named | unreached(files, c['id']) - rep.check(fp[:5] == (pc["duration"], seq["max_h"], seq["max_w"], num, den), - f"{t}: piece {k}: the floor payload's n, h, w, fps_num and fps_den (§7.2) (floor-object-mismatch)") - rep.check(mans[i].get("target") == {"frames": pc["duration"], "h": seq["max_h"], "w": seq["max_w"]}, - f"{t}: piece {k}: the floor's target is its piece's frames and its size (§5.2, §7.2) (floor-object-mismatch)") - keys = [shown_key(o, seq) for o in units] - ok = bool(keys) and keys[0] and (P["floor"]["extra_keyframes"] or not any(keys[1:])) - rep.check(ok, f"{t}: piece {k}: it opens on a shown key frame, with no other unless the policy allows (floor-keyframe-mismatch)") - if not ok: - return named | unreached(files, c['id']) - units_of.append(fp[5]) - frag = fragment(ix["first_piece"] + k, pc["start"], den, [(without_delimiters(o), shown_key(o, seq)) for o in units]) - frags.append((hashlib.sha256(frag).hexdigest() + ".m4s", frag)) - init = init_mp4(num, seq["max_w"], seq["max_h"], av1c(seq, seq_obu)) - for p, want in [("init.mp4", init)] + frags: - path = f"floor/{c['id']}/{p}" - named.add(path) - rep.check(path in files and load(os.path.join(pkg, path)) == want, - f"{t}: {path} is the one the floor's temporal units give (floor-cmaf-mismatch)") + floors, units_of = [], {} + for fk, (rung, fname) in enumerate(rungs if rungs else [(None, c["id"])]): + seq = seq_obu = None + units_of[fname], frags = [], [] + for k, pc in enumerate(pieces): + mine = [i for i in range(len(E)) if own[i] == k] + bases = [i for i in mine if E[i]["role"] == BASE] + fl = [i for i in mine if E[i]["role"] == FLOOR] + floor_only = comp.get("profile") == "floor-only" + ok = len(bases) == (0 if floor_only else 1) and rung is not None and len(fl) == len(rungs) \ + and E[fl[fk]]["cap"] == rung["capability"] + rep.check(ok, f"{t}: piece {k}: one base (none, floor-only), and one floor object of each floor rung's " + f"capability (§5, §7.2, §7.8) (floor-object-mismatch)") + if not ok: + return named | unreached(files, c['id']) + i = fl[fk] + if floor_only: + # §7.8: a floor that presents no base names no motif. + rep.check(mans[i].get("derived_from") is None, + f"{t}: piece {k}: a floor-only floor carries no derived_from (§7.8) (floor-object-mismatch)") + else: + base = bases[0] + df = mans[i]["derived_from"] + rep.check(df["motif"] == E[base]["hash"].hex() and df["motif_capability"] == E[base]["cap"], + f"{t}: piece {k}: the floor presents its piece's base (floor-motif-mismatch)") + rep.check(sup is not None and df["renderer"] == f"supplied@{sup}" and df["settings"] == settings, + f"{t}: piece {k}: a supplied floor's renderer and settings (§7.2) (floor-object-mismatch)") + fp = floor_payload(objs[i][12 + E[i]["man_len"]:]) + if fp is None: + rep.check(False, f"{t}: piece {k}: the floor payload is not §7.2's framing (floor-object-mismatch)") + return named | unreached(files, c['id']) + rep.check(len(fp[5]) == pc["duration"], f"{t}: piece {k}: one temporal unit per frame (floor-object-mismatch)") + try: + units = [obus(u) for u in fp[5]] + if any(not o or o[0][0] != OBU_TD or sum(x[0] == OBU_TD for x in o) != 1 or unit_frame(o) is None for o in units): + raise Malformed("a unit that is not one temporal unit holding a frame") + if seq is None: + seq_obu = next((whole for typ, whole, _ in units[0] if typ == OBU_SEQ), None) if units else None + if seq_obu is None: + raise Malformed("the first unit holds no sequence header") + seq = sequence_header(obus(seq_obu)[0][2]) + except Malformed as x: + rep.check(False, f"{t}: piece {k}: the floor payload ({x}) (floor-av1-malformed)") + return named | unreached(files, c['id']) + # The object's own members against the floor's sequence header (§7.2), before + # its units are held to it (§11.3's order). + rep.check(fp[:5] == (pc["duration"], seq["max_h"], seq["max_w"], num, den), + f"{t}: piece {k}: the floor payload's n, h, w, fps_num and fps_den (§7.2) (floor-object-mismatch)") + rep.check(mans[i].get("target") == {"frames": pc["duration"], "h": seq["max_h"], "w": seq["max_w"]}, + f"{t}: piece {k}: the floor's target is its piece's frames and its size (§5.2, §7.2) (floor-object-mismatch)") + try: + # §7.3 against the package: every sequence header the first's bytes, a shown + # key frame with one. + for o in units: + if any(typ == OBU_SEQ and whole != seq_obu for typ, whole, _ in o): + raise Malformed("a sequence header other than the first") + if shown_key(o, seq) and not any(typ == OBU_SEQ for typ, _, _ in o): + raise Malformed("a shown key frame without a sequence header") + except Malformed as x: + rep.check(False, f"{t}: piece {k}: the floor payload ({x}) (floor-av1-malformed)") + return named | unreached(files, c['id']) + keys = [shown_key(o, seq) for o in units] + ok = bool(keys) and keys[0] and (P["floor"]["extra_keyframes"] or not any(keys[1:])) + rep.check(ok, f"{t}: piece {k}: it opens on a shown key frame, with no other unless the policy allows (floor-keyframe-mismatch)") + if not ok: + return named | unreached(files, c['id']) + units_of[fname].append(fp[5]) + if not P["floor"].get("supplied_form"): + frag = fragment(ix["first_piece"] + k, pc["start"], den, [(without_delimiters(o), shown_key(o, seq)) for o in units]) + frags.append((hashlib.sha256(frag).hexdigest() + ".m4s", frag)) + if P["floor"].get("supplied_form") == "cmaf": + # §7.7: the ladder's own files, read, not rebuilt. + frags = cmaf_package_files(rep, t, pkg, files, P, fname, pieces, units_of[fname], num, den, named) + if frags is None: + return named | unreached(files, c['id']) + else: + init = init_mp4(num, seq["max_w"], seq["max_h"], av1c(seq, seq_obu)) + for p, want in [("init.mp4", init)] + frags: + path = f"floor/{fname}/{p}" + named.add(path) + rep.check(path in files and load(os.path.join(pkg, path)) == want, + f"{t}: {path} is the one the floor's temporal units give (floor-cmaf-mismatch)") + floors.append((fname, av1_codecs(seq), seq["max_w"], seq["max_h"], frags)) # The catalogs (§8.3–§8.8). try: - cats = render_catalogs(P, ix, c["id"], (av1_codecs(seq), seq["max_w"], seq["max_h"], frags), size, - j["policy_digest"]) + cats = render_catalogs(P, ix, c["id"], floors, size, j["policy_digest"]) except (ValueError, OverflowError) as x: # A time or count past the canonical range (a start past 2^53 − 1): no catalog can # be written for this index (§11.3). @@ -3337,19 +3860,93 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix, inputs=True): named.add(path) rep.check(path in files and load(os.path.join(pkg, path)) == cats[path], f"{t}: {path} is the one the index, the objects and the floor give (catalog-mismatch)") - http_views(rep, t, c["id"], cats, {f"{e['hash'].hex()}.wai": objs[i] for i, e in enumerate(E)}) + http_views(rep, t, c["id"], floors[0][0], cats, {f"{e['hash'].hex()}.wai": objs[i] for i, e in enumerate(E)}) if inputs: verify_sources(rep, t, root, P, comp, ix, stage, units_of, objs) return named -def http_views(rep, t, c, cats, objmap): +def read_listing(b): + """SPEC §3.1: a listing's lines as (path, SHA-256 hex), or None when it is not one.""" + if not b or not b.endswith(b"\n"): + return None + out = [] + for line in b[:-1].split(b"\n"): + if len(line) < 67 or line[64:66] != b" " or not re.fullmatch(rb"[0-9a-f]{64}", line[:64]): + return None + try: + out.append((line[66:].decode("ascii"), line[:64].decode())) + except UnicodeError: + return None + paths = [p for p, _ in out] + if not all(all(segment_ok(x) for x in p.split("/")) for p in paths) or paths != sorted(paths, key=lambda p: p.encode()) \ + or len(set(paths)) != len(paths) or len({p.lower() for p in paths}) != len(paths) \ + or any(q.startswith(p + "/") for p in paths for q in paths): + return None + return out + + +def cmaf_package_files(rep, t, pkg, files, P, fname, pieces, units_of, num, den, named): + """§7.7, §11.3: a floor rung's CMAF files in the CMAF form, bound to the receipt by the + supplied floor's listing: floor.listing hashes to floor.supplied_listing and is a + listing; floor//init.mp4 is its line for /init.mp4 and an initialization segment + §7.7 reads; the listing's media segments of /, in its order, one per piece, piece k's + floor//.m4s, §7.7's boxes, opening on a sync sample, a frame a sample from + the piece's start, its samples the piece's floor units without their temporal + delimiters (floor-cmaf-mismatch). Returns each piece's (fragment name, bytes), or None + at the first that fails.""" + named.add("floor.listing") + lb = load(os.path.join(pkg, "floor.listing")) if "floor.listing" in files else b"" + lines = read_listing(lb) + ok = lines is not None and hashlib.sha256(lb).hexdigest() == P["floor"].get("supplied_listing") + rep.check(ok, f"{t}: floor.listing is a listing hashing to floor.supplied_listing (§7.7) (floor-cmaf-mismatch)") + if not ok: + return None + shas = dict(lines) + init_name = f"{fname}/init.mp4" + segs = [p for p, _ in lines if p.startswith(f"{fname}/") and p != init_name] + init_path = f"floor/{fname}/init.mp4" + named.add(init_path) + try: + ib = load(os.path.join(pkg, init_path)) if init_path in files else None + init = cmaf_init(ib) if ib is not None and shas.get(init_name) == hashlib.sha256(ib).hexdigest() else None + except CmafFault: + init = None + rep.check(init is not None, f"{t}: {init_path} is the supplied {init_name}, an initialization segment §7.7 reads (floor-cmaf-mismatch)") + if init is None: + return None + ok = len(segs) == len(pieces) and all(p.endswith(".m4s") and "/" not in p[len(fname) + 1:] for p in segs) + rep.check(ok, f"{t}: {fname}/ holds one media segment per piece in floor.listing (floor-cmaf-mismatch)") + if not ok: + return None + frags = [] + for k, (pc, seg) in enumerate(zip(pieces, segs)): + path = f"floor/{fname}/{shas[seg]}.m4s" + named.add(path) + b = load(os.path.join(pkg, path)) if path in files else None + try: + sm = cmaf_segment(b, init) if b is not None and hashlib.sha256(b).hexdigest() == shas[seg] else None + if sm is not None: + cmaf_times(sm, init[1], num, den, pc["start"]) + except CmafFault: + sm = None + ok = sm is not None and not sm[0][3] & 0x00010000 and cmaf_units(b, sm) == units_of[k] + rep.check(ok, f"{t}: {path}: the supplied {seg}, opening on a sync sample, a frame a sample from piece {k}'s start, " + f"its samples the piece's floor units (§7.7) (floor-cmaf-mismatch)") + if not ok: + return None + frags.append((path.rsplit("/", 1)[1], b)) + return frags + + +def http_views(rep, t, c, first_floor, cats, objmap): """§8.3 read by http-delivery's own verifier (http_verify.py): each class's WAI media - playlist adjacent to the classical one, the union and floor of the objects it lists + playlist adjacent to the classical one (the first floor's; every floor's has the + pieces' durations), the union and floor of the objects it lists (an object listed twice is one, a layer joins every occurrence of its base, http-delivery §4.2) equal to its master's CODECS and X-WAI-FLOOR, and its MPD adjacent to the classical MPD.""" - floor_text = cats[f"hls/classic/floor-{c}.m3u8"].decode() + floor_text = cats[f"hls/classic/floor-{first_floor}.m3u8"].decode() for path in sorted(cats): m = re.fullmatch(r"hls/([^/]+)/wai-" + re.escape(c) + r"\.m3u8", path) if not m: @@ -3387,6 +3984,14 @@ def verify_sources(rep, t, root, P, comp, ix, stage, units_of, objs): refinement's bytes and reconstruction (§5.4), and every entry's sse (§6.6); and the supplied floor (§7.3). `stage` is each entry's prefix as the package decodes.""" num, den = comp["timebase"]["num"], comp["timebase"]["den"] + if comp.get("profile") == "floor-only": + # §7.8: no source; the pieces are the first floor rung's, every piece at most + # max_ms, and the supplied floor as §7.3 and §7.7 check it. + mx = units_from_ms(P["segmentation"]["max_ms"], num, den) + rep.check(all(pc["duration"] <= mx for pc in ix["pieces"]) and all(pc["flags"] == 0 for pc in ix["pieces"]), + f"{t}: every floor-only piece at most max_ms, with no flag (§7.8) (floor-piece-length)") + verify_supplied_floor(rep, t, root, P, comp, ix, range(sum(pc["duration"] for pc in ix["pieces"])), num, den, units_of) + return source = comp.get("source", "") name = source[:-len(".y4m")] if not source.endswith(".y4m") or fixture_spans(name) is None: @@ -3453,39 +4058,114 @@ def verify_sources(rep, t, root, P, comp, ix, stage, units_of, objs): f"{t}: entry {i}: sse against the source (§6.6)") below = nxt - # The supplied floor (§7.3): its listing, its temporal units and its key frames. + verify_supplied_floor(rep, t, root, P, comp, ix, frames, num, den, units_of) + + +def verify_supplied_floor(rep, t, root, P, comp, ix, frames, num, den, units_of): + """The supplied floor (§7.3, §7.7): its listing, then each floor rung's stream or CMAF + files, its temporal units and its key frames, against the index's pieces; a floor-only + component's first floor rung decides them (§7.8): its shown key frames, in the OBU + form, are exactly the pieces' starts. `frames` has one element per source frame.""" + # The supplied floor (§7.3): its listing, then each floor rung's stream, its temporal + # units and its key frames. sup = os.path.join(root, "floor") names = tree(sup) rep.check(hashlib.sha256(listing(sup, names)).hexdigest() == P["floor"].get("supplied_listing"), f"{t}: the supplied floor's listing hashes to floor.supplied_listing (floor-listing-mismatch)") - stream = f"{comp['id']}.obu" - if stream not in names: - rep.check(False, f"{t}: no {stream} in the supplied floor (floor-stream-missing)") + for _, fname in floor_names(P, comp["id"]): + if P["floor"].get("supplied_form") == "cmaf": + verify_supplied_cmaf(rep, t, sup, names, fname, ix, frames, num, den, P, units_of) + continue + stream = f"{fname}.obu" + if stream not in names: + rep.check(False, f"{t}: no {stream} in the supplied floor (floor-stream-missing)") + return + try: + units = temporal_units(load(os.path.join(sup, stream))) + if not units or not any(typ == OBU_SEQ for typ, _, _ in units[0][1]): + raise Malformed("the first unit holds no sequence header") + first = next(whole for typ, whole, _ in units[0][1] if typ == OBU_SEQ) + seq = sequence_header(obus(first)[0][2]) + for u, o in units: + if unit_frame(o) is None: + raise Malformed("a unit holding no frame") + if any(typ == OBU_SEQ and whole != first for typ, whole, _ in o): + raise Malformed("a sequence header other than the first") + if shown_key(o, seq) and not any(typ == OBU_SEQ for typ, _, _ in o): + raise Malformed("a shown key frame without a sequence header") + rep.check(True, f"{t}: {stream} splits into temporal units, each a frame (floor-av1-malformed)") + except Malformed as x: + rep.check(False, f"{t}: {stream} ({x}) (floor-av1-malformed)") + return + rep.check(len(units) == len(frames), f"{t}: {stream}: one temporal unit per source frame (floor-frame-count)") + keys = [f for f, (_, o) in enumerate(units) if shown_key(o, seq)] + starts = [pc["start"] for pc in ix["pieces"]] + plans = comp.get("profile") == "floor-only" and fname == floor_names(P, comp["id"])[0][1] + ok = set(starts) <= set(keys) and (P["floor"]["extra_keyframes"] and not plans or keys == starts) + rep.check(ok, f"{t}: {stream}: shown key frames at the piece starts {starts}, and only there: {keys} (floor-keyframe-mismatch)") + rep.check([[u for u, _ in units[pc["start"]:pc["start"] + pc["duration"]]] for pc in ix["pieces"]] == units_of[fname], + f"{t}: each floor object of {fname} holds its piece's temporal units of {stream} (§7.2)") + + +def verify_supplied_cmaf(rep, t, sup, names, fname, ix, frames, num, den, P, units_of): + """§7.7, the supplied floor in the CMAF form: /init.mp4 (floor-stream-missing), the + other files of / media segments in byte order (floor-cmaf-malformed); each segment's + boxes, a sync sample first (floor-cmaf-malformed), every sample one frame and each + segment starting where the one before ended (floor-cmaf-timing); the samples as + temporal units (floor-av1-malformed), one per source frame (floor-frame-count); one + segment per piece, holding its frames (floor-cmaf-pieces); the key frames + (floor-keyframe-mismatch); and each floor object holding its piece's segment's units.""" + init_name = f"{fname}/init.mp4" + if init_name not in names: + rep.check(False, f"{t}: no {init_name} in the supplied floor (floor-stream-missing)") + return + segs = [n for n in names if n.startswith(f"{fname}/") and n != init_name] + bad = [n for n in segs if "/" in n[len(fname) + 1:] or not n.endswith(".m4s")] + rep.check(segs and not bad, f"{t}: {fname}/ holds init.mp4 and media segments only: {bad} (floor-cmaf-malformed)") + if not segs or bad: return try: - units = temporal_units(load(os.path.join(sup, stream))) - if not units or not any(typ == OBU_SEQ for typ, _, _ in units[0][1]): + init = cmaf_init(load(os.path.join(sup, init_name))) + per_seg, at = [], 0 + for n in segs: + b = load(os.path.join(sup, n)) + sm = cmaf_segment(b, init) + if sm[0][3] & 0x00010000: + raise CmafFault("floor-cmaf-malformed", f"{n}: a segment that does not open on a sync sample") + cmaf_times(sm, init[1], num, den, at) + per_seg.append((n, at, cmaf_units(b, sm))) + at += len(sm) + rep.check(True, f"{t}: {fname}/'s CMAF files read as §7.7's (floor-cmaf-malformed, floor-cmaf-timing)") + except CmafFault as x: + rep.check(False, f"{t}: {fname}/: {x}") + return + units = [u for _, _, us in per_seg for u in us] + try: + parsed = [obus(u) for u in units] + if any(not o or o[0][0] != OBU_TD or sum(x[0] == OBU_TD for x in o) != 1 or unit_frame(o) is None for o in parsed): + raise Malformed("a sample that is not one temporal unit holding a frame") + first = next((whole for typ, whole, _ in parsed[0] if typ == OBU_SEQ), None) + if first is None: raise Malformed("the first unit holds no sequence header") - first = next(whole for typ, whole, _ in units[0][1] if typ == OBU_SEQ) seq = sequence_header(obus(first)[0][2]) - for u, o in units: - if unit_frame(o) is None: - raise Malformed("a unit holding no frame") + for o in parsed: if any(typ == OBU_SEQ and whole != first for typ, whole, _ in o): raise Malformed("a sequence header other than the first") if shown_key(o, seq) and not any(typ == OBU_SEQ for typ, _, _ in o): raise Malformed("a shown key frame without a sequence header") - rep.check(True, f"{t}: the supplied stream splits into temporal units, each a frame (floor-av1-malformed)") + rep.check(True, f"{t}: {fname}/'s samples are temporal units, each a frame (floor-av1-malformed)") except Malformed as x: - rep.check(False, f"{t}: the supplied stream ({x}) (floor-av1-malformed)") + rep.check(False, f"{t}: {fname}/'s samples ({x}) (floor-av1-malformed)") return - rep.check(len(units) == len(frames), f"{t}: one temporal unit per source frame (floor-frame-count)") - keys = [f for f, (_, o) in enumerate(units) if shown_key(o, seq)] + rep.check(len(units) == len(frames), f"{t}: {fname}/: one sample per source frame (floor-frame-count)") starts = [pc["start"] for pc in ix["pieces"]] + rep.check([(a, len(us)) for _, a, us in per_seg] == [(pc["start"], pc["duration"]) for pc in ix["pieces"]], + f"{t}: {fname}/: one media segment per piece, holding its frames (floor-cmaf-pieces)") + keys = [f for f, o in enumerate(parsed) if shown_key(o, seq)] ok = set(starts) <= set(keys) and (P["floor"]["extra_keyframes"] or keys == starts) - rep.check(ok, f"{t}: shown key frames at the piece starts {starts}, and only there: {keys} (floor-keyframe-mismatch)") - rep.check([[u for u, _ in units[pc["start"]:pc["start"] + pc["duration"]]] for pc in ix["pieces"]] == units_of, - f"{t}: each floor object holds its piece's temporal units of the supplied stream (§7.2)") + rep.check(ok, f"{t}: {fname}/: shown key frames at the piece starts {starts}, and only there: {keys} (floor-keyframe-mismatch)") + rep.check([us for _, _, us in per_seg] == units_of[fname], + f"{t}: each floor object of {fname} holds its piece's media segment's samples (§7.7)") def mutation_verdict(m, c, used): @@ -3771,8 +4451,8 @@ def verify(d): ix = parse_index(load(os.path.join(d, c["index"]))) index_rules(ix) f = c["floor"] - got = render_moq(ix["component"], ix["entries"], c["layer_lag_ms"], c["default_deadline"], (f["codec"], f["w"], f["h"]), - c["namespace"]) + got = render_moq(ix["component"], ix["entries"], c["layer_lag_ms"], c["default_deadline"], + [(ix["component"], f["codec"], f["w"], f["h"])], c["namespace"]) rep.check(got == c["moq"].encode(), f"{c['index']}: the MoQ catalog §8.4 gives") names = {t["name"] for t in json.loads(got)["tracks"]} rep.check(all(x in names for t in json.loads(got)["tracks"] for x in t.get("depends", [])), @@ -3802,14 +4482,40 @@ def verify(d): verify_package_mutations(rep, d) # staged-mutations/: the staged golden altered (§5.4, §5.7, §6.5, §8.6). verify_package_mutations(rep, d, "staged-mutations") + # ladder-mutations/: the ladder golden altered (§7.1: several floor rungs). + verify_package_mutations(rep, d, "ladder-mutations") + # cmaf-mutations/: the CMAF golden altered (§7.7: the receipt binds the supplied bytes). + verify_package_mutations(rep, d, "cmaf-mutations") return rep def main(argv): - if len(argv) != 2: + global JOBS + args = argv[1:] + package = None + while args and args[0].startswith("--"): + if args[0] == "--jobs" and len(args) > 1 and args[1].isdigit() and int(args[1]) >= 1: + JOBS, args = int(args[1]), args[2:] + elif args[0] == "--native-blake3": + use_native_blake3() + args = args[1:] + elif args[0] == "--package" and len(args) > 1: + package, args = args[1], args[2:] + else: + print(__doc__) + return 2 + if package is not None: + if args: + print(__doc__) + return 2 + rep = Report() + verify_pack(rep, HERE, "--package", pkg=package, inputs=False) + print(f"pack_verify: {rep.checked - rep.failures}/{rep.checked} checks hold") + return 0 if rep.failures == 0 else 1 + if len(args) != 1: print(__doc__) return 2 - rep = verify(argv[1]) + rep = verify(args[0]) check_reader_limit(rep) print(f"pack_verify: {rep.checked - rep.failures}/{rep.checked} checks hold") return 0 if rep.failures == 0 else 1 diff --git a/wai/pack-conformance/package-mutations/expected.json b/wai/pack-conformance/package-mutations/expected.json index e1842869e..6da029e10 100644 --- a/wai/pack-conformance/package-mutations/expected.json +++ b/wai/pack-conformance/package-mutations/expected.json @@ -282,6 +282,21 @@ "reason": "receipt-json", "remove": [] }, + { + "name": "metered", + "reason": "ok", + "remove": [] + }, + { + "name": "metered-claim-another-figure", + "reason": "measurement-claim-invalid", + "remove": [] + }, + { + "name": "metered-claim-missing", + "reason": "measurement-claim-missing", + "remove": [] + }, { "name": "empty-directory", "reason": "unexpected-file", diff --git a/wai/pack-conformance/package-mutations/metered-claim-another-figure/LISTING b/wai/pack-conformance/package-mutations/metered-claim-another-figure/LISTING new file mode 100644 index 000000000..da0b0dbe2 --- /dev/null +++ b/wai/pack-conformance/package-mutations/metered-claim-another-figure/LISTING @@ -0,0 +1,36 @@ +9414f625ee38f54eb010a003c262ec5ed1ffc95b82510dc032da065846ff4b9b claims/01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2.measurement.json +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +4a2370c37281e64f30cb66a4bd7125854d6280855e90f00229b177bd70af3d8c package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/metered-claim-another-figure/claims/01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2.measurement.json b/wai/pack-conformance/package-mutations/metered-claim-another-figure/claims/01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2.measurement.json new file mode 100644 index 000000000..07001fc41 --- /dev/null +++ b/wai/pack-conformance/package-mutations/metered-claim-another-figure/claims/01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2.measurement.json @@ -0,0 +1 @@ +{"body":{"bracket":null,"build":{"profile":"release","target":null,"toolchain":null},"energy_calibration_ref":null,"energy_counter_filtering":null,"energy_provenance":"ModelBased","energy_uncertainty":null,"figure_index":null,"figure_profile":"wai:video-package-receipt-id","group":null,"joules_micro":500001,"power_reader":"model:500mW"},"class":"measurement","issued_at":"2026-10-21T00:00:00Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"d6935698b54a44f0ff253340fe62334f30a2e8de5d4ab277496e67e666e621b87fdbd558867a7d64f5be637c9388f0cd5696d7629ad87fe89bf09194e0a35500","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","subject":"01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2","subject_type":"receipt"} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/metered-claim-another-figure/package.receipt.json b/wai/pack-conformance/package-mutations/metered-claim-another-figure/package.receipt.json new file mode 100644 index 000000000..a59cb6c3b --- /dev/null +++ b/wai/pack-conformance/package-mutations/metered-claim-another-figure/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"5ffc2be3f21af79078a1158e3d669e4268a35b0ca61a6e73ff252fdf38f346d0","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":"ModelBased","grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":500000,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2","sig":"eb052a53c735830304a6c74230dcc348ca6d0c8110497e916407062da83e627db36ba165167ed217e451a26dfa1f5b5e21cf766725063c4532df82ff9d949604","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/metered-claim-missing/LISTING b/wai/pack-conformance/package-mutations/metered-claim-missing/LISTING new file mode 100644 index 000000000..67b4a1e2b --- /dev/null +++ b/wai/pack-conformance/package-mutations/metered-claim-missing/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +4a2370c37281e64f30cb66a4bd7125854d6280855e90f00229b177bd70af3d8c package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/metered-claim-missing/package.receipt.json b/wai/pack-conformance/package-mutations/metered-claim-missing/package.receipt.json new file mode 100644 index 000000000..a59cb6c3b --- /dev/null +++ b/wai/pack-conformance/package-mutations/metered-claim-missing/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"5ffc2be3f21af79078a1158e3d669e4268a35b0ca61a6e73ff252fdf38f346d0","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":"ModelBased","grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":500000,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2","sig":"eb052a53c735830304a6c74230dcc348ca6d0c8110497e916407062da83e627db36ba165167ed217e451a26dfa1f5b5e21cf766725063c4532df82ff9d949604","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/metered/LISTING b/wai/pack-conformance/package-mutations/metered/LISTING new file mode 100644 index 000000000..ecc747b37 --- /dev/null +++ b/wai/pack-conformance/package-mutations/metered/LISTING @@ -0,0 +1,36 @@ +a2ef584d432d2a1a1af10cb9d342c0fecbb2c05d72fc054d3792a7ab4bacdce6 claims/01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2.measurement.json +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +4a2370c37281e64f30cb66a4bd7125854d6280855e90f00229b177bd70af3d8c package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/metered/claims/01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2.measurement.json b/wai/pack-conformance/package-mutations/metered/claims/01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2.measurement.json new file mode 100644 index 000000000..e320e2364 --- /dev/null +++ b/wai/pack-conformance/package-mutations/metered/claims/01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2.measurement.json @@ -0,0 +1 @@ +{"body":{"bracket":null,"build":{"profile":"release","target":null,"toolchain":null},"energy_calibration_ref":null,"energy_counter_filtering":null,"energy_provenance":"ModelBased","energy_uncertainty":null,"figure_index":null,"figure_profile":"wai:video-package-receipt-id","group":null,"joules_micro":500000,"power_reader":"model:500mW"},"class":"measurement","issued_at":"2026-10-21T00:00:00Z","kind":"claim","parent_receipt_hash":null,"seq":0,"sig":"8e4df4a0568d61e153cd7eca9e8664dc69b11202d8999138b639ffa77f7ade7de212a2b31d3a7977861ab176286a77633257a17d0919c5e04a9d2741b85fc807","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","subject":"01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2","subject_type":"receipt"} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/metered/package.receipt.json b/wai/pack-conformance/package-mutations/metered/package.receipt.json new file mode 100644 index 000000000..a59cb6c3b --- /dev/null +++ b/wai/pack-conformance/package-mutations/metered/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"5ffc2be3f21af79078a1158e3d669e4268a35b0ca61a6e73ff252fdf38f346d0","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":"ModelBased","grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":500000,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"01d891a6690096d3de7c8e7cec802cb95f82deb84b5d45994dd713e7576673f2","sig":"eb052a53c735830304a6c74230dcc348ca6d0c8110497e916407062da83e627db36ba165167ed217e451a26dfa1f5b5e21cf766725063c4532df82ff9d949604","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-cmaf-01/floor/hi/init.mp4 b/wai/pack-conformance/packs/video-cmaf-01/floor/hi/init.mp4 new file mode 100644 index 000000000..af7fc6d9d Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/floor/hi/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0000.m4s b/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0000.m4s new file mode 100644 index 000000000..3d6538a6a Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0000.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0001.m4s b/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0001.m4s new file mode 100644 index 000000000..738585244 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0001.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0002.m4s b/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0002.m4s new file mode 100644 index 000000000..912518601 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0002.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0003.m4s b/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0003.m4s new file mode 100644 index 000000000..ac02ab23e Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0003.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0004.m4s b/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0004.m4s new file mode 100644 index 000000000..4a01d9311 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/floor/hi/seg_0004.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/floor/lo/init.mp4 b/wai/pack-conformance/packs/video-cmaf-01/floor/lo/init.mp4 new file mode 100644 index 000000000..2f94c3ac7 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/floor/lo/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0000.m4s b/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0000.m4s new file mode 100644 index 000000000..f707dcd29 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0000.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0001.m4s b/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0001.m4s new file mode 100644 index 000000000..74171d12c Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0001.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0002.m4s b/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0002.m4s new file mode 100644 index 000000000..537675bb8 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0002.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0003.m4s b/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0003.m4s new file mode 100644 index 000000000..90c0b6fca Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0003.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0004.m4s b/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0004.m4s new file mode 100644 index 000000000..194871127 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/floor/lo/seg_0004.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/key.hex b/wai/pack-conformance/packs/video-cmaf-01/key.hex new file mode 100644 index 000000000..f1f5c260b --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/key.hex @@ -0,0 +1 @@ +dd0a68c39958d90953634b6c1070e75dc5a5900d134e53f6b0dd35f0cec7abaa diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/LISTING b/wai/pack-conformance/packs/video-cmaf-01/package/LISTING new file mode 100644 index 000000000..117677835 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/LISTING @@ -0,0 +1,47 @@ +688cb303ccb16fcdd21c8cb681f936ba813829e71fbf15df0ccb28b04c134447 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +03ab06e026880875581952f91232405077be9a522dbf739b6a9d093458369710 floor.listing +14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889 floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s +759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1 floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s +d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s +e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s +eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8 floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s +a8a19ef9cd4d540037ef298097bbc5317de6a99690c8c4a745cc867c034208d6 floor/hi/init.mp4 +0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s +10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8 floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s +2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73 floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s +a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8 floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s +c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s +6dd501e27365ce9ef90576771aad73de4d200362e6974551847afc3c90604344 floor/lo/init.mp4 +ebdc7e40aecbe8b61124ac78f233b0dfaf2a8c5674d3fc9bb023935de1d9b6cb hls/classic/floor-hi.m3u8 +66f5eb8b89b80e7da839b23f39665eb85480c6a725fa29d93556efee673c1b9e hls/classic/floor-lo.m3u8 +d793737b69231eface4951001e65b3c52cba864f231246a41e64d49c77ebdc97 hls/classic/master.m3u8 +d793737b69231eface4951001e65b3c52cba864f231246a41e64d49c77ebdc97 hls/legacy/master.m3u8 +8c3b5e2b18192b383c8df8675acd435b861a2585bc6eaf0c09292754f44a8cef hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +8c3b5e2b18192b383c8df8675acd435b861a2585bc6eaf0c09292754f44a8cef hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +649330bcfaaee54f802f6b4ddf6e459daca8bb0e8df86c87a9d6bd0d65cd09ea index-v.wsix +68f00abff5ef19f074d510f8cce4592afc2a1d4981a87663965a46dc8bcbe1d3 moq/catalog.json +9f381e2ab764b5e14770b00503c1c90b9c22ba22f87464b2869a70644af11939 objects/08797901ff55d0bb4afba5cffe95df64b7e216f3a0c9ba82006133bb9b6d7f16.wai +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +d5f9cdd996920e07339b79093b9e4e5cb4237f1780f268220cc6f89c79aea90a objects/399048a078b0f2983149266a44eaf207cfa63f490212b69389fba4d72f62c60f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +d03f0813c266294f84483cfe4db177d094c06b95ab3a8471d6b2ec53401f05a7 objects/a236e9070d10fd09342cafdbf6973edd35e5266cf850fe2f38f77536ac803912.wai +2316616549f7f92dc29e34fe656c332c9f4a1e20f4be26396976407acc792ba4 objects/a3ae43406f9966487b6c5f5a4d56e9dca50c327423fcfd2a98f4c342d1dbfaf9.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +6d665f1bef3784cbb428abc28724f74fa83c8f2bc812828621c93c5514b6dc0e objects/c5253efe2949669bb9df01d9f60c01f2570ba4be1c0af26105e092dfccf90d30.wai +0acecba00acc28e313805189fd826ad3a9fab1fe69727ef2d5999fc543426289 objects/c81171fa53998cfe94207ad66ab9b53cfbd4b3f058730e62512ca9f0f6ef2ea3.wai +f3bd9cbe3c2518c997a4d1d3a32145a04f55ef53e0b5760a6ce6aa5e14fe06a7 objects/dea0f6415f639fca99d91a0341aba164078a1bd4780e21d72cbf2234928272ec.wai +d22c4f8ac8a5de43bbec6be04ea8b6da72f2bd133f8f57a31607362670178c68 objects/f467872fe8d3cd1f60f832c997cc14fd00abbf601c61286a612089b4a53a195a.wai +f1f4e32ba9f76e36431d97e0fd52b9025075f553bf6b9f63c484d60573c8ea90 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +428e24f9e1c62fc5590ce3d3a6239a5c274dcfc1391a2ea2abe8dc0562da7d20 policy.json +9d2b4c15ac17ee074f646d0b1ed11ef56557f633ff67a3e6a6d0eb4dd1596548 policy.toml +2f5807b891b8eb0a44b76c4303487be1dcf19e111f06f6c642e14b81c85d7c4d stages/legacy.json +8695fff48ca86321c5198e3a8cf660ccba208df4f40fd1907e76c64a52674cf5 stages/mobile.json +6ae56077cfc368a59489233eeee6ee7fd97437821822637ac62096ab93c85286 stages/tv.json diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/dash/classic.mpd b/wai/pack-conformance/packs/video-cmaf-01/package/dash/classic.mpd new file mode 100644 index 000000000..737f43910 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/dash/classic.mpd @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/dash/mobile.mpd b/wai/pack-conformance/packs/video-cmaf-01/package/dash/mobile.mpd new file mode 100644 index 000000000..d3340daa2 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/dash/mobile.mpd @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/dash/tv.mpd b/wai/pack-conformance/packs/video-cmaf-01/package/dash/tv.mpd new file mode 100644 index 000000000..f0817bb2c --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/dash/tv.mpd @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor.listing b/wai/pack-conformance/packs/video-cmaf-01/package/floor.listing new file mode 100644 index 000000000..1e4f809b4 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/floor.listing @@ -0,0 +1,12 @@ +a8a19ef9cd4d540037ef298097bbc5317de6a99690c8c4a745cc867c034208d6 hi/init.mp4 +14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889 hi/seg_0000.m4s +e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e hi/seg_0001.m4s +d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d hi/seg_0002.m4s +eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8 hi/seg_0003.m4s +759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1 hi/seg_0004.m4s +6dd501e27365ce9ef90576771aad73de4d200362e6974551847afc3c90604344 lo/init.mp4 +0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af lo/seg_0000.m4s +c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc lo/seg_0001.m4s +10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8 lo/seg_0002.m4s +a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8 lo/seg_0003.m4s +2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73 lo/seg_0004.m4s diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s b/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s new file mode 100644 index 000000000..3d6538a6a Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s b/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s new file mode 100644 index 000000000..4a01d9311 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s b/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s new file mode 100644 index 000000000..912518601 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s b/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s new file mode 100644 index 000000000..738585244 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s b/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s new file mode 100644 index 000000000..ac02ab23e Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/init.mp4 b/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/init.mp4 new file mode 100644 index 000000000..af7fc6d9d Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/floor/hi/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s b/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s new file mode 100644 index 000000000..f707dcd29 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s b/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s new file mode 100644 index 000000000..537675bb8 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s b/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s new file mode 100644 index 000000000..194871127 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s b/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s new file mode 100644 index 000000000..90c0b6fca Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s b/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s new file mode 100644 index 000000000..74171d12c Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/init.mp4 b/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/init.mp4 new file mode 100644 index 000000000..2f94c3ac7 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/floor/lo/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/hls/classic/floor-hi.m3u8 b/wai/pack-conformance/packs/video-cmaf-01/package/hls/classic/floor-hi.m3u8 new file mode 100644 index 000000000..36c356114 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/hls/classic/floor-hi.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/hi/init.mp4" +#EXTINF:2.400, +../../floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s +#EXTINF:1.000, +../../floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s +#EXTINF:2.000, +../../floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s +#EXTINF:1.000, +../../floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s +#EXTINF:1.600, +../../floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/hls/classic/floor-lo.m3u8 b/wai/pack-conformance/packs/video-cmaf-01/package/hls/classic/floor-lo.m3u8 new file mode 100644 index 000000000..a9435fdc4 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/hls/classic/floor-lo.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/lo/init.mp4" +#EXTINF:2.400, +../../floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s +#EXTINF:1.000, +../../floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s +#EXTINF:2.000, +../../floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s +#EXTINF:1.000, +../../floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s +#EXTINF:1.600, +../../floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/hls/classic/master.m3u8 b/wai/pack-conformance/packs/video-cmaf-01/package/hls/classic/master.m3u8 new file mode 100644 index 000000000..f03258949 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/hls/classic/master.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=9300,AVERAGE-BANDWIDTH=7915,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=8152,AVERAGE-BANDWIDTH=7583,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/hls/legacy/master.m3u8 b/wai/pack-conformance/packs/video-cmaf-01/package/hls/legacy/master.m3u8 new file mode 100644 index 000000000..f03258949 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/hls/legacy/master.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=9300,AVERAGE-BANDWIDTH=7915,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=8152,AVERAGE-BANDWIDTH=7583,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/hls/mobile/master.m3u8 b/wai/pack-conformance/packs/video-cmaf-01/package/hls/mobile/master.m3u8 new file mode 100644 index 000000000..11e2b8a56 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/hls/mobile/master.m3u8 @@ -0,0 +1,8 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=9300,AVERAGE-BANDWIDTH=7915,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=8152,AVERAGE-BANDWIDTH=7583,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/hls/mobile/wai-v.m3u8 b/wai/pack-conformance/packs/video-cmaf-01/package/hls/mobile/wai-v.m3u8 new file mode 100644 index 000000000..949e945a2 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/hls/mobile/wai-v.m3u8 @@ -0,0 +1,20 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-GAP +#EXTINF:2.400, +../../objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai#n=39826 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXT-X-GAP +#EXTINF:2.000, +../../objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai#n=35882 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai#n=25514 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/hls/mobile/wai.m3u8 b/wai/pack-conformance/packs/video-cmaf-01/package/hls/mobile/wai.m3u8 new file mode 100644 index 000000000..66fa8dc50 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/hls/mobile/wai.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="mobile" +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/mobile.json" +#EXT-X-STREAM-INF:BANDWIDTH=79600,AVERAGE-BANDWIDTH=79600,CODECS="wai1.video.int_motion",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/hls/tv/master.m3u8 b/wai/pack-conformance/packs/video-cmaf-01/package/hls/tv/master.m3u8 new file mode 100644 index 000000000..11e2b8a56 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/hls/tv/master.m3u8 @@ -0,0 +1,8 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=9300,AVERAGE-BANDWIDTH=7915,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=8152,AVERAGE-BANDWIDTH=7583,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/hls/tv/wai-v.m3u8 b/wai/pack-conformance/packs/video-cmaf-01/package/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..8d1c2efe8 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/hls/tv/wai-v.m3u8 @@ -0,0 +1,17 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:2.400, +../../objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai#n=39826 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXTINF:2.000, +../../objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai#n=35882 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXTINF:1.600, +../../objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai#n=25514 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/hls/tv/wai.m3u8 b/wai/pack-conformance/packs/video-cmaf-01/package/hls/tv/wai.m3u8 new file mode 100644 index 000000000..47679d0e9 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/hls/tv/wai.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="tv" +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/tv.json" +#EXT-X-STREAM-INF:BANDWIDTH=143528,AVERAGE-BANDWIDTH=121122,CODECS="wai1.video.int_motion",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/index-v.wsix b/wai/pack-conformance/packs/video-cmaf-01/package/index-v.wsix new file mode 100644 index 000000000..9ba6b999a Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/index-v.wsix differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/moq/catalog.json b/wai/pack-conformance/packs/video-cmaf-01/package/moq/catalog.json new file mode 100644 index 000000000..a1f4775a6 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/moq/catalog.json @@ -0,0 +1 @@ +{"tracks":[{"altGroup":1,"isLive":false,"name":"v/l0","namespace":"video-cmaf-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_motion"],"wai_determinism":"decode-equivalence","wai_receipt_alias":2923025889890445,"wai_receipts":true,"wai_receipts_track":"v/l0/receipts","wai_staged":{"layer":0,"lead_ms":null,"set":"v"}},{"altGroup":1,"codec":"av01.0.00M.08","height":36,"name":"hi/floor","namespace":"video-cmaf-01","packaging":"cmaf","renderGroup":1,"width":64},{"altGroup":1,"codec":"av01.0.00M.08","height":18,"name":"lo/floor","namespace":"video-cmaf-01","packaging":"cmaf","renderGroup":1,"width":32},{"isLive":false,"name":"v/l0/receipts","namespace":"video-cmaf-01","packaging":"wai-receipts"}],"version":1} diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/objects/08797901ff55d0bb4afba5cffe95df64b7e216f3a0c9ba82006133bb9b6d7f16.wai b/wai/pack-conformance/packs/video-cmaf-01/package/objects/08797901ff55d0bb4afba5cffe95df64b7e216f3a0c9ba82006133bb9b6d7f16.wai new file mode 100644 index 000000000..65f62269a Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/objects/08797901ff55d0bb4afba5cffe95df64b7e216f3a0c9ba82006133bb9b6d7f16.wai differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai b/wai/pack-conformance/packs/video-cmaf-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai new file mode 100644 index 000000000..b15b53108 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/objects/399048a078b0f2983149266a44eaf207cfa63f490212b69389fba4d72f62c60f.wai b/wai/pack-conformance/packs/video-cmaf-01/package/objects/399048a078b0f2983149266a44eaf207cfa63f490212b69389fba4d72f62c60f.wai new file mode 100644 index 000000000..0f7f6cf48 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/objects/399048a078b0f2983149266a44eaf207cfa63f490212b69389fba4d72f62c60f.wai differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai b/wai/pack-conformance/packs/video-cmaf-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai new file mode 100644 index 000000000..04de3f188 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai b/wai/pack-conformance/packs/video-cmaf-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai new file mode 100644 index 000000000..622209937 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/objects/a236e9070d10fd09342cafdbf6973edd35e5266cf850fe2f38f77536ac803912.wai b/wai/pack-conformance/packs/video-cmaf-01/package/objects/a236e9070d10fd09342cafdbf6973edd35e5266cf850fe2f38f77536ac803912.wai new file mode 100644 index 000000000..cb34695f2 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/objects/a236e9070d10fd09342cafdbf6973edd35e5266cf850fe2f38f77536ac803912.wai differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/objects/a3ae43406f9966487b6c5f5a4d56e9dca50c327423fcfd2a98f4c342d1dbfaf9.wai b/wai/pack-conformance/packs/video-cmaf-01/package/objects/a3ae43406f9966487b6c5f5a4d56e9dca50c327423fcfd2a98f4c342d1dbfaf9.wai new file mode 100644 index 000000000..47c02f7c0 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/objects/a3ae43406f9966487b6c5f5a4d56e9dca50c327423fcfd2a98f4c342d1dbfaf9.wai differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai b/wai/pack-conformance/packs/video-cmaf-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai new file mode 100644 index 000000000..2ab2cbd49 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/objects/c5253efe2949669bb9df01d9f60c01f2570ba4be1c0af26105e092dfccf90d30.wai b/wai/pack-conformance/packs/video-cmaf-01/package/objects/c5253efe2949669bb9df01d9f60c01f2570ba4be1c0af26105e092dfccf90d30.wai new file mode 100644 index 000000000..f09447b3f Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/objects/c5253efe2949669bb9df01d9f60c01f2570ba4be1c0af26105e092dfccf90d30.wai differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/objects/c81171fa53998cfe94207ad66ab9b53cfbd4b3f058730e62512ca9f0f6ef2ea3.wai b/wai/pack-conformance/packs/video-cmaf-01/package/objects/c81171fa53998cfe94207ad66ab9b53cfbd4b3f058730e62512ca9f0f6ef2ea3.wai new file mode 100644 index 000000000..9ffc1a419 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/objects/c81171fa53998cfe94207ad66ab9b53cfbd4b3f058730e62512ca9f0f6ef2ea3.wai differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/objects/dea0f6415f639fca99d91a0341aba164078a1bd4780e21d72cbf2234928272ec.wai b/wai/pack-conformance/packs/video-cmaf-01/package/objects/dea0f6415f639fca99d91a0341aba164078a1bd4780e21d72cbf2234928272ec.wai new file mode 100644 index 000000000..876778aeb Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/objects/dea0f6415f639fca99d91a0341aba164078a1bd4780e21d72cbf2234928272ec.wai differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/objects/f467872fe8d3cd1f60f832c997cc14fd00abbf601c61286a612089b4a53a195a.wai b/wai/pack-conformance/packs/video-cmaf-01/package/objects/f467872fe8d3cd1f60f832c997cc14fd00abbf601c61286a612089b4a53a195a.wai new file mode 100644 index 000000000..37b7e5f13 Binary files /dev/null and b/wai/pack-conformance/packs/video-cmaf-01/package/objects/f467872fe8d3cd1f60f832c997cc14fd00abbf601c61286a612089b4a53a195a.wai differ diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/package.receipt.json b/wai/pack-conformance/packs/video-cmaf-01/package/package.receipt.json new file mode 100644 index 000000000..a3c5c253b --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-cmaf-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"826e8554322dd535930f822c863f9fd51b609a82006f4ae14cfcdb6b97c4d0d2","media_class":"video","stage_root":"e31fe8e71bd0afaaa9a6a9294203d9a57d9a2925d9bf761ca7acd22e047ae34e"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e12e58d39aa275ce422973435ff58aa70c4a7f1dca1958aee507fad458743adb","receipt_hash":"9e8eb989cbf451c8c17f4cce653a385cf80ab38ed10cf0e29d6e14bba51e760f","sig":"690967e5c99530f332714137e515551c9ce9deada03ed91a13b08149967918429455b04dd15e4c90b015c88eb1a0754f916de57f049094af409cd2439a6a180f","signer_id":"did:key:z6MkfikoLLY6JqXALCaUNmhSCdjtb8Nc3JQU4CioucVJcWS4","signer_pubkey":"12d3e959dc5e44fa2706d7eaf13f0a08af19209accae28933c2e8c512bfb3a89","stored_bytes":127966,"total_bytes":140530,"total_entries":15,"unique_objects":12,"version":2,"work_units":15} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/plan-v.txt b/wai/pack-conformance/packs/video-cmaf-01/package/plan-v.txt new file mode 100644 index 000000000..c7196c34c --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/plan-v.txt @@ -0,0 +1,5 @@ +0 +60 +85 +135 +160 diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/policy.json b/wai/pack-conformance/packs/video-cmaf-01/package/policy.json new file mode 100644 index 000000000..65f7012a8 --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-cmaf-01","class":[{"floor_only":false,"id":"tv","max_layer":0},{"floor_only":false,"id":"mobile","max_kbps":100,"max_layer":0},{"floor_only":true,"id":"legacy","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"video","source":"video-base-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_form":"cmaf","supplied_listing":"03ab06e026880875581952f91232405077be9a522dbf739b6a9d093458369710"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":30000,"mv_precision":1,"q_candidates":[16,12,8],"search":4},"role":"base"},{"capability":"wai.video.av1","component":"v","id":"hi","params":{"source":"supplied"},"role":"floor"},{"capability":"wai.video.av1","component":"v","id":"lo","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":true,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/policy.toml b/wai/pack-conformance/packs/video-cmaf-01/package/policy.toml new file mode 100644 index 000000000..3d37355bd --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/policy.toml @@ -0,0 +1,55 @@ +schema = "wai.pack.policy/1" +asset_id = "video-cmaf-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "hi" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[[ladder]] +id = "lo" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_form = "cmaf" +supplied_listing = "03ab06e026880875581952f91232405077be9a522dbf739b6a9d093458369710" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/stages/legacy.json b/wai/pack-conformance/packs/video-cmaf-01/package/stages/legacy.json new file mode 100644 index 000000000..0a7faedee --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"e12e58d39aa275ce422973435ff58aa70c4a7f1dca1958aee507fad458743adb"} diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/stages/mobile.json b/wai/pack-conformance/packs/video-cmaf-01/package/stages/mobile.json new file mode 100644 index 000000000..d1e9b65fe --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"e12e58d39aa275ce422973435ff58aa70c4a7f1dca1958aee507fad458743adb"} diff --git a/wai/pack-conformance/packs/video-cmaf-01/package/stages/tv.json b/wai/pack-conformance/packs/video-cmaf-01/package/stages/tv.json new file mode 100644 index 000000000..dfd23b59c --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/package/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"p":2,"stages":[{"bytes":35882,"h":"c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2","layer":0}],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"p":4,"stages":[{"bytes":25514,"h":"618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481","layer":0}],"start":160}]}},"policy_digest":"e12e58d39aa275ce422973435ff58aa70c4a7f1dca1958aee507fad458743adb"} diff --git a/wai/pack-conformance/packs/video-cmaf-01/policy.toml b/wai/pack-conformance/packs/video-cmaf-01/policy.toml new file mode 100644 index 000000000..3d37355bd --- /dev/null +++ b/wai/pack-conformance/packs/video-cmaf-01/policy.toml @@ -0,0 +1,55 @@ +schema = "wai.pack.policy/1" +asset_id = "video-cmaf-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "hi" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[[ladder]] +id = "lo" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_form = "cmaf" +supplied_listing = "03ab06e026880875581952f91232405077be9a522dbf739b6a9d093458369710" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/packs/video-floor-only-01/floor/hi/init.mp4 b/wai/pack-conformance/packs/video-floor-only-01/floor/hi/init.mp4 new file mode 100644 index 000000000..af7fc6d9d Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/floor/hi/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0000.m4s b/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0000.m4s new file mode 100644 index 000000000..3d6538a6a Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0000.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0001.m4s b/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0001.m4s new file mode 100644 index 000000000..738585244 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0001.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0002.m4s b/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0002.m4s new file mode 100644 index 000000000..912518601 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0002.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0003.m4s b/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0003.m4s new file mode 100644 index 000000000..ac02ab23e Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0003.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0004.m4s b/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0004.m4s new file mode 100644 index 000000000..4a01d9311 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/floor/hi/seg_0004.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/floor/lo/init.mp4 b/wai/pack-conformance/packs/video-floor-only-01/floor/lo/init.mp4 new file mode 100644 index 000000000..2f94c3ac7 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/floor/lo/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0000.m4s b/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0000.m4s new file mode 100644 index 000000000..f707dcd29 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0000.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0001.m4s b/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0001.m4s new file mode 100644 index 000000000..74171d12c Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0001.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0002.m4s b/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0002.m4s new file mode 100644 index 000000000..537675bb8 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0002.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0003.m4s b/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0003.m4s new file mode 100644 index 000000000..90c0b6fca Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0003.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0004.m4s b/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0004.m4s new file mode 100644 index 000000000..194871127 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/floor/lo/seg_0004.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/key.hex b/wai/pack-conformance/packs/video-floor-only-01/key.hex new file mode 100644 index 000000000..d5721a934 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/key.hex @@ -0,0 +1 @@ +9775a49bc0d2b258d9ef766516894be1dda63647f1a68a264d1c63d30aa3b4a1 diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/LISTING b/wai/pack-conformance/packs/video-floor-only-01/package/LISTING new file mode 100644 index 000000000..7af1ca5b9 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/LISTING @@ -0,0 +1,33 @@ +688cb303ccb16fcdd21c8cb681f936ba813829e71fbf15df0ccb28b04c134447 dash/classic.mpd +03ab06e026880875581952f91232405077be9a522dbf739b6a9d093458369710 floor.listing +14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889 floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s +759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1 floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s +d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s +e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s +eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8 floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s +a8a19ef9cd4d540037ef298097bbc5317de6a99690c8c4a745cc867c034208d6 floor/hi/init.mp4 +0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s +10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8 floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s +2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73 floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s +a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8 floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s +c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s +6dd501e27365ce9ef90576771aad73de4d200362e6974551847afc3c90604344 floor/lo/init.mp4 +d793737b69231eface4951001e65b3c52cba864f231246a41e64d49c77ebdc97 hls/all/master.m3u8 +ebdc7e40aecbe8b61124ac78f233b0dfaf2a8c5674d3fc9bb023935de1d9b6cb hls/classic/floor-hi.m3u8 +66f5eb8b89b80e7da839b23f39665eb85480c6a725fa29d93556efee673c1b9e hls/classic/floor-lo.m3u8 +d793737b69231eface4951001e65b3c52cba864f231246a41e64d49c77ebdc97 hls/classic/master.m3u8 +a7c6facb1be1ed1ebd49547e3860e999064c601c591a54035bd0ec91ca9c9865 index-v.wsix +ad5de743a1febe108739ddc6bee8d3ffdbe70c59a655192f2a55493e0ee0a213 moq/catalog.json +238862ef3eff0ee32abe02d8f470f514bb447a51a1f328d496a2cc552e6fe486 objects/1ca09f3aa9346effeebfc1bd101de2c311a3cd586379d1dea5c235d1b47fde60.wai +74bc950940597f0b96e9b5a87e719171df0b7743d448b0bb9aba17e556a77178 objects/420b3bb3485cfea7ad941b0cf9b2bce45815d3ee881f238b48fae8140e9d88e6.wai +8bc999fab324b1c90dc03387d1687ce89cc5f6f64fb818d528b3b6a06dc3d771 objects/6528538927505ec6ed34d97843fa3a5eaa7dde3aa6b4c3ccfdcb23e710f15d28.wai +8e1749bd5a12a09e05eab464e0d4f03a8c4ba9a841acb9719e56d287e928f347 objects/808567682e2687d0f431e2d2dead4f574212ee1e6addaa99a73ab22258de73fa.wai +8fea2404d5d7e251328fc7b725bf067031f7712de54bfba24d51376ff67cbc08 objects/95f784de520ecef9127c1758264b16b69a0fc1e375bea6515cabc4bcf71a29ad.wai +3e9d5d3f281f366a2056ce2ddac7c75a06bf378a29024b842953247804da9c05 objects/afa7e0ae4f4c7545824836249064aa67d530dc54ae2bad562dc3235f7ebffc9a.wai +82a7b68f2613b043a11b16431713d03ebf59333c5edc20e1ed6631bbb80c714a objects/bac29f2cf9f5bbc85f3095d0eb0b081b8fad0552739024df229481e73d4cd341.wai +ba45cdd651843f80f2a5f8a3b71bfbd64621b4680edee0100136cb22d8303eca objects/cf65056c087382fe4be2f6cb56d64669760e1f7358553731de7ba9dafac01796.wai +07805c170c6b9dcd37afb2c41a45a8e95c1ad645eba6464858d6c898107cde2f package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +747e62660a0bd9b39548798b731f46f488e76e9911bbe9b742b3dc3a3b319094 policy.json +2ec22fbee9ce044ccabc1cd03053cb3717330bd471475e395974dde0da8f7eee policy.toml +41f6cb903dd1a4fa14ae95f8286ad3877d9c40ce09c1df0915fcdd25516a4c24 stages/all.json diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/dash/classic.mpd b/wai/pack-conformance/packs/video-floor-only-01/package/dash/classic.mpd new file mode 100644 index 000000000..737f43910 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/dash/classic.mpd @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor.listing b/wai/pack-conformance/packs/video-floor-only-01/package/floor.listing new file mode 100644 index 000000000..1e4f809b4 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/floor.listing @@ -0,0 +1,12 @@ +a8a19ef9cd4d540037ef298097bbc5317de6a99690c8c4a745cc867c034208d6 hi/init.mp4 +14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889 hi/seg_0000.m4s +e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e hi/seg_0001.m4s +d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d hi/seg_0002.m4s +eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8 hi/seg_0003.m4s +759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1 hi/seg_0004.m4s +6dd501e27365ce9ef90576771aad73de4d200362e6974551847afc3c90604344 lo/init.mp4 +0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af lo/seg_0000.m4s +c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc lo/seg_0001.m4s +10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8 lo/seg_0002.m4s +a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8 lo/seg_0003.m4s +2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73 lo/seg_0004.m4s diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s b/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s new file mode 100644 index 000000000..3d6538a6a Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s b/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s new file mode 100644 index 000000000..4a01d9311 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s b/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s new file mode 100644 index 000000000..912518601 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s b/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s new file mode 100644 index 000000000..738585244 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s b/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s new file mode 100644 index 000000000..ac02ab23e Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/init.mp4 b/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/init.mp4 new file mode 100644 index 000000000..af7fc6d9d Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/floor/hi/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s b/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s new file mode 100644 index 000000000..f707dcd29 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s b/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s new file mode 100644 index 000000000..537675bb8 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s b/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s new file mode 100644 index 000000000..194871127 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s b/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s new file mode 100644 index 000000000..90c0b6fca Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s b/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s new file mode 100644 index 000000000..74171d12c Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/init.mp4 b/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/init.mp4 new file mode 100644 index 000000000..2f94c3ac7 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/floor/lo/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/hls/all/master.m3u8 b/wai/pack-conformance/packs/video-floor-only-01/package/hls/all/master.m3u8 new file mode 100644 index 000000000..f03258949 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/hls/all/master.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=9300,AVERAGE-BANDWIDTH=7915,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=8152,AVERAGE-BANDWIDTH=7583,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/hls/classic/floor-hi.m3u8 b/wai/pack-conformance/packs/video-floor-only-01/package/hls/classic/floor-hi.m3u8 new file mode 100644 index 000000000..36c356114 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/hls/classic/floor-hi.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/hi/init.mp4" +#EXTINF:2.400, +../../floor/hi/14cdbe56c8f57b1427ab9c2a9ebf6669ce8edec06fd7cd0961a01d2aaf720889.m4s +#EXTINF:1.000, +../../floor/hi/e5583297b1456e46366fe3317784fadf8c165c5e16e864629b194dc46290387e.m4s +#EXTINF:2.000, +../../floor/hi/d3616b6368fc800ce421a492fbd0f6263b9e06ee0af1e8269ce4f20d5cb3653d.m4s +#EXTINF:1.000, +../../floor/hi/eb559df9571de1869981ca82672abbabad528b294b15a1d7bf9b635761c584f8.m4s +#EXTINF:1.600, +../../floor/hi/759f476ce9e9b003d7d797060f9c039141d261dd65212035e96bb157783423c1.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/hls/classic/floor-lo.m3u8 b/wai/pack-conformance/packs/video-floor-only-01/package/hls/classic/floor-lo.m3u8 new file mode 100644 index 000000000..a9435fdc4 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/hls/classic/floor-lo.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/lo/init.mp4" +#EXTINF:2.400, +../../floor/lo/0e0407f5a552e405846a411627df17537e5c31accca37390c391a7c1d27353af.m4s +#EXTINF:1.000, +../../floor/lo/c29687008dafdf251b2ca962c3e830f785b38a56c81aee1f4c86f2e8f9115fcc.m4s +#EXTINF:2.000, +../../floor/lo/10b19654062fa37b83c082c922f54529725c36a4ead953b1fbfbbee14d4e79a8.m4s +#EXTINF:1.000, +../../floor/lo/a855b2701af4e7c8c912448be9fc023f911b4e9e94b65770393c4df6d705b5e8.m4s +#EXTINF:1.600, +../../floor/lo/2c484c9bead2b261ac1a18c2aedec11761813f995d11dc9779c5f9b039d59b73.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/hls/classic/master.m3u8 b/wai/pack-conformance/packs/video-floor-only-01/package/hls/classic/master.m3u8 new file mode 100644 index 000000000..f03258949 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/hls/classic/master.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=9300,AVERAGE-BANDWIDTH=7915,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=8152,AVERAGE-BANDWIDTH=7583,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/index-v.wsix b/wai/pack-conformance/packs/video-floor-only-01/package/index-v.wsix new file mode 100644 index 000000000..3d140aa0f Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/index-v.wsix differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/moq/catalog.json b/wai/pack-conformance/packs/video-floor-only-01/package/moq/catalog.json new file mode 100644 index 000000000..308f10183 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/moq/catalog.json @@ -0,0 +1 @@ +{"tracks":[{"altGroup":1,"codec":"av01.0.00M.08","height":36,"name":"hi/floor","namespace":"video-floor-only-01","packaging":"cmaf","renderGroup":1,"width":64},{"altGroup":1,"codec":"av01.0.00M.08","height":18,"name":"lo/floor","namespace":"video-floor-only-01","packaging":"cmaf","renderGroup":1,"width":32}],"version":1} diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/objects/1ca09f3aa9346effeebfc1bd101de2c311a3cd586379d1dea5c235d1b47fde60.wai b/wai/pack-conformance/packs/video-floor-only-01/package/objects/1ca09f3aa9346effeebfc1bd101de2c311a3cd586379d1dea5c235d1b47fde60.wai new file mode 100644 index 000000000..a00277606 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/objects/1ca09f3aa9346effeebfc1bd101de2c311a3cd586379d1dea5c235d1b47fde60.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/objects/420b3bb3485cfea7ad941b0cf9b2bce45815d3ee881f238b48fae8140e9d88e6.wai b/wai/pack-conformance/packs/video-floor-only-01/package/objects/420b3bb3485cfea7ad941b0cf9b2bce45815d3ee881f238b48fae8140e9d88e6.wai new file mode 100644 index 000000000..7fb2a496d Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/objects/420b3bb3485cfea7ad941b0cf9b2bce45815d3ee881f238b48fae8140e9d88e6.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/objects/6528538927505ec6ed34d97843fa3a5eaa7dde3aa6b4c3ccfdcb23e710f15d28.wai b/wai/pack-conformance/packs/video-floor-only-01/package/objects/6528538927505ec6ed34d97843fa3a5eaa7dde3aa6b4c3ccfdcb23e710f15d28.wai new file mode 100644 index 000000000..972d11079 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/objects/6528538927505ec6ed34d97843fa3a5eaa7dde3aa6b4c3ccfdcb23e710f15d28.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/objects/808567682e2687d0f431e2d2dead4f574212ee1e6addaa99a73ab22258de73fa.wai b/wai/pack-conformance/packs/video-floor-only-01/package/objects/808567682e2687d0f431e2d2dead4f574212ee1e6addaa99a73ab22258de73fa.wai new file mode 100644 index 000000000..c05dd7337 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/objects/808567682e2687d0f431e2d2dead4f574212ee1e6addaa99a73ab22258de73fa.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/objects/95f784de520ecef9127c1758264b16b69a0fc1e375bea6515cabc4bcf71a29ad.wai b/wai/pack-conformance/packs/video-floor-only-01/package/objects/95f784de520ecef9127c1758264b16b69a0fc1e375bea6515cabc4bcf71a29ad.wai new file mode 100644 index 000000000..bb025eae3 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/objects/95f784de520ecef9127c1758264b16b69a0fc1e375bea6515cabc4bcf71a29ad.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/objects/afa7e0ae4f4c7545824836249064aa67d530dc54ae2bad562dc3235f7ebffc9a.wai b/wai/pack-conformance/packs/video-floor-only-01/package/objects/afa7e0ae4f4c7545824836249064aa67d530dc54ae2bad562dc3235f7ebffc9a.wai new file mode 100644 index 000000000..97d6c588b Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/objects/afa7e0ae4f4c7545824836249064aa67d530dc54ae2bad562dc3235f7ebffc9a.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/objects/bac29f2cf9f5bbc85f3095d0eb0b081b8fad0552739024df229481e73d4cd341.wai b/wai/pack-conformance/packs/video-floor-only-01/package/objects/bac29f2cf9f5bbc85f3095d0eb0b081b8fad0552739024df229481e73d4cd341.wai new file mode 100644 index 000000000..2bc6503c3 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/objects/bac29f2cf9f5bbc85f3095d0eb0b081b8fad0552739024df229481e73d4cd341.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/objects/cf65056c087382fe4be2f6cb56d64669760e1f7358553731de7ba9dafac01796.wai b/wai/pack-conformance/packs/video-floor-only-01/package/objects/cf65056c087382fe4be2f6cb56d64669760e1f7358553731de7ba9dafac01796.wai new file mode 100644 index 000000000..c62a53095 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-01/package/objects/cf65056c087382fe4be2f6cb56d64669760e1f7358553731de7ba9dafac01796.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/package.receipt.json b/wai/pack-conformance/packs/video-floor-only-01/package/package.receipt.json new file mode 100644 index 000000000..b31fc9434 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-floor-only-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"e7a60288dcb980ca06ad26bafd58755bc73d2e84be2e99d31a69b675089a75e1","media_class":"video","stage_root":"3984709e32c1a5e1d847f2c59da2c86a112d27bca99603bc159c06e57cf2d047"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"2bcc39852d4c611b552492d4e3425b29a5bc391bd12108ca500ed4f320132816","receipt_hash":"1926a837c30b576aadbb90e9dfc4701c49fb441a2a5388399e30c7182de4ed09","sig":"eecc7dae12ab44fd6df8c4b1f69f584cb364964319035d80b98be867b80d5578d48ba6a725cfefdd146b880bc3ac2e9a77fb6ee5a6c676a89127fd5be390d801","signer_id":"did:key:z6MknnKRQcvJ7mmQreTZsgohhB6GaP54fkakkowN8sAy91Vg","signer_pubkey":"7bbf592448d40b9cb054752f88f0040e777a5a5d6ae30aec17d2edf69b22e8df","stored_bytes":14402,"total_bytes":16418,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/plan-v.txt b/wai/pack-conformance/packs/video-floor-only-01/package/plan-v.txt new file mode 100644 index 000000000..c7196c34c --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/plan-v.txt @@ -0,0 +1,5 @@ +0 +60 +85 +135 +160 diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/policy.json b/wai/pack-conformance/packs/video-floor-only-01/package/policy.json new file mode 100644 index 000000000..adb4784b9 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-floor-only-01","class":[{"floor_only":true,"id":"all","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"video","profile":"floor-only","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_form":"cmaf","supplied_listing":"03ab06e026880875581952f91232405077be9a522dbf739b6a9d093458369710"},"ladder":[{"capability":"wai.video.av1","component":"v","id":"hi","params":{"source":"supplied"},"role":"floor"},{"capability":"wai.video.av1","component":"v","id":"lo","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":true,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/policy.toml b/wai/pack-conformance/packs/video-floor-only-01/package/policy.toml new file mode 100644 index 000000000..247b1bda9 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/policy.toml @@ -0,0 +1,38 @@ +schema = "wai.pack.policy/1" +asset_id = "video-floor-only-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +profile = "floor-only" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +max_ms = 4000 + +[[ladder]] +id = "hi" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[[ladder]] +id = "lo" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "03ab06e026880875581952f91232405077be9a522dbf739b6a9d093458369710" +supplied_form = "cmaf" + +[[class]] +id = "all" +floor_only = true diff --git a/wai/pack-conformance/packs/video-floor-only-01/package/stages/all.json b/wai/pack-conformance/packs/video-floor-only-01/package/stages/all.json new file mode 100644 index 000000000..c338395a2 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/package/stages/all.json @@ -0,0 +1 @@ +{"class":"all","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"2bcc39852d4c611b552492d4e3425b29a5bc391bd12108ca500ed4f320132816"} diff --git a/wai/pack-conformance/packs/video-floor-only-01/policy.toml b/wai/pack-conformance/packs/video-floor-only-01/policy.toml new file mode 100644 index 000000000..247b1bda9 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-01/policy.toml @@ -0,0 +1,38 @@ +schema = "wai.pack.policy/1" +asset_id = "video-floor-only-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +profile = "floor-only" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +max_ms = 4000 + +[[ladder]] +id = "hi" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[[ladder]] +id = "lo" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "03ab06e026880875581952f91232405077be9a522dbf739b6a9d093458369710" +supplied_form = "cmaf" + +[[class]] +id = "all" +floor_only = true diff --git a/wai/pack-conformance/packs/video-floor-only-02/floor/hi.obu b/wai/pack-conformance/packs/video-floor-only-02/floor/hi.obu new file mode 100644 index 000000000..a75994f85 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/floor/hi.obu differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/floor/lo.obu b/wai/pack-conformance/packs/video-floor-only-02/floor/lo.obu new file mode 100644 index 000000000..6bdb49216 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/floor/lo.obu differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/key.hex b/wai/pack-conformance/packs/video-floor-only-02/key.hex new file mode 100644 index 000000000..cb349a425 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/key.hex @@ -0,0 +1 @@ +969470be8f2e8fbb31f18a2e3ede613cc2817cbe2fa7a7f8ccb3bd60826044ac diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/LISTING b/wai/pack-conformance/packs/video-floor-only-02/package/LISTING new file mode 100644 index 000000000..37a7b0010 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/package/LISTING @@ -0,0 +1,32 @@ +c5203585fea0962de941d258789157a0590e18b62a2e5d1facbd11e1cfd1ddb9 dash/classic.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/hi/init.mp4 +589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s +6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s +acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74 floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s +d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833 floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s +d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9 floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s +640cd81c83baefbe13e8ccc2bf577395c6602885cc73e201ca34dc267e7f0a48 floor/lo/init.mp4 +3fd88f9dcf835edf0fcca8dff8371d2b4e897ac097fc85901477c950c0da7996 hls/all/master.m3u8 +bfa98255d682517f58df4073bc71fdb3fcca120fc33d16e4aca873392476c586 hls/classic/floor-hi.m3u8 +ad81674ac359096ebed255a26a40e82858e53e9754702429e59d72898df33812 hls/classic/floor-lo.m3u8 +3fd88f9dcf835edf0fcca8dff8371d2b4e897ac097fc85901477c950c0da7996 hls/classic/master.m3u8 +cfa7d9a1a9c8a22d9ac1e22e4874b8a23e6ac5505077faf89dd70e65d3b4145a index-v.wsix +ddec2072b2b70d5df4c6295f7e0b3f7d9b649a3d524a341f8fd6f98a2ea0db89 moq/catalog.json +238862ef3eff0ee32abe02d8f470f514bb447a51a1f328d496a2cc552e6fe486 objects/1ca09f3aa9346effeebfc1bd101de2c311a3cd586379d1dea5c235d1b47fde60.wai +74bc950940597f0b96e9b5a87e719171df0b7743d448b0bb9aba17e556a77178 objects/420b3bb3485cfea7ad941b0cf9b2bce45815d3ee881f238b48fae8140e9d88e6.wai +8bc999fab324b1c90dc03387d1687ce89cc5f6f64fb818d528b3b6a06dc3d771 objects/6528538927505ec6ed34d97843fa3a5eaa7dde3aa6b4c3ccfdcb23e710f15d28.wai +8e1749bd5a12a09e05eab464e0d4f03a8c4ba9a841acb9719e56d287e928f347 objects/808567682e2687d0f431e2d2dead4f574212ee1e6addaa99a73ab22258de73fa.wai +8fea2404d5d7e251328fc7b725bf067031f7712de54bfba24d51376ff67cbc08 objects/95f784de520ecef9127c1758264b16b69a0fc1e375bea6515cabc4bcf71a29ad.wai +3e9d5d3f281f366a2056ce2ddac7c75a06bf378a29024b842953247804da9c05 objects/afa7e0ae4f4c7545824836249064aa67d530dc54ae2bad562dc3235f7ebffc9a.wai +82a7b68f2613b043a11b16431713d03ebf59333c5edc20e1ed6631bbb80c714a objects/bac29f2cf9f5bbc85f3095d0eb0b081b8fad0552739024df229481e73d4cd341.wai +ba45cdd651843f80f2a5f8a3b71bfbd64621b4680edee0100136cb22d8303eca objects/cf65056c087382fe4be2f6cb56d64669760e1f7358553731de7ba9dafac01796.wai +24c0a3b5c168705e24651303e9c7a4da733cd7bf05972dfb795c7020511021ee package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +59ac061dd4cf2b1d6bfbd90f3b928fb52d996877e1612d2e77520e55a4566ba6 policy.json +d352b112ede244612448bd216ece0553ead1c5af1542d874bc7ef5b5b546fd27 policy.toml +c421d0d94d97977ac1ec994510317d67f54cb947cec3464afd08e7dbefbc854a stages/all.json diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/dash/classic.mpd b/wai/pack-conformance/packs/video-floor-only-02/package/dash/classic.mpd new file mode 100644 index 000000000..0bc509f7c --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/package/dash/classic.mpd @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s b/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s new file mode 100644 index 000000000..7e5a29203 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s b/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s new file mode 100644 index 000000000..3e21af5bf Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s b/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s new file mode 100644 index 000000000..ec55da7e8 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s b/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s new file mode 100644 index 000000000..a246e72be Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s b/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s new file mode 100644 index 000000000..98e3a28d6 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/init.mp4 b/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/init.mp4 new file mode 100644 index 000000000..0b3ade258 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/floor/hi/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s b/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s new file mode 100644 index 000000000..78b5c8866 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s b/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s new file mode 100644 index 000000000..0cb031621 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s b/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s new file mode 100644 index 000000000..8747edc9f Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s b/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s new file mode 100644 index 000000000..6adf8fdfd Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s b/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s new file mode 100644 index 000000000..5c06ef47a Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/init.mp4 b/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/init.mp4 new file mode 100644 index 000000000..d419e7893 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/floor/lo/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/hls/all/master.m3u8 b/wai/pack-conformance/packs/video-floor-only-02/package/hls/all/master.m3u8 new file mode 100644 index 000000000..5fb4ccb7c --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/package/hls/all/master.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=9055,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9016,AVERAGE-BANDWIDTH=8723,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/hls/classic/floor-hi.m3u8 b/wai/pack-conformance/packs/video-floor-only-02/package/hls/classic/floor-hi.m3u8 new file mode 100644 index 000000000..d6ac7336d --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/package/hls/classic/floor-hi.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/hi/init.mp4" +#EXTINF:2.400, +../../floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +#EXTINF:1.000, +../../floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +#EXTINF:2.000, +../../floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +#EXTINF:1.000, +../../floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +#EXTINF:1.600, +../../floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/hls/classic/floor-lo.m3u8 b/wai/pack-conformance/packs/video-floor-only-02/package/hls/classic/floor-lo.m3u8 new file mode 100644 index 000000000..628e70c2b --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/package/hls/classic/floor-lo.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/lo/init.mp4" +#EXTINF:2.400, +../../floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s +#EXTINF:1.000, +../../floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s +#EXTINF:2.000, +../../floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s +#EXTINF:1.000, +../../floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s +#EXTINF:1.600, +../../floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/hls/classic/master.m3u8 b/wai/pack-conformance/packs/video-floor-only-02/package/hls/classic/master.m3u8 new file mode 100644 index 000000000..5fb4ccb7c --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/package/hls/classic/master.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=9055,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9016,AVERAGE-BANDWIDTH=8723,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/index-v.wsix b/wai/pack-conformance/packs/video-floor-only-02/package/index-v.wsix new file mode 100644 index 000000000..3338fc15b Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/index-v.wsix differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/moq/catalog.json b/wai/pack-conformance/packs/video-floor-only-02/package/moq/catalog.json new file mode 100644 index 000000000..90091605e --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/package/moq/catalog.json @@ -0,0 +1 @@ +{"tracks":[{"altGroup":1,"codec":"av01.0.00M.08","height":36,"name":"hi/floor","namespace":"video-floor-only-02","packaging":"cmaf","renderGroup":1,"width":64},{"altGroup":1,"codec":"av01.0.00M.08","height":18,"name":"lo/floor","namespace":"video-floor-only-02","packaging":"cmaf","renderGroup":1,"width":32}],"version":1} diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/objects/1ca09f3aa9346effeebfc1bd101de2c311a3cd586379d1dea5c235d1b47fde60.wai b/wai/pack-conformance/packs/video-floor-only-02/package/objects/1ca09f3aa9346effeebfc1bd101de2c311a3cd586379d1dea5c235d1b47fde60.wai new file mode 100644 index 000000000..a00277606 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/objects/1ca09f3aa9346effeebfc1bd101de2c311a3cd586379d1dea5c235d1b47fde60.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/objects/420b3bb3485cfea7ad941b0cf9b2bce45815d3ee881f238b48fae8140e9d88e6.wai b/wai/pack-conformance/packs/video-floor-only-02/package/objects/420b3bb3485cfea7ad941b0cf9b2bce45815d3ee881f238b48fae8140e9d88e6.wai new file mode 100644 index 000000000..7fb2a496d Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/objects/420b3bb3485cfea7ad941b0cf9b2bce45815d3ee881f238b48fae8140e9d88e6.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/objects/6528538927505ec6ed34d97843fa3a5eaa7dde3aa6b4c3ccfdcb23e710f15d28.wai b/wai/pack-conformance/packs/video-floor-only-02/package/objects/6528538927505ec6ed34d97843fa3a5eaa7dde3aa6b4c3ccfdcb23e710f15d28.wai new file mode 100644 index 000000000..972d11079 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/objects/6528538927505ec6ed34d97843fa3a5eaa7dde3aa6b4c3ccfdcb23e710f15d28.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/objects/808567682e2687d0f431e2d2dead4f574212ee1e6addaa99a73ab22258de73fa.wai b/wai/pack-conformance/packs/video-floor-only-02/package/objects/808567682e2687d0f431e2d2dead4f574212ee1e6addaa99a73ab22258de73fa.wai new file mode 100644 index 000000000..c05dd7337 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/objects/808567682e2687d0f431e2d2dead4f574212ee1e6addaa99a73ab22258de73fa.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/objects/95f784de520ecef9127c1758264b16b69a0fc1e375bea6515cabc4bcf71a29ad.wai b/wai/pack-conformance/packs/video-floor-only-02/package/objects/95f784de520ecef9127c1758264b16b69a0fc1e375bea6515cabc4bcf71a29ad.wai new file mode 100644 index 000000000..bb025eae3 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/objects/95f784de520ecef9127c1758264b16b69a0fc1e375bea6515cabc4bcf71a29ad.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/objects/afa7e0ae4f4c7545824836249064aa67d530dc54ae2bad562dc3235f7ebffc9a.wai b/wai/pack-conformance/packs/video-floor-only-02/package/objects/afa7e0ae4f4c7545824836249064aa67d530dc54ae2bad562dc3235f7ebffc9a.wai new file mode 100644 index 000000000..97d6c588b Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/objects/afa7e0ae4f4c7545824836249064aa67d530dc54ae2bad562dc3235f7ebffc9a.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/objects/bac29f2cf9f5bbc85f3095d0eb0b081b8fad0552739024df229481e73d4cd341.wai b/wai/pack-conformance/packs/video-floor-only-02/package/objects/bac29f2cf9f5bbc85f3095d0eb0b081b8fad0552739024df229481e73d4cd341.wai new file mode 100644 index 000000000..2bc6503c3 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/objects/bac29f2cf9f5bbc85f3095d0eb0b081b8fad0552739024df229481e73d4cd341.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/objects/cf65056c087382fe4be2f6cb56d64669760e1f7358553731de7ba9dafac01796.wai b/wai/pack-conformance/packs/video-floor-only-02/package/objects/cf65056c087382fe4be2f6cb56d64669760e1f7358553731de7ba9dafac01796.wai new file mode 100644 index 000000000..c62a53095 Binary files /dev/null and b/wai/pack-conformance/packs/video-floor-only-02/package/objects/cf65056c087382fe4be2f6cb56d64669760e1f7358553731de7ba9dafac01796.wai differ diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/package.receipt.json b/wai/pack-conformance/packs/video-floor-only-02/package/package.receipt.json new file mode 100644 index 000000000..38be24457 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/package/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-floor-only-02","components":[{"id":"v","index":"index-v.wsix","index_digest":"971a487af1835360c74ed399b07f35ae84f3bf8c7017df198854f5cde963f9af","media_class":"video","stage_root":"3984709e32c1a5e1d847f2c59da2c86a112d27bca99603bc159c06e57cf2d047"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"25c7cb3e040bad54d15055c6af2e2d70309d1c3e9822541ef21523214f206ab1","receipt_hash":"edc723e55698895738fe6d13975bdc45f08a5df49683af48fb928ba657400f88","sig":"590405e5a8fe7a7c465c01e409c6fa0886a28255e5a4ca99964a68de9bf9da27cd3de40004368786db60762333a3a5f357ee2dfe802ccb0cb3f0e01de3b2b800","signer_id":"did:key:z6Mkti8EeiFNrK4w1acEn3VgjifvL2twNbMRP4nNPmZQEo4y","signer_pubkey":"d3d27fc405f4080c360727eb3be104adc216cabf43a4fd52baca1116a1548ed6","stored_bytes":14402,"total_bytes":16418,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/plan-v.txt b/wai/pack-conformance/packs/video-floor-only-02/package/plan-v.txt new file mode 100644 index 000000000..c7196c34c --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/package/plan-v.txt @@ -0,0 +1,5 @@ +0 +60 +85 +135 +160 diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/policy.json b/wai/pack-conformance/packs/video-floor-only-02/package/policy.json new file mode 100644 index 000000000..b9c4ed858 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/package/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-floor-only-02","class":[{"floor_only":true,"id":"all","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"video","profile":"floor-only","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"d795df9202fb5f32fe168aa585332a5694ad7d8d7e8b859fc7d57a791f08c990"},"ladder":[{"capability":"wai.video.av1","component":"v","id":"hi","params":{"source":"supplied"},"role":"floor"},{"capability":"wai.video.av1","component":"v","id":"lo","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":true,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/policy.toml b/wai/pack-conformance/packs/video-floor-only-02/package/policy.toml new file mode 100644 index 000000000..8ba3fb538 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/package/policy.toml @@ -0,0 +1,37 @@ +schema = "wai.pack.policy/1" +asset_id = "video-floor-only-02" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +profile = "floor-only" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +max_ms = 4000 + +[[ladder]] +id = "hi" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[[ladder]] +id = "lo" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "d795df9202fb5f32fe168aa585332a5694ad7d8d7e8b859fc7d57a791f08c990" + +[[class]] +id = "all" +floor_only = true diff --git a/wai/pack-conformance/packs/video-floor-only-02/package/stages/all.json b/wai/pack-conformance/packs/video-floor-only-02/package/stages/all.json new file mode 100644 index 000000000..ca52894ab --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/package/stages/all.json @@ -0,0 +1 @@ +{"class":"all","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"25c7cb3e040bad54d15055c6af2e2d70309d1c3e9822541ef21523214f206ab1"} diff --git a/wai/pack-conformance/packs/video-floor-only-02/policy.toml b/wai/pack-conformance/packs/video-floor-only-02/policy.toml new file mode 100644 index 000000000..8ba3fb538 --- /dev/null +++ b/wai/pack-conformance/packs/video-floor-only-02/policy.toml @@ -0,0 +1,37 @@ +schema = "wai.pack.policy/1" +asset_id = "video-floor-only-02" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +profile = "floor-only" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +max_ms = 4000 + +[[ladder]] +id = "hi" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[[ladder]] +id = "lo" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "d795df9202fb5f32fe168aa585332a5694ad7d8d7e8b859fc7d57a791f08c990" + +[[class]] +id = "all" +floor_only = true diff --git a/wai/pack-conformance/packs/video-ladder-01/floor/hi.obu b/wai/pack-conformance/packs/video-ladder-01/floor/hi.obu new file mode 100644 index 000000000..a75994f85 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/floor/hi.obu differ diff --git a/wai/pack-conformance/packs/video-ladder-01/floor/lo.obu b/wai/pack-conformance/packs/video-ladder-01/floor/lo.obu new file mode 100644 index 000000000..6bdb49216 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/floor/lo.obu differ diff --git a/wai/pack-conformance/packs/video-ladder-01/key.hex b/wai/pack-conformance/packs/video-ladder-01/key.hex new file mode 100644 index 000000000..602576559 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/key.hex @@ -0,0 +1 @@ +92baad1b286358f32cf2996070c85a60ab85af6055432d4a754a4fde64fcbe09 diff --git a/wai/pack-conformance/packs/video-ladder-01/package/LISTING b/wai/pack-conformance/packs/video-ladder-01/package/LISTING new file mode 100644 index 000000000..3ac875157 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/LISTING @@ -0,0 +1,46 @@ +c5203585fea0962de941d258789157a0590e18b62a2e5d1facbd11e1cfd1ddb9 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/hi/init.mp4 +589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s +6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s +acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74 floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s +d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833 floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s +d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9 floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s +640cd81c83baefbe13e8ccc2bf577395c6602885cc73e201ca34dc267e7f0a48 floor/lo/init.mp4 +bfa98255d682517f58df4073bc71fdb3fcca120fc33d16e4aca873392476c586 hls/classic/floor-hi.m3u8 +ad81674ac359096ebed255a26a40e82858e53e9754702429e59d72898df33812 hls/classic/floor-lo.m3u8 +3fd88f9dcf835edf0fcca8dff8371d2b4e897ac097fc85901477c950c0da7996 hls/classic/master.m3u8 +3fd88f9dcf835edf0fcca8dff8371d2b4e897ac097fc85901477c950c0da7996 hls/legacy/master.m3u8 +09e75f55cb7ea829a4e630404d2ab540e8be0dbb1c4365dc30802359b658aed9 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +09e75f55cb7ea829a4e630404d2ab540e8be0dbb1c4365dc30802359b658aed9 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +a5140df0bf1f82b88335c13789502298a952b350ad45dfcfcd249b2b9a263a71 index-v.wsix +c310c5a4ae14df691a1a37a6c34e90243ed72be1e8f725d38f1babe1f6866b21 moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +2f5bd830d841eb122c63ae57a15442075478fd522ce55b6f9bcbe41acd51d8ae objects/482fbbb9b39bdb9cbf00515f94269ea591df681ced90b05d97e0df106b49b08e.wai +0057633252a2c2ed5f71fe14ca78075c8e89fc6675a08335ac90f0e56032b825 objects/4ab8f747d181632f342a4887b0a8bb86c174409cb625157784e18c5e5559a381.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +1a6f30fdc000f258c948470386d84beef2ba5a84e922c355093837ae21fd573c objects/6ce13c9ef6872caeb7fbe71661ca88bcc463ceac47f109c8c155e34bb1a6f37d.wai +b773dac63c9d2d9980d0d249ea956e03065c1ae40d17c512df29b690c0c820ef objects/b7bf0717e968d7b6be473785c9f66cbb814cde679bceb1ae37f0765308ddee72.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +cc8a6fc59be29bd33d96fb30ea9e5a30a5aa367a82da9c5784cbadce63396f62 objects/cf842ef3058d3c54d56c0ea3e075efdc77163602b4a1dabcff080e5ea28b9946.wai +01b596b672de3352adb3e01b34fcddfeff975f4ee1d0b88dde160ac10c9bf929 objects/d382ba093b0595c44a43dc3aad22fc0bed42baeb3488f00dfe3567056a7009c5.wai +9d9ae296558ce7f7d644ad0ca3af65eb4fc98d03b0bd6d7a8fc821fc952e86c1 objects/f525b4450c29153dde0b43e4d143dfb76fcc3884c607b744c8a53fa6a1728580.wai +3d573ee482f8187b0e3bc839aeea0f51a67144588d6d207e8466fea91a0c5b34 objects/f7f7f4d6af777bfcce09cdda5ecb754ce6bd8dcd2e03ecfc41359cad989b15f7.wai +bd6086051d0452449da8a90f426f454fee368b4794f7c294218a410ded8d9088 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +d1cf1bd28a06f3a9d695dd64331cb56cf793818e9e9e6cec620183d9dce37b2d policy.json +4ba48d04e5c8e72336e32307b27b4371c96f20f7abf959ac1c1d344ccb9a8106 policy.toml +813329d5521ab5891bb86d29b362e448fae4247c949384b0687d66966546c4fe stages/legacy.json +ca8e0059a23311d7690a0519f85dfb48883721c817cd8fe1cd481cabe14b0032 stages/mobile.json +8bc9c9da674b4765ead0e41d54852ac105161e9638e7ae705e3982847b069e4c stages/tv.json diff --git a/wai/pack-conformance/packs/video-ladder-01/package/dash/classic.mpd b/wai/pack-conformance/packs/video-ladder-01/package/dash/classic.mpd new file mode 100644 index 000000000..0bc509f7c --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/dash/classic.mpd @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-ladder-01/package/dash/mobile.mpd b/wai/pack-conformance/packs/video-ladder-01/package/dash/mobile.mpd new file mode 100644 index 000000000..d3340daa2 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/dash/mobile.mpd @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-ladder-01/package/dash/tv.mpd b/wai/pack-conformance/packs/video-ladder-01/package/dash/tv.mpd new file mode 100644 index 000000000..f0817bb2c --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/dash/tv.mpd @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s b/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s new file mode 100644 index 000000000..7e5a29203 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s b/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s new file mode 100644 index 000000000..3e21af5bf Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s b/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s new file mode 100644 index 000000000..ec55da7e8 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s b/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s new file mode 100644 index 000000000..a246e72be Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s b/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s new file mode 100644 index 000000000..98e3a28d6 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/init.mp4 b/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/init.mp4 new file mode 100644 index 000000000..0b3ade258 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/floor/hi/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s b/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s new file mode 100644 index 000000000..78b5c8866 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s b/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s new file mode 100644 index 000000000..0cb031621 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s b/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s new file mode 100644 index 000000000..8747edc9f Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s b/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s new file mode 100644 index 000000000..6adf8fdfd Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s b/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s new file mode 100644 index 000000000..5c06ef47a Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/init.mp4 b/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/init.mp4 new file mode 100644 index 000000000..d419e7893 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/floor/lo/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/hls/classic/floor-hi.m3u8 b/wai/pack-conformance/packs/video-ladder-01/package/hls/classic/floor-hi.m3u8 new file mode 100644 index 000000000..d6ac7336d --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/hls/classic/floor-hi.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/hi/init.mp4" +#EXTINF:2.400, +../../floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +#EXTINF:1.000, +../../floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +#EXTINF:2.000, +../../floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +#EXTINF:1.000, +../../floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +#EXTINF:1.600, +../../floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-ladder-01/package/hls/classic/floor-lo.m3u8 b/wai/pack-conformance/packs/video-ladder-01/package/hls/classic/floor-lo.m3u8 new file mode 100644 index 000000000..628e70c2b --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/hls/classic/floor-lo.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/lo/init.mp4" +#EXTINF:2.400, +../../floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s +#EXTINF:1.000, +../../floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s +#EXTINF:2.000, +../../floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s +#EXTINF:1.000, +../../floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s +#EXTINF:1.600, +../../floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-ladder-01/package/hls/classic/master.m3u8 b/wai/pack-conformance/packs/video-ladder-01/package/hls/classic/master.m3u8 new file mode 100644 index 000000000..5fb4ccb7c --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/hls/classic/master.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=9055,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9016,AVERAGE-BANDWIDTH=8723,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-ladder-01/package/hls/legacy/master.m3u8 b/wai/pack-conformance/packs/video-ladder-01/package/hls/legacy/master.m3u8 new file mode 100644 index 000000000..5fb4ccb7c --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/hls/legacy/master.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=9055,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9016,AVERAGE-BANDWIDTH=8723,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-ladder-01/package/hls/mobile/master.m3u8 b/wai/pack-conformance/packs/video-ladder-01/package/hls/mobile/master.m3u8 new file mode 100644 index 000000000..5f049c978 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/hls/mobile/master.m3u8 @@ -0,0 +1,8 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=9055,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9016,AVERAGE-BANDWIDTH=8723,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-ladder-01/package/hls/mobile/wai-v.m3u8 b/wai/pack-conformance/packs/video-ladder-01/package/hls/mobile/wai-v.m3u8 new file mode 100644 index 000000000..949e945a2 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/hls/mobile/wai-v.m3u8 @@ -0,0 +1,20 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-GAP +#EXTINF:2.400, +../../objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai#n=39826 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXT-X-GAP +#EXTINF:2.000, +../../objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai#n=35882 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai#n=25514 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-ladder-01/package/hls/mobile/wai.m3u8 b/wai/pack-conformance/packs/video-ladder-01/package/hls/mobile/wai.m3u8 new file mode 100644 index 000000000..66fa8dc50 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/hls/mobile/wai.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="mobile" +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/mobile.json" +#EXT-X-STREAM-INF:BANDWIDTH=79600,AVERAGE-BANDWIDTH=79600,CODECS="wai1.video.int_motion",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/packs/video-ladder-01/package/hls/tv/master.m3u8 b/wai/pack-conformance/packs/video-ladder-01/package/hls/tv/master.m3u8 new file mode 100644 index 000000000..5f049c978 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/hls/tv/master.m3u8 @@ -0,0 +1,8 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=9055,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9016,AVERAGE-BANDWIDTH=8723,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-ladder-01/package/hls/tv/wai-v.m3u8 b/wai/pack-conformance/packs/video-ladder-01/package/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..8d1c2efe8 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/hls/tv/wai-v.m3u8 @@ -0,0 +1,17 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:2.400, +../../objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai#n=39826 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXTINF:2.000, +../../objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai#n=35882 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXTINF:1.600, +../../objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai#n=25514 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-ladder-01/package/hls/tv/wai.m3u8 b/wai/pack-conformance/packs/video-ladder-01/package/hls/tv/wai.m3u8 new file mode 100644 index 000000000..47679d0e9 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/hls/tv/wai.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="tv" +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/tv.json" +#EXT-X-STREAM-INF:BANDWIDTH=143528,AVERAGE-BANDWIDTH=121122,CODECS="wai1.video.int_motion",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/packs/video-ladder-01/package/index-v.wsix b/wai/pack-conformance/packs/video-ladder-01/package/index-v.wsix new file mode 100644 index 000000000..9f10add88 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/index-v.wsix differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/moq/catalog.json b/wai/pack-conformance/packs/video-ladder-01/package/moq/catalog.json new file mode 100644 index 000000000..cb914abd4 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/moq/catalog.json @@ -0,0 +1 @@ +{"tracks":[{"altGroup":1,"isLive":false,"name":"v/l0","namespace":"video-ladder-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_motion"],"wai_determinism":"decode-equivalence","wai_receipt_alias":4716788767000716,"wai_receipts":true,"wai_receipts_track":"v/l0/receipts","wai_staged":{"layer":0,"lead_ms":null,"set":"v"}},{"altGroup":1,"codec":"av01.0.00M.08","height":36,"name":"hi/floor","namespace":"video-ladder-01","packaging":"cmaf","renderGroup":1,"width":64},{"altGroup":1,"codec":"av01.0.00M.08","height":18,"name":"lo/floor","namespace":"video-ladder-01","packaging":"cmaf","renderGroup":1,"width":32},{"isLive":false,"name":"v/l0/receipts","namespace":"video-ladder-01","packaging":"wai-receipts"}],"version":1} diff --git a/wai/pack-conformance/packs/video-ladder-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai b/wai/pack-conformance/packs/video-ladder-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai new file mode 100644 index 000000000..b15b53108 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/objects/482fbbb9b39bdb9cbf00515f94269ea591df681ced90b05d97e0df106b49b08e.wai b/wai/pack-conformance/packs/video-ladder-01/package/objects/482fbbb9b39bdb9cbf00515f94269ea591df681ced90b05d97e0df106b49b08e.wai new file mode 100644 index 000000000..e6688e833 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/objects/482fbbb9b39bdb9cbf00515f94269ea591df681ced90b05d97e0df106b49b08e.wai differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/objects/4ab8f747d181632f342a4887b0a8bb86c174409cb625157784e18c5e5559a381.wai b/wai/pack-conformance/packs/video-ladder-01/package/objects/4ab8f747d181632f342a4887b0a8bb86c174409cb625157784e18c5e5559a381.wai new file mode 100644 index 000000000..cf1710435 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/objects/4ab8f747d181632f342a4887b0a8bb86c174409cb625157784e18c5e5559a381.wai differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai b/wai/pack-conformance/packs/video-ladder-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai new file mode 100644 index 000000000..04de3f188 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai b/wai/pack-conformance/packs/video-ladder-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai new file mode 100644 index 000000000..622209937 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/objects/6ce13c9ef6872caeb7fbe71661ca88bcc463ceac47f109c8c155e34bb1a6f37d.wai b/wai/pack-conformance/packs/video-ladder-01/package/objects/6ce13c9ef6872caeb7fbe71661ca88bcc463ceac47f109c8c155e34bb1a6f37d.wai new file mode 100644 index 000000000..93381e7ad Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/objects/6ce13c9ef6872caeb7fbe71661ca88bcc463ceac47f109c8c155e34bb1a6f37d.wai differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/objects/b7bf0717e968d7b6be473785c9f66cbb814cde679bceb1ae37f0765308ddee72.wai b/wai/pack-conformance/packs/video-ladder-01/package/objects/b7bf0717e968d7b6be473785c9f66cbb814cde679bceb1ae37f0765308ddee72.wai new file mode 100644 index 000000000..21822eab7 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/objects/b7bf0717e968d7b6be473785c9f66cbb814cde679bceb1ae37f0765308ddee72.wai differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai b/wai/pack-conformance/packs/video-ladder-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai new file mode 100644 index 000000000..2ab2cbd49 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/objects/cf842ef3058d3c54d56c0ea3e075efdc77163602b4a1dabcff080e5ea28b9946.wai b/wai/pack-conformance/packs/video-ladder-01/package/objects/cf842ef3058d3c54d56c0ea3e075efdc77163602b4a1dabcff080e5ea28b9946.wai new file mode 100644 index 000000000..5b4b092eb Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/objects/cf842ef3058d3c54d56c0ea3e075efdc77163602b4a1dabcff080e5ea28b9946.wai differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/objects/d382ba093b0595c44a43dc3aad22fc0bed42baeb3488f00dfe3567056a7009c5.wai b/wai/pack-conformance/packs/video-ladder-01/package/objects/d382ba093b0595c44a43dc3aad22fc0bed42baeb3488f00dfe3567056a7009c5.wai new file mode 100644 index 000000000..2ce8b2d3a Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/objects/d382ba093b0595c44a43dc3aad22fc0bed42baeb3488f00dfe3567056a7009c5.wai differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/objects/f525b4450c29153dde0b43e4d143dfb76fcc3884c607b744c8a53fa6a1728580.wai b/wai/pack-conformance/packs/video-ladder-01/package/objects/f525b4450c29153dde0b43e4d143dfb76fcc3884c607b744c8a53fa6a1728580.wai new file mode 100644 index 000000000..f91d83f96 Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/objects/f525b4450c29153dde0b43e4d143dfb76fcc3884c607b744c8a53fa6a1728580.wai differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/objects/f7f7f4d6af777bfcce09cdda5ecb754ce6bd8dcd2e03ecfc41359cad989b15f7.wai b/wai/pack-conformance/packs/video-ladder-01/package/objects/f7f7f4d6af777bfcce09cdda5ecb754ce6bd8dcd2e03ecfc41359cad989b15f7.wai new file mode 100644 index 000000000..ccf6489fc Binary files /dev/null and b/wai/pack-conformance/packs/video-ladder-01/package/objects/f7f7f4d6af777bfcce09cdda5ecb754ce6bd8dcd2e03ecfc41359cad989b15f7.wai differ diff --git a/wai/pack-conformance/packs/video-ladder-01/package/package.receipt.json b/wai/pack-conformance/packs/video-ladder-01/package/package.receipt.json new file mode 100644 index 000000000..232e4656d --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-ladder-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"901134f541a31eed733c22e4e471abd9530566cb54d945b6c86843bb5bcc85cb","media_class":"video","stage_root":"56ff2237e4654a894ebefb9d0588092806b71c58ee502aa43624bba633cd4bf2"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"6938df61d1274e43680652af682498bd5c7f1505c5f51b08c770143643c6388e","receipt_hash":"8ffbff73ab3b6a07c2e9645965a0bf8537bb9023e6b478186d9d9a399e343f3f","sig":"66e04ae9335a32844b7e1f83fad3d05005d296c6e69638e026d543df0596bf5066862df13ce6c4bf80b8dab32f67a0e72811e1227821857c29f9be45faf14c0a","signer_id":"did:key:z6MkqahniHbtvRDna9jmc9dSMo88FCuKnuujLKxbWyK4682w","signer_pubkey":"a558bae731bbba29b0b2308493514f6ec8a51ab40409cce9e3afafecca787224","stored_bytes":127966,"total_bytes":140530,"total_entries":15,"unique_objects":12,"version":2,"work_units":15} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-ladder-01/package/plan-v.txt b/wai/pack-conformance/packs/video-ladder-01/package/plan-v.txt new file mode 100644 index 000000000..c7196c34c --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/plan-v.txt @@ -0,0 +1,5 @@ +0 +60 +85 +135 +160 diff --git a/wai/pack-conformance/packs/video-ladder-01/package/policy.json b/wai/pack-conformance/packs/video-ladder-01/package/policy.json new file mode 100644 index 000000000..2dbbeffda --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-ladder-01","class":[{"floor_only":false,"id":"tv","max_layer":0},{"floor_only":false,"id":"mobile","max_kbps":100,"max_layer":0},{"floor_only":true,"id":"legacy","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"video","source":"video-base-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"d795df9202fb5f32fe168aa585332a5694ad7d8d7e8b859fc7d57a791f08c990"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":30000,"mv_precision":1,"q_candidates":[16,12,8],"search":4},"role":"base"},{"capability":"wai.video.av1","component":"v","id":"hi","params":{"source":"supplied"},"role":"floor"},{"capability":"wai.video.av1","component":"v","id":"lo","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":true,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-ladder-01/package/policy.toml b/wai/pack-conformance/packs/video-ladder-01/package/policy.toml new file mode 100644 index 000000000..07e11d953 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/policy.toml @@ -0,0 +1,54 @@ +schema = "wai.pack.policy/1" +asset_id = "video-ladder-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "hi" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[[ladder]] +id = "lo" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "d795df9202fb5f32fe168aa585332a5694ad7d8d7e8b859fc7d57a791f08c990" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/packs/video-ladder-01/package/stages/legacy.json b/wai/pack-conformance/packs/video-ladder-01/package/stages/legacy.json new file mode 100644 index 000000000..f75b26874 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"6938df61d1274e43680652af682498bd5c7f1505c5f51b08c770143643c6388e"} diff --git a/wai/pack-conformance/packs/video-ladder-01/package/stages/mobile.json b/wai/pack-conformance/packs/video-ladder-01/package/stages/mobile.json new file mode 100644 index 000000000..a17df1d90 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"6938df61d1274e43680652af682498bd5c7f1505c5f51b08c770143643c6388e"} diff --git a/wai/pack-conformance/packs/video-ladder-01/package/stages/tv.json b/wai/pack-conformance/packs/video-ladder-01/package/stages/tv.json new file mode 100644 index 000000000..f2b8286b1 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/package/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"p":2,"stages":[{"bytes":35882,"h":"c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2","layer":0}],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"p":4,"stages":[{"bytes":25514,"h":"618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481","layer":0}],"start":160}]}},"policy_digest":"6938df61d1274e43680652af682498bd5c7f1505c5f51b08c770143643c6388e"} diff --git a/wai/pack-conformance/packs/video-ladder-01/policy.toml b/wai/pack-conformance/packs/video-ladder-01/policy.toml new file mode 100644 index 000000000..07e11d953 --- /dev/null +++ b/wai/pack-conformance/packs/video-ladder-01/policy.toml @@ -0,0 +1,54 @@ +schema = "wai.pack.policy/1" +asset_id = "video-ladder-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "hi" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[[ladder]] +id = "lo" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "d795df9202fb5f32fe168aa585332a5694ad7d8d7e8b859fc7d57a791f08c990" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/packs/video-repack-01/floor/hi.obu b/wai/pack-conformance/packs/video-repack-01/floor/hi.obu new file mode 100644 index 000000000..a75994f85 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/floor/hi.obu differ diff --git a/wai/pack-conformance/packs/video-repack-01/floor/lo.obu b/wai/pack-conformance/packs/video-repack-01/floor/lo.obu new file mode 100644 index 000000000..6bdb49216 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/floor/lo.obu differ diff --git a/wai/pack-conformance/packs/video-repack-01/key.hex b/wai/pack-conformance/packs/video-repack-01/key.hex new file mode 100644 index 000000000..a96d52610 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/key.hex @@ -0,0 +1 @@ +8b961c20169b07e8cb3f95ffb999a577064ed11dafa170e74aee28902fc72b53 diff --git a/wai/pack-conformance/packs/video-repack-01/package/LISTING b/wai/pack-conformance/packs/video-repack-01/package/LISTING new file mode 100644 index 000000000..392e46d95 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/LISTING @@ -0,0 +1,46 @@ +c5203585fea0962de941d258789157a0590e18b62a2e5d1facbd11e1cfd1ddb9 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/hi/init.mp4 +589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s +6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s +acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74 floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s +d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833 floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s +d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9 floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s +640cd81c83baefbe13e8ccc2bf577395c6602885cc73e201ca34dc267e7f0a48 floor/lo/init.mp4 +bfa98255d682517f58df4073bc71fdb3fcca120fc33d16e4aca873392476c586 hls/classic/floor-hi.m3u8 +ad81674ac359096ebed255a26a40e82858e53e9754702429e59d72898df33812 hls/classic/floor-lo.m3u8 +3fd88f9dcf835edf0fcca8dff8371d2b4e897ac097fc85901477c950c0da7996 hls/classic/master.m3u8 +3fd88f9dcf835edf0fcca8dff8371d2b4e897ac097fc85901477c950c0da7996 hls/legacy/master.m3u8 +09e75f55cb7ea829a4e630404d2ab540e8be0dbb1c4365dc30802359b658aed9 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +09e75f55cb7ea829a4e630404d2ab540e8be0dbb1c4365dc30802359b658aed9 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +4f7eb4c2a9e8b3d7dc271612457f50945ce6b2543ce7ba81c53b231f2c0182ad index-v.wsix +8fc573e6d71f2ed7d22bf048eea5cb3dba0a62cd441c24d9740fc1838b3812d1 moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +2f5bd830d841eb122c63ae57a15442075478fd522ce55b6f9bcbe41acd51d8ae objects/482fbbb9b39bdb9cbf00515f94269ea591df681ced90b05d97e0df106b49b08e.wai +0057633252a2c2ed5f71fe14ca78075c8e89fc6675a08335ac90f0e56032b825 objects/4ab8f747d181632f342a4887b0a8bb86c174409cb625157784e18c5e5559a381.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +1a6f30fdc000f258c948470386d84beef2ba5a84e922c355093837ae21fd573c objects/6ce13c9ef6872caeb7fbe71661ca88bcc463ceac47f109c8c155e34bb1a6f37d.wai +b773dac63c9d2d9980d0d249ea956e03065c1ae40d17c512df29b690c0c820ef objects/b7bf0717e968d7b6be473785c9f66cbb814cde679bceb1ae37f0765308ddee72.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +cc8a6fc59be29bd33d96fb30ea9e5a30a5aa367a82da9c5784cbadce63396f62 objects/cf842ef3058d3c54d56c0ea3e075efdc77163602b4a1dabcff080e5ea28b9946.wai +01b596b672de3352adb3e01b34fcddfeff975f4ee1d0b88dde160ac10c9bf929 objects/d382ba093b0595c44a43dc3aad22fc0bed42baeb3488f00dfe3567056a7009c5.wai +9d9ae296558ce7f7d644ad0ca3af65eb4fc98d03b0bd6d7a8fc821fc952e86c1 objects/f525b4450c29153dde0b43e4d143dfb76fcc3884c607b744c8a53fa6a1728580.wai +3d573ee482f8187b0e3bc839aeea0f51a67144588d6d207e8466fea91a0c5b34 objects/f7f7f4d6af777bfcce09cdda5ecb754ce6bd8dcd2e03ecfc41359cad989b15f7.wai +d7031e1c023b1329f38d3d3fe35a7f82801a0b0db0e777ae0738d6c9fc2cff18 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +e82149a73c47b4616d2dfbb43d47b0f2e38f6044868ef27f171b5e310f10bd84 policy.json +f9944f8f431f512496fdba587d90cc9bad4801a98e28d95522123bc24cadb084 policy.toml +7ef94e8fe74a6e68543a965544cf440420020347b47e9c6f48d5e3b121bf627b stages/legacy.json +57d38e0987db1ca04fbb0dffb724b5237aa15e21247bc459c25f771070794fab stages/mobile.json +89ffbd0ef534b5c102a335a2de3ecdc9129180e975ba2af498f1c1c935bc862e stages/tv.json diff --git a/wai/pack-conformance/packs/video-repack-01/package/dash/classic.mpd b/wai/pack-conformance/packs/video-repack-01/package/dash/classic.mpd new file mode 100644 index 000000000..0bc509f7c --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/dash/classic.mpd @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-repack-01/package/dash/mobile.mpd b/wai/pack-conformance/packs/video-repack-01/package/dash/mobile.mpd new file mode 100644 index 000000000..d3340daa2 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/dash/mobile.mpd @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-repack-01/package/dash/tv.mpd b/wai/pack-conformance/packs/video-repack-01/package/dash/tv.mpd new file mode 100644 index 000000000..f0817bb2c --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/dash/tv.mpd @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-repack-01/package/floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s b/wai/pack-conformance/packs/video-repack-01/package/floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s new file mode 100644 index 000000000..7e5a29203 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s b/wai/pack-conformance/packs/video-repack-01/package/floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s new file mode 100644 index 000000000..3e21af5bf Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s b/wai/pack-conformance/packs/video-repack-01/package/floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s new file mode 100644 index 000000000..ec55da7e8 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s b/wai/pack-conformance/packs/video-repack-01/package/floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s new file mode 100644 index 000000000..a246e72be Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s b/wai/pack-conformance/packs/video-repack-01/package/floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s new file mode 100644 index 000000000..98e3a28d6 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/floor/hi/init.mp4 b/wai/pack-conformance/packs/video-repack-01/package/floor/hi/init.mp4 new file mode 100644 index 000000000..0b3ade258 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/floor/hi/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s b/wai/pack-conformance/packs/video-repack-01/package/floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s new file mode 100644 index 000000000..78b5c8866 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s b/wai/pack-conformance/packs/video-repack-01/package/floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s new file mode 100644 index 000000000..0cb031621 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s b/wai/pack-conformance/packs/video-repack-01/package/floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s new file mode 100644 index 000000000..8747edc9f Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s b/wai/pack-conformance/packs/video-repack-01/package/floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s new file mode 100644 index 000000000..6adf8fdfd Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s b/wai/pack-conformance/packs/video-repack-01/package/floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s new file mode 100644 index 000000000..5c06ef47a Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/floor/lo/init.mp4 b/wai/pack-conformance/packs/video-repack-01/package/floor/lo/init.mp4 new file mode 100644 index 000000000..d419e7893 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/floor/lo/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/hls/classic/floor-hi.m3u8 b/wai/pack-conformance/packs/video-repack-01/package/hls/classic/floor-hi.m3u8 new file mode 100644 index 000000000..d6ac7336d --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/hls/classic/floor-hi.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/hi/init.mp4" +#EXTINF:2.400, +../../floor/hi/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +#EXTINF:1.000, +../../floor/hi/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +#EXTINF:2.000, +../../floor/hi/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +#EXTINF:1.000, +../../floor/hi/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +#EXTINF:1.600, +../../floor/hi/576b7d2156c2b595bf48bf34f67996d7f89e75d105f0dc71c9841bb96e6cd0fc.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-repack-01/package/hls/classic/floor-lo.m3u8 b/wai/pack-conformance/packs/video-repack-01/package/hls/classic/floor-lo.m3u8 new file mode 100644 index 000000000..628e70c2b --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/hls/classic/floor-lo.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/lo/init.mp4" +#EXTINF:2.400, +../../floor/lo/d0f68eb18af5b67736ddccd97ed45c4c0274b0460f6abc8f54940ac378165833.m4s +#EXTINF:1.000, +../../floor/lo/6bba50408f894d0e9d5516a53992fdd2aa86db43ccdc684b69c51a688856a0db.m4s +#EXTINF:2.000, +../../floor/lo/acd166807d05fe4f512407f717659d88613f6d0d360485effe3751504cefbb74.m4s +#EXTINF:1.000, +../../floor/lo/589ad79210bff258db68916876846a2b0d530ea2e44260af66ddc51ad14ce07f.m4s +#EXTINF:1.600, +../../floor/lo/d550ebb204adf0b5a7f310c7d07189e7bed63e12e34e9ed4d9bf96a101d864a9.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-repack-01/package/hls/classic/master.m3u8 b/wai/pack-conformance/packs/video-repack-01/package/hls/classic/master.m3u8 new file mode 100644 index 000000000..5fb4ccb7c --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/hls/classic/master.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=9055,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9016,AVERAGE-BANDWIDTH=8723,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-repack-01/package/hls/legacy/master.m3u8 b/wai/pack-conformance/packs/video-repack-01/package/hls/legacy/master.m3u8 new file mode 100644 index 000000000..5fb4ccb7c --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/hls/legacy/master.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=9055,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9016,AVERAGE-BANDWIDTH=8723,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-repack-01/package/hls/mobile/master.m3u8 b/wai/pack-conformance/packs/video-repack-01/package/hls/mobile/master.m3u8 new file mode 100644 index 000000000..5f049c978 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/hls/mobile/master.m3u8 @@ -0,0 +1,8 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=9055,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9016,AVERAGE-BANDWIDTH=8723,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-repack-01/package/hls/mobile/wai-v.m3u8 b/wai/pack-conformance/packs/video-repack-01/package/hls/mobile/wai-v.m3u8 new file mode 100644 index 000000000..949e945a2 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/hls/mobile/wai-v.m3u8 @@ -0,0 +1,20 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-GAP +#EXTINF:2.400, +../../objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai#n=39826 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXT-X-GAP +#EXTINF:2.000, +../../objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai#n=35882 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai#n=25514 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-repack-01/package/hls/mobile/wai.m3u8 b/wai/pack-conformance/packs/video-repack-01/package/hls/mobile/wai.m3u8 new file mode 100644 index 000000000..66fa8dc50 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/hls/mobile/wai.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="mobile" +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/mobile.json" +#EXT-X-STREAM-INF:BANDWIDTH=79600,AVERAGE-BANDWIDTH=79600,CODECS="wai1.video.int_motion",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/packs/video-repack-01/package/hls/tv/master.m3u8 b/wai/pack-conformance/packs/video-repack-01/package/hls/tv/master.m3u8 new file mode 100644 index 000000000..5f049c978 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/hls/tv/master.m3u8 @@ -0,0 +1,8 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=10440,AVERAGE-BANDWIDTH=9055,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-hi.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=9016,AVERAGE-BANDWIDTH=8723,CODECS="av01.0.00M.08",RESOLUTION=32x18 +../classic/floor-lo.m3u8 diff --git a/wai/pack-conformance/packs/video-repack-01/package/hls/tv/wai-v.m3u8 b/wai/pack-conformance/packs/video-repack-01/package/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..8d1c2efe8 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/hls/tv/wai-v.m3u8 @@ -0,0 +1,17 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:2.400, +../../objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai#n=39826 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXTINF:2.000, +../../objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai#n=35882 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXTINF:1.600, +../../objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai#n=25514 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-repack-01/package/hls/tv/wai.m3u8 b/wai/pack-conformance/packs/video-repack-01/package/hls/tv/wai.m3u8 new file mode 100644 index 000000000..47679d0e9 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/hls/tv/wai.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="tv" +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/tv.json" +#EXT-X-STREAM-INF:BANDWIDTH=143528,AVERAGE-BANDWIDTH=121122,CODECS="wai1.video.int_motion",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/packs/video-repack-01/package/index-v.wsix b/wai/pack-conformance/packs/video-repack-01/package/index-v.wsix new file mode 100644 index 000000000..89d0ff7ae Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/index-v.wsix differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/moq/catalog.json b/wai/pack-conformance/packs/video-repack-01/package/moq/catalog.json new file mode 100644 index 000000000..406272968 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/moq/catalog.json @@ -0,0 +1 @@ +{"tracks":[{"altGroup":1,"isLive":false,"name":"v/l0","namespace":"video-repack-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_motion"],"wai_determinism":"decode-equivalence","wai_receipt_alias":4731536367675475,"wai_receipts":true,"wai_receipts_track":"v/l0/receipts","wai_staged":{"layer":0,"lead_ms":null,"set":"v"}},{"altGroup":1,"codec":"av01.0.00M.08","height":36,"name":"hi/floor","namespace":"video-repack-01","packaging":"cmaf","renderGroup":1,"width":64},{"altGroup":1,"codec":"av01.0.00M.08","height":18,"name":"lo/floor","namespace":"video-repack-01","packaging":"cmaf","renderGroup":1,"width":32},{"isLive":false,"name":"v/l0/receipts","namespace":"video-repack-01","packaging":"wai-receipts"}],"version":1} diff --git a/wai/pack-conformance/packs/video-repack-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai b/wai/pack-conformance/packs/video-repack-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai new file mode 100644 index 000000000..b15b53108 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/objects/482fbbb9b39bdb9cbf00515f94269ea591df681ced90b05d97e0df106b49b08e.wai b/wai/pack-conformance/packs/video-repack-01/package/objects/482fbbb9b39bdb9cbf00515f94269ea591df681ced90b05d97e0df106b49b08e.wai new file mode 100644 index 000000000..e6688e833 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/objects/482fbbb9b39bdb9cbf00515f94269ea591df681ced90b05d97e0df106b49b08e.wai differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/objects/4ab8f747d181632f342a4887b0a8bb86c174409cb625157784e18c5e5559a381.wai b/wai/pack-conformance/packs/video-repack-01/package/objects/4ab8f747d181632f342a4887b0a8bb86c174409cb625157784e18c5e5559a381.wai new file mode 100644 index 000000000..cf1710435 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/objects/4ab8f747d181632f342a4887b0a8bb86c174409cb625157784e18c5e5559a381.wai differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai b/wai/pack-conformance/packs/video-repack-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai new file mode 100644 index 000000000..04de3f188 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai b/wai/pack-conformance/packs/video-repack-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai new file mode 100644 index 000000000..622209937 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/objects/6ce13c9ef6872caeb7fbe71661ca88bcc463ceac47f109c8c155e34bb1a6f37d.wai b/wai/pack-conformance/packs/video-repack-01/package/objects/6ce13c9ef6872caeb7fbe71661ca88bcc463ceac47f109c8c155e34bb1a6f37d.wai new file mode 100644 index 000000000..93381e7ad Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/objects/6ce13c9ef6872caeb7fbe71661ca88bcc463ceac47f109c8c155e34bb1a6f37d.wai differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/objects/b7bf0717e968d7b6be473785c9f66cbb814cde679bceb1ae37f0765308ddee72.wai b/wai/pack-conformance/packs/video-repack-01/package/objects/b7bf0717e968d7b6be473785c9f66cbb814cde679bceb1ae37f0765308ddee72.wai new file mode 100644 index 000000000..21822eab7 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/objects/b7bf0717e968d7b6be473785c9f66cbb814cde679bceb1ae37f0765308ddee72.wai differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai b/wai/pack-conformance/packs/video-repack-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai new file mode 100644 index 000000000..2ab2cbd49 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/objects/cf842ef3058d3c54d56c0ea3e075efdc77163602b4a1dabcff080e5ea28b9946.wai b/wai/pack-conformance/packs/video-repack-01/package/objects/cf842ef3058d3c54d56c0ea3e075efdc77163602b4a1dabcff080e5ea28b9946.wai new file mode 100644 index 000000000..5b4b092eb Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/objects/cf842ef3058d3c54d56c0ea3e075efdc77163602b4a1dabcff080e5ea28b9946.wai differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/objects/d382ba093b0595c44a43dc3aad22fc0bed42baeb3488f00dfe3567056a7009c5.wai b/wai/pack-conformance/packs/video-repack-01/package/objects/d382ba093b0595c44a43dc3aad22fc0bed42baeb3488f00dfe3567056a7009c5.wai new file mode 100644 index 000000000..2ce8b2d3a Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/objects/d382ba093b0595c44a43dc3aad22fc0bed42baeb3488f00dfe3567056a7009c5.wai differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/objects/f525b4450c29153dde0b43e4d143dfb76fcc3884c607b744c8a53fa6a1728580.wai b/wai/pack-conformance/packs/video-repack-01/package/objects/f525b4450c29153dde0b43e4d143dfb76fcc3884c607b744c8a53fa6a1728580.wai new file mode 100644 index 000000000..f91d83f96 Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/objects/f525b4450c29153dde0b43e4d143dfb76fcc3884c607b744c8a53fa6a1728580.wai differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/objects/f7f7f4d6af777bfcce09cdda5ecb754ce6bd8dcd2e03ecfc41359cad989b15f7.wai b/wai/pack-conformance/packs/video-repack-01/package/objects/f7f7f4d6af777bfcce09cdda5ecb754ce6bd8dcd2e03ecfc41359cad989b15f7.wai new file mode 100644 index 000000000..ccf6489fc Binary files /dev/null and b/wai/pack-conformance/packs/video-repack-01/package/objects/f7f7f4d6af777bfcce09cdda5ecb754ce6bd8dcd2e03ecfc41359cad989b15f7.wai differ diff --git a/wai/pack-conformance/packs/video-repack-01/package/package.receipt.json b/wai/pack-conformance/packs/video-repack-01/package/package.receipt.json new file mode 100644 index 000000000..695ca2459 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-repack-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"df4e5e68edf28f11f0ba914f92f05d3d3dcdd4e5c2c808175e3aa45694a45592","media_class":"video","stage_root":"56ff2237e4654a894ebefb9d0588092806b71c58ee502aa43624bba633cd4bf2"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"re-pack","parent_receipt_hash":"22c6f558063499040127d58b062f08f9ec2a6cee54b51c97d42e8741d76ad728","policy_digest":"21db788ba0009de8df8a540d30841d707e18fbbd68b5bcc1ca6b998ff1c05008","receipt_hash":"82efcb204fb9f2be928a9217a2c39c55b8ccbc6345664c435bb44e22072bfed4","sig":"1d69fa0993386204d349f55f0a53499c2dcb7cde762f1d66e46ddd01e69d15e0f91b01404732e2cc4ab7f08be3ff86d5550070c7ca55477dec746e1c73268109","signer_id":"did:key:z6MkgBeQ3WGF1Gc5juV274JXu3VfVACXK93TwQY57g5jVYbx","signer_pubkey":"19b75ae888e816033d1436d72002d1a375ca87444efbb150d6cc2b32c8ef5f87","stored_bytes":127966,"total_bytes":140530,"total_entries":15,"unique_objects":12,"version":2,"work_units":15} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-repack-01/package/plan-v.txt b/wai/pack-conformance/packs/video-repack-01/package/plan-v.txt new file mode 100644 index 000000000..c7196c34c --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/plan-v.txt @@ -0,0 +1,5 @@ +0 +60 +85 +135 +160 diff --git a/wai/pack-conformance/packs/video-repack-01/package/policy.json b/wai/pack-conformance/packs/video-repack-01/package/policy.json new file mode 100644 index 000000000..98fbb8714 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-repack-01","class":[{"floor_only":false,"id":"tv","max_layer":0},{"floor_only":false,"id":"mobile","max_kbps":100,"max_layer":0},{"floor_only":true,"id":"legacy","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"video","source":"video-base-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"d795df9202fb5f32fe168aa585332a5694ad7d8d7e8b859fc7d57a791f08c990"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":30000,"mv_precision":1,"q_candidates":[16,12,8],"search":4},"role":"base"},{"capability":"wai.video.av1","component":"v","id":"hi","params":{"source":"supplied"},"role":"floor"},{"capability":"wai.video.av1","component":"v","id":"lo","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":true,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-repack-01/package/policy.toml b/wai/pack-conformance/packs/video-repack-01/package/policy.toml new file mode 100644 index 000000000..4492ee643 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/policy.toml @@ -0,0 +1,54 @@ +schema = "wai.pack.policy/1" +asset_id = "video-repack-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "hi" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[[ladder]] +id = "lo" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "d795df9202fb5f32fe168aa585332a5694ad7d8d7e8b859fc7d57a791f08c990" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/packs/video-repack-01/package/stages/legacy.json b/wai/pack-conformance/packs/video-repack-01/package/stages/legacy.json new file mode 100644 index 000000000..171fd7b89 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"21db788ba0009de8df8a540d30841d707e18fbbd68b5bcc1ca6b998ff1c05008"} diff --git a/wai/pack-conformance/packs/video-repack-01/package/stages/mobile.json b/wai/pack-conformance/packs/video-repack-01/package/stages/mobile.json new file mode 100644 index 000000000..09649ed2c --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"21db788ba0009de8df8a540d30841d707e18fbbd68b5bcc1ca6b998ff1c05008"} diff --git a/wai/pack-conformance/packs/video-repack-01/package/stages/tv.json b/wai/pack-conformance/packs/video-repack-01/package/stages/tv.json new file mode 100644 index 000000000..30815e7af --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/package/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"p":2,"stages":[{"bytes":35882,"h":"c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2","layer":0}],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"p":4,"stages":[{"bytes":25514,"h":"618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481","layer":0}],"start":160}]}},"policy_digest":"21db788ba0009de8df8a540d30841d707e18fbbd68b5bcc1ca6b998ff1c05008"} diff --git a/wai/pack-conformance/packs/video-repack-01/parent b/wai/pack-conformance/packs/video-repack-01/parent new file mode 100644 index 000000000..07b23b316 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/parent @@ -0,0 +1 @@ +video-base-01 diff --git a/wai/pack-conformance/packs/video-repack-01/policy.toml b/wai/pack-conformance/packs/video-repack-01/policy.toml new file mode 100644 index 000000000..4492ee643 --- /dev/null +++ b/wai/pack-conformance/packs/video-repack-01/policy.toml @@ -0,0 +1,54 @@ +schema = "wai.pack.policy/1" +asset_id = "video-repack-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "hi" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[[ladder]] +id = "lo" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "d795df9202fb5f32fe168aa585332a5694ad7d8d7e8b859fc7d57a791f08c990" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/policy_diff.py b/wai/pack-conformance/policy_diff.py index 9f01e22cf..f785cbfd4 100644 --- a/wai/pack-conformance/policy_diff.py +++ b/wai/pack-conformance/policy_diff.py @@ -125,6 +125,11 @@ def random_policy(rng): body = [] for c in comps: body += ["[[component]]", f'id = "{c}"', "media = " + val(q + biased(["video", "audio", "bogus"]) + q)] + # §7.8's profile and a source, which a floor-only component may not have. + if rng.random() < 0.15: + body.append("profile = " + val(q + biased(["floor-only", "bogus"]) + q)) + if rng.random() < 0.1: + body.append('source = "v.y4m"') if rng.random() > 0.05: num, den = val(str(biased([25, 0, 48000, 4294967295, 16777216]))), val(str(biased([1, 1001, 0, 600]))) body.append("timebase = { num = " + num + ", den = " + den + " }") @@ -147,6 +152,13 @@ def random_policy(rng): body.append(f'refines = "{rng.choice(rungs + ["nope"])}"') if rng.random() < 0.5: body.append("params = " + biased(PARAMS)) + if rng.random() < 0.3: + # §7.7's form of a supplied floor, and the floor's other members. + body.append("[floor]") + if rng.random() < 0.6: + body.append("supplied_form = " + val(q + biased(["cmaf", "obu"]) + q)) + if rng.random() < 0.4: + body.append("extra_keyframes = " + val(biased(["true", "false"]))) if rng.random() < 0.3: body += ["[live]", "layer_lag_ms = " + biased(["[0]", "[0, 200]", "[0, 200, 400]", "[1]", "[0, 100, 50]"])] for i in range(biased([0, 0, 1, 2])): diff --git a/wai/pack-conformance/staged-mutations/expected.json b/wai/pack-conformance/staged-mutations/expected.json index cbdafd185..9393a597f 100644 --- a/wai/pack-conformance/staged-mutations/expected.json +++ b/wai/pack-conformance/staged-mutations/expected.json @@ -1,6 +1,11 @@ { "pack": "video-staged-01", "cases": [ + { + "name": "ok", + "reason": "ok", + "remove": [] + }, { "name": "refinement-deadline", "reason": "index-policy-mismatch", diff --git a/wai/pack-conformance/supplied_cmaf.py b/wai/pack-conformance/supplied_cmaf.py new file mode 100644 index 000000000..b10f7bc50 --- /dev/null +++ b/wai/pack-conformance/supplied_cmaf.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Write a golden's supplied floor in the CMAF form (extensions/packaging.md §7.7) from +another golden's supplied floor in the OBU form (§7.3). + +A CMAF floor is a ladder's own initialization segment and media segments, as a media +muxer wrote them, so this golden's are written by one: each `.obu` of the source +golden's floor is muxed, without re-encoding (`-c copy`), by ffmpeg's HLS muxer into fMP4 +segments, cutting at every key frame (`-hls_time` of one frame), so each media segment +holds one piece of the plan; its `init.mp4` and `seg_.m4s` files, `` four digits, are +`floor//`. The policy's `floor.supplied_listing` is set to the SHA-256 of the floor +directory's listing (SPEC §3.1), which `wai-pack listing` prints. + +A muxer is not a pure function of its input across versions, so the files are committed +and pinned by the listing: a packer reads them and checks them against the plan, and +never writes them. This script is how the committed files were written, not a step of +verification. + + python3 supplied_cmaf.py > > +""" +import hashlib +import os +import re +import shutil +import subprocess +import sys +import tempfile + + +def main(argv): + if len(argv) != 6: + print(__doc__) + return 2 + ffmpeg, wai_pack, src, dst, rate = argv[1:] + num, den = (int(x) for x in rate.split("/")) + floor_in, floor_out = os.path.join(src, "floor"), os.path.join(dst, "floor") + if os.path.isdir(floor_out): + shutil.rmtree(floor_out) + os.makedirs(floor_out) + for f in sorted(os.listdir(floor_in)): + if not f.endswith(".obu"): + continue + name = f[:-len(".obu")] + with tempfile.TemporaryDirectory() as tmp: + subprocess.run([ffmpeg, "-nostdin", "-v", "error", "-f", "obu", "-framerate", f"{num}/{den}", + "-i", os.path.join(floor_in, f), "-c", "copy", "-f", "hls", "-hls_time", f"{den / num:.6f}", + "-hls_segment_type", "fmp4", "-hls_fmp4_init_filename", "init.mp4", + "-hls_segment_filename", os.path.join(tmp, "seg_%04d.m4s"), "-hls_playlist_type", "vod", + os.path.join(tmp, "index.m3u8")], check=True) + out = os.path.join(floor_out, name) + os.makedirs(out) + for g in sorted(os.listdir(tmp)): + if g == "init.mp4" or g.endswith(".m4s"): + shutil.copyfile(os.path.join(tmp, g), os.path.join(out, g)) + print(f"{out}: {sorted(os.listdir(out))}") + listing = hashlib.sha256(subprocess.run([wai_pack, "listing", floor_out], check=True, capture_output=True).stdout).hexdigest() + p = os.path.join(dst, "policy.toml") + policy = open(p, encoding="utf-8").read() + open(p, "w", encoding="utf-8").write(re.sub(r'supplied_listing = "[0-9a-f]{64}"', f'supplied_listing = "{listing}"', policy)) + print(f"listing {listing}") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/wai/pack-conformance/supplied_floor.py b/wai/pack-conformance/supplied_floor.py index d01635825..02e58eb52 100644 --- a/wai/pack-conformance/supplied_floor.py +++ b/wai/pack-conformance/supplied_floor.py @@ -6,10 +6,14 @@ from the fixture generator and nothing else. This script states how it was made: 1. `wai-pack fixture gen ` writes the fixture the policy's component names; 2. `wai-pack plan` cuts it with the policy and writes the keyframe plan (§7.3); -3. each piece is coded alone by an AV1 encoder (`aomenc`, one pass, constant quality, no - lag and no alternate reference, so each temporal unit holds one shown frame, and no - key frame but the piece's first), in the low-overhead OBU format; -4. the pieces' streams, concatenated in order, are `floor/.obu`; +3. for each floor rung of the policy, in ladder order, each piece is coded alone by an AV1 + encoder (`aomenc`, one pass, constant quality, no lag and no alternate reference, so + each temporal unit holds one shown frame, and no key frame but the piece's first), in + the low-overhead OBU format; a rung given `--scale =` codes the fixture with + its width and height divided by d, each sample the mean of its d × d block, rounded + half up, and `--cq =` sets its quality (40 by default); +4. each rung's pieces' streams, concatenated in order, are `floor/.obu`: the + component's id when it has one floor rung, the rung's id when it has several (§7.1); 5. the policy's `floor.supplied_listing` is set to the SHA-256 of the floor directory's listing (SPEC §3.1), which `wai-pack listing` prints. @@ -19,6 +23,7 @@ never encodes it. This script is how the committed stream was written, not a ste verification. python3 supplied_floor.py directory> [aomenc] + [--scale =]... [--cq =]... """ import os import re @@ -46,45 +51,101 @@ def frames_of(y4m): return head, out +def scaled(head, frames, d): + """The frames with width and height divided by d: each sample of each plane the mean + of its d x d block, rounded half up (4:2:0, so the chroma planes are halved too).""" + if d == 1: + return head, frames + w = int(re.search(rb" W(\d+)", head).group(1)) + h = int(re.search(rb" H(\d+)", head).group(1)) + if w % (2 * d) or h % (2 * d): + raise SystemExit(f"{w}x{h} does not divide by {2 * d}") + cw, ch = (w + 1) // 2, (h + 1) // 2 + + def plane(b, pw, ph): + out = bytearray() + for y in range(0, ph, d): + for x in range(0, pw, d): + t = sum(b[(y + j) * pw + x + i] for j in range(d) for i in range(d)) + out.append((2 * t + d * d) // (2 * d * d)) + return bytes(out) + out = [] + for f in frames: + line_end = f.index(b"\n") + 1 + body = f[line_end:] + y, u, v = body[:w * h], body[w * h:w * h + cw * ch], body[w * h + cw * ch:] + out.append(f[:line_end] + plane(y, w, h) + plane(u, cw, ch) + plane(v, cw, ch)) + head = re.sub(rb" W\d+", b" W%d" % (w // d), head) + head = re.sub(rb" H\d+", b" H%d" % (h // d), head) + return head, out + + +def floor_rungs(policy): + """The policy's floor rungs, in ladder order.""" + out = [] + for block in re.split(r"\n(?=\[)", policy): + if block.startswith("[[ladder]]") and re.search(r'role = "floor"', block): + out.append(re.search(r'id = "([^"]+)"', block).group(1)) + return out + + def main(argv): - if len(argv) < 3: + opts = {"--scale": {}, "--cq": {}} + rest = [] + it = iter(argv[1:]) + for a in it: + if a in opts: + k, v = next(it).split("=") + opts[a][k] = int(v) + else: + rest.append(a) + if len(rest) < 2: print(__doc__) return 2 - wai_pack, pack = argv[1], os.path.abspath(argv[2]) - aomenc = argv[3] if len(argv) > 3 else "aomenc" + wai_pack, pack = rest[0], os.path.abspath(rest[1]) + aomenc = rest[2] if len(rest) > 2 else "aomenc" policy_path = os.path.join(pack, "policy.toml") policy = open(policy_path, encoding="utf-8").read() comp = re.search(r'\[\[component\]\]\s*id = "([^"]+)"', policy).group(1) source = re.search(r'source = "([^"]+)\.y4m"', policy).group(1) + rungs = floor_rungs(policy) + names = [comp] if len(rungs) == 1 else rungs + floor = os.path.join(pack, "floor") with tempfile.TemporaryDirectory() as tmp: src = os.path.join(tmp, "source.y4m") run(wai_pack, "fixture", "gen", source, "-o", src) plan = os.path.join(tmp, "plan.txt") run(wai_pack, "plan", "--policy", policy_path, "--component", comp, "--source", src, "--out", plan) starts = [int(x) for x in open(plan).read().split()] - head, frames = frames_of(open(src, "rb").read()) - bounds = starts + [len(frames)] - stream = b"" - for k, (a, b) in enumerate(zip(bounds, bounds[1:])): - piece = os.path.join(tmp, f"p{k}.y4m") - with open(piece, "wb") as f: - f.write(head + b"".join(frames[a:b])) - out = os.path.join(tmp, f"p{k}.obu") - run(aomenc, "--obu", f"--limit={b - a}", "--passes=1", "--lag-in-frames=0", "--auto-alt-ref=0", - "--kf-min-dist=0", f"--kf-max-dist={b - a}", "--end-usage=q", "--cq-level=40", "--cpu-used=6", - "--threads=1", "--row-mt=0", "-q", "-o", out, piece) - stream += open(out, "rb").read() - floor = os.path.join(pack, "floor") + head0, frames0 = frames_of(open(src, "rb").read()) + bounds = starts + [len(frames0)] + streams = {} + for rung, name in zip(rungs, names): + head, frames = scaled(head0, frames0, opts["--scale"].get(rung, 1)) + stream = b"" + for k, (a, b) in enumerate(zip(bounds, bounds[1:])): + piece = os.path.join(tmp, f"{rung}-p{k}.y4m") + with open(piece, "wb") as f: + f.write(head + b"".join(frames[a:b])) + out = os.path.join(tmp, f"{rung}-p{k}.obu") + run(aomenc, "--obu", f"--limit={b - a}", "--passes=1", "--lag-in-frames=0", "--auto-alt-ref=0", + "--kf-min-dist=0", f"--kf-max-dist={b - a}", "--end-usage=q", f"--cq-level={opts['--cq'].get(rung, 40)}", + "--cpu-used=6", "--threads=1", "--row-mt=0", "-q", "-o", out, piece) + stream += open(out, "rb").read() + streams[name] = stream os.makedirs(floor, exist_ok=True) - for name in os.listdir(floor): - os.remove(os.path.join(floor, name)) - with open(os.path.join(floor, f"{comp}.obu"), "wb") as f: - f.write(stream) + for n in os.listdir(floor): + os.remove(os.path.join(floor, n)) + for name, stream in streams.items(): + with open(os.path.join(floor, f"{name}.obu"), "wb") as f: + f.write(stream) import hashlib listing = hashlib.sha256(run(wai_pack, "listing", floor)).hexdigest() policy = re.sub(r'supplied_listing = "[0-9a-f]{64}"', f'supplied_listing = "{listing}"', policy) open(policy_path, "w", encoding="utf-8").write(policy) - print(f"{floor}/{comp}.obu: {len(stream)} bytes, {len(frames)} frames, key frames at {starts}; listing {listing}") + for name, stream in streams.items(): + print(f"{floor}/{name}.obu: {len(stream)} bytes, {len(frames0)} frames, key frames at {starts}") + print(f"listing {listing}") return 0 diff --git a/wai/wai-pack/src/av1.rs b/wai/wai-pack/src/av1.rs index 2b13e87fd..387954a13 100644 --- a/wai/wai-pack/src/av1.rs +++ b/wai/wai-pack/src/av1.rs @@ -131,6 +131,153 @@ pub fn temporal_units(b: &[u8]) -> Result>, Av1Error> { Ok(out) } +/// Why a temporal unit could not be read from a stream: the stream is malformed, or the +/// stream could not be read. +#[derive(Debug)] +pub enum ReadError { + Malformed(Av1Error), + Io(std::io::Error), +} + +impl From for ReadError { + fn from(e: Av1Error) -> Self { + ReadError::Malformed(e) + } +} + +impl From for ReadError { + fn from(e: std::io::Error) -> Self { + ReadError::Io(e) + } +} + +/// The temporal units of a low-overhead OBU stream read one at a time: the split +/// [`temporal_units`] makes, the same units refused for the same faults, without holding +/// the stream. Each unit is given as the stream holds it, its temporal delimiter +/// included. +pub struct TuReader { + r: std::io::BufReader, + /// The delimiter that starts the next unit, read with the unit before it. + next: Option>, + at: u64, + units: u64, + done: bool, +} + +impl TuReader { + pub fn new(r: R) -> Self { + TuReader { r: std::io::BufReader::with_capacity(1 << 16, r), next: None, at: 0, units: 0, done: false } + } + + /// One OBU: its type, its bytes and its payload's length; `None` at a clean end of + /// the stream. + fn obu(&mut self) -> Result, u64)>, ReadError> { + use std::io::Read as _; + let mut h = [0u8; 1]; + if self.r.read(&mut h)? == 0 { + return Ok(None); + } + let at = self.at; + let h = h[0]; + if h & 0x80 != 0 { + return Err(Av1Error(format!("the forbidden bit is set at byte {at}")).into()); + } + if h & 0x02 == 0 { + return Err(Av1Error(format!("an OBU at byte {at} has no size field")).into()); + } + let mut bytes = vec![h]; + if h & 0x04 != 0 { + let mut e = [0u8; 1]; + if self.r.read(&mut e)? == 0 { + return Err(Av1Error("an OBU header runs past the end".into()).into()); + } + bytes.push(e[0]); + } + let mut size = 0u64; + let mut ended = false; + for i in 0..8 { + let mut b = [0u8; 1]; + if self.r.read(&mut b)? == 0 { + return Err(Av1Error("a size field runs past the end".into()).into()); + } + bytes.push(b[0]); + size |= ((b[0] & 0x7f) as u64) << (7 * i); + if b[0] & 0x80 == 0 { + ended = true; + break; + } + } + if !ended { + return Err(Av1Error("a size field longer than 8 bytes".into()).into()); + } + if size > u32::MAX as u64 { + return Err(Av1Error("a size field past 2^32 - 1".into()).into()); + } + let head = bytes.len(); + // The payload grows with the bytes read, never to the size field's value ahead + // of them. + (&mut self.r).take(size).read_to_end(&mut bytes)?; + if (bytes.len() - head) as u64 != size { + return Err(Av1Error(format!("the OBU at byte {at} runs past the end")).into()); + } + self.at += bytes.len() as u64; + Ok(Some(((h >> 3) & 0x0f, bytes, size))) + } + + /// The reader under this one, once every unit has been read. + pub fn into_inner(self) -> R { + self.r.into_inner() + } + + /// The next temporal unit, its delimiter included; `None` after the last. + pub fn next_unit(&mut self) -> Result>, ReadError> { + if self.done { + return Ok(None); + } + let mut unit = match self.next.take() { + Some(td) => td, + None => match self.obu()? { + None => { + self.done = true; + return Err(Av1Error("no temporal unit".into()).into()); + } + Some((t, b, size)) if t == OBU_TEMPORAL_DELIMITER => { + if size != 0 { + return Err(Av1Error("a temporal delimiter with a payload".into()).into()); + } + b + } + Some(_) => return Err(Av1Error("the stream does not begin with a temporal delimiter".into()).into()), + }, + }; + let mut n = 1usize; + loop { + match self.obu()? { + None => { + self.done = true; + break; + } + Some((t, b, size)) if t == OBU_TEMPORAL_DELIMITER => { + if size != 0 { + return Err(Av1Error("a temporal delimiter with a payload".into()).into()); + } + self.next = Some(b); + break; + } + Some((_, b, _)) => { + unit.extend_from_slice(&b); + n += 1; + } + } + } + if n < 2 { + return Err(Av1Error("a temporal unit holds only its delimiter".into()).into()); + } + self.units += 1; + Ok(Some(unit)) + } +} + struct Bits<'a> { b: &'a [u8], at: usize, diff --git a/wai/wai-pack/src/bin/wai_pack.rs b/wai/wai-pack/src/bin/wai_pack.rs index a0db30b7e..b52b3c278 100644 --- a/wai/wai-pack/src/bin/wai_pack.rs +++ b/wai/wai-pack/src/bin/wai_pack.rs @@ -11,14 +11,23 @@ //! supplied floor //! wai-pack pack --policy --out --key [--source =]... //! [--floor ] [--signer-id ] [--royalty-free-only] [--meter none|model:] +//! [--base-from ] [--link-supplied] [--jobs ] //! pack the policy's components into : objects, stage indexes, the floor's CMAF //! files, catalogs, the receipt (and, metered, its measurement claim) and LISTING; -//! is new or empty (`pack-out-not-empty` otherwise, before any work), and a -//! refusal writes nothing and prints its reason. Sources default to each +//! is new or empty (`pack-out-not-empty` otherwise, before any work). Each file +//! is written as it is made, the source read a frame at a time and the floor a unit +//! at a time (packaging §11.4), so what the packer holds does not grow with the +//! source's length; a refusal removes what it wrote and prints its reason. --jobs +//! codes that many pieces at once, each alone, so the package is the same for any n. Sources default to each //! component's `source`, and the floor to `floor/`, beside the policy; the signer id //! defaults to the key's did:key. A meter that gives no figure (a pack too short to //! reach a microjoule at , or a figure past 2^64 - 1) refuses the pack -//! (`pack-meter`); it never writes an unmetered package in its place +//! (`pack-meter`); it never writes an unmetered package in its place. With +//! --base-from, a re-pack (packaging §11.5): every piece's base and refinements are +//! taken from that package, verified and matched to this one piece by piece, instead +//! of coded, and the receipt names it as the package it re-packs. With +//! --link-supplied, a supplied CMAF floor's files (packaging §7.7) are written as hard +//! links to the supplied ones where the file system allows //! wai-pack live --policy --out --key [--signer-id ] [--window ] //! pack a Y4M stream read from standard input as it arrives (packaging §9.2): each //! piece coded as the segmenter closes it, and its objects written to /objects/ @@ -35,8 +44,9 @@ //! the objects it lists, removes the objects only unsealed pieces published, writes //! no plan file or LISTING, and exits 1 saying what it kept. No floor: a supplied //! floor is the on-demand package's (§7.3). is new or empty -//! wai-pack verify -//! verify a package (packaging §9): every reason, or its counts and pack digest +//! wai-pack verify [--parent ] +//! verify a package (packaging §9): every reason, or its counts and pack digest; with +//! --parent, a re-pack against the parent its receipt names (packaging §11.5) //! wai-pack listing [--primary] //! the listing (SPEC §3.1) of every file of but LISTING; with --primary, of the //! plan files and the objects the directory's stage indexes (index-*.wsix, and @@ -66,6 +76,10 @@ //! window,lookahead},"frames":[[RGB8 bytes]...],"stream":bool} and writes, per line, //! {"pieces":[[start,len,flags,abs_diff,changed,compared,occupancy]...],"scores":[...]}, //! pushing frames one at a time when "stream" is true +//! wai-pack cmaf-verdict ... +//! a conformance aid: the supplied CMAF reader's verdict on each file (packaging §7.7), +//! `init\t`, then `\t\t\t` per +//! segment, the digest the SHA-256 of each sample's `offset,length,duration,flags,dts;` //! wai-pack fixture gen [-o ] //! write a conformance fixture (Y4M); `fixture list` names them //! wai-pack conformance regen|check @@ -86,9 +100,9 @@ fn usage() -> ExitCode { eprintln!( "usage:\n wai-pack policy [--royalty-free-only]\n \ wai-pack plan --policy --component [--source ] [--out ]\n \ - wai-pack pack --policy --out --key [--source =]... [--floor ] [--signer-id ] [--royalty-free-only] [--meter none|model:]\n \ + wai-pack pack --policy --out --key [--source =]... [--floor ] [--signer-id ] [--royalty-free-only] [--meter none|model:] [--base-from ] [--link-supplied] [--jobs ]\n \ wai-pack live --policy --out --key [--signer-id ] [--window ]\n \ - wai-pack verify \n \ + wai-pack verify [--parent ]\n \ wai-pack listing [--primary]\n \ wai-pack sim --policy --class --rate --startup [--source ] [--rtt ] [--chunk ] [--enc ] [--cap ] [--fifo]\n \ wai-pack stage-json\n \ @@ -176,19 +190,30 @@ fn cmd_pack(args: &[String]) -> Result<(), String> { let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?; let base = Path::new(path).parent().unwrap_or(Path::new(".")); let p = Policy::parse_bytes(&bytes).map_err(|e| e.to_string())?; + // Each source is a file the packer reads a frame at a time, and the supplied floor a + // directory it reads a file at a time: neither is held whole. let mut sources = std::collections::BTreeMap::new(); for c in &p.components { let given = args.iter().zip(args.iter().skip(1)).filter(|(f, _)| *f == "--source").find_map(|(_, v)| v.strip_prefix(&format!("{}=", c.id))); let file = given.map(std::path::PathBuf::from).or_else(|| c.source.as_ref().map(|s| base.join(s))); if let Some(f) = file { - sources.insert(c.id.clone(), std::fs::read(&f).map_err(|e| format!("{}: {e}", f.display()))?); + if !f.is_file() { + return Err(format!("{}: no such file", f.display())); + } + sources.insert(c.id.clone(), wai_pack::files::Input::Path(f)); } } let floor_dir = opt(args, "--floor").map(std::path::PathBuf::from).unwrap_or_else(|| base.join("floor")); - let floor = if floor_dir.is_dir() { wai::pin::read_dir_set(&floor_dir).map_err(|e| e.to_string())? } else { Default::default() }; + let floor = if floor_dir.is_dir() { wai_pack::files::FileSet::Dir(floor_dir) } else { wai_pack::files::FileSet::default() }; let signer_id = opt(args, "--signer-id").map(str::to_string).unwrap_or_else(|| wai::signer_id::did_key(&key.verifying_key().to_bytes())); - let mut o = wai_pack::pack::PackOpts::new(key, signer_id, floor); + let mut o = wai_pack::pack::PackOpts::with_floor(key, signer_id, floor); + o.jobs = match opt(args, "--jobs") { + None => 1, + Some(j) => j.parse().ok().filter(|&n: &usize| n >= 1).ok_or_else(|| format!("pack: --jobs {j}: a count of at least 1"))?, + }; o.overrides = Overrides { royalty_free_only: args.iter().any(|a| a == "--royalty-free-only") }; + o.base_from = opt(args, "--base-from").map(|d| wai_pack::files::FileSet::Dir(d.into())); + o.link_supplied = args.iter().any(|a| a == "--link-supplied"); match opt(args, "--meter").unwrap_or("none") { "none" => {} m => { @@ -199,18 +224,28 @@ fn cmd_pack(args: &[String]) -> Result<(), String> { o.issued_at = wai::claim_receipt::rfc3339_utc(now); } } - let pk = wai_pack::pack::pack(&bytes, &sources, &mut o).map_err(|e| e.to_string())?; - pk.write(Path::new(out)).map_err(|e| format!("{out}: {e}"))?; - println!("receipt {}", hex(&pk.receipt.receipt_hash())); - println!("pack digest {}", pk.pack_digest()); - println!("entries {} ({} distinct, {} bytes referenced, {} stored)", pk.receipt.total_entries, pk.receipt.unique_objects, pk.receipt.total_bytes, pk.receipt.stored_bytes); - println!("energy {}", if pk.receipt.energy_provenance.is_some() { format!("{} uJ ({})", pk.receipt.joules_micro, pk.receipt.energy_provenance.as_ref().map(|p| p.label()).unwrap_or("")) } else { "not metered".into() }); + // Every file is written as it is made; a refusal removes what was written. + let mut sink = wai_pack::files::DirSink::new(Path::new(out)).map_err(|e| format!("{out}: {e}"))?; + let receipt = wai_pack::pack::pack_into(&bytes, &sources, &mut o, &mut sink).map_err(|e| e.to_string())?; + sink.keep(); + let listing = std::fs::read(Path::new(out).join("LISTING")).map_err(|e| format!("{out}: {e}"))?; + println!("receipt {}", hex(&receipt.receipt_hash())); + println!("pack digest {}", wai_pack::sha256_hex(&listing)); + println!("entries {} ({} distinct, {} bytes referenced, {} stored)", receipt.total_entries, receipt.unique_objects, receipt.total_bytes, receipt.stored_bytes); + println!("energy {}", if receipt.energy_provenance.is_some() { format!("{} uJ ({})", receipt.joules_micro, receipt.energy_provenance.as_ref().map(|p| p.label()).unwrap_or("")) } else { "not metered".into() }); Ok(()) } fn cmd_verify(args: &[String]) -> Result<(), String> { let dir = args.first().ok_or("verify: a package directory")?; - match wai_pack::verify::verify_dir(Path::new(dir)) { + let verdict = match opt(args, "--parent") { + None => wai_pack::verify::verify_dir(Path::new(dir)), + Some(parent) => { + let read = |d: &str| wai_pack::verify::read_strict(Path::new(d)).map_err(|e| vec![wai_pack::verify::Reason { code: "unexpected-file".into(), detail: e }]); + read(dir).and_then(|f| read(parent).and_then(|p| wai_pack::verify::verify_repack(&f, &p))) + } + }; + match verdict { Ok(r) => { println!("ok {dir}"); println!("receipt {}", hex(&r.receipt_hash)); @@ -441,6 +476,39 @@ fn cmd_source_verdict(args: &[String]) -> Result<(), String> { Ok(()) } +/// `cmaf-verdict ...`: the supplied CMAF reader's verdict on each file +/// (packaging §7.7): `init\t`, then per segment `\t\t\t`, +/// the digest the SHA-256 of `,,,,;` per +/// sample; a reader that panics prints `PANIC`. +fn cmd_cmaf_verdict(args: &[String]) -> Result<(), String> { + let (init, segs) = args.split_first().ok_or("cmaf-verdict: ...")?; + let ib = std::fs::read(init).map_err(|e| format!("{init}: {e}"))?; + let i = match std::panic::catch_unwind(|| wai_pack::cmaf_read::read_init(&ib)) { + Err(_) => { + println!("init\tPANIC"); + return Ok(()); + } + Ok(Err(f)) => { + println!("init\t{}", f.code()); + return Ok(()); + } + Ok(Ok(i)) => i, + }; + println!("init\tok"); + for p in segs { + let b = std::fs::read(p).map_err(|e| format!("{p}: {e}"))?; + match std::panic::catch_unwind(|| wai_pack::cmaf_read::read_segment(&b, &i)) { + Err(_) => println!("{p}\tPANIC\t0\t-"), + Ok(Err(f)) => println!("{p}\t{}\t0\t-", f.code()), + Ok(Ok(s)) => { + let text: String = s.iter().map(|x| format!("{},{},{},{},{};", x.at, x.len, x.duration, x.flags, x.dts)).collect(); + println!("{p}\tok\t{}\t{}", s.len(), wai_pack::sha256_hex(text.as_bytes())); + } + } + } + Ok(()) +} + fn cmd_segment_json() -> Result<(), String> { use std::io::BufRead as _; for line in std::io::stdin().lock().lines() { @@ -543,6 +611,7 @@ fn main() -> ExitCode { "listing" => cmd_listing(rest), "policy-verdict" => cmd_policy_verdict(rest), "source-verdict" => cmd_source_verdict(rest), + "cmaf-verdict" => cmd_cmaf_verdict(rest), "segment-json" => cmd_segment_json(), "fixture" => cmd_fixture(rest), "conformance" => cmd_conformance(rest), diff --git a/wai/wai-pack/src/catalog.rs b/wai/wai-pack/src/catalog.rs index 47f3daa8c..7d73f10ff 100644 --- a/wai/wai-pack/src/catalog.rs +++ b/wai/wai-pack/src/catalog.rs @@ -3,10 +3,11 @@ //! file is a pure function of the stage index, the objects and the floor, so a verifier //! renders each again and compares bytes. //! -//! - `hls/classic/master.m3u8` and `hls/classic/floor-.m3u8`: the classical -//! presentation, the floor alone; it names nothing of WAI. Its variant is -//! `../classic/floor-.m3u8`, which resolves to the same playlist from every -//! class's directory, so a class's master is this one byte for byte. +//! - `hls/classic/master.m3u8` and `hls/classic/floor-.m3u8`: the classical +//! presentation, the floors alone; it names nothing of WAI. It has one variant per +//! floor rung, in ladder order, `../classic/floor-.m3u8` for its floor name `` +//! (packaging §7.1), which resolves to the same playlist from every class's directory, +//! so a class's master is this one byte for byte. //! - `hls//master.m3u8`: the classical master with, for a class offered WAI //! objects, the one line http-delivery §11.1 lets a publisher add, naming //! `wai.m3u8`, and no other change (http-delivery §11.1, §11.8). @@ -14,20 +15,23 @@ //! stage view as `EXT-X-SESSION-DATA`, and one variant whose `CODECS` is the union and //! `X-WAI-FLOOR` the floor (http-delivery §4.2) of the objects it lists, with //! `BANDWIDTH` the peak and `AVERAGE-BANDWIDTH` the mean of `8 · bytes / duration`. -//! - `hls//wai-.m3u8`: the WAI media playlist, adjacent to the floor's -//! (http-delivery §11.2), rendered from the floor playlist and the objects by +//! - `hls//wai-.m3u8`: the WAI media playlist, adjacent to the floors' +//! (http-delivery §11.2; every floor has the pieces' durations), rendered from the +//! first floor's playlist and the objects by //! `wai::http_binding::hls::wai_media_playlist`: a piece's base as its segment, each //! layer the class lists as a `#WAI-LAYER` line after the segment's `EXTINF` //! (http-delivery §10.4), and a piece the class takes as floor only marked //! `#EXT-X-GAP`, so the sink takes the floor's segment there. //! - `dash/classic.mpd` and `dash/.mpd`: the same as two MPDs (http-delivery -//! §11.1): the classical one, and the class's WAI MPD with `EssentialProperty` and the +//! §11.1): the classical one, one Representation per floor rung (with one floor rung +//! its codecs and size on the AdaptationSet, with several on each Representation), and +//! the class's WAI MPD with `EssentialProperty` and the //! floor in a `SupplementalProperty`, one Representation per layer, each layer's //! depending on the one below (`dependencyId`), and a timeline with a gap where the //! class lists the floor only. //! - `moq/catalog.json`: one track per (component, layer, deadline) of packaging `wai`, a -//! refinement track depending on the track below, the floor a `cmaf` track sharing the -//! base track's `altGroup`. +//! refinement track depending on the track below, each floor a `cmaf` track +//! `/floor` sharing the base track's `altGroup`. //! - `stages/.json`: per piece, the stages the class lists. //! //! Durations: a piece of `d` units of `num/den` lasts `floor(d · den · 1000 / num)` @@ -68,9 +72,11 @@ pub const SCHEME_STAGES: &str = "tag:transaction.science,2026:wai/stages"; pub const PRIORITY_BASE: u64 = 64; pub const PRIORITY_STEP: u64 = 32; -/// A component's floor as the catalogs name it. +/// A floor rung's floor as the catalogs name it. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FloorView { + /// Its floor name (packaging §7.1). + pub name: String, pub codec: String, pub w: u32, pub h: u32, @@ -80,6 +86,26 @@ pub struct FloorView { pub fragments: Vec<(String, u64)>, } +/// Where the catalogs read a package's objects: by content hash, each whole. +pub trait Objects { + fn object(&self, h: &[u8; 32]) -> Option>; +} + +impl Objects for BTreeMap<[u8; 32], Vec> { + fn object(&self, h: &[u8; 32]) -> Option> { + self.get(h).cloned() + } +} + +/// The objects a [`crate::files::Sink`] holds, read back from it one at a time. +pub struct SinkObjects<'a>(pub &'a dyn crate::files::Sink); + +impl Objects for SinkObjects<'_> { + fn object(&self, h: &[u8; 32]) -> Option> { + self.0.get(&crate::objects::object_path(h)).ok() + } +} + /// A component as the catalogs read it. pub struct CompView<'a> { pub index: &'a StageIndex, @@ -87,8 +113,9 @@ pub struct CompView<'a> { pub w: u32, pub h: u32, /// Every object of the index by its content hash. - pub objects: &'a BTreeMap<[u8; 32], Vec>, - pub floor: Option, + pub objects: &'a dyn Objects, + /// Its floor rungs' floors, in ladder order. + pub floors: Vec, /// The capability of the component's default base rung (§5.7): the base an HTTP view /// carries (§8.3). pub base_cap: String, @@ -180,8 +207,8 @@ impl CompView<'_> { for p in 0..self.index.pieces.len() { for e in self.http_listed(c, p) { if seen.insert(e.content_hash) { - let env = self.objects.get(&e.content_hash).ok_or_else(|| CatalogError(format!("object {} is not held", crate::hex(&e.content_hash))))?; - out.push(SeqObject::from_envelope(env).map_err(|r| CatalogError(r.to_string()))?); + let env = self.objects.object(&e.content_hash).ok_or_else(|| CatalogError(format!("object {} is not held", crate::hex(&e.content_hash))))?; + out.push(SeqObject::from_envelope(&env).map_err(|r| CatalogError(r.to_string()))?); } } } @@ -207,8 +234,7 @@ impl CompView<'_> { rates(&items, self.tb) } - fn floor_rates(&self) -> (u64, u64) { - let f = self.floor.as_ref().expect("a floor"); + fn floor_rates(&self, f: &FloorView) -> (u64, u64) { let items: Vec<(u64, u64)> = f.fragments.iter().zip(&self.index.pieces).map(|((_, len), p)| (*len, p.duration as u64)).collect(); rates(&items, self.tb) } @@ -236,22 +262,20 @@ fn media_render(v: &CompView, map_uri: Option) -> HlsRender { } } -/// `hls/classic/floor-.m3u8`. -pub fn render_floor_media(v: &CompView) -> String { - let f = v.floor.as_ref().expect("a floor"); +/// `hls/classic/floor-.m3u8`, for the floor `f`. +pub fn render_floor_media(v: &CompView, f: &FloorView) -> String { let segments: Vec = f.fragments.iter().enumerate() .map(|(p, (path, _))| Segment { index: p as u64, kind: SegmentKind::Content, uri: format!("../../{path}"), duration_ms: piece_ms(v, p), ad_id: None }) .collect(); render_hls_with(&segments, &media_render(v, Some(format!("../../{}", f.init))), &[]) } -/// The floor's variant, as every master names it: `../classic/floor-.m3u8`, the +/// A floor's variant, as every master names it: `../classic/floor-.m3u8`, the /// same URI from `hls/classic/` and from a class's directory. -fn floor_variant(v: &CompView) -> StreamInf { - let f = v.floor.as_ref().expect("a floor"); - let (peak, mean) = v.floor_rates(); +fn floor_variant(v: &CompView, f: &FloorView) -> StreamInf { + let (peak, mean) = v.floor_rates(f); StreamInf { - uri: format!("../classic/floor-{}.m3u8", v.id()), + uri: format!("../classic/floor-{}.m3u8", f.name), bandwidth: peak, average_bandwidth: Some(mean), codecs: f.codec.clone(), @@ -265,9 +289,13 @@ fn master(session_data: Vec, variants: Vec) -> Result Vec { + v.floors.iter().map(|f| floor_variant(v, f)).collect() +} + +/// `hls/classic/master.m3u8`: one variant per floor rung, in ladder order. pub fn render_classic_master(v: &CompView) -> String { - master(Vec::new(), vec![floor_variant(v)]).expect("a floor variant renders") + master(Vec::new(), floor_variants(v)).expect("a floor variant renders") } /// `hls//master.m3u8`: the classical master, and the line naming the class's @@ -277,7 +305,7 @@ pub fn render_class_master(v: &CompView, c: usize) -> Result vec![SessionData::Value { data_id: DATA_ID_MULTIVARIANT.into(), value: "wai.m3u8".into() }], None => Vec::new(), }; - master(data, vec![floor_variant(v)]) + master(data, floor_variants(v)) } /// `hls//wai.m3u8`, or `None` when the class lists no WAI object. @@ -302,14 +330,15 @@ pub fn render_wai_multivariant(v: &CompView, c: usize) -> Result, } fn object(v: &CompView, e: &EntryRec) -> Result, CatalogError> { - v.objects.get(&e.content_hash).cloned().ok_or_else(|| CatalogError(format!("object {} is not held", crate::hex(&e.content_hash)))) + v.objects.object(&e.content_hash).ok_or_else(|| CatalogError(format!("object {} is not held", crate::hex(&e.content_hash)))) } -/// `hls//wai-.m3u8`: `wai_media_playlist` over the floor playlist, each piece's -/// base as its segment (a gap where the class takes the floor), with the layers it -/// lists. +/// `hls//wai-.m3u8`: `wai_media_playlist` over the first floor's playlist (every +/// floor's has the pieces' durations), each piece's base as its segment (a gap where the +/// class takes the floor), with the layers it lists. pub fn render_wai_media(v: &CompView, c: usize) -> Result { - let floor = parse_media_playlist(&render_floor_media(v))?; + let first = v.floors.first().ok_or_else(|| CatalogError("a component with no floor".into()))?; + let floor = parse_media_playlist(&render_floor_media(v, first))?; let mut segs = Vec::with_capacity(v.index.pieces.len()); for p in 0..v.index.pieces.len() { let base = v.index.piece_entries(p).iter().find(|e| e.role == Role::Base).expect("a piece has a base"); @@ -350,23 +379,32 @@ fn mpd(v: &CompView, set: AdaptationSet) -> Result { })?) } -/// `dash/classic.mpd`. +/// `dash/classic.mpd`: one Representation per floor rung, in ladder order, `-floor`. +/// With one floor rung its codecs and size are the AdaptationSet's; with several, each +/// Representation's, and the set carries none. pub fn render_dash_classic(v: &CompView) -> Result { - let f = v.floor.as_ref().expect("a floor"); - let (peak, _) = v.floor_rates(); let all: Vec = (0..v.index.pieces.len()).collect(); - let rep = Representation { - id: format!("{}-floor", v.id()), - dependency_id: None, - bandwidth: peak, - timescale: v.tb.num as u64, - initialization: Some(format!("../{}", f.init)), - timeline: timeline(v.index, &all, v.tb)?, - media: f.fragments.iter().map(|(path, _)| format!("../{path}")).collect(), - }; + let one = v.floors.len() == 1; + let mut reps = Vec::with_capacity(v.floors.len()); + for f in &v.floors { + let (peak, _) = v.floor_rates(f); + reps.push(Representation { + id: format!("{}-floor", f.name), + dependency_id: None, + bandwidth: peak, + codecs: (!one).then(|| f.codec.clone()), + size: (!one).then_some((f.w, f.h)), + timescale: v.tb.num as u64, + initialization: Some(format!("../{}", f.init)), + timeline: timeline(v.index, &all, v.tb)?, + media: f.fragments.iter().map(|(path, _)| format!("../{path}")).collect(), + }); + } + let first = v.floors.first().ok_or_else(|| CatalogError("a component with no floor".into()))?; let set = AdaptationSet { - id: 1, content_type: Some("video".into()), mime_type: "video/mp4".into(), codecs: f.codec.clone(), size: Some((f.w, f.h)), - segment_alignment: true, start_with_sap: Some(1), essential: Vec::new(), supplemental: Vec::new(), representations: vec![rep], + id: 1, content_type: Some("video".into()), mime_type: "video/mp4".into(), + codecs: one.then(|| first.codec.clone()), size: one.then_some((first.w, first.h)), + segment_alignment: true, start_with_sap: Some(1), essential: Vec::new(), supplemental: Vec::new(), representations: reps, }; mpd(v, set) } @@ -386,6 +424,8 @@ pub fn render_dash_class(v: &CompView, c: usize) -> Result, Catal id: format!("{}-l{k}", v.id()), dependency_id: (k > 0).then(|| format!("{}-l{}", v.id(), k - 1)), bandwidth: peak, + codecs: None, + size: None, timescale: v.tb.num as u64, initialization: None, timeline: timeline(v.index, &ps.iter().map(|(p, _)| *p).collect::>(), v.tb)?, @@ -393,7 +433,7 @@ pub fn render_dash_class(v: &CompView, c: usize) -> Result, Catal }); } let set = AdaptationSet { - id: 1, content_type: Some("video".into()), mime_type: wai::http_binding::MEDIA_TYPE.into(), codecs: union, size: Some((v.w, v.h)), + id: 1, content_type: Some("video".into()), mime_type: wai::http_binding::MEDIA_TYPE.into(), codecs: Some(union), size: Some((v.w, v.h)), segment_alignment: false, start_with_sap: None, essential: vec![Descriptor { scheme: SCHEME_HTTP.into(), value: None }], supplemental: vec![ @@ -474,7 +514,8 @@ pub fn moq_tracks(v: &CompView, namespace: &str, layer_lag_ms: &[u64], default_d /// `wai_determinism`, receipts declared with an alias ([`receipt_alias`]) and a receipts /// track of its own, `/receipts`; a refinement track carries /// `wai_delivery_timeout_ms` (§8.6), and the base track `altGroup` 1, which it shares with -/// the floor, a `cmaf` track. The receipts tracks follow, in their media tracks' order. +/// each floor, a `cmaf` track `/floor` for its floor name, in ladder order. The +/// receipts tracks follow, in their media tracks' order. /// The result passes §4.1's catalog check. pub fn render_moq_catalog(v: &CompView, namespace: &str, layer_lag_ms: &[u64], default_deadline: u64) -> Result { use wai::staged::moq::{receipts_track_json, SetTrack, StagedTrack}; @@ -526,13 +567,13 @@ pub fn render_moq_catalog(v: &CompView, namespace: &str, layer_lag_ms: &[u64], d if let Some(to) = t.timeout_ms { j["wai_delivery_timeout_ms"] = json!(to); } - if t.layer == 0 && v.floor.is_some() { + if t.layer == 0 && !v.floors.is_empty() { j["altGroup"] = json!(1); } out.push(j); } - if let Some(f) = &v.floor { - out.push(json!({"altGroup": 1, "codec": f.codec, "height": f.h, "name": format!("{id}/floor"), "namespace": namespace, "packaging": "cmaf", "renderGroup": 1, "width": f.w})); + for f in &v.floors { + out.push(json!({"altGroup": 1, "codec": f.codec, "height": f.h, "name": format!("{}/floor", f.name), "namespace": namespace, "packaging": "cmaf", "renderGroup": 1, "width": f.w})); } for t in &tracks { out.push(receipts_track_json(namespace, &format!("{}/receipts", t.name), false)); @@ -590,11 +631,13 @@ pub fn render_stage_view(v: &CompView, c: usize, policy_digest: &[u8; 32]) -> Re pub fn render_all(v: &CompView, policy: &crate::policy::Policy, policy_digest: &[u8; 32]) -> Result>, CatalogError> { let mut out: BTreeMap> = BTreeMap::new(); let id = v.id().to_string(); - if v.floor.is_none() { + if v.floors.is_empty() { return Err(CatalogError(format!("component {id} has no floor"))); } out.insert("hls/classic/master.m3u8".into(), render_classic_master(v).into_bytes()); - out.insert(format!("hls/classic/floor-{id}.m3u8"), render_floor_media(v).into_bytes()); + for f in &v.floors { + out.insert(format!("hls/classic/floor-{}.m3u8", f.name), render_floor_media(v, f).into_bytes()); + } out.insert("dash/classic.mpd".into(), render_dash_classic(v)?.into_bytes()); out.insert("moq/catalog.json".into(), render_moq_catalog(v, &policy.asset_id, &policy.layer_lag_ms, policy.deadline_ms)?.into_bytes()); for c in 0..v.index.classes.len() { @@ -629,7 +672,7 @@ mod tests { w: 64, h: 36, objects, - floor: Some(FloorView { codec: "av01.0.00M.08".into(), w: 64, h: 36, init: "floor/v/init.mp4".into(), fragments: vec![("floor/v/a.m4s".into(), 2000), ("floor/v/b.m4s".into(), 1000)] }), + floors: vec![FloorView { name: "v".into(), codec: "av01.0.00M.08".into(), w: 64, h: 36, init: "floor/v/init.mp4".into(), fragments: vec![("floor/v/a.m4s".into(), 2000), ("floor/v/b.m4s".into(), 1000)] }], base_cap: "wai.video.int_motion".into(), } } @@ -668,15 +711,15 @@ mod tests { assert_eq!(m1.lines().filter(|l| l.starts_with("#WAI-LAYER")).count(), 1, "class 1 lists layer 1 of piece 0 only"); assert_eq!(m1.lines().filter(|l| *l == "#EXT-X-GAP").count(), 1, "and piece 1 as floor only"); let extinf = |s: &str| s.lines().filter(|l| l.starts_with("#EXTINF") || l.starts_with("#EXT-X-TARGETDURATION") || l.starts_with("#EXT-X-MEDIA-SEQUENCE")).map(str::to_string).collect::>(); - assert_eq!(extinf(&media), extinf(&render_floor_media(&v)), "adjacent: the same sequence and durations to the millisecond"); + assert_eq!(extinf(&media), extinf(&render_floor_media(&v, &v.floors[0])), "adjacent: the same sequence and durations to the millisecond"); // The layers sit after their segment's EXTINF, where http-delivery's reader finds // them; the WAI playlist is adjacent to the floor's; a class's master is the // classical master with the one line http-delivery §11.1 allows. let read = wai::http_binding::hls::parse_media_playlist(&media).unwrap(); assert_eq!(read.segments[0].extra.layers.len(), 2); assert!(read.is_rendered_form(&media)); - wai::http_binding::hls::check_adjacent(&media, &render_floor_media(&v)).unwrap(); - wai::http_binding::hls::check_adjacent(&m1, &render_floor_media(&v)).unwrap(); + wai::http_binding::hls::check_adjacent(&media, &render_floor_media(&v, &v.floors[0])).unwrap(); + wai::http_binding::hls::check_adjacent(&m1, &render_floor_media(&v, &v.floors[0])).unwrap(); wai::http_binding::hls::classical_unchanged(&render_classic_master(&v), &render_class_master(&v, 0).unwrap()).unwrap(); let dash = render_dash_class(&v, 0).unwrap().unwrap(); assert!(dash.contains(" Option { - [PieceKind::Natural, PieceKind::Static, PieceKind::Synthetic, PieceKind::MotionHigh].get(c as usize).copied() + match c { + 6 => Some(PieceKind::None), + _ => [PieceKind::Natural, PieceKind::Static, PieceKind::Synthetic, PieceKind::MotionHigh].get(c as usize).copied(), + } } /// The policy's spelling (`rule.when.piece_kind`). @@ -35,6 +41,7 @@ impl PieceKind { PieceKind::Static => "static", PieceKind::Synthetic => "synthetic", PieceKind::MotionHigh => "motion_high", + PieceKind::None => "none", } } } diff --git a/wai/wai-pack/src/cmaf_read.rs b/wai/wai-pack/src/cmaf_read.rs new file mode 100644 index 000000000..f437afb0d --- /dev/null +++ b/wai/wai-pack/src/cmaf_read.rs @@ -0,0 +1,416 @@ +//! The reader of a supplied CMAF floor ([packaging] §7.7): an initialization segment and +//! media segments as an existing ladder holds them, read box by box (ISO/IEC 14496-12), +//! so the floor's CMAF files are the ladder's own bytes and its temporal units are read +//! out of them. +//! +//! - `init.mp4`: its top-level boxes tile the file; one `moov`, holding one `trak` and an +//! `mvex` with one `trex` for that track; the track's `tkhd` names it, its `mdhd` gives +//! the timescale (version 0 or 1, at least 1), its `hdlr` is `vide`, its sample +//! description is one `av01` entry, and an `edts`, where there is one, is one `elst` +//! entry that starts at media time 0 at rate 1. +//! - a media segment: its top-level boxes tile the file: any of `styp`, `sidx`, `prft`, +//! `emsg`, `free` and `skip` first, then one or more `moof` each followed by its `mdat`, +//! and nothing else. A `moof` holds an `mfhd` and one `traf`: a `tfhd` of the init's +//! track, with no base data offset and no empty duration; a `tfdt`; and one or more +//! `trun`, each with its data offset. Each sample's duration, size and flags are its +//! `trun`'s, else the `tfhd`'s defaults, else the `trex`'s; its bytes lie in the `mdat` +//! after its `moof`, from the `moof`'s first byte plus the data offset, the samples of +//! a run one after another. +//! +//! Anything else is refused with what failed. Every read is bounds-checked and integer. +//! +//! [packaging]: ../../../extensions/packaging.md + +/// Why a supplied CMAF file is refused: its boxes (`floor-cmaf-malformed`), or its times +/// (`floor-cmaf-timing`). +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CmafFault { + Malformed(String), + Timing(String), +} + +impl CmafFault { + pub fn code(&self) -> &'static str { + match self { + CmafFault::Malformed(_) => "floor-cmaf-malformed", + CmafFault::Timing(_) => "floor-cmaf-timing", + } + } + pub fn detail(&self) -> &str { + match self { + CmafFault::Malformed(d) | CmafFault::Timing(d) => d, + } + } +} + +fn bad(why: impl Into) -> Result { + Err(CmafFault::Malformed(why.into())) +} + +/// One box: its type, and the offsets of its body and end in the bytes it was read from. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Bx { + pub kind: [u8; 4], + pub start: usize, + pub body: usize, + pub end: usize, +} + +/// The boxes that tile `b[from..to]`: each a 32-bit size of at least 8 (or 1, with a +/// 64-bit size after the type, of at least 16), its type, and its body; a size of 0 (to +/// the end) is refused, as is a box past `to`. +pub fn boxes_in(b: &[u8], from: usize, to: usize) -> Result, CmafFault> { + let mut out = Vec::new(); + let mut at = from; + while at < to { + let size32 = b.get(at..at + 4).map(|x| u32::from_be_bytes(x.try_into().expect("4"))).ok_or_else(|| CmafFault::Malformed(format!("a box header runs past the end at byte {at}")))?; + let kind: [u8; 4] = b.get(at + 4..at + 8).and_then(|x| x.try_into().ok()).ok_or_else(|| CmafFault::Malformed(format!("a box header runs past the end at byte {at}")))?; + let (size, head) = match size32 { + 0 => return bad(format!("a box of size 0 at byte {at}")), + 1 => { + let s = b.get(at + 8..at + 16).map(|x| u64::from_be_bytes(x.try_into().expect("8"))).ok_or_else(|| CmafFault::Malformed("a 64-bit box size runs past the end".into()))?; + if s < 16 { + return bad(format!("a 64-bit box size below 16 at byte {at}")); + } + (s, 16usize) + } + s if s < 8 => return bad(format!("a box size below 8 at byte {at}")), + s => (s as u64, 8usize), + }; + let end = (at as u64).checked_add(size).filter(|&e| e <= to as u64).ok_or_else(|| CmafFault::Malformed(format!("a box runs past its parent at byte {at}")))? as usize; + out.push(Bx { kind, start: at, body: at + head, end }); + at = end; + } + Ok(out) +} + +fn only<'a>(bs: &'a [Bx], kind: &[u8; 4], within: &str) -> Result<&'a Bx, CmafFault> { + let v: Vec<&Bx> = bs.iter().filter(|x| &x.kind == kind).collect(); + match v.as_slice() { + [one] => Ok(one), + _ => bad(format!("{within} holds {} {} boxes, not one", v.len(), String::from_utf8_lossy(kind))), + } +} + +/// A 32-bit field at `at` of a box that ends at `end`: a field past its box is refused. +fn u32_at(b: &[u8], at: usize, end: usize) -> Result { + if at + 4 > end { + return bad(format!("a field runs past its box at byte {at}")); + } + Ok(u32::from_be_bytes(b[at..at + 4].try_into().expect("4"))) +} + +fn u64_at(b: &[u8], at: usize, end: usize) -> Result { + if at + 8 > end { + return bad(format!("a field runs past its box at byte {at}")); + } + Ok(u64::from_be_bytes(b[at..at + 8].try_into().expect("8"))) +} + +/// A full box's version and flags. +fn full(b: &[u8], x: &Bx) -> Result<(u8, u32), CmafFault> { + let v = u32_at(b, x.body, x.end)?; + Ok(((v >> 24) as u8, v & 0x00ff_ffff)) +} + +/// What `trex` gives a track's samples by default. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Defaults { + pub duration: u32, + pub size: u32, + pub flags: u32, +} + +/// A supplied initialization segment, read. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Init { + pub track_id: u32, + pub timescale: u32, + pub defaults: Defaults, +} + +/// Read a supplied `init.mp4` (§7.7). +pub fn read_init(b: &[u8]) -> Result { + let top = boxes_in(b, 0, b.len())?; + let moov = only(&top, b"moov", "init.mp4")?; + let mb = boxes_in(b, moov.body, moov.end)?; + let trak = only(&mb, b"trak", "moov")?; + let mvex = only(&mb, b"mvex", "moov")?; + let tb = boxes_in(b, trak.body, trak.end)?; + let tkhd = only(&tb, b"tkhd", "trak")?; + let (tv, _) = full(b, tkhd)?; + let track_id = u32_at(b, tkhd.body + 4 + if tv == 1 { 16 } else { 8 }, tkhd.end)?; + if let Some(edts) = tb.iter().find(|x| &x.kind == b"edts") { + let eb = boxes_in(b, edts.body, edts.end)?; + let elst = only(&eb, b"elst", "edts")?; + let (ev, _) = full(b, elst)?; + let n = u32_at(b, elst.body + 4, elst.end)?; + let (mt, rate) = if ev == 1 { (u64_at(b, elst.body + 8 + 8, elst.end)? as i64, u32_at(b, elst.body + 8 + 16, elst.end)?) } else { (u32_at(b, elst.body + 8 + 4, elst.end)? as i32 as i64, u32_at(b, elst.body + 8 + 8, elst.end)?) }; + if n != 1 || mt != 0 || rate != 0x0001_0000 { + return Err(CmafFault::Timing("an edit list other than one entry at media time 0 and rate 1".into())); + } + } + let mdia = only(&tb, b"mdia", "trak")?; + let md = boxes_in(b, mdia.body, mdia.end)?; + let mdhd = only(&md, b"mdhd", "mdia")?; + let (mv, _) = full(b, mdhd)?; + let timescale = u32_at(b, mdhd.body + 4 + if mv == 1 { 16 } else { 8 }, mdhd.end)?; + if timescale == 0 { + return Err(CmafFault::Timing("a timescale of 0".into())); + } + let hdlr = only(&md, b"hdlr", "mdia")?; + if hdlr.body + 12 > hdlr.end || b.get(hdlr.body + 8..hdlr.body + 12) != Some(b"vide") { + return bad("the track's handler is not vide"); + } + let minf = only(&md, b"minf", "mdia")?; + let mi = boxes_in(b, minf.body, minf.end)?; + let stbl = only(&mi, b"stbl", "minf")?; + let st = boxes_in(b, stbl.body, stbl.end)?; + let stsd = only(&st, b"stsd", "stbl")?; + if u32_at(b, stsd.body + 4, stsd.end)? != 1 { + return bad("a sample description of other than one entry"); + } + let entries = boxes_in(b, stsd.body + 8, stsd.end)?; + match entries.as_slice() { + [e] if &e.kind == b"av01" => {} + _ => return bad("the sample description is not one av01 entry"), + } + let xb = boxes_in(b, mvex.body, mvex.end)?; + let trex = only(&xb, b"trex", "mvex")?; + full(b, trex)?; + if u32_at(b, trex.body + 4, trex.end)? != track_id { + return bad("trex names another track than tkhd"); + } + let defaults = Defaults { duration: u32_at(b, trex.body + 12, trex.end)?, size: u32_at(b, trex.body + 16, trex.end)?, flags: u32_at(b, trex.body + 20, trex.end)? }; + Ok(Init { track_id, timescale, defaults }) +} + +/// A sample of a media segment: where its bytes are, how long it lasts and its flags. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SampleRef { + pub at: usize, + pub len: usize, + pub duration: u32, + pub flags: u32, + /// Its decode time, in the track's timescale. + pub dts: u64, +} + +/// A sample whose flags carry `sample_is_non_sync_sample` is not a sync sample. +pub const NON_SYNC_BIT: u32 = 0x0001_0000; + +const PRELUDE: [&[u8; 4]; 6] = [b"styp", b"sidx", b"prft", b"emsg", b"free", b"skip"]; + +/// Read a supplied media segment (§7.7) against its initialization segment: every sample, +/// in order. +pub fn read_segment(b: &[u8], init: &Init) -> Result, CmafFault> { + let top = boxes_in(b, 0, b.len())?; + let first = top.iter().position(|x| &x.kind == b"moof").ok_or_else(|| CmafFault::Malformed("a segment with no moof".into()))?; + if let Some(x) = top[..first].iter().find(|x| !PRELUDE.contains(&&x.kind)) { + return bad(format!("a {} box before the first moof", String::from_utf8_lossy(&x.kind))); + } + let rest = &top[first..]; + if rest.len() % 2 != 0 || rest.chunks(2).any(|p| &p[0].kind != b"moof" || &p[1].kind != b"mdat") { + return bad("after the first moof, other than moof and mdat pairs"); + } + let mut out = Vec::new(); + let mut next_dts: Option = None; + for pair in rest.chunks(2) { + let (moof, mdat) = (&pair[0], &pair[1]); + let mb = boxes_in(b, moof.body, moof.end)?; + only(&mb, b"mfhd", "moof")?; + if let Some(x) = mb.iter().find(|x| &x.kind != b"mfhd" && &x.kind != b"traf") { + return bad(format!("a {} box in a moof", String::from_utf8_lossy(&x.kind))); + } + let traf = only(&mb, b"traf", "moof")?; + let tb = boxes_in(b, traf.body, traf.end)?; + if let Some(x) = tb.iter().find(|x| ![b"tfhd", b"tfdt", b"trun"].contains(&&x.kind)) { + return bad(format!("a {} box in a traf", String::from_utf8_lossy(&x.kind))); + } + let tfhd = only(&tb, b"tfhd", "traf")?; + let (_, tf) = full(b, tfhd)?; + if tf & !(0x02 | 0x08 | 0x10 | 0x20 | 0x02_0000) != 0 { + return bad(format!("tfhd flags {tf:#x}: a base data offset, an empty duration or an unknown flag")); + } + if u32_at(b, tfhd.body + 4, tfhd.end)? != init.track_id { + return bad("a tfhd of another track"); + } + let mut at = tfhd.body + 8; + let mut field = |set: bool| -> Result, CmafFault> { + if !set { + return Ok(None); + } + let v = u32_at(b, at, tfhd.end)?; + at += 4; + Ok(Some(v)) + }; + let _sdi = field(tf & 0x02 != 0)?; + let d_dur = field(tf & 0x08 != 0)?.unwrap_or(init.defaults.duration); + let d_size = field(tf & 0x10 != 0)?.unwrap_or(init.defaults.size); + let d_flags = field(tf & 0x20 != 0)?.unwrap_or(init.defaults.flags); + if at > tfhd.end { + return bad("tfhd shorter than its flags say"); + } + let tfdt = only(&tb, b"tfdt", "traf")?; + let (dv, _) = full(b, tfdt)?; + let mut dts = if dv == 1 { u64_at(b, tfdt.body + 4, tfdt.end)? } else { u32_at(b, tfdt.body + 4, tfdt.end)? as u64 }; + if let Some(n) = next_dts + && n != dts + { + return Err(CmafFault::Timing("a moof whose decode time is not where the one before it ended".into())); + } + let runs: Vec<&Bx> = tb.iter().filter(|x| &x.kind == b"trun").collect(); + if runs.is_empty() { + return bad("a traf with no trun"); + } + for trun in runs { + let (_, rf) = full(b, trun)?; + if rf & !(0x01 | 0x04 | 0x100 | 0x200 | 0x400 | 0x800) != 0 || rf & 0x01 == 0 { + return bad(format!("trun flags {rf:#x}: no data offset, or an unknown flag")); + } + let n = u32_at(b, trun.body + 4, trun.end)? as usize; + let off = u32_at(b, trun.body + 8, trun.end)? as i32 as i64; + let mut at = trun.body + 12; + let first_flags = if rf & 0x04 != 0 { + let v = u32_at(b, at, trun.end)?; + at += 4; + Some(v) + } else { + None + }; + let per = [rf & 0x100 != 0, rf & 0x200 != 0, rf & 0x400 != 0, rf & 0x800 != 0].iter().filter(|&&x| x).count(); + // The run's fields fit its box before any sample is read; a sample of no bytes + // is refused, so a run's samples are at most its mdat's bytes. + let need = n.checked_mul(4 * per).and_then(|x| at.checked_add(x)).ok_or_else(|| CmafFault::Malformed("a sample count past the box".into()))?; + if need > trun.end { + return bad("trun shorter than its samples"); + } + let mut data = moof.start as i64 + off; + for i in 0..n { + let mut f = |set: bool| -> Result, CmafFault> { + if !set { + return Ok(None); + } + let v = u32_at(b, at, trun.end)?; + at += 4; + Ok(Some(v)) + }; + let duration = f(rf & 0x100 != 0)?.unwrap_or(d_dur); + let size = f(rf & 0x200 != 0)?.unwrap_or(d_size) as i64; + let own_flags = f(rf & 0x400 != 0)?; + let cto = f(rf & 0x800 != 0)?; + let flags = if i == 0 { first_flags.or(own_flags).unwrap_or(d_flags) } else { own_flags.unwrap_or(d_flags) }; + if cto.is_some_and(|c| c != 0) { + return Err(CmafFault::Timing("a sample whose composition time is not its decode time".into())); + } + if size == 0 { + return bad("a sample of no bytes"); + } + if data < mdat.body as i64 || data + size > mdat.end as i64 { + return bad("a sample's bytes outside the mdat after its moof"); + } + out.push(SampleRef { at: data as usize, len: size as usize, duration, flags, dts }); + data += size; + dts = dts.checked_add(duration as u64).ok_or_else(|| CmafFault::Timing("a decode time past 2^64 - 1".into()))?; + } + } + next_dts = Some(dts); + } + if out.is_empty() { + return bad("a segment with no sample"); + } + Ok(out) +} + +/// The temporal delimiter a sample leaves out (an OBU of type 2 with its size field and an +/// empty payload, [AV1] §5.6): a sample is a temporal unit without it. +pub const TEMPORAL_DELIMITER: [u8; 2] = [0x12, 0x00]; + +/// A segment's samples as temporal units (§7.7): each the temporal delimiter, then the +/// sample's bytes. +pub fn units(b: &[u8], samples: &[SampleRef]) -> Vec> { + samples.iter().map(|s| [&TEMPORAL_DELIMITER[..], &b[s.at..s.at + s.len]].concat()).collect() +} + +/// A segment's times against the component's timebase `num/den` (§7.7): each sample lasts +/// one frame (`duration · num = timescale · den`), and its first sample's decode time is +/// frame `start`'s (`dts · num = start · timescale · den`) (`floor-cmaf-timing`). +pub fn check_times(samples: &[SampleRef], timescale: u32, num: u32, den: u32, start: u64) -> Result<(), CmafFault> { + let frame = |d: u64| d as u128 * num as u128 == timescale as u128 * den as u128; + if let Some(s) = samples.iter().find(|s| !frame(s.duration as u64)) { + return Err(CmafFault::Timing(format!("a sample lasting {} ticks of {timescale}, not one frame", s.duration))); + } + let first = samples.first().map_or(0, |s| s.dts); + if first as u128 * num as u128 != start as u128 * timescale as u128 * den as u128 { + return Err(CmafFault::Timing(format!("a segment starting at decode time {first}, not frame {start}'s"))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cmaf::{write_fragment, write_init_av1, Sample}; + + fn seq() -> crate::av1::SequenceHeader { + crate::av1::SequenceHeader { + seq_profile: 0, + still_picture: false, + reduced_still_picture_header: false, + seq_level_idx_0: 0, + seq_tier_0: 0, + max_frame_width: 64, + max_frame_height: 36, + high_bitdepth: false, + twelve_bit: false, + mono_chrome: false, + chroma_subsampling_x: true, + chroma_subsampling_y: true, + chroma_sample_position: 0, + } + } + + /// §7.6's own files read back: the writer's init and fragment are a supplied CMAF floor + /// too, their samples and times recovered exactly. + #[test] + fn the_writer_s_files_read_back() { + let init = write_init_av1(&seq(), &[0x0a, 0x01, 0x00], 64, 36, 25); + let i = read_init(&init).unwrap(); + assert_eq!((i.track_id, i.timescale), (1, 25)); + let s = vec![Sample { data: vec![1, 2, 3], duration: 1, sync: true }, Sample { data: vec![4, 5], duration: 1, sync: false }]; + let f = write_fragment(3, 50, &s); + let got = read_segment(&f, &i).unwrap(); + assert_eq!(got.len(), 2); + assert_eq!(&f[got[0].at..got[0].at + got[0].len], &[1, 2, 3]); + assert_eq!(&f[got[1].at..got[1].at + got[1].len], &[4, 5]); + assert_eq!((got[0].dts, got[1].dts), (50, 51)); + assert_eq!(got[0].flags & NON_SYNC_BIT, 0); + assert_ne!(got[1].flags & NON_SYNC_BIT, 0); + check_times(&got, 25, 25, 1, 50).unwrap(); + assert_eq!(check_times(&got, 25, 25, 1, 49).unwrap_err().code(), "floor-cmaf-timing"); + assert_eq!(check_times(&got, 25, 30, 1, 50).unwrap_err().code(), "floor-cmaf-timing", "another rate"); + assert_eq!(units(&f, &got)[1], vec![0x12, 0x00, 4, 5]); + } + + /// What a segment may not be: a box of size 0, a sample past its mdat, a box the + /// reader does not take in a traf, and an mdat that does not follow its moof. + #[test] + fn a_malformed_segment_is_refused() { + let i = read_init(&write_init_av1(&seq(), &[0x0a, 0x01, 0x00], 64, 36, 25)).unwrap(); + let f = write_fragment(1, 0, &[Sample { data: vec![9; 10], duration: 1, sync: true }]); + let code = |b: &[u8]| read_segment(b, &i).unwrap_err().code(); + let mut zero = f.clone(); + zero[..4].copy_from_slice(&0u32.to_be_bytes()); + assert_eq!(code(&zero), "floor-cmaf-malformed"); + // The mdat cut short: its box no longer holds the sample. + let mut short = f[..f.len() - 3].to_vec(); + let n = short.len(); + let mdat_at = n - (8 + 7); + short[mdat_at..mdat_at + 4].copy_from_slice(&15u32.to_be_bytes()); + assert_eq!(code(&short), "floor-cmaf-malformed"); + assert_eq!(code(&[&f[..], &f[..f.len() - 18]].concat()), "floor-cmaf-malformed", "a moof with no mdat after it"); + let free = [&8u32.to_be_bytes()[..], b"free"].concat(); + assert!(read_segment(&[&free[..], &f[..]].concat(), &i).is_ok(), "a free box before the moof"); + assert_eq!(code(&[&f[..], &free[..]].concat()), "floor-cmaf-malformed", "and none after it"); + } +} diff --git a/wai/wai-pack/src/files.rs b/wai/wai-pack/src/files.rs new file mode 100644 index 000000000..2c375026a --- /dev/null +++ b/wai/wai-pack/src/files.rs @@ -0,0 +1,398 @@ +//! Where a packer's inputs come from and where its package goes, without holding either +//! whole ([packaging] §11.4): a supplied set of files (a supplied floor, §7.3, or an earlier +//! package, §9.2) read from memory or from a directory a file at a time, a source opened +//! as many times as the packer reads it, and a sink that takes each package file as it is +//! made. +//! +//! A directory is read as SPEC §3.1's listing reads one (`wai::pin::read_dir_set`): a +//! name beginning with `.` and a `*.sha256` file directly in it are skipped, and a +//! symbolic link or a name a listing path cannot carry is refused. A file's SHA-256 is +//! taken as it streams, so a listing is computed without holding the files. +//! +//! [packaging]: ../../../extensions/packaging.md + +use std::collections::BTreeMap; +use std::io::Read; +use std::path::{Path, PathBuf}; + +/// A set of files by listing path. +#[derive(Debug, Clone)] +pub enum FileSet { + /// Held in memory. + Memory(BTreeMap>), + /// Below a directory, read when asked. + Dir(PathBuf), +} + +impl Default for FileSet { + fn default() -> FileSet { + FileSet::Memory(BTreeMap::new()) + } +} + +fn walk(root: &Path, rel: &str, out: &mut Vec) -> Result<(), String> { + let dir = if rel.is_empty() { root.to_path_buf() } else { root.join(rel) }; + for entry in std::fs::read_dir(&dir).map_err(|e| format!("{}: {e}", dir.display()))? { + let entry = entry.map_err(|e| e.to_string())?; + let name = entry.file_name().into_string().map_err(|n| format!("{n:?} is not a listing path"))?; + if name.starts_with('.') { + continue; + } + let ty = entry.file_type().map_err(|e| e.to_string())?; + let path = if rel.is_empty() { name.clone() } else { format!("{rel}/{name}") }; + if ty.is_symlink() { + return Err(format!("'{path}' is a symbolic link")); + } else if ty.is_dir() { + walk(root, &path, out)?; + } else if rel.is_empty() && name.ends_with(".sha256") { + continue; + } else { + if !wai::pin::is_listing_path(&path) { + return Err(format!("'{path}' is not a listing path")); + } + out.push(path); + } + } + Ok(()) +} + +impl FileSet { + /// Every file's path, in byte order. + pub fn names(&self) -> Result, String> { + let mut v = match self { + FileSet::Memory(m) => m.keys().cloned().collect(), + FileSet::Dir(d) => { + let mut v = Vec::new(); + if d.is_dir() { + walk(d, "", &mut v)?; + } + v + } + }; + v.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes())); + Ok(v) + } + + /// Whether the set holds `name`. + pub fn contains(&self, name: &str) -> bool { + match self { + FileSet::Memory(m) => m.contains_key(name), + FileSet::Dir(d) => wai::pin::is_listing_path(name) && d.join(name).is_file(), + } + } + + /// `name`'s bytes, read as a stream. + pub fn open(&self, name: &str) -> std::io::Result> { + match self { + FileSet::Memory(m) => m + .get(name) + .map(|b| Box::new(std::io::Cursor::new(b.as_slice())) as Box) + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, name.to_string())), + FileSet::Dir(d) => Ok(Box::new(std::fs::File::open(d.join(name))?)), + } + } + + /// `name`'s bytes, whole: for a file the packer holds anyway (a policy, an index). + pub fn read(&self, name: &str) -> std::io::Result> { + let mut v = Vec::new(); + self.open(name)?.read_to_end(&mut v)?; + Ok(v) + } + + /// `name`'s length in bytes. + pub fn len_of(&self, name: &str) -> std::io::Result { + match self { + FileSet::Memory(m) => m.get(name).map(|b| b.len() as u64).ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, name.to_string())), + FileSet::Dir(d) => Ok(std::fs::metadata(d.join(name))?.len()), + } + } + + /// The file `name` as a path on disk, when the set is a directory. + pub fn path_of(&self, name: &str) -> Option { + match self { + FileSet::Memory(_) => None, + FileSet::Dir(d) => Some(d.join(name)), + } + } + + /// Every file's SHA-256, by path, each read once as a stream. + pub fn sha256s(&self) -> Result, String> { + let mut out = BTreeMap::new(); + for n in self.names()? { + let r = self.open(&n).map_err(|e| format!("{n}: {e}"))?; + out.insert(n.clone(), sha256_of(r).map_err(|e| format!("{n}: {e}"))?); + } + Ok(out) + } + + /// The set's listing (SPEC §3.1), and each file's SHA-256 by path. + pub fn listing(&self) -> Result<(Vec, BTreeMap), String> { + let shas = self.sha256s()?; + let l = wai::pin::listing_from_sha256(&shas).map_err(|e| e.to_string())?; + Ok((l, shas)) + } +} + +/// The SHA-256 of a stream, as lowercase hex. +pub fn sha256_of(mut r: impl Read) -> std::io::Result { + use sha2::Digest as _; + let mut h = sha2::Sha256::new(); + let mut buf = vec![0u8; 1 << 16]; + loop { + let n = r.read(&mut buf)?; + if n == 0 { + break; + } + h.update(&buf[..n]); + } + Ok(crate::hex(&h.finalize())) +} + +/// A reader that hashes what passes through it with SHA-256. +pub struct HashingReader { + inner: R, + h: sha2::Sha256, +} + +impl HashingReader { + pub fn new(inner: R) -> Self { + use sha2::Digest as _; + HashingReader { inner, h: sha2::Sha256::new() } + } + + /// Read what is left, and give the SHA-256 of everything read, as lowercase hex. + pub fn finish(mut self) -> std::io::Result { + use sha2::Digest as _; + std::io::copy(&mut self.inner, &mut DigestWriter(&mut self.h))?; + Ok(crate::hex(&self.h.finalize())) + } +} + +struct DigestWriter<'a>(&'a mut sha2::Sha256); + +impl std::io::Write for DigestWriter<'_> { + fn write(&mut self, b: &[u8]) -> std::io::Result { + use sha2::Digest as _; + self.0.update(b); + Ok(b.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl Read for HashingReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + use sha2::Digest as _; + let n = self.inner.read(buf)?; + self.h.update(&buf[..n]); + Ok(n) + } +} + +/// A component's source: its bytes in memory, or a file the packer opens as many times as +/// it reads it. +#[derive(Debug, Clone)] +pub enum Input<'a> { + Bytes(&'a [u8]), + Path(PathBuf), +} + +impl Input<'_> { + pub fn open(&self) -> std::io::Result> { + match self { + Input::Bytes(b) => Ok(Box::new(std::io::Cursor::new(*b))), + Input::Path(p) => Ok(Box::new(std::fs::File::open(p).map_err(|e| std::io::Error::new(e.kind(), format!("{}: {e}", p.display())))?)), + } + } +} + +/// Where a package's files go as they are made. Each file is taken whole, once; an object +/// the package already holds (one content hash at two entries, §6.2) is taken once. +pub trait Sink { + /// Take the file `path`. + fn put(&mut self, path: &str, bytes: &[u8]) -> std::io::Result<()>; + /// Take the file `path` as a copy of `name` of `from`, which the caller has read and + /// checked: a directory sink may link it rather than copy it (`link` true). + fn put_from(&mut self, path: &str, from: &FileSet, name: &str, sha256: &str, link: bool) -> std::io::Result<()>; + /// Whether the sink holds `path`. + fn has(&self, path: &str) -> bool; + /// The bytes of a file it holds. + fn get(&self, path: &str) -> std::io::Result>; + /// Every file's SHA-256 by path: what `LISTING` lists. + fn sha256s(&self) -> BTreeMap; +} + +/// A sink that keeps every file in memory. +#[derive(Debug, Default)] +pub struct MemSink { + pub files: BTreeMap>, +} + +impl Sink for MemSink { + fn put(&mut self, path: &str, bytes: &[u8]) -> std::io::Result<()> { + self.files.insert(path.to_string(), bytes.to_vec()); + Ok(()) + } + fn put_from(&mut self, path: &str, from: &FileSet, name: &str, _sha256: &str, _link: bool) -> std::io::Result<()> { + let b = from.read(name)?; + self.files.insert(path.to_string(), b); + Ok(()) + } + fn has(&self, path: &str) -> bool { + self.files.contains_key(path) + } + fn get(&self, path: &str) -> std::io::Result> { + self.files.get(path).cloned().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, path.to_string())) + } + fn sha256s(&self) -> BTreeMap { + self.files.iter().map(|(k, v)| (k.clone(), crate::sha256_hex(v))).collect() + } +} + +/// A sink that writes each file below a directory as it is made (written beside its place +/// and renamed into it) and keeps only its SHA-256. Until [`DirSink::keep`] is called, +/// dropping it removes every file it wrote and every directory it made, so a refused pack +/// leaves the directory as it found it. +#[derive(Debug)] +pub struct DirSink { + root: PathBuf, + made_root: bool, + sha: BTreeMap, + dirs: Vec, + kept: bool, +} + +impl DirSink { + /// A sink into `root`, which is new or holds nothing (`crate::pack::out_dir_ready`). + pub fn new(root: &Path) -> std::io::Result { + let made_root = !root.exists(); + std::fs::create_dir_all(root)?; + Ok(DirSink { root: root.to_path_buf(), made_root, sha: BTreeMap::new(), dirs: Vec::new(), kept: false }) + } + + fn place(&mut self, path: &str) -> std::io::Result { + let full = self.root.join(path); + let mut missing = Vec::new(); + let mut at = full.parent().map(Path::to_path_buf); + while let Some(d) = at { + if d == self.root || d.exists() { + break; + } + missing.push(d.clone()); + at = d.parent().map(Path::to_path_buf); + } + for d in missing.into_iter().rev() { + std::fs::create_dir(&d)?; + self.dirs.push(d); + } + Ok(full) + } + + /// Keep what was written. + pub fn keep(mut self) { + self.kept = true; + } +} + +impl Sink for DirSink { + fn put(&mut self, path: &str, bytes: &[u8]) -> std::io::Result<()> { + let full = self.place(path)?; + let tmp = full.with_file_name(format!(".{}.partial", full.file_name().and_then(|n| n.to_str()).unwrap_or("file"))); + std::fs::write(&tmp, bytes)?; + std::fs::rename(&tmp, &full)?; + self.sha.insert(path.to_string(), crate::sha256_hex(bytes)); + Ok(()) + } + fn put_from(&mut self, path: &str, from: &FileSet, name: &str, sha256: &str, link: bool) -> std::io::Result<()> { + let full = self.place(path)?; + let tmp = full.with_file_name(format!(".{}.partial", full.file_name().and_then(|n| n.to_str()).unwrap_or("file"))); + let linked = link && from.path_of(name).is_some_and(|src| std::fs::hard_link(src, &tmp).is_ok()); + if !linked { + let mut r = from.open(name)?; + let mut f = std::fs::File::create(&tmp)?; + std::io::copy(&mut r, &mut f)?; + } + // The file is held to the digest the caller checked, whichever way it came. + if sha256_of(std::fs::File::open(&tmp)?)? != sha256 { + let _ = std::fs::remove_file(&tmp); + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{name} changed while it was read"))); + } + std::fs::rename(&tmp, &full)?; + self.sha.insert(path.to_string(), sha256.to_string()); + Ok(()) + } + fn has(&self, path: &str) -> bool { + self.sha.contains_key(path) + } + fn get(&self, path: &str) -> std::io::Result> { + std::fs::read(self.root.join(path)) + } + fn sha256s(&self) -> BTreeMap { + self.sha.clone() + } +} + +impl Drop for DirSink { + fn drop(&mut self) { + if self.kept { + return; + } + for p in self.sha.keys() { + let _ = std::fs::remove_file(self.root.join(p)); + } + for d in self.dirs.iter().rev() { + let _ = std::fs::remove_dir(d); + } + if self.made_root { + let _ = std::fs::remove_dir(&self.root); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_directory_and_memory_list_alike() { + let d = std::env::temp_dir().join(format!("wai-pack-files-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(d.join("a")).unwrap(); + std::fs::write(d.join("a/x.bin"), b"xyz").unwrap(); + std::fs::write(d.join("b.obu"), b"12").unwrap(); + std::fs::write(d.join(".hidden"), b"skipped").unwrap(); + std::fs::write(d.join("set.sha256"), b"skipped").unwrap(); + let m: BTreeMap> = [("a/x.bin".to_string(), b"xyz".to_vec()), ("b.obu".to_string(), b"12".to_vec())].into(); + let (ld, _) = FileSet::Dir(d.clone()).listing().unwrap(); + let (lm, _) = FileSet::Memory(m.clone()).listing().unwrap(); + assert_eq!(ld, lm); + let view: BTreeMap = m.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); + assert_eq!(ld, wai::pin::listing_from(&view).unwrap(), "the listing wai::pin writes"); + std::fs::remove_dir_all(&d).unwrap(); + } + + #[test] + fn a_dropped_dir_sink_leaves_nothing() { + let d = std::env::temp_dir().join(format!("wai-pack-sink-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&d); + { + let mut s = DirSink::new(&d).unwrap(); + s.put("objects/a.wai", b"1").unwrap(); + s.put("floor/v/init.mp4", b"2").unwrap(); + assert!(d.join("floor/v/init.mp4").is_file()); + } + assert!(!d.exists(), "a refused pack leaves no directory it made"); + std::fs::create_dir_all(&d).unwrap(); + { + let mut s = DirSink::new(&d).unwrap(); + s.put("x/y.txt", b"3").unwrap(); + } + assert!(d.is_dir() && std::fs::read_dir(&d).unwrap().next().is_none(), "and an empty one it was given stays empty"); + let mut s = DirSink::new(&d).unwrap(); + s.put("x/y.txt", b"3").unwrap(); + s.keep(); + assert_eq!(std::fs::read(d.join("x/y.txt")).unwrap(), b"3"); + std::fs::remove_dir_all(&d).unwrap(); + } +} diff --git a/wai/wai-pack/src/floor.rs b/wai/wai-pack/src/floor.rs index b47c1b180..9522617a0 100644 --- a/wai/wai-pack/src/floor.rs +++ b/wai/wai-pack/src/floor.rs @@ -1,10 +1,11 @@ -//! A supplied floor ([packaging] §7.3): a directory of AV1 streams, one per video -//! component, named `.obu` in the low-overhead OBU format, whose listing -//! (SPEC §3.1) the policy pins by SHA-256 (`floor.supplied_listing`). The floor is checked -//! against the plan before anything is written: +//! A supplied floor ([packaging] §7.3): a directory of AV1 streams, one per floor rung of +//! a video component, named `.obu` (§7.1) in the low-overhead OBU format, +//! whose listing (SPEC §3.1) the policy pins by SHA-256 (`floor.supplied_listing`). The +//! floor is checked against the plan before anything is written, each stream read a +//! temporal unit at a time ([`check_stream`], [`check_keyframes`]): //! //! - the directory's listing hashes to `floor.supplied_listing` (`floor-listing-mismatch`); -//! - the component's stream is in it (`floor-stream-missing`) and parses +//! - each floor rung's stream is in it (`floor-stream-missing`) and parses //! (`floor-av1-malformed`): temporal units, a sequence header in every unit that starts //! at a key frame, every sequence header the same; //! - it has one temporal unit per source frame (`floor-frame-count`); @@ -54,41 +55,222 @@ pub struct Floor { /// The SHA-256 of a set's listing, as 64 lowercase hexadecimal digits. pub fn listing_sha256(files: &BTreeMap>) -> Result { - let view: BTreeMap = files.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); - let listing = wai::pin::listing_from(&view).map_err(|e| FloorReason { code: "floor-listing-mismatch", detail: e.to_string() })?; - Ok(crate::sha256_hex(&listing)) + Ok(set_listing(&crate::files::FileSet::Memory(files.clone()))?.0) } -/// Check component `component`'s supplied floor against its plan: `starts` (the piece -/// starts, in frames) of a source of `frames` frames. -pub fn floor_supplied(files: &BTreeMap>, listing: Option<&str>, component: &str, starts: &[u64], frames: u64, extra_keyframes: bool) -> Result { - let got = listing_sha256(files)?; - if listing != Some(got.as_str()) { - return refuse("floor-listing-mismatch", format!("the supplied floor's listing is {got}, the policy pins {}", listing.unwrap_or("none"))); - } - let name = format!("{component}.obu"); - let stream = files.get(&name).ok_or_else(|| FloorReason { code: "floor-stream-missing", detail: format!("no {name} in the supplied floor") })?; - let malformed = |e: av1::Av1Error| FloorReason { code: "floor-av1-malformed", detail: e.0 }; - let tus = av1::temporal_units(stream).map_err(malformed)?; - let first = tus[0].sequence_header().ok_or_else(|| FloorReason { code: "floor-av1-malformed", detail: "the first temporal unit carries no sequence header".into() })?; - let seq_obu = first.bytes.to_vec(); - let seq = SequenceHeader::parse(first.payload).map_err(malformed)?; - if tus.len() as u64 != frames { - return refuse("floor-frame-count", format!("{} temporal units for {frames} source frames", tus.len())); - } - let mut key = Vec::with_capacity(tus.len()); - for (i, tu) in tus.iter().enumerate() { - if let Some(s) = tu.sequence_header() - && s.bytes != seq_obu.as_slice() - { - return refuse("floor-av1-malformed", format!("temporal unit {i} carries another sequence header")); +/// The SHA-256 of a file set's listing, as 64 lowercase hexadecimal digits, and each +/// file's SHA-256 by path, each file read once as a stream. +pub fn set_listing(files: &crate::files::FileSet) -> Result<(String, BTreeMap), FloorReason> { + let (listing, shas) = files.listing().map_err(|e| FloorReason { code: "floor-listing-mismatch", detail: e })?; + Ok((crate::sha256_hex(&listing), shas)) +} + +/// What the first read of one supplied stream gives: its sequence header and the OBU that +/// carries it, which of its units start at a shown key frame, and its unit count. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StreamCheck { + pub seq: SequenceHeader, + pub seq_obu: Vec, + /// One per temporal unit: whether it starts at a shown key frame. + pub key: Vec, +} + +/// §7.3's checks of a floor's temporal units that need no plan, a unit at a time: each a +/// temporal unit holding a frame, the first carrying a sequence header, every sequence +/// header the first's bytes, and every unit that starts at a shown key frame carrying one +/// (`floor-av1-malformed`). A unit that is not one temporal unit, or a first unit without +/// a sequence header, is refused when it is offered; a unit's other faults are refused at +/// [`UnitChecker::finish`], after the unit count, in unit order, as a reader of the whole +/// stream refuses them. +#[derive(Debug, Default)] +pub struct UnitChecker { + seq: Option<(SequenceHeader, Vec)>, + key: Vec, + later: Option, +} + +impl UnitChecker { + /// The next unit, as the stream holds it. + pub fn push(&mut self, u: &[u8]) -> Result<(), FloorReason> { + let malformed = |e: av1::Av1Error| FloorReason { code: "floor-av1-malformed", detail: e.0 }; + let i = self.key.len(); + let tus = av1::temporal_units(u).map_err(malformed)?; + let [tu] = tus.as_slice() else { return refuse("floor-av1-malformed", format!("unit {i} is not one temporal unit")) }; + if self.seq.is_none() { + let first = tu.sequence_header().ok_or_else(|| FloorReason { code: "floor-av1-malformed", detail: "the first temporal unit carries no sequence header".into() })?; + self.seq = Some((SequenceHeader::parse(first.payload).map_err(malformed)?, first.bytes.to_vec())); } - let k = av1::starts_with_shown_key_frame(tu, &seq).map_err(malformed)?; - if k && tu.sequence_header().is_none() { - return refuse("floor-av1-malformed", format!("temporal unit {i} starts a key frame without a sequence header")); + let (s, obu) = self.seq.as_ref().expect("set"); + let mut k = false; + if self.later.is_none() { + if let Some(h) = tu.sequence_header() + && h.bytes != obu.as_slice() + { + self.later = Some(FloorReason { code: "floor-av1-malformed", detail: format!("temporal unit {i} carries another sequence header") }); + } else { + match av1::starts_with_shown_key_frame(tu, s) { + Err(e) => self.later = Some(malformed(e)), + Ok(true) if tu.sequence_header().is_none() => { + self.later = Some(FloorReason { code: "floor-av1-malformed", detail: format!("temporal unit {i} starts a key frame without a sequence header") }) + } + Ok(x) => k = x, + } + } } - key.push(k); + self.key.push(k); + Ok(()) } + + /// The units offered so far. + pub fn count(&self) -> u64 { + self.key.len() as u64 + } + + /// One unit per source frame (`floor-frame-count`), then the faults held back. + pub fn finish(self, frames: u64) -> Result { + let (seq, seq_obu) = self.seq.ok_or_else(|| FloorReason { code: "floor-av1-malformed", detail: "no temporal unit".into() })?; + if self.key.len() as u64 != frames { + return refuse("floor-frame-count", format!("{} temporal units for {frames} source frames", self.key.len())); + } + if let Some(r) = self.later { + return Err(r); + } + Ok(StreamCheck { seq, seq_obu, key: self.key }) + } +} + +/// §7.3's checks of one supplied stream that need no plan, read a unit at a time +/// ([`UnitChecker`]), and its unit count (`floor-frame-count`): the source's frames, or, +/// for `None`, the stream decides it (a floor-only component's first floor rung, §7.8). +/// An I/O failure is `io`. +pub fn check_stream(r: R, frames: Option) -> Result { + let mut rd = av1::TuReader::new(r); + let read = |e: av1::ReadError| match e { + av1::ReadError::Malformed(m) => FloorReason { code: "floor-av1-malformed", detail: m.0 }, + av1::ReadError::Io(e) => FloorReason { code: "io", detail: e.to_string() }, + }; + let mut c = UnitChecker::default(); + while let Some(u) = rd.next_unit().map_err(read)? { + c.push(&u)?; + } + let n = c.count(); + c.finish(frames.unwrap_or(n)) +} + +/// A floor-only component's pieces (§7.8): one from each start of `starts` to the next (the +/// last to the floor's end, `frames`), each at most `max` units, the policy's `max_ms` on +/// the component's timebase (`floor-piece-length`, §4.4). +pub fn floor_pieces(starts: &[u64], frames: u64, max: u64) -> Result, FloorReason> { + let mut out = Vec::with_capacity(starts.len()); + for (k, &s) in starts.iter().enumerate() { + let e = starts.get(k + 1).copied().unwrap_or(frames); + if e - s > max { + return refuse("floor-piece-length", format!("piece {k}: {} frames, past the policy's max_ms ({max} units)", e - s)); + } + out.push(crate::scenecut::Piece { start: s, len: e - s, flags: 0, stats: crate::scenecut::PieceStats::default() }); + } + Ok(out) +} + +/// One media segment of a supplied CMAF floor (§7.7): its file in the supplied floor, its +/// SHA-256, its first frame and its frames. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CmafSegment { + pub file: String, + pub sha256: String, + pub start: u64, + pub frames: u64, +} + +/// A floor rung's supplied CMAF floor, checked (§7.7): its units' check, its +/// initialization segment's file and SHA-256, and its media segments. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CmafCheck { + pub units: StreamCheck, + pub init: crate::cmaf_read::Init, + pub init_file: String, + pub init_sha256: String, + pub segments: Vec, +} + +fn cmaf_fault(f: crate::cmaf_read::CmafFault, file: &str) -> FloorReason { + FloorReason { code: f.code(), detail: format!("{file}: {}", f.detail()) } +} + +/// The files of a supplied CMAF floor of floor name `name` (§7.7): `/init.mp4` +/// (`floor-stream-missing` without it) and its media segments, the other files of +/// `/`, each `*.m4s` and in no directory below it (`floor-cmaf-malformed`), in byte +/// order of their names, at least one. +pub fn cmaf_files_of(shas: &BTreeMap, name: &str) -> Result<(String, Vec), FloorReason> { + let init = format!("{name}/init.mp4"); + if !shas.contains_key(&init) { + return refuse("floor-stream-missing", format!("no {init} in the supplied floor")); + } + let prefix = format!("{name}/"); + let mut segs = Vec::new(); + for f in shas.keys().filter(|k| k.starts_with(&prefix) && **k != init) { + let rest = &f[prefix.len()..]; + if rest.contains('/') || !rest.ends_with(".m4s") { + return refuse("floor-cmaf-malformed", format!("{f}: not a media segment of {prefix}")); + } + segs.push(f.clone()); + } + if segs.is_empty() { + return refuse("floor-cmaf-malformed", format!("no media segment in {prefix}")); + } + Ok((init, segs)) +} + +/// §7.7's checks of one floor rung's supplied CMAF floor that need no plan, a segment at a +/// time: the initialization segment's boxes; each media segment's boxes, its first sample +/// a sync sample (`floor-cmaf-malformed`), every sample one frame long and the segment +/// starting where the one before it ended, the first at frame 0 (`floor-cmaf-timing`); +/// and its samples, each a temporal unit without its temporal delimiter, as §7.3's +/// stream ([`UnitChecker`]). `frames` is the source's frame count, or `None` for a floor +/// whose units decide it (§7.8). +pub fn check_cmaf(set: &crate::files::FileSet, shas: &BTreeMap, name: &str, num: u32, den: u32, frames: Option) -> Result { + let io = |f: &str, e: std::io::Error| FloorReason { code: "io", detail: format!("{f}: {e}") }; + let (init_file, segs) = cmaf_files_of(shas, name)?; + let ib = set.read(&init_file).map_err(|e| io(&init_file, e))?; + let init = crate::cmaf_read::read_init(&ib).map_err(|f| cmaf_fault(f, &init_file))?; + let mut c = UnitChecker::default(); + let mut segments = Vec::with_capacity(segs.len()); + for f in segs { + let b = set.read(&f).map_err(|e| io(&f, e))?; + let samples = crate::cmaf_read::read_segment(&b, &init).map_err(|x| cmaf_fault(x, &f))?; + if samples[0].flags & crate::cmaf_read::NON_SYNC_BIT != 0 { + return refuse("floor-cmaf-malformed", format!("{f}: a segment that does not open on a sync sample")); + } + let start = c.count(); + crate::cmaf_read::check_times(&samples, init.timescale, num, den, start).map_err(|x| cmaf_fault(x, &f))?; + for u in crate::cmaf_read::units(&b, &samples) { + c.push(&u)?; + } + segments.push(CmafSegment { sha256: shas[&f].clone(), file: f, start, frames: samples.len() as u64 }); + } + let n = c.count(); + let units = c.finish(frames.unwrap_or(n))?; + Ok(CmafCheck { units, init, init_sha256: shas[&init_file].clone(), init_file, segments }) +} + +/// §7.7: one media segment per piece of the plan `starts` (of a source of `frames` +/// frames), each holding exactly its piece's frames (`floor-cmaf-pieces`). +pub fn check_cmaf_pieces(segments: &[CmafSegment], starts: &[u64], frames: u64) -> Result<(), FloorReason> { + if segments.len() != starts.len() { + return refuse("floor-cmaf-pieces", format!("{} media segments for {} pieces", segments.len(), starts.len())); + } + for (k, (s, &start)) in segments.iter().zip(starts).enumerate() { + let end = starts.get(k + 1).copied().unwrap_or(frames); + if (s.start, s.frames) != (start, end - start) { + return refuse("floor-cmaf-pieces", format!("{}: frames {} to {}, piece {k} is {start} to {end}", s.file, s.start, s.start + s.frames)); + } + } + Ok(()) +} + +/// §7.3: a unit starts at a shown key frame at every piece start of the plan `starts`, +/// and at no other frame unless `extra_keyframes` (`floor-keyframe-mismatch`). +pub fn check_keyframes(key: &[bool], starts: &[u64], extra_keyframes: bool) -> Result<(), FloorReason> { for (i, &k) in key.iter().enumerate() { let at_start = starts.binary_search(&(i as u64)).is_ok(); if at_start && !k { @@ -98,13 +280,7 @@ pub fn floor_supplied(files: &BTreeMap>, listing: Option<&str>, return refuse("floor-keyframe-mismatch", format!("a key frame at {i}, inside a piece")); } } - let mut pieces = Vec::with_capacity(starts.len()); - for (p, &s) in starts.iter().enumerate() { - let e = starts.get(p + 1).copied().unwrap_or(frames); - let (s, e) = (s as usize, e as usize); - pieces.push(FloorPiece { units: tus[s..e].iter().map(av1::TemporalUnit::bytes).collect(), key: key[s..e].to_vec() }); - } - Ok(Floor { seq, seq_obu, pieces }) + Ok(()) } /// The payload of a `wai.video.av1` object as the reference sink decodes it diff --git a/wai/wai-pack/src/golden.rs b/wai/wai-pack/src/golden.rs index f1e5804d7..64c9e0530 100644 --- a/wai/wai-pack/src/golden.rs +++ b/wai/wai-pack/src/golden.rs @@ -31,7 +31,7 @@ use wai::stage_index::{self as wsix, ClassRec, EntryRec, Header, MediaClass, Pie use wai::wai_video_manifest::EnergyProvenance; /// The golden packs. -pub const PACKS: &[&str] = &["video-base-01", "video-staged-01"]; +pub const PACKS: &[&str] = &["video-base-01", "video-staged-01", "video-ladder-01", "video-repack-01", "video-cmaf-01", "video-floor-only-01", "video-floor-only-02"]; type Files = BTreeMap>; @@ -52,23 +52,45 @@ fn read_key(p: &Path) -> std::io::Result { Ok(SigningKey::from_bytes(&seed)) } -/// Pack golden `name` from its inputs under `dir/packs/`. -pub fn pack_golden(dir: &Path, name: &str) -> std::io::Result { +/// A golden's inputs under `dir/packs/`: its policy's bytes, each component's +/// fixture by id, and the options it packs with (its key, its supplied floor, the parent +/// its file `parent` names). +pub fn golden_inputs(dir: &Path, name: &str) -> std::io::Result<(Vec, Files, crate::pack::PackOpts)> { let d = dir.join("packs").join(name); let policy = std::fs::read(d.join("policy.toml"))?; let floor = wai::pin::read_dir_set(&d.join("floor")).map_err(io)?; let key = read_key(&d.join("key.hex"))?; let p = crate::policy::Policy::parse_bytes(&policy).map_err(io)?; let mut sources = BTreeMap::new(); - for c in &p.components { + // A floor-only component has no source (§7.8). + for c in p.components.iter().filter(|c| !c.floor_only()) { let src = c.source.as_deref().and_then(|s| s.strip_suffix(".y4m")).ok_or_else(|| io(format!("component {} names no fixture", c.id)))?; sources.insert(c.id.clone(), crate::fixture::generate(src).ok_or_else(|| io(format!("no fixture {src}")))?); } let signer_id = wai::signer_id::did_key(&key.verifying_key().to_bytes()); let mut o = crate::pack::PackOpts::new(key, signer_id, floor); + // A re-pack golden names its parent golden in `parent` (§11.5). + if let Some(parent) = parent_of(dir, name)? { + o.base_from = Some(crate::files::FileSet::Memory(read_tree(&dir.join("packs").join(parent).join("package"))?)); + } + Ok((policy, sources, o)) +} + +/// Pack golden `name` from its inputs under `dir/packs/`. +pub fn pack_golden(dir: &Path, name: &str) -> std::io::Result { + let (policy, sources, mut o) = golden_inputs(dir, name)?; crate::pack::pack(&policy, &sources, &mut o).map_err(io) } +/// The golden a re-pack golden takes its bases from, named in its `parent` file. +pub fn parent_of(dir: &Path, name: &str) -> std::io::Result> { + match std::fs::read_to_string(dir.join("packs").join(name).join("parent")) { + Ok(t) => Ok(Some(t.trim().to_string())), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(e), + } +} + // ─────────────────────────────── mutations ─────────────────────────────── fn env(cap: &str, refines: Option<(&[u8; 32], &[u8; 32], u8)>, derived: bool, payload: &[u8]) -> Vec { @@ -206,6 +228,18 @@ fn index_cases() -> Vec { ("floor-before-refinement", "entry-order", |ix| ix.entries.swap(2, 3)), ("deadline-on-base", "deadline-role", |ix| ix.entries[0].deadline_ms = 5), ("floor-without-derived-from", "floor-derived-from", |ix| ix.entries[3].flags = 0), + // A piece of floors alone (a floor-only component's, §7.8): its floor presents no + // base, so it carries no derived_from, and one that does is refused. + ("floor-only-piece", "ok", |ix| { + ix.entries.remove(4); + ix.pieces[1].n_entries = 1; + ix.entries[4].flags = 0; + }), + ("floor-only-piece-with-derived-from", "floor-derived-from", |ix| { + ix.entries.remove(4); + ix.pieces[1].n_entries = 1; + }), + ("piece-kind-none", "ok", |ix| ix.pieces[0].kind = wsix::PIECE_KIND_NONE), ("entry-flag-unknown", "entry-flags", |ix| ix.entries[0].flags |= 0x80), ("piece-gap", "piece-order", |ix| ix.pieces[1].start = 51), ("piece-empty", "piece-order", |ix| ix.pieces[0].duration = 0), @@ -543,37 +577,51 @@ pub fn reseal_as(files: &mut Files, key: &SigningKey, policy_json: Option<&[u8]> } } } - let mut pieces = Vec::new(); - let mut seq: Option<(crate::av1::SequenceHeader, Vec)> = None; - for p in 0..ix.pieces.len() { - let Some(fe) = ix.piece_entries(p).iter().find(|e| e.role == Role::Floor) else { break }; - let w = Wai::from_bytes(objects.get(&fe.content_hash).ok_or("a floor object is not held")?).map_err(|e| e.to_string())?; - let (units, _) = crate::floor::av1_units(&w.payload).ok_or("a floor payload is not the framing")?; - let mut key_frames = Vec::new(); - for u in &units { - let tus = crate::av1::temporal_units(u).map_err(|e| e.0)?; - let tu = tus.first().ok_or("an empty unit")?; - if seq.is_none() { - let s = tu.sequence_header().ok_or("no sequence header")?; - seq = Some((crate::av1::SequenceHeader::parse(s.payload).map_err(|e| e.0)?, s.bytes.to_vec())); - } - key_frames.push(crate::av1::starts_with_shown_key_frame(tu, &seq.as_ref().expect("set").0).map_err(|e| e.0)?); - } - pieces.push(crate::floor::FloorPiece { units, key: key_frames }); + // A supplied CMAF floor's files are the ladder's own (§7.7): kept as they are. The OBU + // form's are rebuilt from the floor objects (§7.6). + let cmaf = policy.floor.cmaf(); + if !cmaf { + files.retain(|k, _| !k.starts_with("floor/")); } - files.retain(|k, _| !k.starts_with("floor/")); files.retain(|k, _| !["hls/", "dash/", "moq/", "stages/"].iter().any(|d| k.starts_with(d))); - let mut floor_view = None; - if let Some((s, obu)) = seq { - let fl = crate::floor::Floor { seq: s, seq_obu: obu, pieces }; - let starts: Vec = ix.pieces.iter().map(|p| p.start).collect(); - let (ip, init, frags) = crate::pack::cmaf_files(&comp.id, &fl, &starts, tb.den, tb.num)?; - floor_view = Some(FloorView { codec: fl.seq.codec_string(), w: fl.seq.max_frame_width, h: fl.seq.max_frame_height, init: ip.clone(), fragments: frags.iter().map(|(p, b)| (p.clone(), b.len() as u64)).collect() }); - files.insert(ip, init); - files.extend(frags); + // Each floor rung's floor, from the k-th floor entry of each piece (§5.7). + let mut floor_views = Vec::new(); + for (k, (_, name)) in crate::pack::floor_rungs(&policy, &comp.id).into_iter().enumerate() { + let mut pieces = Vec::new(); + let mut seq: Option<(crate::av1::SequenceHeader, Vec)> = None; + for p in 0..ix.pieces.len() { + let Some(fe) = ix.piece_entries(p).iter().filter(|e| e.role == Role::Floor).nth(k) else { break }; + let w = Wai::from_bytes(objects.get(&fe.content_hash).ok_or("a floor object is not held")?).map_err(|e| e.to_string())?; + let (units, _) = crate::floor::av1_units(&w.payload).ok_or("a floor payload is not the framing")?; + let mut key_frames = Vec::new(); + for u in &units { + let tus = crate::av1::temporal_units(u).map_err(|e| e.0)?; + let tu = tus.first().ok_or("an empty unit")?; + if seq.is_none() { + let s = tu.sequence_header().ok_or("no sequence header")?; + seq = Some((crate::av1::SequenceHeader::parse(s.payload).map_err(|e| e.0)?, s.bytes.to_vec())); + } + key_frames.push(crate::av1::starts_with_shown_key_frame(tu, &seq.as_ref().expect("set").0).map_err(|e| e.0)?); + } + pieces.push(crate::floor::FloorPiece { units, key: key_frames }); + } + if let Some((s, obu)) = seq { + let fl = crate::floor::Floor { seq: s, seq_obu: obu, pieces }; + if cmaf { + if let Ok((_, frags)) = crate::verify::cmaf_supplied(files, &policy, &name, &ix, tb, &fl.pieces) { + floor_views.push(FloorView { name: name.clone(), codec: fl.seq.codec_string(), w: fl.seq.max_frame_width, h: fl.seq.max_frame_height, init: format!("floor/{name}/init.mp4"), fragments: frags }); + } + continue; + } + let starts: Vec = ix.pieces.iter().map(|p| p.start).collect(); + let (ip, init, frags) = crate::pack::cmaf_files(&name, &fl, &starts, tb.den, tb.num)?; + floor_views.push(FloorView { name, codec: fl.seq.codec_string(), w: fl.seq.max_frame_width, h: fl.seq.max_frame_height, init: ip.clone(), fragments: frags.iter().map(|(p, b)| (p.clone(), b.len() as u64)).collect() }); + files.insert(ip, init); + files.extend(frags); + } } let (w, h) = dims.unwrap_or((0, 0)); - let view = CompView { index: &ix, tb, w, h, objects: &objects, floor: floor_view, base_cap: crate::catalog::default_base_cap(&policy, &comp.id) }; + let view = CompView { index: &ix, tb, w, h, objects: &objects, floors: floor_views, base_cap: crate::catalog::default_base_cap(&policy, &comp.id) }; if let Ok(cats) = crate::catalog::render_all(&view, &policy, &policy_digest) { files.extend(cats); } @@ -584,6 +632,65 @@ pub fn reseal_as(files: &mut Files, key: &SigningKey, policy_json: Option<&[u8]> Ok(()) } +/// What a metered package carries as its measurement claim (§9.4). +#[derive(Clone, Copy, PartialEq, Eq)] +enum Claim { + /// The claim the packer writes. + Written, + /// A claim about a figure one microjoule off, signed by the package key. + AnotherFigure, + /// No claim. + None, +} + +/// The golden re-sealed as a metered pack (§9.4): a model meter of 500 mW over one second, +/// so the figure is 500,000 µJ, `ModelBased`, and its measurement claim, every member of +/// which is fixed (the build record names a release build and nothing it cannot know here, +/// `issued_at` is fixed), so the files are the same on every machine. +fn metered(g: &Files, key: &SigningKey, claim: Claim) -> Files { + use wai::claim_receipt::{ClaimBody, ClaimReceipt, FigureHeader, MeasurementClaim, SubjectType}; + use wai::energy_figure::EnergyFigures; + use wai::energy_meter::Meter as _; + let mut f = g.clone(); + let policy = crate::policy::Policy::parse_bytes(&f["policy.toml"]).expect("the golden's policy"); + let comp = &policy.components[0]; + let ixb = f[&wai::package::index_file(&comp.id)].clone(); + let mut t = 0u64; + let mut meter = wai::energy_meter::ModelMeter::new(move || { + let v = t; + t += 1_000_000; + v + }, 500); + meter.start(); + let energy = meter.stop().expect("a figure").as_seal_args(); + let did = wai::signer_id::did_key(&key.verifying_key().to_bytes()); + let r = PackageReceiptV2::seal(key, &did, &policy.asset_id, policy.digest(), &[(comp.id.clone(), ixb.clone())], &crate::pack::packager_id(), energy, GrantRef::unbounded("wai.video.package"), Parent::None).expect("the metered receipt seals"); + f.insert("package.receipt.json".into(), r.to_json().into_bytes()); + let indexes: Vec<&[u8]> = vec![&ixb]; + let view = (&r, indexes.as_slice()).figure_view().expect("the receipt verifies against its index"); + let build = wai::energy_report::BuildRecord { profile: wai::energy_report::BuildProfile::Release, toolchain: None, target: None }; + let issued = "2026-10-21T00:00:00Z"; + let written = match claim { + Claim::Written => Some(wai::energy_figure::meter_report::emit(&view, None, None, build, "model:500mW", key, &did, None, issued).expect("the claim checks against the receipt")), + Claim::AnotherFigure => { + let fig = view.figure(None).expect("a figure"); + let body = ClaimBody::Measurement(MeasurementClaim { + figure: FigureHeader::new(&view.profile, None, None, fig.joules_micro + 1, fig.class.as_ref().expect("a class")), + bracket: None, + build, + power_reader: Some("model:500mW".into()), + }); + Some(ClaimReceipt::seal_unchecked(key, &did, SubjectType::Receipt, view.identity, 0, issued, body, None).expect("the claim seals")) + } + Claim::None => None, + }; + if let Some(c) = written { + f.insert(crate::pack::claim_path(&r), c.to_json().into_bytes()); + } + relist(&mut f); + f +} + /// Write `LISTING` again over every other file. pub fn relist(files: &mut Files) { files.remove("LISTING"); @@ -621,8 +728,8 @@ pub fn catalogs() -> (Files, Value) { let (mut ix, objs) = layered(); m(&mut ix); let objects: BTreeMap<[u8; 32], Vec> = objs.iter().map(|o| (objects::object_hash(o), o.clone())).collect(); - let floor = FloorView { codec: "av01.0.00M.08".into(), w: 64, h: 36, init: "floor/v/init.mp4".into(), fragments: Vec::new() }; - let v = CompView { index: &ix, tb: crate::policy::Timebase { num: 25, den: 1 }, w: 8, h: 8, objects: &objects, floor: Some(floor), base_cap: "wai.video.int_motion".into() }; + let floor = FloorView { name: "v".into(), codec: "av01.0.00M.08".into(), w: 64, h: 36, init: "floor/v/init.mp4".into(), fragments: Vec::new() }; + let v = CompView { index: &ix, tb: crate::policy::Timebase { num: 25, den: 1 }, w: 8, h: 8, objects: &objects, floors: vec![floor], base_cap: "wai.video.int_motion".into() }; let moq = render_moq_catalog(&v, CATALOG_NAMESPACE, &lag, deadline).expect("a catalog"); let path = format!("catalogs/{name}.wsix"); files.insert(path.clone(), ix.to_bytes()); @@ -924,6 +1031,11 @@ pub fn package_mutations(g: &Files, key: &SigningKey) -> Vec { f.insert("package.receipt.json".into(), bad.into_bytes()); add(name, "receipt-json", listed(f)); } + // A metered package (§9.4): its claim as the packer writes it, which every verifier + // accepts; a claim about another figure, signed by the package key; and no claim. + add("metered", "ok", metered(g, key, Claim::Written)); + add("metered-claim-another-figure", "measurement-claim-invalid", metered(g, key, Claim::AnotherFigure)); + add("metered-claim-missing", "measurement-claim-missing", metered(g, key, Claim::None)); // Directories (§11.1, "no other"): one that holds nothing, one that holds nothing // under a dot-name (a version-control directory's), and one that cannot be read. let dir = |name: &'static str, empty: &[&str], unreadable: &[&str]| PackageMutation { @@ -966,7 +1078,7 @@ fn prefix_frames(f: &Files, ix: &StageIndex, k: usize) -> Vec> { } /// Staged packages a verifier must refuse (§11.3), each the staged golden altered and -/// re-sealed with its key: a refinement on another deadline than its plan's (§8.6); a +/// re-sealed with its key, and the golden itself, which it accepts: a refinement on another deadline than its plan's (§8.6); a /// piece missing its top layer, and the static piece's still where the policy's rule now /// gives it the default ladder (§5.7); a top layer coded again at another step, its /// prefix digest left as it was; and the still's prefix digest of its one frame, not of @@ -978,6 +1090,9 @@ pub fn staged_mutations(g: &Files, key: &SigningKey) -> Vec { }; let mut out: Vec = Vec::new(); let mut add = |name: &'static str, reason: &str, files: Files| out.push(PackageMutation { name, reason: reason.into(), files, empty_dirs: Vec::new(), unreadable_dirs: Vec::new() }); + // The staged golden itself, verified without its sources: every layer's prefix digest + // is held to it (a verifier's decode of the layers, however it runs, is checked). + add("ok", "ok", g.clone()); let ix = golden_index(g); let last = ix.pieces.len() - 1; let top = |ix: &StageIndex, p: usize| ix.pieces[p].first_entry as usize + ix.piece_entries(p).iter().rposition(|e| e.role == Role::Refinement).expect("a layer"); @@ -1207,6 +1322,92 @@ pub fn staged_overlays(g: &Files, key: &SigningKey) -> (Files, Value) { overlays("staged-mutations", STAGED_PACK, g, staged_mutations(g, key)) } +/// The ladder golden: two floor rungs over one base (§7.1), which the ladder mutations +/// alter. +pub const LADDER_PACK: &str = "video-ladder-01"; + +/// Ladder packages a verifier must refuse or accept (§11.3), each the ladder golden +/// altered and re-sealed with its key where the receipt or `LISTING` alone would catch +/// it: the golden itself (`ok`); piece 2's two floor objects swapped in the index, so the +/// first floor rung's place holds an object framed for the second's size +/// (`floor-object-mismatch`); the second floor's CMAF files under the component's name +/// rather than its floor name (`floor-cmaf-mismatch`); and a classical master that lists +/// one floor of two (`catalog-mismatch`). +pub fn ladder_mutations(g: &Files, key: &SigningKey) -> Vec { + let mut out: Vec = Vec::new(); + let mut add = |name: &'static str, reason: &str, files: Files| out.push(PackageMutation { name, reason: reason.into(), files, empty_dirs: Vec::new(), unreadable_dirs: Vec::new() }); + add("ok", "ok", g.clone()); + let mut f = g.clone(); + let mut x = golden_index(&f); + let at = x.pieces[2].first_entry as usize; + x.entries.swap(at + 1, at + 2); + set_golden_index(&mut f, &x); + reseal(&mut f, key).expect("the alteration re-seals"); + add("floors-swapped", "floor-object-mismatch", f); + let mut f = g.clone(); + let moved: Vec = f.keys().filter(|k| k.starts_with("floor/lo/")).cloned().collect(); + for k in moved { + let b = f.remove(&k).expect("held"); + f.insert(k.replacen("floor/lo/", "floor/v/", 1), b); + } + relist(&mut f); + add("floor-under-the-component-s-name", "floor-cmaf-mismatch", f); + let mut f = g.clone(); + let master = String::from_utf8(f["hls/classic/master.m3u8"].clone()).expect("UTF-8"); + let one: Vec<&str> = master.lines().take(5).collect(); + f.insert("hls/classic/master.m3u8".into(), (one.join("\n") + "\n").into_bytes()); + relist(&mut f); + add("master-lists-one-floor", "catalog-mismatch", f); + out +} + +/// The ladder mutations as overlays on the ladder golden (`ladder-mutations/`), in the +/// layout of [`package_overlays`]. +pub fn ladder_overlays(g: &Files, key: &SigningKey) -> (Files, Value) { + overlays("ladder-mutations", LADDER_PACK, g, ladder_mutations(g, key)) +} + +/// The golden of a supplied floor in the CMAF form (§7.7), which the CMAF mutations alter. +pub const CMAF_PACK: &str = "video-cmaf-01"; + +/// CMAF packages a verifier must refuse or accept (§7.7, §11.3), each the CMAF golden +/// altered and re-sealed with its key where the receipt or `LISTING` alone would catch it: +/// the golden itself (`ok`); a media segment replaced by one of the same samples, a `free` +/// box before it, under its own SHA-256, which is not the supplied segment +/// (`floor-cmaf-mismatch`); `floor.listing` with a line's digest altered, and without it +/// (`floor-cmaf-mismatch`): the receipt binds the ladder's bytes through it. +pub fn cmaf_mutations(g: &Files, key: &SigningKey) -> Vec { + let mut out: Vec = Vec::new(); + let mut add = |name: &'static str, reason: &str, files: Files| out.push(PackageMutation { name, reason: reason.into(), files, empty_dirs: Vec::new(), unreadable_dirs: Vec::new() }); + add("ok", "ok", g.clone()); + let mut f = g.clone(); + let seg = f.keys().find(|k| k.starts_with("floor/hi/") && k.ends_with(".m4s")).expect("a segment").clone(); + let mut b = vec![0, 0, 0, 8, b'f', b'r', b'e', b'e']; + b.extend_from_slice(&f[&seg]); + f.remove(&seg); + f.insert(format!("floor/hi/{}.m4s", crate::sha256_hex(&b)), b); + reseal(&mut f, key).expect("the alteration re-seals"); + add("segment-not-the-supplied-one", "floor-cmaf-mismatch", f); + let mut f = g.clone(); + let mut l = f["floor.listing"].clone(); + let at = l.len() - 2 - "lo/seg_0004.m4s".len() - 64; + l[at] = if l[at] == b'0' { b'1' } else { b'0' }; + f.insert("floor.listing".into(), l); + relist(&mut f); + add("floor-listing-altered", "floor-cmaf-mismatch", f); + let mut f = g.clone(); + f.remove("floor.listing"); + relist(&mut f); + add("floor-listing-missing", "floor-cmaf-mismatch", f); + out +} + +/// The CMAF mutations as overlays on the CMAF golden (`cmaf-mutations/`), in the layout of +/// [`package_overlays`]. +pub fn cmaf_overlays(g: &Files, key: &SigningKey) -> (Files, Value) { + overlays("cmaf-mutations", CMAF_PACK, g, cmaf_mutations(g, key)) +} + fn overlays(dir: &str, pack: &str, g: &Files, mutations: Vec) -> (Files, Value) { let mut files = BTreeMap::new(); let mut cases = Vec::new(); @@ -1645,15 +1846,21 @@ pub fn regen(dir: &Path) -> std::io::Result<()> { let dl = dir.join("delivery"); std::fs::create_dir_all(&dl)?; write_json(&dl.join("expected.json"), &delivery(dir)?)?; - let golden = read_tree(&dir.join("packs").join(STAGED_PACK).join("package"))?; - let key = read_key(&dir.join("packs").join(STAGED_PACK).join("key.hex"))?; - let (files, expected) = staged_overlays(&golden, &key); - let sm = dir.join("staged-mutations"); - if sm.exists() { - std::fs::remove_dir_all(&sm)?; + for (sub, pack, f) in [ + ("staged-mutations", STAGED_PACK, staged_overlays as fn(&Files, &SigningKey) -> (Files, Value)), + ("ladder-mutations", LADDER_PACK, ladder_overlays), + ("cmaf-mutations", CMAF_PACK, cmaf_overlays), + ] { + let golden = read_tree(&dir.join("packs").join(pack).join("package"))?; + let key = read_key(&dir.join("packs").join(pack).join("key.hex"))?; + let (files, expected) = f(&golden, &key); + let sm = dir.join(sub); + if sm.exists() { + std::fs::remove_dir_all(&sm)?; + } + write_files(dir, &files)?; + write_json(&sm.join("expected.json"), &expected)?; } - write_files(dir, &files)?; - write_json(&sm.join("expected.json"), &expected)?; Ok(()) } @@ -1682,7 +1889,11 @@ pub fn check(dir: &Path) -> std::io::Result> { Ok(p) => same_tree(&format!("packs/{name}/package"), &p.files, &held, &mut out), Err(e) => out.push(format!("packs/{name}: {e}")), } - if let Err(v) = crate::verify::verify_files(&held) { + let verdict = match parent_of(dir, name)? { + None => crate::verify::verify_files(&held), + Some(parent) => crate::verify::verify_repack(&held, &read_tree(&dir.join("packs").join(parent).join("package"))?), + }; + if let Err(v) = verdict { for r in v { out.push(format!("packs/{name}/package: {r}")); } @@ -1771,6 +1982,8 @@ pub fn check(dir: &Path) -> std::io::Result> { for (sub, pack, f) in [ ("package-mutations", MUTATED_PACK, package_overlays as fn(&Files, &SigningKey) -> (Files, Value)), ("staged-mutations", STAGED_PACK, staged_overlays), + ("ladder-mutations", LADDER_PACK, ladder_overlays), + ("cmaf-mutations", CMAF_PACK, cmaf_overlays), ] { let golden = read_tree(&dir.join("packs").join(pack).join("package"))?; let key = read_key(&dir.join("packs").join(pack).join("key.hex"))?; diff --git a/wai/wai-pack/src/lib.rs b/wai/wai-pack/src/lib.rs index dfa81609d..fbd97740d 100644 --- a/wai/wai-pack/src/lib.rs +++ b/wai/wai-pack/src/lib.rs @@ -6,7 +6,8 @@ //! ([`scenecut`]), piece classification ([`classify`]), a piece's plan, ladder and class //! plan ([`plan`]), the base encode's choice of step and the refinement layers //! ([`encode`]), stills ([`png`]), the floor ([`floor`], [`av1`], [`cmaf`]), the objects -//! ([`objects`]), the catalogs ([`catalog`]), the packer ([`pack`]) and its verifier +//! ([`objects`]), the catalogs ([`catalog`]), where a packer's inputs come from and its +//! files go ([`files`]), the packer ([`pack`]) and its verifier //! ([`verify`]), the live packer ([`live`]), delivery over a simulated link //! ([`delivery`]), the synthetic fixtures ([`fixture`]), and the conformance corpus's //! regeneration and check ([`conformance`], [`golden`]). @@ -21,9 +22,11 @@ pub mod capcheck; pub mod catalog; pub mod classify; pub mod cmaf; +pub mod cmaf_read; pub mod conformance; pub mod delivery; pub mod encode; +pub mod files; pub mod fixture; pub mod floor; pub mod golden; diff --git a/wai/wai-pack/src/pack.rs b/wai/wai-pack/src/pack.rs index 9a5fe6a6c..e79024087 100644 --- a/wai/wai-pack/src/pack.rs +++ b/wai/wai-pack/src/pack.rs @@ -7,32 +7,49 @@ //! [`crate::plan::piece_ladder`]): a `wai.video.int_motion` base (its step chosen per //! piece by [`crate::encode::choose_q`]) or a still of `wai.image.png` ([`crate::png`]), //! the refinements of `wai.video.int_refine` over an `int_motion` base -//! ([`crate::encode::encode_refinement`], §5.4), and a supplied AV1 floor. Every piece is -//! coded by [`Coder`], which the live packer (`crate::live`) shares. What it does not pack -//! yet it refuses by name rather than leaving out: another medium +//! ([`crate::encode::encode_refinement`], §5.4), and one supplied AV1 floor per floor +//! rung (§7.1: several floor rungs carry a ladder of renditions over one base). Every +//! piece is coded by [`Coder`], which the live packer (`crate::live`) shares. What it does +//! not pack yet it refuses by name rather than leaving out: another medium //! (`pack-media-unsupported`), a floor to encode (`pack-floor-encode-unsupported`), a rung //! of a capability it does not encode or a refinement over a base it does not refine //! (`pack-capability-unsupported:`). Its own failures are refused by name too //! (`pack-encode`, `pack-piece-length`, `pack-catalog`, `pack-meter`, -//! `pack-measurement-claim`, `pack-listing`; packaging §11.4), and a package is written -//! only into a new or empty directory ([`out_dir_ready`], `pack-out-not-empty`). +//! `pack-measurement-claim`, `pack-listing`, `pack-source-changed`; packaging §11.4), and a +//! package is written only into a new or empty directory ([`out_dir_ready`], +//! `pack-out-not-empty`). +//! +//! It holds neither its source nor its floor whole. [`pack_into`] reads the source twice, +//! a frame at a time: once to cut its pieces (the segmenter holds a few frames' records, +//! §4.3), once to code them, a piece at a time; each floor stream is read a temporal unit +//! at a time, once to check it against the plan before anything is written (§7.3) and +//! once to cut it into the pieces' floor objects; and every file goes to a +//! [`crate::files::Sink`] as it is made, the catalogs reading objects back from it. What +//! it holds across a pack is a record per piece and per entry, and one byte per frame for +//! each floor's key frames; the frames of the pieces it is coding at once +//! ([`PackOpts::jobs`]); and one piece of each floor. [`pack`] is the same packer into +//! memory. //! //! The package: //! //! ```text //! policy.toml policy.json plan-.txt index-.wsix package.receipt.json //! objects/.wai … -//! floor//init.mp4 floor//.m4s … (CMAF; never in an index) +//! floor//init.mp4 floor//.m4s … (CMAF; never in an index) //! hls/classic/… hls//… dash/… moq/catalog.json stages/.json //! claims/.measurement.json (a metered pack) //! LISTING (SPEC §3.1, of every other file) //! ``` //! +//! where `` is each floor rung's floor name (§7.1): the component's id when it has one +//! floor rung, the rung's id when it has several. +//! //! [packaging]: ../../../extensions/packaging.md use crate::catalog::{self, CompView, FloorView}; use crate::classify::{self, PieceKind}; use crate::encode::{choose_q, MotionParams}; +use crate::files::{FileSet, Input, MemSink, Sink}; use crate::floor::{self, Floor}; use crate::objects; use crate::plan::{class_plan, plan_piece}; @@ -74,8 +91,9 @@ impl std::error::Error for PackError {} pub struct PackOpts { pub signer: SigningKey, pub signer_id: String, - /// The supplied floor's files, by listing path. - pub floor: BTreeMap>, + /// The supplied floor's files, by listing path: in memory, or a directory read a file + /// at a time. + pub floor: FileSet, /// The override a command line applies (§3.2). pub overrides: Overrides, /// The meter that brackets the pack, or none (the unmetered form). @@ -85,11 +103,27 @@ pub struct PackOpts { /// The `issued_at` of a measurement claim (RFC 3339, UTC). pub issued_at: String, pub grant: GrantRef, + /// How many pieces are coded at once, each alone (§4.1: a piece references nothing + /// outside itself), so the package is the same for any value; at least 1. + pub jobs: usize, + /// An earlier package whose bases and refinements this pack takes instead of coding + /// them (§11.5): a re-pack, whose receipt names it (parent kind `re-pack`). + pub base_from: Option, + /// A supplied CMAF floor's files (§7.7) are written into a directory package as hard + /// links to the supplied ones where the file system allows, so a server holds the + /// ladder's bytes once; every byte is the same either way. + pub link_supplied: bool, } impl PackOpts { - /// Unmetered options with the grant `wai.video.package` and no ceilings. + /// Unmetered options with the grant `wai.video.package` and no ceilings, coding one + /// piece at a time, the supplied floor's files given in memory. pub fn new(signer: SigningKey, signer_id: impl Into, floor: BTreeMap>) -> PackOpts { + PackOpts::with_floor(signer, signer_id, FileSet::Memory(floor)) + } + + /// [`PackOpts::new`] over a supplied floor given as a file set. + pub fn with_floor(signer: SigningKey, signer_id: impl Into, floor: FileSet) -> PackOpts { PackOpts { signer, signer_id: signer_id.into(), @@ -99,6 +133,9 @@ impl PackOpts { power_reader: String::new(), issued_at: String::new(), grant: GrantRef::unbounded("wai.video.package"), + jobs: 1, + base_from: None, + link_supplied: false, } } } @@ -193,31 +230,42 @@ pub fn object_lengths(env: &[u8]) -> Result<(u32, u64), String> { Ok((man, env.len() as u64 - 12 - man as u64)) } -fn rungs_of<'a>(p: &'a Policy, comp: &str, role: RungRole) -> Vec<&'a Rung> { - p.ladder.iter().filter(|r| r.component == comp && r.role == role).collect() -} - -/// A component's CMAF files: the initialization segment's path and bytes, and each -/// piece's fragment's. +/// A floor's CMAF files: the initialization segment's path and bytes, and each piece's +/// fragment's. pub type CmafFiles = (String, Vec, Vec<(String, Vec)>); -/// The floor's CMAF files of a component: the initialization segment and each piece's -/// fragment, by path. `floor` is the checked floor, `starts` the piece starts. -pub fn cmaf_files(comp: &str, floor: &Floor, starts: &[u64], tb_den: u32, timescale: u32) -> Result { +/// One piece's fragment of the floor named `name` (§7.6): its path and bytes. `units` are +/// its temporal units as the stream holds them, `key` which start at a shown key frame. +pub fn cmaf_fragment(name: &str, piece: u64, start: u64, units: &[Vec], key: &[bool], tb_den: u32) -> Result<(String, Vec), String> { + let mut samples = Vec::with_capacity(units.len()); + for (u, k) in units.iter().zip(key) { + let tus = crate::av1::temporal_units(u).map_err(|e| e.to_string())?; + let [tu] = tus.as_slice() else { return Err("a floor unit is not one temporal unit".into()) }; + samples.push(crate::cmaf::Sample { data: tu.sample(), duration: tb_den, sync: *k }); + } + let tfdt = start.checked_mul(tb_den as u64).ok_or("a fragment's decode time past 2^64 - 1")?; + let seq = u32::try_from(piece + 1).map_err(|_| "a piece number past 2^32 - 2")?; + let f = crate::cmaf::write_fragment(seq, tfdt, &samples); + Ok((format!("floor/{name}/{}.m4s", crate::sha256_hex(&f)), f)) +} + +/// The CMAF files of the floor named `name` (§7.6): the initialization segment and each +/// piece's fragment, by path. `floor` is the checked floor, `starts` the piece starts. +pub fn cmaf_files(name: &str, floor: &Floor, starts: &[u64], tb_den: u32, timescale: u32) -> Result { let init = crate::cmaf::write_init_av1(&floor.seq, &floor.seq_obu, floor.seq.max_frame_width, floor.seq.max_frame_height, timescale); let mut frags = Vec::with_capacity(floor.pieces.len()); for (p, piece) in floor.pieces.iter().enumerate() { - let mut samples = Vec::with_capacity(piece.units.len()); - for (u, k) in piece.units.iter().zip(&piece.key) { - let tus = crate::av1::temporal_units(u).map_err(|e| e.to_string())?; - let [tu] = tus.as_slice() else { return Err("a floor unit is not one temporal unit".into()) }; - samples.push(crate::cmaf::Sample { data: tu.sample(), duration: tb_den, sync: *k }); - } - let tfdt = starts[p].checked_mul(tb_den as u64).ok_or("a fragment's decode time past 2^64 - 1")?; - let f = crate::cmaf::write_fragment(p as u32 + 1, tfdt, &samples); - frags.push((format!("floor/{comp}/{}.m4s", crate::sha256_hex(&f)), f)); + frags.push(cmaf_fragment(name, p as u64, starts[p], &piece.units, &piece.key, tb_den)?); } - Ok((format!("floor/{comp}/init.mp4"), init, frags)) + Ok((format!("floor/{name}/init.mp4"), init, frags)) +} + +/// A component's floor rungs, in ladder order, each with its floor name (§7.1): the +/// component's id when it has one floor rung, the rung's id when it has several. +pub fn floor_rungs<'a>(p: &'a Policy, comp: &str) -> Vec<(&'a Rung, String)> { + let rungs: Vec<&Rung> = p.ladder.iter().filter(|r| r.component == comp && r.role == RungRole::Floor).collect(); + let one = rungs.len() == 1; + rungs.into_iter().map(|r| (r, if one { comp.to_string() } else { r.id.clone() })).collect() } /// The keyframe plan's file: each piece start, one decimal per line. @@ -523,8 +571,245 @@ pub fn window_index(policy: &Policy, comp: &crate::policy::Component, pieces: &[ Ok(Window { index, objects }) } -/// Pack `policy_bytes` over `sources` (each component's source bytes, by id). +/// One piece of a parent package (§11.5): where it lies and what it is, the rungs its +/// ladder codes its base and refinements in (capability and parameters), and those +/// entries; their objects are read from the parent when the piece is coded. +struct ParentPiece { + start: u64, + duration: u64, + flags: u8, + kind: u8, + rungs: Vec<(String, serde_json::Map)>, + deadline_ms: u64, + entries: Vec, +} + +/// A parent package, verified, as a re-pack reads it (§11.5): its files, which it reads an +/// object at a time, its receipt's hash and its pieces. +struct ParentPack { + files: FileSet, + receipt_hash: [u8; 32], + pieces: Vec, +} + +impl ParentPack { + fn object(&self, e: &EntryRec) -> Result, PackError> { + let path = objects::object_path(&e.content_hash); + let b = self.files.read(&path).map_err(|x| PackError::new("repack-parent-invalid", format!("{path}: {x}")))?; + if objects::object_hash(&b) != e.content_hash { + return Err(PackError::new("repack-parent-invalid", format!("{path} changed since the parent was verified"))); + } + Ok(b) + } +} + +/// The rungs a piece's ladder codes its base and refinements in: each one's capability +/// and parameters, in layer order. +fn coded_rungs(l: &crate::plan::PieceLadder<'_>) -> Vec<(String, serde_json::Map)> { + std::iter::once(l.base).chain(l.refinements.iter().copied()).map(|r| (r.capability.clone(), r.params.clone())).collect() +} + +/// Read and verify a parent package (§11.5): it verifies as §11.3 verifies a package +/// (`repack-parent-invalid`, with its reason), and its component `comp` is this one's, of +/// one media and timebase (`repack-parent-mismatch`). The verifier reads the parent whole; +/// what is kept after is its entries, and its objects are read again one at a time. +fn load_parent(set: &FileSet, comp: &crate::policy::Component) -> Result { + let invalid = |why: String| PackError::new("repack-parent-invalid", why); + let files: BTreeMap> = match set { + FileSet::Memory(m) => m.clone(), + FileSet::Dir(d) => crate::verify::read_strict(d).map_err(invalid)?, + }; + crate::verify::verify_files(&files).map_err(|v| invalid(v.iter().map(|r| r.to_string()).collect::>().join("; ")))?; + let text = std::str::from_utf8(&files["package.receipt.json"]).map_err(|_| invalid("a receipt that is not UTF-8".into()))?; + let r = PackageReceiptV2::from_json(text).map_err(|e| invalid(e.to_string()))?; + let mismatch = |why: String| PackError::new("repack-parent-mismatch", why); + let pp = Policy::parse_bytes(&files["policy.toml"]).map_err(|e| invalid(e.to_string()))?; + let pc = pp.component(&comp.id).ok_or_else(|| mismatch(format!("the parent has no component {}", comp.id)))?; + if pc.media != comp.media || pc.timebase != comp.timebase { + return Err(mismatch(format!("the parent's component {} is of another media or timebase", comp.id))); + } + let ix = StageIndex::parse(&files[&wai::package::index_file(&comp.id)]).map_err(|e| invalid(e.detail))?; + let mut pieces = Vec::with_capacity(ix.pieces.len()); + for (p, rec) in ix.pieces.iter().enumerate() { + let kind = PieceKind::from_code(rec.kind).ok_or_else(|| mismatch(format!("parent piece {p}: a kind a video piece does not have")))?; + let plan = plan_piece(&pp, pc.timebase, rec.start, rec.duration as u64, kind); + let ladder = crate::plan::piece_ladder(&pp, &comp.id, &plan).ok_or_else(|| mismatch(format!("parent piece {p}: no base rung")))?; + let mut entries = Vec::new(); + for e in ix.piece_entries(p).iter().filter(|e| matches!(e.role, Role::Base | Role::Refinement)) { + let mut e = e.clone(); + // The entry it refines, counted from its piece's first entry. + if e.refines != wsix::NO_REFINES { + e.refines -= rec.first_entry; + } + e.capability = 0; + entries.push(e); + } + pieces.push(ParentPiece { + start: rec.start, + duration: rec.duration as u64, + flags: rec.flags, + kind: rec.kind, + rungs: coded_rungs(&ladder), + deadline_ms: plan.deadline_ms, + entries, + }); + } + let files = match set { + FileSet::Memory(_) => FileSet::Memory(files), + FileSet::Dir(d) => FileSet::Dir(d.clone()), + }; + Ok(ParentPack { files, receipt_hash: r.receipt_hash(), pieces }) +} + +/// A re-pack's checks of its pieces against its parent's, before anything is written +/// (§11.5): one piece per piece, at the same start, of the same duration, flags and kind, +/// whose ladder codes its base and refinements in rungs of the same capabilities and +/// parameters, with the same deadline (`repack-parent-mismatch`). +fn match_parent(parent: &ParentPack, policy: &Policy, comp: &crate::policy::Component, pieces: &[crate::scenecut::Piece]) -> Result<(), PackError> { + let mismatch = |why: String| PackError::new("repack-parent-mismatch", why); + if parent.pieces.len() != pieces.len() { + return Err(mismatch(format!("the parent has {} pieces, the source {}", parent.pieces.len(), pieces.len()))); + } + for (p, (pc, piece)) in parent.pieces.iter().zip(pieces).enumerate() { + let kind = classify::classify(&piece.stats, &policy.classify); + if (pc.start, pc.duration, pc.flags, pc.kind) != (piece.start, piece.len, piece.flags, kind as u8) { + return Err(mismatch(format!("piece {p}: not the parent's piece (its start, duration, flags or kind)"))); + } + let plan = plan_piece(policy, comp.timebase, piece.start, piece.len, kind); + let ladder = crate::plan::piece_ladder(policy, &comp.id, &plan).ok_or_else(|| mismatch(format!("piece {p}: no base rung")))?; + if coded_rungs(&ladder) != pc.rungs || (!ladder.refinements.is_empty() && plan.deadline_ms != pc.deadline_ms) { + return Err(mismatch(format!("piece {p}: its base and refinements are coded in other rungs, parameters or deadline than the parent's"))); + } + } + Ok(()) +} + +/// A piece taken from its parent (§11.5): the parent's base and refinement objects and +/// entries, each prefix reconstructed by the reference sink and held to this source by the +/// SSE its entry states (`repack-source-mismatch`). +fn reuse_piece(parent: &ParentPack, pc: &ParentPiece, piece: &crate::scenecut::Piece, policy: &Policy, comp: &crate::policy::Component, src: &[Vec]) -> Result { + let objs: Vec> = pc.entries.iter().map(|e| parent.object(e)).collect::>()?; + let kind = classify::classify(&piece.stats, &policy.classify); + let plan = plan_piece(policy, comp.timebase, piece.start, piece.len, kind); + let ladder = crate::plan::piece_ladder(policy, &comp.id, &plan).ok_or_else(|| PackError::new("policy-no-base", format!("component {}", comp.id)))?; + let wrong = |k: usize| PackError::new("repack-source-mismatch", format!("piece at {}: the parent's entry {k} does not reconstruct this source with the SSE it states", piece.start)); + let caps: Vec<&str> = std::iter::once(ladder.base.capability.as_str()).chain(ladder.refinements.iter().map(|r| r.capability.as_str())).collect(); + let (base, base_env) = (&pc.entries[0], &objs[0]); + let recon: Vec> = if caps[0] == CAP_PNG { + let w = wai::container::Wai::from_bytes(base_env).map_err(|e| PackError::new("repack-parent-invalid", e.to_string()))?; + let (_, _, rgb) = crate::png::decode(&w.payload).map_err(|e| PackError::new("repack-parent-invalid", e))?; + vec![rgb; src.len()] + } else { + let w = wai::container::Wai::from_bytes(base_env).map_err(|e| PackError::new("repack-parent-invalid", e.to_string()))?; + match wai::int_payload::decode_int(caps[0], &w.payload, &wai::int_payload::IntParams::none()) { + Ok(wai::int_payload::IntDecoded::Video { frames, .. }) => frames, + _ => return Err(PackError::new("repack-parent-invalid", "a parent base that does not decode to frames")), + } + }; + if recon.len() != src.len() || crate::encode::sse(&recon, src) != base.sse { + return Err(wrong(0)); + } + let stage = |e: &EntryRec, env: &[u8], cap: &str| Stage { + lens: Lens { byte_len: e.byte_len, manifest_len: e.manifest_len, payload_len: e.payload_len }, + envelope: env.to_vec(), + hash: e.content_hash, + capability: cap.to_string(), + role: e.role, + layer: e.layer, + prefix_tier: e.prefix_tier, + prefix_digest: e.prefix_digest, + sse: e.sse, + deadline_ms: e.deadline_ms, + flags: 0, + }; + let mut stages = vec![stage(base, base_env, caps[0])]; + if pc.entries.len() > 1 { + let (info, units) = wai::staged::decode_base(base_env, &BTreeMap::new(), None, None).map_err(|e| PackError::new("repack-parent-invalid", e.to_string()))?; + let mut chain = wai::staged::StageChain::new(info, units, sink_config()); + for (k, (e, env)) in pc.entries.iter().zip(&objs).enumerate().skip(1) { + let applied = chain.offer(env, 0, wai::staged::ReceiptEvidence::None).map_err(|e| PackError::new("repack-parent-invalid", e.to_string()))?; + if applied.iter().any(|r| r.envelope.is_some()) { + return Err(PackError::new("repack-parent-invalid", format!("piece at {}: a parent layer that does not apply", piece.start))); + } + let frames: Vec> = chain.units().iter().filter_map(|u| match &u.buf.samples { + wai::staged::Samples::U8(v) => Some(v.clone()), + wai::staged::Samples::I16(_) => None, + }).collect(); + if frames.len() != src.len() || crate::encode::sse(&frames, src) != e.sse { + return Err(wrong(k)); + } + stages.push(stage(e, env, caps[k])); + } + } + Ok(CodedPiece { piece: piece.clone(), kind, plan, stages }) +} + +/// Pack `policy_bytes` over `sources` (each component's source bytes, by id), into +/// memory: [`pack_into`] with a [`MemSink`]. pub fn pack(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut PackOpts) -> Result { + let inputs: BTreeMap> = sources.iter().map(|(k, v)| (k.clone(), Input::Bytes(v))).collect(); + let mut sink = MemSink::default(); + let receipt = pack_into(policy_bytes, &inputs, opts, &mut sink)?; + Ok(PackOutput { files: sink.files, receipt }) +} + +/// One floor rung as the packer reads it: the rung, its floor name, its units' check, and +/// its supplied files. +struct FloorIn<'a> { + rung: &'a Rung, + name: String, + check: floor::StreamCheck, + form: FloorForm, +} + +/// A floor rung's supplied files. +enum FloorForm { + /// §7.3: one stream, `.obu`, and its SHA-256 as the listing gives it. + Obu { file: String, sha256: String }, + /// §7.7: an initialization segment and one media segment per piece, the ladder's own. + Cmaf(floor::CmafCheck), +} + +/// Where the second read of a floor rung is: a stream a unit at a time, or the next media +/// segment. +enum FloorRead<'s> { + Obu(Box>>>), + Cmaf(usize), +} + +fn io_err(what: &str, e: impl std::fmt::Display) -> PackError { + PackError::new("io", format!("{what}: {e}")) +} + +/// The SHA-256 of one piece's frames, in order: what the second read of the source must +/// give again. +fn frames_digest(frames: &[Vec]) -> [u8; 32] { + use sha2::Digest as _; + let mut h = sha2::Sha256::new(); + for f in frames { + h.update(f); + } + h.finalize().into() +} + +/// Read `n` frames from `rd`, or refuse: the source ended or changed since the first read +/// (`pack-source-changed`), or a frame does not read (its reason). +fn read_frames(rd: &mut crate::source::y4m::Y4mReader, n: u64) -> Result>, PackError> { + let mut v = Vec::with_capacity(n as usize); + for _ in 0..n { + match rd.next_frame() { + Some(Ok(f)) => v.push(f.rgb), + Some(Err(e)) => return Err(PackError::new(e.code(), e.to_string())), + None => return Err(PackError::new("pack-source-changed", "the source ended earlier on its second read")), + } + } + Ok(v) +} + +/// Pack `policy_bytes` over `sources` into `sink`, each file as it is made. The checks are +/// [`pack`]'s and their order the verifier's (§11.3, §11.4); a refusal leaves in the sink +/// what it had written, which a [`crate::files::DirSink`] removes when it is dropped. +pub fn pack_into(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut PackOpts, sink: &mut dyn Sink) -> Result { let policy = Policy::parse_bytes(policy_bytes).map_err(|e| PackError::new(e.code(), e.to_string()))?.effective(&opts.overrides); // The policy's own refusals, in the order a package's verifier applies them (§11.3). if let Some(r) = crate::verify::policy_refusal(&policy) { @@ -538,77 +823,305 @@ pub fn pack(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut } // The rungs it codes, before the source is read. Coder::new(&policy, comp, 0, 0)?; - let floors = rungs_of(&policy, &comp.id, RungRole::Floor); - let [floor_rung] = floors.as_slice() else { - return Err(PackError::new("floor-missing", format!("component {} needs one floor rung (§7.1)", comp.id))); - }; - if floor_rung.params.get("source").and_then(|v| v.as_str()) != Some("supplied") { + let floors = floor_rungs(&policy, &comp.id); + if floors.is_empty() { + return Err(PackError::new("floor-missing", format!("component {} needs a floor rung (§7.1)", comp.id))); + } + if floors.iter().any(|(r, _)| r.params.get("source").and_then(|v| v.as_str()) != Some("supplied")) { return Err(PackError::new("pack-floor-encode-unsupported", "this revision packs a supplied floor")); } let tb = comp.timebase; - let src = sources.get(&comp.id).ok_or_else(|| PackError::new("pack-source-missing", format!("no source for component {}", comp.id)))?; - let (hd, frames) = crate::source::y4m::read_all(src).map_err(|e| PackError::new(e.code(), e.to_string()))?; - if hd.rate.is_some_and(|(n, d)| (n, d) != (tb.num, tb.den)) { - return Err(PackError::new("pack-source-rate", "the source's frame rate is not the component's timebase")); - } - if frames.is_empty() { - return Err(PackError::new("y4m-truncated", "a source of no frames")); - } - let (w, h) = (hd.w, hd.h); - let mut seg = Segmenter::new(SegParams::from_policy(&policy.segmentation, tb), w, h); + // A floor-only component (§7.8) has no source: its pieces are its first floor rung's. + let floor_only = comp.floor_only(); + let source_err = |e: crate::source::SourceError| PackError::new(e.code(), e.to_string()); + let src = if floor_only { None } else { Some(sources.get(&comp.id).ok_or_else(|| PackError::new("pack-source-missing", format!("no source for component {}", comp.id)))?) }; + let (mut w, mut h) = (0u32, 0u32); let mut pieces = Vec::new(); - for f in &frames { - pieces.extend(seg.push(&f.rgb)); + let mut n_frames = 0u64; + let mut digests: Vec<[u8; 32]> = Vec::new(); + if let Some(src) = src { + // The first read of the source: its pieces (§4.3), a frame at a time. + let mut rd = crate::source::y4m::Y4mReader::new(src.open().map_err(|e| io_err("the source", e))?).map_err(source_err)?; + let hd = rd.header; + (w, h) = (hd.w, hd.h); + let mut seg = Segmenter::new(SegParams::from_policy(&policy.segmentation, tb), w, h); + let mut held: Vec> = Vec::new(); + while let Some(f) = rd.next_frame() { + let f = f.map_err(source_err)?; + n_frames += 1; + let closed = seg.push(&f.rgb); + held.push(f.rgb); + for p in closed { + // The frames of a closed piece leave the hold once their digest is taken. + let first = n_frames - held.len() as u64; + let take = (p.start + p.len - first) as usize; + digests.push(frames_digest(&held[..take])); + held.drain(..take); + pieces.push(p); + } + } + for p in seg.finish() { + let first = n_frames - held.len() as u64; + let take = (p.start + p.len - first) as usize; + digests.push(frames_digest(&held[..take])); + held.drain(..take); + pieces.push(p); + } + drop(held); + if hd.rate.is_some_and(|(n, d)| (n, d) != (tb.num, tb.den)) { + return Err(PackError::new("pack-source-rate", "the source's frame rate is not the component's timebase")); + } + if n_frames == 0 { + return Err(PackError::new("y4m-truncated", "a source of no frames")); + } } - pieces.extend(seg.finish()); - let starts: Vec = pieces.iter().map(|p| p.start).collect(); - let floor_listing = policy.floor.supplied_listing.clone(); - let fl = floor::floor_supplied(&opts.floor, floor_listing.as_deref(), &comp.id, &starts, frames.len() as u64, policy.floor.extra_keyframes) - .map_err(|r| PackError::new(r.code, r.detail))?; - let listing = floor_listing.expect("checked by floor_supplied"); + let mut starts: Vec = pieces.iter().map(|p| p.start).collect(); + + // The supplied floor, checked before anything is written (§7.3): the listing once, + // then each floor rung's stream in ladder order. + let floor_reason = |r: floor::FloorReason| PackError::new(r.code, r.detail); + let (got, shas) = floor::set_listing(&opts.floor).map_err(floor_reason)?; + let listing = policy.floor.supplied_listing.clone(); + if listing.as_deref() != Some(got.as_str()) { + return Err(PackError::new("floor-listing-mismatch", format!("the supplied floor's listing is {got}, the policy pins {}", listing.as_deref().unwrap_or("none")))); + } + let listing = listing.expect("checked"); + let mut fins: Vec = Vec::new(); + // A supplied CMAF floor's listing travels in the package (§7.7): it names each of the + // ladder's files by its SHA-256, and the policy, which the receipt signs, pins it. + let floor_listing = if policy.floor.cmaf() { Some(wai::pin::listing_from_sha256(&shas).map_err(|e| PackError::new("floor-listing-mismatch", e.to_string()))?) } else { None }; + for (i, (rung, name)) in floors.iter().enumerate() { + // A floor-only component's first floor rung decides its frames and its pieces + // (§7.8): a piece per media segment, or one at each shown key frame. + let plans = floor_only && i == 0; + let frames = (!plans).then_some(n_frames); + let (check, form) = if policy.floor.cmaf() { + let c = floor::check_cmaf(&opts.floor, &shas, name, tb.num, tb.den, frames).map_err(floor_reason)?; + if plans { + n_frames = c.units.key.len() as u64; + starts = c.segments.iter().map(|s| s.start).collect(); + } + floor::check_cmaf_pieces(&c.segments, &starts, n_frames).map_err(floor_reason)?; + (c.units.clone(), FloorForm::Cmaf(c)) + } else { + let file = format!("{name}.obu"); + let sha256 = shas.get(&file).cloned().ok_or_else(|| PackError::new("floor-stream-missing", format!("no {file} in the supplied floor")))?; + let r = opts.floor.open(&file).map_err(|e| io_err(&file, e))?; + let check = floor::check_stream(r, frames).map_err(floor_reason)?; + if plans { + n_frames = check.key.len() as u64; + starts = check.key.iter().enumerate().filter(|(_, k)| **k).map(|(f, _)| f as u64).collect(); + if starts.first() != Some(&0) { + return Err(PackError::new("floor-keyframe-mismatch", format!("{file}: its first unit is not a shown key frame"))); + } + } + (check, FloorForm::Obu { file, sha256 }) + }; + floor::check_keyframes(&check.key, &starts, policy.floor.extra_keyframes).map_err(floor_reason)?; + if plans { + pieces = floor::floor_pieces(&starts, n_frames, tb.units_from_ms(policy.segmentation.max_ms)).map_err(floor_reason)?; + } + fins.push(FloorIn { rung, name: name.clone(), check, form }); + } + + // A re-pack's parent, verified and matched piece by piece (§11.5), before anything is + // written. + let parent = match &opts.base_from { + None => None, + Some(_) if floor_only => return Err(PackError::new("repack-parent-mismatch", "a floor-only component has no base to take (§7.8)")), + Some(set) => { + let pp = load_parent(set, comp)?; + match_parent(&pp, &policy, comp, &pieces)?; + Some(pp) + } + }; if let Some(m) = opts.meter.as_mut() { m.start(); } let coder = Coder::new(&policy, comp, w, h)?; - let (fw, fh) = (fl.seq.max_frame_width, fl.seq.max_frame_height); - let mut coded = Vec::with_capacity(pieces.len()); - for (p, piece) in pieces.iter().enumerate() { - let src_frames: Vec> = frames[piece.start as usize..(piece.start + piece.len) as usize].iter().map(|f| f.rgb.clone()).collect(); - let mut cp = coder.code(piece, &src_frames)?; - // The floor, presenting the piece's base (§7.2). - let base = &cp.stages[0]; - let df = objects::supplied_derived_from(&base.hash, &base.capability, &listing); - let payload = floor::av1_payload(&fl.pieces[p].units, fh, fw, tb.num, tb.den); - let env = objects::floor_envelope(comp.media.label(), &floor_rung.capability, payload, piece.len, fh, fw, &df); - cp.stages.push(Stage { - hash: objects::object_hash(&env), - capability: floor_rung.capability.clone(), - role: Role::Floor, - layer: 0, - prefix_tier: wsix::TIER_NONE, - prefix_digest: [0u8; 32], - sse: wsix::SSE_UNKNOWN, - deadline_ms: 0, - flags: wsix::ENTRY_DERIVED_FROM, - lens: Lens::of(&env)?, - envelope: env, - }); - coded.push(cp); - } - let Window { index, objects } = window_index(&policy, comp, &coded, 0, 0, [0u8; 32])?; - let policy_digest = policy.digest(); - let (init_path, init, frags) = cmaf_files(&comp.id, &fl, &starts, tb.den, tb.num).map_err(|e| PackError::new("floor-av1-malformed", e))?; - let view = CompView { - index: &index, - tb, - w, - h, - objects: &objects, - floor: Some(FloorView { codec: fl.seq.codec_string(), w: fw, h: fh, init: init_path.clone(), fragments: frags.iter().map(|(p, b)| (p.clone(), b.len() as u64)).collect() }), - base_cap: catalog::default_base_cap(&policy, &comp.id), + // The second read: each piece coded, and each floor rung's units for it. + let mut rd = match src { + Some(src) => Some(crate::source::y4m::Y4mReader::new(src.open().map_err(|e| io_err("the source", e))?).map_err(source_err)?), + None => None, }; - let catalogs = catalog::render_all(&view, &policy, &policy_digest).map_err(|e| PackError::new("pack-catalog", e.0))?; + if let Some(l) = &floor_listing { + sink.put(crate::verify::FLOOR_LISTING, l).map_err(|e| io_err(crate::verify::FLOOR_LISTING, e))?; + } + let mut streams: Vec = Vec::with_capacity(fins.len()); + let mut frag_views: Vec> = vec![Vec::new(); fins.len()]; + let mut inits: Vec = Vec::with_capacity(fins.len()); + for f in &fins { + let path = format!("floor/{}/init.mp4", f.name); + match &f.form { + FloorForm::Obu { file, .. } => { + let r = crate::files::HashingReader::new(opts.floor.open(file).map_err(|e| io_err(file, e))?); + streams.push(FloorRead::Obu(Box::new(crate::av1::TuReader::new(r)))); + let c = &f.check; + let init = crate::cmaf::write_init_av1(&c.seq, &c.seq_obu, c.seq.max_frame_width, c.seq.max_frame_height, tb.num); + sink.put(&path, &init).map_err(|e| io_err(&path, e))?; + } + // The ladder's own initialization segment, as supplied (§7.7). + FloorForm::Cmaf(c) => { + streams.push(FloorRead::Cmaf(0)); + if opts.link_supplied { + sink.put_from(&path, &opts.floor, &c.init_file, &c.init_sha256, true).map_err(|e| PackError::new("floor-listing-mismatch", format!("{path}: {e}")))?; + } else { + let b = opts.floor.read(&c.init_file).map_err(|e| io_err(&c.init_file, e))?; + if crate::sha256_hex(&b) != c.init_sha256 { + return Err(PackError::new("floor-listing-mismatch", format!("{} changed while it was read", c.init_file))); + } + sink.put(&path, &b).map_err(|e| io_err(&path, e))?; + } + } + } + inits.push(path); + } + let jobs = opts.jobs.max(1); + let mut coded: Vec = Vec::with_capacity(pieces.len()); + for batch in (0..pieces.len()).collect::>().chunks(jobs) { + let mut srcs: Vec>> = Vec::with_capacity(batch.len()); + for &p in batch { + let Some(rd) = rd.as_mut() else { + srcs.push(Vec::new()); + continue; + }; + let fr = read_frames(rd, pieces[p].len)?; + if frames_digest(&fr) != digests[p] { + return Err(PackError::new("pack-source-changed", format!("piece {p}'s frames differ on the source's second read"))); + } + srcs.push(fr); + } + let one = |p: usize, fr: &[Vec]| match &parent { + // A floor-only piece: its floors alone, of kind none (§7.8). + _ if floor_only => { + let piece = &pieces[p]; + Ok(CodedPiece { piece: piece.clone(), kind: PieceKind::None, plan: plan_piece(&policy, tb, piece.start, piece.len, PieceKind::None), stages: Vec::new() }) + } + None => coder.code(&pieces[p], fr), + Some(pp) => reuse_piece(pp, &pp.pieces[p], &pieces[p], &policy, comp, fr), + }; + let results: Vec> = if batch.len() == 1 { + vec![one(batch[0], &srcs[0])] + } else { + std::thread::scope(|s| { + let hs: Vec<_> = batch.iter().zip(&srcs).map(|(&p, fr)| { + let one = &one; + s.spawn(move || one(p, fr)) + }).collect(); + hs.into_iter().map(|h| h.join().unwrap_or_else(|_| Err(PackError::new("pack-encode", "a coding thread panicked")))).collect() + }) + }; + drop(srcs); + for (&p, r) in batch.iter().zip(results) { + let piece = &pieces[p]; + let mut cp = r?; + // The floors, each presenting the piece's base (§7.2); a floor-only piece's + // present none (§7.8). + let df = cp.stages.first().map(|b| objects::supplied_derived_from(&b.hash, &b.capability, &listing)); + for (fi, f) in fins.iter().enumerate() { + let (fw, fh) = (f.check.seq.max_frame_width, f.check.seq.max_frame_height); + let changed = |what: &str| PackError::new("floor-listing-mismatch", format!("{what} changed while it was read")); + let units: Vec> = match (&mut streams[fi], &f.form) { + (FloorRead::Obu(rd), FloorForm::Obu { file, .. }) => { + let mut units = Vec::with_capacity(piece.len as usize); + for _ in 0..piece.len { + match rd.next_unit() { + Ok(Some(u)) => units.push(u), + Ok(None) | Err(crate::av1::ReadError::Malformed(_)) => return Err(changed(file)), + Err(crate::av1::ReadError::Io(e)) => return Err(io_err(file, e)), + } + } + let at = piece.start as usize; + let key = &f.check.key[at..at + piece.len as usize]; + let (path, frag) = cmaf_fragment(&f.name, p as u64, piece.start, &units, key, tb.den).map_err(|e| PackError::new("floor-av1-malformed", e))?; + sink.put(&path, &frag).map_err(|e| io_err(&path, e))?; + frag_views[fi].push((path, frag.len() as u64)); + units + } + // The piece's media segment, the ladder's own bytes under its SHA-256 + // (§7.7), and its samples as the piece's units. + (FloorRead::Cmaf(next), FloorForm::Cmaf(c)) => { + let seg = &c.segments[*next]; + *next += 1; + let b = opts.floor.read(&seg.file).map_err(|e| io_err(&seg.file, e))?; + if crate::sha256_hex(&b) != seg.sha256 { + return Err(changed(&seg.file)); + } + let samples = crate::cmaf_read::read_segment(&b, &c.init).map_err(|_| changed(&seg.file))?; + let path = format!("floor/{}/{}.m4s", f.name, seg.sha256); + if opts.link_supplied { + sink.put_from(&path, &opts.floor, &seg.file, &seg.sha256, true).map_err(|e| PackError::new("floor-listing-mismatch", format!("{path}: {e}")))?; + } else if !sink.has(&path) { + sink.put(&path, &b).map_err(|e| io_err(&path, e))?; + } + frag_views[fi].push((path, b.len() as u64)); + crate::cmaf_read::units(&b, &samples) + } + _ => unreachable!("a floor's read is of its form"), + }; + let payload = floor::av1_payload(&units, fh, fw, tb.num, tb.den); + let env = match &df { + Some(df) => objects::floor_envelope(comp.media.label(), &f.rung.capability, payload, piece.len, fh, fw, df), + None => objects::base_envelope(comp.media.label(), &f.rung.capability, payload, piece.len, fh, fw), + }; + cp.stages.push(Stage { + hash: objects::object_hash(&env), + capability: f.rung.capability.clone(), + role: Role::Floor, + layer: 0, + prefix_tier: wsix::TIER_NONE, + prefix_digest: [0u8; 32], + sse: wsix::SSE_UNKNOWN, + deadline_ms: 0, + flags: if df.is_some() { wsix::ENTRY_DERIVED_FROM } else { 0 }, + lens: Lens::of(&env)?, + envelope: env, + }); + } + // Each object is written once, and its bytes are not held after. + for st in &mut cp.stages { + let path = objects::object_path(&st.hash); + if !sink.has(&path) { + sink.put(&path, &st.envelope).map_err(|e| io_err(&path, e))?; + } + st.envelope = Vec::new(); + } + coded.push(cp); + } + } + // Each stream read again whole is the one the listing pinned. + for (f, s) in fins.iter().zip(streams) { + if let (FloorRead::Obu(mut rd), FloorForm::Obu { file, sha256 }) = (s, &f.form) { + if !matches!(rd.next_unit(), Ok(None)) { + return Err(PackError::new("floor-listing-mismatch", format!("{file} changed while it was read"))); + } + let got = (*rd).into_inner().finish().map_err(|e| io_err(file, e))?; + if got != *sha256 { + return Err(PackError::new("floor-listing-mismatch", format!("{file} changed while it was read"))); + } + } + } + if rd.as_mut().is_some_and(|rd| rd.next_frame().is_some()) { + return Err(PackError::new("pack-source-changed", "the source holds more frames on its second read")); + } + let Window { index, .. } = window_index(&policy, comp, &coded, 0, 0, [0u8; 32])?; + drop(coded); + let policy_digest = policy.digest(); + let views: Vec = fins + .iter() + .zip(inits) + .zip(frag_views) + .map(|((f, init), fragments)| FloorView { name: f.name.clone(), codec: f.check.seq.codec_string(), w: f.check.seq.max_frame_width, h: f.check.seq.max_frame_height, init, fragments }) + .collect(); let index_bytes = index.to_bytes(); + let catalogs = { + let objs = catalog::SinkObjects(&*sink); + let view = CompView { index: &index, tb, w, h, objects: &objs, floors: views, base_cap: catalog::default_base_cap(&policy, &comp.id) }; + catalog::render_all(&view, &policy, &policy_digest).map_err(|e| PackError::new("pack-catalog", e.0))? + }; + for (p, b) in &catalogs { + sink.put(p, b).map_err(|e| io_err(p, e))?; + } // A meter that was asked for and gives no figure (a span too short, a product past // its range) refuses the pack: it never turns into an unmetered package. let energy = match opts.meter.as_mut() { @@ -624,31 +1137,22 @@ pub fn pack(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut &packager_id(), energy, opts.grant.clone(), - Parent::None, + parent.as_ref().map_or(Parent::None, |pp| Parent::RePack(pp.receipt_hash)), ) .map_err(|r| PackError::new(r.code, r.detail))?; - let mut files: BTreeMap> = BTreeMap::new(); - files.insert("policy.toml".into(), policy_bytes.to_vec()); - files.insert("policy.json".into(), policy.canonical_json().into_bytes()); - files.insert(format!("plan-{}.txt", comp.id), plan_file(&starts).into_bytes()); - files.insert(wai::package::index_file(&comp.id), index_bytes.clone()); - for (h, env) in objects { - files.insert(objects::object_path(&h), env); - } - files.insert(init_path, init); - for (p, b) in frags { - files.insert(p, b); - } - files.extend(catalogs); - files.insert("package.receipt.json".into(), receipt.to_json().into_bytes()); + let put = |sink: &mut dyn Sink, p: &str, b: &[u8]| sink.put(p, b).map_err(|e| io_err(p, e)); + put(sink, "policy.toml", policy_bytes)?; + put(sink, "policy.json", policy.canonical_json().as_bytes())?; + put(sink, &format!("plan-{}.txt", comp.id), plan_file(&starts).as_bytes())?; + put(sink, &wai::package::index_file(&comp.id), &index_bytes)?; + put(sink, "package.receipt.json", receipt.to_json().as_bytes())?; if receipt.energy_provenance.is_some() { let claim = measurement_claim(&receipt, &index_bytes, opts).map_err(|e| PackError::new("pack-measurement-claim", e))?; - files.insert(claim_path(&receipt), claim.into_bytes()); + put(sink, &claim_path(&receipt), claim.as_bytes())?; } - let view: BTreeMap = files.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); - let listing = wai::pin::listing_from(&view).map_err(|e| PackError::new("pack-listing", e.to_string()))?; - files.insert("LISTING".into(), listing); - Ok(PackOutput { files, receipt }) + let listing = wai::pin::listing_from_sha256(&sink.sha256s()).map_err(|e| PackError::new("pack-listing", e.to_string()))?; + put(sink, "LISTING", &listing)?; + Ok(receipt) } /// Where a package's measurement claim is: `claims/.measurement.json`. diff --git a/wai/wai-pack/src/policy.rs b/wai/wai-pack/src/policy.rs index 56946cc6e..fafc7e387 100644 --- a/wai/wai-pack/src/policy.rs +++ b/wai/wai-pack/src/policy.rs @@ -123,6 +123,8 @@ pub enum PolicyReason { RefinesBranch, /// A component has no base rung. NoBase, + /// A floor-only component with a base or a refinement rung, a source, or no floor rung. + FloorOnly, /// A class's or a rule's `max_layer` that no rung reaches. ClassLayer, /// A rule's `then.ladder` names an unknown rung, rungs of two components, or no base. @@ -166,6 +168,7 @@ impl PolicyReason { RefinesBase => "policy-refines-base", RefinesBranch => "policy-refines-branch", NoBase => "policy-no-base", + FloorOnly => "policy-floor-only", ClassLayer => "policy-class-layer", RuleLadder => "policy-rule-ladder", LayerLag => "policy-layer-lag", @@ -270,6 +273,19 @@ pub struct Component { pub media: Media, pub source: Option, pub timebase: Timebase, + /// `"floor-only"`: a component of floor rungs alone, with no base and no source + /// (§7.8); absent, the standard profile. + pub profile: Option, +} + +/// The one value of `component.profile` (§7.8). +pub const PROFILE_FLOOR_ONLY: &str = "floor-only"; + +impl Component { + /// Whether the component is floor-only (§7.8). + pub fn floor_only(&self) -> bool { + self.profile.as_deref() == Some(PROFILE_FLOOR_ONLY) + } } /// `[segmentation]`. @@ -333,8 +349,21 @@ impl Rung { pub struct Floor { pub supplied_listing: Option, pub extra_keyframes: bool, + /// `"cmaf"`: the supplied floor is CMAF files, an existing ladder's (§7.7); absent, it + /// is the OBU form (§7.3). + pub supplied_form: Option, } +impl Floor { + /// Whether the supplied floor is in the CMAF form (§7.7). + pub fn cmaf(&self) -> bool { + self.supplied_form.as_deref() == Some(SUPPLIED_CMAF) + } +} + +/// The one value of `floor.supplied_form`: the CMAF form of a supplied floor (§7.7). +pub const SUPPLIED_CMAF: &str = "cmaf"; + /// One delivery class. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Class { @@ -695,7 +724,7 @@ fn read(doc: &toml::Table) -> R<(Policy, Option>)> { } let mut components = Vec::new(); for c in top.tables("component")? { - c.keys(&["id", "media", "source", "timebase"])?; + c.keys(&["id", "media", "source", "timebase", "profile"])?; let id = c.id("id")?; let media = Media::from_label(&c.string_req("media")?).ok_or_else(|| c.err(PolicyReason::Value, "media"))?; let source = c.string("source")?; @@ -703,7 +732,11 @@ fn read(doc: &toml::Table) -> R<(Policy, Option>)> { tb.keys(&["num", "den"])?; let num = tb.uint_in("num", 1, u32::MAX as u64, None)? as u32; let den = tb.uint_in("den", 1, u32::MAX as u64, None)? as u32; - components.push(Component { id, media, source, timebase: Timebase { num, den } }); + let profile = c.string("profile")?; + if profile.as_deref().is_some_and(|p| p != PROFILE_FLOOR_ONLY) { + return Err(c.err(PolicyReason::Value, "profile")); + } + components.push(Component { id, media, source, timebase: Timebase { num, den }, profile }); } let s = top.table("segmentation")?; @@ -749,12 +782,17 @@ fn read(doc: &toml::Table) -> R<(Policy, Option>)> { } let f = top.table("floor")?; - f.keys(&["supplied_listing", "extra_keyframes"])?; + f.keys(&["supplied_listing", "extra_keyframes", "supplied_form"])?; let supplied_listing = f.string("supplied_listing")?; if supplied_listing.as_deref().is_some_and(|s| !is_hex64(s)) { return Err(f.err(PolicyReason::Value, "supplied_listing")); } - let floor = Floor { supplied_listing, extra_keyframes: f.boolean("extra_keyframes", false)? }; + let extra_keyframes = f.boolean("extra_keyframes", false)?; + let supplied_form = f.string("supplied_form")?; + if supplied_form.as_deref().is_some_and(|s| s != SUPPLIED_CMAF) { + return Err(f.err(PolicyReason::Value, "supplied_form")); + } + let floor = Floor { supplied_listing, extra_keyframes, supplied_form }; let l = top.table("live")?; l.keys(&["layer_lag_ms"])?; @@ -983,8 +1021,16 @@ impl Policy { if let Some(t) = first_duplicate(self.ladder.iter().filter_map(|r| r.refines.as_deref())) { return e(P::RefinesBranch, &format!("ladder.{t}")); } - if let Some(c) = self.components.iter().find(|c| !self.ladder.iter().any(|r| r.component == c.id && r.role == Role::Base)) { - return e(P::NoBase, &format!("component.{}", c.id)); + for c in &self.components { + let has = |role: Role| self.ladder.iter().any(|r| r.component == c.id && r.role == role); + if c.floor_only() { + // §7.8: floor rungs alone, and no source. + if has(Role::Base) || has(Role::Refinement) || c.source.is_some() || !has(Role::Floor) { + return e(P::FloorOnly, &format!("component.{}", c.id)); + } + } else if !has(Role::Base) { + return e(P::NoBase, &format!("component.{}", c.id)); + } } let top = self.layer_count() - 1; if let Some(c) = self.classes.iter().find(|c| c.max_layer > top) { @@ -1000,11 +1046,13 @@ impl Policy { // over a rung it names, so the layers it codes are a chain from that base; // and every floor rung of the component (§7.1: every piece has a floor). let named = |id: &str| ids.iter().any(|i| i == id); + let floor_only = |comp: &str| self.components.iter().any(|c| c.id == comp && c.floor_only()); + // A floor-only component's ladder is its floor rungs, every one (§7.8). let ok = rungs.is_some_and(|rs| { !rs.is_empty() && rs.iter().all(|x| x.component == rs[0].component) && first_duplicate(ids.iter().map(String::as_str)).is_none() - && rs.iter().filter(|x| x.role == Role::Base).count() == 1 + && rs.iter().filter(|x| x.role == Role::Base).count() == usize::from(!floor_only(&rs[0].component)) && rs.iter().all(|x| x.refines.as_deref().is_none_or(named)) && self.ladder.iter().filter(|x| x.component == rs[0].component && x.role == Role::Floor).all(|f| named(&f.id)) }); @@ -1033,8 +1081,9 @@ impl Policy { /// The effective policy as JSON ([packaging] §3.2): every member the schema /// defines, defaults written in; an optional member with no default (`source`, - /// `supplied_listing`, `max_kbps`, `refines`, and a rule's `when` and `then` keys) is - /// omitted when absent. Arrays of tables keep their file order. + /// `profile`, `supplied_listing`, `supplied_form`, `max_kbps`, `refines`, and a rule's + /// `when` and `then` keys) is omitted when absent. Arrays of tables keep their file + /// order. /// /// [packaging]: ../../../extensions/packaging.md pub fn to_value(&self) -> Value { @@ -1057,6 +1106,9 @@ impl Policy { tb.insert("num".into(), c.timebase.num.into()); tb.insert("den".into(), c.timebase.den.into()); m.insert("timebase".into(), tb.into()); + if let Some(p) = &c.profile { + m.insert("profile".into(), p.clone().into()); + } Value::Object(m) }) .collect::>(); @@ -1095,6 +1147,9 @@ impl Policy { floor.insert("supplied_listing".into(), l.clone().into()); } floor.insert("extra_keyframes".into(), self.floor.extra_keyframes.into()); + if let Some(f) = &self.floor.supplied_form { + floor.insert("supplied_form".into(), f.clone().into()); + } top.insert("floor".into(), floor.into()); let mut live = Map::new(); live.insert("layer_lag_ms".into(), self.layer_lag_ms.clone().into()); diff --git a/wai/wai-pack/src/verify.rs b/wai/wai-pack/src/verify.rs index 2e3b12a71..97418c624 100644 --- a/wai/wai-pack/src/verify.rs +++ b/wai/wai-pack/src/verify.rs @@ -120,14 +120,20 @@ pub fn verify_dir(dir: &std::path::Path) -> Result> { verify_files(&files) } -/// The policy's own refusals a package is held to (§11.1, §3.6): its ids, then what it -/// asks a packager to emit. +/// The policy's own refusals a package is held to (§11.1, §3.6): its ids and floor names, +/// then what it asks a packager to emit. pub fn policy_refusal(policy: &Policy) -> Option { let classes_ok = policy.classes.iter().all(|c| crate::pack::id_is_path_segment(&c.id) && !c.id.eq_ignore_ascii_case("classic")); let folded: std::collections::BTreeSet = policy.classes.iter().map(|c| c.id.to_ascii_lowercase()).collect(); if !policy.components.iter().all(|c| crate::pack::id_is_path_segment(&c.id)) || !classes_ok || folded.len() != policy.classes.len() { return Some(reason("pack-id", "a component or class id that cannot name a file, two class ids equal in any case, or a class named classic")); } + // Floor names (§7.1) name paths too: each a path segment, no two equal in any case. + let names: Vec = policy.components.iter().flat_map(|c| crate::pack::floor_rungs(policy, &c.id).into_iter().map(|(_, n)| n)).collect(); + let folded: std::collections::BTreeSet = names.iter().map(|n| n.to_ascii_lowercase()).collect(); + if !names.iter().all(|n| crate::pack::id_is_path_segment(n)) || folded.len() != names.len() { + return Some(reason("pack-id", "a floor name that cannot name a file, or two floor names equal in any case")); + } if let Err(v) = crate::capcheck::check(policy) { return Some(reason(v[0].code.clone(), format!("ladder.{}", v[0].rung))); } @@ -246,6 +252,52 @@ pub fn verify_files(files: &BTreeMap>) -> Result>, parent: &BTreeMap>) -> Result> { + let rep = verify_files(files)?; + let one = |r: Reason| vec![r]; + verify_files(parent).map_err(|v| one(reason("repack-parent-invalid", v.iter().map(|r| r.to_string()).collect::>().join("; "))))?; + let receipt = |f: &BTreeMap>| PackageReceiptV2::from_json(std::str::from_utf8(&f["package.receipt.json"]).unwrap_or_default()).ok(); + let (Some(r), Some(pr)) = (receipt(files), receipt(parent)) else { return Err(one(reason("receipt-json", "a receipt that does not read"))) }; + let mismatch = |why: String| one(reason("repack-parent-mismatch", why)); + if r.parent != wai::package::Parent::RePack(pr.receipt_hash()) { + return Err(mismatch("the receipt does not name this parent as the package it re-packs".into())); + } + for c in &r.components { + let ix = StageIndex::parse(&files[&c.index_file()]).map_err(|e| one(reason(e.code, e.detail)))?; + let pix = parent.get(&c.index_file()).and_then(|b| StageIndex::parse(b).ok()).ok_or_else(|| mismatch(format!("the parent has no component {}", c.id)))?; + if ix.pieces.len() != pix.pieces.len() { + return Err(mismatch(format!("component {}: {} pieces, the parent's {}", c.id, ix.pieces.len(), pix.pieces.len()))); + } + // An entry's fields, with the entry it refines counted from its piece's first and + // its capability by name. + let fields = |x: &StageIndex, p: usize| -> Vec<(wsix::EntryRec, String)> { + let first = x.pieces[p].first_entry; + x.piece_entries(p).iter().filter(|e| matches!(e.role, Role::Base | Role::Refinement)).map(|e| { + let mut e = e.clone(); + if e.refines != wsix::NO_REFINES { + e.refines -= first; + } + let cap = x.capability(&e).to_string(); + e.capability = 0; + e.flags &= !wsix::ENTRY_DUP; + (e, cap) + }).collect() + }; + for p in 0..ix.pieces.len() { + let (a, b) = (&ix.pieces[p], &pix.pieces[p]); + if (a.start, a.duration, a.kind, a.flags) != (b.start, b.duration, b.kind, b.flags) || fields(&ix, p) != fields(&pix, p) { + return Err(mismatch(format!("component {} piece {p}: its base and refinements are not the parent's", c.id))); + } + } + } + Ok(rep) +} + /// The index against its component's policy (§11.3 step 6): `None` when it holds. pub fn index_against_policy(policy: &Policy, comp: &Component, ix: &StageIndex) -> Option { let bad = |why: String| Some(reason("index-policy-mismatch", format!("component {}: {why}", comp.id))); @@ -287,17 +339,25 @@ pub fn index_against_policy(policy: &Policy, comp: &Component, ix: &StageIndex) return bad(format!("piece {p}: {} units, past the policy's max_ms ({max} units, §4.4)", ix.pieces[p].duration)); } for (p, piece) in ix.pieces.iter().enumerate() { - let Some(kind) = PieceKind::from_code(piece.kind) else { return bad(format!("piece {p}: a kind a video piece does not have")) }; + // A floor-only component's pieces are of kind none, which no other piece is (§7.8). + let Some(kind) = PieceKind::from_code(piece.kind).filter(|k| (*k == PieceKind::None) == comp.floor_only()) else { + return bad(format!("piece {p}: a kind a piece of this component does not have")); + }; let pp = plan_piece(policy, tb, piece.start, piece.duration as u64, kind); if (pp.rules, pp.moments) != (piece.rules, piece.moments) { return bad(format!("piece {p}: its rules and moments are not §8.1's")); } - let Some(ladder) = crate::plan::piece_ladder(policy, &comp.id, &pp) else { return bad("no base rung".into()) }; - let mut want: Vec<(Role, &str, u8)> = vec![(Role::Base, ladder.base.capability.as_str(), 0)]; - for (k, r) in ladder.refinements.iter().enumerate() { - want.push((Role::Refinement, r.capability.as_str(), (k + 1).min(255) as u8)); + let mut want: Vec<(Role, &str, u8)> = Vec::new(); + if comp.floor_only() { + want.extend(crate::pack::floor_rungs(policy, &comp.id).into_iter().map(|(r, _)| (Role::Floor, r.capability.as_str(), 0))); + } else { + let Some(ladder) = crate::plan::piece_ladder(policy, &comp.id, &pp) else { return bad("no base rung".into()) }; + want.push((Role::Base, ladder.base.capability.as_str(), 0)); + for (k, r) in ladder.refinements.iter().enumerate() { + want.push((Role::Refinement, r.capability.as_str(), (k + 1).min(255) as u8)); + } + want.extend(ladder.floors.iter().map(|r| (Role::Floor, r.capability.as_str(), 0))); } - want.extend(ladder.floors.iter().map(|r| (Role::Floor, r.capability.as_str(), 0))); let got: Vec<(Role, &str, u8)> = ix.piece_entries(p).iter().map(|e| (e.role, ix.capability(e), e.layer)).collect(); if got != want { return bad(format!("piece {p}: its entries are not the rungs of its ladder (§5.7)")); @@ -379,7 +439,10 @@ fn verify_component( if !errs.is_empty() { return Err(errs); } - let (floor, produced) = floor_view(policy, comp, ix, &objects).map_err(|e| vec![e])?; + let (floors, produced) = floor_view(files, policy, comp, ix, &objects).map_err(|e| vec![e])?; + if policy.floor.cmaf() { + expected.insert(FLOOR_LISTING.to_string()); + } for (p, b) in &produced { expected.insert(p.clone()); if files.get(p) != Some(b) { @@ -390,7 +453,7 @@ fn verify_component( return Err(errs); } let (w, hgt) = dims.unwrap_or((0, 0)); - let view = CompView { index: ix, tb: Timebase { num: tb.num, den: tb.den }, w, h: hgt, objects: &objects, floor, base_cap: catalog::default_base_cap(policy, &comp.id) }; + let view = CompView { index: ix, tb: Timebase { num: tb.num, den: tb.den }, w, h: hgt, objects: &objects, floors, base_cap: catalog::default_base_cap(policy, &comp.id) }; catalog::render_all(&view, policy, policy_digest).map_err(|e| vec![reason("catalog-mismatch", format!("no catalog can be written: {}", e.0))]) } @@ -494,60 +557,139 @@ fn prefix_digests(media: &str, ix: &StageIndex, objects: &BTreeMap<[u8; 32], Vec if errs.is_empty() { Ok(dims) } else { Err(errs) } } -/// Before any frame is decoded (§11.3): each piece's floor object, where the policy has a -/// floor rung and the object's payload reads as §7.2's framing, holds one unit per frame -/// of its piece (`floor-object-mismatch`). A floor that does not read is left to +/// Before any frame is decoded (§11.3): each piece's floor objects, where the policy has +/// floor rungs and an object's payload reads as §7.2's framing, hold one unit per frame of +/// their piece (`floor-object-mismatch`). A floor that does not read is left to /// [`floor_view`], which runs when everything before it holds. fn floor_unit_counts(policy: &Policy, comp: &Component, ix: &StageIndex, objects: &BTreeMap<[u8; 32], Vec>) -> Option { policy.ladder.iter().find(|r| r.component == comp.id && r.role == RungRole::Floor)?; for p in 0..ix.pieces.len() { - let floors: Vec<&wsix::EntryRec> = ix.piece_entries(p).iter().filter(|e| e.role == Role::Floor).collect(); - let [fe] = floors.as_slice() else { continue }; - let Some(w) = objects.get(&fe.content_hash).and_then(|b| wai::container::Wai::from_bytes(b).ok()) else { continue }; - let Some((units, _)) = crate::floor::av1_units(&w.payload) else { continue }; - if units.len() as u64 != ix.pieces[p].duration as u64 { - return Some(reason("floor-object-mismatch", format!("piece {p}: {} floor units for {} frames, before any frame is decoded", units.len(), ix.pieces[p].duration))); + for fe in ix.piece_entries(p).iter().filter(|e| e.role == Role::Floor) { + let Some(w) = objects.get(&fe.content_hash).and_then(|b| wai::container::Wai::from_bytes(b).ok()) else { continue }; + let Some((units, _)) = crate::floor::av1_units(&w.payload) else { continue }; + if units.len() as u64 != ix.pieces[p].duration as u64 { + return Some(reason("floor-object-mismatch", format!("piece {p}: {} floor units for {} frames, before any frame is decoded", units.len(), ix.pieces[p].duration))); + } } } None } -type FloorFiles = (Option, Vec<(String, Vec)>); +type FloorFiles = (Vec, Vec<(String, Vec)>); -/// The floor of a component as its objects give it (§7.2, §7.3, §7.6), and the CMAF files -/// its temporal units give. Per piece: one floor object of the floor rung's capability, -/// presenting the piece's base (`floor-motif-mismatch`), whose renderer, settings, target -/// and framing header are §7.2's (`floor-object-mismatch`), one temporal unit per frame; -/// the first unit of the first piece carries a sequence header, every sequence header is -/// the first's bytes, and a shown key frame carries one (`floor-av1-malformed`); each -/// piece's first unit is a shown key frame, and no other is unless the policy allows extra -/// key frames (`floor-keyframe-mismatch`). -fn floor_view(policy: &Policy, comp: &Component, ix: &StageIndex, objects: &BTreeMap<[u8; 32], Vec>) -> Result { +/// The floors of a component as its objects give them (§7.2, §7.3, §7.6), and the CMAF +/// files their temporal units give, floor rung by floor rung in ladder order. Per piece +/// and floor rung: one floor object of the rung's capability, at its place among the +/// piece's floors, presenting the piece's base (`floor-motif-mismatch`), whose renderer, +/// settings, target and framing header are §7.2's (`floor-object-mismatch`), one temporal +/// unit per frame; the first unit of the rung's first piece carries a sequence header, +/// every sequence header is the first's bytes, and a shown key frame carries one +/// (`floor-av1-malformed`); each piece's first unit is a shown key frame, and no other is +/// unless the policy allows extra key frames (`floor-keyframe-mismatch`). +fn floor_view(files: &BTreeMap>, policy: &Policy, comp: &Component, ix: &StageIndex, objects: &BTreeMap<[u8; 32], Vec>) -> Result { + let mut views = Vec::new(); + let mut produced = Vec::new(); + for (k, (rung, name)) in crate::pack::floor_rungs(policy, &comp.id).into_iter().enumerate() { + let (v, got) = one_floor(files, policy, comp, ix, objects, rung, &name, k)?; + views.push(v); + produced.extend(got); + } + Ok((views, produced)) +} + +/// The package file that carries a supplied CMAF floor's listing (§7.7, §11.1). +pub const FLOOR_LISTING: &str = "floor.listing"; + +/// A floor rung's CMAF files in the CMAF form (§7.7, §11.3), bound to the receipt through +/// the supplied floor's listing: `floor.listing` hashes to the policy's +/// `floor.supplied_listing` and is a listing; `floor//init.mp4` is its line for +/// `/init.mp4` and an initialization segment §7.7 reads; and the listing's media +/// segments of `/`, in its order, are one per piece, piece *k*'s +/// `floor//.m4s`, §7.7's boxes, opening on a sync sample, every sample +/// one frame and the first at the piece's start, its samples the piece's floor units +/// without their temporal delimiters (`floor-cmaf-mismatch`). Returns the files, so they +/// are the package's expected ones, and each piece's fragment. +#[allow(clippy::type_complexity)] +pub fn cmaf_supplied(files: &BTreeMap>, policy: &Policy, name: &str, ix: &StageIndex, tb: Timebase, pieces: &[crate::floor::FloorPiece]) -> Result<(Vec<(String, Vec)>, Vec<(String, u64)>), Reason> { + let bad = |why: String| reason("floor-cmaf-mismatch", why); + let lb = files.get(FLOOR_LISTING).ok_or_else(|| bad(format!("no {FLOOR_LISTING}")))?; + if Some(crate::sha256_hex(lb)) != policy.floor.supplied_listing { + return Err(bad(format!("{FLOOR_LISTING} does not hash to the policy's floor.supplied_listing"))); + } + let lines = wai::pin::parse_listing(lb).map_err(|e| bad(format!("{FLOOR_LISTING}: {e}")))?; + let shas: BTreeMap = lines.iter().map(|(p, d)| (p.clone(), crate::hex(d))).collect(); + let (init_file, segs) = crate::floor::cmaf_files_of(&shas, name).map_err(|r| bad(r.detail))?; + let init_path = format!("floor/{name}/init.mp4"); + let ib = files.get(&init_path).ok_or_else(|| bad(format!("no {init_path}")))?; + if crate::sha256_hex(ib) != shas[&init_file] { + return Err(bad(format!("{init_path} is not the supplied {init_file}"))); + } + let init = crate::cmaf_read::read_init(ib).map_err(|f| bad(format!("{init_path}: {}", f.detail())))?; + if segs.len() != pieces.len() { + return Err(bad(format!("{} media segments of {name}/ for {} pieces", segs.len(), pieces.len()))); + } + let mut produced = vec![(init_path, ib.clone())]; + let mut frags = Vec::with_capacity(pieces.len()); + for (p, (piece, file)) in pieces.iter().zip(&segs).enumerate() { + let path = format!("floor/{name}/{}.m4s", shas[file]); + let b = files.get(&path).ok_or_else(|| bad(format!("piece {p}: no {path}, the supplied {file}")))?; + if crate::sha256_hex(b) != shas[file] { + return Err(bad(format!("{path}: not named by its SHA-256"))); + } + let samples = crate::cmaf_read::read_segment(b, &init).map_err(|f| bad(format!("{path}: {}", f.detail())))?; + if samples[0].flags & crate::cmaf_read::NON_SYNC_BIT != 0 { + return Err(bad(format!("{path}: a segment that does not open on a sync sample"))); + } + crate::cmaf_read::check_times(&samples, init.timescale, tb.num, tb.den, ix.pieces[p].start).map_err(|f| bad(format!("{path}: {}", f.detail())))?; + if crate::cmaf_read::units(b, &samples) != piece.units { + return Err(bad(format!("{path}: its samples are not piece {p}'s floor units"))); + } + produced.push((path.clone(), b.clone())); + frags.push((path, b.len() as u64)); + } + Ok((produced, frags)) +} + +/// One floor's view and the CMAF files it gives. +type OneFloor = (FloorView, Vec<(String, Vec)>); + +/// [`floor_view`] for the `k`-th floor rung, `rung`, of floor name `name`. +#[allow(clippy::too_many_arguments)] +fn one_floor(files: &BTreeMap>, policy: &Policy, comp: &Component, ix: &StageIndex, objects: &BTreeMap<[u8; 32], Vec>, rung: &crate::policy::Rung, name: &str, k: usize) -> Result { let tb = comp.timebase; - let rung = policy.ladder.iter().find(|r| r.component == comp.id && r.role == RungRole::Floor); - let Some(rung) = rung else { return Ok((None, Vec::new())) }; let object = |why: String| reason("floor-object-mismatch", why); let listing = policy.floor.supplied_listing.clone().ok_or_else(|| object("a supplied floor with no listing in the policy".into()))?; let df_want = crate::objects::supplied_derived_from(&[0u8; 32], "", &listing); + let n_floors = crate::pack::floor_rungs(policy, &comp.id).len(); let mut pieces = Vec::new(); let mut seq: Option<(crate::av1::SequenceHeader, Vec)> = None; // The objects first: their members, framing and units. for p in 0..ix.pieces.len() { let es = ix.piece_entries(p); let floors: Vec<&wsix::EntryRec> = es.iter().filter(|e| e.role == Role::Floor).collect(); - let [fe] = floors.as_slice() else { return Err(object(format!("piece {p}: not one floor object"))) }; + if floors.len() != n_floors { + return Err(object(format!("piece {p}: not one floor object per floor rung"))); + } + let fe = floors[k]; if ix.capability(fe) != rung.capability { - return Err(object(format!("piece {p}: the floor is not the floor rung's capability"))); + return Err(object(format!("piece {p}: floor {name} is not its floor rung's capability"))); } - let base = es.iter().find(|e| e.role == Role::Base).ok_or_else(|| reason("floor-motif-mismatch", format!("piece {p} has no base")))?; let bytes = objects.get(&fe.content_hash).ok_or_else(|| object(format!("piece {p}: the floor object is not held")))?; let w = wai::container::Wai::from_bytes(bytes).map_err(|e| object(e.to_string()))?; - let df = w.manifest.derived_from.as_ref().and_then(|v| wai::container::DerivedFrom::from_value(v).ok()).ok_or_else(|| reason("floor-motif-mismatch", format!("piece {p}")))?; - if df.motif != crate::hex(&base.content_hash) || df.motif_capability != ix.capability(base) { - return Err(reason("floor-motif-mismatch", format!("piece {p}: the floor does not present its piece's base"))); - } - if df.renderer != df_want.renderer || df.settings != df_want.settings { - return Err(object(format!("piece {p}: a supplied floor's renderer and settings are §7.2's"))); + if comp.floor_only() { + // §7.8: a floor that presents no base carries no derived_from. + if w.manifest.derived_from.as_ref().is_some_and(|v| !v.is_null()) { + return Err(object(format!("piece {p}: a floor-only floor names a motif"))); + } + } else { + let base = es.iter().find(|e| e.role == Role::Base).ok_or_else(|| reason("floor-motif-mismatch", format!("piece {p} has no base")))?; + let df = w.manifest.derived_from.as_ref().and_then(|v| wai::container::DerivedFrom::from_value(v).ok()).ok_or_else(|| reason("floor-motif-mismatch", format!("piece {p}")))?; + if df.motif != crate::hex(&base.content_hash) || df.motif_capability != ix.capability(base) { + return Err(reason("floor-motif-mismatch", format!("piece {p}: the floor does not present its piece's base"))); + } + if df.renderer != df_want.renderer || df.settings != df_want.settings { + return Err(object(format!("piece {p}: a supplied floor's renderer and settings are §7.2's"))); + } } let (units, hdr) = crate::floor::av1_units(&w.payload).ok_or_else(|| object(format!("piece {p}: the floor's payload is not §7.2's framing")))?; if units.len() as u64 != ix.pieces[p].duration as u64 { @@ -592,20 +734,23 @@ fn floor_view(policy: &Policy, comp: &Component, ix: &StageIndex, objects: &BTre } pieces.push(crate::floor::FloorPiece { units, key }); } - let Some((s, obu)) = seq else { return Ok((None, Vec::new())) }; + let (s, obu) = seq.ok_or_else(|| object("a component with no piece".into()))?; let fl = crate::floor::Floor { seq: s, seq_obu: obu, pieces }; let starts: Vec = ix.pieces.iter().map(|p| p.start).collect(); - let (init_path, init, frags) = crate::pack::cmaf_files(&comp.id, &fl, &starts, tb.den, tb.num).map_err(|e| reason("floor-cmaf-mismatch", e))?; - let view = FloorView { - codec: fl.seq.codec_string(), - w: fl.seq.max_frame_width, - h: fl.seq.max_frame_height, - init: init_path.clone(), - fragments: frags.iter().map(|(p, b)| (p.clone(), b.len() as u64)).collect(), + let init_path = format!("floor/{name}/init.mp4"); + // The CMAF form's files are the ladder's own, read (§7.7); the OBU form's, rebuilt + // from the units (§7.6). + let (produced, fragments) = if policy.floor.cmaf() { + cmaf_supplied(files, policy, name, ix, tb, &fl.pieces)? + } else { + let (init_path, init, frags) = crate::pack::cmaf_files(name, &fl, &starts, tb.den, tb.num).map_err(|e| reason("floor-cmaf-mismatch", e))?; + let fragments = frags.iter().map(|(p, b)| (p.clone(), b.len() as u64)).collect(); + let mut produced = vec![(init_path, init)]; + produced.extend(frags); + (produced, fragments) }; - let mut produced = vec![(init_path, init)]; - produced.extend(frags); - Ok((Some(view), produced)) + let view = FloorView { name: name.to_string(), codec: fl.seq.codec_string(), w: fl.seq.max_frame_width, h: fl.seq.max_frame_height, init: init_path, fragments }; + Ok((view, produced)) } fn check_claim(r: &PackageReceiptV2, indexes: &[&[u8]], b: &[u8]) -> Result<(), String> { diff --git a/wai/wai-pack/tests/cmaf.rs b/wai/wai-pack/tests/cmaf.rs new file mode 100644 index 000000000..acf48c32a --- /dev/null +++ b/wai/wai-pack/tests/cmaf.rs @@ -0,0 +1,174 @@ +//! A supplied floor in the CMAF form (packaging §7.7): the golden `video-cmaf-01` takes +//! `video-ladder-01`'s two AV1 streams as a media muxer wrote them into fMP4 segments, and +//! its floor's CMAF files are those segments, byte for byte, bound to the receipt by the +//! supplied floor's listing, which the package carries. Each fault of a supplied CMAF floor +//! is refused by name, and a package whose segments are not the supplied ones is refused. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use wai::stage_index::{Role, StageIndex}; + +type Files = BTreeMap>; + +fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../pack-conformance") +} + +fn held(p: &Path) -> Files { + wai::pin::read_dir_set(p).unwrap() +} + +fn golden() -> Files { + held(&corpus().join("packs/video-cmaf-01/package")) +} + +fn supplied() -> Files { + held(&corpus().join("packs/video-cmaf-01/floor")) +} + +fn policy() -> String { + String::from_utf8(std::fs::read(corpus().join("packs/video-cmaf-01/policy.toml")).unwrap()).unwrap() +} + +/// Pack the golden's fixture under `policy` over the supplied floor `floor` (its listing +/// pinned in the policy). +fn pack(policy: &str, floor: Files) -> Result { + let listing = wai_pack::floor::listing_sha256(&floor).unwrap(); + let at = policy.find("supplied_listing = \"").unwrap() + "supplied_listing = \"".len(); + let policy = format!("{}{listing}{}", &policy[..at], &policy[at + 64..]); + let mut o = wai_pack::pack::PackOpts::new(ed25519_dalek::SigningKey::from_bytes(&[0x5a; 32]), "did:key:test", floor); + let mut src = BTreeMap::new(); + src.insert("v".to_string(), wai_pack::fixture::generate("video-base-01").unwrap()); + wai_pack::pack::pack(policy.as_bytes(), &src, &mut o) +} + +fn code(r: Result) -> String { + r.map(|_| "ok".to_string()).unwrap_or_else(|e| e.code) +} + +/// Where box `kind` begins in `b` (its size field). +fn box_at(b: &[u8], kind: &[u8; 4]) -> usize { + b.windows(4).position(|w| w == kind).unwrap() - 4 +} + +/// The floor's CMAF files are the supplied ones, byte for byte, and `floor.listing` is the +/// supplied listing; each floor object holds its piece's samples as temporal units, which +/// are exactly the OBU form's units of the same streams (`video-ladder-01`'s). +#[test] +fn a_cmaf_floor_is_the_ladder_s_own_bytes() { + let pkg = golden(); + wai_pack::verify::verify_files(&pkg).unwrap(); + let sup = supplied(); + let view: BTreeMap = sup.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); + assert_eq!(pkg["floor.listing"], wai::pin::listing_from(&view).unwrap()); + let ix = StageIndex::parse(&pkg["index-v.wsix"]).unwrap(); + let ladder = held(&corpus().join("packs/video-ladder-01/floor")); + for (k, name) in ["hi", "lo"].iter().enumerate() { + assert_eq!(pkg[&format!("floor/{name}/init.mp4")], sup[&format!("{name}/init.mp4")]); + let segs: Vec<&String> = sup.keys().filter(|p| p.starts_with(&format!("{name}/seg_"))).collect(); + assert_eq!(segs.len(), ix.pieces.len()); + let tus: Vec> = wai_pack::av1::temporal_units(&ladder[&format!("{name}.obu")]).unwrap().iter().map(|t| t.bytes()).collect(); + for (p, seg) in segs.iter().enumerate() { + let b = &sup[*seg]; + assert_eq!(&pkg[&format!("floor/{name}/{}.m4s", wai_pack::sha256_hex(b))], b, "{seg}, as supplied"); + let fe = ix.piece_entries(p).iter().filter(|e| e.role == Role::Floor).nth(k).unwrap(); + let w = wai::container::Wai::from_bytes(&pkg[&wai_pack::objects::object_path(&fe.content_hash)]).unwrap(); + let (units, _) = wai_pack::floor::av1_units(&w.payload).unwrap(); + let (s, n) = (ix.pieces[p].start as usize, ix.pieces[p].duration as usize); + assert_eq!(units, tus[s..s + n].to_vec(), "piece {p} of {name}: the stream's own units"); + } + } + let master = String::from_utf8(pkg["hls/classic/master.m3u8"].clone()).unwrap(); + assert!(master.contains("../classic/floor-hi.m3u8") && master.contains("../classic/floor-lo.m3u8")); +} + +/// §7.7's refusals, each before anything is written. +#[test] +fn a_supplied_cmaf_floor_s_faults_are_refused() { + let p = policy(); + let sup = supplied(); + assert_eq!(code(pack(&p, sup.clone())), "ok"); + let with = |edit: &dyn Fn(&mut Files)| { + let mut f = sup.clone(); + edit(&mut f); + code(pack(&p, f)) + }; + assert_eq!(with(&|f| { f.remove("lo/init.mp4"); }), "floor-stream-missing"); + assert_eq!(with(&|f| { f.insert("hi/index.m3u8".into(), b"#EXTM3U\n".to_vec()); }), "floor-cmaf-malformed"); + assert_eq!(with(&|f| { f.insert("hi/x/seg.m4s".into(), Vec::new()); }), "floor-cmaf-malformed", "a segment in a directory below"); + assert_eq!(with(&|f| { + let b = f.get_mut("hi/seg_0002.m4s").unwrap(); + b[..4].copy_from_slice(&0u32.to_be_bytes()); + }), "floor-cmaf-malformed", "a box of size 0"); + assert_eq!(with(&|f| { + let b = f.get_mut("hi/seg_0002.m4s").unwrap(); + let t = box_at(b, b"trun"); + b[t + 24..t + 28].copy_from_slice(&0x0101_0000u32.to_be_bytes()); + }), "floor-cmaf-malformed", "a segment that does not open on a sync sample"); + assert_eq!(with(&|f| { + let b = f.get_mut("hi/seg_0002.m4s").unwrap(); + let t = box_at(b, b"tfhd"); + let d = u32::from_be_bytes(b[t + 16..t + 20].try_into().unwrap()); + b[t + 16..t + 20].copy_from_slice(&(d + 1).to_be_bytes()); + }), "floor-cmaf-timing", "a sample longer than one frame"); + assert_eq!(with(&|f| { f.remove("lo/seg_0004.m4s"); }), "floor-frame-count"); + // Pieces of at most two seconds: the first segment holds two pieces. + let short = p.replace("max_ms = 4000", "max_ms = 2000").replace("target_ms = 2000", "target_ms = 1000"); + assert_eq!(code(pack(&short, sup.clone())), "floor-cmaf-pieces"); + // Without supplied_form, the floor is read in the OBU form, which holds no stream. + assert_eq!(code(pack(&p.replace("supplied_form = \"cmaf\"\n", ""), sup)), "floor-stream-missing"); +} + +/// A package whose segment is not the supplied one is refused, though it is a segment of +/// the same samples: the receipt binds the supplied bytes through `floor.listing`. +#[test] +fn the_package_binds_the_supplied_bytes() { + let mut f = golden(); + let seg = f.keys().find(|k| k.starts_with("floor/hi/") && k.ends_with(".m4s")).unwrap().clone(); + let mut b = vec![0, 0, 0, 8, b'f', b'r', b'e', b'e']; + b.extend_from_slice(&f[&seg]); + f.remove(&seg); + f.insert(format!("floor/hi/{}.m4s", wai_pack::sha256_hex(&b)), b); + let key = wai_pack::unhex(std::fs::read_to_string(corpus().join("packs/video-cmaf-01/key.hex")).unwrap().trim()).unwrap(); + wai_pack::golden::reseal(&mut f, &ed25519_dalek::SigningKey::from_bytes(&key.try_into().unwrap())).unwrap(); + let got: Vec = wai_pack::verify::verify_files(&f).err().unwrap_or_default().into_iter().map(|r| r.code).collect(); + assert_eq!(got.first().map(String::as_str), Some("floor-cmaf-mismatch"), "{got:?}"); +} + +/// Packed into a directory with `link_supplied`, the floor's CMAF files are the supplied +/// files themselves (hard links where the file system allows): the package verifies, and +/// on a file system of inodes each shares the supplied file's. +#[test] +fn a_linked_floor_is_the_supplied_files() { + let dir = std::env::temp_dir().join(format!("wai-pack-cmaf-link-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let policy = std::fs::read(corpus().join("packs/video-cmaf-01/policy.toml")).unwrap(); + let key = wai_pack::unhex(std::fs::read_to_string(corpus().join("packs/video-cmaf-01/key.hex")).unwrap().trim()).unwrap(); + let key = ed25519_dalek::SigningKey::from_bytes(&key.try_into().unwrap()); + let id = wai::signer_id::did_key(&key.verifying_key().to_bytes()); + // The supplied floor copied beside the output, so a link is on one file system. + let sup = dir.join("floor"); + for (p, b) in supplied() { + std::fs::create_dir_all(sup.join(&p).parent().unwrap()).unwrap(); + std::fs::write(sup.join(&p), b).unwrap(); + } + let mut o = wai_pack::pack::PackOpts::with_floor(key, id, wai_pack::files::FileSet::Dir(sup.clone())); + o.link_supplied = true; + let fixture = wai_pack::fixture::generate("video-base-01").unwrap(); + let mut src = BTreeMap::new(); + src.insert("v".to_string(), wai_pack::files::Input::Bytes(&fixture)); + let out = dir.join("out"); + let mut sink = wai_pack::files::DirSink::new(&out).unwrap(); + wai_pack::pack::pack_into(&policy, &src, &mut o, &mut sink).unwrap(); + sink.keep(); + assert_eq!(held(&out), golden(), "the golden, byte for byte"); + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt as _; + let ino = |p: &Path| std::fs::metadata(p).unwrap().ino(); + assert_eq!(ino(&out.join("floor/hi/init.mp4")), ino(&sup.join("hi/init.mp4"))); + let s0 = std::fs::read(sup.join("lo/seg_0000.m4s")).unwrap(); + assert_eq!(ino(&out.join(format!("floor/lo/{}.m4s", wai_pack::sha256_hex(&s0)))), ino(&sup.join("lo/seg_0000.m4s"))); + } + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/wai/wai-pack/tests/floor_only.rs b/wai/wai-pack/tests/floor_only.rs new file mode 100644 index 000000000..1606cf9f1 --- /dev/null +++ b/wai/wai-pack/tests/floor_only.rs @@ -0,0 +1,109 @@ +//! The floor-only profile (packaging §7.8): a component of floor rungs alone, with no +//! integer base and no source. The goldens `video-floor-only-01` (the CMAF form) and +//! `video-floor-only-02` (the OBU form) carry the ladder's two floors and nothing else; +//! their pieces are the first floor rung's; and what the profile refuses is refused by +//! name. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use wai::stage_index::{Role, StageIndex, PIECE_KIND_NONE}; + +type Files = BTreeMap>; + +fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../pack-conformance") +} + +fn held(p: &Path) -> Files { + wai::pin::read_dir_set(p).unwrap() +} + +fn policy(g: &str) -> String { + String::from_utf8(std::fs::read(corpus().join(format!("packs/{g}/policy.toml"))).unwrap()).unwrap() +} + +/// Pack `policy` over the supplied floor `floor` (its listing pinned), with no source. +fn pack(policy: &str, floor: Files, base_from: Option) -> Result { + let listing = wai_pack::floor::listing_sha256(&floor).unwrap(); + let at = policy.find("supplied_listing = \"").unwrap() + "supplied_listing = \"".len(); + let policy = format!("{}{listing}{}", &policy[..at], &policy[at + 64..]); + let mut o = wai_pack::pack::PackOpts::new(ed25519_dalek::SigningKey::from_bytes(&[0x5a; 32]), "did:key:test", floor); + o.base_from = base_from.map(wai_pack::files::FileSet::Memory); + wai_pack::pack::pack(policy.as_bytes(), &BTreeMap::new(), &mut o) +} + +fn code(r: Result) -> String { + r.map(|_| "ok".to_string()).unwrap_or_else(|e| e.code) +} + +/// Each piece is its two floors, presenting no base (no derived_from), of kind none; the +/// pieces are the first floor's segments or key frames, which are the ladder golden's +/// plan; the floor objects hold the same units as the ladder golden's; no class lists a +/// WAI object, so there is no WAI playlist, and the MoQ catalog is the floors' tracks. +#[test] +fn a_floor_only_package_is_its_floors_alone() { + let ladder = held(&corpus().join("packs/video-ladder-01/package")); + let lix = StageIndex::parse(&ladder["index-v.wsix"]).unwrap(); + for g in ["video-floor-only-01", "video-floor-only-02"] { + let pkg = held(&corpus().join(format!("packs/{g}/package"))); + wai_pack::verify::verify_files(&pkg).unwrap(); + let ix = StageIndex::parse(&pkg["index-v.wsix"]).unwrap(); + assert_eq!(ix.pieces.iter().map(|p| (p.start, p.duration)).collect::>(), lix.pieces.iter().map(|p| (p.start, p.duration)).collect::>(), "{g}: the ladder's plan"); + for (p, piece) in ix.pieces.iter().enumerate() { + assert_eq!((piece.kind, piece.flags), (PIECE_KIND_NONE, 0)); + let es = ix.piece_entries(p); + assert_eq!(es.iter().map(|e| e.role).collect::>(), vec![Role::Floor, Role::Floor], "{g} piece {p}"); + for (k, e) in es.iter().enumerate() { + assert_eq!(e.flags & wai::stage_index::ENTRY_DERIVED_FROM, 0); + let w = wai::container::Wai::from_bytes(&pkg[&wai_pack::objects::object_path(&e.content_hash)]).unwrap(); + assert!(w.manifest.derived_from.is_none()); + let lw = wai::container::Wai::from_bytes(&ladder[&wai_pack::objects::object_path(&lix.piece_entries(p)[1 + k].content_hash)]).unwrap(); + assert_eq!(w.payload, lw.payload, "{g} piece {p}: the ladder golden's units"); + } + } + assert!(!pkg.keys().any(|k| k.ends_with("/wai.m3u8") || k.starts_with("dash/all")), "{g}: no WAI view"); + let moq: serde_json::Value = serde_json::from_slice(&pkg["moq/catalog.json"]).unwrap(); + let names: Vec<&str> = moq["tracks"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect(); + assert_eq!(names, vec!["hi/floor", "lo/floor"]); + } +} + +/// §3.4 and §7.8's refusals. +#[test] +fn what_the_floor_only_profile_refuses() { + let p = policy("video-floor-only-02"); + let fl = held(&corpus().join("packs/video-floor-only-02/floor")); + assert_eq!(code(pack(&p, fl.clone(), None)), "ok"); + let base_rung = "\n[[ladder]]\nid = \"b0\"\ncomponent = \"v\"\ncapability = \"wai.video.int_motion\"\nrole = \"base\"\n\n[floor]"; + assert_eq!(code(pack(&p.replacen("\n[floor]", base_rung, 1), fl.clone(), None)), "policy-floor-only", "a base rung"); + assert_eq!(code(pack(&p.replace("profile = \"floor-only\"", "profile = \"floor-only\"\nsource = \"v.y4m\""), fl.clone(), None)), "policy-floor-only", "a source"); + assert_eq!(code(pack(&p.replace("profile = \"floor-only\"", "profile = \"other\""), fl.clone(), None)), "policy-value"); + assert_eq!(code(pack(&p.replace("profile = \"floor-only\"\n", ""), fl.clone(), None)), "policy-no-base", "the standard profile needs a base"); + // A rule's ladder is every floor rung, and only them. + let rule = |l: &str| format!("{p}\n[[rule]]\nid = \"r\"\nthen = {{ ladder = [{l}] }}\n"); + assert_eq!(code(pack(&rule("\"hi\", \"lo\""), fl.clone(), None)), "ok"); + assert_eq!(code(pack(&rule("\"hi\""), fl.clone(), None)), "policy-rule-ladder"); + // A piece past max_ms: the first floor's first piece is 60 frames, 2.4 s. + assert_eq!(code(pack(&p.replace("max_ms = 4000", "max_ms = 2000\nmin_ms = 1000\ntarget_ms = 2000"), fl.clone(), None)), "floor-piece-length"); + // The first floor rung decides the plan: one that opens on an inter frame has none. The + // stream opens on frame 1, its sequence header put after its delimiter (a first unit + // without one is refused before, floor-av1-malformed). + let tus = wai_pack::av1::temporal_units(&fl["hi.obu"]).unwrap(); + let seq = tus[0].sequence_header().unwrap().bytes.to_vec(); + let hi: Vec> = tus.iter().map(|t| t.bytes()).collect(); + let mut opened = hi[1..].to_vec(); + opened[0] = [&opened[0][..2], &seq[..], &opened[0][2..]].concat(); + let mut off = fl.clone(); + off.insert("hi.obu".into(), opened.concat()); + assert_eq!(code(pack(&p, off, None)), "floor-keyframe-mismatch"); + let mut cut = fl.clone(); + cut.insert("hi.obu".into(), hi[1..].concat()); + assert_eq!(code(pack(&p, cut, None)), "floor-av1-malformed", "a first unit without a sequence header"); + // The other floor rungs are held to it. + let mut short = fl.clone(); + let lo: Vec> = wai_pack::av1::temporal_units(&fl["lo.obu"]).unwrap().iter().map(|t| t.bytes()).collect(); + short.insert("lo.obu".into(), lo[..lo.len() - 1].concat()); + assert_eq!(code(pack(&p, short, None)), "floor-frame-count"); + // Nothing to re-pack: a floor-only component has no base. + assert_eq!(code(pack(&p, fl, Some(held(&corpus().join("packs/video-base-01/package"))))), "repack-parent-mismatch"); +} diff --git a/wai/wai-pack/tests/ladder.rs b/wai/wai-pack/tests/ladder.rs new file mode 100644 index 000000000..11bd737f6 --- /dev/null +++ b/wai/wai-pack/tests/ladder.rs @@ -0,0 +1,157 @@ +//! Several floor rungs in one package (packaging §7.1): the golden `video-ladder-01` of +//! `wai/pack-conformance` carries two AV1 floors over one integer base, which is +//! `video-base-01`'s, object for object; the catalogs list both floors; floor names that +//! cannot name a file, and each floor's streams, are refused by name. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use wai::stage_index::{Role, StageIndex}; +use wai_pack::golden; + +type Files = BTreeMap>; + +fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../pack-conformance") +} + +fn held(p: &Path) -> Files { + wai::pin::read_dir_set(p).unwrap() +} + +fn ladder_package() -> Files { + held(&corpus().join("packs/video-ladder-01/package")) +} + +fn ladder_policy() -> String { + String::from_utf8(std::fs::read(corpus().join("packs/video-ladder-01/policy.toml")).unwrap()).unwrap() +} + +fn ladder_floor() -> Files { + held(&corpus().join("packs/video-ladder-01/floor")) +} + +/// Pack the ladder golden's fixture under `policy` (its listing pinned to `floor`'s). +fn pack(policy: &str, floor: Files) -> Result { + let listing = wai_pack::floor::listing_sha256(&floor).unwrap(); + let at = policy.find("supplied_listing = \"").unwrap() + "supplied_listing = \"".len(); + let policy = format!("{}{listing}{}", &policy[..at], &policy[at + 64..]); + let mut o = wai_pack::pack::PackOpts::new(ed25519_dalek::SigningKey::from_bytes(&[0x5a; 32]), "did:key:test", floor); + let mut src = BTreeMap::new(); + src.insert("v".to_string(), wai_pack::fixture::generate("video-base-01").unwrap()); + wai_pack::pack::pack(policy.as_bytes(), &src, &mut o) +} + +fn code(r: Result) -> String { + r.map(|_| "ok".to_string()).unwrap_or_else(|e| e.code) +} + +/// One base per piece under two floors, in ladder order; the bases are `video-base-01`'s +/// objects (the same fixture and base rung); each floor object presents its piece's base; +/// the classical master lists one variant per floor, each its own size; and each floor's +/// fragments hold every frame of its piece. +#[test] +fn a_ladder_is_one_base_under_its_floors() { + let pkg = ladder_package(); + wai_pack::verify::verify_files(&pkg).unwrap(); + let ix = StageIndex::parse(&pkg["index-v.wsix"]).unwrap(); + let base_pkg = held(&corpus().join("packs/video-base-01/package")); + let base_ix = StageIndex::parse(&base_pkg["index-v.wsix"]).unwrap(); + for p in 0..ix.pieces.len() { + let es = ix.piece_entries(p); + assert_eq!(es.iter().map(|e| e.role).collect::>(), vec![Role::Base, Role::Floor, Role::Floor], "piece {p}"); + assert_eq!(es[0].content_hash, base_ix.piece_entries(p)[0].content_hash, "piece {p}: video-base-01's base"); + assert_ne!(es[1].content_hash, es[2].content_hash, "two renditions"); + for fe in &es[1..] { + let w = wai::container::Wai::from_bytes(&pkg[&wai_pack::objects::object_path(&fe.content_hash)]).unwrap(); + let df = wai::container::DerivedFrom::from_value(w.manifest.derived_from.as_ref().unwrap()).unwrap(); + assert_eq!(df.motif, wai_pack::hex(&es[0].content_hash), "piece {p}: the floor presents the piece's base"); + } + } + let master = String::from_utf8(pkg["hls/classic/master.m3u8"].clone()).unwrap(); + let variants: Vec<&str> = master.lines().filter(|l| !l.starts_with('#')).collect(); + assert_eq!(variants, vec!["../classic/floor-hi.m3u8", "../classic/floor-lo.m3u8"]); + assert!(master.contains("RESOLUTION=64x36") && master.contains("RESOLUTION=32x18"), "{master}"); + for c in ["tv", "mobile", "legacy"] { + let m = String::from_utf8(pkg[&format!("hls/{c}/master.m3u8")].clone()).unwrap(); + wai::http_binding::hls::classical_unchanged(&master, &m).unwrap(); + } + for name in ["hi", "lo"] { + let pl = String::from_utf8(pkg[&format!("hls/classic/floor-{name}.m3u8")].clone()).unwrap(); + let frags: Vec<&str> = pl.lines().filter(|l| !l.starts_with('#')).map(|l| l.trim_start_matches("../../")).collect(); + assert_eq!(frags.len(), ix.pieces.len()); + for (p, f) in frags.iter().enumerate() { + assert!(f.starts_with(&format!("floor/{name}/")), "{f}"); + let top = wai_pack::cmaf::boxes(&pkg[*f]).unwrap(); + let traf = wai_pack::cmaf::boxes(top[0].1).unwrap().into_iter().find(|(k, _)| k == b"traf").unwrap().1; + let trun = wai_pack::cmaf::boxes(traf).unwrap().into_iter().find(|(k, _)| k == b"trun").unwrap().1; + assert_eq!(u32::from_be_bytes(trun[4..8].try_into().unwrap()), ix.pieces[p].duration); + } + // A WAI media playlist is adjacent to every floor's, which share the pieces' + // durations. + wai::http_binding::hls::check_adjacent(std::str::from_utf8(&pkg["hls/tv/wai-v.m3u8"]).unwrap(), &pl).unwrap(); + } + let dash = String::from_utf8(pkg["dash/classic.mpd"].clone()).unwrap(); + assert!(dash.contains(" = moq["tracks"].as_array().unwrap().iter().filter(|t| t["packaging"] == "cmaf").map(|t| t["name"].as_str().unwrap()).collect(); + assert_eq!(floors, vec!["hi/floor", "lo/floor"]); + // The same inputs pack to the same bytes. + assert_eq!(golden::pack_golden(&corpus(), "video-ladder-01").unwrap().files, pkg); +} + +/// §7.1, §7.3, §11.1: with several floor rungs each stream is `.obu`, each is +/// checked as one floor is, and a floor name that cannot name a file, or two equal in any +/// case, is refused (`pack-id`); with one floor rung, the stream is the component's. +#[test] +fn each_floor_is_checked_and_named() { + let p = ladder_policy(); + let fl = ladder_floor(); + assert_eq!(code(pack(&p, fl.clone())), "ok"); + // A floor rung whose id names no file, and two that fold to one name. + assert_eq!(code(pack(&p.replace("id = \"lo\"", "id = \"l o\""), fl.clone())), "pack-id"); + assert_eq!(code(pack(&p.replace("id = \"lo\"", "id = \"HI\""), fl.clone())), "pack-id"); + // One floor rung: its stream is the component's, v.obu. + let one = p.replace("[[ladder]]\nid = \"lo\"\ncomponent = \"v\"\ncapability = \"wai.video.av1\"\nrole = \"floor\"\nparams = { source = \"supplied\" }\n\n", ""); + assert_ne!(one, p); + let only_hi: Files = [("hi.obu".to_string(), fl["hi.obu"].clone())].into(); + assert_eq!(code(pack(&one, only_hi)), "floor-stream-missing"); + let as_v: Files = [("v.obu".to_string(), fl["hi.obu"].clone())].into(); + assert_eq!(code(pack(&one, as_v)), "ok"); + // The second floor's stream: missing, a unit short, or a piece opening on an inter + // frame. + let mut no_lo = fl.clone(); + no_lo.remove("lo.obu"); + assert_eq!(code(pack(&p, no_lo)), "floor-stream-missing"); + let lo: Vec> = wai_pack::av1::temporal_units(&fl["lo.obu"]).unwrap().iter().map(|t| t.bytes()).collect(); + let mut short = fl.clone(); + short.insert("lo.obu".into(), lo[..lo.len() - 1].concat()); + assert_eq!(code(pack(&p, short)), "floor-frame-count"); + let mut inter = lo.clone(); + inter[85] = lo[86].clone(); + let mut off = fl.clone(); + off.insert("lo.obu".into(), inter.concat()); + assert_eq!(code(pack(&p, off)), "floor-keyframe-mismatch"); +} + +/// §11.3: a floor object at another floor rung's place is refused: piece 2's two floors +/// swapped in the index, the package re-sealed, gives the first floor an object whose +/// framing header is not its sequence header's size (`floor-object-mismatch`, which §11.3 +/// checks before the units' sequence headers). +#[test] +fn a_floor_at_another_rung_s_place_is_refused() { + let mut f = ladder_package(); + let mut ix = StageIndex::parse(&f["index-v.wsix"]).unwrap(); + let at = ix.pieces[2].first_entry as usize; + ix.entries.swap(at + 1, at + 2); + f.insert("index-v.wsix".into(), ix.to_bytes()); + let key = ed25519_dalek::SigningKey::from_bytes(&golden_key()); + golden::reseal(&mut f, &key).unwrap(); + let got: Vec = wai_pack::verify::verify_files(&f).err().unwrap_or_default().into_iter().map(|r| r.code).collect(); + assert_eq!(got, vec!["floor-object-mismatch"]); +} + +fn golden_key() -> [u8; 32] { + let t = std::fs::read_to_string(corpus().join("packs/video-ladder-01/key.hex")).unwrap(); + let b = wai_pack::unhex(t.trim()).unwrap(); + b.try_into().unwrap() +} diff --git a/wai/wai-pack/tests/pack_alloc.rs b/wai/wai-pack/tests/pack_alloc.rs new file mode 100644 index 000000000..cda015d30 --- /dev/null +++ b/wai/wai-pack/tests/pack_alloc.rs @@ -0,0 +1,95 @@ +//! What the packer holds does not grow with the source (packaging §11.4): a source eight +//! times as long, read from a file and packed into a directory, allocates at its peak what +//! the short one does, give or take the records of its pieces, though it is millions of +//! bytes longer. Its own binary: it installs a counting global allocator. + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +struct Peak; +static LIVE: AtomicUsize = AtomicUsize::new(0); +static PEAK: AtomicUsize = AtomicUsize::new(0); + +unsafe impl GlobalAlloc for Peak { + unsafe fn alloc(&self, l: Layout) -> *mut u8 { + let p = unsafe { System.alloc(l) }; + if !p.is_null() { + let now = LIVE.fetch_add(l.size(), Ordering::SeqCst) + l.size(); + PEAK.fetch_max(now, Ordering::SeqCst); + } + p + } + unsafe fn dealloc(&self, p: *mut u8, l: Layout) { + unsafe { System.dealloc(p, l) }; + LIVE.fetch_sub(l.size(), Ordering::SeqCst); + } +} + +#[global_allocator] +static A: Peak = Peak; + +fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../pack-conformance") +} + +/// The golden `video-base-01`'s fixture and supplied floor played `k` times over, written +/// below `dir`: the source as one Y4M, and the floor as one stream of the golden's units +/// repeated, so its key frames fall on the repeated plan; the golden's policy pinned to +/// that floor. Returns the policy, the source's path and the floor's directory. +fn repeated(dir: &Path, k: usize) -> (Vec, PathBuf, PathBuf) { + let y = wai_pack::fixture::generate("video-base-01").unwrap(); + let head = y.iter().position(|&c| c == b'\n').unwrap() + 1; + let mut src = y[..head].to_vec(); + for _ in 0..k { + src.extend_from_slice(&y[head..]); + } + let units: Vec> = wai_pack::av1::temporal_units(&std::fs::read(corpus().join("packs/video-base-01/floor/v.obu")).unwrap()).unwrap().iter().map(|t| t.bytes()).collect(); + let floor_dir = dir.join(format!("floor-{k}")); + std::fs::create_dir_all(&floor_dir).unwrap(); + std::fs::write(floor_dir.join("v.obu"), units.iter().cycle().take(k * units.len()).flatten().copied().collect::>()).unwrap(); + let src_path = dir.join(format!("v-{k}.y4m")); + std::fs::write(&src_path, &src).unwrap(); + let listing = wai_pack::floor::listing_sha256(&wai::pin::read_dir_set(&floor_dir).unwrap()).unwrap(); + let p = String::from_utf8(std::fs::read(corpus().join("packs/video-base-01/policy.toml")).unwrap()).unwrap(); + let at = p.find("supplied_listing = \"").unwrap() + "supplied_listing = \"".len(); + (format!("{}{listing}{}", &p[..at], &p[at + 64..]).into_bytes(), src_path, floor_dir) +} + +/// Pack into `out` from files, and give the peak allocated beyond what was live before. +fn pack_peak(policy: &[u8], src: &Path, floor: &Path, out: &Path) -> (wai::package::PackageReceiptV2, usize) { + let mut o = wai_pack::pack::PackOpts::with_floor(ed25519_dalek::SigningKey::from_bytes(&[0x5a; 32]), "did:key:test", wai_pack::files::FileSet::Dir(floor.to_path_buf())); + let mut sources = BTreeMap::new(); + sources.insert("v".to_string(), wai_pack::files::Input::Path(src.to_path_buf())); + let base = LIVE.load(Ordering::SeqCst); + PEAK.store(base, Ordering::SeqCst); + let mut sink = wai_pack::files::DirSink::new(out).unwrap(); + let r = wai_pack::pack::pack_into(policy, &sources, &mut o, &mut sink).unwrap(); + sink.keep(); + (r, PEAK.load(Ordering::SeqCst) - base) +} + +#[test] +fn a_longer_source_takes_no_more_memory() { + let dir = std::env::temp_dir().join(format!("wai-pack-alloc-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let (p1, s1, f1) = repeated(&dir, 1); + let (p8, s8, f8) = repeated(&dir, 8); + let (r1, peak1) = pack_peak(&p1, &s1, &f1, &dir.join("out-1")); + let (r8, peak8) = pack_peak(&p8, &s8, &f8, &dir.join("out-8")); + // The long pack is the short one eight times: eight times the entries, its repeated + // pieces stored once. + assert_eq!(r8.total_entries, 8 * r1.total_entries); + assert_eq!(r8.unique_objects, r1.unique_objects, "every repetition's pieces are the first's objects"); + wai_pack::verify::verify_dir(&dir.join("out-8")).unwrap(); + let grown = std::fs::metadata(&s8).unwrap().len() - std::fs::metadata(&s1).unwrap().len(); + assert!(grown > 4_000_000, "the long source is {grown} bytes longer"); + // Its records grow with its pieces (a few hundred bytes each); nothing grows with its + // frames. + eprintln!("peak allocated: {} entries {peak1} B; {} entries {peak8} B", r1.total_entries, r8.total_entries); + let slack = 256 * 1024; + assert!(peak8 < peak1 + slack, "the long pack's peak {peak8} B is past the short one's {peak1} B by more than {slack} B"); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/wai/wai-pack/tests/packs.rs b/wai/wai-pack/tests/packs.rs index 8529b7f50..c55df348b 100644 --- a/wai/wai-pack/tests/packs.rs +++ b/wai/wai-pack/tests/packs.rs @@ -301,3 +301,31 @@ fn the_receipts_json_round_trips() { assert!(expected["cases"].as_array().unwrap().iter().any(|c| c["receipt_hash"] == "71a9da59a3202abb2c3fd22ee692b1485a9c674bb8c311f4c82455e6be29ae2b"), "the legacy pin is unchanged"); assert_eq!(expected["cases"].as_array().unwrap().iter().filter(|c| c["signature"] == "refused").count(), 3, "a key of small order, its second spelling, an R of small order"); } + +/// The packer into a directory, coding three pieces at once, writes every golden's +/// package byte for byte (§11.2: a piece is coded alone, so the order work is done in +/// changes nothing), the files read a frame and a unit at a time. +#[test] +fn every_golden_packs_alike_into_a_directory_on_several_threads() { + let base = std::env::temp_dir().join(format!("wai-pack-dir-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + for name in golden::PACKS { + let (policy, sources, mut o) = golden::golden_inputs(&corpus(), name).unwrap(); + o.jobs = 3; + o.floor = wai_pack::files::FileSet::Dir(corpus().join("packs").join(name).join("floor")); + std::fs::create_dir_all(&base).unwrap(); + // A floor-only golden has no source (§7.8). + let mut inputs = BTreeMap::new(); + if let Some(v) = sources.get("v") { + let src = base.join(format!("{name}.y4m")); + std::fs::write(&src, v).unwrap(); + inputs.insert("v".to_string(), wai_pack::files::Input::Path(src)); + } + let out = base.join(name); + let mut sink = wai_pack::files::DirSink::new(&out).unwrap(); + wai_pack::pack::pack_into(&policy, &inputs, &mut o, &mut sink).unwrap(); + sink.keep(); + assert_eq!(held(&out), held(&corpus().join("packs").join(name).join("package")), "{name}"); + } + let _ = std::fs::remove_dir_all(&base); +} diff --git a/wai/wai-pack/tests/refusals.rs b/wai/wai-pack/tests/refusals.rs index 275891504..16f708bd4 100644 --- a/wai/wai-pack/tests/refusals.rs +++ b/wai/wai-pack/tests/refusals.rs @@ -96,6 +96,45 @@ fn every_floor_reason() { assert_eq!(case(extra.concat(), &|p: String| p.replace("[floor]\n", "[floor]\nextra_keyframes = true\n")), "ok"); } +/// §7.3's note on key-frame sequence headers: a stream whose key frames after the first +/// lost their sequence header is refused; with the stream's own first header put back after +/// each key frame's delimiter it is the original stream and packs; with a header of other +/// bytes put back (as a sample entry's `av1C` header can be, differing in a tool flag), it +/// is refused again. +#[test] +fn a_key_frame_header_put_back_is_the_stream_s_own() { + let floor = golden_floor(); + let tus = wai_pack::av1::temporal_units(&floor).unwrap(); + let seq = tus[0].sequence_header().unwrap().bytes.to_vec(); + let units: Vec> = tus.iter().map(|t| t.bytes()).collect(); + // Each unit without a sequence header, but for the first. + let stripped: Vec> = tus.iter().enumerate().map(|(i, t)| { + if i == 0 { + return t.bytes(); + } + t.obus.iter().filter(|o| o.obu_type != wai_pack::av1::OBU_SEQUENCE_HEADER).flat_map(|o| o.bytes.iter().copied()).collect() + }).collect(); + let id = |p: String| p; + assert_ne!(stripped, units, "the golden's key frames carry the header"); + assert_eq!(case(stripped.concat(), &id), "floor-av1-malformed", "a key frame without one"); + let put_back = |hdr: &[u8]| -> Vec { + tus.iter().zip(&stripped).enumerate().flat_map(|(i, (t, u))| { + if i > 0 && t.sequence_header().is_some() { [&u[..2], hdr, &u[2..]].concat() } else { u.clone() } + }).collect() + }; + assert_eq!(put_back(&seq), units.concat(), "the stream's own header put back is the stream"); + assert_eq!(case(put_back(&seq), &id), "ok"); + // Another header: the stream's with its last payload byte changed (a tool flag bit). + let mut other = seq.clone(); + let last = other.len() - 1; + other[last] ^= 0x40; + let mut s = put_back(&other); + // The first unit keeps the stream's own header, which every other must equal. + let first = units[0].len(); + s.splice(..first, units[0].iter().copied()); + assert_eq!(case(s, &id), "floor-av1-malformed", "a header of other bytes"); +} + /// §11.4: what this revision does not pack yet is refused by name, every /// `pack-capability-unsupported` naming its capability; what it packs now (refinement /// rungs, a rule's ladder subset, a still) packs. diff --git a/wai/wai-pack/tests/repack.rs b/wai/wai-pack/tests/repack.rs new file mode 100644 index 000000000..36fa919ac --- /dev/null +++ b/wai/wai-pack/tests/repack.rs @@ -0,0 +1,135 @@ +//! A re-pack from a parent (packaging §11.5): a pack that takes every piece's base and +//! refinements from an earlier package instead of coding them. The golden +//! `video-repack-01` takes `video-base-01`'s bases under two floors; its files are those of +//! a pack that codes, but for its receipt's parent and `LISTING`; a parent that does not +//! verify, is not this source's, or is coded in other rungs is refused by name; and a +//! verifier holding the parent checks the re-pack against it. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use wai_pack::files::FileSet; + +type Files = BTreeMap>; + +fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../pack-conformance") +} + +fn held(p: &Path) -> Files { + wai::pin::read_dir_set(p).unwrap() +} + +fn package(name: &str) -> Files { + held(&corpus().join("packs").join(name).join("package")) +} + +fn policy() -> String { + String::from_utf8(std::fs::read(corpus().join("packs/video-repack-01/policy.toml")).unwrap()).unwrap() +} + +/// Pack `policy` over `source` with the re-pack golden's floor and key, taking the bases +/// from `parent` when given. +fn pack(policy: &str, source: Vec, parent: Option) -> Result { + let d = corpus().join("packs/video-repack-01"); + let key = ed25519_dalek::SigningKey::from_bytes(&wai_pack::unhex(std::fs::read_to_string(d.join("key.hex")).unwrap().trim()).unwrap().try_into().unwrap()); + let id = wai::signer_id::did_key(&key.verifying_key().to_bytes()); + let mut o = wai_pack::pack::PackOpts::new(key, id, held(&d.join("floor"))); + o.base_from = parent.map(FileSet::Memory); + let mut src = BTreeMap::new(); + src.insert("v".to_string(), source); + wai_pack::pack::pack(policy.as_bytes(), &src, &mut o) +} + +fn fixture() -> Vec { + wai_pack::fixture::generate("video-base-01").unwrap() +} + +fn code(r: Result) -> String { + r.map(|_| "ok".to_string()).unwrap_or_else(|e| e.code) +} + +fn receipt(f: &Files) -> wai::package::PackageReceiptV2 { + wai::package::PackageReceiptV2::from_json(std::str::from_utf8(&f["package.receipt.json"]).unwrap()).unwrap() +} + +/// The re-pack takes the parent's objects: its files are a coding pack's of the same +/// policy, but for the receipt, which names the parent, and `LISTING`; and it verifies +/// against its parent, and against no other package. +#[test] +fn a_repack_is_a_coding_pack_with_its_parent_named() { + let parent = package("video-base-01"); + let re = pack(&policy(), fixture(), Some(parent.clone())).unwrap(); + assert_eq!(re.files, package("video-repack-01"), "the golden"); + let coded = pack(&policy(), fixture(), None).unwrap(); + let differ: Vec<&String> = re.files.keys().filter(|k| coded.files.get(*k) != Some(&re.files[*k])).collect(); + assert_eq!(differ, vec!["LISTING", "package.receipt.json"]); + assert_eq!(re.files.keys().collect::>(), coded.files.keys().collect::>()); + assert_eq!(re.receipt.parent, wai::package::Parent::RePack(receipt(&parent).receipt_hash())); + assert_eq!(coded.receipt.parent, wai::package::Parent::None); + wai_pack::verify::verify_repack(&re.files, &parent).unwrap(); + let codes = |r: Result>| r.err().unwrap_or_default().into_iter().map(|x| x.code).collect::>(); + assert_eq!(codes(wai_pack::verify::verify_repack(&re.files, &package("video-ladder-01"))), vec!["repack-parent-mismatch"], "a parent it does not name"); + assert_eq!(codes(wai_pack::verify::verify_repack(&coded.files, &parent)), vec!["repack-parent-mismatch"], "a pack that names no parent"); +} + +/// §11.5's refusals, each before anything is written: a parent that does not verify, one +/// whose pieces are another source's, one coded under other base parameters, and a source +/// one sample from the parent's. +#[test] +fn a_parent_that_is_not_this_source_s_is_refused() { + let parent = package("video-base-01"); + // An object of the parent altered, its LISTING written again: it does not verify. + let mut corrupt = parent.clone(); + let obj = corrupt.keys().find(|k| k.starts_with("objects/")).unwrap().clone(); + let b = corrupt.get_mut(&obj).unwrap(); + let last = b.len() - 1; + b[last] ^= 1; + corrupt.remove("LISTING"); + let view: BTreeMap = corrupt.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); + let l = wai::pin::listing_from(&view).unwrap(); + corrupt.insert("LISTING".into(), l); + assert_eq!(code(pack(&policy(), fixture(), Some(corrupt))), "repack-parent-invalid"); + // Another fixture's package: other pieces. + assert_eq!(code(pack(&policy(), fixture(), Some(package("video-staged-01")))), "repack-parent-mismatch"); + // This policy's base under other parameters than the parent's. + let other = policy().replace("q_candidates = [16, 12, 8]", "q_candidates = [16, 12]"); + assert_ne!(other, policy()); + assert_eq!(code(pack(&other, fixture(), Some(parent.clone()))), "repack-parent-mismatch"); + // One luma sample of frame 30 changed: the same pieces and kinds, another source. + let mut src = fixture(); + let head = src.iter().position(|&c| c == b'\n').unwrap() + 1; + let frame = 64 * 36 + 2 * 32 * 18; + let at = head + 30 * (6 + frame) + 6 + 64 * 10 + 20; + src[at] = src[at].wrapping_add(40); + assert_eq!(code(pack(&policy(), src, Some(parent))), "repack-source-mismatch"); +} + +/// The command line: `pack --base-from` writes the re-pack, and `verify --parent` checks it +/// against the parent it names. +#[test] +fn the_command_line_repacks_and_verifies_against_the_parent() { + let dir = std::env::temp_dir().join(format!("wai-pack-repack-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let g = corpus().join("packs/video-repack-01"); + std::fs::write(dir.join("v.y4m"), fixture()).unwrap(); + let bin = env!("CARGO_BIN_EXE_wai-pack"); + let out = std::process::Command::new(bin) + .args(["pack", "--policy"]).arg(g.join("policy.toml")) + .arg("--out").arg(dir.join("out")) + .arg("--key").arg(g.join("key.hex")) + .arg("--source").arg(format!("v={}", dir.join("v.y4m").display())) + .arg("--floor").arg(g.join("floor")) + .arg("--base-from").arg(corpus().join("packs/video-base-01/package")) + .output() + .unwrap(); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + assert_eq!(held(&dir.join("out")), package("video-repack-01")); + let verify = |parent: &str| { + std::process::Command::new(bin).arg("verify").arg(dir.join("out")).arg("--parent").arg(corpus().join("packs").join(parent).join("package")).output().unwrap() + }; + assert!(verify("video-base-01").status.success()); + let wrong = verify("video-ladder-01"); + assert!(!wrong.status.success() && String::from_utf8_lossy(&wrong.stderr).contains("repack-parent-mismatch")); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/wai/wai-rs/src/http_binding/dash.rs b/wai/wai-rs/src/http_binding/dash.rs index 6b2448c60..39a426c41 100644 --- a/wai/wai-rs/src/http_binding/dash.rs +++ b/wai/wai-rs/src/http_binding/dash.rs @@ -105,27 +105,30 @@ pub struct Descriptor { pub value: Option, } -/// A `Representation` with a `SegmentList`: its `@id`, `@dependencyId` and -/// `@bandwidth`, the list's timescale, an `Initialization`, the `S` elements (`t`, -/// `d`) and one `SegmentURL` per segment. +/// A `Representation` with a `SegmentList`: its `@id`, `@dependencyId`, `@bandwidth`, and +/// `@codecs`, `@width` and `@height` where the representations of its set differ in +/// them, the list's timescale, an `Initialization`, the `S` elements (`t`, `d`) and one +/// `SegmentURL` per segment. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Representation { pub id: String, pub dependency_id: Option, pub bandwidth: u64, + pub codecs: Option, + pub size: Option<(u32, u32)>, pub timescale: u64, pub initialization: Option, pub timeline: Vec<(u64, u64)>, pub media: Vec, } -/// An `AdaptationSet`. +/// An `AdaptationSet`: `@codecs` where every representation of it shares it. #[derive(Clone, Debug, PartialEq, Eq)] pub struct AdaptationSet { pub id: u32, pub content_type: Option, pub mime_type: String, - pub codecs: String, + pub codecs: Option, pub size: Option<(u32, u32)>, pub segment_alignment: bool, pub start_with_sap: Option, @@ -186,7 +189,8 @@ fn id_ok(s: &str) -> bool { /// (`@id`, `@contentType`, `@mimeType`, `@codecs`, `@width`, `@height`, /// `@segmentAlignment`, `@startWithSAP`, those given, in that order) with its essential /// then supplemental descriptors and its representations (`@id`, `@dependencyId`, -/// `@bandwidth`), each a `SegmentList` (`@timescale`) of an `Initialization`, a +/// `@bandwidth`, `@codecs`, `@width`, `@height`, those given, in that order), each a +/// `SegmentList` (`@timescale`) of an `Initialization`, a /// `SegmentTimeline` of one `S` per segment with its `t` and `d`, and one `SegmentURL` /// per segment. Attribute values are escaped, elements are indented by two spaces a /// level, and lines end in LF. Refused (`playlist_malformed`) for no adaptation set or @@ -212,7 +216,10 @@ pub fn render_mpd(m: &Mpd) -> Result { if let Some(c) = &a.content_type { out.push_str(&format!(" contentType=\"{}\"", esc(c)?)); } - out.push_str(&format!(" mimeType=\"{}\" codecs=\"{}\"", esc(&a.mime_type)?, esc(&a.codecs)?)); + out.push_str(&format!(" mimeType=\"{}\"", esc(&a.mime_type)?)); + if let Some(c) = &a.codecs { + out.push_str(&format!(" codecs=\"{}\"", esc(c)?)); + } if let Some((w, h)) = a.size { out.push_str(&format!(" width=\"{w}\" height=\"{h}\"")); } @@ -240,7 +247,14 @@ pub fn render_mpd(m: &Mpd) -> Result { if let Some(d) = &r.dependency_id { out.push_str(&format!(" dependencyId=\"{}\"", esc(d)?)); } - out.push_str(&format!(" bandwidth=\"{}\">\n \n", r.bandwidth, r.timescale)); + out.push_str(&format!(" bandwidth=\"{}\"", r.bandwidth)); + if let Some(c) = &r.codecs { + out.push_str(&format!(" codecs=\"{}\"", esc(c)?)); + } + if let Some((w, h)) = r.size { + out.push_str(&format!(" width=\"{w}\" height=\"{h}\"")); + } + out.push_str(&format!(">\n \n", r.timescale)); if let Some(i) = &r.initialization { out.push_str(&format!(" \n", esc(i)?)); } @@ -285,7 +299,7 @@ pub fn wai_mpd(t: &DashTimeline, set_id: u32, union: &Element, floor: &Element, id: set_id, content_type: None, mime_type: super::MEDIA_TYPE.into(), - codecs: codecs::render(union), + codecs: Some(codecs::render(union)), size: None, segment_alignment: true, start_with_sap: None, @@ -297,7 +311,7 @@ pub fn wai_mpd(t: &DashTimeline, set_id: u32, union: &Element, floor: &Element, } let offered: Vec<(&(u64, u64), &String)> = t.segments.iter().zip(&r.media).filter_map(|(s, m)| m.as_ref().map(|m| (s, m))).collect(); Ok(Representation { - id: r.id.clone(), dependency_id: None, bandwidth: r.bandwidth, timescale: t.timescale, initialization: None, + id: r.id.clone(), dependency_id: None, bandwidth: r.bandwidth, codecs: None, size: None, timescale: t.timescale, initialization: None, timeline: offered.iter().map(|(s, _)| **s).collect(), media: offered.iter().map(|(_, m)| (*m).clone()).collect(), }) }).collect::>()?, diff --git a/wai/wai-rs/src/http_binding/tests_playlists.rs b/wai/wai-rs/src/http_binding/tests_playlists.rs index c6e315bb4..30d177ef9 100644 --- a/wai/wai-rs/src/http_binding/tests_playlists.rs +++ b/wai/wai-rs/src/http_binding/tests_playlists.rs @@ -748,11 +748,11 @@ fn multivariant_of_either_kind() { fn render_mpd_of_either_kind() { use super::dash::*; let rep = |id: &str, dep: Option<&str>, init: Option<&str>, tl: Vec<(u64, u64)>| Representation { - id: id.into(), dependency_id: dep.map(String::from), bandwidth: 100, timescale: 25, initialization: init.map(String::from), + id: id.into(), dependency_id: dep.map(String::from), bandwidth: 100, codecs: None, size: None, timescale: 25, initialization: init.map(String::from), media: tl.iter().map(|(t, _)| format!("s{t}.wai")).collect(), timeline: tl, }; let set = |mime: &str, align: bool, sap: Option, reps: Vec| AdaptationSet { - id: 1, content_type: Some("video".into()), mime_type: mime.into(), codecs: "c".into(), size: Some((64, 36)), segment_alignment: align, + id: 1, content_type: Some("video".into()), mime_type: mime.into(), codecs: Some("c".into()), size: Some((64, 36)), segment_alignment: align, start_with_sap: sap, essential: vec![], supplemental: vec![Descriptor { scheme: SCHEME_FLOOR.into(), value: Some("f&g".into()) }], representations: reps, }; let m = Mpd { duration_ms: 8000, min_buffer_s: 3, period_id: Some("p0".into()), period_start_ms: 0, adaptation_sets: vec![ @@ -765,6 +765,19 @@ fn render_mpd_of_either_kind() { rep("v-l0", None, None, vec![(0, 60), (60, 25), (85, 50)]), rep("v-l1", Some("v-l0"), None, vec![(0, 60), (85, 50)])])], ..m.clone() }; let t = render_mpd(&w).unwrap(); assert!(t.contains("") && !t.contains("segmentAlignment")); + // Representations that differ in codecs and size carry their own, and the set none. + let mut ladder = m.clone(); + ladder.adaptation_sets[0].codecs = None; + ladder.adaptation_sets[0].size = None; + let mut hi = rep("hi-floor", None, Some("../floor/hi/init.mp4"), vec![(0, 60), (60, 25)]); + (hi.codecs, hi.size) = (Some("av01.0.08M.10".into()), Some((1920, 1080))); + let mut lo = rep("lo-floor", None, Some("../floor/lo/init.mp4"), vec![(0, 60), (60, 25)]); + (lo.codecs, lo.size) = (Some("av01.0.01M.10".into()), Some((640, 360))); + ladder.adaptation_sets[0].representations = vec![hi, lo]; + let t = render_mpd(&ladder).unwrap(); + assert!(t.contains(""), "{t}"); + assert!(t.contains(""), "{t}"); + assert!(t.contains(""), "{t}"); let mut bad = w.clone(); bad.adaptation_sets[0].representations[1].media.pop(); assert_eq!(render_mpd(&bad).unwrap_err().code(), "playlist_malformed"); @@ -784,7 +797,7 @@ fn render_mpd_of_either_kind() { assert_eq!(sup.attr("value"), Some("a\tb\nc\rd")); for c in ['\u{0}', '\u{1}', '\u{b}', '\u{1f}', '\u{fffe}', '\u{ffff}'] { let mut bad = w.clone(); - bad.adaptation_sets[0].codecs = format!("a{c}b"); + bad.adaptation_sets[0].codecs = Some(format!("a{c}b")); assert_eq!(render_mpd(&bad).unwrap_err().code(), "playlist_malformed", "{c:?}"); } } diff --git a/wai/wai-rs/src/stage_index.rs b/wai/wai-rs/src/stage_index.rs index 11cacae57..7f197f7d0 100644 --- a/wai/wai-rs/src/stage_index.rs +++ b/wai/wai-rs/src/stage_index.rs @@ -238,7 +238,8 @@ pub struct PieceRec { /// Its first entry, window-relative. pub first_entry: u32, pub n_entries: u16, - /// 0 natural, 1 static, 2 synthetic, 3 motion_high, 4 world segment, 5 audio. + /// 0 natural, 1 static, 2 synthetic, 3 motion_high, 4 world segment, 5 audio, 6 none (a + /// piece of a floor-only component, which no source classifies). pub kind: u8, pub flags: u8, /// Bit `i`: rule `i` of the policy matched the piece. @@ -247,6 +248,10 @@ pub struct PieceRec { pub moments: u32, } +/// The piece kind of a piece no source classifies: a floor-only component's (packaging +/// §7.8). +pub const PIECE_KIND_NONE: u8 = 6; + /// An entry record. #[derive(Clone, Debug, PartialEq, Eq)] pub struct EntryRec { @@ -641,8 +646,9 @@ impl StageIndex { /// decode-equivalence over a prefix that does not hold it (`tier-weakest`); a tier /// other than §6.4's (`tier-mismatch`); a non-zero prefix digest without /// decode-equivalence (`digest-without-tier`); a deadline on an entry that is not a - /// refinement (`deadline-role`); a floor without `derived_from` - /// (`floor-derived-from`); a content hash an earlier entry has at another length + /// refinement (`deadline-role`); a floor without `derived_from` in a piece that has a + /// base, or with it in one that has none (`floor-derived-from`); a content hash an + /// earlier entry has at another length /// (`dup-length`); the duplicate flag other than set exactly when an earlier entry /// has the content hash (`dup-flag`); /// 3. class by class: a flag with no meaning (`class-flags`); a plan value other than @@ -675,7 +681,7 @@ impl StageIndex { return r("piece-entries", format!("piece {i}'s entries do not follow the previous piece's")); } next_entry += p.n_entries as u64; - if p.kind > 5 || p.flags & !0x0F != 0 { + if p.kind > PIECE_KIND_NONE || p.flags & !0x0F != 0 { return r("piece-kind", format!("piece {i}: kind {} flags {:#x}", p.kind, p.flags)); } } @@ -687,6 +693,9 @@ impl StageIndex { let piece_no = h.first_piece.checked_add(pi as u64); let from = p.first_entry as usize; let to = from + p.n_entries as usize; + // A floor presents its piece's base (derived_from); a piece with no base, a + // floor-only one's, has floors that present nothing. + let has_base = self.entries[from..to].iter().any(|e| e.role == Role::Base); for k in from..to { let e = &self.entries[k]; if Some(e.piece) != piece_no { @@ -742,8 +751,8 @@ impl StageIndex { if !layered && e.deadline_ms != 0 { return r("deadline-role", format!("entry {k}, not a refinement, states a deadline")); } - if e.role == Role::Floor && e.flags & ENTRY_DERIVED_FROM == 0 { - return r("floor-derived-from", format!("entry {k}, a floor, does not carry derived_from")); + if e.role == Role::Floor && (e.flags & ENTRY_DERIVED_FROM != 0) != has_base { + return r("floor-derived-from", format!("entry {k}, a floor, carries derived_from where its piece {} a base", if has_base { "has" } else { "has no" })); } let dup = match seen.get(&e.content_hash) { Some(&len) if len != e.byte_len => return r("dup-length", format!("entry {k} repeats a content hash at another length")), diff --git a/wai/wai-rs/src/stage_index/tests.rs b/wai/wai-rs/src/stage_index/tests.rs index 9f293a4d7..43af24306 100644 --- a/wai/wai-rs/src/stage_index/tests.rs +++ b/wai/wai-rs/src/stage_index/tests.rs @@ -102,6 +102,22 @@ fn an_index_round_trips_and_checks() { } } +/// A piece of floors alone, a floor-only component's (packaging §7.8): its floors carry +/// no derived_from, which they would only as the presentation of a base, and one that does +/// is refused; such a piece's kind is `none`, which nothing classifies. +#[test] +fn a_piece_of_floors_alone_presents_no_base() { + let (mut ix, _) = fixture(); + ix.entries.remove(4); + ix.pieces[1].n_entries = 1; + assert_eq!(code(&ix), "floor-derived-from", "a floor with derived_from in a piece with no base"); + ix.entries[4].flags = 0; + ix.pieces[1].kind = PIECE_KIND_NONE; + assert_eq!(code(&ix), "ok"); + let back = StageIndex::parse(&ix.to_bytes()).unwrap(); + assert_eq!(back.pieces[1].kind, PIECE_KIND_NONE); +} + #[test] fn the_plan_digest_is_over_the_starts() { let mut want = blake3::Hasher::new(); @@ -158,6 +174,7 @@ fn every_index_mutation_names_its_reason() { ("piece-entries", |ix| ix.pieces[1].first_entry = 3), ("piece-entries", |ix| ix.entries[5].piece = 0), ("piece-kind", |ix| ix.pieces[0].kind = 9), + ("piece-kind", |ix| ix.pieces[0].kind = PIECE_KIND_NONE + 1), ("class-plan", |ix| ix.plan[0][1] = 1), ("class-plan", |ix| ix.plan[0][0] = 3), ("class-plan", |ix| {