diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index ff0975324..2f62fe3e3 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -717,6 +717,20 @@ jobs: working-directory: wai shell: bash run: python3 staged-measure-conformance/sim_verify.py staged-measure-conformance + # The simulator differentially, outside the corpus: seeded random models (a refinement + # with its own delivery timeout among them) and plans, each simulated by + # wai_stage_measure sim and by sim_verify.py, the refusal or the whole result, the + # stall bound and the longest stall equal, and every stall within the bound where it + # applies. Two fixed seeds, so a run is reproducible. + - name: sim_diff.py, wai_stage_measure against sim_verify.py + working-directory: wai + shell: bash + run: | + set -euo pipefail + (cd wai-rs && cargo build --no-default-features --features stage_measure --bin wai_stage_measure) + for seed in 1 2; do + python3 staged-measure-conformance/sim_diff.py wai-rs/target/debug/wai_stage_measure "$seed" 1500 + done # The quantum corpus's second implementation (quantum-sim extension), integers only: # it derives the circuit/1 phase table by its recurrence and the circuit2 table by an # interval-certified procedure, and requires both literal tables and their digests, diff --git a/.github/workflows/wai-pack.yml b/.github/workflows/wai-pack.yml index 83075ff3e..ef3999f3d 100644 --- a/.github/workflows/wai-pack.yml +++ b/.github/workflows/wai-pack.yml @@ -9,15 +9,19 @@ name: wai-pack # packed again byte for byte and verified, every mutation's reason. # - independent: the corpus recomputed by its second implementation, written in Python # from the extension text and sharing no code with wai-pack, the golden packages -# verified and re-derived from their sources among it. +# verified and re-derived from their sources among it (every refinement layer and +# still byte for byte, every delivery case through the staged-measurement simulator's +# Python twin); and the differentials: source readers, policies, scene cuts, the +# staged packer (layers, stills, piece plans, deliveries), and a still's reader. # - media: the golden's floor, its CMAF initialization segment and fragments, read and # decoded by a media prober (test 14). # - licences: every crate in wai-pack's dependency graph is under a licence deny.toml # allows; a source-available licence fails it. # # 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 and integer -# payload verifiers and the registry export pack_verify.py reads, and this file. +# 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 +# reads, and this file. concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -32,6 +36,10 @@ on: - 'wai/claim-conformance/_blake3.py' - 'wai/claim-conformance/_ed25519.py' - 'wai/int-payload-conformance/int_payload_verify.py' + - 'wai/staged-conformance/staged_verify.py' + - 'wai/staged-measure-conformance/sim_verify.py' + - 'wai/staged-measure-conformance/measure_verify.py' + - 'wai/http-conformance/http_verify.py' - 'wai/registry/capabilities.json' - '.github/workflows/wai-pack.yml' push: @@ -43,6 +51,10 @@ on: - 'wai/claim-conformance/_blake3.py' - 'wai/claim-conformance/_ed25519.py' - 'wai/int-payload-conformance/int_payload_verify.py' + - 'wai/staged-conformance/staged_verify.py' + - 'wai/staged-measure-conformance/sim_verify.py' + - 'wai/staged-measure-conformance/measure_verify.py' + - 'wai/http-conformance/http_verify.py' - 'wai/registry/capabilities.json' - '.github/workflows/wai-pack.yml' @@ -129,13 +141,14 @@ jobs: echo "$target: $n tests (floor $floor)" [ "$n" -ge "$floor" ] || { echo "::error::$target lists $n tests, under its floor of $floor"; fail=1; } done <<'EOF' - --lib 28 + --lib 34 --test=conformance 1 --test=packs 7 --test=review 9 --test=refusals 5 --test=scenecut 3 --test=source_alloc 1 + --test=staged 10 EOF exit $fail @@ -199,6 +212,27 @@ jobs: shell: bash working-directory: wai/wai-pack run: python3 ../pack-conformance/segment_diff.py target/release/wai-pack 1 2000 + # The staged packer differentially: refinement layers, stills, piece plans and + # deliveries on seeded random inputs, every result of wai-pack equal to + # pack_verify.py's (debug and release, both with overflow checks). + - name: stage_diff.py, wai-pack against pack_verify.py + shell: bash + working-directory: wai/wai-pack + run: | + 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 + # 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 + # Python's zlib (debug and release). + - name: png_diff.py, wai-pack against pack_verify.py + shell: bash + working-directory: wai/wai-pack + run: | + set -euo pipefail + python3 ../pack-conformance/png_diff.py target/debug/wai-pack 1 2000 + python3 ../pack-conformance/png_diff.py target/release/wai-pack 2 4000 media: name: wai-pack · the floor through a media prober @@ -216,22 +250,25 @@ jobs: # 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. - - name: The golden's floor reads and decodes + - name: The goldens' floors read and decode shell: bash - working-directory: wai/pack-conformance/packs/video-base-01/package + working-directory: wai/pack-conformance/packs run: | set -euo pipefail - 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 "key frames: $keys" - [ "$keys" = "$want" ] || { echo "::error::key frames at $keys; the plan says $want"; exit 1; } - ffmpeg -v error -xerror -i "$tmp/floor.mp4" -f null - - echo "the floor reads as one AV1 track of 200 frames, a key frame at each piece start" + for g in video-base-01 video-staged-01; 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" ) + done licences: name: wai-pack · dependency licences diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index fdd155ff7..6caadec37 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,81 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### Packaging: refinement layers, stills, a piece's ladder, live windows and delivery + +- **Refinement layers in packages** ([packaging](extensions/packaging.md) §5.4). For + each refinement rung of a piece's ladder the packer writes a `wai.video.int_refine` + object over the piece's `wai.video.int_motion` base: the manifest `wai stage refine` + writes, and a `WIR1` from `wai-rs`'s refinement encoder (`staged::wir1::encode`), one + unit per frame, toward the source from the stage below as a sink reconstructs it. Each + layer is applied to the decoded base by the reference sink before it is kept; the + index states each prefix's tier, digest, SSE and deadline. +- **A piece's ladder, and stills** (§5.7). A rule's `then.ladder` chooses the rungs a + piece is coded in; otherwise the component's first base rung, its refinement chain and + its floors. §3.4 item 10 now requires a rule's ladder to name exactly one base, every + refinement over a rung it names, and every floor of its component. A base of + `wai.image.png` is a still: the piece's first frame, held for the piece, as a PNG every + byte of which is stated (the `Sub` filter, one fixed-Huffman deflate block whose + matches are stated), so an independent implementation writes the same file. +- **Catalogs** (§8.3, §8.4). The MoQ catalog is now a stage set of moq-streaming-format + §4.1, written by `wai::staged::moq`: `wai_staged` on every track, a receipts track and + an alias per media track, `depends` on a track of every layer below; it replaces + `priority` (MOQT's own, §8.5) and `wai_layer`, which the registry no longer lists. Over + HTTP, whose variants cannot mix bases with no capability in common (http-delivery §4.2), + a still's piece is played from the floor; its stage view and MoQ tracks list it. +- **Live windows** (§9.2). `wai-pack live` packs a Y4M stream from standard input as it + arrives, with the coder the on-demand packer uses, in windows sealed by version-2 + receipts chained `live-window`; its bases and refinements are the on-demand package's, + byte for byte. `wai-pack listing --primary` lists what the two share. It writes as it + goes: each object when it is published and each window when it is sealed, every file + whole, and the plan file and `LISTING` at the end. It does not keep an object's bytes + once written, or a record per file: each listing line goes to a hidden file and is + sorted into `LISTING` at the end. A source that fails mid-stream keeps every sealed window and the objects + it lists, removes the rest, and exits 1 saying what it kept. + `wai::pin::listing_from_sha256` writes a listing from per-file digests. +- **Stills read by PNG's rules** (§5.7). Both reference readers take a PNG of 8-bit RGB + without interlace and agree on what that admits. They read a suggested palette, + ancillary chunks and split image data. They refuse an interlace, bytes after `IEND` + or after the zlib stream, a short row, a zlib window past 32 KiB, and every other + chunk fault (`stills/`). Both inflate as the zlib library does: no over-subscribed + Huffman code, and no incomplete one but a lone one-bit code. wai-pack's reader has + its own inflater, and `png_diff.py` runs it against `pack_verify.py`'s zlib. +- **The longest piece** (§3.4, §4.4). `max_ms` is at most ten minutes, and at most + `2^24` units on every component's timebase (`policy-piece-bounds`). A verifier + refuses a piece longer than the policy's `max_ms` before it decodes a frame. It + checks each floor object's unit count before decoding (§11.3). It hashes a still's + frame held for its piece without repeating the frame. +- **Manifests checked by both verifiers** (§11.3). A base's manifest must be §5.2's and a + refinement's §5.4's (`object-manifest`, a new reason). A base that does not decode + is `prefix-digest`. +- **A repeated object over HTTP** ([http-delivery](extensions/http-delivery.md) §4.2). + A repeated object contributes once, with its layers, and a layer joins every + occurrence of its base. `http_verify.py` and `wai-rs` now agree on a playlist's + codecs and floor where a piece repeats. `pack_verify.py` checks every golden playlist + with `http_verify.py`'s union, floor and adjacency checks. +- **One stall bound.** `stage_measure::sim::stall_bound_for` is the only bound: a + model's, which takes each refinement's own timeout. wai-web's simulator prints it, as + the native tool does. +- **Delivery, simulated** (§8.9). `wai_pack::delivery` and `wai-pack sim` run + staged-measurement's simulator over what a live packer publishes and what a class + subscribes to: per-track delivery timeouts, §8.5's priorities, the subscriber's cap, + group receipts on each track's chain. The simulator's model gains a refinement's own + `timeout_us`, and §10.4's bound takes the longest refinement timeout + ([staged-measurement](extensions/staged-measurement.md) Appendix B); its corpus adds a + per-track model. +- **The corpus**: the golden `video-staged-01` (a moving shot, a bumper played twice, a + still shot, a second moving shot; two refinement layers, a still, the event's deadline, + three classes), its supplied floor and how it was written (`supplied_floor.py`), + `staged-mutations/`, `delivery/`, `stills/` and `live/` (the golden packed live in + windows of two pieces, whose chain `pack_verify.py` checks). `stage_diff.py` runs + refinement layers, stills (wide ones among them, so every deflate distance code is + used), piece plans and deliveries differentially against `pack_verify.py`, which + re-derives every layer and still from the source byte for byte. `sim_diff.py` runs the + delivery simulator differentially against `sim_verify.py`. `pack-refinement-unsupported` and + `pack-rule-ladder-unsupported` are gone. +- The base golden `video-base-01` keeps its objects, index and receipt; its MoQ catalog + and its `LISTING` change. + ### The record made true: training, paths, protected content, reader limits, security - **`int_synth`'s weights are quantised after float training.** SPEC §5 and diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index 6bdc0011a..785fdbfa8 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -308,7 +308,6 @@ and a role are fixed once registered. | `wai_capabilities` | catalog | defined | moq-streaming-format §4 | | `wai_delivery_timeout_ms` | catalog | defined | packaging §8 | | `wai_determinism` | catalog | defined | moq-streaming-format §4 | -| `wai_layer` | catalog | defined | packaging §8 | | `wai_receipt_alias` | catalog | defined | moq-streaming-format §4 | | `wai_receipts` | catalog | defined | moq-streaming-format §4 | | `wai_receipts_track` | catalog | defined | moq-streaming-format §4 | diff --git a/wai/extensions/http-delivery.md b/wai/extensions/http-delivery.md index 09c47aff4..37c7adea3 100644 --- a/wai/extensions/http-delivery.md +++ b/wai/extensions/http-delivery.md @@ -143,8 +143,11 @@ lcalnum = %x61-7A / %x30-39 sequence's objects — a `WAI1`, through its `capability` (staged delivery never takes a base through its fallback), or a component of a `WAI2`, by the content hash of the component's exploded envelope ([SPEC](../SPEC.md) §7.1), through the component's - capability. A sequence with a layer whose base it does not hold cannot be described - (`codecs_layer_unbound`). `alt`s are ordered by first appearance, and the + capability. An object a sequence lists more than once (a repeated piece) is one object + of it, described once with every layer it has, and a layer joins every object of the + sequence that is its base, so where a layer is listed beside one occurrence of its base + it describes them all. A sequence with a layer whose base it does not hold cannot be + described (`codecs_layer_unbound`). `alt`s are ordered by first appearance, and the capabilities after `+` by first appearance in table order, then a sequence's layers in order. The union is exhaustive: a sink that finds, in a head it has verified, a selectable, companion or refinement capability the union does not list refuses that diff --git a/wai/extensions/packaging.md b/wai/extensions/packaging.md index 6c6bfae9d..287b0aae7 100644 --- a/wai/extensions/packaging.md +++ b/wai/extensions/packaging.md @@ -3,13 +3,15 @@ > Status: Draft. A packager turns sources into WAI objects under a declared policy; > 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 (§5), the stage index (§6), the floor -> (§7), what each class is offered over HTTP and MoQ (§8), the package receipt (§9), -> and the package and its verification (§11). Reference impl: `wai-pack` — `policy`, -> `capcheck`, `source`, `scenecut`, `classify`, `encode`, `floor`, `cmaf`, `catalog`, -> `pack`, `verify`; in `wai-rs`, the encoder `int_video_enc`, the stage index -> `stage_index` and the receipt `package`; the corpus -> [`pack-conformance/`](../pack-conformance/). Keywords **MUST**, **MUST NOT**, +> 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 +> 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` — +> `policy`, `capcheck`, `source`, `scenecut`, `classify`, `plan`, `encode`, `png`, +> `floor`, `cmaf`, `catalog`, `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**, > **SHOULD**, **MAY** are RFC 2119/8174. ## 1. Scope @@ -147,7 +149,8 @@ first that fails is the refusal: a reference never names two things (`policy-duplicate-id`). 3. `lead` names a component (`policy-lead`). 4. `segmentation.closed` is true: every piece is closed, §4.1 (`policy-open-gop`). -5. `1 ≤ min_ms ≤ target_ms ≤ max_ms` (`policy-piece-bounds`). +5. `1 ≤ min_ms ≤ target_ms ≤ max_ms ≤ 600000`, and `max_ms` is at most `2^24` units + on every component's timebase (§4.4) (`policy-piece-bounds`). 6. Every rung names a component (`policy-ladder-component`). 7. A rung carries `refines` exactly when its role is `refinement` (`policy-refines-role`); `refines` names a rung (`policy-dangling-refines`); no @@ -159,8 +162,10 @@ first that fails is the refusal: 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, at least one of - them a base (`policy-rule-ladder`). +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`). 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`) @@ -242,6 +247,15 @@ A duration in milliseconds converts to timebase units as `floor(ms · num / (1000 · den))`; a result of 0 becomes 1, and one above `2^64 − 1` becomes `2^64 − 1`. +A piece is at most ten minutes long (`max_ms` at most 600000) and at most `2^24` units +on its component's timebase (§3.4 item 5). A verifier takes a piece whole: it decodes +every frame of a base, holds a still's one frame for every frame of its piece and hashes +each (§5.7, §6.5), and reads one floor unit per frame (§7.2). So the longest piece a +policy allows is the most work a verifier does for one piece, and a package states it. +Ten minutes is far past any piece a delivery cuts (the default `max_ms` is four +seconds); `2^24` units bounds that work on any timebase, since a timebase may have up to +`2^32 − 1` units a second. + **4.5** *Components.* Every component of a package shares one presentation timeline that starts at 0. The policy names one **lead** component; every other component cuts its pieces at the lead's boundary instants, each converted to its own timebase as @@ -292,8 +306,9 @@ object. The reference packager writes a base's manifest as SPEC §3 orders it, w `media` the component's media, `intent` `replicate`, `model_requirement` the rung's capability with no `fallback` member, `conditioning.kind` the capability's last dot-separated segment (`int_motion`, `av1`), and `target` `{"frames": n, "h": h, "w": w}` -(the piece's frame count and dimensions; for a floor, its sequence header's maximum -frame height and width); a floor's `derived_from` (§7.2) comes after `conditioning`. +(the piece's frame count and dimensions, for a still too, whose one frame is held for +the piece, §5.7; for a floor, its sequence header's maximum frame height and width); a +floor's `derived_from` (§7.2) comes after `conditioning`. **5.3** *Name.* An object is named by its content hash ([jwp-receipts](jwp-receipts.md) §2.1), `BLAKE3("moq-jwp:object\x01" ‖ envelope)`, as `objects/<64 lowercase hex>.wai`: @@ -303,8 +318,30 @@ its HTTP strong `ETag` ([http-delivery](http-delivery.md) §5.2). **5.4** *Refinements.* A refinement object carries `refines` ([staged-delivery](staged-delivery.md) §3): `base` the content hash of its piece's base, `layer` its layer, and as producers in `prev` only objects of the layers below it, the -object its entry refines (§6.2) among them. The reference packager does not write -refinements yet. +object its entry refines (§6.2) among them. + +The reference packager writes, for each refinement rung of a piece's ladder (§5.7), one +object of `wai.video.int_refine` over the piece's `wai.video.int_motion` base, layer by +layer. Its manifest is the one `wai stage refine` writes, as SPEC §3 orders it: `media` +the component's, `intent` `replicate`, the capability with no `fallback`, +`conditioning.kind` `wir1`, `refines` with `base` the base's content hash, `prev` the +content hash of the object of the layer below (the base for layer 1) as a string, and +`layer`, and `target` null. Its payload is a `WIR1` (staged-delivery Appendix A) of unit +kind RGB8, one unit per frame of the piece, the identity predictor and one table, +written by `wai-rs`'s refinement encoder (`staged::wir1::encode`) at the rung's step +`q`: + +- each sample's residual is `rdiv(source − below, q)`, the quotient rounded half away + from zero, where *below* is the stage the layer refines as a sink reconstructs it; + no frame predicts another; +- the table's `offset` is the least residual and it spans every residual (where they + span more than 4095, the 4095 around the lower median), fitted by §5.5's rule, the + escape at 1; a residual past it is coded through the escape; +- one rANS stream per frame. + +The stage the layer reconstructs, `clamp(below + r·q, 0, 255)`, is what the next layer +refines. The packer applies each layer to the decoded base with the reference sink +(staged-delivery §4) before it keeps it, and the sink's output must be the encoder's. **5.5** *Integer video.* An object of `wai.video.int_motion` carries a `WIV1` payload with keyframe 0, as SPEC §5 pins it ([integer-payloads](integer-payloads.md) §3); this @@ -356,6 +393,52 @@ candidates that meet it alike are decided by their step. Where no candidate meet it chooses the smallest. The function the reference packager's base encode calls is `wai_pack::encode::choose_q`. +**5.7** *A piece's ladder, and stills.* A piece is coded in the rungs of its **ladder**: +those the matching rules' `then.ladder` names (§3.4 folds them: the last that sets it), +where they are rungs of the piece's component; otherwise the component's default ladder, +its first base rung in ladder order, the chain of refinement rungs over it, and its floor +rungs. A rule's ladder therefore chooses a piece's representation: its base, how many +layers refine it, and nothing else (§3.4 item 10). A piece has one entry per rung of its +ladder: its base, each refinement in layer order, then each floor (§6.2), and a +verifier holds an index to that (§11.3). + +A base of `wai.image.png` is a **still**: the piece's first frame, held for the piece. A +sink presents that frame for each of the piece's frames, so its prefix is the frame +repeated and its digest the clip digest of the repeats (§6.5), its `sse` is over the +repeats against the piece's frames (§6.6), and its manifest's `target` names the piece's +frames (§5.2). The reference packager writes the frame as a PNG every byte of which is a +pure function of the frame, so an independent implementation writes the same file: + +- the signature, `IHDR` (the width, the height, bit depth 8, colour type 2, compression + 0, filter 0, interlace 0), one `IDAT` holding the whole compressed stream, and `IEND`; + each chunk a big-endian length, its type, its data and the CRC-32 of type and data; +- every row filtered `Sub` (filter byte 1): each byte less the byte three before it in + the row, modulo 256, the first pixel's bytes less 0; +- a zlib stream (RFC 1950) of `0x78 0x01`, one final deflate block (RFC 1951) of the + fixed Huffman codes, and the Adler-32 of the filtered rows; +- the block's symbols, from the first byte of the filtered rows: where at least three + bytes remain and an earlier position within 32768 bytes starts with the same three + bytes, the latest such position is the match, as long as the two runs agree (at most + 258 bytes), written as that length and the distance to it; otherwise one literal. + Every position a symbol covers that has three bytes left becomes the latest position + of its three bytes. The block ends with symbol 256, and the last byte is padded with + zero bits. + +A verifier reads a still as a PNG of 8-bit RGB without interlace, by PNG's chunk rules: +every chunk's type four letters and its CRC checked; `IHDR` first; an optional `PLTE` +(a suggested palette) before the image data; the `IDAT`s consecutive; ancillary chunks +read past; any other critical chunk refused; `IEND` last, empty, and nothing after it. +It inflates the zlib stream as the zlib library's inflate does, so two verifiers agree +on every stream: the header's check bits, method 8, a window of at most 32 KiB and no +preset dictionary; a stored block's length and its complement agree; at most 286 +literal/length and 30 distance codes, a complete code-length code, a repeat with a +length before it and within the code lengths, and a code for the end of the block; no +Huffman code over-subscribed, and none incomplete but a literal/length or distance code +whose longest code is one bit (a lone one-bit code, or none); a code with no symbol, or +a distance past the bytes written, refused where it is read; the Adler-32, and nothing +after it. The rows are exactly `1 + 3·width` bytes each, under the five filters. A still +a verifier does not read is `prefix-digest` (§11.3). + ## 6. Stage index **6.1** A package carries, per component, one stage index, the file `index-.wsix` @@ -517,8 +600,8 @@ entropy-consistency for `NeuralEntropyExact`, and none otherwise (SPEC §5, §7) **6.5** `prefix_digest` is the SHA-256 decoded-output digest (SPEC §7, *Digests*) of the prefix the entry completes when `prefix_tier` is decode-equivalence, and all zero -otherwise: for video, the clip digest over RGB8 frames; for an image, the RGB8 frame -digest; for audio, the digest of the s16le interleaved samples; for world, feed, avatar +otherwise: for video, the clip digest over RGB8 frames (for a still, of its frame held +for the piece's frames, §5.7); for an image, the RGB8 frame digest; for audio, the digest of the s16le interleaved samples; for world, feed, avatar and film entries, zero (their state hashes live in their receipts). A verifier decodes the prefix and compares. @@ -641,14 +724,19 @@ offered in playlists of their own. and **MUST NOT** be used to decide whether a `wai1.*` value can be decoded ([http-delivery](http-delivery.md) §4.3). - Each class's WAI playlist **MUST** name its class and its stage view. - +- A variant lists no bases without a capability in common ([http-delivery](http-delivery.md) + §4.2, `codecs_no_common_capability`). A class's HTTP view lists a piece only where the + piece's base is of the component's default base capability (§5.7, its first base rung's), + and plays the floor for any other piece, as for a piece the class takes as floor only: a + still (§5.7) among motion bases is carried over HTTP by the floor. The class's stage view + (§8.8) and its MoQ tracks (§8.4) list it. Durations: a piece of *d* units lasts `floor(d · den · 1000 / num)` milliseconds, written `S.mmm` (whole seconds, a full stop, three digits). A target duration is the largest `ceil(d · den / num)`. A rate is `ceil(8 · bytes · num / (d · den))` bits per 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 the class does not take as floor only. Every playlist +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`: @@ -662,9 +750,9 @@ codecs string *F* and maximum frame size `FW×FH`: `#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, `#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8"` + 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. -- `hls//wai.m3u8`, when the class lists a WAI object: `#EXTM3U`, +- `hls//wai.m3u8`, when the class's HTTP view lists a WAI object: `#EXTM3U`, `#EXT-X-VERSION:7`, `#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE=""`, `#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/.json"`, @@ -673,7 +761,8 @@ codecs string *F* and maximum frame size `FW×FH`: 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 - class takes it as floor only, `#EXTINF:,`, each refinement the class lists, + 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="`, then its base as `../../objects/.wai#n=`: a layer line follows its segment's `EXTINF` ([http-delivery](http-delivery.md) §10.4). @@ -693,7 +782,7 @@ one `AdaptationSet`, `` and ``, indented by two spaces a level: ``: ``, a `SegmentTimeline` of one `` per piece, and one `` per piece. -- `dash/.mpd`, when the class lists a WAI object: +- `dash/.mpd`, when the class's HTTP view lists a WAI object: `` holding ``, ``s of schemes `tag:transaction.science,2026:wai/floor` (value the @@ -711,31 +800,40 @@ of one component; how the variants and groups of several components are written defined with the packing of several components. **8.4** *MoQ.* `moq/catalog.json` is RFC 8785 canonical JSON and a line feed: -`{"tracks": [...], "version": 1}`, the tracks by layer, then by deadline ascending, -then the floor. Each (component, layer, deadline) is one track of -`packaging` `"wai"`: name `/l`, or `/l@` for a refinement whose -deadline is not `defaults.deadline_ms`; `renderGroup` 1; `priority` §8.5's; -`wai_capabilities` its entries' capabilities by first appearance; `wai_layer` *k*; -`wai_receipts` false (no object of this revision carries a `WAI_RECEIPT` property); -`wai_determinism` the weakest tier of its entries, absent when that is none. The base -track has `altGroup` 1 when the component has a floor. MSF's catalog defines `depends` -as the tracks on which a track is dependent, and this section has a sink receive every -one of them (below): a dependency is all of its tracks, not any one. So a refinement track -`depends` on every track that carries an entry its own entries refine, in track order — -the layer below at the same deadline, where a piece's layers share its deadline as the -reference packager writes them — and never on a track the catalog does not hold. It -carries `wai_delivery_timeout_ms` (§8.6). The floor is a track -`/floor` of `packaging` `"cmaf"` with `altGroup` 1, `renderGroup` 1, `codec`, -`width` and `height`. A sink **MUST NOT** subscribe to a track unless it also receives -every track that track depends on, so a sink that selects the floor subscribes to no -layer track. `wai_layer` and `wai_delivery_timeout_ms` are catalog fields this -extension defines under the `wai_` prefix of -[moq-streaming-format](moq-streaming-format.md) §4; a later revision of that extension -may define them there instead. +`{"tracks": [...], "version": 1}`. A component's bases and refinements are a stage set of +[moq-streaming-format](moq-streaming-format.md) §4.1, one track per (layer, deadline), +by layer, then by deadline ascending, each written as that section's entry: namespace +the policy's `asset_id`; name `/l`, or `/l@` for a refinement +whose deadline is not `defaults.deadline_ms`; `packaging` `"wai"`; `isLive` false; +`role` `video`; `renderGroup` 1; `wai_staged` with `set` the component's id, `layer` *k* +and `lead_ms` null (an on-demand package has no live schedule); `wai_capabilities` its +entries' capabilities by first appearance (a still among them on the base track, §5.7); +`wai_determinism` the weakest tier of its entries, absent when that is none; and +receipts declared: `wai_receipts` true, `wai_receipt_alias` the first eight bytes of +`BLAKE3("wai:pack-receipt-alias\x01" ‖ namespace ‖ 0x00 ‖ name)` as a little-endian +integer, less than `2^53` (so two packages published under one key keep apart chains), +and `wai_receipts_track` `/receipts`. A track `depends` on every track that +carries an entry below one of its own entries, down the chain to the base, in track +order: a track of each layer below it, none at or above it, and never one the catalog +does not hold. A sink receives every track a track depends on (moq-streaming-format +§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 +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 +groups (a group a piece) on that track's chain as it publishes them (jwp-receipts §2.4.1; +§8.9 simulates it). **8.5** *Priority.* The base track's publisher priority is 64 and layer *k*'s -`64 + 32·k`: a refinement track's value is strictly greater than that of the track it -depends on, so it is delivered after it (MOQT: a lower value is delivered first). +`64 + 32·k`: a refinement track's value is strictly greater than that of every track it +depends on, so it is delivered after them (MOQT: a lower value is delivered first), and +moq-streaming-format §4.1's rule holds; a receipts track's is its media track's. The +priority is MOQT's (`DEFAULT_PUBLISHER_PRIORITY`), set by the publisher, not a catalog +field. **8.6** *Deadline.* An entry's `deadline_ms` is the longest a refinement may arrive after the base of its piece was published and still be presented; the rules' or @@ -752,12 +850,43 @@ to wait for a refinement. What the receipts of a dropped object say is **8.8** *Stage views.* `stages/.json` is RFC 8785 canonical JSON and a line feed: `{"class": "", "components": {"": {"media_playlist": "../hls//wai-.m3u8", "pieces": [...]}}, "policy_digest": ""}`, with -`media_playlist` only when the class lists a WAI object, and each piece +`media_playlist` only when the class's HTTP view lists a WAI object (§8.3), and each piece `{"dur", "p", "start", "stages": [...]}` with `"floor_only": true` when the class takes it as floor only (and `stages` empty), and each listed stage `{"bytes", "h", "layer"}`, a refinement's with `"refines"` (the content hash of the entry it refines) and `"deadline_ms"`. +**8.9** *Delivery, simulated.* How a package's objects reach a subscriber over a link is +simulated by staged-measurement's delivery simulator ([staged-measurement](staged-measurement.md) +§10, Appendix B); its figures are labelled simulated, with their plan, and are never a +measurement of a network. The reference packager's `delivery` states the model and the +plan, and reads the outcome: + +- **Model.** Each object a class lists of each piece (§8.1; a floor-only piece gives none), + on the component's frames as units (a frame of a whole number of microseconds): base the + piece, its layer, units the piece's frames, bytes the whole envelope, and its capture + time the instant it is published. A live packer (§9.2) publishes a piece's base when the + frame that decides the piece's end has arrived (the next piece's first frame plus + `lookahead_frames`, or the source's last frame, §4.3; frame *f* arriving at + `f · frame_us`), plus the encoder's delay, and layer *k* `layer_lag_ms[k]` after its + base. Each refinement takes its track's delivery timeout, `deadline_ms − + layer_lag_ms[k]` milliseconds (§8.6), as its own (staged-measurement Appendix B). +- **Plan.** Live; the link's round trip, rate and chunk; strict priority, which is §8.5's + order; each (piece, layer) a subgroup of its own; no base timeout; the subscriber's + layer cap (moq-streaming-format §4.1, *Capping*); one decode worker, bases first, each + decode 1 µs (the simulation is of the link); playout from the start-up, and a refinement + that arrives after its piece's instant discarded (§8.7). +- **Outcome.** Per piece, at its first frame's presentation instant (the start-up plus its + start): the highest layer presented by then, or none where its base arrived later; its + base's arrival; the layers dropped or abandoned. A class that lists nothing of any piece + sends nothing. + +Under §8.5's priorities a refinement delays a base by at most one chunk and one decode +(staged-measurement §10.4). The publisher seals each track's group with a group receipt on +the track's chain (`wai_receipt_alias`, §8.4); a subscriber accepts a delivered object by +its Merkle proof, and holds a dropped one by inclusion alone, never as delivered +([jwp-receipts](jwp-receipts.md) §4.4). + ## 9. Package receipt **9.1** A version-2 package receipt binds the asset id; the policy digest; its parent @@ -829,6 +958,35 @@ 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`. +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` +more have arrived, and codes each piece as it closes with the coder the on-demand packer +uses (§5.4–§5.7). So a live and an on-demand pack of one source under one policy write +the same plan and the same bases and refinements, byte for byte. It writes no floor: a +supplied floor is made from the whole source (§7.3), so it is the on-demand package's. +Its pieces are gathered into windows: each a stage index of its pieces' bases and +refinements (`first_piece` and `first_entry` continuing the chain, `prev_index_digest` +the previous window's index digest, zero for the first) sealed by a version-2 receipt +whose parent is the previous window's (`live-window`; none for the first). A piece's +objects are published when the frame that decides its end has arrived: its base then, +and layer *k* `layer_lag_ms[k]` after it (§8.6, §8.9). + +It writes as it goes: each object when it is published, and each window's index and +then its receipt when the window is sealed, every file whole (written beside its place +and renamed into it); the plan file and `LISTING` at the end of the source. It keeps no +record per file of the session: each file's listing line goes to a hidden file beside +the package as the file is written, and is sorted into `LISTING` at the end (the one +moment it holds a line per file); an object already written is known by its file; an +object's bytes are not held once written. What it holds is what no window has sealed +yet: those pieces' entry fields and the content hash of each object they published +first. So with windows of `n` pieces that is at most `n` pieces', and with one window +over the whole source (the default) every piece's until the end. Besides those it holds +the frames of the piece being cut and each piece's first frame, eight bytes a piece, for +the plan file. A source that fails before its end (a short frame, a frame of another +size) leaves every sealed window and the objects it lists, removes the objects only +unsealed pieces published, writes no plan file or `LISTING`, and is an error that names +what was kept. + **9.3** A version-1 receipt (`wai.video.package`'s segment map) keeps its bytes and still verifies. Its JSON form is RFC 8785 canonical with `version` 1: `asset_id`, `energy_provenance` (`null` when unmetered) with its evidence, `grant`, `joules_micro`, @@ -931,15 +1089,28 @@ 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 entry's role, capability and layer are a rung's of its component (a base, a - refinement at the rung's layer, or a floor); every piece's kind is one a video piece - has, and its rules, moments and class plan are §8.1's from its kind, span and + every piece's kind is one a video piece has; 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 + 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; - - every decode-equivalent prefix decodes to its `prefix_digest` (`prefix-digest`); + - 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`); + - 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 + frame size (`object-manifest`); a refinement's manifest is §5.4's + (`object-manifest`), and it applies in layer order, to every frame of the stage + below it, by a staged-delivery §4 sink under the reference tools' cost limit and + chain budget, or it is `prefix-digest`; a base that does not decode, or a layer that + 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 @@ -957,15 +1128,17 @@ failed check names is not applied: catalog file past the ones given (`catalog-unexpected`); any other file (`unexpected-file`). -**11.4** *What this revision packs.* The reference packager packs one video component -with a `wai.video.int_motion` base (§5.5, §5.6) and a supplied AV1 floor (§7.3). It -refuses by name, before writing anything, what it does not pack yet: another medium -(`pack-media-unsupported`), a refinement rung (`pack-refinement-unsupported`), a rule's -ladder subset (`pack-rule-ladder-unsupported`), a floor to encode -(`pack-floor-encode-unsupported`), and a rung of a capability it does not encode -(`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 +**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 +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 +of a capability it does not encode, or a refinement over a base it does not refine +(`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 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 @@ -993,6 +1166,6 @@ would leave behind the ones it does not write. | `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`, `prefix-digest`, `catalog-mismatch`, `catalog-unexpected`, `object-unlisted` | §11.3 | -| `pack-id`, `pack-source-missing`, `pack-source-rate`, `pack-media-unsupported`, `pack-refinement-unsupported`, `pack-rule-ladder-unsupported`, `pack-floor-encode-unsupported`, `pack-capability-unsupported` (naming the capability) | §11.1, §11.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 | diff --git a/wai/extensions/staged-measurement.md b/wai/extensions/staged-measurement.md index e113eb8d9..bfad27ae5 100644 --- a/wai/extensions/staged-measurement.md +++ b/wai/extensions/staged-measurement.md @@ -307,13 +307,15 @@ under the plan minus the same time under the base-only model. The stall bound is `S = S_link + S_decode`: - `S_link`: under `strict-priority`, the serialisation time of one chunk; under `fifo` - with a refinement timeout `T`, `T` plus one chunk; otherwise there is no bound. + with a refinement timeout, `T` plus one chunk, where `T` is the longest timeout any + refinement takes (its own, where the model gives it one, or the plan's); otherwise + (a refinement that takes none) there is no bound. - `S_decode`: under `base-first` decode, one refinement's decode time; otherwise there is no bound. **Preconditions.** The bound holds for every model and every plan with no base timeout (`delivery_timeout_us.base` is `null`), a bounded link schedule (`strict-priority`, -or `fifo` with a refinement timeout) and `base-first` decode, whatever else the plan +or `fifo` where every refinement takes a timeout) and `base-first` decode, whatever else the plan states (carriage, window, live captures, layer cap, workers, decode times, playout deadlines). A plan outside them has no bound: a base its refinements delay past its own timeout is not presented at all, and the two unbounded parts above have counter-plans. @@ -494,9 +496,12 @@ them (`wai-sim-model/1`): ``` `units` is `[from, count]` over the title's units and `bytes` the object's counted -bytes (§3). A model is well formed when every object has at least one byte and one -unit, no two objects cover one unit at one layer, and every unit an object of layer -`k ≥ 1` covers is covered at layer `k − 1` by an object of the same base. A measured +bytes (§3). A refinement **MAY** carry `timeout_us`, a delivery timeout of its own in +place of the plan's for its layer: what a MoQ track's `DELIVERY_TIMEOUT` sets for every +object on it ([packaging](packaging.md) §8, its deadlines). A model is well formed when +every object has at least one byte and one unit, no base carries `timeout_us`, no two +objects cover one unit at one layer, and every unit an object of layer `k ≥ 1` covers is +covered at layer `k − 1` by an object of the same base. A measured title's model is its objects with their counted bytes, and its bases' capture times. A **plan** (every member required, none other allowed): @@ -524,7 +529,8 @@ microseconds, and `h = ⌊rtt_us / 2⌋`; a time or a byte sum past 64 bits is r with no window), and at `max(h, capture_us)` live; a live plan over an object with no capture time is refused. - *Deadline.* An object's availability plus its layer's timeout (`base` for layer 0, - `refinement` otherwise); none when that timeout is `null`. + `refinement` otherwise, or a refinement's own `timeout_us`); none when that timeout is + `null`. - *Cap.* An object above `layer_cap` is capped: never sent, never offered. - *Datagrams.* Under `datagram`, an object of more than `chunk_bytes` that is not capped is refused (`datagram-too-large`). diff --git a/wai/http-conformance/cases.json b/wai/http-conformance/cases.json index 47b9054fd..672f87603 100644 --- a/wai/http-conformance/cases.json +++ b/wai/http-conformance/cases.json @@ -289,6 +289,8 @@ {"name":"parse_head_kid_string","op":"parse_head","input":{"file":"bad/OK.wai"},"expect":{"error":"head_malformed"}}, {"name":"parse_head_kind_number","op":"parse_head","input":{"file":"bad/OKN.wai"},"expect":{"error":"head_malformed"}}, {"name":"union_O10_layers","op":"union_floor","input":{"objects":["O10","O10L1","O10L2"]},"expect":{"union":"wai1.audio.flac+audio.int_refine","floor":"wai1.audio.flac+audio.int_refine"}}, +{"name":"union_O10_repeated_layer_after_first","op":"union_floor","input":{"objects":["O10","O10L1","O10"]},"expect":{"union":"wai1.audio.flac+audio.int_refine","floor":"wai1.audio.flac+audio.int_refine"}}, +{"name":"union_O10_repeated_layer_after_second","op":"union_floor","input":{"objects":["O10","O10","O10L1"]},"expect":{"union":"wai1.audio.flac+audio.int_refine","floor":"wai1.audio.flac+audio.int_refine"}}, {"name":"union_O10_layers_unbound","op":"union_floor","input":{"objects":["O10L1","O10L2"]},"expect":{"error":"codecs_layer_unbound"}}, {"name":"fetch_O10_base","op":"fetch","input":{"object":"O10","advertises":["wai.audio.flac"]},"expect":{"role":"base","pick":0,"verified_by":"whole-object","why_whole":"single_component","body_bytes":1910,"requests":2,"companions":[],"refinements":[],"abandoned":[],"dropped":[],"skipped":[],"component_sha256":"67ddfc4700c518d0f0028fd03920901057f419d9fc84349e1c9cf7a75314fefc"}}, {"name":"fetch_O10L1_discovered","op":"fetch","input":{"object":"O10L1","fragment":"#n=12711&h=1009","advertises":[]},"expect":{"role":"layer","pick":0,"verified_by":"whole-object","why_whole":"layer","body_bytes":12892,"requests":3,"companions":[],"refinements":[],"abandoned":[],"dropped":[],"skipped":[],"component_sha256":"96c9e16b31256d323efe54d996bfab6223f4eeb017da5d55ece7fd74fee750e2"}}, diff --git a/wai/http-conformance/falsify/union-no-layer.patch b/wai/http-conformance/falsify/union-no-layer.patch index fd3392a45..f4ceffa70 100644 --- a/wai/http-conformance/falsify/union-no-layer.patch +++ b/wai/http-conformance/falsify/union-no-layer.patch @@ -1,15 +1,15 @@ -# must fail: union_O10_layers +# must fail: union_O10_layers union_O10_repeated_layer_after_first union_O10_repeated_layer_after_second # a layer joins no alt (codecs::described) diff --git a/wai/wai-rs/src/http_binding/codecs.rs b/wai/wai-rs/src/http_binding/codecs.rs -index 530ae822..161ed997 100644 +index a4d35e39..821b7f51 100644 --- a/wai/wai-rs/src/http_binding/codecs.rs +++ b/wai/wai-rs/src/http_binding/codecs.rs -@@ -326,7 +326,7 @@ fn described(objects: &[SeqObject]) -> Result, CodecsError> { - let el = els[k].as_mut().expect("a base is not a layer"); - let alt = el.alts.iter_mut().find(|a| a.cap == base_cap) - .ok_or_else(|| CodecsError::LayerUnbound(format!("wai.{cap}: its base's capability is not dispatched")))?; -- push_unique(&mut alt.plus, cap); -+ let _ = (alt, cap); +@@ -334,7 +334,7 @@ fn described(objects: &[SeqObject]) -> Result, CodecsError> { + let el = els[k].as_mut().expect("a base is not a layer"); + let alt = el.alts.iter_mut().find(|a| a.cap == base_cap) + .ok_or_else(|| CodecsError::LayerUnbound(format!("wai.{cap}: its base's capability is not dispatched")))?; +- push_unique(&mut alt.plus, cap.clone()); ++ let _ = (alt, &cap); + } } Ok(els.into_iter().flatten().collect()) - } diff --git a/wai/http-conformance/http_verify.py b/wai/http-conformance/http_verify.py index dae9097f1..a72102e23 100644 --- a/wai/http-conformance/http_verify.py +++ b/wai/http-conformance/http_verify.py @@ -606,6 +606,15 @@ def base_capability(b, base): def described(objects): + """§4.2: each object that is not a layer as an element, each layer's capability after + `+` in the alt of every object of the sequence that is its base. An object listed more + than once is one object, described once with every layer it has.""" + seen, uniq = set(), [] + for o in objects: + if o["cid"] not in seen: + seen.add(o["cid"]) + uniq.append(o) + objects = uniq els = [None if is_layer(o["head"]) else element_of(o["head"]) for o in objects] for o in objects: if not is_layer(o["head"]): @@ -617,19 +626,15 @@ def described(objects): base = SV.parse_refines(m.get("refines"))["base"] except SV.Malformed: raise Refusal("codecs_malformed") - target = None - for k, b in enumerate(objects): - bc = base_capability(b, base) - if bc is not None: - target = (k, bc) - break - if target is None: + targets = [(k, bc) for k, b in enumerate(objects) for bc in [base_capability(b, base)] if bc is not None] + if not targets: raise Refusal("codecs_layer_unbound") - alt = next((a for a in els[target[0]][1] if a[0] == target[1]), None) - if alt is None: - raise Refusal("codecs_layer_unbound") - if cap not in alt[1:]: - alt.append(cap) + for k, bc in targets: + alt = next((a for a in els[k][1] if a[0] == bc), None) + if alt is None: + raise Refusal("codecs_layer_unbound") + if cap not in alt[1:]: + alt.append(cap) return [e for e in els if e is not None] diff --git a/wai/pack-conformance/README.md b/wai/pack-conformance/README.md index 8b42af2be..375ff2408 100644 --- a/wai/pack-conformance/README.md +++ b/wai/pack-conformance/README.md @@ -2,9 +2,10 @@ Vectors for [`extensions/packaging.md`](../extensions/packaging.md): the policy, its digest and what it may ask a packager to emit (§3), how a source is cut into pieces -(§4), and the integer video payloads a packager writes (§5.5). +(§4), the integer video payloads, refinement layers and stills a packager writes (§5), +packages (§6–§11), and their delivery over a simulated link (§8.9). -**Provenance.** Generated here: the `wai-pack` fixture generator (`wai-pack/src/fixture.rs`, `wai-pack/src/conformance.rs`) writes every frame from integer functions of the frame and pixel index, and a package's floor is an AV1 coding of those frames. No footage. +**Provenance.** Generated here: the `wai-pack` fixture generator (`wai-pack/src/fixture.rs`, `wai-pack/src/conformance.rs`) writes every frame from integer functions of the frame and pixel index; a package's bases, refinement layers and stills are codings of those frames, and its floor an AV1 coding of them by an encoder, as `supplied_floor.py` states. No footage. - `policy/` — policies a packager accepts (every rung one it may emit, §3.6), and `expected.json`: each policy's effective canonical JSON and @@ -12,7 +13,8 @@ digest and what it may ask a packager to emit (§3), how a source is cut into pi override changes it, §3.2), and the pairs `eq--a.toml` and `eq--b.toml`, two spellings of one policy that must have one digest: every default omitted or written out; standard or inline - tables, dotted keys, member order, string escapes, integer notation and comments. + tables, dotted keys, member order, string escapes, integer notation and comments; + and `max-piece.toml`, the longest piece a policy may allow (§4.4), at both bounds. - `refusals/` — `policy/*.toml`, one fault each (the byte layer's among them: a byte-order mark, bytes that are not UTF-8, an integer at 2^63, nesting to depth 33 and 80), and `source/*` (Y4M and WAV), and `expected.json`: each one's reason (§12). Every @@ -47,6 +49,14 @@ digest and what it may ask a packager to emit (§3), how a source is cut into pi `video-base-01`: objects, the stage index, the floor's objects and CMAF files, the catalogs, the receipt and `LISTING`. The fixture's bumper plays twice, so its pieces are one object each time. +- `packs/video-staged-01/` — a staged golden package (§5.4, §5.7): a moving shot, the + bumper, a still shot, the bumper again and a second moving shot, under a policy of an + `int_motion` base with two `int_refine` layers, a still for a static piece (a rule's + ladder), three classes (one capped by rate, one floor only), a moment whose rule + moves the last piece's layers to an 800 ms deadline, and layer lags. Its floor was + 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. - `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 @@ -58,6 +68,42 @@ digest and what it may ask a packager to emit (§3), how a source is cut into pi `"signature": "refused"`. - `catalogs/` — stage indexes no golden package has (refinements on more than one deadline), with the MoQ catalog each gives (§8.4). +- `live/` — the staged golden's source packed live (§9.2) with its key, in windows of + two pieces: each window's stage index and receipt (`windows//`), and + `expected.json`: each window's first piece and entry, its pieces, its index digest and + its receipt hash. Each window verifies against its receipt; the first starts the + chain, and each later one names the receipt before it as its `live-window` parent, + the index before it as its `prev_index_digest`, and continues its pieces and entries. + Its pieces, bases and refinements are the golden's, entry for entry (a live pack has + no floor). +- `stills/` — PNG files a still's reader reads or refuses (§5.7: any PNG of 8-bit RGB + without interlace), each a generated 5 × 3 picture written chunk by chunk, and + `expected.json`: each file's verdict and, read, the SHA-256 of its RGB8 samples. Read: + the module's own still, and files with a suggested palette, ancillary chunks, and the + image data split across two chunks. Refused: an interlace, bytes after `IEND`, a short + row, a palette after the image data, an unknown critical chunk, no `IEND`, a chunk + between the image data, a chunk's CRC, a sixth filter, another colour type, a zlib + 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 + 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 + and a still's digest of its one frame rather than its frame held for the piece (each + `prefix-digest`), and a piece and a still each longer than the policy's `max_ms` + (`index-policy-mismatch`, refused before a frame is decoded, so a still's declared + duration never sets what a verifier allocates); a policy past §4.4's longest piece + (`max_ms` 600001, `policy-piece-bounds`); and the still given the longest piece a + policy allows (`max_ms` ten minutes), its manifest naming those frames, refused by its + floor's unit count before any frame is decoded (`floor-object-mismatch`); a still + whose manifest's `target` holds a member more, and a top layer whose `target` is not + null (each `object-manifest`); and the still as a PNG §5.7 does not read, its one + dynamic block's lone distance code two bits long (`prefix-digest`). +- `delivery/` — `expected.json`: the staged golden packed live and delivered to a `tv` + subscriber over a simulated link (§8.9), under §8.5's priorities, without them, and + capped at layer 1: each piece's presented layer, base arrival and dropped layers, and + the byte counts. Labelled simulated. - `package-mutations/` — packages a verifier must refuse (§11.3), each the golden altered and, where the receipt or `LISTING` alone would catch the alteration, re-sealed with the golden's key: what a dishonest packager holding the key could @@ -81,6 +127,8 @@ index and the pixel position, and the corpus pins its SHA-256: piece, cut where `max` forces it. - `video-base-01` — 64 × 36: three shots with a one-second bumper between them, the bumper the same frames both times (its span reads time from its own start). +- `video-staged-01` — 64 × 36: a moving shot, the bumper, a shot that does not change, + the bumper again, and a second moving shot. Files are compared as bytes; `.gitattributes` turns off line-ending conversion. @@ -96,8 +144,9 @@ Files are compared as bytes; `.gitattributes` turns off line-ending conversion. digests of each equal pair to be equal, decodes every encode payload through `decode_int` to the encoder's reconstruction and recomputes its SSE. It packs each golden again and compares every file, verifies the committed package - (`wai-pack verify`), and gives every mutation and every package mutation its - reason. `cargo test` + (`wai-pack verify`), gives every mutation and every package mutation its + reason, reads every still to its verdict, and checks the live windows' chain. + `cargo test` runs the same check, with the scene cuts of each fixture stated as tests (`tests/scenecut.rs`). - `pack_verify.py`, Python standard library and, to sign what it re-derives, `cryptography` (TOML through `tomllib`, so 3.11 or @@ -116,10 +165,21 @@ Files are compared as bytes; `.gitattributes` turns off line-ending conversion. and sequence header reader, the CMAF files, and every catalog: it verifies each golden package in §11.3's order, rebuilding the floor's files and re-rendering the catalogs byte for byte, gives every mutation its reason, checks the version-1 receipts, and - verifies every package mutation in the same order, refused with its reason. - From the regenerated fixture it re-derives what only the source decides: the pieces, - their rules, moments and class plan, each base's SSE and step, and the encoder's - choices; and it checks the supplied floor against the policy's listing and the plan. + verifies every package mutation and staged mutation in the same order, refused with + its reason. Every prefix is decoded: a still by its own PNG reader, and each refinement + layer by the `WIR1` decoder of `../staged-conformance/staged_verify.py` (written from + staged-delivery); and the MoQ catalog is checked by that file's moq-streaming-format + §4.1 check. Every HTTP playlist of a golden is read by + `../http-conformance/http_verify.py`'s union, floor and adjacency checks + (http-delivery §4.2), and every file of `stills/` by its own PNG reader, to the + corpus's verdict. Each window of `live/` it reads as an index and a receipt, checks + the chain, and compares with the golden's index. From the regenerated fixture it re-derives what only the source decides: + the pieces, their rules, moments, ladders and class plan, each base's SSE and step and + the encoder's choices, each still's bytes, and each refinement layer's bytes with its + own rANS coder and table fit; and it checks the supplied floor against the policy's + listing and the plan. The delivery cases it re-derives from the fixture's cut and the + package's index by its own mapping onto the simulator of + `../staged-measure-conformance/sim_verify.py`. python3 pack-conformance/pack_verify.py pack-conformance @@ -138,6 +198,32 @@ Files are compared as bytes; `.gitattributes` turns off line-ending conversion. python3 pack-conformance/segment_diff.py 1 2000 +- `stage_diff.py` runs the staged packer differentially: refinement layers over random + frames and steps, stills of random pictures, piece plans under random policies (rules + with ladder subsets, maximum layers and deadlines, classes with rates, layer lags) and + deliveries of random publications over random links, each result of + `wai-pack stage-json` equal to `pack_verify.py`'s. + + python3 pack-conformance/stage_diff.py 1 3000 + +- `png_diff.py` runs a still's reader differentially: streams the zlib library writes + (every level and strategy, windows from 512 bytes to 32 KiB) and the packager's own + stills; dynamic blocks built on each of RFC 1951's and zlib's code-set rules (complete + codes, no distance code, a lone one-bit distance code used and misused, a lone longer + one, over-subscribed and incomplete codes of each kind, repeats with nothing before + them or past the code lengths, no end-of-block code, too many symbols, a distance past + the bytes written, stored blocks with a wrong complement, fixed blocks with symbols + they cannot use, several blocks of mixed kinds); and those altered. Each verdict, and + each read picture's SHA-256, from `wai-pack stage-json`'s `read-png` must be + `pack_verify.py`'s, which inflates with Python's zlib; and each construction must have + the verdict the rules give it. + + python3 pack-conformance/png_diff.py 1 4000 + +- `supplied_floor.py` is how a golden's supplied floor was written (an AV1 encoder over + each piece of the generated fixture, the policy's listing set to it); it is not a step + of verification, since an encoder's output is pinned by the listing, not re-derived. + - `source_diff.py` runs the source readers differentially: seeded Y4M and WAV mutations (dimensions and channel counts at and past their range, every format field, truncations, single-byte changes), each file's verdict from diff --git a/wai/pack-conformance/catalogs/expected.json b/wai/pack-conformance/catalogs/expected.json index 9be559248..52110366b 100644 --- a/wai/pack-conformance/catalogs/expected.json +++ b/wai/pack-conformance/catalogs/expected.json @@ -2,6 +2,7 @@ "cases": [ { "index": "catalogs/one-deadline.wsix", + "namespace": "catalogs", "layer_lag_ms": [ 0, 200, @@ -13,10 +14,11 @@ "w": 64, "h": 36 }, - "moq": "{\"tracks\":[{\"altGroup\":1,\"name\":\"v/l0\",\"packaging\":\"wai\",\"priority\":64,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_motion\"],\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":0,\"wai_receipts\":false},{\"depends\":[\"v/l0\"],\"name\":\"v/l1\",\"packaging\":\"wai\",\"priority\":96,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":1300,\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":1,\"wai_receipts\":false},{\"depends\":[\"v/l1\"],\"name\":\"v/l2\",\"packaging\":\"wai\",\"priority\":128,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":1100,\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":2,\"wai_receipts\":false},{\"altGroup\":1,\"codec\":\"av01.0.00M.08\",\"height\":36,\"name\":\"v/floor\",\"packaging\":\"cmaf\",\"renderGroup\":1,\"width\":64}],\"version\":1}\n" + "moq": "{\"tracks\":[{\"altGroup\":1,\"isLive\":false,\"name\":\"v/l0\",\"namespace\":\"catalogs\",\"packaging\":\"wai\",\"renderGroup\":1,\"role\":\"video\",\"wai_capabilities\":[\"wai.video.int_motion\"],\"wai_determinism\":\"decode-equivalence\",\"wai_receipt_alias\":6758122338894672,\"wai_receipts\":true,\"wai_receipts_track\":\"v/l0/receipts\",\"wai_staged\":{\"layer\":0,\"lead_ms\":null,\"set\":\"v\"}},{\"depends\":[\"v/l0\"],\"isLive\":false,\"name\":\"v/l1\",\"namespace\":\"catalogs\",\"packaging\":\"wai\",\"renderGroup\":1,\"role\":\"video\",\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":1300,\"wai_determinism\":\"decode-equivalence\",\"wai_receipt_alias\":7122743273789469,\"wai_receipts\":true,\"wai_receipts_track\":\"v/l1/receipts\",\"wai_staged\":{\"layer\":1,\"lead_ms\":null,\"set\":\"v\"}},{\"depends\":[\"v/l0\",\"v/l1\"],\"isLive\":false,\"name\":\"v/l2\",\"namespace\":\"catalogs\",\"packaging\":\"wai\",\"renderGroup\":1,\"role\":\"video\",\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":1100,\"wai_determinism\":\"decode-equivalence\",\"wai_receipt_alias\":5138816494470210,\"wai_receipts\":true,\"wai_receipts_track\":\"v/l2/receipts\",\"wai_staged\":{\"layer\":2,\"lead_ms\":null,\"set\":\"v\"}},{\"altGroup\":1,\"codec\":\"av01.0.00M.08\",\"height\":36,\"name\":\"v/floor\",\"namespace\":\"catalogs\",\"packaging\":\"cmaf\",\"renderGroup\":1,\"width\":64},{\"isLive\":false,\"name\":\"v/l0/receipts\",\"namespace\":\"catalogs\",\"packaging\":\"wai-receipts\"},{\"isLive\":false,\"name\":\"v/l1/receipts\",\"namespace\":\"catalogs\",\"packaging\":\"wai-receipts\"},{\"isLive\":false,\"name\":\"v/l2/receipts\",\"namespace\":\"catalogs\",\"packaging\":\"wai-receipts\"}],\"version\":1}\n" }, { "index": "catalogs/layer-1-on-its-own-deadline.wsix", + "namespace": "catalogs", "layer_lag_ms": [ 0, 200, @@ -28,10 +30,11 @@ "w": 64, "h": 36 }, - "moq": "{\"tracks\":[{\"altGroup\":1,\"name\":\"v/l0\",\"packaging\":\"wai\",\"priority\":64,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_motion\"],\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":0,\"wai_receipts\":false},{\"depends\":[\"v/l0\"],\"name\":\"v/l1@800\",\"packaging\":\"wai\",\"priority\":96,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":600,\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":1,\"wai_receipts\":false},{\"depends\":[\"v/l1@800\"],\"name\":\"v/l2\",\"packaging\":\"wai\",\"priority\":128,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":1100,\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":2,\"wai_receipts\":false},{\"altGroup\":1,\"codec\":\"av01.0.00M.08\",\"height\":36,\"name\":\"v/floor\",\"packaging\":\"cmaf\",\"renderGroup\":1,\"width\":64}],\"version\":1}\n" + "moq": "{\"tracks\":[{\"altGroup\":1,\"isLive\":false,\"name\":\"v/l0\",\"namespace\":\"catalogs\",\"packaging\":\"wai\",\"renderGroup\":1,\"role\":\"video\",\"wai_capabilities\":[\"wai.video.int_motion\"],\"wai_determinism\":\"decode-equivalence\",\"wai_receipt_alias\":6758122338894672,\"wai_receipts\":true,\"wai_receipts_track\":\"v/l0/receipts\",\"wai_staged\":{\"layer\":0,\"lead_ms\":null,\"set\":\"v\"}},{\"depends\":[\"v/l0\"],\"isLive\":false,\"name\":\"v/l1@800\",\"namespace\":\"catalogs\",\"packaging\":\"wai\",\"renderGroup\":1,\"role\":\"video\",\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":600,\"wai_determinism\":\"decode-equivalence\",\"wai_receipt_alias\":6300050926507138,\"wai_receipts\":true,\"wai_receipts_track\":\"v/l1@800/receipts\",\"wai_staged\":{\"layer\":1,\"lead_ms\":null,\"set\":\"v\"}},{\"depends\":[\"v/l0\",\"v/l1@800\"],\"isLive\":false,\"name\":\"v/l2\",\"namespace\":\"catalogs\",\"packaging\":\"wai\",\"renderGroup\":1,\"role\":\"video\",\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":1100,\"wai_determinism\":\"decode-equivalence\",\"wai_receipt_alias\":5138816494470210,\"wai_receipts\":true,\"wai_receipts_track\":\"v/l2/receipts\",\"wai_staged\":{\"layer\":2,\"lead_ms\":null,\"set\":\"v\"}},{\"altGroup\":1,\"codec\":\"av01.0.00M.08\",\"height\":36,\"name\":\"v/floor\",\"namespace\":\"catalogs\",\"packaging\":\"cmaf\",\"renderGroup\":1,\"width\":64},{\"isLive\":false,\"name\":\"v/l0/receipts\",\"namespace\":\"catalogs\",\"packaging\":\"wai-receipts\"},{\"isLive\":false,\"name\":\"v/l1@800/receipts\",\"namespace\":\"catalogs\",\"packaging\":\"wai-receipts\"},{\"isLive\":false,\"name\":\"v/l2/receipts\",\"namespace\":\"catalogs\",\"packaging\":\"wai-receipts\"}],\"version\":1}\n" }, { "index": "catalogs/both-layers-on-800.wsix", + "namespace": "catalogs", "layer_lag_ms": [ 0, 200, @@ -43,7 +46,7 @@ "w": 64, "h": 36 }, - "moq": "{\"tracks\":[{\"altGroup\":1,\"name\":\"v/l0\",\"packaging\":\"wai\",\"priority\":64,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_motion\"],\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":0,\"wai_receipts\":false},{\"depends\":[\"v/l0\"],\"name\":\"v/l1@800\",\"packaging\":\"wai\",\"priority\":96,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":600,\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":1,\"wai_receipts\":false},{\"depends\":[\"v/l1@800\"],\"name\":\"v/l2@800\",\"packaging\":\"wai\",\"priority\":128,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":400,\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":2,\"wai_receipts\":false},{\"altGroup\":1,\"codec\":\"av01.0.00M.08\",\"height\":36,\"name\":\"v/floor\",\"packaging\":\"cmaf\",\"renderGroup\":1,\"width\":64}],\"version\":1}\n" + "moq": "{\"tracks\":[{\"altGroup\":1,\"isLive\":false,\"name\":\"v/l0\",\"namespace\":\"catalogs\",\"packaging\":\"wai\",\"renderGroup\":1,\"role\":\"video\",\"wai_capabilities\":[\"wai.video.int_motion\"],\"wai_determinism\":\"decode-equivalence\",\"wai_receipt_alias\":6758122338894672,\"wai_receipts\":true,\"wai_receipts_track\":\"v/l0/receipts\",\"wai_staged\":{\"layer\":0,\"lead_ms\":null,\"set\":\"v\"}},{\"depends\":[\"v/l0\"],\"isLive\":false,\"name\":\"v/l1@800\",\"namespace\":\"catalogs\",\"packaging\":\"wai\",\"renderGroup\":1,\"role\":\"video\",\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":600,\"wai_determinism\":\"decode-equivalence\",\"wai_receipt_alias\":6300050926507138,\"wai_receipts\":true,\"wai_receipts_track\":\"v/l1@800/receipts\",\"wai_staged\":{\"layer\":1,\"lead_ms\":null,\"set\":\"v\"}},{\"depends\":[\"v/l0\",\"v/l1@800\"],\"isLive\":false,\"name\":\"v/l2@800\",\"namespace\":\"catalogs\",\"packaging\":\"wai\",\"renderGroup\":1,\"role\":\"video\",\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":400,\"wai_determinism\":\"decode-equivalence\",\"wai_receipt_alias\":2193082958864004,\"wai_receipts\":true,\"wai_receipts_track\":\"v/l2@800/receipts\",\"wai_staged\":{\"layer\":2,\"lead_ms\":null,\"set\":\"v\"}},{\"altGroup\":1,\"codec\":\"av01.0.00M.08\",\"height\":36,\"name\":\"v/floor\",\"namespace\":\"catalogs\",\"packaging\":\"cmaf\",\"renderGroup\":1,\"width\":64},{\"isLive\":false,\"name\":\"v/l0/receipts\",\"namespace\":\"catalogs\",\"packaging\":\"wai-receipts\"},{\"isLive\":false,\"name\":\"v/l1@800/receipts\",\"namespace\":\"catalogs\",\"packaging\":\"wai-receipts\"},{\"isLive\":false,\"name\":\"v/l2@800/receipts\",\"namespace\":\"catalogs\",\"packaging\":\"wai-receipts\"}],\"version\":1}\n" } ] } diff --git a/wai/pack-conformance/delivery/expected.json b/wai/pack-conformance/delivery/expected.json new file mode 100644 index 000000000..59396b3b2 --- /dev/null +++ b/wai/pack-conformance/delivery/expected.json @@ -0,0 +1,321 @@ +{ + "pack": "video-staged-01", + "class": "tv", + "label": "simulated", + "cases": [ + { + "name": "strict", + "plan": { + "rtt_us": 0, + "bytes_per_ms": 40, + "chunk_bytes": 1200, + "enc_us": 0, + "startup_us": 3200000, + "layer_cap": null, + "fifo": false + }, + "sim_plan": { + "plan": "wai-sim/1", + "mode": "live", + "link": { + "rtt_us": 0, + "bytes_per_ms": 40, + "chunk_bytes": 1200 + }, + "sender": { + "schedule": "strict-priority", + "carriage": "subgroup", + "layer_cap": null, + "delivery_timeout_us": { + "base": null, + "refinement": null + }, + "vod_window_us": null + }, + "sink": { + "workers": 1, + "decode_order": "base-first", + "decode_us": { + "base": 1, + "refinement": 1 + }, + "playout_start_us": 3200000, + "late_refinement": "discard" + } + }, + "presented": [ + 0, + 1, + 0, + 1, + 0 + ], + "outcome": { + "label": "simulated", + "never_sent_capped": 0, + "pieces": [ + { + "piece": 0, + "due_us": 3200000, + "presented": 0, + "floor_only": false, + "base_arrival_us": 2890750, + "dropped": [ + 1 + ] + }, + { + "piece": 1, + "due_us": 5200000, + "presented": 1, + "floor_only": false, + "base_arrival_us": 3493200, + "dropped": [ + 2 + ] + }, + { + "piece": 2, + "due_us": 6200000, + "presented": 0, + "floor_only": false, + "base_arrival_us": 4864350, + "dropped": [] + }, + { + "piece": 3, + "due_us": 7800000, + "presented": 1, + "floor_only": false, + "base_arrival_us": 6082450, + "dropped": [ + 2 + ] + }, + { + "piece": 4, + "due_us": 8800000, + "presented": 0, + "floor_only": false, + "base_arrival_us": 8750250, + "dropped": [ + 1, + 2 + ] + } + ], + "delivered": 139408, + "dropped": 275887, + "abandoned": 55200 + } + }, + { + "name": "fifo", + "plan": { + "rtt_us": 0, + "bytes_per_ms": 40, + "chunk_bytes": 1200, + "enc_us": 0, + "startup_us": 3200000, + "layer_cap": null, + "fifo": true + }, + "sim_plan": { + "plan": "wai-sim/1", + "mode": "live", + "link": { + "rtt_us": 0, + "bytes_per_ms": 40, + "chunk_bytes": 1200 + }, + "sender": { + "schedule": "fifo", + "carriage": "subgroup", + "layer_cap": null, + "delivery_timeout_us": { + "base": null, + "refinement": null + }, + "vod_window_us": null + }, + "sink": { + "workers": 1, + "decode_order": "base-first", + "decode_us": { + "base": 1, + "refinement": 1 + }, + "playout_start_us": 3200000, + "late_refinement": "discard" + } + }, + "presented": [ + 0, + 0, + 0, + 1, + 0 + ], + "outcome": { + "label": "simulated", + "never_sent_capped": 0, + "pieces": [ + { + "piece": 0, + "due_us": 3200000, + "presented": 0, + "floor_only": false, + "base_arrival_us": 2890750, + "dropped": [ + 1 + ] + }, + { + "piece": 1, + "due_us": 5200000, + "presented": 0, + "floor_only": false, + "base_arrival_us": 4003200, + "dropped": [ + 1, + 2 + ] + }, + { + "piece": 2, + "due_us": 6200000, + "presented": 0, + "floor_only": false, + "base_arrival_us": 4864350, + "dropped": [] + }, + { + "piece": 3, + "due_us": 7800000, + "presented": 1, + "floor_only": false, + "base_arrival_us": 6082450, + "dropped": [ + 2 + ] + }, + { + "piece": 4, + "due_us": 8800000, + "presented": 0, + "floor_only": false, + "base_arrival_us": 8750250, + "dropped": [ + 1, + 2 + ] + } + ], + "delivered": 108709, + "dropped": 275386, + "abandoned": 86400 + } + }, + { + "name": "capped", + "plan": { + "rtt_us": 0, + "bytes_per_ms": 40, + "chunk_bytes": 1200, + "enc_us": 0, + "startup_us": 3200000, + "layer_cap": 1, + "fifo": false + }, + "sim_plan": { + "plan": "wai-sim/1", + "mode": "live", + "link": { + "rtt_us": 0, + "bytes_per_ms": 40, + "chunk_bytes": 1200 + }, + "sender": { + "schedule": "strict-priority", + "carriage": "subgroup", + "layer_cap": 1, + "delivery_timeout_us": { + "base": null, + "refinement": null + }, + "vod_window_us": null + }, + "sink": { + "workers": 1, + "decode_order": "base-first", + "decode_us": { + "base": 1, + "refinement": 1 + }, + "playout_start_us": 3200000, + "late_refinement": "discard" + } + }, + "presented": [ + 0, + 1, + 0, + 1, + 0 + ], + "outcome": { + "label": "simulated", + "never_sent_capped": 181901, + "pieces": [ + { + "piece": 0, + "due_us": 3200000, + "presented": 0, + "floor_only": false, + "base_arrival_us": 2890750, + "dropped": [ + 1 + ] + }, + { + "piece": 1, + "due_us": 5200000, + "presented": 1, + "floor_only": false, + "base_arrival_us": 3493200, + "dropped": [] + }, + { + "piece": 2, + "due_us": 6200000, + "presented": 0, + "floor_only": false, + "base_arrival_us": 4864350, + "dropped": [] + }, + { + "piece": 3, + "due_us": 7800000, + "presented": 1, + "floor_only": false, + "base_arrival_us": 6082450, + "dropped": [] + }, + { + "piece": 4, + "due_us": 8800000, + "presented": 0, + "floor_only": false, + "base_arrival_us": 8750250, + "dropped": [ + 1 + ] + } + ], + "delivered": 139408, + "dropped": 122786, + "abandoned": 26400 + } + } + ] +} diff --git a/wai/pack-conformance/live/expected.json b/wai/pack-conformance/live/expected.json new file mode 100644 index 000000000..83f381b36 --- /dev/null +++ b/wai/pack-conformance/live/expected.json @@ -0,0 +1,31 @@ +{ + "pack": "video-staged-01", + "component": "v", + "window_pieces": 2, + "windows": [ + { + "window": 0, + "first_piece": 0, + "pieces": 2, + "first_entry": 0, + "index_digest": "66b9399454a9937a52eef21bb7c452c8a03257d707d4a8238e99db447926aca0", + "receipt_hash": "c8143d2452499a4d3772e40c528255eae90e781511995a76b1cabd12c87b5950" + }, + { + "window": 1, + "first_piece": 2, + "pieces": 2, + "first_entry": 6, + "index_digest": "b31b0d7e1faab1b7331bf26f8400ca848692aba315e2e8083086d1523baac510", + "receipt_hash": "03e9c415609e457da2a3d47298d91c541b36f03b634fb56060c9a4da1c2d8f13" + }, + { + "window": 2, + "first_piece": 4, + "pieces": 1, + "first_entry": 10, + "index_digest": "61c64b3027c30891039da2d2b8e30744d83bd6f5b695d6b00ea616c575ceab94", + "receipt_hash": "11621610dbfff4321e11dc4ff8ba32cb2cb9e61fb17707a114bea4ec85d84e6c" + } + ] +} diff --git a/wai/pack-conformance/live/windows/0/index-v.wsix b/wai/pack-conformance/live/windows/0/index-v.wsix new file mode 100644 index 000000000..4162eb3fa Binary files /dev/null and b/wai/pack-conformance/live/windows/0/index-v.wsix differ diff --git a/wai/pack-conformance/live/windows/0/package.receipt.json b/wai/pack-conformance/live/windows/0/package.receipt.json new file mode 100644 index 000000000..8645505f3 --- /dev/null +++ b/wai/pack-conformance/live/windows/0/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"66b9399454a9937a52eef21bb7c452c8a03257d707d4a8238e99db447926aca0","media_class":"video","stage_root":"3aeb034c4d2f20e768d8e08bec6911370ff20790f8b04d38bc8618ecf18c2edf"}],"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":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"c8143d2452499a4d3772e40c528255eae90e781511995a76b1cabd12c87b5950","sig":"03bb324e8047e88a7d28494d3c132473992b40742476691b11ea0d4405fd6800928bed7a45303f0c7e3ccb293dfb41a1e2e68b4da3c06c6b70f35d44664fec05","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":260808,"total_bytes":260808,"total_entries":6,"unique_objects":6,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/live/windows/1/index-v.wsix b/wai/pack-conformance/live/windows/1/index-v.wsix new file mode 100644 index 000000000..5e7f198c5 Binary files /dev/null and b/wai/pack-conformance/live/windows/1/index-v.wsix differ diff --git a/wai/pack-conformance/live/windows/1/package.receipt.json b/wai/pack-conformance/live/windows/1/package.receipt.json new file mode 100644 index 000000000..7db9eb4ad --- /dev/null +++ b/wai/pack-conformance/live/windows/1/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"b31b0d7e1faab1b7331bf26f8400ca848692aba315e2e8083086d1523baac510","media_class":"video","stage_root":"c64c85665b4723be6efbd42ecd2c69e201a8d12c7eb0f2f3739186f5fdc368ed"}],"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":"live-window","parent_receipt_hash":"c8143d2452499a4d3772e40c528255eae90e781511995a76b1cabd12c87b5950","policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"03e9c415609e457da2a3d47298d91c541b36f03b634fb56060c9a4da1c2d8f13","sig":"fb568c45c8645e5213aed52462bbcdf1354f2b55fc0cd134ce563ab2b5721c2f5346111392ef46d2b193aacaa4553cae1a202e6149fcd1abdea9e743d0037a00","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":81674,"total_bytes":81674,"total_entries":4,"unique_objects":4,"version":2,"work_units":4} \ No newline at end of file diff --git a/wai/pack-conformance/live/windows/2/index-v.wsix b/wai/pack-conformance/live/windows/2/index-v.wsix new file mode 100644 index 000000000..c8bf7b0ee Binary files /dev/null and b/wai/pack-conformance/live/windows/2/index-v.wsix differ diff --git a/wai/pack-conformance/live/windows/2/package.receipt.json b/wai/pack-conformance/live/windows/2/package.receipt.json new file mode 100644 index 000000000..9f921e639 --- /dev/null +++ b/wai/pack-conformance/live/windows/2/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"61c64b3027c30891039da2d2b8e30744d83bd6f5b695d6b00ea616c575ceab94","media_class":"video","stage_root":"32198182aa20797140685219dfcaa36ac3611d1ae2f3c0c77cb9161317d86c1d"}],"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":"live-window","parent_receipt_hash":"03e9c415609e457da2a3d47298d91c541b36f03b634fb56060c9a4da1c2d8f13","policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"11621610dbfff4321e11dc4ff8ba32cb2cb9e61fb17707a114bea4ec85d84e6c","sig":"651ed23c5b03c585516ecafbbe23c589a83678bae88f26539bd7aedfc92c3241ee0c38aa53f233145cff1c2a7eaba5233329dfd5b3eae2d0c53734c5a2d66900","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":214312,"total_bytes":214312,"total_entries":3,"unique_objects":3,"version":2,"work_units":3} \ No newline at end of file diff --git a/wai/pack-conformance/pack_verify.py b/wai/pack-conformance/pack_verify.py index bbe78dcff..edb463710 100644 --- a/wai/pack-conformance/pack_verify.py +++ b/wai/pack-conformance/pack_verify.py @@ -62,20 +62,30 @@ from their specifications, reads the capability registry from its published expo Exit status 0 only if every vector verifies. Python 3.11 or later (tomllib). """ import hashlib +import itertools import json import os import re import shutil +import struct import sys import tempfile import tomllib +import zlib HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance")) sys.path.insert(0, os.path.join(HERE, "..", "int-payload-conformance")) +sys.path.insert(0, os.path.join(HERE, "..", "staged-conformance")) +sys.path.insert(0, os.path.join(HERE, "..", "staged-measure-conformance")) from _blake3 import blake3 # noqa: E402 import _ed25519 as ED # noqa: E402 import int_payload_verify as IPV # noqa: E402 +import staged_verify as SV # noqa: E402 +import sim_verify as SIM # noqa: E402 +sys.path.insert(0, os.path.join(HERE, "..", "http-conformance")) +import http_verify as HV # noqa: E402 +HV.load_registry() MAX_INTEGER = (1 << 53) - 1 # The longest manifest a reader must accept (SPEC §2, *Reader limits*). @@ -469,6 +479,10 @@ def constrain(P, lag): no("policy-open-gop") if not (1 <= s["min_ms"] <= s["target_ms"] <= s["max_ms"]): no("policy-piece-bounds") + # §4.4: at most ten minutes, and at most 2^24 units on every component's timebase. + if s["max_ms"] > 600000 or any(units_from_ms(s["max_ms"], c["timebase"]["num"], c["timebase"]["den"]) > 1 << 24 + for c in P["component"]): + no("policy-piece-bounds") comps = {c["id"] for c in P["component"]} if any(r["component"] not in comps for r in P["ladder"]): no("policy-ladder-component") @@ -506,9 +520,14 @@ def constrain(P, lag): no("policy-class-layer") for r in P["rule"]: if "ladder" in r["then"]: - rs = [by.get(i) for i in r["then"]["ladder"]] + ids = r["then"]["ladder"] + 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). ok = rs and all(rs) and all(x["component"] == rs[0]["component"] for x in rs) \ - and any(x["role"] == "base" for x in rs) + and len(set(ids)) == len(ids) and sum(x["role"] == "base" for x in rs) == 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: no("policy-rule-ladder") lag = [0] * n if lag is None else lag @@ -821,6 +840,13 @@ def fixture_spans(name): (50, ramp(30, 220, 2, 1, 1, True, 12, 150, 120), None), (25, shot, None, LOCAL), (40, ramp(16, 160, 1, 2, 1, True, 13, 128, 128), None)] + if name == "video-staged-01": + shot = blocks([40, 120, 200], 8, 6, 1, 100, 160) + return 64, 36, [(50, ramp(20, 200, 1, 1, 1, True, 21, 110, 140), None), + (25, shot, None, LOCAL), + (40, ramp(60, 180, 2, 1, 0, False, 0, 140, 116), None), + (25, shot, None, LOCAL), + (60, ramp(30, 120, 1, 2, 1, True, 22, 150, 120), None)] if name == "cuts-01": a = ramp(20, 100, 1, 1, 1, True, 1, 110, 140) b = ramp(120, 220, 2, 1, 1, True, 2, 150, 120) @@ -1615,6 +1641,334 @@ def object_rules(ix, i, data): return m +# ---------------------------------------------------------------- stages (§5.4, §5.7) + +RANS_L = 1 << 31 + + +def rans_encode(values, cdf, offset): + """integer-payloads §2's coder, written from the text: each value's symbols (its table + symbol, and past the table's span the bypass escape: a count of 4-bit groups, itself + in groups of at most 15, then the groups low first), coded last first into a 64-bit + state that emits its low 32 bits while too large; the stream is the final state, low + word first, then the emitted words in the order a decoder reads them.""" + n = len(cdf) + top = n - 2 + syms = [] + for value in values: + v = value - offset + sym, raw = (top, -2 * v - 1) if v < 0 else (top, 2 * (v - top)) if v >= top else (v, None) + syms.append((cdf[sym], cdf[sym + 1] - cdf[sym])) + if raw is not None: + groups = 0 + while raw >> (4 * groups): + groups += 1 + count = groups + while count >= 15: + syms.append((None, 15)) + count -= 15 + syms.append((None, count)) + syms += [(None, (raw >> (4 * j)) & 15) for j in range(groups)] + x, words = RANS_L, [] + for start, f in reversed(syms): + freq = f if start is not None else 1 << 12 + x_max = ((RANS_L >> 16) << 32) * freq + if start is not None: + while x >= x_max: + words.append(x & M32) + x >>= 32 + x = ((x // freq) << 16) + x % freq + start + else: + if x >= x_max: + words.append(x & M32) + x >>= 32 + x = (x << 4) | f + return struct.pack("> 32) + b"".join(struct.pack("= 8: + self.out.append(self.acc & 255) + self.acc >>= 8 + self.n -= 8 + + def code(self, c, n): + self.put(int(format(c, f"0{n}b")[::-1], 2) if n else 0, n) + + def lit(self, s): + """RFC 1951 §3.2.6's fixed literal/length code.""" + if s <= 143: + self.code(0x30 + s, 8) + elif s <= 255: + self.code(0x190 + s - 144, 9) + elif s <= 279: + self.code(s - 256, 7) + else: + self.code(0xC0 + s - 280, 8) + + def finish(self): + return bytes(self.out + (bytes([self.acc]) if self.n else b"")) + + +def deflate_fixed(d): + """§5.7's deflate: one final block of the fixed codes; at each position with three + bytes left, the latest earlier position within 32768 bytes that starts with the same + three bytes is the match, as long as the two runs agree (at most 258); else a literal; + every position a symbol covers with three bytes left becomes its three bytes' latest.""" + b = DeflateBits() + b.put(1, 1) + b.put(1, 2) + last, n, i = {}, len(d), 0 + while i < n: + j = last.get(d[i:i + 3]) if i + 3 <= n else None + if j is not None and i - j <= 32768: + m = 0 + while m < min(258, n - i) and d[j + m] == d[i + m]: + m += 1 + li = max(k for k, x in enumerate(LEN_BASE) if x <= m) + b.lit(257 + li) + b.put(m - LEN_BASE[li], LEN_EXTRA[li]) + di = max(k for k, x in enumerate(DIST_BASE) if x <= i - j) + b.code(di, 5) + b.put(i - j - DIST_BASE[di], DIST_EXTRA[di]) + else: + b.lit(d[i]) + m = 1 + for q in range(i, i + m): + if q + 3 <= n: + last[d[q:q + 3]] = q + i += m + b.lit(256) + return b.finish() + + +def png_still(rgb, w, h): + """§5.7: a frame as the still the reference packager writes: IHDR (8-bit RGB), one IDAT + of a zlib stream (0x78 0x01, the deflate above over the rows each filtered Sub, the + Adler-32), IEND.""" + stride = 3 * w + raw = bytearray() + for y in range(h): + row = rgb[y * stride:(y + 1) * stride] + raw.append(1) + raw += bytes((row[i] - (row[i - 3] if i >= 3 else 0)) & 255 for i in range(stride)) + z = b"\x78\x01" + deflate_fixed(bytes(raw)) + struct.pack(">I", zlib.adler32(bytes(raw))) + + def chunk(kind, data): + return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data)) + return b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0)) + chunk(b"IDAT", z) + chunk(b"IEND", b"") + + +def png_frame(b): + """§5.7: a still read as any PNG of 8-bit RGB without interlace, by PNG's chunk rules: + every chunk's type four letters and its CRC checked; IHDR first (8-bit RGB, methods + 0, interlace 0); an optional PLTE before the image data, read past; the IDATs + consecutive; ancillary chunks read past; any other critical chunk refused; IEND last, + empty, nothing after it; the zlib stream whole with its Adler-32, its rows exactly + 1 + 3·width bytes, its window 32 KiB at most and nothing after it; the five filters. Its width, height and RGB8 bytes; ValueError + where it is not such a PNG.""" + if b[:8] != b"\x89PNG\r\n\x1a\n": + raise ValueError("not a PNG signature") + at, ihdr, z, ended, plte, idat = 8, None, b"", False, False, 0 + while at < len(b): + if ended: + raise ValueError("bytes after IEND") + if at + 12 > len(b): + raise ValueError("a chunk past the end") + n = struct.unpack_from(">I", b, at)[0] + kind, data = b[at + 4:at + 8], b[at + 8:at + 8 + n] + if at + 12 + n > len(b): + raise ValueError("a chunk past the end") + if not all(65 <= c <= 90 or 97 <= c <= 122 for c in kind): + raise ValueError("a chunk type that is not four letters") + if zlib.crc32(kind + data) != struct.unpack_from(">I", b, at + 8 + n)[0]: + raise ValueError("a chunk CRC") + if idat == 1 and kind != b"IDAT": + idat = 2 + if kind == b"IHDR": + if ihdr is not None or at != 8 or n != 13: + raise ValueError("IHDR is not one 13-byte chunk first") + w, h = struct.unpack(">II", data[:8]) + if data[8:] != bytes([8, 2, 0, 0, 0]) or w == 0 or h == 0 or 3 * w * h > 1 << 26: + raise ValueError("not 8-bit RGB without interlace, or a size past 2^26 samples") + ihdr = (w, h) + elif ihdr is None: + raise ValueError("a chunk before IHDR") + elif kind == b"PLTE": + if plte or idat or n == 0 or n % 3 or n > 768: + raise ValueError("PLTE: more than one, after the image data, or not 1 to 256 entries") + plte = True + elif kind == b"IDAT": + if idat == 2: + raise ValueError("IDAT chunks that are not consecutive") + idat = 1 + z += data + elif kind == b"IEND": + if n: + raise ValueError("IEND with data") + ended = True + elif not kind[0] & 0x20: + raise ValueError("an unknown critical chunk") + at += 12 + n + if ihdr is None or not ended: + raise ValueError("no IHDR or no IEND") + w, h = ihdr + if not z or z[0] >> 4 > 7: + raise ValueError("the zlib stream: no header, or a window past 32 KiB") + d = zlib.decompressobj() + try: + raw = d.decompress(z, (3 * w + 1) * h + 1) + except zlib.error as x: + raise ValueError(f"the zlib stream: {x}") + if not d.eof or d.unused_data or d.unconsumed_tail or len(raw) != (3 * w + 1) * h: + raise ValueError("the rows are not the picture's size, or the stream is not whole") + stride, out = 3 * w, bytearray(3 * w * h) + for y in range(h): + f, row = raw[y * (stride + 1)], raw[y * (stride + 1) + 1:(y + 1) * (stride + 1)] + if f > 4: + raise ValueError(f"row {y}: filter {f}") + for i in range(stride): + a = out[y * stride + i - 3] if i >= 3 else 0 + up = out[(y - 1) * stride + i] if y else 0 + c = out[(y - 1) * stride + i - 3] if y and i >= 3 else 0 + if f == 4: + pp = a + up - c + pa, pb, pc = abs(pp - a), abs(pp - up), abs(pp - c) + pr = a if pa <= pb and pa <= pc else (up if pb <= pc else c) + else: + pr = (0, a, up, (a + up) // 2)[f] + out[y * stride + i] = (row[i] + pr) & 255 + return w, h, bytes(out) + + +# ---------------------------------------------------------------- delivery (§8.9) + + +def deliver(P, row, published, plan): + """§8.9: a live packer's publications delivered to a subscriber whose class plan is + `row`, simulated by staged-measurement Appendix B's simulator (sim_verify.py's, written + from that text): the objects the class lists, on the frames as units, each published + at its frame's instant plus the encoder's delay plus its layer's lag, a refinement with + its track's timeout (deadline − lag); live, strict priority (§8.5) or fifo, a subgroup + each, the subscriber's cap, one worker, decodes of 1 µs, playout from the start-up, a + late refinement discarded. Per piece, at its first frame's instant: the highest layer + presented, its base's arrival and the layers dropped.""" + comp = P["component"][0] + num, den = comp["timebase"]["num"], comp["timebase"]["den"] + if 1_000_000 * den % num: + raise ValueError("a frame that is not a whole number of microseconds") + frame_us = 1_000_000 * den // num + lag = P["live"]["layer_lag_ms"] + objects = [] + for x in published: + listed = row[x["piece"]] + if listed == 0xFF or x["layer"] > listed: + continue + lg = lag[x["layer"]] if x["layer"] < len(lag) else 0 + o = {"id": f"p{x['piece']}/l{x['layer']}", "base": x["piece"], "layer": x["layer"], + "units": [x["start"], x["duration"]], "bytes": x["bytes"], + "capture_us": x["at_frame"] * frame_us + plan["enc_us"] + lg * 1000} + if x["layer"]: + o["timeout_us"] = max(0, x["deadline_ms"] - lg) * 1000 + objects.append(o) + m = {"unit_us": frame_us, "objects": objects} + sp = {"plan": "wai-sim/1", "mode": "live", + "link": {"rtt_us": plan["rtt_us"], "bytes_per_ms": plan["bytes_per_ms"], "chunk_bytes": plan["chunk_bytes"]}, + "sender": {"schedule": "fifo" if plan.get("fifo") else "strict-priority", "carriage": "subgroup", + "layer_cap": plan.get("layer_cap"), "delivery_timeout_us": {"base": None, "refinement": None}, + "vod_window_us": None}, + "sink": {"workers": 1, "decode_order": "base-first", "decode_us": {"base": 1, "refinement": 1}, + "playout_start_us": plan["startup_us"], "late_refinement": "discard"}} + # A class that lists nothing of any piece sends nothing. + r = SIM.simulate(m, sp) if objects else {"objects": [], "presentations": [], "delivered": 0, "dropped": 0, + "abandoned": 0, "never_sent_capped": 0} + pieces = {} + for x in published: + pieces.setdefault(x["piece"], {"piece": x["piece"], "due_us": plan["startup_us"] + x["start"] * frame_us, + "presented": None, "floor_only": row[x["piece"]] == 0xFF, + "base_arrival_us": None, "dropped": [], "_start": x["start"]}) + for o, ev in zip(objects, r["objects"]): + pc = pieces[o["base"]] + if o["layer"] == 0: + pc["base_arrival_us"] = ev["arrived_us"] + if ev["state"] in ("dropped", "abandoned"): + pc["dropped"].append(o["layer"]) + for pc in pieces.values(): + at = [l for u, l, tt in r["presentations"] if u == pc["_start"] and tt <= pc["due_us"]] + if 1 in at: + pc["presented"] = max(at) - 1 + del pc["_start"] + return {"label": "simulated", "pieces": [pieces[k] for k in sorted(pieces)], "delivered": r["delivered"], + "dropped": r["dropped"], "abandoned": r["abandoned"], "never_sent_capped": r["never_sent_capped"]} + + +def published_of(P, ix, n_frames): + """What a live packer publishes for the package whose index is `ix` (§9.2, §4.3): each + piece's bases and refinements, published once the frame that decides the piece's end + has arrived — the frame lookahead_frames past the next piece's start, or the source's + last.""" + L = P["segmentation"]["lookahead_frames"] + pcs, E = ix["pieces"], ix["entries"] + out = [] + for k, pc in enumerate(pcs): + at = min(pcs[k + 1]["start"] + L, n_frames - 1) if k + 1 < len(pcs) else n_frames - 1 + for e in E[pc["first_entry"]:pc["first_entry"] + pc["n_entries"]]: + if e["role"] in (BASE, REFINEMENT): + out.append({"piece": ix["first_piece"] + k, "start": pc["start"], "duration": pc["duration"], "at_frame": at, + "layer": e["layer"], "deadline_ms": e["deadline"], "bytes": e["byte_len"]}) + return out + + # ---------------------------------------------------------------- package receipts (§9) @@ -2132,25 +2486,58 @@ def listed_bytes(ix, k, top): if e["role"] in (BASE, REFINEMENT) and e["layer"] <= top) +def piece_then(P, ix, k): + """§8.1: piece k's matching rules and its moments as bits, and the rules' `then` + folded in document order (a later member overrides an earlier one).""" + num, den = ix["tb"] + pc = ix["pieces"][k] + s_ms, e_ms = pc["start"] * den * 1000 // num, (pc["start"] + pc["duration"]) * den * 1000 // num + moments = [i for i, m in enumerate(P["moment"]) if s_ms < m["to_ms"] and e_ms > m["from_ms"]] + tags = {t for i in moments for t in P["moment"][i]["tags"]} + rules, then = [], {} + for i, r in enumerate(P["rule"]): + w = r["when"] + if "piece_kind" in w and PIECE_KINDS[pc["kind"]] not in w["piece_kind"]: + continue + if "moment_tag" in w and not tags & set(w["moment_tag"]): + continue + rules.append(i) + then.update(r["then"]) + return sum(1 << i for i in rules), sum(1 << i for i in moments), then + + +def piece_ladder(P, cid, then): + """§5.7: the rungs a piece of component cid is coded in, (base, [refinements in layer + order], [floors]): the rules' `then.ladder` where it names rungs of cid, else the + component's first base rung in ladder order, the refinements over it and its floors.""" + mine = [r for r in P["ladder"] if r["component"] == cid] + ids = then.get("ladder") + if ids is not None and all(any(r["id"] == i for r in mine) for i in ids): + mine = [r for r in mine if r["id"] in ids] + base = next(r for r in mine if r["role"] == "base") + chain, cur = [], base + while True: + nxt = next((r for r in mine if r["role"] == "refinement" and r.get("refines") == cur["id"]), None) + if nxt is None: + break + chain.append(nxt) + cur = nxt + return base, chain, [r for r in mine if r["role"] == "floor"] + + +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"), "") + + def class_plan(P, ix): """§8.1: each piece's moments and matching rules as bits, and per class the highest layer it lists for each piece, 0xFF for the floor only.""" num, den = ix["tb"] bits, rows = [], [[] for _ in P["class"]] for k, pc in enumerate(ix["pieces"]): - s_ms, e_ms = pc["start"] * den * 1000 // num, (pc["start"] + pc["duration"]) * den * 1000 // num - moments = [i for i, m in enumerate(P["moment"]) if s_ms < m["to_ms"] and e_ms > m["from_ms"]] - tags = {t for i in moments for t in P["moment"][i]["tags"]} - rules, then = [], {} - for i, r in enumerate(P["rule"]): - w = r["when"] - if "piece_kind" in w and PIECE_KINDS[pc["kind"]] not in w["piece_kind"]: - continue - if "moment_tag" in w and not tags & set(w["moment_tag"]): - continue - rules.append(i) - then.update(r["then"]) - bits.append((sum(1 << i for i in rules), sum(1 << i for i in moments))) + rb, mb, then = piece_then(P, ix, k) + bits.append((rb, mb)) for row, cl in zip(rows, P["class"]): if cl["floor_only"]: row.append(0xFF) @@ -2174,26 +2561,37 @@ def listed(ix, k, top): def codecs_value(objects): - """http-delivery §4.2 over a sequence of WAI1 objects (entries): the union, an alt per - distinct capability of the objects that are not layers, a layer's capability after `+` - in its base's alt; and the floor, the alts every such object has.""" - alts, joined = [], {} - for e in objects: - if e["role"] != REFINEMENT and e["cap"] not in alts: - alts.append(e["cap"]) - bases = [e for e in objects if e["role"] != REFINEMENT] + """http-delivery §4.2 over a sequence of WAI1 objects (entries, in order; an object + listed twice is one object): each object that is not a layer is an element of one alt, + its capability; a layer's capability follows `+` in the alt of the object its base is. + The union: every alt by first appearance, with every capability joined to it in any + object; the floor: the alts every such object has, each with the capabilities joined + to it in every one.""" + seen, seq = set(), [] for e in objects: + if e["hash"] not in seen: + seen.add(e["hash"]) + seq.append(e) + els = [[e["cap"], []] if e["role"] != REFINEMENT else None for e in seq] + for e in seq: if e["role"] == REFINEMENT: - base_cap = e.get("base_cap") - joined.setdefault(base_cap, []) - if e["cap"] not in joined[base_cap]: - joined[base_cap].append(e["cap"]) + k = next(k for k, b in enumerate(seq) if b["role"] != REFINEMENT and b["hash"] == e["base_hash"]) + if e["cap"] not in els[k][1]: + els[k][1].append(e["cap"]) + els = [x for x in els if x is not None] + union = [] + for cap, plus in els: + u = next((a for a in union if a[0] == cap), None) + if u is None: + union.append([cap, list(plus)]) + else: + u[1] += [p for p in plus if p not in u[1]] + floor = [[cap, [p for p in plus if all(p in x[1] for x in els if x[0] == cap)]] for cap, plus in union + if all(x[0] == cap for x in els)] - def alt(c): - return "+".join(x[len("wai."):] for x in [c] + joined.get(c, [])) - union = "wai1." + "|".join(alt(c) for c in alts) - floor = "wai1." + "|".join(alt(c) for c in alts if all(b["cap"] == c for b in bases)) - return union, floor + def alt(cap, plus): + return "+".join(x[len("wai."):] for x in [cap] + plus) + 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): @@ -2245,13 +2643,15 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): lines += [f' ' for name, _ in frags] out["dash/classic.mpd"] = text(lines + [" ", " "] + mpd_tail) E = ix["entries"] - by_hash = {} - for e in E: - by_hash.setdefault(e["hash"], e) + # §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] 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))] - wai = any(lists) + http = [lists[k] if 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"') @@ -2269,7 +2669,7 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): v["stages"].append(s) views.append(v) comp = {"pieces": views} - if any(lists[k] for k in range(len(pieces))): + if wai: comp["media_playlist"] = f"../hls/{name}/wai-{c}.m3u8" view = {"class": name, "components": {c: comp}, "policy_digest": policy_hex} out[f"stages/{name}.json"] = (jcs(view) + "\n").encode() @@ -2277,12 +2677,11 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): continue objs = [] for k in range(len(pieces)): - base = next((e for e in lists[k] if e["role"] == BASE), None) - for e in lists[k]: - objs.append(dict(e, base_cap=base and base["cap"])) + for e in http[k]: + objs.append(dict(e, base_hash=bases[k]["hash"])) union, fl = codecs_value(objs) - wb, wa = rates([(sum(e["byte_len"] for e in lists[k]), pieces[k]["duration"]) - for k in range(len(pieces)) if lists[k]]) + wb, wa = rates([(sum(e["byte_len"] for e in http[k]), pieces[k]["duration"]) + for k in range(len(pieces)) if http[k]]) out[f"hls/{name}/wai.m3u8"] = text(head + [ f'#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="{name}"', f'#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/{name}.json"', @@ -2292,13 +2691,13 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): for k, pc in enumerate(pieces): p0 = pc["first_entry"] base = next(e for e in E[p0:p0 + pc["n_entries"]] if e["role"] == BASE) - if row[k] == 0xFF: + if not http[k]: lines.append("#EXT-X-GAP") # A layer line follows its segment's EXTINF, before the segment's URI # (http-delivery §10.4). lines.append(f"#EXTINF:{ms_text(pc['duration'], num, den)},") lines += [f'#WAI-LAYER:N={e["layer"]},URI="../../objects/{e["hash"].hex()}.wai#n={e["byte_len"]}"' - for e in lists[k] if e["role"] == REFINEMENT] + for e in http[k] if e["role"] == REFINEMENT] lines.append(f"../../objects/{base['hash'].hex()}.wai#n={base['byte_len']}") out[f"hls/{name}/wai-{c}.m3u8"] = text(lines + ["#EXT-X-ENDLIST"]) lines = mpd_head + [ @@ -2309,9 +2708,9 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): f' ', ' '] - for layer in range(max(v for v in row if v != 0xFF) + 1): - ks = [k for k in range(len(pieces)) if any(e["layer"] == layer for e in lists[k])] - mine = [[e for e in lists[k] if e["layer"] == layer] for k in ks] + for layer in range(max(row[k] for k in range(len(pieces)) if http[k]) + 1): + ks = [k for k in range(len(pieces)) if any(e["layer"] == layer for e in http[k])] + mine = [[e for e in http[k] if e["layer"] == layer] for k in ks] peak = max((bps(sum(e["byte_len"] for e in m), pieces[k]["duration"], num, den) for k, m in zip(ks, mine)), default=0) dep = f' dependencyId="{c}-l{layer - 1}"' if layer else "" @@ -2322,15 +2721,24 @@ 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) + (F, FW, FH) if has_floor else None, P["asset_id"]) return out -def render_moq(c, E, lag, default_deadline, floor): - """§8.4: moq/catalog.json. One track per (layer, deadline), by layer, then by deadline - ascending; a refinement track depends on every track that carries an entry its entries - refine, in track order, so it never names a track the catalog does not hold. floor: - (codecs, width, height) or None.""" +def receipt_alias(ns, name): + """§8.4: a track's wai_receipt_alias, the first eight bytes of + BLAKE3("wai:pack-receipt-alias\x01" ‖ namespace ‖ 0x00 ‖ name), little-endian, below 2^53.""" + 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): + """§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).""" keys = sorted({(e["layer"], e["deadline"]) for e in E if e["role"] in (BASE, REFINEMENT)}) def name(key): @@ -2343,27 +2751,40 @@ def render_moq(c, E, lag, default_deadline, floor): for e in es: if e["cap"] not in caps: caps.append(e["cap"]) - t = {"name": name((layer, deadline)), "packaging": "wai", "renderGroup": 1, - "priority": 64 + 32 * layer, "wai_capabilities": caps, "wai_layer": layer, "wai_receipts": False} - if min(e["tier"] for e in es): - t["wai_determinism"] = TIER_NAMES[min(e["tier"] for e in es)] - if layer == 0 and floor is not None: - t["altGroup"] = 1 - if layer: - deps = set() - for e in es: - below = E[e["refines"]] - dt = keys.index((below["layer"], below["deadline"])) + deps = set() + for e in es: + cur = e + while cur["role"] == REFINEMENT: + cur = E[cur["refines"]] + dt = keys.index((cur["layer"], cur["deadline"])) if dt >= ti: raise ValueError("a refinement refines an entry on no earlier track") deps.add(dt) + nm = name((layer, deadline)) + t = {"namespace": ns, "name": nm, "packaging": "wai", "isLive": False, "role": "video", "renderGroup": 1, + "wai_capabilities": caps, "wai_receipts": True, "wai_receipt_alias": receipt_alias(ns, nm), + "wai_receipts_track": f"{nm}/receipts", "wai_staged": {"set": c, "layer": layer, "lead_ms": None}} + if deps: t["depends"] = [name(keys[x]) for x in sorted(deps)] + weakest = min(e["tier"] for e in es) + if weakest: + 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: + 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", "packaging": "cmaf", - "renderGroup": 1, "width": floor[1]}) - return (jcs({"tracks": tracks, "version": 1}) + "\n").encode() + tracks.append({"altGroup": 1, "codec": floor[0], "height": floor[2], "name": f"{c}/floor", "namespace": ns, + "packaging": "cmaf", "renderGroup": 1, "width": floor[1]}) + 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") + cat = {"tracks": tracks, "version": 1} + bad = [s for s in SV.check_catalog(cat, ns) if s["reason"] is not None] + if bad: + raise ValueError(f"the stage set fails moq-streaming-format §4.1: {bad}") + return (jcs(cat) + "\n").encode() # ---------------------------------------------------------------- the corpus @@ -2428,6 +2849,36 @@ def seg_params(P, tb): "window": s["cut_window"], "lookahead": s["lookahead_frames"]} +class Held: + """A still's one frame held for `n` frames (§5.7): its length is `n` and it iterates + the frame `n` times, holding neither `n` copies nor `n` references.""" + def __init__(self, frame, n): + self.frame, self.n = frame, n + + def __len__(self): + return self.n + + def __iter__(self): + return itertools.repeat(self.frame, 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).""" + h = hashlib.sha256() + if isinstance(frames, Held): + d = hashlib.sha256(frames.frame).digest() + block, left = d * 1024, frames.n + while left >= 1024: + h.update(block) + left -= 1024 + h.update(d * left) + return h.digest() + for f in frames: + h.update(hashlib.sha256(f).digest()) + return h.digest() + + def clip_digest(frames): """SPEC §7, *Digests*: the SHA-256 of the frames' SHA-256 digests concatenated.""" return hashlib.sha256(b"".join(hashlib.sha256(f).digest() for f in frames)).digest() @@ -2583,16 +3034,16 @@ def package_verdict(d, name, pkg): return m.group(1) if m else f"no reason named: {cap.failed[0]}" -def verify_package_mutations(rep, d): +def verify_package_mutations(rep, d, sub="package-mutations"): """package-mutations/: each case the golden less the files it removes (and the directories that leaves with nothing in them), with its overlay and the directories it adds that hold nothing (`empty_dirs`) or cannot be read (`unreadable_dirs`, mode 0 where the platform has modes), written out and verified as a package (§11.3); refused with the corpus's reason, or accepted where it says `ok`.""" - m = os.path.join(d, "package-mutations") + m = os.path.join(d, sub) held = tree(m) if held is None: - rep.check(False, "package-mutations/: a symbolic link or an entry that is neither file nor directory") + rep.check(False, f"{sub}/: a symbolic link or an entry that is neither file nor directory") return pm = json.loads(load(os.path.join(m, "expected.json"))) golden = os.path.join(d, "packs", pm["pack"], "package") @@ -2619,9 +3070,9 @@ def verify_package_mutations(rep, d): got = package_verdict(d, pm["pack"], pkg) for p in c.get("unreadable_dirs", []): os.chmod(os.path.join(pkg, p), 0o755) - rep.check(got == c["reason"], f"package-mutations {c['name']}: {got}, the corpus says {c['reason']}") + rep.check(got == c["reason"], f"{sub} {c['name']}: {got}, the corpus says {c['reason']}") rest = [p for p in held if p not in used] - rep.check(not rest, f"package-mutations/: files no case uses: {rest}") + rep.check(not rest, f"{sub}/: files no case uses: {rest}") def unreached(files, c): @@ -2647,17 +3098,30 @@ def index_against_policy(P, comp, c, ix): want = [(x["id"], min(x["max_layer"], 255), int(x["floor_only"]), min(x.get("max_kbps", 0), U32)) for x in P["class"]] if [(x["name"], x["max_layer"], x["flags"], x["max_kbps"]) for x in ix["classes"]] != want: return "the class records" - lay = layers(P) - for i, e in enumerate(ix["entries"]): - role = ROLES[e["role"]] - if not any(r["component"] == c["id"] and r["role"] == role and r["capability"] == e["cap"] and lay[r["id"]] == e["layer"] - for r in P["ladder"]): - return f"entry {i}: no rung of the component" - if any(pc["kind"] >= len(KINDS) for pc in ix["pieces"]): - return "a piece kind a video piece does not have" - bits, rows = class_plan(P, ix) - if bits != [(pc["rules"], pc["moments"]) for pc in ix["pieces"]]: - return "the rules and moments" + # Each piece: its kind, its rules and moments, its entries the rungs of its ladder + # (§5.7) with the plan's deadline on each refinement (§8.6), then the class plan. + E = ix["entries"] + # §4.4: no piece lasts longer than max_ms; first, so nothing is decoded for one that does. + mx = units_from_ms(P["segmentation"]["max_ms"], *ix["tb"]) + 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" + for k, pc in enumerate(ix["pieces"]): + if pc["kind"] >= len(KINDS): + return f"piece {k}: a kind a video piece 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] + 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" + deadline = then.get("deadline_ms", P["defaults"]["deadline_ms"]) + if any(e["role"] == REFINEMENT and e["deadline"] != deadline for e in mine): + return f"piece {k}: a refinement's deadline is not its plan's" + _, rows = class_plan(P, ix) if rows != ix["plan"]: return "the class plan" return None @@ -2706,33 +3170,65 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix, inputs=True): rep.check(False, f"{t}: entry {i}: the object rules (§6.3) ({x.code})") if len(mans) != len(E): return named | unreached(files, c['id']) + # Before any frame is decoded (§11.3): each piece's floor object, where the policy has a + # floor rung and the payload reads as §7.2's framing, holds one unit per frame. + 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']) size = None + own = owners(ix) + stage = {} # entry index → the frames of the prefix it completes, decoded for i, e in enumerate(E): - if e["role"] != BASE: - continue ml = e["man_len"] - try: - w = IPV.parse_wiv1(objs[i][12 + ml:], 0) - if e["hash"] not in decoded: - decoded[e["hash"]] = (w, IPV.decode_wiv1_k0(w)) - except IPV.Refused as x: - rep.check(False, f"{t}: entry {i}: the WIV1 payload is refused ({x})") - return named | unreached(files, c['id']) - size = size or (w["w"], w["h"]) - # §5.2: the reference packager's base manifest, as SPEC §3 orders it. - want = {"wai": "1.0", "media": comp["media"], "intent": "replicate", - "model_requirement": {"capability": e["cap"]}, "conditioning": {"kind": e["cap"].split(".")[-1]}, - "target": {"frames": pieces[owners(ix)[i]]["duration"], "h": w["h"], "w": w["w"]}} - rep.check(objs[i][8:8 + ml] == json.dumps(want, separators=(",", ":")).encode(), - f"{t}: entry {i}: the base's manifest is §5.2's") + payload = objs[i][12 + ml:] + k = own[i] + n = pieces[k]["duration"] + if 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 + # count, not a list of n frames. + pw, ph, rgb = png_frame(payload) + wv, frames = None, Held(rgb, n) + else: + wv = IPV.parse_wiv1(payload, 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: + rep.check(False, f"{t}: entry {i}: the base does not decode ({x}) (prefix-digest)") + return named | unreached(files, c['id']) + decoded.setdefault(e["hash"], (wv, frames)) + stage[i] = frames + 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", + "model_requirement": {"capability": e["cap"]}, "conditioning": {"kind": e["cap"].split(".")[-1]}, + "target": {"frames": n, "h": ph, "w": pw}} + rep.check(objs[i][8:8 + ml] == json.dumps(want, separators=(",", ":")).encode(), + f"{t}: entry {i}: the base's manifest is §5.2's (object-manifest)") + elif e["role"] == REFINEMENT: + base = E[pieces[k]["first_entry"]] + # §5.4: the refinement's manifest, as `wai stage refine` writes it. + want = {"wai": "1.0", "media": comp["media"], "intent": "replicate", + "model_requirement": {"capability": e["cap"]}, "conditioning": {"kind": "wir1"}, + "refines": {"base": base["hash"].hex(), "prev": E[e["refines"]]["hash"].hex(), "layer": e["layer"]}, + "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)") + try: + stage[i] = wir1_apply(payload, stage[e["refines"]]) + except (SV.Refused, KeyError, IndexError, struct.error) as x: + rep.check(False, f"{t}: entry {i}: the layer does not apply to the stage below it ({x}) (prefix-digest)") + return named | unreached(files, c['id']) for i, e in enumerate(E): - if e["tier"] != 2: - continue - if e["role"] != BASE: - rep.check(False, f"{t}: entry {i}: a refinement's prefix, which this verifier does not decode") - continue - rep.check(clip_digest(decoded[e["hash"]][1]) == e["prefix"], - f"{t}: entry {i}: the prefix decodes to its prefix_digest (prefix-digest)") + if e["tier"] == 2: + 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) @@ -2812,14 +3308,55 @@ 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)}) if inputs: - verify_sources(rep, t, root, P, comp, ix, decoded, units_of) + verify_sources(rep, t, root, P, comp, ix, stage, units_of, objs) return named -def verify_sources(rep, t, root, P, comp, ix, decoded, units_of): +def http_views(rep, t, c, 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 + (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() + for path in sorted(cats): + m = re.fullmatch(r"hls/([^/]+)/wai-" + re.escape(c) + r"\.m3u8", path) + if not m: + continue + cls, text = m.group(1), cats[path].decode() + try: + HV.check_adjacent(text, floor_text) + adjacent = True + except HV.Refusal: + adjacent = False + rep.check(adjacent, f"{t}: {path} is adjacent to the floor's playlist (http-delivery §11.2)") + try: + _, _, every = HV.listed_objects(objmap, HV.parse_media_playlist(text)) + union, floor = HV.union_floor_of(every) + got = (HV.render(union), HV.render(floor)) + except (HV.Refusal, SystemExit) as x: + got = (f"refused {x}", "") + master = cats[f"hls/{cls}/wai.m3u8"].decode() + want = (re.search(r'[,:]CODECS="([^"]+)"', master).group(1), re.search(r'X-WAI-FLOOR="([^"]+)"', master).group(1)) + rep.check(got == want, f"{t}: hls/{cls}/wai.m3u8's CODECS and X-WAI-FLOOR are http-delivery §4.2's over what it lists: {got}, {want}") + mpd = cats.get(f"dash/{cls}.mpd") + if mpd is not None: + try: + HV.check_dash_adjacent(mpd.decode(), cats["dash/classic.mpd"].decode()) + adjacent = True + except HV.Refusal: + adjacent = False + rep.check(adjacent, f"{t}: dash/{cls}.mpd is adjacent to the classical MPD (http-delivery §11.2)") + + +def verify_sources(rep, t, root, P, comp, ix, stage, units_of, objs): """What only the source decides: the pieces (§4.3, §4.6) and their rules, moments and - class plan (§8.1), each base's sse (§6.6) and step (§5.6), and the supplied floor (§7.3).""" + class plan (§8.1); each piece's ladder (§5.7) re-derived from the source: an + `int_motion` base's step (§5.6) and choices (§5.5), a still's bytes (§5.7), each + 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"] source = comp.get("source", "") name = source[:-len(".y4m")] @@ -2836,35 +3373,56 @@ def verify_sources(rep, t, root, P, comp, ix, decoded, units_of): rep.check(bits == [(pc["rules"], pc["moments"]) for pc in ix["pieces"]], f"{t}: every piece's rules and moments (§8.1)") rep.check(rows == ix["plan"], f"{t}: the class plan is §8.1's") - rung = next(r for r in P["ladder"] if r["component"] == comp["id"] and r["role"] == "base") - p = rung["params"] - own, cache, chosen = owners(ix), {}, set() - for i, e in enumerate(ix["entries"]): - if e["role"] != BASE: - continue - pc = ix["pieces"][own[i]] - src = frames[pc["start"]:pc["start"] + pc["duration"]] - wv, dec = decoded[e["hash"]] - sse = sum((a - b) ** 2 for s, f in zip(src, dec) for a, b in zip(s, f)) - rep.check(len(dec) == len(src) and sse == e["sse"], f"{t}: entry {i}: sse against the source (§6.6)") - n = len(src) - rep.check((wv["block"], wv["mv_precision"]) == ((p["block"], p["mv_precision"]) if n > 1 else (1, 1)), - f"{t}: entry {i}: the payload's block and precision are the rung's (§5.5)") - key = hashlib.sha256(b"".join(src)).digest() + E = ix["entries"] + cache, chosen = {}, set() - def sse_of(q, key=key, src=src): - if (key, q) not in cache: - cache[key, q] = closed_loop(src, h, w, p["block"], p["search"], p["mv_precision"], q) - return sse_px_milli(cache[key, q][1], h, w, len(src)) - q = choose_q(sse_of, p["q_candidates"], p["max_sse_px_milli"]) - rep.check(wv["q"] == q, f"{t}: entry {i}: the step is §5.6's choice ({wv['q']}, the rule {q})") - rep.check([bytes(f) for f in cache[key, q][0]] == dec, - f"{t}: entry {i}: the payload decodes to the encoder's reconstruction at that step (§5.5)") - if (e["hash"], key) not in chosen: - chosen.add((e["hash"], key)) - bad = encoder_choices(wv, p, src, dec) - rep.check(not bad, f"{t}: entry {i}: every vector, coded residual and the table are the encoder's (§5.5)" - + (f": {bad[0]}" if bad else "")) + def load_object_payload(ix, i): + return objs[i][12 + E[i]["man_len"]:] + for k, pc in enumerate(ix["pieces"]): + _, _, then = piece_then(P, ix, k) + base_rung, chain, _ = piece_ladder(P, comp["id"], then) + src = frames[pc["start"]:pc["start"] + pc["duration"]] + n = len(src) + i = pc["first_entry"] + e = E[i] + recon = stage[i] + rep.check(sum((a - b) ** 2 for s, f in zip(src, recon) for a, b in zip(s, f)) == e["sse"] and len(recon) == n, + f"{t}: entry {i}: sse against the source (§6.6)") + if base_rung["capability"] == "wai.image.png": + # §5.7: the still is the piece's first frame, written as the module states. + m = load_object_payload(ix, i) + rep.check(m == png_still(src[0], w, h), f"{t}: entry {i}: the still is the piece's first frame, §5.7's bytes") + else: + p = base_rung["params"] + wv = IPV.parse_wiv1(load_object_payload(ix, i), 0) + rep.check((wv["block"], wv["mv_precision"]) == ((p["block"], p["mv_precision"]) if n > 1 else (1, 1)), + f"{t}: entry {i}: the payload's block and precision are the rung's (§5.5)") + key = hashlib.sha256(b"".join(src)).digest() + + def sse_of(q, key=key, src=src, p=p): + if (key, q, p["block"], p["search"], p["mv_precision"]) not in cache: + cache[key, q, p["block"], p["search"], p["mv_precision"]] = closed_loop(src, h, w, p["block"], p["search"], p["mv_precision"], q) + return sse_px_milli(cache[key, q, p["block"], p["search"], p["mv_precision"]][1], h, w, len(src)) + q = choose_q(sse_of, p["q_candidates"], p["max_sse_px_milli"]) + rep.check(wv["q"] == q, f"{t}: entry {i}: the step is §5.6's choice ({wv['q']}, the rule {q})") + rep.check([bytes(f) for f in cache[key, q, p["block"], p["search"], p["mv_precision"]][0]] == recon, + f"{t}: entry {i}: the payload decodes to the encoder's reconstruction at that step (§5.5)") + if (e["hash"], key) not in chosen: + chosen.add((e["hash"], key)) + bad = encoder_choices(wv, p, src, recon) + rep.check(not bad, f"{t}: entry {i}: every vector, coded residual and the table are the encoder's (§5.5)" + + (f": {bad[0]}" if bad else "")) + # §5.4: each refinement, written again from the source and the stage below. + below = recon + for j, rung in enumerate(chain): + i = pc["first_entry"] + 1 + j + e = E[i] + mine, nxt = wir1_layer(below, src, h, w, rung["params"]["q"]) + rep.check(load_object_payload(ix, i) == mine, f"{t}: entry {i}: the layer is §5.4's bytes from the source") + rep.check(nxt == stage[i], f"{t}: entry {i}: it reconstructs what the package decodes") + rep.check(sum((a - b) ** 2 for s, f in zip(src, nxt) for a, b in zip(s, f)) == e["sse"], + 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. sup = os.path.join(root, "floor") @@ -2953,6 +3511,68 @@ def verify_v1_receipt(rep, name, b, want_hash, refused=False): f"{name}: the signature " + ("is refused" if refused else "verifies")) +LIVE_PIECE_FIELDS = ("start", "duration", "kind", "flags", "rules", "moments") +# An entry's fields a window shares with the on-demand index; its flags (a duplicate is +# one within its window) and its refines (a window-relative index) are the window's own. +LIVE_ENTRY_FIELDS = ("hash", "byte_len", "piece", "cap", "role", "layer", "tier", "deadline", "prefix", "sse", + "man_len", "payload_len") + + +def verify_live(rep, d): + lv = json.loads(load(os.path.join(d, "live", "expected.json"))) + gpkg = os.path.join(d, "packs", lv["pack"], "package") + comp = lv["component"] + gix = parse_index(load(os.path.join(gpkg, f"index-{comp}.wsix"))) + gkey = read_receipt(load(os.path.join(gpkg, "package.receipt.json")))["json"]["signer_pubkey"] + names, prev, covered = ["expected.json"], None, 0 + for w in lv["windows"]: + n, tag = w["window"], f"live window {w['window']}" + ipath, rpath = f"windows/{n}/index-{comp}.wsix", f"windows/{n}/package.receipt.json" + names += [ipath, rpath] + ib, rb = load(os.path.join(d, "live", ipath)), load(os.path.join(d, "live", rpath)) + try: + r = read_receipt(rb) + [ix] = verify_receipt_v2(r, [ib]) + except Refused as x: + rep.check(False, f"{tag}: refused {x.code}") + return + j = r["json"] + rep.check(j["receipt_hash"] == w["receipt_hash"] and index_digest(ib).hex() == w["index_digest"], + f"{tag}: its receipt hash and index digest are the corpus's") + rep.check(j["signer_pubkey"] == gkey, f"{tag}: signed by the golden's key") + rep.check((ix["first_piece"], ix["first_entry"], len(ix["pieces"])) == (w["first_piece"], w["first_entry"], w["pieces"]), + f"{tag}: its first piece, first entry and pieces are the corpus's") + if prev is None: + rep.check(j["parent_kind"] == "none" and not any(ix["prev"]) and ix["first_piece"] == 0 and ix["first_entry"] == 0, + f"{tag}: the first window starts the chain") + else: + pj, pix, pib = prev + rep.check(j["parent_kind"] == "live-window" and j["parent_receipt_hash"] == pj["receipt_hash"], + f"{tag}: its parent is the window before's receipt (live-window)") + rep.check(ix["prev"] == index_digest(pib), f"{tag}: prev_index_digest is the window before's index digest") + rep.check(ix["first_piece"] == pix["first_piece"] + len(pix["pieces"]) + and ix["first_entry"] == pix["first_entry"] + len(pix["entries"]), + f"{tag}: first_piece and first_entry continue the chain") + last = pix["pieces"][-1] + rep.check(ix["pieces"][0]["start"] == last["start"] + last["duration"], f"{tag}: its first piece starts where the window before ends") + a = ix["first_piece"] + rep.check(len(ix["pieces"]) == lv["window_pieces"] or w is lv["windows"][-1], f"{tag}: {lv['window_pieces']} pieces, or the last window") + for k, pc in enumerate(ix["pieces"]): + gpc = gix["pieces"][a + k] if a + k < len(gix["pieces"]) else None + rep.check(gpc is not None and all(pc[f] == gpc[f] for f in LIVE_PIECE_FIELDS), f"{tag}: piece {a + k} is the golden's") + if gpc is None: + continue + got = [tuple(e[f] for f in LIVE_ENTRY_FIELDS) for e in ix["entries"][pc["first_entry"]:pc["first_entry"] + pc["n_entries"]]] + want = [tuple(e[f] for f in LIVE_ENTRY_FIELDS) for e in gix["entries"][gpc["first_entry"]:gpc["first_entry"] + gpc["n_entries"]] + if e["role"] != FLOOR] + rep.check(got == want, f"{tag}: piece {a + k}'s bases and refinements are the golden's, entry for entry") + rep.check(ix["plan"] == [row[a:a + len(ix["pieces"])] for row in gix["plan"]], f"{tag}: its class plan is the golden's for its pieces") + rep.check(ix["plan_digest"].hex() == plan_digest([pc["start"] for pc in ix["pieces"]]), f"{tag}: its plan_digest is its starts' (§4.2)") + prev, covered = (j, ix, ib), covered + len(ix["pieces"]) + rep.check(covered == len(gix["pieces"]), "live/: the windows hold every piece of the golden") + rep.check(tree(os.path.join(d, "live")) == sorted(names), "live/: every file is a window's") + + def verify(d): rep = Report() pol = json.loads(load(os.path.join(d, "policy", "expected.json"))) @@ -3095,20 +3715,64 @@ def verify(d): rep.check(tree(os.path.join(d, "receipts")) == sorted([c["file"] for c in rc["cases"]] + ["expected.json"]), "receipts/: every receipt is a case") + # stills/: PNG files a still's reader reads or refuses (§5.7), each read by this + # verifier's own reader. + st = json.loads(load(os.path.join(d, "stills", "expected.json"))) + for c in st["cases"]: + try: + w, h, px = png_frame(load(os.path.join(d, "stills", c["file"]))) + got = ("ok", w, h, hashlib.sha256(px).hexdigest()) + except ValueError as x: + got = ("refused", str(x)) + want = ("ok", st["width"], st["height"], c.get("rgb_sha256")) if c["verdict"] == "ok" else ("refused",) + rep.check(got[:len(want)] == want, f"stills/{c['file']}: {got[0]}, the corpus says {c['verdict']}") + rep.check(tree(os.path.join(d, "stills")) == sorted([c["file"] for c in st["cases"]] + ["expected.json"]), + "stills/: every file is a case") + + # live/: the staged golden's source packed live in windows (§9.2). Each window is read + # as an index under §6.3's rules with the receipt that seals it (§9.1, signature by + # WAI's strict rule), chained to the window before by its parent receipt, + # prev_index_digest, first_piece and first_entry, signed by the golden's key, and + # holding the golden's pieces with their bases and refinements, entry for entry. + verify_live(rep, d) + # catalogs/: MoQ catalogs of indexes with refinements on more than one deadline (§8.4). cc = json.loads(load(os.path.join(d, "catalogs", "expected.json"))) for c in cc["cases"]: 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"])) + got = render_moq(ix["component"], ix["entries"], c["layer_lag_ms"], c["default_deadline"], (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", [])), f"{c['index']}: no dependency names a track the catalog does not hold") rep.check(tree(os.path.join(d, "catalogs")) == sorted([c["index"][len("catalogs/"):] for c in cc["cases"]] + ["expected.json"]), "catalogs/: every index is a case") + # delivery/: the staged golden packed live and its delivery simulated (§8.9), each case + # re-derived from the fixture's cut and the package's index by this verifier's own + # mapping onto sim_verify.py's simulator. + dl = json.loads(load(os.path.join(d, "delivery", "expected.json"))) + pkg = os.path.join(d, "packs", dl["pack"], "package") + P = parse_policy_bytes(load(os.path.join(pkg, "policy.toml"))) + comp = P["component"][0] + ix = parse_index(load(os.path.join(pkg, f"index-{comp['id']}.wsix"))) + n_frames = len(read_y4m(fixture(comp["source"][:-len(".y4m")]))[4]) + pub = published_of(P, ix, n_frames) + ci = [c["id"] for c in P["class"]].index(dl["class"]) + for c in dl["cases"]: + got = deliver(P, ix["plan"][ci], pub, c["plan"]) + rep.check(got == c["outcome"], f"delivery {c['name']}: the outcome sim_verify.py's simulator gives") + rep.check([0xFF if x["presented"] is None else x["presented"] for x in got["pieces"]] == c["presented"], + f"delivery {c['name']}: the presented vector") + if not c["plan"].get("fifo"): + rep.check(all(x["base_arrival_us"] is not None and x["base_arrival_us"] <= x["due_us"] for x in got["pieces"]), + f"delivery {c['name']}: under §8.5's priorities every base arrives by its piece's instant") + rep.check(tree(os.path.join(d, "delivery")) == ["expected.json"], "delivery/: no file that is no case") 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") return rep diff --git a/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/LISTING b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/LISTING index 01bf4d047..9c1b5f81a 100644 --- a/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/LISTING +++ b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/LISTING @@ -16,7 +16,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 13b7a7cf473134c1a9dfb0a380c4464bcd5e3a7f2e007efe65f32fd9422175c0 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/class-named-classic/LISTING b/wai/pack-conformance/package-mutations/class-named-classic/LISTING index 6265087ff..fc2080697 100644 --- a/wai/pack-conformance/package-mutations/class-named-classic/LISTING +++ b/wai/pack-conformance/package-mutations/class-named-classic/LISTING @@ -16,7 +16,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 44692fd01ae15a4d7edaa1813241e55e8b2367180dc700b0c780d2d4014a4f95 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/component-medium/LISTING b/wai/pack-conformance/package-mutations/component-medium/LISTING index 9c51bd196..ddb71b31f 100644 --- a/wai/pack-conformance/package-mutations/component-medium/LISTING +++ b/wai/pack-conformance/package-mutations/component-medium/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 1cfc2a0d1b54f1a6ec47ebec95de9bc1cba3a9df40dbc3868d684ffa1f33db8d index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/floor-cmaf/LISTING b/wai/pack-conformance/package-mutations/floor-cmaf/LISTING index 319a49f2a..5423767da 100644 --- a/wai/pack-conformance/package-mutations/floor-cmaf/LISTING +++ b/wai/pack-conformance/package-mutations/floor-cmaf/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/LISTING b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/LISTING index c2d314047..288bfc1c7 100644 --- a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/LISTING +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/LISTING @@ -17,7 +17,7 @@ c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 b1ebb904e14be6aa830e400b4d732bc46ac47986f5b20202be0a261ace2369f0 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame/LISTING b/wai/pack-conformance/package-mutations/floor-extra-key-frame/LISTING index 64a0b6e16..ca27450fc 100644 --- a/wai/pack-conformance/package-mutations/floor-extra-key-frame/LISTING +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame/LISTING @@ -17,7 +17,7 @@ c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 216c7a0baa5505fb92e3fe5cd2101962f220c6b188fe4ec3a5f655703373738a index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/floor-framing-header/LISTING b/wai/pack-conformance/package-mutations/floor-framing-header/LISTING index 931e0e34a..76c6338dd 100644 --- a/wai/pack-conformance/package-mutations/floor-framing-header/LISTING +++ b/wai/pack-conformance/package-mutations/floor-framing-header/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 e1ff9d513a5576dd32236a21b4ad00d1e97b3d9247c8a0b05f44ff2ee3080594 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai 4dacc52e68b4fd119cb6da1387961aeabaa7dda4ed75ea9df881e92bfb2552d3 objects/504d5519c3690a705d7e2cb4089ef6bf474311b8809bfe1a7c6a3f00628e8b4c.wai diff --git a/wai/pack-conformance/package-mutations/floor-motif/LISTING b/wai/pack-conformance/package-mutations/floor-motif/LISTING index ec84d7965..6711c2407 100644 --- a/wai/pack-conformance/package-mutations/floor-motif/LISTING +++ b/wai/pack-conformance/package-mutations/floor-motif/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 44b744229777bd582f45bf670e2dd873798cb21c205f55efaea164acb3a6128f index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai 3ba33185bdcba397875e354354a5f54b06b561da6fa98f98584458752eddd41e objects/455892cdcffca9b861b34ac980ef50e2cfbd4062e81ed7029da687026ef4bd11.wai diff --git a/wai/pack-conformance/package-mutations/floor-object-corrupt/LISTING b/wai/pack-conformance/package-mutations/floor-object-corrupt/LISTING index 68d854de0..4ba7b1276 100644 --- a/wai/pack-conformance/package-mutations/floor-object-corrupt/LISTING +++ b/wai/pack-conformance/package-mutations/floor-object-corrupt/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/floor-object-missing/LISTING b/wai/pack-conformance/package-mutations/floor-object-missing/LISTING index bf9412dae..17eb2c80d 100644 --- a/wai/pack-conformance/package-mutations/floor-object-missing/LISTING +++ b/wai/pack-conformance/package-mutations/floor-object-missing/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/LISTING b/wai/pack-conformance/package-mutations/floor-opens-on-inter/LISTING index 17d460995..bfa8ccea5 100644 --- a/wai/pack-conformance/package-mutations/floor-opens-on-inter/LISTING +++ b/wai/pack-conformance/package-mutations/floor-opens-on-inter/LISTING @@ -17,7 +17,7 @@ fa991c3f0242f452b02f5c70b72e10e864db388a40b654bfe64dcca8423b077e hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 f2f570cba26269e220795992dd224078b6c05473940ab1f6a900f2c8d1b220b1 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/floor-renderer-settings/LISTING b/wai/pack-conformance/package-mutations/floor-renderer-settings/LISTING index 8a5621993..3eeb438f0 100644 --- a/wai/pack-conformance/package-mutations/floor-renderer-settings/LISTING +++ b/wai/pack-conformance/package-mutations/floor-renderer-settings/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 ed10f43afaa754358e36b19a8fe5a9b614a272d5741bfaebe5fb24ea2a457086 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/floor-sequence-header/LISTING b/wai/pack-conformance/package-mutations/floor-sequence-header/LISTING index 648e939f1..08fcb6588 100644 --- a/wai/pack-conformance/package-mutations/floor-sequence-header/LISTING +++ b/wai/pack-conformance/package-mutations/floor-sequence-header/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 752666c9b5af4bcf8d7eb7a09e43b368069828d2b9848dd882660e64025c74a2 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 59c59bd437e2dac861a3b442f36b2387faa5942315c2b9fc0ffadd4163431973 objects/092d88d17b4fd8de5ed3d820c618dccba83a80a0b2d863a797ca9afb4c9ba259.wai 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai diff --git a/wai/pack-conformance/package-mutations/floor-target/LISTING b/wai/pack-conformance/package-mutations/floor-target/LISTING index 09f31dbac..b50d665d0 100644 --- a/wai/pack-conformance/package-mutations/floor-target/LISTING +++ b/wai/pack-conformance/package-mutations/floor-target/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 11eee7726f33e9ca6e1d16eddb0a01bdd4cff18cfe6caa625585f19429173bbf index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/index-capability/LISTING b/wai/pack-conformance/package-mutations/index-capability/LISTING index b6a910fa9..6c1203980 100644 --- a/wai/pack-conformance/package-mutations/index-capability/LISTING +++ b/wai/pack-conformance/package-mutations/index-capability/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 baabd81a9cad6e8afacbb18311924557956a0e37f5f271e8d458d651921707c4 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/index-class-plan/LISTING b/wai/pack-conformance/package-mutations/index-class-plan/LISTING index ed3027502..7040a088e 100644 --- a/wai/pack-conformance/package-mutations/index-class-plan/LISTING +++ b/wai/pack-conformance/package-mutations/index-class-plan/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 3c481132f733d1076cad2742c8a4b4667dad83397c1a57abfeb8333ab9944cbb index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/index-class-rate/LISTING b/wai/pack-conformance/package-mutations/index-class-rate/LISTING index 740278e5d..1a7d25f27 100644 --- a/wai/pack-conformance/package-mutations/index-class-rate/LISTING +++ b/wai/pack-conformance/package-mutations/index-class-rate/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 8bfd45971ced265bfc01d043d53359c2d23aa8ac4c6512b0a421f78e1bdfe476 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/index-class-records/LISTING b/wai/pack-conformance/package-mutations/index-class-records/LISTING index 7cc454ca4..7b0eb9732 100644 --- a/wai/pack-conformance/package-mutations/index-class-records/LISTING +++ b/wai/pack-conformance/package-mutations/index-class-records/LISTING @@ -20,7 +20,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 00641a12e923f55c78bdc6d1262a0d4dc218a7c35904590e3dbaf16a7607f745 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/index-string-order/LISTING b/wai/pack-conformance/package-mutations/index-string-order/LISTING index e9eaac9b9..10e49e106 100644 --- a/wai/pack-conformance/package-mutations/index-string-order/LISTING +++ b/wai/pack-conformance/package-mutations/index-string-order/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 c3a5e23418c1ff2cc8e12281df47a82bebe61b31260997a71311f1275d104334 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/LISTING b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/LISTING index 68171dd88..3a85f5d42 100644 --- a/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/LISTING +++ b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 ad140ff82229d3f47f4b2b67be03006ae5a86f89444b4db2b1265ed32766b965 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor/LISTING b/wai/pack-conformance/package-mutations/patent-pool-floor/LISTING index c7f7266ef..209182060 100644 --- a/wai/pack-conformance/package-mutations/patent-pool-floor/LISTING +++ b/wai/pack-conformance/package-mutations/patent-pool-floor/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 ad140ff82229d3f47f4b2b67be03006ae5a86f89444b4db2b1265ed32766b965 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/LISTING b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/LISTING index 3e0643153..2092e52e2 100644 --- a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/LISTING +++ b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 108469a87ed44e9a7ce517eae4507fc789b7a3ffa926ded766ca463d6443e161 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai diff --git a/wai/pack-conformance/package-mutations/receipt-duplicate-member/LISTING b/wai/pack-conformance/package-mutations/receipt-duplicate-member/LISTING index 434078306..5547ab3e4 100644 --- a/wai/pack-conformance/package-mutations/receipt-duplicate-member/LISTING +++ b/wai/pack-conformance/package-mutations/receipt-duplicate-member/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/receipt-newline/LISTING b/wai/pack-conformance/package-mutations/receipt-newline/LISTING index c3efd6ffa..063494dfe 100644 --- a/wai/pack-conformance/package-mutations/receipt-newline/LISTING +++ b/wai/pack-conformance/package-mutations/receipt-newline/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/receipt-pretty/LISTING b/wai/pack-conformance/package-mutations/receipt-pretty/LISTING index 69775e496..0f972a990 100644 --- a/wai/pack-conformance/package-mutations/receipt-pretty/LISTING +++ b/wai/pack-conformance/package-mutations/receipt-pretty/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/package-mutations/sha256-file/LISTING b/wai/pack-conformance/package-mutations/sha256-file/LISTING index d3a83f4cb..6f18fd74d 100644 --- a/wai/pack-conformance/package-mutations/sha256-file/LISTING +++ b/wai/pack-conformance/package-mutations/sha256-file/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/packs/video-base-01/package/LISTING b/wai/pack-conformance/packs/video-base-01/package/LISTING index a9920d840..e2a8366c8 100644 --- a/wai/pack-conformance/packs/video-base-01/package/LISTING +++ b/wai/pack-conformance/packs/video-base-01/package/LISTING @@ -17,7 +17,7 @@ c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master. ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix -57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +115e8e1e7ee0765de9bd2f71576cae01ce6576a5ce2a24f3374879eb74d9a6af moq/catalog.json 07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai 4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai diff --git a/wai/pack-conformance/packs/video-base-01/package/moq/catalog.json b/wai/pack-conformance/packs/video-base-01/package/moq/catalog.json index 40bccfcac..c3211ef22 100644 --- a/wai/pack-conformance/packs/video-base-01/package/moq/catalog.json +++ b/wai/pack-conformance/packs/video-base-01/package/moq/catalog.json @@ -1 +1 @@ -{"tracks":[{"altGroup":1,"name":"v/l0","packaging":"wai","priority":64,"renderGroup":1,"wai_capabilities":["wai.video.int_motion"],"wai_determinism":"decode-equivalence","wai_layer":0,"wai_receipts":false},{"altGroup":1,"codec":"av01.0.00M.08","height":36,"name":"v/floor","packaging":"cmaf","renderGroup":1,"width":64}],"version":1} +{"tracks":[{"altGroup":1,"isLive":false,"name":"v/l0","namespace":"video-base-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_motion"],"wai_determinism":"decode-equivalence","wai_receipt_alias":1379901173764869,"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":"v/floor","namespace":"video-base-01","packaging":"cmaf","renderGroup":1,"width":64},{"isLive":false,"name":"v/l0/receipts","namespace":"video-base-01","packaging":"wai-receipts"}],"version":1} diff --git a/wai/pack-conformance/packs/video-staged-01/floor/v.obu b/wai/pack-conformance/packs/video-staged-01/floor/v.obu new file mode 100644 index 000000000..22908cd30 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/floor/v.obu differ diff --git a/wai/pack-conformance/packs/video-staged-01/key.hex b/wai/pack-conformance/packs/video-staged-01/key.hex new file mode 100644 index 000000000..313fa763f --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/key.hex @@ -0,0 +1 @@ +a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5 diff --git a/wai/pack-conformance/packs/video-staged-01/package/LISTING b/wai/pack-conformance/packs/video-staged-01/package/LISTING new file mode 100644 index 000000000..2f8f2fa52 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/LISTING @@ -0,0 +1,41 @@ +f9359ffd69743b04bc491f9fe32e6bac50a3f2e6c9ec9e747b0f16572bea0dec dash/classic.mpd +e3f5ca3e780c57e10b71b30b644dd5c488f85cc11cb12557c75307ea6590d657 dash/mobile.mpd +825126074444a4419069e844ae4a0b9dcb02a2ef5830168e4e77967de9df710a dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362 floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +b3a3be45bb384a06e744c8c2293ba59e16738791abe3ac90b262c2db9a0c5744 hls/classic/floor-v.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/classic/master.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/legacy/master.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/mobile/master.m3u8 +1b7bf011b974365e6bc6532de6d0c54fa9b1447f75c84f3c52d02e1c433e146a hls/mobile/wai-v.m3u8 +adce889f610eaa20b77909693de01dfae787518748223b0774cd56004e0c9353 hls/mobile/wai.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/tv/master.m3u8 +1ea26eaea415f8b459f5994a950e14016a501c2464b9ed8d77db800e16da31d2 hls/tv/wai-v.m3u8 +fc9db3d8e327724c8a7ee793d4b1f1949cf95df1c1941bfcc2bf539fe4184e77 hls/tv/wai.m3u8 +424e86d090780853634a8b95c8a75e9a012fc28e74e0fede44127823cc3e5efe index-v.wsix +0387c4128de60831575723b868864c82f9d8d129aacec6ad8797db5ca5e5acf4 moq/catalog.json +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +849b7e2cba0f65d3183119f6f4f2c77e5c2a1db9a38d8552369db2dec28df7a7 objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai +22c6777c989d288a6a89876062a244194fde54f5b091b0f603c4ef11f1bc4f60 objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai +83657a16f8686cb03f1da0b5fe3a60ad24b32e1b3f24c17e5b9e5e54720571be objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai +38725131c01f75141fe1790d40209348da7f45241df0bf82a794e8c8196a0fb6 objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +e191fd2fe53e15c30c964e355340cbfbb44562078d6c18c8321c9d3539c43423 objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +6204e0570e77912b82c4b0bbbb51d3612334367b2ab62021a93578043e296ec9 package.receipt.json +8454596c213291b896c4483017406c58076e27ad0cab4fab54b850841dfa1a29 plan-v.txt +11536aab05b3fe23aa428a52617da9d156f6326d42ee5b8785af4713b436ecf2 policy.json +61ad0d0ba75977d401ebaeb00b5a8f5795f3b7a1408018fcbb02038f2c412e16 policy.toml +703346f293954ae90516c7cf1a8616e6851f3dba3ca3f51967b388493b1736ff stages/legacy.json +b8beb6eaa1c5a04801b0dcbcbd7f0885f5b0730871708ce0a59093e3c8ea1fdb stages/mobile.json +9c4eda5ea5972cbca98b3b89503235fcd807450c129545ca86c322337e27d872 stages/tv.json diff --git a/wai/pack-conformance/packs/video-staged-01/package/dash/classic.mpd b/wai/pack-conformance/packs/video-staged-01/package/dash/classic.mpd new file mode 100644 index 000000000..3407d9f40 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/dash/classic.mpd @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-staged-01/package/dash/mobile.mpd b/wai/pack-conformance/packs/video-staged-01/package/dash/mobile.mpd new file mode 100644 index 000000000..6fdc05cc3 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/dash/mobile.mpd @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-staged-01/package/dash/tv.mpd b/wai/pack-conformance/packs/video-staged-01/package/dash/tv.mpd new file mode 100644 index 000000000..698e85327 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/dash/tv.mpd @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-staged-01/package/floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s b/wai/pack-conformance/packs/video-staged-01/package/floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s new file mode 100644 index 000000000..2ba26d8a1 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s b/wai/pack-conformance/packs/video-staged-01/package/floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s new file mode 100644 index 000000000..67af7baa6 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s b/wai/pack-conformance/packs/video-staged-01/package/floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s new file mode 100644 index 000000000..40e6758ca Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s b/wai/pack-conformance/packs/video-staged-01/package/floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s new file mode 100644 index 000000000..62b794088 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s b/wai/pack-conformance/packs/video-staged-01/package/floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s new file mode 100644 index 000000000..66a9616f4 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/floor/v/init.mp4 b/wai/pack-conformance/packs/video-staged-01/package/floor/v/init.mp4 new file mode 100644 index 000000000..0b3ade258 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/floor/v/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/hls/classic/floor-v.m3u8 b/wai/pack-conformance/packs/video-staged-01/package/hls/classic/floor-v.m3u8 new file mode 100644 index 000000000..609fa33ba --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/hls/classic/floor-v.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/v/init.mp4" +#EXTINF:2.000, +../../floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +#EXTINF:1.000, +../../floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +#EXTINF:1.600, +../../floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +#EXTINF:1.000, +../../floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +#EXTINF:2.400, +../../floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-staged-01/package/hls/classic/master.m3u8 b/wai/pack-conformance/packs/video-staged-01/package/hls/classic/master.m3u8 new file mode 100644 index 000000000..93630b459 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/hls/classic/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=9334,AVERAGE-BANDWIDTH=8796,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/packs/video-staged-01/package/hls/legacy/master.m3u8 b/wai/pack-conformance/packs/video-staged-01/package/hls/legacy/master.m3u8 new file mode 100644 index 000000000..93630b459 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/hls/legacy/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=9334,AVERAGE-BANDWIDTH=8796,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/packs/video-staged-01/package/hls/mobile/master.m3u8 b/wai/pack-conformance/packs/video-staged-01/package/hls/mobile/master.m3u8 new file mode 100644 index 000000000..c9f4be8cb --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/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=9334,AVERAGE-BANDWIDTH=8796,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/packs/video-staged-01/package/hls/mobile/wai-v.m3u8 b/wai/pack-conformance/packs/video-staged-01/package/hls/mobile/wai-v.m3u8 new file mode 100644 index 000000000..1b2cdf31e --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-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 +#EXTINF:2.000, +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai#n=974 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXTINF:2.400, +../../objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai#n=31610 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-staged-01/package/hls/mobile/wai.m3u8 b/wai/pack-conformance/packs/video-staged-01/package/hls/mobile/wai.m3u8 new file mode 100644 index 000000000..c4646ab7d --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-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=323176,AVERAGE-BANDWIDTH=173043,CODECS="wai1.video.int_motion+video.int_refine",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/packs/video-staged-01/package/hls/tv/master.m3u8 b/wai/pack-conformance/packs/video-staged-01/package/hls/tv/master.m3u8 new file mode 100644 index 000000000..c9f4be8cb --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/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=9334,AVERAGE-BANDWIDTH=8796,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/packs/video-staged-01/package/hls/tv/wai-v.m3u8 b/wai/pack-conformance/packs/video-staged-01/package/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..6097d50b1 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/hls/tv/wai-v.m3u8 @@ -0,0 +1,25 @@ +#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.000, +#WAI-LAYER:N=1,URI="../../objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai#n=67779" +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai#n=974 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXTINF:2.400, +#WAI-LAYER:N=1,URI="../../objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai#n=81407" +#WAI-LAYER:N=2,URI="../../objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai#n=101295" +../../objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai#n=31610 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-staged-01/package/hls/tv/wai.m3u8 b/wai/pack-conformance/packs/video-staged-01/package/hls/tv/wai.m3u8 new file mode 100644 index 000000000..97f423ab5 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-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=714374,AVERAGE-BANDWIDTH=586902,CODECS="wai1.video.int_motion+video.int_refine",X-WAI-FLOOR="wai1.video.int_motion+video.int_refine",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/packs/video-staged-01/package/index-v.wsix b/wai/pack-conformance/packs/video-staged-01/package/index-v.wsix new file mode 100644 index 000000000..fee174a88 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/index-v.wsix differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/moq/catalog.json b/wai/pack-conformance/packs/video-staged-01/package/moq/catalog.json new file mode 100644 index 000000000..26756c120 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/moq/catalog.json @@ -0,0 +1 @@ +{"tracks":[{"altGroup":1,"isLive":false,"name":"v/l0","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_motion","wai.image.png"],"wai_determinism":"decode-equivalence","wai_receipt_alias":6826975590310646,"wai_receipts":true,"wai_receipts_track":"v/l0/receipts","wai_staged":{"layer":0,"lead_ms":null,"set":"v"}},{"depends":["v/l0"],"isLive":false,"name":"v/l1@800","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":600,"wai_determinism":"decode-equivalence","wai_receipt_alias":4051354593871184,"wai_receipts":true,"wai_receipts_track":"v/l1@800/receipts","wai_staged":{"layer":1,"lead_ms":null,"set":"v"}},{"depends":["v/l0"],"isLive":false,"name":"v/l1","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":1300,"wai_determinism":"decode-equivalence","wai_receipt_alias":5745064158311842,"wai_receipts":true,"wai_receipts_track":"v/l1/receipts","wai_staged":{"layer":1,"lead_ms":null,"set":"v"}},{"depends":["v/l0","v/l1@800"],"isLive":false,"name":"v/l2@800","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":400,"wai_determinism":"decode-equivalence","wai_receipt_alias":8428654497284120,"wai_receipts":true,"wai_receipts_track":"v/l2@800/receipts","wai_staged":{"layer":2,"lead_ms":null,"set":"v"}},{"depends":["v/l0","v/l1"],"isLive":false,"name":"v/l2","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":1100,"wai_determinism":"decode-equivalence","wai_receipt_alias":6697012243957121,"wai_receipts":true,"wai_receipts_track":"v/l2/receipts","wai_staged":{"layer":2,"lead_ms":null,"set":"v"}},{"altGroup":1,"codec":"av01.0.00M.08","height":36,"name":"v/floor","namespace":"video-staged-01","packaging":"cmaf","renderGroup":1,"width":64},{"isLive":false,"name":"v/l0/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l1@800/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l1/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l2@800/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l2/receipts","namespace":"video-staged-01","packaging":"wai-receipts"}],"version":1} diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai new file mode 100644 index 000000000..34c4f062f Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai new file mode 100644 index 000000000..d817a5851 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai new file mode 100644 index 000000000..f9c13f7d3 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai new file mode 100644 index 000000000..f5c123081 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai new file mode 100644 index 000000000..76a603bce Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai new file mode 100644 index 000000000..b4300b4a2 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai new file mode 100644 index 000000000..949dacb1f Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai new file mode 100644 index 000000000..a5436b748 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai new file mode 100644 index 000000000..044c591aa Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai new file mode 100644 index 000000000..ea3e576f4 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai new file mode 100644 index 000000000..de80c717a Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai new file mode 100644 index 000000000..1686b0d52 Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai new file mode 100644 index 000000000..21929485d Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai b/wai/pack-conformance/packs/video-staged-01/package/objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai new file mode 100644 index 000000000..bedddb8cd Binary files /dev/null and b/wai/pack-conformance/packs/video-staged-01/package/objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai differ diff --git a/wai/pack-conformance/packs/video-staged-01/package/package.receipt.json b/wai/pack-conformance/packs/video-staged-01/package/package.receipt.json new file mode 100644 index 000000000..09e0296f1 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"6842a02b45fb2ddf8095621836901abdbf2526451d740976ffe7079fc210932c","media_class":"video","stage_root":"c10304943d547971b05fdb57231dcd6e097d7df38cc68ea92c376ddc957e022d"}],"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":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"4441a653f101172ee9d4b76f788a10f23ab359e7bf3116fb09327699c2c83ef0","sig":"f62bd725148a4203b440769906eacf7083895accc0191782f7e43197d92a598b90f1a5ea04eedd493dda87a5e622889562f0f09a5010cb053700b016f44cb504","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":484464,"total_bytes":566398,"total_entries":18,"unique_objects":14,"version":2,"work_units":18} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-staged-01/package/plan-v.txt b/wai/pack-conformance/packs/video-staged-01/package/plan-v.txt new file mode 100644 index 000000000..ce9efa1e3 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/plan-v.txt @@ -0,0 +1,5 @@ +0 +50 +75 +115 +140 diff --git a/wai/pack-conformance/packs/video-staged-01/package/policy.json b/wai/pack-conformance/packs/video-staged-01/package/policy.json new file mode 100644 index 000000000..2b02a3984 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","class":[{"floor_only":false,"id":"tv","max_layer":2},{"floor_only":false,"id":"mobile","max_kbps":330,"max_layer":1},{"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-staged-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"f00d8bf055ef725d99f925cc0c1c97b6b7fc956c82872f63840c246b4b69e150"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":60000,"mv_precision":1,"q_candidates":[24,16],"search":4},"role":"base"},{"capability":"wai.video.int_refine","component":"v","id":"r1","params":{"q":12},"refines":"b0","role":"refinement"},{"capability":"wai.video.int_refine","component":"v","id":"r2","params":{"q":4},"refines":"r1","role":"refinement"},{"capability":"wai.image.png","component":"v","id":"still","params":{},"role":"base"},{"capability":"wai.video.av1","component":"v","id":"f0","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0,200,400]},"moment":[{"from_ms":5600,"id":"m-event","tags":["event"],"to_ms":8000}],"royalty_free_only":true,"rule":[{"id":"static","then":{"ladder":["still","f0"]},"when":{"piece_kind":["static"]}},{"id":"motion","then":{"max_layer":1},"when":{"piece_kind":["motion_high"]}},{"id":"event","then":{"deadline_ms":800},"when":{"moment_tag":["event"]}}],"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-staged-01/package/policy.toml b/wai/pack-conformance/packs/video-staged-01/package/policy.toml new file mode 100644 index 000000000..9541c224c --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/policy.toml @@ -0,0 +1,94 @@ +schema = "wai.pack.policy/1" +asset_id = "video-staged-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-staged-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 = [24, 16], max_sse_px_milli = 60000 } + +[[ladder]] +id = "r1" +component = "v" +capability = "wai.video.int_refine" +role = "refinement" +refines = "b0" +params = { q = 12 } + +[[ladder]] +id = "r2" +component = "v" +capability = "wai.video.int_refine" +role = "refinement" +refines = "r1" +params = { q = 4 } + +[[ladder]] +id = "still" +component = "v" +capability = "wai.image.png" +role = "base" + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "f00d8bf055ef725d99f925cc0c1c97b6b7fc956c82872f63840c246b4b69e150" + +[live] +layer_lag_ms = [0, 200, 400] + +[[class]] +id = "tv" +max_layer = 2 + +[[class]] +id = "mobile" +max_layer = 1 +max_kbps = 330 + +[[class]] +id = "legacy" +floor_only = true + +[[moment]] +id = "m-event" +from_ms = 5600 +to_ms = 8000 +tags = ["event"] + +[[rule]] +id = "static" +when = { piece_kind = ["static"] } +then = { ladder = ["still", "f0"] } + +[[rule]] +id = "motion" +when = { piece_kind = ["motion_high"] } +then = { max_layer = 1 } + +[[rule]] +id = "event" +when = { moment_tag = ["event"] } +then = { deadline_ms = 800 } diff --git a/wai/pack-conformance/packs/video-staged-01/package/stages/legacy.json b/wai/pack-conformance/packs/video-staged-01/package/stages/legacy.json new file mode 100644 index 000000000..c4964ed93 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":50,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":50},{"dur":40,"floor_only":true,"p":2,"stages":[],"start":75},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":115},{"dur":60,"floor_only":true,"p":4,"stages":[],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/packs/video-staged-01/package/stages/mobile.json b/wai/pack-conformance/packs/video-staged-01/package/stages/mobile.json new file mode 100644 index 000000000..b09903278 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":974,"h":"8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"}],"start":115},{"dur":60,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0}],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/packs/video-staged-01/package/stages/tv.json b/wai/pack-conformance/packs/video-staged-01/package/stages/tv.json new file mode 100644 index 000000000..d55038742 --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/package/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0},{"bytes":67779,"deadline_ms":1500,"h":"7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec","layer":1,"refines":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27"}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":974,"h":"8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":115},{"dur":60,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0},{"bytes":81407,"deadline_ms":800,"h":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d","layer":1,"refines":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9"},{"bytes":101295,"deadline_ms":800,"h":"a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87","layer":2,"refines":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d"}],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/packs/video-staged-01/policy.toml b/wai/pack-conformance/packs/video-staged-01/policy.toml new file mode 100644 index 000000000..9541c224c --- /dev/null +++ b/wai/pack-conformance/packs/video-staged-01/policy.toml @@ -0,0 +1,94 @@ +schema = "wai.pack.policy/1" +asset_id = "video-staged-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-staged-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 = [24, 16], max_sse_px_milli = 60000 } + +[[ladder]] +id = "r1" +component = "v" +capability = "wai.video.int_refine" +role = "refinement" +refines = "b0" +params = { q = 12 } + +[[ladder]] +id = "r2" +component = "v" +capability = "wai.video.int_refine" +role = "refinement" +refines = "r1" +params = { q = 4 } + +[[ladder]] +id = "still" +component = "v" +capability = "wai.image.png" +role = "base" + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "f00d8bf055ef725d99f925cc0c1c97b6b7fc956c82872f63840c246b4b69e150" + +[live] +layer_lag_ms = [0, 200, 400] + +[[class]] +id = "tv" +max_layer = 2 + +[[class]] +id = "mobile" +max_layer = 1 +max_kbps = 330 + +[[class]] +id = "legacy" +floor_only = true + +[[moment]] +id = "m-event" +from_ms = 5600 +to_ms = 8000 +tags = ["event"] + +[[rule]] +id = "static" +when = { piece_kind = ["static"] } +then = { ladder = ["still", "f0"] } + +[[rule]] +id = "motion" +when = { piece_kind = ["motion_high"] } +then = { max_layer = 1 } + +[[rule]] +id = "event" +when = { moment_tag = ["event"] } +then = { deadline_ms = 800 } diff --git a/wai/pack-conformance/png_diff.py b/wai/pack-conformance/png_diff.py new file mode 100644 index 000000000..06e951b3d --- /dev/null +++ b/wai/pack-conformance/png_diff.py @@ -0,0 +1,535 @@ +#!/usr/bin/env python3 +"""A still's reader, differentially: wai-pack's PNG reader against pack_verify.py's, which +reads the zlib stream with Python's zlib module (extensions/packaging.md §5.7). + +The inputs, seeded: + +- streams the zlib library writes, at every level and strategy and at windows from + 512 bytes to 32 KiB, and the reference packager's own stills; +- dynamic blocks built here, each code set chosen to sit on one of RFC 1951's and zlib's + rules: complete codes; no distance code at all; a lone distance code of one bit (the + one incomplete set zlib takes), used and misused; a lone distance code of 2 to 15 bits; + over-subscribed and incomplete literal/length, distance and code-length codes; a + repeat with no length before it, or past the code lengths; no code for the end of the + block; too many length or distance symbols; a distance past the bytes written; stored + blocks with a wrong complement; fixed blocks with the symbols they cannot use; and + several blocks of mixed kinds; +- and every one of those altered: bits flipped, cut short, bytes appended, the header's + bytes and the Adler-32 changed, chunks dropped, repeated, moved or added, the IHDR's + fields and a chunk's length changed. + +Each one's verdict and, read, its samples' SHA-256 must be the same from both readers, +and the constructions whose verdict the rules decide must have it. + + python3 png_diff.py [seed] [count] + +Exit status 0 only if every input agrees. +""" +import hashlib +import heapq +import json +import os +import random +import struct +import subprocess +import sys +import zlib + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import pack_verify as PV # noqa: E402 + +SIG = b"\x89PNG\r\n\x1a\n" +ORDER = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15] + + +def chunk(kind, data): + return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data)) + + +def png_of(w, h, z, extra=()): + ihdr = struct.pack(">II", w, h) + bytes([8, 2, 0, 0, 0]) + return SIG + chunk(b"IHDR", ihdr) + b"".join(chunk(k, d) for k, d in extra) + chunk(b"IDAT", z) + chunk(b"IEND", b"") + + +def picture(rng): + """A picture's filtered rows: random, flat or striped samples, each row with a filter.""" + w, h = rng.randint(1, 40), rng.randint(1, 12) + kind = rng.randrange(3) + raw = bytearray() + for y in range(h): + raw.append(rng.randrange(5)) + for x in range(3 * w): + raw.append(rng.randrange(256) if kind == 0 else (7 if kind == 1 else (x // 3 * 11 + y) % 256)) + return w, h, bytes(raw) + + +class Bits: + def __init__(self): + self.out, self.acc, self.n = bytearray(), 0, 0 + + def put(self, v, n): + for i in range(n): + self.acc |= ((v >> i) & 1) << self.n + self.n += 1 + if self.n == 8: + self.out.append(self.acc) + self.acc, self.n = 0, 0 + + def code(self, c, n): + for i in reversed(range(n)): + self.put((c >> i) & 1, 1) + + def align(self): + if self.n: + self.out.append(self.acc) + self.acc, self.n = 0, 0 + + +def canonical(lengths): + """RFC 1951 §3.2.2's codes of `lengths` (an over-subscribed set gives codes anyway).""" + count = [0] * 16 + for x in lengths: + count[x] += 1 + count[0] = 0 + nxt, code = [0] * 16, 0 + for b in range(1, 16): + code = (code + count[b - 1]) << 1 + nxt[b] = code + out = [] + for x in lengths: + out.append(nxt[x] if x else 0) + if x: + nxt[x] += 1 + return out + + +def huffman(freq, n, limit): + """Code lengths for symbols 0..n-1 from their counts (a symbol of count 0 gets none): + Huffman's, or equal lengths where Huffman's pass `limit`. Complete for two or more + symbols; one symbol gets one bit.""" + used = [s for s in range(n) if freq.get(s)] + lengths = [0] * n + if len(used) == 1: + lengths[used[0]] = 1 + return lengths + heap = [(freq[s], i, [s]) for i, s in enumerate(used)] + heapq.heapify(heap) + k = len(heap) + while len(heap) > 1: + a, b = heapq.heappop(heap), heapq.heappop(heap) + for s in a[2] + b[2]: + lengths[s] += 1 + heapq.heappush(heap, (a[0] + b[0], k, a[2] + b[2])) + k += 1 + if max(lengths) > limit: + # Equal lengths, then the shortest codes split off until the set is complete. + bits = max(1, (len(used) - 1).bit_length()) + lengths = [bits if s in used else 0 for s in range(n)] + spare = (1 << bits) - len(used) + for s in used: + if spare == 0: + break + lengths[s] -= 1 + spare -= 1 + return lengths + + +LEN_BASE, LEN_EXTRA = PV.LEN_BASE, PV.LEN_EXTRA +DIST_BASE, DIST_EXTRA = PV.DIST_BASE, PV.DIST_EXTRA + + +def tokens(raw, dists, rng): + """`raw` as literals and matches, each match at a distance of a symbol in `dists`.""" + out, i = [], 0 + while i < len(raw): + best = None + if dists and rng.random() < 0.8: + for s in dists: + for d in {DIST_BASE[s], DIST_BASE[s] + (1 << DIST_EXTRA[s]) - 1}: + if d <= i: + m = 0 + while m < 258 and i + m < len(raw) and raw[i + m - d] == raw[i + m]: + m += 1 + if m >= 3 and (best is None or m > best[0]): + best = (m, d, s) + if best: + out.append(("m", best[0], best[1], best[2])) + i += best[0] + else: + out.append(("l", raw[i])) + i += 1 + return out + + +def length_symbol(m): + li = max(k for k, x in enumerate(LEN_BASE) if x <= m) + return 257 + li, m - LEN_BASE[li], LEN_EXTRA[li] + + +def write_tokens(b, toks, lit, lc, dist, dc, bad_dist_bit=False): + misused = False + for t in toks: + if t[0] == "l": + b.code(lc[t[1]], lit[t[1]]) + continue + _, m, d, s = t + sym, extra, nbits = length_symbol(m) + b.code(lc[sym], lit[sym]) + b.put(extra, nbits) + if bad_dist_bit and not misused: + b.put(1, 1) # the unassigned half of a lone one-bit code + misused = True + continue + b.code(dc[s], dist[s]) + b.put(d - DIST_BASE[s], DIST_EXTRA[s]) + b.code(lc[256], lit[256]) + + +def over(lengths): + """A complete set made over-subscribed: a code for a symbol that had none, or, with + every symbol coded, its longest code a bit shorter.""" + free = [x for x in range(len(lengths)) if not lengths[x]] + if free: + lengths[free[0]] = 1 + else: + s = max(range(len(lengths)), key=lambda x: lengths[x]) + lengths[s] -= 1 + + +def dynamic_block(b, toks, last, variant, rng): + """One dynamic block of `toks` with the code sets `variant` asks for.""" + freq = {256: 1} + dfreq = {} + for t in toks: + s = t[1] if t[0] == "l" else length_symbol(t[1])[0] + freq[s] = freq.get(s, 0) + 1 + if t[0] == "m": + dfreq[t[3]] = dfreq.get(t[3], 0) + 1 + if len(freq) == 1: + freq[0] = 1 + nlen = max(257, max(freq) + 1) + lit = huffman(freq, nlen, 15) + if variant == "dist-none": + ndist, dist = 1, [0] + elif dfreq: + ndist = max(dfreq) + 1 + dist = huffman(dfreq, ndist, 15) + else: + ndist, dist = 1, [0] + used_d = [s for s in range(ndist) if dist[s]] + if variant.startswith("dist-lone-") and len(used_d) == 1: + dist[used_d[0]] = 1 if variant in ("dist-lone-1", "dist-lone-1-misused") else rng.randint(2, 15) + elif variant == "dist-over": + dist = dist + [1, 1, 1] + ndist = len(dist) + elif variant == "dist-incomplete" and used_d: + # One code a bit longer: the set falls short of complete, its longest past one bit. + s = min(used_d, key=lambda x: dist[x]) + dist[s] += 1 + elif variant == "lit-incomplete": + s = min((x for x in range(nlen) if lit[x]), key=lambda x: lit[x]) + lit[s] += 1 + elif variant == "lit-over": + # A code for a symbol that had none, on a set that was complete. + over(lit) + elif variant == "no-eob": + lit[256] = 0 + ndist = min(ndist, 30) + dist = dist[:ndist] + lc, dc = canonical(lit), canonical(dist) + seq = lit + dist + # The code lengths, run-length coded at random (§3.2.7). + syms, i = [], 0 + if variant == "rep16-first": + syms.append((16, 0, 2)) + while i < len(seq): + x = seq[i] + run = 1 + while i + run < len(seq) and seq[i + run] == x: + run += 1 + if x == 0 and run >= 11 and rng.random() < 0.7: + n = min(run, 138) + syms.append((18, n - 11, 7)) + i += n + elif x == 0 and run >= 3 and rng.random() < 0.7: + n = min(run, 10) + syms.append((17, n - 3, 3)) + i += n + elif i > 0 and seq[i - 1] == x and run >= 3 and rng.random() < 0.7: + n = min(run, 6) + syms.append((16, n - 3, 2)) + i += n + else: + syms.append((x, 0, 0)) + i += 1 + if variant == "rep-past": + syms.append((17, 7, 3)) + cfreq = {} + for s, _, _ in syms: + cfreq[s] = cfreq.get(s, 0) + 1 + if len(cfreq) == 1: + cfreq[0 if 0 not in cfreq else 1] = 1 + cl = huffman(cfreq, 19, 7) + if variant == "cl-incomplete": + s = min((x for x in range(19) if 0 < cl[x] < 7), key=lambda x: cl[x]) + cl[s] += 1 + elif variant == "cl-over": + over(cl) + cc = canonical(cl) + ncode = max(4, max(k for k, s in enumerate(ORDER) if cl[s]) + 1) + b.put(last, 1) + b.put(2, 2) + hlit = nlen - 257 if variant != "hlit-big" else rng.choice([30, 31]) + hdist = ndist - 1 if variant != "hdist-big" else rng.choice([30, 31]) + b.put(hlit, 5) + b.put(hdist, 5) + b.put(ncode - 4, 4) + for s in ORDER[:ncode]: + b.put(cl[s], 3) + for s, extra, nbits in syms: + b.code(cc[s], cl[s]) + b.put(extra, nbits) + write_tokens(b, toks, lit, lc, dist, dc, bad_dist_bit=variant == "dist-lone-1-misused") + + +def fixed_block(b, toks, last, variant): + lit = [8] * 144 + [9] * 112 + [7] * 24 + [8] * 8 + lc = canonical(lit) + dist, dc = [5] * 32, canonical([5] * 32) + b.put(last, 1) + b.put(1, 2) + if variant == "fixed-lit-286": + b.code(lc[286], 8) + elif variant == "fixed-dist-30" and any(t[0] == "m" for t in toks): + k = next(i for i, t in enumerate(toks) if t[0] == "m") + toks = toks[:k] + [("m", toks[k][1], toks[k][2], 30)] + toks[k + 1:] + dist_base = DIST_BASE + [0, 0] + for t in toks: + if t[0] == "l": + b.code(lc[t[1]], 8 if t[1] < 144 else 9) + else: + sym, extra, nbits = length_symbol(t[1]) + b.code(lc[sym], lit[sym]) + b.put(extra, nbits) + b.code(dc[t[3]], 5) + if t[3] < 30: + b.put(t[2] - dist_base[t[3]], DIST_EXTRA[t[3]]) + b.code(lc[256], 7) + return + write_tokens(b, toks, lit, lc, dist, dc) + + +def stored_block(b, data, last, variant): + b.put(last, 1) + b.put(0, 2) + b.align() + n = len(data) + nn = (~n) & 0xFFFF if variant != "stored-complement" else n + b.out += struct.pack("= 2 else [0, len(raw)] + parts = [raw[:cut[0]], raw[cut[0]:cut[1]], raw[cut[1]:]] + for k, part in enumerate(parts): + last = int(k == 2) + kind = rng.randrange(3) + if kind == 0: + stored_block(b, part, last, "stored") + else: + # Matches only within this part, so a distance never reaches into another. + toks = tokens(part, list(range(4)), rng) + if kind == 1: + fixed_block(b, toks, last, "") + else: + dynamic_block(b, toks, last, "complete", rng) + else: + dists = [] + if variant not in ("dist-none",): + dists = [rng.randrange(4)] if variant.startswith("dist-lone-") else rng.sample(range(8), rng.randint(1, 4)) + toks = tokens(raw, dists, rng) + if variant.startswith("dist-lone-") or variant in ("dist-over", "dist-incomplete"): + if not any(t[0] == "m" for t in toks): + return None + if variant == "far": + toks = [("m", 3, 1, 0)] + toks + dynamic_block(b, toks, 1, "dist-lone-L" if variant == "dist-lone-L" else variant, rng) + b.align() + z = b"\x78\x01" + bytes(b.out) + struct.pack(">I", zlib.adler32(raw)) + return png_of(w, h, z) + + +def zlib_streams(rng): + out = [] + for _ in range(40): + w, h, raw = picture(rng) + co = zlib.compressobj(rng.choice([0, 1, 6, 9]), zlib.DEFLATED, rng.choice([9, 10, 12, 15]), 9, + rng.choice([zlib.Z_DEFAULT_STRATEGY, zlib.Z_FIXED, zlib.Z_HUFFMAN_ONLY, zlib.Z_RLE])) + out.append(png_of(w, h, co.compress(raw) + co.flush())) + rgb = bytes(rng.randrange(256) if rng.random() < 0.5 else 9 for _ in range(3 * w * h)) + out.append(PV.png_still(rgb, w, h)) + return out + + +def chunks_of(b): + at, out = 8, [] + while at + 12 <= len(b): + n = struct.unpack_from(">I", b, at)[0] + if at + 12 + n > len(b): + break + out.append((b[at + 4:at + 8], b[at + 8:at + 8 + n])) + at += 12 + n + return out + + +def build(chs): + return SIG + b"".join(chunk(k, d) for k, d in chs) + + +def mutate(b, rng): + if len(b) < 2: + return b + bytes([rng.randrange(256)]) + chs = chunks_of(b) + op = rng.randrange(12) + if op == 0 or not chs: + b = bytearray(b) + for _ in range(rng.randint(1, 3)): + i = rng.randrange(len(b)) + b[i] ^= 1 << rng.randrange(8) + return bytes(b) + if op == 1: + return b[:rng.randrange(len(b))] + if op == 2: + i, j = rng.randrange(len(chs)), rng.randrange(len(chs)) + chs[i], chs[j] = chs[j], chs[i] + return build(chs) + if op == 3: + chs.insert(rng.randrange(len(chs) + 1), chs[rng.randrange(len(chs))]) + return build(chs) + if op == 4: + del chs[rng.randrange(len(chs))] + return build(chs) + if op == 5: + kind = rng.choice([b"tEXt", b"PLTE", b"IDAT", b"IEND", b"IHDR", b"abCD", b"ABCD", b"gAMA"]) + data = bytes(rng.randrange(256) for _ in range(rng.choice([0, 1, 3, 6, 13, 768, 771]))) + chs.insert(rng.randrange(len(chs) + 1), (kind, data)) + return build(chs) + idats = [i for i, (k, _) in enumerate(chs) if k == b"IDAT"] + if op in (6, 7, 8, 9) and idats: + z = bytearray(b"".join(chs[i][1] for i in idats)) + if op == 6 and z: + for _ in range(rng.randint(1, 4)): + i = rng.randrange(len(z)) + z[i] ^= 1 << rng.randrange(8) + elif op == 7: + z = z[:rng.randrange(len(z) + 1)] + elif op == 8: + z += bytes(rng.randrange(256) for _ in range(rng.randint(1, 6))) + elif op == 9 and len(z) >= 2: + z[rng.randrange(2)] = rng.randrange(256) + if rng.random() < 0.7: + flg = z[1] & 0xE0 + z[1] = flg | (31 - (z[0] * 256 + flg) % 31) % 31 + z = bytes(z) + cut = rng.randrange(len(z) + 1) + rest = [c for i, c in enumerate(chs) if i not in idats] + pos = idats[0] + return build(rest[:pos] + [(b"IDAT", z[:cut]), (b"IDAT", z[cut:])] + rest[pos:]) + if op == 10: + for i, (k, d) in enumerate(chs): + if k == b"IHDR" and len(d) == 13: + d = bytearray(d) + d[rng.randrange(13)] = rng.choice([0, 1, 2, 3, 6, 8, 16, 255]) + chs[i] = (k, bytes(d)) + return build(chs) + return b + bytes(rng.randrange(256) for _ in range(rng.randint(1, 12))) + + +def py_verdict(b): + try: + w, h, px = PV.png_frame(b) + return {"verdict": "ok", "w": w, "h": h, "rgb_sha256": hashlib.sha256(px).hexdigest()} + except ValueError: + return {"verdict": "refused"} + + +def main(argv): + if len(argv) < 2: + print(__doc__) + return 2 + binary = argv[1] + seed = int(argv[2]) if len(argv) > 2 else 1 + count = int(argv[3]) if len(argv) > 3 else 2000 + rng = random.Random(seed) + cases = [] # (bytes, variant or None) + for f in sorted(os.listdir(os.path.join(HERE, "stills"))): + if f.endswith(".png"): + cases.append((open(os.path.join(HERE, "stills", f), "rb").read(), None)) + cases += [(b, None) for b in zlib_streams(rng)] + names = sorted(VARIANTS) + while len(cases) < count // 2: + v = names[len(cases) % len(names)] + b = construct(rng, v) + if b is not None: + cases.append((b, v)) + base = [b for b, _ in cases] + while len(cases) < count: + b = rng.choice(base) + for _ in range(rng.choice([1, 1, 2, 3])): + b = mutate(b, rng) + cases.append((b, None)) + lines = "".join(json.dumps({"op": "read-png", "hex": b.hex()}) + "\n" for b, _ in cases) + out = subprocess.run([binary, "stage-json"], input=lines.encode(), capture_output=True, check=True).stdout.decode().splitlines() + if len(out) != len(cases): + print(f"FAIL wai-pack answered {len(out)} of {len(cases)} lines") + return 1 + fails, seen = 0, {"ok": 0, "refused": 0} + by_variant = {} + for (b, v), got in zip(cases, out): + rust, mine = json.loads(got), py_verdict(b) + seen[mine["verdict"]] += 1 + if v is not None: + by_variant[v] = by_variant.get(v, 0) + 1 + why = None + if rust != mine: + why = f"wai-pack {json.dumps(rust)}, pack_verify.py {json.dumps(mine)}" + elif v is not None and VARIANTS[v] != mine["verdict"]: + why = f"the construction {v} should be {VARIANTS[v]}, both readers say {mine['verdict']}" + if why: + fails += 1 + if fails <= 5: + print(f"FAIL {v or 'input'} {b[:64].hex()}…: {why}") + missing = [v for v in names if v not in by_variant] + if missing: + fails += 1 + print(f"FAIL constructions never built: {missing}") + print(f"png_diff seed {seed}: {len(cases) - fails}/{len(cases)} agree ({seen['ok']} read, {seen['refused']} refused; " + f"{sum(by_variant.values())} constructions of {len(by_variant)} kinds)") + return 0 if fails == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/wai/pack-conformance/policy/expected.json b/wai/pack-conformance/policy/expected.json index bcf864418..fac9bf491 100644 --- a/wai/pack-conformance/policy/expected.json +++ b/wai/pack-conformance/policy/expected.json @@ -25,6 +25,11 @@ "canonical": "{\"asset_id\":\"pack-fixture-01\",\"class\":[{\"floor_only\":false,\"id\":\"mobile\",\"max_kbps\":900,\"max_layer\":1},{\"floor_only\":false,\"id\":\"tv\",\"max_layer\":2},{\"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\":\"src/video.y4m\",\"timebase\":{\"den\":1,\"num\":25}},{\"id\":\"a\",\"media\":\"audio\",\"source\":\"src/audio.wav\",\"timebase\":{\"den\":1,\"num\":48000}}],\"defaults\":{\"deadline_ms\":1500},\"floor\":{\"extra_keyframes\":false,\"supplied_listing\":\"0000000000000000000000000000000000000000000000000000000000000000\"},\"ladder\":[{\"capability\":\"wai.video.int_motion\",\"component\":\"v\",\"id\":\"b0\",\"params\":{\"block\":16,\"max_sse_px_milli\":40000,\"mv_precision\":2,\"q_candidates\":[16,12,8],\"search\":7},\"role\":\"base\"},{\"capability\":\"wai.video.int_refine\",\"component\":\"v\",\"id\":\"r1\",\"params\":{\"q\":6},\"refines\":\"b0\",\"role\":\"refinement\"},{\"capability\":\"wai.video.int_refine\",\"component\":\"v\",\"id\":\"r2\",\"params\":{\"q\":3},\"refines\":\"r1\",\"role\":\"refinement\"},{\"capability\":\"wai.image.png\",\"component\":\"v\",\"id\":\"still\",\"params\":{},\"role\":\"base\"},{\"capability\":\"wai.video.av1\",\"component\":\"v\",\"id\":\"f0\",\"params\":{\"source\":\"supplied\"},\"role\":\"floor\"},{\"capability\":\"wai.audio.flac\",\"component\":\"a\",\"id\":\"ab\",\"params\":{\"source\":\"encode\"},\"role\":\"base\"}],\"lead\":\"v\",\"live\":{\"layer_lag_ms\":[0,200,400]},\"moment\":[{\"from_ms\":30000,\"id\":\"m-goal\",\"tags\":[\"event\"],\"to_ms\":34000}],\"royalty_free_only\":true,\"rule\":[{\"id\":\"static\",\"then\":{\"ladder\":[\"still\",\"f0\"]},\"when\":{\"piece_kind\":[\"static\"]}},{\"id\":\"motion\",\"then\":{\"max_layer\":1},\"when\":{\"piece_kind\":[\"motion_high\"]}},{\"id\":\"event\",\"then\":{\"deadline_ms\":800,\"max_layer\":2},\"when\":{\"moment_tag\":[\"event\"]}}],\"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}}", "digest": "1997e19e06f6bf191d5de605ba1a4719a3372d5e7a168b5364cc82638d9dd0e9" }, + { + "file": "max-piece.toml", + "canonical": "{\"asset_id\":\"max-piece\",\"class\":[],\"classify\":{\"motion_high_milli\":9000,\"static_mad_milli\":300,\"static_permille\":5,\"synthetic_bins\":12},\"component\":[{\"id\":\"v\",\"media\":\"video\",\"timebase\":{\"den\":600,\"num\":16777216}}],\"defaults\":{\"deadline_ms\":1500},\"floor\":{\"extra_keyframes\":false},\"ladder\":[{\"capability\":\"wai.video.int_motion\",\"component\":\"v\",\"id\":\"b0\",\"params\":{\"block\":16,\"max_sse_px_milli\":0,\"mv_precision\":2,\"q_candidates\":[8],\"search\":7},\"role\":\"base\"}],\"lead\":\"v\",\"live\":{\"layer_lag_ms\":[0]},\"moment\":[],\"royalty_free_only\":false,\"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\":600000,\"min_ms\":1000,\"scene_cut\":\"wai.pack.scenecut/1\",\"target_ms\":2000}}", + "digest": "9167074601943ba9c5042ae719712c8119a728e2710a355371b2602c2c112e67" + }, { "file": "eq-defaults-a.toml", "canonical": "{\"asset_id\":\"r\",\"class\":[],\"classify\":{\"motion_high_milli\":9000,\"static_mad_milli\":300,\"static_permille\":5,\"synthetic_bins\":12},\"component\":[{\"id\":\"v\",\"media\":\"video\",\"timebase\":{\"den\":1,\"num\":25}}],\"defaults\":{\"deadline_ms\":1500},\"floor\":{\"extra_keyframes\":false},\"ladder\":[{\"capability\":\"wai.video.int_motion\",\"component\":\"v\",\"id\":\"b0\",\"params\":{\"block\":16,\"max_sse_px_milli\":0,\"mv_precision\":2,\"q_candidates\":[8],\"search\":7},\"role\":\"base\"}],\"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}}", diff --git a/wai/pack-conformance/policy/max-piece.toml b/wai/pack-conformance/policy/max-piece.toml new file mode 100644 index 000000000..912b8aaff --- /dev/null +++ b/wai/pack-conformance/policy/max-piece.toml @@ -0,0 +1,19 @@ +# Accepted: the longest piece a policy may allow (§4.4): max_ms of ten minutes, which is +# exactly 2^24 units on a timebase of 16777216 units in 600 seconds. +schema = "wai.pack.policy/1" +asset_id = "max-piece" +lead = "v" + +[[component]] +id = "v" +media = "video" +timebase = { num = 16777216, den = 600 } + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" + +[segmentation] +max_ms = 600000 diff --git a/wai/pack-conformance/policy_diff.py b/wai/pack-conformance/policy_diff.py index c0dc9c0f1..9f01e22cf 100644 --- a/wai/pack-conformance/policy_diff.py +++ b/wai/pack-conformance/policy_diff.py @@ -126,11 +126,11 @@ def random_policy(rng): for c in comps: body += ["[[component]]", f'id = "{c}"', "media = " + val(q + biased(["video", "audio", "bogus"]) + q)] if rng.random() > 0.05: - num, den = val(str(biased([25, 0, 48000]))), val(str(biased([1, 1001, 0]))) + num, den = val(str(biased([25, 0, 48000, 4294967295, 16777216]))), val(str(biased([1, 1001, 0, 600]))) body.append("timebase = { num = " + num + ", den = " + den + " }") if rng.random() < 0.6: body.append("[segmentation]") - for k, choices in [("target_ms", [2000, 500, 0]), ("min_ms", [1000, 3000, 0]), ("max_ms", [4000, 100]), + for k, choices in [("target_ms", [2000, 500, 0]), ("min_ms", [1000, 3000, 0]), ("max_ms", [4000, 100, 600000, 600001, 349525]), ("cut_abs_permille", [300, 0, 1001]), ("cut_window", [8, 0, 2, 3, 65]), ("lookahead_frames", [6, 65]), ("closed", ["true", "false"])]: if rng.random() < 0.4: diff --git a/wai/pack-conformance/refusals/expected.json b/wai/pack-conformance/refusals/expected.json index dc59a8eff..b066bda6b 100644 --- a/wai/pack-conformance/refusals/expected.json +++ b/wai/pack-conformance/refusals/expected.json @@ -80,6 +80,14 @@ "file": "policy/open-gop.toml", "reason": "policy-open-gop" }, + { + "file": "policy/piece-bounds-max-ms.toml", + "reason": "policy-piece-bounds" + }, + { + "file": "policy/piece-bounds-units.toml", + "reason": "policy-piece-bounds" + }, { "file": "policy/piece-bounds.toml", "reason": "policy-piece-bounds" diff --git a/wai/pack-conformance/refusals/policy/piece-bounds-max-ms.toml b/wai/pack-conformance/refusals/policy/piece-bounds-max-ms.toml new file mode 100644 index 000000000..0605072d7 --- /dev/null +++ b/wai/pack-conformance/refusals/policy/piece-bounds-max-ms.toml @@ -0,0 +1,18 @@ +# Refused: max_ms past ten minutes (§4.4), the largest allowed plus one. +schema = "wai.pack.policy/1" +asset_id = "r" +lead = "v" + +[[component]] +id = "v" +media = "video" +timebase = { num = 25, den = 1 } + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" + +[segmentation] +max_ms = 600001 diff --git a/wai/pack-conformance/refusals/policy/piece-bounds-units.toml b/wai/pack-conformance/refusals/policy/piece-bounds-units.toml new file mode 100644 index 000000000..a16d7e1c8 --- /dev/null +++ b/wai/pack-conformance/refusals/policy/piece-bounds-units.toml @@ -0,0 +1,17 @@ +# Refused: the default max_ms (4000) is past 2^24 units on a timebase of 2^32 − 1 units a +# second (§4.4). +schema = "wai.pack.policy/1" +asset_id = "r" +lead = "v" + +[[component]] +id = "v" +media = "video" +timebase = { num = 4294967295, den = 1 } + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" + diff --git a/wai/pack-conformance/stage_diff.py b/wai/pack-conformance/stage_diff.py new file mode 100644 index 000000000..75f5386f5 --- /dev/null +++ b/wai/pack-conformance/stage_diff.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +"""The staged packer, differentially: wai-pack against pack_verify.py, which shares no +code with it, on seeded random inputs (extensions/packaging.md §5.4, §5.7, §8.1, §8.9). + +Each case is one line to `wai-pack stage-json` and the same computation in +pack_verify.py, and the two results must be equal: + +- `refine`: a refinement layer (§5.4) over random frames of 1×1 to 7×6, one to four of + them, the stage below near the source or anywhere, at steps from 1 to 255: the WIR1 + payload byte for byte and each reconstructed frame; +- `still`: a still (§5.7) of a random, flat, striped or repeating picture up to 48×40, + and every sixth one a picture 1024 to 10923 columns wide whose rows repeat (some with a + few bytes changed), so the row above is a match one filtered row back: every deflate + distance code from 23 to 29 is used, and at 10923 columns the row above is past the + 32768-byte window. The PNG byte for byte, and its decode. A run with nine or more wide + pictures must use distance codes 23 to 29; +- `plan`: a random policy (bases, refinement chains, a still, classes with rates and + floors, moments, rules with ladder subsets, maximum layers and deadlines, layer lags) + over random pieces with random layer sizes: the policy's verdict, and per piece its + rules and moments, its ladder's rungs, its deadline and every class's plan (§3.4, + §5.7, §8.1); +- `deliver`: random publications (pieces, layers, sizes, deadlines, the frame each is + published after) under a random link, start-up, encoder delay, cap and schedule: every + piece's outcome and the byte counts (§8.9), pack_verify.py running sim_verify.py's + simulator. + + python3 stage_diff.py [seed] [count] + +Exit status 0 only if every case agrees. +""" +import hashlib +import itertools +import json +import os +import random +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import pack_verify as PV # noqa: E402 + + +def frames(rng, n, size): + return [[rng.randrange(256) for _ in range(size)] for _ in range(n)] + + +def refine_case(rng): + h, w, n = rng.randint(1, 6), rng.randint(1, 7), rng.randint(1, 4) + src = frames(rng, n, 3 * w * h) + if rng.random() < 0.6: + spread = rng.choice([1, 3, 12, 40]) + below = [[min(255, max(0, x + rng.randint(-spread, spread))) for x in f] for f in src] + else: + below = frames(rng, n, 3 * w * h) + q = rng.choice([1, 2, 3, 5, 7, 12, 16, 64, 255]) + return {"op": "refine", "h": h, "w": w, "q": q, "below": below, "src": src} + + +def refine_py(c): + payload, recon = PV.wir1_layer([bytes(f) for f in c["below"]], [bytes(f) for f in c["src"]], c["h"], c["w"], c["q"]) + return {"payload": payload.hex(), "recon": [hashlib.sha256(f).hexdigest() for f in recon]} + + +# Widths whose filtered row (3·w + 1 bytes) is a distance of codes 23, 24, 25, 26, 27, +# 28, 29 and 29 (RFC 1951 §3.2.5), and one past the 32768-byte window. +WIDE = [1024, 1366, 2048, 2731, 4096, 5462, 8192, 10922, 10923] +STILLS = itertools.count() +DISTANCE_CODES = set() + + +def wide_still_case(rng, w): + h = rng.randint(2, 3) + row = [rng.randrange(256) for _ in range(3 * w)] + rgb = [] + for _ in range(h): + r = list(row) + for _ in range(rng.choice([0, 0, 1, 5])): + r[rng.randrange(len(r))] = rng.randrange(256) + rgb += r + return {"op": "still", "w": w, "h": h, "rgb": rgb} + + +def still_case(rng): + k = next(STILLS) + if k % 6 == 5: + return wide_still_case(rng, WIDE[(k // 6) % len(WIDE)]) + w, h = rng.randint(1, 48), rng.randint(1, 40) + kind = rng.randrange(4) + if kind == 0: + rgb = [rng.randrange(256) for _ in range(3 * w * h)] + elif kind == 1: + v = rng.randrange(256) + rgb = [v] * (3 * w * h) + elif kind == 2: + period = rng.randint(1, 9) + rgb = [(i // period * 37) % 256 for i in range(3 * w * h)] + else: + tile = [rng.randrange(256) for _ in range(rng.randint(1, 20))] + rgb = [tile[i % len(tile)] for i in range(3 * w * h)] + return {"op": "still", "w": w, "h": h, "rgb": rgb} + + +class CountedBits(PV.DeflateBits): + """pack_verify.py's bit writer, noting each distance code (the only 5-bit code).""" + def code(self, c, n): + if n == 5: + DISTANCE_CODES.add(c) + super().code(c, n) + + +PV.DeflateBits = CountedBits + + +def still_py(c): + png = PV.png_still(bytes(c["rgb"]), c["w"], c["h"]) + _, _, back = PV.png_frame(png) + return {"png": png.hex(), "decoded_sha256": hashlib.sha256(back).hexdigest()} + + +KINDS = ["natural", "static", "synthetic", "motion_high"] + + +def policy_text(rng, layers_wanted=None): + """A random policy: one video component, a base and a chain of refinements, maybe a + still, a floor; classes, moments, rules; layer lags and deadlines that may break §3.4.""" + tb = rng.choice([(25, 1), (30000, 1001), (24, 1), (50, 1)]) + depth = layers_wanted if layers_wanted is not None else rng.randint(0, 3) + lines = ['schema = "wai.pack.policy/1"', 'asset_id = "diff"', 'lead = "v"', "", + "[[component]]", 'id = "v"', 'media = "video"', "", "[component.timebase]", f"num = {tb[0]}", f"den = {tb[1]}", ""] + rungs = [("b0", "wai.video.int_motion", "base", None, "")] + prev = "b0" + for k in range(depth): + rungs.append((f"r{k + 1}", "wai.video.int_refine", "refinement", prev, f"params = {{ q = {rng.randint(1, 255)} }}")) + prev = f"r{k + 1}" + still = rng.random() < 0.5 + if still: + rungs.append(("still", "wai.image.png", "base", None, "")) + rungs.append(("f0", "wai.video.av1", "floor", None, "")) + if rng.random() < 0.15: + rng.shuffle(rungs) + for rid, cap, role, ref, params in rungs: + lines += ["[[ladder]]", f'id = "{rid}"', 'component = "v"', f'capability = "{cap}"', f'role = "{role}"'] + if ref: + lines.append(f'refines = "{ref}"') + if params: + lines.append(params) + lines.append("") + lag, x = [0], 0 + for _ in range(depth): + x += rng.choice([0, 0, 100, 200, 400]) + lag.append(x) + if rng.random() < 0.1: + lag = lag[:-1] or [0, 0] + lines += ["[live]", f"layer_lag_ms = {lag}", ""] + if rng.random() < 0.7: + lines += ["[defaults]", f"deadline_ms = {rng.choice([300, 800, 1500, 2000, 401])}", ""] + for i in range(rng.randint(0, 3)): + lines += ["[[class]]", f'id = "c{i}"', f"max_layer = {rng.randint(0, depth)}"] + if rng.random() < 0.5: + lines.append(f"max_kbps = {rng.choice([1, 5, 50, 200, 1000, 50000])}") + if rng.random() < 0.2: + lines.append("floor_only = true") + lines.append("") + tags = ["a", "b", "event"] + for i in range(rng.randint(0, 3)): + lo = rng.randint(0, 8000) + lines += ["[[moment]]", f'id = "m{i}"', f"from_ms = {lo}", f"to_ms = {lo + rng.randint(1, 4000)}", + f"tags = {json.dumps(rng.sample(tags, rng.randint(0, 2)))}", ""] + ids = [r[0] for r in rungs] + for i in range(rng.randint(0, 4)): + when = [] + if rng.random() < 0.6: + when.append(f"piece_kind = {json.dumps(rng.sample(KINDS, rng.randint(1, 2)))}") + if rng.random() < 0.4: + when.append(f"moment_tag = {json.dumps(rng.sample(tags, rng.randint(1, 2)))}") + then = [] + r = rng.random() + if r < 0.35: + if still and rng.random() < 0.5: + lad = ["still", "f0"] + else: + lad = ["b0"] + [f"r{k + 1}" for k in range(rng.randint(0, depth))] + ["f0"] + if rng.random() < 0.15: + lad = rng.sample(ids, rng.randint(1, len(ids))) + then.append(f"ladder = {json.dumps(lad)}") + if rng.random() < 0.4: + then.append(f"max_layer = {rng.randint(0, depth)}") + if rng.random() < 0.4: + then.append(f"deadline_ms = {rng.choice([300, 800, 1500, 600, 2])}") + lines += ["[[rule]]", f'id = "u{i}"', f"when = {{ {', '.join(when)} }}", f"then = {{ {', '.join(then)} }}", ""] + return "\n".join(lines) + "\n", tb, depth + + +def plan_case(rng): + text, tb, depth = policy_text(rng) + pieces, layer_bytes, start = [], [], 0 + for _ in range(rng.randint(1, 6)): + d = rng.randint(1, 120) + pieces.append({"start": start, "duration": d, "kind": rng.randrange(4)}) + layer_bytes.append([rng.randint(1, 60000) for _ in range(rng.randint(1, depth + 1))]) + start += d + return {"op": "plan", "policy": text, "pieces": pieces, "layer_bytes": layer_bytes} + + +def plan_py(c): + try: + P = PV.parse_policy(c["policy"]) + except PV.Refused as e: + return {"verdict": e.code} + comp = P["component"][0] + tb = (comp["timebase"]["num"], comp["timebase"]["den"]) + # A stage index in miniature: each piece's layers, one entry each, of the given sizes. + entries, ps = [], [] + for pc, lb in zip(c["pieces"], c["layer_bytes"]): + ps.append(dict(pc, first_entry=len(entries), n_entries=len(lb))) + entries += [{"role": PV.BASE if k == 0 else PV.REFINEMENT, "layer": k, "byte_len": b} for k, b in enumerate(lb)] + ix = {"tb": tb, "pieces": ps, "entries": entries} + bits, rows = PV.class_plan(P, ix) + out = [] + for k in range(len(ps)): + _, _, then = PV.piece_then(P, ix, k) + base, chain, floors = PV.piece_ladder(P, comp["id"], then) + out.append({"rules": bits[k][0], "moments": bits[k][1], "ladder": [base["id"]] + [r["id"] for r in chain] + [r["id"] for r in floors], + "deadline_ms": then.get("deadline_ms", P["defaults"]["deadline_ms"]), "class_plan": [row[k] for row in rows]}) + return {"verdict": "ok", "pieces": out} + + +def deliver_case(rng): + depth = rng.randint(0, 2) + while True: + text, tb, _ = policy_text(random.Random(rng.random()), depth) + try: + P = PV.parse_policy(text) + except PV.Refused: + continue + num, den = P["component"][0]["timebase"]["num"], P["component"][0]["timebase"]["den"] + if 1_000_000 * den % num == 0: + break + deadlines = sorted({P["defaults"]["deadline_ms"]} | {r["then"]["deadline_ms"] for r in P["rule"] if "deadline_ms" in r["then"]}) + published, start, n = [], 0, rng.randint(1, 5) + L = P["segmentation"]["lookahead_frames"] + for p in range(n): + d = rng.randint(1, 60) + at = start + d + L if p + 1 < n else start + d - 1 + dl = rng.choice(deadlines) + for k in range(rng.randint(0, depth) + 1): + published.append({"piece": p, "start": start, "duration": d, "at_frame": at, "layer": k, + "deadline_ms": 0 if k == 0 else dl, "bytes": rng.randint(1, 40000)}) + start += d + row = [rng.choice([0, 1, 2, 2, 0xFF]) for _ in range(n)] + plan = {"rtt_us": rng.choice([0, 1, 40000]), "bytes_per_ms": rng.randint(1, 300), "chunk_bytes": rng.choice([1, 100, 1200, 50000]), + "enc_us": rng.choice([0, 1000, 33000]), "startup_us": rng.randint(0, 6_000_000), + "layer_cap": rng.choice([None, None, 0, 1]), "fifo": rng.random() < 0.3} + return {"op": "deliver", "policy": text, "class_plan": row, "published": published, "plan": plan} + + +def deliver_py(c): + P = PV.parse_policy(c["policy"]) + return PV.deliver(P, c["class_plan"], c["published"], c["plan"]) + + +def main(argv): + if len(argv) < 2: + print(__doc__) + return 2 + binary = argv[1] + seed = int(argv[2]) if len(argv) > 2 else 1 + count = int(argv[3]) if len(argv) > 3 else 1000 + rng = random.Random(seed) + makers = [(refine_case, refine_py), (still_case, still_py), (plan_case, plan_py), (deliver_case, deliver_py)] + cases = [] + for i in range(count): + make, py = makers[i % len(makers)] + cases.append((make(rng), py)) + lines = "".join(json.dumps(c) + "\n" for c, _ in cases) + out = subprocess.run([binary, "stage-json"], input=lines.encode(), capture_output=True, check=True).stdout.decode().splitlines() + if len(out) != len(cases): + print(f"FAIL wai-pack answered {len(out)} of {len(cases)} lines") + return 1 + fails, seen = 0, {} + for (c, py), got in zip(cases, out): + rust = json.loads(got) + mine = py(c) + seen[c["op"]] = seen.get(c["op"], 0) + 1 + if "verdict" in mine and mine["verdict"] != "ok": + seen["refused"] = seen.get("refused", 0) + 1 + if rust != mine: + fails += 1 + if fails <= 5: + print(f"FAIL {c['op']}: {json.dumps(c)[:300]}\n wai-pack: {json.dumps(rust)[:400]}\n pack_verify.py: {json.dumps(mine)[:400]}") + wide = sum(1 for c, _ in cases if c["op"] == "still" and c["w"] >= 1024) + missing = sorted(set(range(23, 30)) - DISTANCE_CODES) + if wide >= len(WIDE) and missing: + fails += 1 + print(f"FAIL {wide} wide stills, and distance codes {missing} never used") + print(f"stage_diff: {len(cases) - fails}/{len(cases)} agree ({', '.join(f'{k} {v}' for k, v in sorted(seen.items()))}; " + f"{wide} stills 1024 or more wide, distance codes {min(DISTANCE_CODES, default='-')} to {max(DISTANCE_CODES, default='-')} used)") + return 0 if fails == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/wai/pack-conformance/staged-mutations/expected.json b/wai/pack-conformance/staged-mutations/expected.json new file mode 100644 index 000000000..cbdafd185 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/expected.json @@ -0,0 +1,90 @@ +{ + "pack": "video-staged-01", + "cases": [ + { + "name": "refinement-deadline", + "reason": "index-policy-mismatch", + "remove": [] + }, + { + "name": "layer-dropped", + "reason": "index-policy-mismatch", + "remove": [ + "objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai" + ] + }, + { + "name": "still-not-the-ladder", + "reason": "index-policy-mismatch", + "remove": [] + }, + { + "name": "layer-replaced", + "reason": "prefix-digest", + "remove": [ + "objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai" + ] + }, + { + "name": "piece-past-max", + "reason": "index-policy-mismatch", + "remove": [] + }, + { + "name": "still-past-max", + "reason": "index-policy-mismatch", + "remove": [ + "floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s", + "floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s", + "objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai", + "objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai", + "objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai", + "objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai" + ] + }, + { + "name": "max-ms-past-cap", + "reason": "policy-piece-bounds", + "remove": [] + }, + { + "name": "still-at-cap", + "reason": "floor-object-mismatch", + "remove": [ + "floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s", + "floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s", + "objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai", + "objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai", + "objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai", + "objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai", + "objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai" + ] + }, + { + "name": "still-manifest-target", + "reason": "object-manifest", + "remove": [ + "objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai" + ] + }, + { + "name": "refinement-manifest-target", + "reason": "object-manifest", + "remove": [ + "objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai" + ] + }, + { + "name": "still-incomplete-dist", + "reason": "prefix-digest", + "remove": [ + "objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai" + ] + }, + { + "name": "still-held-digest", + "reason": "prefix-digest", + "remove": [] + } + ] +} diff --git a/wai/pack-conformance/staged-mutations/layer-dropped/LISTING b/wai/pack-conformance/staged-mutations/layer-dropped/LISTING new file mode 100644 index 000000000..497bde805 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-dropped/LISTING @@ -0,0 +1,40 @@ +f9359ffd69743b04bc491f9fe32e6bac50a3f2e6c9ec9e747b0f16572bea0dec dash/classic.mpd +e3f5ca3e780c57e10b71b30b644dd5c488f85cc11cb12557c75307ea6590d657 dash/mobile.mpd +6329f7722e2cfd5b690ae3e8761f626b9a513f0525ab78b1d520222f8560f497 dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362 floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +b3a3be45bb384a06e744c8c2293ba59e16738791abe3ac90b262c2db9a0c5744 hls/classic/floor-v.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/classic/master.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/legacy/master.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/mobile/master.m3u8 +1b7bf011b974365e6bc6532de6d0c54fa9b1447f75c84f3c52d02e1c433e146a hls/mobile/wai-v.m3u8 +adce889f610eaa20b77909693de01dfae787518748223b0774cd56004e0c9353 hls/mobile/wai.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/tv/master.m3u8 +9a3c159f30e372273ae966c3eea837c9539bf66dff217d8e1cc4432002a203ea hls/tv/wai-v.m3u8 +d6dc24d2117d3183179ba84cf134d84b27e1b5e410620921ec56e353f7332af5 hls/tv/wai.m3u8 +a1ecbd6ea4c08332f6ae0465791551f4371a4cbf4aa1c45f4674062e92631673 index-v.wsix +8ae5eb928b7f51890d539d4db18556cb9e6f61e98e3630eb064074bf97225be0 moq/catalog.json +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +849b7e2cba0f65d3183119f6f4f2c77e5c2a1db9a38d8552369db2dec28df7a7 objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai +22c6777c989d288a6a89876062a244194fde54f5b091b0f603c4ef11f1bc4f60 objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai +83657a16f8686cb03f1da0b5fe3a60ad24b32e1b3f24c17e5b9e5e54720571be objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai +38725131c01f75141fe1790d40209348da7f45241df0bf82a794e8c8196a0fb6 objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +908d00b2cef08fc0e207a6aabf68fc4ca4b42b1f13afe3b8ae72eda6645c1d43 package.receipt.json +8454596c213291b896c4483017406c58076e27ad0cab4fab54b850841dfa1a29 plan-v.txt +11536aab05b3fe23aa428a52617da9d156f6326d42ee5b8785af4713b436ecf2 policy.json +61ad0d0ba75977d401ebaeb00b5a8f5795f3b7a1408018fcbb02038f2c412e16 policy.toml +703346f293954ae90516c7cf1a8616e6851f3dba3ca3f51967b388493b1736ff stages/legacy.json +b8beb6eaa1c5a04801b0dcbcbd7f0885f5b0730871708ce0a59093e3c8ea1fdb stages/mobile.json +be4045b592cd50c23581bfb69bd09506e826434c7f02d71e18cb207e5ae415d2 stages/tv.json diff --git a/wai/pack-conformance/staged-mutations/layer-dropped/dash/tv.mpd b/wai/pack-conformance/staged-mutations/layer-dropped/dash/tv.mpd new file mode 100644 index 000000000..daf6ee14a --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-dropped/dash/tv.mpd @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/staged-mutations/layer-dropped/hls/tv/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/layer-dropped/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..222d09fb3 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-dropped/hls/tv/wai-v.m3u8 @@ -0,0 +1,24 @@ +#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.000, +#WAI-LAYER:N=1,URI="../../objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai#n=67779" +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai#n=974 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXTINF:2.400, +#WAI-LAYER:N=1,URI="../../objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai#n=81407" +../../objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai#n=31610 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/layer-dropped/hls/tv/wai.m3u8 b/wai/pack-conformance/staged-mutations/layer-dropped/hls/tv/wai.m3u8 new file mode 100644 index 000000000..f463c8ca3 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-dropped/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=645600,AVERAGE-BANDWIDTH=460283,CODECS="wai1.video.int_motion+video.int_refine",X-WAI-FLOOR="wai1.video.int_motion+video.int_refine",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/layer-dropped/index-v.wsix b/wai/pack-conformance/staged-mutations/layer-dropped/index-v.wsix new file mode 100644 index 000000000..8c2b105c4 Binary files /dev/null and b/wai/pack-conformance/staged-mutations/layer-dropped/index-v.wsix differ diff --git a/wai/pack-conformance/staged-mutations/layer-dropped/moq/catalog.json b/wai/pack-conformance/staged-mutations/layer-dropped/moq/catalog.json new file mode 100644 index 000000000..99b1979ff --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-dropped/moq/catalog.json @@ -0,0 +1 @@ +{"tracks":[{"altGroup":1,"isLive":false,"name":"v/l0","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_motion","wai.image.png"],"wai_determinism":"decode-equivalence","wai_receipt_alias":6826975590310646,"wai_receipts":true,"wai_receipts_track":"v/l0/receipts","wai_staged":{"layer":0,"lead_ms":null,"set":"v"}},{"depends":["v/l0"],"isLive":false,"name":"v/l1@800","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":600,"wai_determinism":"decode-equivalence","wai_receipt_alias":4051354593871184,"wai_receipts":true,"wai_receipts_track":"v/l1@800/receipts","wai_staged":{"layer":1,"lead_ms":null,"set":"v"}},{"depends":["v/l0"],"isLive":false,"name":"v/l1","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":1300,"wai_determinism":"decode-equivalence","wai_receipt_alias":5745064158311842,"wai_receipts":true,"wai_receipts_track":"v/l1/receipts","wai_staged":{"layer":1,"lead_ms":null,"set":"v"}},{"depends":["v/l0","v/l1"],"isLive":false,"name":"v/l2","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":1100,"wai_determinism":"decode-equivalence","wai_receipt_alias":6697012243957121,"wai_receipts":true,"wai_receipts_track":"v/l2/receipts","wai_staged":{"layer":2,"lead_ms":null,"set":"v"}},{"altGroup":1,"codec":"av01.0.00M.08","height":36,"name":"v/floor","namespace":"video-staged-01","packaging":"cmaf","renderGroup":1,"width":64},{"isLive":false,"name":"v/l0/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l1@800/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l1/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l2/receipts","namespace":"video-staged-01","packaging":"wai-receipts"}],"version":1} diff --git a/wai/pack-conformance/staged-mutations/layer-dropped/package.receipt.json b/wai/pack-conformance/staged-mutations/layer-dropped/package.receipt.json new file mode 100644 index 000000000..4d0a8277e --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-dropped/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"d575f6a574a1071e88402aba762f31ad28d60f8986d03561b02a621319883549","media_class":"video","stage_root":"2f036a0a49ba5f09ef0a2aaaecabdb8ac179706fbe03427968a53e9bc5b7ecfa"}],"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":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"86dd2e2e75bc342bae920fef1cb0eb60f113a72753146f979cb5f6285ca3d35b","sig":"2221ea6e710b83c81a1e28db737c2ebd331f7052054d603e2a236871d74b14e8730690d2b886e03346eceb7a52c898c3143d617864edea4e2abddb6a36ae080b","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":383169,"total_bytes":465103,"total_entries":17,"unique_objects":13,"version":2,"work_units":17} \ No newline at end of file diff --git a/wai/pack-conformance/staged-mutations/layer-dropped/stages/tv.json b/wai/pack-conformance/staged-mutations/layer-dropped/stages/tv.json new file mode 100644 index 000000000..7fcf2cfd4 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-dropped/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0},{"bytes":67779,"deadline_ms":1500,"h":"7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec","layer":1,"refines":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27"}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":974,"h":"8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":115},{"dur":60,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0},{"bytes":81407,"deadline_ms":800,"h":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d","layer":1,"refines":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9"}],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/layer-replaced/LISTING b/wai/pack-conformance/staged-mutations/layer-replaced/LISTING new file mode 100644 index 000000000..b95ddbfec --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-replaced/LISTING @@ -0,0 +1,41 @@ +f9359ffd69743b04bc491f9fe32e6bac50a3f2e6c9ec9e747b0f16572bea0dec dash/classic.mpd +e3f5ca3e780c57e10b71b30b644dd5c488f85cc11cb12557c75307ea6590d657 dash/mobile.mpd +9f6fa5094b9f075b1bdbc6f22f89c3208f9108958fb45adcf9b7ea60ac24f7a8 dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362 floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +b3a3be45bb384a06e744c8c2293ba59e16738791abe3ac90b262c2db9a0c5744 hls/classic/floor-v.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/classic/master.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/legacy/master.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/mobile/master.m3u8 +1b7bf011b974365e6bc6532de6d0c54fa9b1447f75c84f3c52d02e1c433e146a hls/mobile/wai-v.m3u8 +adce889f610eaa20b77909693de01dfae787518748223b0774cd56004e0c9353 hls/mobile/wai.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/tv/master.m3u8 +ec09c87f857b5d0eecaa0b50e728938696896d514ac5e7a66aaa3da10aaebe3a hls/tv/wai-v.m3u8 +cf1448779f95e872d6799e969f904c4fcf5a75bc189af32656c04936000eddc4 hls/tv/wai.m3u8 +328c5486665e380888f13951172882a68f1d97190c16e76f1c7f013b25838f39 index-v.wsix +0387c4128de60831575723b868864c82f9d8d129aacec6ad8797db5ca5e5acf4 moq/catalog.json +640bbd18bfdbfe43041201d54647bb3b89dddac3d827c3b9558d6f4c1f7aa9ef objects/2a61c2099ae5ac83c3da578d33dc965806c5323fa223e0f5e7da666a46fd4e3f.wai +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +849b7e2cba0f65d3183119f6f4f2c77e5c2a1db9a38d8552369db2dec28df7a7 objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai +22c6777c989d288a6a89876062a244194fde54f5b091b0f603c4ef11f1bc4f60 objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai +83657a16f8686cb03f1da0b5fe3a60ad24b32e1b3f24c17e5b9e5e54720571be objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai +38725131c01f75141fe1790d40209348da7f45241df0bf82a794e8c8196a0fb6 objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +7913d4989c560605b6c0ebf8e1d881caa3e9bc14c67748b2fa69c8622b53382f package.receipt.json +8454596c213291b896c4483017406c58076e27ad0cab4fab54b850841dfa1a29 plan-v.txt +11536aab05b3fe23aa428a52617da9d156f6326d42ee5b8785af4713b436ecf2 policy.json +61ad0d0ba75977d401ebaeb00b5a8f5795f3b7a1408018fcbb02038f2c412e16 policy.toml +703346f293954ae90516c7cf1a8616e6851f3dba3ca3f51967b388493b1736ff stages/legacy.json +b8beb6eaa1c5a04801b0dcbcbd7f0885f5b0730871708ce0a59093e3c8ea1fdb stages/mobile.json +fc65cfd8e8725ede3ac50720908d30363821a75e8bb994e8ae2cafc7c7aea9a1 stages/tv.json diff --git a/wai/pack-conformance/staged-mutations/layer-replaced/dash/tv.mpd b/wai/pack-conformance/staged-mutations/layer-replaced/dash/tv.mpd new file mode 100644 index 000000000..a1defddea --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-replaced/dash/tv.mpd @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/staged-mutations/layer-replaced/hls/tv/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/layer-replaced/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..783566ec0 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-replaced/hls/tv/wai-v.m3u8 @@ -0,0 +1,25 @@ +#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.000, +#WAI-LAYER:N=1,URI="../../objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai#n=67779" +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai#n=974 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXTINF:2.400, +#WAI-LAYER:N=1,URI="../../objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai#n=81407" +#WAI-LAYER:N=2,URI="../../objects/2a61c2099ae5ac83c3da578d33dc965806c5323fa223e0f5e7da666a46fd4e3f.wai#n=82235" +../../objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai#n=31610 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/layer-replaced/hls/tv/wai.m3u8 b/wai/pack-conformance/staged-mutations/layer-replaced/hls/tv/wai.m3u8 new file mode 100644 index 000000000..6e46a0c48 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-replaced/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=650840,AVERAGE-BANDWIDTH=563077,CODECS="wai1.video.int_motion+video.int_refine",X-WAI-FLOOR="wai1.video.int_motion+video.int_refine",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/layer-replaced/index-v.wsix b/wai/pack-conformance/staged-mutations/layer-replaced/index-v.wsix new file mode 100644 index 000000000..1c220dfcd Binary files /dev/null and b/wai/pack-conformance/staged-mutations/layer-replaced/index-v.wsix differ diff --git a/wai/pack-conformance/staged-mutations/layer-replaced/objects/2a61c2099ae5ac83c3da578d33dc965806c5323fa223e0f5e7da666a46fd4e3f.wai b/wai/pack-conformance/staged-mutations/layer-replaced/objects/2a61c2099ae5ac83c3da578d33dc965806c5323fa223e0f5e7da666a46fd4e3f.wai new file mode 100644 index 000000000..f41e72b3c Binary files /dev/null and b/wai/pack-conformance/staged-mutations/layer-replaced/objects/2a61c2099ae5ac83c3da578d33dc965806c5323fa223e0f5e7da666a46fd4e3f.wai differ diff --git a/wai/pack-conformance/staged-mutations/layer-replaced/package.receipt.json b/wai/pack-conformance/staged-mutations/layer-replaced/package.receipt.json new file mode 100644 index 000000000..61b0b98a2 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-replaced/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"26a2ab38aa3a111f4e20b002d4865dee22979a45e05ac8dc87a85ef0dc5eaaee","media_class":"video","stage_root":"ee8c7f3bc403737be16d1c2ef468dfc2501edf038a74d5c3bbfba6f90fa3c486"}],"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":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"293d85123e773ced00d6e321f3c19bd4eab627a8fea5bd253c61b36b3a70c218","sig":"bf6825f0958e47597d32de456994434b36bd4df14f0bfe9d8af74ba5f72b1960a01a647a20d97120e456c2c62803e091bf52b27c3cb0bbe8fa24104e01b6f40a","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":465404,"total_bytes":547338,"total_entries":18,"unique_objects":14,"version":2,"work_units":18} \ No newline at end of file diff --git a/wai/pack-conformance/staged-mutations/layer-replaced/stages/tv.json b/wai/pack-conformance/staged-mutations/layer-replaced/stages/tv.json new file mode 100644 index 000000000..bcd95661d --- /dev/null +++ b/wai/pack-conformance/staged-mutations/layer-replaced/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0},{"bytes":67779,"deadline_ms":1500,"h":"7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec","layer":1,"refines":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27"}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":974,"h":"8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":115},{"dur":60,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0},{"bytes":81407,"deadline_ms":800,"h":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d","layer":1,"refines":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9"},{"bytes":82235,"deadline_ms":800,"h":"2a61c2099ae5ac83c3da578d33dc965806c5323fa223e0f5e7da666a46fd4e3f","layer":2,"refines":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d"}],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/max-ms-past-cap/LISTING b/wai/pack-conformance/staged-mutations/max-ms-past-cap/LISTING new file mode 100644 index 000000000..a4821b213 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/max-ms-past-cap/LISTING @@ -0,0 +1,41 @@ +f9359ffd69743b04bc491f9fe32e6bac50a3f2e6c9ec9e747b0f16572bea0dec dash/classic.mpd +e3f5ca3e780c57e10b71b30b644dd5c488f85cc11cb12557c75307ea6590d657 dash/mobile.mpd +825126074444a4419069e844ae4a0b9dcb02a2ef5830168e4e77967de9df710a dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362 floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +b3a3be45bb384a06e744c8c2293ba59e16738791abe3ac90b262c2db9a0c5744 hls/classic/floor-v.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/classic/master.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/legacy/master.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/mobile/master.m3u8 +1b7bf011b974365e6bc6532de6d0c54fa9b1447f75c84f3c52d02e1c433e146a hls/mobile/wai-v.m3u8 +adce889f610eaa20b77909693de01dfae787518748223b0774cd56004e0c9353 hls/mobile/wai.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/tv/master.m3u8 +1ea26eaea415f8b459f5994a950e14016a501c2464b9ed8d77db800e16da31d2 hls/tv/wai-v.m3u8 +fc9db3d8e327724c8a7ee793d4b1f1949cf95df1c1941bfcc2bf539fe4184e77 hls/tv/wai.m3u8 +424e86d090780853634a8b95c8a75e9a012fc28e74e0fede44127823cc3e5efe index-v.wsix +0387c4128de60831575723b868864c82f9d8d129aacec6ad8797db5ca5e5acf4 moq/catalog.json +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +849b7e2cba0f65d3183119f6f4f2c77e5c2a1db9a38d8552369db2dec28df7a7 objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai +22c6777c989d288a6a89876062a244194fde54f5b091b0f603c4ef11f1bc4f60 objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai +83657a16f8686cb03f1da0b5fe3a60ad24b32e1b3f24c17e5b9e5e54720571be objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai +38725131c01f75141fe1790d40209348da7f45241df0bf82a794e8c8196a0fb6 objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +e191fd2fe53e15c30c964e355340cbfbb44562078d6c18c8321c9d3539c43423 objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +6204e0570e77912b82c4b0bbbb51d3612334367b2ab62021a93578043e296ec9 package.receipt.json +8454596c213291b896c4483017406c58076e27ad0cab4fab54b850841dfa1a29 plan-v.txt +11536aab05b3fe23aa428a52617da9d156f6326d42ee5b8785af4713b436ecf2 policy.json +36b5c54344949f8fda383212f94d78cabadcaf22be64b30f1ed8544c1a247588 policy.toml +703346f293954ae90516c7cf1a8616e6851f3dba3ca3f51967b388493b1736ff stages/legacy.json +b8beb6eaa1c5a04801b0dcbcbd7f0885f5b0730871708ce0a59093e3c8ea1fdb stages/mobile.json +9c4eda5ea5972cbca98b3b89503235fcd807450c129545ca86c322337e27d872 stages/tv.json diff --git a/wai/pack-conformance/staged-mutations/max-ms-past-cap/policy.toml b/wai/pack-conformance/staged-mutations/max-ms-past-cap/policy.toml new file mode 100644 index 000000000..9dd229e9e --- /dev/null +++ b/wai/pack-conformance/staged-mutations/max-ms-past-cap/policy.toml @@ -0,0 +1,94 @@ +schema = "wai.pack.policy/1" +asset_id = "video-staged-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-staged-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 600001 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [24, 16], max_sse_px_milli = 60000 } + +[[ladder]] +id = "r1" +component = "v" +capability = "wai.video.int_refine" +role = "refinement" +refines = "b0" +params = { q = 12 } + +[[ladder]] +id = "r2" +component = "v" +capability = "wai.video.int_refine" +role = "refinement" +refines = "r1" +params = { q = 4 } + +[[ladder]] +id = "still" +component = "v" +capability = "wai.image.png" +role = "base" + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "f00d8bf055ef725d99f925cc0c1c97b6b7fc956c82872f63840c246b4b69e150" + +[live] +layer_lag_ms = [0, 200, 400] + +[[class]] +id = "tv" +max_layer = 2 + +[[class]] +id = "mobile" +max_layer = 1 +max_kbps = 330 + +[[class]] +id = "legacy" +floor_only = true + +[[moment]] +id = "m-event" +from_ms = 5600 +to_ms = 8000 +tags = ["event"] + +[[rule]] +id = "static" +when = { piece_kind = ["static"] } +then = { ladder = ["still", "f0"] } + +[[rule]] +id = "motion" +when = { piece_kind = ["motion_high"] } +then = { max_layer = 1 } + +[[rule]] +id = "event" +when = { moment_tag = ["event"] } +then = { deadline_ms = 800 } diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/LISTING b/wai/pack-conformance/staged-mutations/piece-past-max/LISTING new file mode 100644 index 000000000..4574e1f47 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/LISTING @@ -0,0 +1,41 @@ +563e5378ed4d13a3a3cb11aab1873afbe07ab8adecdac26f2468702be55ad9e1 dash/classic.mpd +c0e75fa3880482dc7fb86d53e8847e25ce33d0103ceae62a15dbf4bc0cf6bc3a dash/mobile.mpd +f214f0c0583980e1fff9b6d96dc17743190e85cf875ebeadb6df435926764a61 dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362 floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +2a696d51aac0a2865bb265ccb3029c13a3933f80c09a9f7c9083582e358ff409 hls/classic/floor-v.m3u8 +04281fde53ffef78936bb5b8d7228671ff20cca068cd745c03ab2cbd16b16522 hls/classic/master.m3u8 +04281fde53ffef78936bb5b8d7228671ff20cca068cd745c03ab2cbd16b16522 hls/legacy/master.m3u8 +24cb2a192f121d6a9c086f59a2750bf8a578319fe1bb04acac25e3de9a3921b7 hls/mobile/master.m3u8 +9c80c3469829f2f7d2639a30124d233cc252c117e14f348e9b6aa991824c5ec6 hls/mobile/wai-v.m3u8 +6808dbfba087fe644fd7e73321ea66a5e1351daa49b877d6489f8ac7ab3ab98f hls/mobile/wai.m3u8 +24cb2a192f121d6a9c086f59a2750bf8a578319fe1bb04acac25e3de9a3921b7 hls/tv/master.m3u8 +099ea6ee49bc03f2bb5dc8c0a5f679ad7c9e294e95007976a1ad858d3573b336 hls/tv/wai-v.m3u8 +162024e0bb15e0e744fa46f25cc9ba8cf3da7eb66dd5625e0b672f0b74a48b80 hls/tv/wai.m3u8 +22eff3b157e4cdf5a2e350ec5957ac18b43978568d3ae4119d36611ee0dccb7c index-v.wsix +0387c4128de60831575723b868864c82f9d8d129aacec6ad8797db5ca5e5acf4 moq/catalog.json +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +849b7e2cba0f65d3183119f6f4f2c77e5c2a1db9a38d8552369db2dec28df7a7 objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai +22c6777c989d288a6a89876062a244194fde54f5b091b0f603c4ef11f1bc4f60 objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai +83657a16f8686cb03f1da0b5fe3a60ad24b32e1b3f24c17e5b9e5e54720571be objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai +38725131c01f75141fe1790d40209348da7f45241df0bf82a794e8c8196a0fb6 objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +e191fd2fe53e15c30c964e355340cbfbb44562078d6c18c8321c9d3539c43423 objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +2295a23169c0307290385a1cbb1541b6de760b3fc0880f84eff1d01a29729d55 package.receipt.json +8454596c213291b896c4483017406c58076e27ad0cab4fab54b850841dfa1a29 plan-v.txt +11536aab05b3fe23aa428a52617da9d156f6326d42ee5b8785af4713b436ecf2 policy.json +61ad0d0ba75977d401ebaeb00b5a8f5795f3b7a1408018fcbb02038f2c412e16 policy.toml +81e00b0beb6ac3c0970cdd19cffb21f68f582c80195625b6ba72b82cd91447a2 stages/legacy.json +2f8dbfbfd553817137d59b595b335413a1f82947cb9c9678128f0705b7f3dee3 stages/mobile.json +2654c4b1f0e81d8b1295720e70369f9344a10257fca1852a971b3acb19567c6d stages/tv.json diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/dash/classic.mpd b/wai/pack-conformance/staged-mutations/piece-past-max/dash/classic.mpd new file mode 100644 index 000000000..21aed37ec --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/dash/classic.mpd @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/dash/mobile.mpd b/wai/pack-conformance/staged-mutations/piece-past-max/dash/mobile.mpd new file mode 100644 index 000000000..0d0aa3ed8 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/dash/mobile.mpd @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/dash/tv.mpd b/wai/pack-conformance/staged-mutations/piece-past-max/dash/tv.mpd new file mode 100644 index 000000000..4da20fabe --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/dash/tv.mpd @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/hls/classic/floor-v.m3u8 b/wai/pack-conformance/staged-mutations/piece-past-max/hls/classic/floor-v.m3u8 new file mode 100644 index 000000000..e243737b1 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/hls/classic/floor-v.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:171798692 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/v/init.mp4" +#EXTINF:2.000, +../../floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +#EXTINF:1.000, +../../floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +#EXTINF:1.600, +../../floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +#EXTINF:1.000, +../../floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +#EXTINF:171798691.800, +../../floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/hls/classic/master.m3u8 b/wai/pack-conformance/staged-mutations/piece-past-max/hls/classic/master.m3u8 new file mode 100644 index 000000000..0a72b1fee --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/hls/classic/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=9120,AVERAGE-BANDWIDTH=1,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/hls/legacy/master.m3u8 b/wai/pack-conformance/staged-mutations/piece-past-max/hls/legacy/master.m3u8 new file mode 100644 index 000000000..0a72b1fee --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/hls/legacy/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=9120,AVERAGE-BANDWIDTH=1,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/hls/mobile/master.m3u8 b/wai/pack-conformance/staged-mutations/piece-past-max/hls/mobile/master.m3u8 new file mode 100644 index 000000000..e5acfa27a --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/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=9120,AVERAGE-BANDWIDTH=1,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/hls/mobile/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/piece-past-max/hls/mobile/wai-v.m3u8 new file mode 100644 index 000000000..f71957793 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/hls/mobile/wai-v.m3u8 @@ -0,0 +1,21 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:171798692 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:2.000, +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai#n=974 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXTINF:171798691.800, +#WAI-LAYER:N=1,URI="../../objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai#n=81407" +../../objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai#n=31610 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/hls/mobile/wai.m3u8 b/wai/pack-conformance/staged-mutations/piece-past-max/hls/mobile/wai.m3u8 new file mode 100644 index 000000000..4b5404564 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/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=323176,AVERAGE-BANDWIDTH=1,CODECS="wai1.video.int_motion+video.int_refine",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/hls/tv/master.m3u8 b/wai/pack-conformance/staged-mutations/piece-past-max/hls/tv/master.m3u8 new file mode 100644 index 000000000..e5acfa27a --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/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=9120,AVERAGE-BANDWIDTH=1,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/hls/tv/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/piece-past-max/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..090805eba --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/hls/tv/wai-v.m3u8 @@ -0,0 +1,25 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:171798692 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:2.000, +#WAI-LAYER:N=1,URI="../../objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai#n=67779" +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai#n=974 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXTINF:171798691.800, +#WAI-LAYER:N=1,URI="../../objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai#n=81407" +#WAI-LAYER:N=2,URI="../../objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai#n=101295" +../../objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai#n=31610 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/hls/tv/wai.m3u8 b/wai/pack-conformance/staged-mutations/piece-past-max/hls/tv/wai.m3u8 new file mode 100644 index 000000000..68175af41 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/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=645600,AVERAGE-BANDWIDTH=1,CODECS="wai1.video.int_motion+video.int_refine",X-WAI-FLOOR="wai1.video.int_motion+video.int_refine",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/index-v.wsix b/wai/pack-conformance/staged-mutations/piece-past-max/index-v.wsix new file mode 100644 index 000000000..799dfc3c1 Binary files /dev/null and b/wai/pack-conformance/staged-mutations/piece-past-max/index-v.wsix differ diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/package.receipt.json b/wai/pack-conformance/staged-mutations/piece-past-max/package.receipt.json new file mode 100644 index 000000000..285beb81f --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"e5fbd69479a0dac74d364b2c8d144dc7cc137310b232bbae5e5ffdfa762de852","media_class":"video","stage_root":"c10304943d547971b05fdb57231dcd6e097d7df38cc68ea92c376ddc957e022d"}],"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":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"6914d6429b5e8da3e847fb266a60ec6213c7b9f3689b9cd3c5682db7bba8c440","sig":"593b61a457e37d4d7a3fd1b39f52734fcfe225e57d0af69a4b538ef3577979aa5f83744bfcd765e1223229b53b89e44055da7c6428b5c3e5097ca5ad6723610f","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":484464,"total_bytes":566398,"total_entries":18,"unique_objects":14,"version":2,"work_units":18} \ No newline at end of file diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/stages/legacy.json b/wai/pack-conformance/staged-mutations/piece-past-max/stages/legacy.json new file mode 100644 index 000000000..5fe4822ba --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":50,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":50},{"dur":40,"floor_only":true,"p":2,"stages":[],"start":75},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":115},{"dur":4294967295,"floor_only":true,"p":4,"stages":[],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/stages/mobile.json b/wai/pack-conformance/staged-mutations/piece-past-max/stages/mobile.json new file mode 100644 index 000000000..878f9fbee --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":974,"h":"8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"}],"start":115},{"dur":4294967295,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0},{"bytes":81407,"deadline_ms":800,"h":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d","layer":1,"refines":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9"}],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/piece-past-max/stages/tv.json b/wai/pack-conformance/staged-mutations/piece-past-max/stages/tv.json new file mode 100644 index 000000000..cce32db32 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/piece-past-max/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0},{"bytes":67779,"deadline_ms":1500,"h":"7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec","layer":1,"refines":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27"}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":974,"h":"8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":115},{"dur":4294967295,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0},{"bytes":81407,"deadline_ms":800,"h":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d","layer":1,"refines":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9"},{"bytes":101295,"deadline_ms":800,"h":"a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87","layer":2,"refines":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d"}],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/refinement-deadline/LISTING b/wai/pack-conformance/staged-mutations/refinement-deadline/LISTING new file mode 100644 index 000000000..87207c884 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/refinement-deadline/LISTING @@ -0,0 +1,41 @@ +f9359ffd69743b04bc491f9fe32e6bac50a3f2e6c9ec9e747b0f16572bea0dec dash/classic.mpd +e3f5ca3e780c57e10b71b30b644dd5c488f85cc11cb12557c75307ea6590d657 dash/mobile.mpd +825126074444a4419069e844ae4a0b9dcb02a2ef5830168e4e77967de9df710a dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362 floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +b3a3be45bb384a06e744c8c2293ba59e16738791abe3ac90b262c2db9a0c5744 hls/classic/floor-v.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/classic/master.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/legacy/master.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/mobile/master.m3u8 +1b7bf011b974365e6bc6532de6d0c54fa9b1447f75c84f3c52d02e1c433e146a hls/mobile/wai-v.m3u8 +adce889f610eaa20b77909693de01dfae787518748223b0774cd56004e0c9353 hls/mobile/wai.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/tv/master.m3u8 +1ea26eaea415f8b459f5994a950e14016a501c2464b9ed8d77db800e16da31d2 hls/tv/wai-v.m3u8 +fc9db3d8e327724c8a7ee793d4b1f1949cf95df1c1941bfcc2bf539fe4184e77 hls/tv/wai.m3u8 +91d119b931466472d04db80f36672a2127a88c1785ab8ac779e464583eaeae24 index-v.wsix +f067d52b986c834745fed1ede3ee8e55e14cca70e739a4f183f60a9a3082b7e9 moq/catalog.json +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +849b7e2cba0f65d3183119f6f4f2c77e5c2a1db9a38d8552369db2dec28df7a7 objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai +22c6777c989d288a6a89876062a244194fde54f5b091b0f603c4ef11f1bc4f60 objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai +83657a16f8686cb03f1da0b5fe3a60ad24b32e1b3f24c17e5b9e5e54720571be objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai +38725131c01f75141fe1790d40209348da7f45241df0bf82a794e8c8196a0fb6 objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +e191fd2fe53e15c30c964e355340cbfbb44562078d6c18c8321c9d3539c43423 objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +db854d9d598122af9def8c9218d4026cbc5aa71b7df6a8f8b76504bd62b60591 package.receipt.json +8454596c213291b896c4483017406c58076e27ad0cab4fab54b850841dfa1a29 plan-v.txt +11536aab05b3fe23aa428a52617da9d156f6326d42ee5b8785af4713b436ecf2 policy.json +61ad0d0ba75977d401ebaeb00b5a8f5795f3b7a1408018fcbb02038f2c412e16 policy.toml +703346f293954ae90516c7cf1a8616e6851f3dba3ca3f51967b388493b1736ff stages/legacy.json +b8beb6eaa1c5a04801b0dcbcbd7f0885f5b0730871708ce0a59093e3c8ea1fdb stages/mobile.json +98c5eca61a270ce5793a982705a51ad888d3b9bb4169004997cb90a5280e4cd7 stages/tv.json diff --git a/wai/pack-conformance/staged-mutations/refinement-deadline/index-v.wsix b/wai/pack-conformance/staged-mutations/refinement-deadline/index-v.wsix new file mode 100644 index 000000000..333fce458 Binary files /dev/null and b/wai/pack-conformance/staged-mutations/refinement-deadline/index-v.wsix differ diff --git a/wai/pack-conformance/staged-mutations/refinement-deadline/moq/catalog.json b/wai/pack-conformance/staged-mutations/refinement-deadline/moq/catalog.json new file mode 100644 index 000000000..e98963bc8 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/refinement-deadline/moq/catalog.json @@ -0,0 +1 @@ +{"tracks":[{"altGroup":1,"isLive":false,"name":"v/l0","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_motion","wai.image.png"],"wai_determinism":"decode-equivalence","wai_receipt_alias":6826975590310646,"wai_receipts":true,"wai_receipts_track":"v/l0/receipts","wai_staged":{"layer":0,"lead_ms":null,"set":"v"}},{"depends":["v/l0"],"isLive":false,"name":"v/l1@800","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":600,"wai_determinism":"decode-equivalence","wai_receipt_alias":4051354593871184,"wai_receipts":true,"wai_receipts_track":"v/l1@800/receipts","wai_staged":{"layer":1,"lead_ms":null,"set":"v"}},{"depends":["v/l0"],"isLive":false,"name":"v/l1","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":1300,"wai_determinism":"decode-equivalence","wai_receipt_alias":5745064158311842,"wai_receipts":true,"wai_receipts_track":"v/l1/receipts","wai_staged":{"layer":1,"lead_ms":null,"set":"v"}},{"depends":["v/l0","v/l1@800","v/l1"],"isLive":false,"name":"v/l2","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":1100,"wai_determinism":"decode-equivalence","wai_receipt_alias":6697012243957121,"wai_receipts":true,"wai_receipts_track":"v/l2/receipts","wai_staged":{"layer":2,"lead_ms":null,"set":"v"}},{"altGroup":1,"codec":"av01.0.00M.08","height":36,"name":"v/floor","namespace":"video-staged-01","packaging":"cmaf","renderGroup":1,"width":64},{"isLive":false,"name":"v/l0/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l1@800/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l1/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l2/receipts","namespace":"video-staged-01","packaging":"wai-receipts"}],"version":1} diff --git a/wai/pack-conformance/staged-mutations/refinement-deadline/package.receipt.json b/wai/pack-conformance/staged-mutations/refinement-deadline/package.receipt.json new file mode 100644 index 000000000..34b1f3638 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/refinement-deadline/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"5444f17f41695562be99397f4db028d232ae8619c39be0378f915daf6cff7520","media_class":"video","stage_root":"3007bcbc7476a09fbd33229f2dbb55b260aaeba9e5c31a111526af8c05748503"}],"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":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"cca77dcb5768f9ac52c778e6b6328bb994bb54ab51be42a9b8629187686cfbf7","sig":"1697273ef344a669c6e9febb9c13d6f07ef0586e090d030dc7c35def98236cef07588dd92c3c45037ef9fb21bd487fcab6ec0578ae46f6b5e16efd066193c907","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":484464,"total_bytes":566398,"total_entries":18,"unique_objects":14,"version":2,"work_units":18} \ No newline at end of file diff --git a/wai/pack-conformance/staged-mutations/refinement-deadline/stages/tv.json b/wai/pack-conformance/staged-mutations/refinement-deadline/stages/tv.json new file mode 100644 index 000000000..1216857e8 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/refinement-deadline/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0},{"bytes":67779,"deadline_ms":1500,"h":"7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec","layer":1,"refines":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27"}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":974,"h":"8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":115},{"dur":60,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0},{"bytes":81407,"deadline_ms":800,"h":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d","layer":1,"refines":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9"},{"bytes":101295,"deadline_ms":1500,"h":"a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87","layer":2,"refines":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d"}],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/refinement-manifest-target/LISTING b/wai/pack-conformance/staged-mutations/refinement-manifest-target/LISTING new file mode 100644 index 000000000..aaeed97d3 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/refinement-manifest-target/LISTING @@ -0,0 +1,41 @@ +f9359ffd69743b04bc491f9fe32e6bac50a3f2e6c9ec9e747b0f16572bea0dec dash/classic.mpd +e3f5ca3e780c57e10b71b30b644dd5c488f85cc11cb12557c75307ea6590d657 dash/mobile.mpd +88403f0b1e76e972d5568b6c610d8b9c8168e066eef3639cff1652437582e23f dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362 floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +b3a3be45bb384a06e744c8c2293ba59e16738791abe3ac90b262c2db9a0c5744 hls/classic/floor-v.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/classic/master.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/legacy/master.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/mobile/master.m3u8 +1b7bf011b974365e6bc6532de6d0c54fa9b1447f75c84f3c52d02e1c433e146a hls/mobile/wai-v.m3u8 +adce889f610eaa20b77909693de01dfae787518748223b0774cd56004e0c9353 hls/mobile/wai.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/tv/master.m3u8 +641ac8fcf0edae28860393d55b59956500de66b9d70631d05a4cc35d5c401897 hls/tv/wai-v.m3u8 +9fc04144671dbedefe16a418944f190493a57ce7dfb2c074a400d8ed9bab9a58 hls/tv/wai.m3u8 +e90609ca0c6788bdd26aa721b8a53c2f95d09d12bb7f2a3c709956334d2f9d4c index-v.wsix +0387c4128de60831575723b868864c82f9d8d129aacec6ad8797db5ca5e5acf4 moq/catalog.json +684f9b7716feb8400e14799a21cbc0e1134e2d578baddab485a3ff6760964669 objects/0919a83b399562f8f5ab406974dbf53c8e09d44b7c18f373fca930e9f1094e93.wai +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +849b7e2cba0f65d3183119f6f4f2c77e5c2a1db9a38d8552369db2dec28df7a7 objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai +22c6777c989d288a6a89876062a244194fde54f5b091b0f603c4ef11f1bc4f60 objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai +83657a16f8686cb03f1da0b5fe3a60ad24b32e1b3f24c17e5b9e5e54720571be objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai +38725131c01f75141fe1790d40209348da7f45241df0bf82a794e8c8196a0fb6 objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +500e2089d40ae283f92f87796377d797db5a8afcc9832b3e48574cc0e1cab1c4 package.receipt.json +8454596c213291b896c4483017406c58076e27ad0cab4fab54b850841dfa1a29 plan-v.txt +11536aab05b3fe23aa428a52617da9d156f6326d42ee5b8785af4713b436ecf2 policy.json +61ad0d0ba75977d401ebaeb00b5a8f5795f3b7a1408018fcbb02038f2c412e16 policy.toml +703346f293954ae90516c7cf1a8616e6851f3dba3ca3f51967b388493b1736ff stages/legacy.json +b8beb6eaa1c5a04801b0dcbcbd7f0885f5b0730871708ce0a59093e3c8ea1fdb stages/mobile.json +3368ea50fb636647172c5ade72396f806f477111d784e02f3747e16b4e60d30a stages/tv.json diff --git a/wai/pack-conformance/staged-mutations/refinement-manifest-target/dash/tv.mpd b/wai/pack-conformance/staged-mutations/refinement-manifest-target/dash/tv.mpd new file mode 100644 index 000000000..33999b97c --- /dev/null +++ b/wai/pack-conformance/staged-mutations/refinement-manifest-target/dash/tv.mpd @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/staged-mutations/refinement-manifest-target/hls/tv/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/refinement-manifest-target/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..b809f9619 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/refinement-manifest-target/hls/tv/wai-v.m3u8 @@ -0,0 +1,25 @@ +#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.000, +#WAI-LAYER:N=1,URI="../../objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai#n=67779" +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai#n=974 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXTINF:2.400, +#WAI-LAYER:N=1,URI="../../objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai#n=81407" +#WAI-LAYER:N=2,URI="../../objects/0919a83b399562f8f5ab406974dbf53c8e09d44b7c18f373fca930e9f1094e93.wai#n=101293" +../../objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai#n=31610 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/refinement-manifest-target/hls/tv/wai.m3u8 b/wai/pack-conformance/staged-mutations/refinement-manifest-target/hls/tv/wai.m3u8 new file mode 100644 index 000000000..02cf087fc --- /dev/null +++ b/wai/pack-conformance/staged-mutations/refinement-manifest-target/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=714367,AVERAGE-BANDWIDTH=586899,CODECS="wai1.video.int_motion+video.int_refine",X-WAI-FLOOR="wai1.video.int_motion+video.int_refine",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/refinement-manifest-target/index-v.wsix b/wai/pack-conformance/staged-mutations/refinement-manifest-target/index-v.wsix new file mode 100644 index 000000000..c4414fc33 Binary files /dev/null and b/wai/pack-conformance/staged-mutations/refinement-manifest-target/index-v.wsix differ diff --git a/wai/pack-conformance/staged-mutations/refinement-manifest-target/objects/0919a83b399562f8f5ab406974dbf53c8e09d44b7c18f373fca930e9f1094e93.wai b/wai/pack-conformance/staged-mutations/refinement-manifest-target/objects/0919a83b399562f8f5ab406974dbf53c8e09d44b7c18f373fca930e9f1094e93.wai new file mode 100644 index 000000000..f1b76b61e Binary files /dev/null and b/wai/pack-conformance/staged-mutations/refinement-manifest-target/objects/0919a83b399562f8f5ab406974dbf53c8e09d44b7c18f373fca930e9f1094e93.wai differ diff --git a/wai/pack-conformance/staged-mutations/refinement-manifest-target/package.receipt.json b/wai/pack-conformance/staged-mutations/refinement-manifest-target/package.receipt.json new file mode 100644 index 000000000..25bee42c4 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/refinement-manifest-target/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"94893f594e5a75b69c22d56ba708422ded9c1ec961800ca98a589872ae7bfe06","media_class":"video","stage_root":"410887606cde2b077949f3969a3c3e15c88fd4eca96fbfc40578104d54123edf"}],"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":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"f38eaf41a38726d47b2cef7bb79f25de42e23424385d00ef6a7569390b2c1db0","sig":"0c86e32565262ccea7caa5f4e989fe228a61b16f2956cb2c3cb02f128388ae56e7997c9548957ee8ffb0b66d249456fc0df7df386c8c1876f273abc711550107","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":484462,"total_bytes":566396,"total_entries":18,"unique_objects":14,"version":2,"work_units":18} \ No newline at end of file diff --git a/wai/pack-conformance/staged-mutations/refinement-manifest-target/stages/tv.json b/wai/pack-conformance/staged-mutations/refinement-manifest-target/stages/tv.json new file mode 100644 index 000000000..3ce42d359 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/refinement-manifest-target/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0},{"bytes":67779,"deadline_ms":1500,"h":"7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec","layer":1,"refines":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27"}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":974,"h":"8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":115},{"dur":60,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0},{"bytes":81407,"deadline_ms":800,"h":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d","layer":1,"refines":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9"},{"bytes":101293,"deadline_ms":800,"h":"0919a83b399562f8f5ab406974dbf53c8e09d44b7c18f373fca930e9f1094e93","layer":2,"refines":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d"}],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/LISTING b/wai/pack-conformance/staged-mutations/still-at-cap/LISTING new file mode 100644 index 000000000..fd40874c5 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/LISTING @@ -0,0 +1,35 @@ +27ba282182985cfc6a7811ce3ee7d7da6733f14148cec48bc8cef3dfd6bcf12b dash/classic.mpd +05f95a5c55497ebfe5146485460f1964924abfb82031bfb6bc439652d92bc403 dash/mobile.mpd +a1b5ae6179b1ccf1b839e5beb537fb04d53d435401bcdc55948f0b5880636839 dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +ac98cd1c948a6fe21d7402ee885fed6db86d60a33b328a98770c07a9df44e5ec hls/classic/floor-v.m3u8 +e3e203490a794965fe4bb8ab9ffe75615dc53fddc18f705ed5fca542558ac370 hls/classic/master.m3u8 +e3e203490a794965fe4bb8ab9ffe75615dc53fddc18f705ed5fca542558ac370 hls/legacy/master.m3u8 +a7974d0968cc8dcd30703ddf53e149c7c6a5996bcf1d77cd043439dc40aefbac hls/mobile/master.m3u8 +501762490c55c1d183634385532bac19b719b9d486dcf03fa9d702c73502ec97 hls/mobile/wai-v.m3u8 +31284c7b277203d780c46aaa97e00e475cf32223e9871fb5ffd2471cdbac9a64 hls/mobile/wai.m3u8 +a7974d0968cc8dcd30703ddf53e149c7c6a5996bcf1d77cd043439dc40aefbac hls/tv/master.m3u8 +99b9aabe5eb6aac46b6ae7a233ff99468f477a0e1e51d6a38a6a5abd5f0f62ee hls/tv/wai-v.m3u8 +fd7f47142bf62014377c02de762f2b76c26e681a908f1713e2ba53d90de51377 hls/tv/wai.m3u8 +040159d92dc28a379dc8636c11c158ef65a99349cc8be5114166d4245ff6e007 index-v.wsix +22652936803efca38577556f8a2cc61b4a8dde41c9737d8379855a84d6fb7aa8 moq/catalog.json +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +660655a910324053f875b527d757be9351b79fcab94b2cb81b8a4024d8a528cc objects/e8eb7b1a422709e42bc81aa6f0afc12f658a8a6427ee1261f05204e8b4126adb.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +c0ecb3d5882897fc1df3f49856422fb1490ad30f40e3504f382e2dbcfaae2e40 package.receipt.json +6261e5b48f122bd416b9f71b2416fba9f1e3c17b67682ba6a3f26e0a27ef9f5e plan-v.txt +be4b23903fd6785aecac915be7a2f87a31ad02240e17849d73a9c240a84c3814 policy.json +86c0015adc05f80f474c3dfa5a235e28a006496772c1956f98786783afc57f9b policy.toml +f17595ba6c62ba2524e0d899c67d1dcb0bd3be00947e26917dd25c358eb22aba stages/legacy.json +fa1e2dd8fa9d1aee45af6a96e70fa94302b1f80212293a6de20ce94979ef6e57 stages/mobile.json +0d7f00e4854fc3c5b75644f422d00577ecb2354153ace0e87da6256d3fcee8ef stages/tv.json diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/dash/classic.mpd b/wai/pack-conformance/staged-mutations/still-at-cap/dash/classic.mpd new file mode 100644 index 000000000..07664a4c5 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/dash/classic.mpd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/dash/mobile.mpd b/wai/pack-conformance/staged-mutations/still-at-cap/dash/mobile.mpd new file mode 100644 index 000000000..0339b7d5d --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/dash/mobile.mpd @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/dash/tv.mpd b/wai/pack-conformance/staged-mutations/still-at-cap/dash/tv.mpd new file mode 100644 index 000000000..307e34f2c --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/dash/tv.mpd @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/hls/classic/floor-v.m3u8 b/wai/pack-conformance/staged-mutations/still-at-cap/hls/classic/floor-v.m3u8 new file mode 100644 index 000000000..7f2e4b468 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/hls/classic/floor-v.m3u8 @@ -0,0 +1,14 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:600 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/v/init.mp4" +#EXTINF:2.000, +../../floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +#EXTINF:1.000, +../../floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +#EXTINF:600.000, +../../floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/hls/classic/master.m3u8 b/wai/pack-conformance/staged-mutations/still-at-cap/hls/classic/master.m3u8 new file mode 100644 index 000000000..7614a173f --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/hls/classic/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=8840,AVERAGE-BANDWIDTH=67,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/hls/legacy/master.m3u8 b/wai/pack-conformance/staged-mutations/still-at-cap/hls/legacy/master.m3u8 new file mode 100644 index 000000000..7614a173f --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/hls/legacy/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=8840,AVERAGE-BANDWIDTH=67,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/hls/mobile/master.m3u8 b/wai/pack-conformance/staged-mutations/still-at-cap/hls/mobile/master.m3u8 new file mode 100644 index 000000000..ba0ebb040 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/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=8840,AVERAGE-BANDWIDTH=67,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/hls/mobile/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/still-at-cap/hls/mobile/wai-v.m3u8 new file mode 100644 index 000000000..eb43b9525 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/hls/mobile/wai-v.m3u8 @@ -0,0 +1,15 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:600 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:2.000, +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:600.000, +../../objects/e8eb7b1a422709e42bc81aa6f0afc12f658a8a6427ee1261f05204e8b4126adb.wai#n=977 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/hls/mobile/wai.m3u8 b/wai/pack-conformance/staged-mutations/still-at-cap/hls/mobile/wai.m3u8 new file mode 100644 index 000000000..d0eb98cb7 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/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=323176,AVERAGE-BANDWIDTH=177139,CODECS="wai1.video.int_motion+video.int_refine",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/hls/tv/master.m3u8 b/wai/pack-conformance/staged-mutations/still-at-cap/hls/tv/master.m3u8 new file mode 100644 index 000000000..ba0ebb040 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/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=8840,AVERAGE-BANDWIDTH=67,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/hls/tv/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/still-at-cap/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..479f2f9c7 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/hls/tv/wai-v.m3u8 @@ -0,0 +1,17 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:600 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:2.000, +#WAI-LAYER:N=1,URI="../../objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai#n=67779" +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:600.000, +../../objects/e8eb7b1a422709e42bc81aa6f0afc12f658a8a6427ee1261f05204e8b4126adb.wai#n=977 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/hls/tv/wai.m3u8 b/wai/pack-conformance/staged-mutations/still-at-cap/hls/tv/wai.m3u8 new file mode 100644 index 000000000..e60a26af9 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/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=645600,AVERAGE-BANDWIDTH=465358,CODECS="wai1.video.int_motion+video.int_refine",X-WAI-FLOOR="wai1.video.int_motion+video.int_refine",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/index-v.wsix b/wai/pack-conformance/staged-mutations/still-at-cap/index-v.wsix new file mode 100644 index 000000000..726e2b4f5 Binary files /dev/null and b/wai/pack-conformance/staged-mutations/still-at-cap/index-v.wsix differ diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/moq/catalog.json b/wai/pack-conformance/staged-mutations/still-at-cap/moq/catalog.json new file mode 100644 index 000000000..340931feb --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/moq/catalog.json @@ -0,0 +1 @@ +{"tracks":[{"altGroup":1,"isLive":false,"name":"v/l0","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_motion","wai.image.png"],"wai_determinism":"decode-equivalence","wai_receipt_alias":6826975590310646,"wai_receipts":true,"wai_receipts_track":"v/l0/receipts","wai_staged":{"layer":0,"lead_ms":null,"set":"v"}},{"depends":["v/l0"],"isLive":false,"name":"v/l1","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":1300,"wai_determinism":"decode-equivalence","wai_receipt_alias":5745064158311842,"wai_receipts":true,"wai_receipts_track":"v/l1/receipts","wai_staged":{"layer":1,"lead_ms":null,"set":"v"}},{"depends":["v/l0","v/l1"],"isLive":false,"name":"v/l2","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":1100,"wai_determinism":"decode-equivalence","wai_receipt_alias":6697012243957121,"wai_receipts":true,"wai_receipts_track":"v/l2/receipts","wai_staged":{"layer":2,"lead_ms":null,"set":"v"}},{"altGroup":1,"codec":"av01.0.00M.08","height":36,"name":"v/floor","namespace":"video-staged-01","packaging":"cmaf","renderGroup":1,"width":64},{"isLive":false,"name":"v/l0/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l1/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l2/receipts","namespace":"video-staged-01","packaging":"wai-receipts"}],"version":1} diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/objects/e8eb7b1a422709e42bc81aa6f0afc12f658a8a6427ee1261f05204e8b4126adb.wai b/wai/pack-conformance/staged-mutations/still-at-cap/objects/e8eb7b1a422709e42bc81aa6f0afc12f658a8a6427ee1261f05204e8b4126adb.wai new file mode 100644 index 000000000..66ffb59e7 Binary files /dev/null and b/wai/pack-conformance/staged-mutations/still-at-cap/objects/e8eb7b1a422709e42bc81aa6f0afc12f658a8a6427ee1261f05204e8b4126adb.wai differ diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/package.receipt.json b/wai/pack-conformance/staged-mutations/still-at-cap/package.receipt.json new file mode 100644 index 000000000..be16a10c0 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"d63bc24ddb0ee451a141ec642ffa38ca50274d366df66180537466b00841d91c","media_class":"video","stage_root":"1f14fc81730f5f6c334ac4189f74f274deee40959b6c781416803d28a7e2b3d4"}],"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":"c8b34bde7e3c363ce045b0c602640abc2b93a120f05932250b8b15afb633c618","receipt_hash":"fca44f050468ff40abf3d5afda9e6f39bb625e830b6cd49201ebb805839f7ef0","sig":"3835f7b7953adda716251a5930ac8da498b2cd14790aebc8ec1cf556e6ce84716e981cd40fec3639532ce2edba13c84878da66c7fb5e137d82c6b962c10e7b05","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":267312,"total_bytes":267312,"total_entries":10,"unique_objects":10,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/plan-v.txt b/wai/pack-conformance/staged-mutations/still-at-cap/plan-v.txt new file mode 100644 index 000000000..941262bd3 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/plan-v.txt @@ -0,0 +1,3 @@ +0 +50 +75 diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/policy.json b/wai/pack-conformance/staged-mutations/still-at-cap/policy.json new file mode 100644 index 000000000..216552ece --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","class":[{"floor_only":false,"id":"tv","max_layer":2},{"floor_only":false,"id":"mobile","max_kbps":330,"max_layer":1},{"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-staged-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"f00d8bf055ef725d99f925cc0c1c97b6b7fc956c82872f63840c246b4b69e150"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":60000,"mv_precision":1,"q_candidates":[24,16],"search":4},"role":"base"},{"capability":"wai.video.int_refine","component":"v","id":"r1","params":{"q":12},"refines":"b0","role":"refinement"},{"capability":"wai.video.int_refine","component":"v","id":"r2","params":{"q":4},"refines":"r1","role":"refinement"},{"capability":"wai.image.png","component":"v","id":"still","params":{},"role":"base"},{"capability":"wai.video.av1","component":"v","id":"f0","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0,200,400]},"moment":[{"from_ms":5600,"id":"m-event","tags":["event"],"to_ms":8000}],"royalty_free_only":true,"rule":[{"id":"static","then":{"ladder":["still","f0"]},"when":{"piece_kind":["static"]}},{"id":"motion","then":{"max_layer":1},"when":{"piece_kind":["motion_high"]}},{"id":"event","then":{"deadline_ms":800},"when":{"moment_tag":["event"]}}],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":600000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/policy.toml b/wai/pack-conformance/staged-mutations/still-at-cap/policy.toml new file mode 100644 index 000000000..652ee926f --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/policy.toml @@ -0,0 +1,94 @@ +schema = "wai.pack.policy/1" +asset_id = "video-staged-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-staged-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 600000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [24, 16], max_sse_px_milli = 60000 } + +[[ladder]] +id = "r1" +component = "v" +capability = "wai.video.int_refine" +role = "refinement" +refines = "b0" +params = { q = 12 } + +[[ladder]] +id = "r2" +component = "v" +capability = "wai.video.int_refine" +role = "refinement" +refines = "r1" +params = { q = 4 } + +[[ladder]] +id = "still" +component = "v" +capability = "wai.image.png" +role = "base" + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "f00d8bf055ef725d99f925cc0c1c97b6b7fc956c82872f63840c246b4b69e150" + +[live] +layer_lag_ms = [0, 200, 400] + +[[class]] +id = "tv" +max_layer = 2 + +[[class]] +id = "mobile" +max_layer = 1 +max_kbps = 330 + +[[class]] +id = "legacy" +floor_only = true + +[[moment]] +id = "m-event" +from_ms = 5600 +to_ms = 8000 +tags = ["event"] + +[[rule]] +id = "static" +when = { piece_kind = ["static"] } +then = { ladder = ["still", "f0"] } + +[[rule]] +id = "motion" +when = { piece_kind = ["motion_high"] } +then = { max_layer = 1 } + +[[rule]] +id = "event" +when = { moment_tag = ["event"] } +then = { deadline_ms = 800 } diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/stages/legacy.json b/wai/pack-conformance/staged-mutations/still-at-cap/stages/legacy.json new file mode 100644 index 000000000..c80f4ce9c --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":50,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":50},{"dur":15000,"floor_only":true,"p":2,"stages":[],"start":75}]}},"policy_digest":"c8b34bde7e3c363ce045b0c602640abc2b93a120f05932250b8b15afb633c618"} diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/stages/mobile.json b/wai/pack-conformance/staged-mutations/still-at-cap/stages/mobile.json new file mode 100644 index 000000000..c5f8434d7 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"}],"start":50},{"dur":15000,"p":2,"stages":[{"bytes":977,"h":"e8eb7b1a422709e42bc81aa6f0afc12f658a8a6427ee1261f05204e8b4126adb","layer":0}],"start":75}]}},"policy_digest":"c8b34bde7e3c363ce045b0c602640abc2b93a120f05932250b8b15afb633c618"} diff --git a/wai/pack-conformance/staged-mutations/still-at-cap/stages/tv.json b/wai/pack-conformance/staged-mutations/still-at-cap/stages/tv.json new file mode 100644 index 000000000..98ab664c7 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-at-cap/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0},{"bytes":67779,"deadline_ms":1500,"h":"7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec","layer":1,"refines":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27"}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":50},{"dur":15000,"p":2,"stages":[{"bytes":977,"h":"e8eb7b1a422709e42bc81aa6f0afc12f658a8a6427ee1261f05204e8b4126adb","layer":0}],"start":75}]}},"policy_digest":"c8b34bde7e3c363ce045b0c602640abc2b93a120f05932250b8b15afb633c618"} diff --git a/wai/pack-conformance/staged-mutations/still-held-digest/LISTING b/wai/pack-conformance/staged-mutations/still-held-digest/LISTING new file mode 100644 index 000000000..217b941c5 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-held-digest/LISTING @@ -0,0 +1,41 @@ +f9359ffd69743b04bc491f9fe32e6bac50a3f2e6c9ec9e747b0f16572bea0dec dash/classic.mpd +e3f5ca3e780c57e10b71b30b644dd5c488f85cc11cb12557c75307ea6590d657 dash/mobile.mpd +825126074444a4419069e844ae4a0b9dcb02a2ef5830168e4e77967de9df710a dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362 floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +b3a3be45bb384a06e744c8c2293ba59e16738791abe3ac90b262c2db9a0c5744 hls/classic/floor-v.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/classic/master.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/legacy/master.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/mobile/master.m3u8 +1b7bf011b974365e6bc6532de6d0c54fa9b1447f75c84f3c52d02e1c433e146a hls/mobile/wai-v.m3u8 +adce889f610eaa20b77909693de01dfae787518748223b0774cd56004e0c9353 hls/mobile/wai.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/tv/master.m3u8 +1ea26eaea415f8b459f5994a950e14016a501c2464b9ed8d77db800e16da31d2 hls/tv/wai-v.m3u8 +fc9db3d8e327724c8a7ee793d4b1f1949cf95df1c1941bfcc2bf539fe4184e77 hls/tv/wai.m3u8 +363d8ccb40ea92cdc6ce5b8e00b9281db2c180af287999072ce0dfd0be1977d2 index-v.wsix +0387c4128de60831575723b868864c82f9d8d129aacec6ad8797db5ca5e5acf4 moq/catalog.json +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +849b7e2cba0f65d3183119f6f4f2c77e5c2a1db9a38d8552369db2dec28df7a7 objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai +22c6777c989d288a6a89876062a244194fde54f5b091b0f603c4ef11f1bc4f60 objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai +83657a16f8686cb03f1da0b5fe3a60ad24b32e1b3f24c17e5b9e5e54720571be objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai +38725131c01f75141fe1790d40209348da7f45241df0bf82a794e8c8196a0fb6 objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +e191fd2fe53e15c30c964e355340cbfbb44562078d6c18c8321c9d3539c43423 objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +d7d7d5cbd6f11916451d71fd7a48f5b1d6d8ee65e9f1fad97bcc10553c3557cd package.receipt.json +8454596c213291b896c4483017406c58076e27ad0cab4fab54b850841dfa1a29 plan-v.txt +11536aab05b3fe23aa428a52617da9d156f6326d42ee5b8785af4713b436ecf2 policy.json +61ad0d0ba75977d401ebaeb00b5a8f5795f3b7a1408018fcbb02038f2c412e16 policy.toml +703346f293954ae90516c7cf1a8616e6851f3dba3ca3f51967b388493b1736ff stages/legacy.json +b8beb6eaa1c5a04801b0dcbcbd7f0885f5b0730871708ce0a59093e3c8ea1fdb stages/mobile.json +9c4eda5ea5972cbca98b3b89503235fcd807450c129545ca86c322337e27d872 stages/tv.json diff --git a/wai/pack-conformance/staged-mutations/still-held-digest/index-v.wsix b/wai/pack-conformance/staged-mutations/still-held-digest/index-v.wsix new file mode 100644 index 000000000..fbf9ed58e Binary files /dev/null and b/wai/pack-conformance/staged-mutations/still-held-digest/index-v.wsix differ diff --git a/wai/pack-conformance/staged-mutations/still-held-digest/package.receipt.json b/wai/pack-conformance/staged-mutations/still-held-digest/package.receipt.json new file mode 100644 index 000000000..8805d8696 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-held-digest/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"88d5ec96f371328b49efda1bb9fb940b3878eb2b67bf525da030990545ae9b1d","media_class":"video","stage_root":"8b80abc97ab16dc4f09f2113600feb1e1366a350897a256550fee7bb51c6ef44"}],"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":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"c88caab5558890fcca9b1349a53a5ca0e3593434f6e4fa17964e678f8bd6a8d5","sig":"066303b7070b751aed70cf980cf32a31057dad17fc9111aa1863a204663020e475784d191c344fb806b5c8d41999d12e8605f7e23c9927199b503685e788fa03","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":484464,"total_bytes":566398,"total_entries":18,"unique_objects":14,"version":2,"work_units":18} \ No newline at end of file diff --git a/wai/pack-conformance/staged-mutations/still-incomplete-dist/LISTING b/wai/pack-conformance/staged-mutations/still-incomplete-dist/LISTING new file mode 100644 index 000000000..d0a175cb4 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-incomplete-dist/LISTING @@ -0,0 +1,41 @@ +f9359ffd69743b04bc491f9fe32e6bac50a3f2e6c9ec9e747b0f16572bea0dec dash/classic.mpd +e3f5ca3e780c57e10b71b30b644dd5c488f85cc11cb12557c75307ea6590d657 dash/mobile.mpd +825126074444a4419069e844ae4a0b9dcb02a2ef5830168e4e77967de9df710a dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362 floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +b3a3be45bb384a06e744c8c2293ba59e16738791abe3ac90b262c2db9a0c5744 hls/classic/floor-v.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/classic/master.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/legacy/master.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/mobile/master.m3u8 +e7d4cd2aa50bed86c1b837d273028e20e4a181a1f477153d785e5bcbf55668d6 hls/mobile/wai-v.m3u8 +adce889f610eaa20b77909693de01dfae787518748223b0774cd56004e0c9353 hls/mobile/wai.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/tv/master.m3u8 +c4f0ca96dca1a87e8c3128a545ed9ddd84b1630f662345d949609f9ab3765af6 hls/tv/wai-v.m3u8 +fc9db3d8e327724c8a7ee793d4b1f1949cf95df1c1941bfcc2bf539fe4184e77 hls/tv/wai.m3u8 +9215679a85d4f7d7404b68d5042802383a68c06021164d113d8387191fcbd0e8 index-v.wsix +0387c4128de60831575723b868864c82f9d8d129aacec6ad8797db5ca5e5acf4 moq/catalog.json +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +933a727c55cfb03d21b27717fd6f923fbfc58c1928541c96d27b374c26970e7a objects/3f0d5ef876822b9294b86dbab0a5ed7a68026623192adb7725face30faf40b65.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +22c6777c989d288a6a89876062a244194fde54f5b091b0f603c4ef11f1bc4f60 objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai +83657a16f8686cb03f1da0b5fe3a60ad24b32e1b3f24c17e5b9e5e54720571be objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai +38725131c01f75141fe1790d40209348da7f45241df0bf82a794e8c8196a0fb6 objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +e191fd2fe53e15c30c964e355340cbfbb44562078d6c18c8321c9d3539c43423 objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +1ecdf2a13fa326a52017442880ef7ac6d168eff1570c1bc12d37e7f538b5ef24 package.receipt.json +8454596c213291b896c4483017406c58076e27ad0cab4fab54b850841dfa1a29 plan-v.txt +11536aab05b3fe23aa428a52617da9d156f6326d42ee5b8785af4713b436ecf2 policy.json +61ad0d0ba75977d401ebaeb00b5a8f5795f3b7a1408018fcbb02038f2c412e16 policy.toml +703346f293954ae90516c7cf1a8616e6851f3dba3ca3f51967b388493b1736ff stages/legacy.json +0f0dc3aa2798ddfc28baf0c15458cab16b20cc1ab032db0043145d42299d5de5 stages/mobile.json +72fe1316e743f97a39bb1d6acc486d05d6e2bb180bbee8f5d5c9829cf84c6622 stages/tv.json diff --git a/wai/pack-conformance/staged-mutations/still-incomplete-dist/hls/mobile/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/still-incomplete-dist/hls/mobile/wai-v.m3u8 new file mode 100644 index 000000000..cacb3893c --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-incomplete-dist/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 +#EXTINF:2.000, +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/3f0d5ef876822b9294b86dbab0a5ed7a68026623192adb7725face30faf40b65.wai#n=7266 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXTINF:2.400, +../../objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai#n=31610 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/still-incomplete-dist/hls/tv/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/still-incomplete-dist/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..281ce8027 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-incomplete-dist/hls/tv/wai-v.m3u8 @@ -0,0 +1,25 @@ +#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.000, +#WAI-LAYER:N=1,URI="../../objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai#n=67779" +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/3f0d5ef876822b9294b86dbab0a5ed7a68026623192adb7725face30faf40b65.wai#n=7266 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXTINF:2.400, +#WAI-LAYER:N=1,URI="../../objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai#n=81407" +#WAI-LAYER:N=2,URI="../../objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai#n=101295" +../../objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai#n=31610 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/still-incomplete-dist/index-v.wsix b/wai/pack-conformance/staged-mutations/still-incomplete-dist/index-v.wsix new file mode 100644 index 000000000..556cdcc9e Binary files /dev/null and b/wai/pack-conformance/staged-mutations/still-incomplete-dist/index-v.wsix differ diff --git a/wai/pack-conformance/staged-mutations/still-incomplete-dist/objects/3f0d5ef876822b9294b86dbab0a5ed7a68026623192adb7725face30faf40b65.wai b/wai/pack-conformance/staged-mutations/still-incomplete-dist/objects/3f0d5ef876822b9294b86dbab0a5ed7a68026623192adb7725face30faf40b65.wai new file mode 100644 index 000000000..942820390 Binary files /dev/null and b/wai/pack-conformance/staged-mutations/still-incomplete-dist/objects/3f0d5ef876822b9294b86dbab0a5ed7a68026623192adb7725face30faf40b65.wai differ diff --git a/wai/pack-conformance/staged-mutations/still-incomplete-dist/package.receipt.json b/wai/pack-conformance/staged-mutations/still-incomplete-dist/package.receipt.json new file mode 100644 index 000000000..94978185b --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-incomplete-dist/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"a141cee10159c15acdfab49f6faf1c8fbe1c22534e5419753f21bc2ca2d873fb","media_class":"video","stage_root":"2bcb771bcb7f6f93f7b15cc7c6d13e9a6e7956e113426b14ddec297b328f2455"}],"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":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"e4eafe6ee51d145f60e008013279332502d57efb0183b6adee826b943ba1649e","sig":"1cc695b8e524defe79e3c5ac10bddddc555aede86a167e8fc12bd5b9a5d9df274c4ccf5a18a9af6a7f27cae32637cfaa535e04c38fa29b687b79f5542cfa5e08","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":490756,"total_bytes":572690,"total_entries":18,"unique_objects":14,"version":2,"work_units":18} \ No newline at end of file diff --git a/wai/pack-conformance/staged-mutations/still-incomplete-dist/stages/mobile.json b/wai/pack-conformance/staged-mutations/still-incomplete-dist/stages/mobile.json new file mode 100644 index 000000000..4472d6717 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-incomplete-dist/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":7266,"h":"3f0d5ef876822b9294b86dbab0a5ed7a68026623192adb7725face30faf40b65","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"}],"start":115},{"dur":60,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0}],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/still-incomplete-dist/stages/tv.json b/wai/pack-conformance/staged-mutations/still-incomplete-dist/stages/tv.json new file mode 100644 index 000000000..06accccb6 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-incomplete-dist/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0},{"bytes":67779,"deadline_ms":1500,"h":"7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec","layer":1,"refines":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27"}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":7266,"h":"3f0d5ef876822b9294b86dbab0a5ed7a68026623192adb7725face30faf40b65","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":115},{"dur":60,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0},{"bytes":81407,"deadline_ms":800,"h":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d","layer":1,"refines":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9"},{"bytes":101295,"deadline_ms":800,"h":"a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87","layer":2,"refines":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d"}],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/still-manifest-target/LISTING b/wai/pack-conformance/staged-mutations/still-manifest-target/LISTING new file mode 100644 index 000000000..db79dee1b --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-manifest-target/LISTING @@ -0,0 +1,41 @@ +f9359ffd69743b04bc491f9fe32e6bac50a3f2e6c9ec9e747b0f16572bea0dec dash/classic.mpd +e3f5ca3e780c57e10b71b30b644dd5c488f85cc11cb12557c75307ea6590d657 dash/mobile.mpd +825126074444a4419069e844ae4a0b9dcb02a2ef5830168e4e77967de9df710a dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362 floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +b3a3be45bb384a06e744c8c2293ba59e16738791abe3ac90b262c2db9a0c5744 hls/classic/floor-v.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/classic/master.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/legacy/master.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/mobile/master.m3u8 +e2e2d0c4e02660e13ce06dd3359b18bed8e75b63f28db819ddad61f59af210cf hls/mobile/wai-v.m3u8 +adce889f610eaa20b77909693de01dfae787518748223b0774cd56004e0c9353 hls/mobile/wai.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/tv/master.m3u8 +6d8b1dcae5b236c0e51e914121eb1334dc9228f2aad15fc995281cbe707fe5dc hls/tv/wai-v.m3u8 +fc9db3d8e327724c8a7ee793d4b1f1949cf95df1c1941bfcc2bf539fe4184e77 hls/tv/wai.m3u8 +8bb0cd3e9d6010987c3df90b5de437f31d460084ff1ed56175b4f2a7165a69f9 index-v.wsix +0387c4128de60831575723b868864c82f9d8d129aacec6ad8797db5ca5e5acf4 moq/catalog.json +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +22c6777c989d288a6a89876062a244194fde54f5b091b0f603c4ef11f1bc4f60 objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai +9a9052e3dba5e5a10979317d2c8b349cec2d5ee6f59cf8d256add7d6b5be6cbd objects/953df17a7ac1be8ab159a1472dacbc716b2b6b563be5ac275fdf8625459150cb.wai +83657a16f8686cb03f1da0b5fe3a60ad24b32e1b3f24c17e5b9e5e54720571be objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai +38725131c01f75141fe1790d40209348da7f45241df0bf82a794e8c8196a0fb6 objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +e191fd2fe53e15c30c964e355340cbfbb44562078d6c18c8321c9d3539c43423 objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +38570bad3ed03466bc76b5e8165f395cfe87705bee6a23d51fcaf691f59c8bdb package.receipt.json +8454596c213291b896c4483017406c58076e27ad0cab4fab54b850841dfa1a29 plan-v.txt +11536aab05b3fe23aa428a52617da9d156f6326d42ee5b8785af4713b436ecf2 policy.json +61ad0d0ba75977d401ebaeb00b5a8f5795f3b7a1408018fcbb02038f2c412e16 policy.toml +703346f293954ae90516c7cf1a8616e6851f3dba3ca3f51967b388493b1736ff stages/legacy.json +d9019a7cdd0c4c256a6eb7913de7621264a412f4684a583583e16cc3a451e8b0 stages/mobile.json +9fcd13984d1e9d8771c1641facfce7c6a615b4c9831251cfb153b60e64a7ab55 stages/tv.json diff --git a/wai/pack-conformance/staged-mutations/still-manifest-target/hls/mobile/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/still-manifest-target/hls/mobile/wai-v.m3u8 new file mode 100644 index 000000000..1e48f3049 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-manifest-target/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 +#EXTINF:2.000, +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/953df17a7ac1be8ab159a1472dacbc716b2b6b563be5ac275fdf8625459150cb.wai#n=980 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXTINF:2.400, +../../objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai#n=31610 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/still-manifest-target/hls/tv/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/still-manifest-target/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..826a9c737 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-manifest-target/hls/tv/wai-v.m3u8 @@ -0,0 +1,25 @@ +#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.000, +#WAI-LAYER:N=1,URI="../../objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai#n=67779" +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/953df17a7ac1be8ab159a1472dacbc716b2b6b563be5ac275fdf8625459150cb.wai#n=980 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXTINF:2.400, +#WAI-LAYER:N=1,URI="../../objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai#n=81407" +#WAI-LAYER:N=2,URI="../../objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai#n=101295" +../../objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai#n=31610 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/still-manifest-target/index-v.wsix b/wai/pack-conformance/staged-mutations/still-manifest-target/index-v.wsix new file mode 100644 index 000000000..8bcca441f Binary files /dev/null and b/wai/pack-conformance/staged-mutations/still-manifest-target/index-v.wsix differ diff --git a/wai/pack-conformance/staged-mutations/still-manifest-target/objects/953df17a7ac1be8ab159a1472dacbc716b2b6b563be5ac275fdf8625459150cb.wai b/wai/pack-conformance/staged-mutations/still-manifest-target/objects/953df17a7ac1be8ab159a1472dacbc716b2b6b563be5ac275fdf8625459150cb.wai new file mode 100644 index 000000000..b70f47bbf Binary files /dev/null and b/wai/pack-conformance/staged-mutations/still-manifest-target/objects/953df17a7ac1be8ab159a1472dacbc716b2b6b563be5ac275fdf8625459150cb.wai differ diff --git a/wai/pack-conformance/staged-mutations/still-manifest-target/package.receipt.json b/wai/pack-conformance/staged-mutations/still-manifest-target/package.receipt.json new file mode 100644 index 000000000..4053e866d --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-manifest-target/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"e25fede082dfba1c538d6b4076359dd3ae4d7ab8d41387c1f38b49e7eca629fe","media_class":"video","stage_root":"d1ef5fd0c2ab9ec75c350183447a917a893d143bbcc1e15f4adf512ee7391611"}],"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":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"49d199b2ee4bd3f46c0c08ca53da6909b57817b4aed773309c6be2745ffc3e0e","sig":"ab94baab551f2ad6834f6dd272fde13ae80c6e5a36b557422bf522779a615c758636a14834d7fbce5805b531a03f0a2356ea2caba860823157648ca12024af0f","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":484470,"total_bytes":566404,"total_entries":18,"unique_objects":14,"version":2,"work_units":18} \ No newline at end of file diff --git a/wai/pack-conformance/staged-mutations/still-manifest-target/stages/mobile.json b/wai/pack-conformance/staged-mutations/still-manifest-target/stages/mobile.json new file mode 100644 index 000000000..c65bd5901 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-manifest-target/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":980,"h":"953df17a7ac1be8ab159a1472dacbc716b2b6b563be5ac275fdf8625459150cb","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"}],"start":115},{"dur":60,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0}],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/still-manifest-target/stages/tv.json b/wai/pack-conformance/staged-mutations/still-manifest-target/stages/tv.json new file mode 100644 index 000000000..2f89abf81 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-manifest-target/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0},{"bytes":67779,"deadline_ms":1500,"h":"7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec","layer":1,"refines":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27"}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":980,"h":"953df17a7ac1be8ab159a1472dacbc716b2b6b563be5ac275fdf8625459150cb","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":115},{"dur":60,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0},{"bytes":81407,"deadline_ms":800,"h":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d","layer":1,"refines":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9"},{"bytes":101295,"deadline_ms":800,"h":"a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87","layer":2,"refines":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d"}],"start":140}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/still-not-the-ladder/LISTING b/wai/pack-conformance/staged-mutations/still-not-the-ladder/LISTING new file mode 100644 index 000000000..fadf1da03 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-not-the-ladder/LISTING @@ -0,0 +1,41 @@ +f9359ffd69743b04bc491f9fe32e6bac50a3f2e6c9ec9e747b0f16572bea0dec dash/classic.mpd +e3f5ca3e780c57e10b71b30b644dd5c488f85cc11cb12557c75307ea6590d657 dash/mobile.mpd +825126074444a4419069e844ae4a0b9dcb02a2ef5830168e4e77967de9df710a dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362 floor/v/c3470c0a3624d3e458b8fe12a0ff68df725d27cf7768984d6f6aca0149dd2362.m4s +ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b floor/v/ca9a1818ed7001e0ef0543bf3864b4ad937362fa89b834f4838349bcb789fc5b.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +b3a3be45bb384a06e744c8c2293ba59e16738791abe3ac90b262c2db9a0c5744 hls/classic/floor-v.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/classic/master.m3u8 +586a9ebcc3a05b8497658933f27caca010ff6bd74c885960a4bab04c2406879c hls/legacy/master.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/mobile/master.m3u8 +1b7bf011b974365e6bc6532de6d0c54fa9b1447f75c84f3c52d02e1c433e146a hls/mobile/wai-v.m3u8 +adce889f610eaa20b77909693de01dfae787518748223b0774cd56004e0c9353 hls/mobile/wai.m3u8 +44a2f73da356e854f8fd05a564ad30ef05aff81a2f26fb316757a07acb79bf3c hls/tv/master.m3u8 +1ea26eaea415f8b459f5994a950e14016a501c2464b9ed8d77db800e16da31d2 hls/tv/wai-v.m3u8 +fc9db3d8e327724c8a7ee793d4b1f1949cf95df1c1941bfcc2bf539fe4184e77 hls/tv/wai.m3u8 +35c672629c384c20029fdc16bc3cb4f7c03b8a057868f5116bb4646f04e39eb0 index-v.wsix +0387c4128de60831575723b868864c82f9d8d129aacec6ad8797db5ca5e5acf4 moq/catalog.json +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +849b7e2cba0f65d3183119f6f4f2c77e5c2a1db9a38d8552369db2dec28df7a7 objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai +22c6777c989d288a6a89876062a244194fde54f5b091b0f603c4ef11f1bc4f60 objects/93439520366affbd9ec0a51d57120d0e45a6b4651884ece2eaf5b8954c188038.wai +83657a16f8686cb03f1da0b5fe3a60ad24b32e1b3f24c17e5b9e5e54720571be objects/98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d.wai +38725131c01f75141fe1790d40209348da7f45241df0bf82a794e8c8196a0fb6 objects/9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +e191fd2fe53e15c30c964e355340cbfbb44562078d6c18c8321c9d3539c43423 objects/a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +767cc608341117aa7c83d39581a6eaa511fcab80d5ff646ad06702ba6c78ec88 package.receipt.json +8454596c213291b896c4483017406c58076e27ad0cab4fab54b850841dfa1a29 plan-v.txt +8d245ca30f6284892c1d026f8d906feecba81d69ed80253f5bbcca6591530c0c policy.json +8ebd338a2e1caa82b8e7470f9926b0c07bb6f3ce7055560d315003118802fdc8 policy.toml +64c2be77a22f1f7ba46e72433d4023cf74237f83d2517d62d6419a04450aa462 stages/legacy.json +4e24738063718168c55f8af00766473c5214c772028ecfd26fa354741eb0c42d stages/mobile.json +a9ae80e6e6d3c9d2b8468964dd5977c18370941143d20282fcf9721207726de6 stages/tv.json diff --git a/wai/pack-conformance/staged-mutations/still-not-the-ladder/index-v.wsix b/wai/pack-conformance/staged-mutations/still-not-the-ladder/index-v.wsix new file mode 100644 index 000000000..2a3c99b8d Binary files /dev/null and b/wai/pack-conformance/staged-mutations/still-not-the-ladder/index-v.wsix differ diff --git a/wai/pack-conformance/staged-mutations/still-not-the-ladder/package.receipt.json b/wai/pack-conformance/staged-mutations/still-not-the-ladder/package.receipt.json new file mode 100644 index 000000000..e65f18c54 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-not-the-ladder/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"354e187c770d3eb6787c40e13fe460f1f27852573caee30137afeacc92d4614e","media_class":"video","stage_root":"c10304943d547971b05fdb57231dcd6e097d7df38cc68ea92c376ddc957e022d"}],"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":"ab2c90a3d795e6d0fec30af3762d616546a6b41737ca62a884b44779db70b0e5","receipt_hash":"40cf6b24f9f2ff1a6a03f8748796d3669b9b2bc14d7544fdd23a1a627330f92d","sig":"6be47cdbd84173902b73449288cd0eedcd16e0f38baba0c51588218922191e9b10c20d69db39b59673062a817b6066dd0ed05dfa86e5349d610e1d57174d2f07","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":484464,"total_bytes":566398,"total_entries":18,"unique_objects":14,"version":2,"work_units":18} \ No newline at end of file diff --git a/wai/pack-conformance/staged-mutations/still-not-the-ladder/policy.json b/wai/pack-conformance/staged-mutations/still-not-the-ladder/policy.json new file mode 100644 index 000000000..39ae28f54 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-not-the-ladder/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","class":[{"floor_only":false,"id":"tv","max_layer":2},{"floor_only":false,"id":"mobile","max_kbps":330,"max_layer":1},{"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-staged-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"f00d8bf055ef725d99f925cc0c1c97b6b7fc956c82872f63840c246b4b69e150"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":60000,"mv_precision":1,"q_candidates":[24,16],"search":4},"role":"base"},{"capability":"wai.video.int_refine","component":"v","id":"r1","params":{"q":12},"refines":"b0","role":"refinement"},{"capability":"wai.video.int_refine","component":"v","id":"r2","params":{"q":4},"refines":"r1","role":"refinement"},{"capability":"wai.image.png","component":"v","id":"still","params":{},"role":"base"},{"capability":"wai.video.av1","component":"v","id":"f0","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0,200,400]},"moment":[{"from_ms":5600,"id":"m-event","tags":["event"],"to_ms":8000}],"royalty_free_only":true,"rule":[{"id":"static","then":{"ladder":["b0","r1","f0"]},"when":{"piece_kind":["static"]}},{"id":"motion","then":{"max_layer":1},"when":{"piece_kind":["motion_high"]}},{"id":"event","then":{"deadline_ms":800},"when":{"moment_tag":["event"]}}],"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/staged-mutations/still-not-the-ladder/policy.toml b/wai/pack-conformance/staged-mutations/still-not-the-ladder/policy.toml new file mode 100644 index 000000000..f4aef7f68 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-not-the-ladder/policy.toml @@ -0,0 +1,94 @@ +schema = "wai.pack.policy/1" +asset_id = "video-staged-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-staged-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 = [24, 16], max_sse_px_milli = 60000 } + +[[ladder]] +id = "r1" +component = "v" +capability = "wai.video.int_refine" +role = "refinement" +refines = "b0" +params = { q = 12 } + +[[ladder]] +id = "r2" +component = "v" +capability = "wai.video.int_refine" +role = "refinement" +refines = "r1" +params = { q = 4 } + +[[ladder]] +id = "still" +component = "v" +capability = "wai.image.png" +role = "base" + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "f00d8bf055ef725d99f925cc0c1c97b6b7fc956c82872f63840c246b4b69e150" + +[live] +layer_lag_ms = [0, 200, 400] + +[[class]] +id = "tv" +max_layer = 2 + +[[class]] +id = "mobile" +max_layer = 1 +max_kbps = 330 + +[[class]] +id = "legacy" +floor_only = true + +[[moment]] +id = "m-event" +from_ms = 5600 +to_ms = 8000 +tags = ["event"] + +[[rule]] +id = "static" +when = { piece_kind = ["static"] } +then = { ladder = ["b0", "r1", "f0"] } + +[[rule]] +id = "motion" +when = { piece_kind = ["motion_high"] } +then = { max_layer = 1 } + +[[rule]] +id = "event" +when = { moment_tag = ["event"] } +then = { deadline_ms = 800 } diff --git a/wai/pack-conformance/staged-mutations/still-not-the-ladder/stages/legacy.json b/wai/pack-conformance/staged-mutations/still-not-the-ladder/stages/legacy.json new file mode 100644 index 000000000..1d82dafec --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-not-the-ladder/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":50,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":50},{"dur":40,"floor_only":true,"p":2,"stages":[],"start":75},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":115},{"dur":60,"floor_only":true,"p":4,"stages":[],"start":140}]}},"policy_digest":"ab2c90a3d795e6d0fec30af3762d616546a6b41737ca62a884b44779db70b0e5"} diff --git a/wai/pack-conformance/staged-mutations/still-not-the-ladder/stages/mobile.json b/wai/pack-conformance/staged-mutations/still-not-the-ladder/stages/mobile.json new file mode 100644 index 000000000..3af2303dc --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-not-the-ladder/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":974,"h":"8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"}],"start":115},{"dur":60,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0}],"start":140}]}},"policy_digest":"ab2c90a3d795e6d0fec30af3762d616546a6b41737ca62a884b44779db70b0e5"} diff --git a/wai/pack-conformance/staged-mutations/still-not-the-ladder/stages/tv.json b/wai/pack-conformance/staged-mutations/still-not-the-ladder/stages/tv.json new file mode 100644 index 000000000..5cf23cc1a --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-not-the-ladder/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0},{"bytes":67779,"deadline_ms":1500,"h":"7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec","layer":1,"refines":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27"}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":50},{"dur":40,"p":2,"stages":[{"bytes":974,"h":"8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f","layer":0}],"start":75},{"dur":25,"p":3,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":115},{"dur":60,"p":4,"stages":[{"bytes":31610,"h":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9","layer":0},{"bytes":81407,"deadline_ms":800,"h":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d","layer":1,"refines":"9fb263069ffb6a50052e79baeeee7c5814106011d57be9c4f6396c5dbd9902a9"},{"bytes":101295,"deadline_ms":800,"h":"a59afe3368e48b835e3f0c170d8fc42b87fce91bb8567f7e8a9b091db0c51b87","layer":2,"refines":"98bb5bbdb41e89f39288a59eb469b152e55b175983e46521ed2bb40999fc511d"}],"start":140}]}},"policy_digest":"ab2c90a3d795e6d0fec30af3762d616546a6b41737ca62a884b44779db70b0e5"} diff --git a/wai/pack-conformance/staged-mutations/still-past-max/LISTING b/wai/pack-conformance/staged-mutations/still-past-max/LISTING new file mode 100644 index 000000000..5126aa3c1 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/LISTING @@ -0,0 +1,35 @@ +1dd9f02cff2cc3b45db466553ff9fc8e2c07303fe30ea2e436662dcaabe9ef75 dash/classic.mpd +c6b1e2f7d9b8506d1ab829a633ea6fa5e1145fc6ecd73746f23f5c56d74aeae7 dash/mobile.mpd +612c156b15f41cae40fe3367bb44e62d86e9fd5ebad65f44b6fad5c4a2603e1d dash/tv.mpd +312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2 floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768 floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +03e68ee03febde6a20fc604ad4d0847df54e626bafbd9f0da433d15aab73a7ad hls/classic/floor-v.m3u8 +76be7b8cc0dd840f0db67800c9606d212322f3a54cb010b2194354956bba612d hls/classic/master.m3u8 +76be7b8cc0dd840f0db67800c9606d212322f3a54cb010b2194354956bba612d hls/legacy/master.m3u8 +b2436c0656f9feb9bf45ea68ab98e5829c215e63d0b34514c4588a01e8401750 hls/mobile/master.m3u8 +13fefa90f537a9fe15766224d40216b249a17df19b156c4c0d0921488cd8cc01 hls/mobile/wai-v.m3u8 +31284c7b277203d780c46aaa97e00e475cf32223e9871fb5ffd2471cdbac9a64 hls/mobile/wai.m3u8 +b2436c0656f9feb9bf45ea68ab98e5829c215e63d0b34514c4588a01e8401750 hls/tv/master.m3u8 +b43798b36c7fe1f197a79290e7d300471d8591b26ab9b3a4e155d3342c1a6c19 hls/tv/wai-v.m3u8 +fd7f47142bf62014377c02de762f2b76c26e681a908f1713e2ba53d90de51377 hls/tv/wai.m3u8 +72b8864c759939e988c6112c758aa8ffe72e6149db5b0e460fecd62aa6b53d46 index-v.wsix +22652936803efca38577556f8a2cc61b4a8dde41c9737d8379855a84d6fb7aa8 moq/catalog.json +fc3c7e6fc88dae24c824d2a0f14af72e86bbb5aa53c4ad6c0d40c9733fa1358b objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai +8cedefd8d5f0ae91768d7107a0a86b07580526f329339e5384c7e710f6c7edbf objects/3522115bc258246c4dd67879e2b9c10f3be41cbc88aec1254e1d26e9d87b4423.wai +588689b9e92b610b20d5f08ce6762adb6d32ac5221fffa3232882434050ad59d objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai +f6876fbfe12e3a3c164efa2ce9d65e2bc066ce5bb2bba16f285de485c9fddb78 objects/66c03382125a5a72828ca5223599007817784a12129e431066290f8ff5df562e.wai +84fcac3182c4dc25b84001bc58e2fa140ae3b8117a42fa4897af0e7ff7a1e920 objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai +849b7e2cba0f65d3183119f6f4f2c77e5c2a1db9a38d8552369db2dec28df7a7 objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai +f9ce66461527b6c1493336ca950908511fe0414fc3d836aff6c045a015090851 objects/a411541dd2eb555c9fd5b6dfe0cd00baae6574568225afa839e36af62f6bbdae.wai +59f1b4eae347ec3f0647ccefa738701dcce32ee83a5fa1c0aca29c3a27c7858d objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai +2f5da0ff307fd4a466607d2ef35ac4fc7215353d70f592c875cafe77c7a2fcc7 objects/d955b5f2232046829b6a5da0c436412c91e97b7d0c16afb07e5f6e68a39ef66c.wai +bf7bb63e1e3f03967f8da44a5700f4ded079e6d4d9a6c0ff9503c458269c486f objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai +d8a93f21183c28797a913f0d418ff9ce2b57bb10118f6bcd6621d626677f6437 package.receipt.json +6261e5b48f122bd416b9f71b2416fba9f1e3c17b67682ba6a3f26e0a27ef9f5e plan-v.txt +11536aab05b3fe23aa428a52617da9d156f6326d42ee5b8785af4713b436ecf2 policy.json +61ad0d0ba75977d401ebaeb00b5a8f5795f3b7a1408018fcbb02038f2c412e16 policy.toml +1fa994294a440e0b5b16a2160fe6dfcf917400862e8d84bae99c8e6c0be19fcb stages/legacy.json +43ba56b1cafbedd9b02b757f1ee91dd58b09003fd66723f0e13f67befd2dedc5 stages/mobile.json +bad2931b8833d813a4bec6fb8d5d6ef1042891b4b0fec33e7229407e1cdd423d stages/tv.json diff --git a/wai/pack-conformance/staged-mutations/still-past-max/dash/classic.mpd b/wai/pack-conformance/staged-mutations/still-past-max/dash/classic.mpd new file mode 100644 index 000000000..e9d9c90ec --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/dash/classic.mpd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/staged-mutations/still-past-max/dash/mobile.mpd b/wai/pack-conformance/staged-mutations/still-past-max/dash/mobile.mpd new file mode 100644 index 000000000..8ee10f0a9 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/dash/mobile.mpd @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/staged-mutations/still-past-max/dash/tv.mpd b/wai/pack-conformance/staged-mutations/still-past-max/dash/tv.mpd new file mode 100644 index 000000000..120e1e1a1 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/dash/tv.mpd @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/staged-mutations/still-past-max/hls/classic/floor-v.m3u8 b/wai/pack-conformance/staged-mutations/still-past-max/hls/classic/floor-v.m3u8 new file mode 100644 index 000000000..1292b1000 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/hls/classic/floor-v.m3u8 @@ -0,0 +1,14 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:171798692 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/v/init.mp4" +#EXTINF:2.000, +../../floor/v/39f61548f8f65c68612e02ed11803c3477ed293bc1be12608be69ffd652de768.m4s +#EXTINF:1.000, +../../floor/v/470bbd4e4bc7d8c7a14adafdf50ae5c256fb5086b13389099cf0babf164986fa.m4s +#EXTINF:171798691.800, +../../floor/v/312da579d4bf042888e3ee044b8346360fad3032cdb8cacb7fcf19d7a26c3ba2.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/still-past-max/hls/classic/master.m3u8 b/wai/pack-conformance/staged-mutations/still-past-max/hls/classic/master.m3u8 new file mode 100644 index 000000000..57ba51d6c --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/hls/classic/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=8840,AVERAGE-BANDWIDTH=1,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/still-past-max/hls/legacy/master.m3u8 b/wai/pack-conformance/staged-mutations/still-past-max/hls/legacy/master.m3u8 new file mode 100644 index 000000000..57ba51d6c --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/hls/legacy/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=8840,AVERAGE-BANDWIDTH=1,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/still-past-max/hls/mobile/master.m3u8 b/wai/pack-conformance/staged-mutations/still-past-max/hls/mobile/master.m3u8 new file mode 100644 index 000000000..f190a7fdd --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/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=8840,AVERAGE-BANDWIDTH=1,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/still-past-max/hls/mobile/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/still-past-max/hls/mobile/wai-v.m3u8 new file mode 100644 index 000000000..0de63220d --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/hls/mobile/wai-v.m3u8 @@ -0,0 +1,15 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:171798692 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:2.000, +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:171798691.800, +../../objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai#n=974 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/still-past-max/hls/mobile/wai.m3u8 b/wai/pack-conformance/staged-mutations/still-past-max/hls/mobile/wai.m3u8 new file mode 100644 index 000000000..d0eb98cb7 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/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=323176,AVERAGE-BANDWIDTH=177139,CODECS="wai1.video.int_motion+video.int_refine",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/still-past-max/hls/tv/master.m3u8 b/wai/pack-conformance/staged-mutations/still-past-max/hls/tv/master.m3u8 new file mode 100644 index 000000000..f190a7fdd --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/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=8840,AVERAGE-BANDWIDTH=1,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/still-past-max/hls/tv/wai-v.m3u8 b/wai/pack-conformance/staged-mutations/still-past-max/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..12593a511 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/hls/tv/wai-v.m3u8 @@ -0,0 +1,17 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:171798692 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:2.000, +#WAI-LAYER:N=1,URI="../../objects/7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec.wai#n=67779" +../../objects/cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27.wai#n=26030 +#EXTINF:1.000, +#WAI-LAYER:N=1,URI="../../objects/656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b.wai#n=30699" +#WAI-LAYER:N=2,URI="../../objects/f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9.wai#n=40303" +../../objects/2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61.wai#n=9698 +#EXT-X-GAP +#EXTINF:171798691.800, +../../objects/8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f.wai#n=974 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/staged-mutations/still-past-max/hls/tv/wai.m3u8 b/wai/pack-conformance/staged-mutations/still-past-max/hls/tv/wai.m3u8 new file mode 100644 index 000000000..e60a26af9 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/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=645600,AVERAGE-BANDWIDTH=465358,CODECS="wai1.video.int_motion+video.int_refine",X-WAI-FLOOR="wai1.video.int_motion+video.int_refine",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/staged-mutations/still-past-max/index-v.wsix b/wai/pack-conformance/staged-mutations/still-past-max/index-v.wsix new file mode 100644 index 000000000..dade532db Binary files /dev/null and b/wai/pack-conformance/staged-mutations/still-past-max/index-v.wsix differ diff --git a/wai/pack-conformance/staged-mutations/still-past-max/moq/catalog.json b/wai/pack-conformance/staged-mutations/still-past-max/moq/catalog.json new file mode 100644 index 000000000..340931feb --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/moq/catalog.json @@ -0,0 +1 @@ +{"tracks":[{"altGroup":1,"isLive":false,"name":"v/l0","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_motion","wai.image.png"],"wai_determinism":"decode-equivalence","wai_receipt_alias":6826975590310646,"wai_receipts":true,"wai_receipts_track":"v/l0/receipts","wai_staged":{"layer":0,"lead_ms":null,"set":"v"}},{"depends":["v/l0"],"isLive":false,"name":"v/l1","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":1300,"wai_determinism":"decode-equivalence","wai_receipt_alias":5745064158311842,"wai_receipts":true,"wai_receipts_track":"v/l1/receipts","wai_staged":{"layer":1,"lead_ms":null,"set":"v"}},{"depends":["v/l0","v/l1"],"isLive":false,"name":"v/l2","namespace":"video-staged-01","packaging":"wai","renderGroup":1,"role":"video","wai_capabilities":["wai.video.int_refine"],"wai_delivery_timeout_ms":1100,"wai_determinism":"decode-equivalence","wai_receipt_alias":6697012243957121,"wai_receipts":true,"wai_receipts_track":"v/l2/receipts","wai_staged":{"layer":2,"lead_ms":null,"set":"v"}},{"altGroup":1,"codec":"av01.0.00M.08","height":36,"name":"v/floor","namespace":"video-staged-01","packaging":"cmaf","renderGroup":1,"width":64},{"isLive":false,"name":"v/l0/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l1/receipts","namespace":"video-staged-01","packaging":"wai-receipts"},{"isLive":false,"name":"v/l2/receipts","namespace":"video-staged-01","packaging":"wai-receipts"}],"version":1} diff --git a/wai/pack-conformance/staged-mutations/still-past-max/package.receipt.json b/wai/pack-conformance/staged-mutations/still-past-max/package.receipt.json new file mode 100644 index 000000000..3665c6c6d --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-staged-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"def786bca935792aa5e8e076c5aa64628533e5ba1ce52cbbe070295482d170b1","media_class":"video","stage_root":"4bcbbc9b81de159ed100e3dc118187dfc9ce432bcb5942d2552b183eb66bff1f"}],"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":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57","receipt_hash":"6393d087ac18dfbdbb923ae0e2cf3b826cc87762052ef7813bef66e82c43df56","sig":"e82501852a56cc9118e48f3680f4d7433837222c5a348e5cf7c70144b72bc1b456afee0e17997290558acec5367de61f2caf6a26d789b472b1b3268fcb228305","signer_id":"did:key:z6MkhGok4PzLmtw1v5uW3Bpip8gHHifrCt42QJn4gEBfRVQG","signer_pubkey":"29e5833a915a6429a4e3a7948475c338ef436eb82be89c92f059704403db9d55","stored_bytes":267309,"total_bytes":267309,"total_entries":10,"unique_objects":10,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/staged-mutations/still-past-max/plan-v.txt b/wai/pack-conformance/staged-mutations/still-past-max/plan-v.txt new file mode 100644 index 000000000..941262bd3 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/plan-v.txt @@ -0,0 +1,3 @@ +0 +50 +75 diff --git a/wai/pack-conformance/staged-mutations/still-past-max/stages/legacy.json b/wai/pack-conformance/staged-mutations/still-past-max/stages/legacy.json new file mode 100644 index 000000000..26eccd4f1 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":50,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":50},{"dur":4294967295,"floor_only":true,"p":2,"stages":[],"start":75}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/still-past-max/stages/mobile.json b/wai/pack-conformance/staged-mutations/still-past-max/stages/mobile.json new file mode 100644 index 000000000..449191cc0 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"}],"start":50},{"dur":4294967295,"p":2,"stages":[{"bytes":974,"h":"8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f","layer":0}],"start":75}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/staged-mutations/still-past-max/stages/tv.json b/wai/pack-conformance/staged-mutations/still-past-max/stages/tv.json new file mode 100644 index 000000000..4d9ae4ab7 --- /dev/null +++ b/wai/pack-conformance/staged-mutations/still-past-max/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":50,"p":0,"stages":[{"bytes":26030,"h":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27","layer":0},{"bytes":67779,"deadline_ms":1500,"h":"7aeabafc9c50d289b5f65a7480a88774f94be9eec5b706c16835a59bbe59a0ec","layer":1,"refines":"cf015505a36b0b76b50c41f15f3a0210163886054d53d35f1c4d414bf1c22d27"}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9698,"h":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61","layer":0},{"bytes":30699,"deadline_ms":1500,"h":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b","layer":1,"refines":"2b95867d323012d663b0f8c88fc8d8d0c3b5642a9784f00d30ce2ab176341c61"},{"bytes":40303,"deadline_ms":1500,"h":"f5cacd6f092d317d0972e3d8ed4efcf4702b820bd5295aebdd252dd8dbc409e9","layer":2,"refines":"656e5f72c8b8165d9f59f30a6cbca465383cc3d478b6474e2a91e592796f320b"}],"start":50},{"dur":4294967295,"p":2,"stages":[{"bytes":974,"h":"8173f0eca11aaae0c29d415aedb60252cd8c8c32460c13e124ad70d9a4acd90f","layer":0}],"start":75}]}},"policy_digest":"1bed669917d9d78e1a0f65a5a998db50e98e3dbdf836cc2ae00ff2ac97d67e57"} diff --git a/wai/pack-conformance/stills/ancillary.png b/wai/pack-conformance/stills/ancillary.png new file mode 100644 index 000000000..7aaba1cc9 Binary files /dev/null and b/wai/pack-conformance/stills/ancillary.png differ diff --git a/wai/pack-conformance/stills/bytes-after-end.png b/wai/pack-conformance/stills/bytes-after-end.png new file mode 100644 index 000000000..f6884ee3e Binary files /dev/null and b/wai/pack-conformance/stills/bytes-after-end.png differ diff --git a/wai/pack-conformance/stills/bytes-after-stream.png b/wai/pack-conformance/stills/bytes-after-stream.png new file mode 100644 index 000000000..c7e4c1acd Binary files /dev/null and b/wai/pack-conformance/stills/bytes-after-stream.png differ diff --git a/wai/pack-conformance/stills/chunk-between-data.png b/wai/pack-conformance/stills/chunk-between-data.png new file mode 100644 index 000000000..e9163447b Binary files /dev/null and b/wai/pack-conformance/stills/chunk-between-data.png differ diff --git a/wai/pack-conformance/stills/crc.png b/wai/pack-conformance/stills/crc.png new file mode 100644 index 000000000..d800e30c1 Binary files /dev/null and b/wai/pack-conformance/stills/crc.png differ diff --git a/wai/pack-conformance/stills/data-split.png b/wai/pack-conformance/stills/data-split.png new file mode 100644 index 000000000..9b9c80fd1 Binary files /dev/null and b/wai/pack-conformance/stills/data-split.png differ diff --git a/wai/pack-conformance/stills/expected.json b/wai/pack-conformance/stills/expected.json new file mode 100644 index 000000000..1a84e6819 --- /dev/null +++ b/wai/pack-conformance/stills/expected.json @@ -0,0 +1,92 @@ +{ + "width": 5, + "height": 3, + "cases": [ + { + "file": "plain.png", + "verdict": "ok", + "rgb_sha256": "dda77728ad8fa93b3f7ee3446053cba09376c63660e90b064b42a68e43850dfb" + }, + { + "file": "palette.png", + "verdict": "ok", + "rgb_sha256": "dda77728ad8fa93b3f7ee3446053cba09376c63660e90b064b42a68e43850dfb" + }, + { + "file": "ancillary.png", + "verdict": "ok", + "rgb_sha256": "dda77728ad8fa93b3f7ee3446053cba09376c63660e90b064b42a68e43850dfb" + }, + { + "file": "data-split.png", + "verdict": "ok", + "rgb_sha256": "dda77728ad8fa93b3f7ee3446053cba09376c63660e90b064b42a68e43850dfb" + }, + { + "file": "module-still.png", + "verdict": "ok", + "rgb_sha256": "dda77728ad8fa93b3f7ee3446053cba09376c63660e90b064b42a68e43850dfb" + }, + { + "file": "lone-one-bit-distance.png", + "verdict": "ok", + "rgb_sha256": "dda77728ad8fa93b3f7ee3446053cba09376c63660e90b064b42a68e43850dfb" + }, + { + "file": "still-incomplete-dist.png", + "verdict": "refused" + }, + { + "file": "interlace.png", + "verdict": "refused" + }, + { + "file": "bytes-after-end.png", + "verdict": "refused" + }, + { + "file": "short-row.png", + "verdict": "refused" + }, + { + "file": "palette-after-data.png", + "verdict": "refused" + }, + { + "file": "unknown-critical.png", + "verdict": "refused" + }, + { + "file": "no-end.png", + "verdict": "refused" + }, + { + "file": "chunk-between-data.png", + "verdict": "refused" + }, + { + "file": "crc.png", + "verdict": "refused" + }, + { + "file": "filter-5.png", + "verdict": "refused" + }, + { + "file": "rgba.png", + "verdict": "refused" + }, + { + "file": "window-past-32k.png", + "verdict": "refused" + }, + { + "file": "bytes-after-stream.png", + "verdict": "refused" + }, + { + "file": "stream-checksum.png", + "verdict": "refused" + } + ] +} diff --git a/wai/pack-conformance/stills/filter-5.png b/wai/pack-conformance/stills/filter-5.png new file mode 100644 index 000000000..e3a252b2c Binary files /dev/null and b/wai/pack-conformance/stills/filter-5.png differ diff --git a/wai/pack-conformance/stills/interlace.png b/wai/pack-conformance/stills/interlace.png new file mode 100644 index 000000000..9280e6bf0 Binary files /dev/null and b/wai/pack-conformance/stills/interlace.png differ diff --git a/wai/pack-conformance/stills/lone-one-bit-distance.png b/wai/pack-conformance/stills/lone-one-bit-distance.png new file mode 100644 index 000000000..fdc6b5ac4 Binary files /dev/null and b/wai/pack-conformance/stills/lone-one-bit-distance.png differ diff --git a/wai/pack-conformance/stills/module-still.png b/wai/pack-conformance/stills/module-still.png new file mode 100644 index 000000000..4de6a08c4 Binary files /dev/null and b/wai/pack-conformance/stills/module-still.png differ diff --git a/wai/pack-conformance/stills/no-end.png b/wai/pack-conformance/stills/no-end.png new file mode 100644 index 000000000..77a2948fc Binary files /dev/null and b/wai/pack-conformance/stills/no-end.png differ diff --git a/wai/pack-conformance/stills/palette-after-data.png b/wai/pack-conformance/stills/palette-after-data.png new file mode 100644 index 000000000..32dbb3858 Binary files /dev/null and b/wai/pack-conformance/stills/palette-after-data.png differ diff --git a/wai/pack-conformance/stills/palette.png b/wai/pack-conformance/stills/palette.png new file mode 100644 index 000000000..d0e70bc0f Binary files /dev/null and b/wai/pack-conformance/stills/palette.png differ diff --git a/wai/pack-conformance/stills/plain.png b/wai/pack-conformance/stills/plain.png new file mode 100644 index 000000000..c066a0d5e Binary files /dev/null and b/wai/pack-conformance/stills/plain.png differ diff --git a/wai/pack-conformance/stills/rgba.png b/wai/pack-conformance/stills/rgba.png new file mode 100644 index 000000000..8724d0995 Binary files /dev/null and b/wai/pack-conformance/stills/rgba.png differ diff --git a/wai/pack-conformance/stills/short-row.png b/wai/pack-conformance/stills/short-row.png new file mode 100644 index 000000000..1a044cd67 Binary files /dev/null and b/wai/pack-conformance/stills/short-row.png differ diff --git a/wai/pack-conformance/stills/still-incomplete-dist.png b/wai/pack-conformance/stills/still-incomplete-dist.png new file mode 100644 index 000000000..8afd3405d Binary files /dev/null and b/wai/pack-conformance/stills/still-incomplete-dist.png differ diff --git a/wai/pack-conformance/stills/stream-checksum.png b/wai/pack-conformance/stills/stream-checksum.png new file mode 100644 index 000000000..fd2662ca6 Binary files /dev/null and b/wai/pack-conformance/stills/stream-checksum.png differ diff --git a/wai/pack-conformance/stills/unknown-critical.png b/wai/pack-conformance/stills/unknown-critical.png new file mode 100644 index 000000000..d4555b35c Binary files /dev/null and b/wai/pack-conformance/stills/unknown-critical.png differ diff --git a/wai/pack-conformance/stills/window-past-32k.png b/wai/pack-conformance/stills/window-past-32k.png new file mode 100644 index 000000000..da2ef01d8 Binary files /dev/null and b/wai/pack-conformance/stills/window-past-32k.png differ diff --git a/wai/pack-conformance/supplied_floor.py b/wai/pack-conformance/supplied_floor.py new file mode 100644 index 000000000..d01635825 --- /dev/null +++ b/wai/pack-conformance/supplied_floor.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Write a golden package's supplied floor (extensions/packaging.md §7.3) from its fixture. + +The floor of a golden is an AV1 coding of its generated fixture, so its every frame comes +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`; +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. + +An encoder is not a pure function of its input across versions, so the stream is committed +and pinned by its listing: a packer checks the floor against the listing and the plan, and +never encodes it. This script is how the committed stream was written, not a step of +verification. + + python3 supplied_floor.py directory> [aomenc] +""" +import os +import re +import subprocess +import sys +import tempfile + + +def run(*args): + return subprocess.run(args, check=True, capture_output=True).stdout + + +def frames_of(y4m): + """The stream header line and each frame's bytes (its FRAME line included).""" + head_end = y4m.index(b"\n") + 1 + head = y4m[:head_end] + w = int(re.search(rb" W(\d+)", head).group(1)) + h = int(re.search(rb" H(\d+)", head).group(1)) + size = w * h + 2 * ((w + 1) // 2) * ((h + 1) // 2) + out, at = [], head_end + while at < len(y4m): + line = y4m.index(b"\n", at) + 1 + out.append(y4m[at:line + size]) + at = line + size + return head, out + + +def main(argv): + if len(argv) < 3: + print(__doc__) + return 2 + wai_pack, pack = argv[1], os.path.abspath(argv[2]) + aomenc = argv[3] if len(argv) > 3 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) + 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") + 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) + 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}") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index ca312240b..59e0b6d5d 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -1758,13 +1758,6 @@ "section": "§4", "status": "defined" }, - { - "name": "wai_layer", - "kind": "catalog", - "document": "extensions/packaging.md", - "section": "§8", - "status": "defined" - }, { "name": "wai_receipt_alias", "kind": "catalog", diff --git a/wai/staged-measure-conformance/README.md b/wai/staged-measure-conformance/README.md index ee75844aa..891e2a01f 100644 --- a/wai/staged-measure-conformance/README.md +++ b/wai/staged-measure-conformance/README.md @@ -62,6 +62,9 @@ identity (Appendix D). Across the corpus: the record-wide deduplication totals ( with no refinement timeout, whose stall passes the bound of the same `fifo` link with a short timeout (`fifo-timeout-short`), and `fifo` decode on one worker, whose stall passes the bound of the same plan with `base-first` decode (`strict-window`). + The model `per-track` gives each refinement a delivery timeout of its own, one per + layer, as a MoQ track's `DELIVERY_TIMEOUT` does (Appendix B): with them a `fifo` link + with no plan timeout is bounded, by the longest of them. **Provenance.** Generated here. Each base is a committed vector under [`../wai-rs/tests/vectors/`](../wai-rs/tests/vectors/), generated too (that directory's @@ -112,6 +115,12 @@ steps (the curve of §6.3). `wai-rs`: Appendix B implemented from the text, with each fixture's model taken from `measure_verify.py`'s own evaluation. It reproduces every result, checks every claim, and requires a counter-case for each part of the bound a schedule can lose. +- `sim_diff.py [seed] [count]` runs the simulator differentially: + seeded random models (groups of a base and up to three layers, each layer one object + or one per unit, a refinement with its own delivery timeout or the plan's) under + random plans, each simulated by `wai_stage_measure sim` and by `sim_verify.py`. The + two must agree on the refusal, or on the whole result, the stall bound (§10.4) and the + longest stall; and where the bound applies, every stall must be within it. - `measure_verify.py .`: pure Python with the standard library, sharing no code with `wai-rs`. It reimplements, from the extension and SPEC: the byte rule and the formats it counts; each base's units from its case's stage 0; every layer applied in layer diff --git a/wai/staged-measure-conformance/sim/cases.json b/wai/staged-measure-conformance/sim/cases.json index 08e336a8d..8deb0b169 100644 --- a/wai/staged-measure-conformance/sim/cases.json +++ b/wai/staged-measure-conformance/sim/cases.json @@ -86,6 +86,24 @@ "plan": "plans/reorder-decode.json", "claim": "within-bound" }, + { + "name": "per-track/strict-slow-timeout", + "model": "models/per-track.json", + "plan": "plans/strict-slow-timeout.json", + "claim": "within-bound" + }, + { + "name": "per-track/fifo-timeout-subgroup", + "model": "models/per-track.json", + "plan": "plans/fifo-timeout-subgroup.json", + "claim": "within-bound" + }, + { + "name": "per-track/fifo-no-timeout", + "model": "models/per-track.json", + "plan": "plans/fifo-no-timeout.json", + "claim": "within-bound" + }, { "name": "audio/strict-subgroup", "model": "fixture:audio", diff --git a/wai/staged-measure-conformance/sim/models/per-track.json b/wai/staged-measure-conformance/sim/models/per-track.json new file mode 100644 index 000000000..5698e2378 --- /dev/null +++ b/wai/staged-measure-conformance/sim/models/per-track.json @@ -0,0 +1,399 @@ +{ + "format": "wai-sim-model/1", + "unit_us": 40000, + "objects": [ + { + "id": "g0-base", + "base": 0, + "layer": 0, + "units": [ + 0, + 5 + ], + "bytes": 5000, + "capture_us": 200000 + }, + { + "id": "g0-l1-u0", + "base": 0, + "layer": 1, + "units": [ + 0, + 1 + ], + "bytes": 1500, + "capture_us": 200000, + "timeout_us": 60000 + }, + { + "id": "g0-l1-u1", + "base": 0, + "layer": 1, + "units": [ + 1, + 1 + ], + "bytes": 1537, + "capture_us": 200000, + "timeout_us": 60000 + }, + { + "id": "g0-l1-u2", + "base": 0, + "layer": 1, + "units": [ + 2, + 1 + ], + "bytes": 1574, + "capture_us": 200000, + "timeout_us": 60000 + }, + { + "id": "g0-l1-u3", + "base": 0, + "layer": 1, + "units": [ + 3, + 1 + ], + "bytes": 1611, + "capture_us": 200000, + "timeout_us": 60000 + }, + { + "id": "g0-l1-u4", + "base": 0, + "layer": 1, + "units": [ + 4, + 1 + ], + "bytes": 1648, + "capture_us": 200000, + "timeout_us": 60000 + }, + { + "id": "g0-l2-u0", + "base": 0, + "layer": 2, + "units": [ + 0, + 1 + ], + "bytes": 900, + "capture_us": 200000, + "timeout_us": 20000 + }, + { + "id": "g0-l2-u1", + "base": 0, + "layer": 2, + "units": [ + 1, + 1 + ], + "bytes": 937, + "capture_us": 200000, + "timeout_us": 20000 + }, + { + "id": "g0-l2-u2", + "base": 0, + "layer": 2, + "units": [ + 2, + 1 + ], + "bytes": 974, + "capture_us": 200000, + "timeout_us": 20000 + }, + { + "id": "g0-l2-u3", + "base": 0, + "layer": 2, + "units": [ + 3, + 1 + ], + "bytes": 1011, + "capture_us": 200000, + "timeout_us": 20000 + }, + { + "id": "g0-l2-u4", + "base": 0, + "layer": 2, + "units": [ + 4, + 1 + ], + "bytes": 1048, + "capture_us": 200000, + "timeout_us": 20000 + }, + { + "id": "g1-base", + "base": 1, + "layer": 0, + "units": [ + 5, + 5 + ], + "bytes": 5700, + "capture_us": 400000 + }, + { + "id": "g1-l1-u0", + "base": 1, + "layer": 1, + "units": [ + 5, + 1 + ], + "bytes": 1511, + "capture_us": 400000, + "timeout_us": 60000 + }, + { + "id": "g1-l1-u1", + "base": 1, + "layer": 1, + "units": [ + 6, + 1 + ], + "bytes": 1548, + "capture_us": 400000, + "timeout_us": 60000 + }, + { + "id": "g1-l1-u2", + "base": 1, + "layer": 1, + "units": [ + 7, + 1 + ], + "bytes": 1585, + "capture_us": 400000, + "timeout_us": 60000 + }, + { + "id": "g1-l1-u3", + "base": 1, + "layer": 1, + "units": [ + 8, + 1 + ], + "bytes": 1622, + "capture_us": 400000, + "timeout_us": 60000 + }, + { + "id": "g1-l1-u4", + "base": 1, + "layer": 1, + "units": [ + 9, + 1 + ], + "bytes": 1659, + "capture_us": 400000, + "timeout_us": 60000 + }, + { + "id": "g1-l2-u0", + "base": 1, + "layer": 2, + "units": [ + 5, + 1 + ], + "bytes": 911, + "capture_us": 400000, + "timeout_us": 20000 + }, + { + "id": "g1-l2-u1", + "base": 1, + "layer": 2, + "units": [ + 6, + 1 + ], + "bytes": 948, + "capture_us": 400000, + "timeout_us": 20000 + }, + { + "id": "g1-l2-u2", + "base": 1, + "layer": 2, + "units": [ + 7, + 1 + ], + "bytes": 985, + "capture_us": 400000, + "timeout_us": 20000 + }, + { + "id": "g1-l2-u3", + "base": 1, + "layer": 2, + "units": [ + 8, + 1 + ], + "bytes": 1022, + "capture_us": 400000, + "timeout_us": 20000 + }, + { + "id": "g1-l2-u4", + "base": 1, + "layer": 2, + "units": [ + 9, + 1 + ], + "bytes": 1059, + "capture_us": 400000, + "timeout_us": 20000 + }, + { + "id": "g2-base", + "base": 2, + "layer": 0, + "units": [ + 10, + 5 + ], + "bytes": 6400, + "capture_us": 600000 + }, + { + "id": "g2-l1-u0", + "base": 2, + "layer": 1, + "units": [ + 10, + 1 + ], + "bytes": 1522, + "capture_us": 600000, + "timeout_us": 60000 + }, + { + "id": "g2-l1-u1", + "base": 2, + "layer": 1, + "units": [ + 11, + 1 + ], + "bytes": 1559, + "capture_us": 600000, + "timeout_us": 60000 + }, + { + "id": "g2-l1-u2", + "base": 2, + "layer": 1, + "units": [ + 12, + 1 + ], + "bytes": 1596, + "capture_us": 600000, + "timeout_us": 60000 + }, + { + "id": "g2-l1-u3", + "base": 2, + "layer": 1, + "units": [ + 13, + 1 + ], + "bytes": 1633, + "capture_us": 600000, + "timeout_us": 60000 + }, + { + "id": "g2-l1-u4", + "base": 2, + "layer": 1, + "units": [ + 14, + 1 + ], + "bytes": 1670, + "capture_us": 600000, + "timeout_us": 60000 + }, + { + "id": "g2-l2-u0", + "base": 2, + "layer": 2, + "units": [ + 10, + 1 + ], + "bytes": 922, + "capture_us": 600000, + "timeout_us": 20000 + }, + { + "id": "g2-l2-u1", + "base": 2, + "layer": 2, + "units": [ + 11, + 1 + ], + "bytes": 959, + "capture_us": 600000, + "timeout_us": 20000 + }, + { + "id": "g2-l2-u2", + "base": 2, + "layer": 2, + "units": [ + 12, + 1 + ], + "bytes": 996, + "capture_us": 600000, + "timeout_us": 20000 + }, + { + "id": "g2-l2-u3", + "base": 2, + "layer": 2, + "units": [ + 13, + 1 + ], + "bytes": 1033, + "capture_us": 600000, + "timeout_us": 20000 + }, + { + "id": "g2-l2-u4", + "base": 2, + "layer": 2, + "units": [ + 14, + 1 + ], + "bytes": 1070, + "capture_us": 600000, + "timeout_us": 20000 + } + ] +} diff --git a/wai/staged-measure-conformance/sim/results.json b/wai/staged-measure-conformance/sim/results.json index 486d84bc8..9816943db 100644 --- a/wai/staged-measure-conformance/sim/results.json +++ b/wai/staged-measure-conformance/sim/results.json @@ -5597,6 +5597,1268 @@ ] } }, + { + "name": "per-track/strict-slow-timeout", + "stall_bound_us": 5000, + "max_stall_us": 0, + "result": { + "objects": [ + { + "id": "g0-base", + "sent_first_us": 20000, + "arrived_us": 52500, + "state": "delivered" + }, + { + "id": "g0-l1-u0", + "sent_first_us": 62750, + "arrived_us": 86500, + "state": "delivered" + }, + { + "id": "g0-l1-u1", + "sent_first_us": 66500, + "arrived_us": 90343, + "state": "delivered" + }, + { + "id": "g0-l1-u2", + "sent_first_us": 70343, + "arrived_us": 94278, + "state": "delivered" + }, + { + "id": "g0-l1-u3", + "sent_first_us": 74278, + "arrived_us": 98306, + "state": "delivered" + }, + { + "id": "g0-l1-u4", + "sent_first_us": 78306, + "arrived_us": null, + "state": "abandoned" + }, + { + "id": "g0-l2-u0", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g0-l2-u1", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g0-l2-u2", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g0-l2-u3", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g0-l2-u4", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-base", + "sent_first_us": 32500, + "arrived_us": 66750, + "state": "delivered" + }, + { + "id": "g1-l1-u0", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l1-u1", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l1-u2", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l1-u3", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l1-u4", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l2-u0", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l2-u1", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l2-u2", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l2-u3", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l2-u4", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-base", + "sent_first_us": 46750, + "arrived_us": 82750, + "state": "delivered" + }, + { + "id": "g2-l1-u0", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l1-u1", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l1-u2", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l1-u3", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l1-u4", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u0", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u1", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u2", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u3", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u4", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + } + ], + "presentations": [ + [ + 0, + 1, + 56500 + ], + [ + 0, + 2, + 88750 + ], + [ + 1, + 1, + 56500 + ], + [ + 1, + 2, + 92343 + ], + [ + 2, + 1, + 56500 + ], + [ + 2, + 2, + 96278 + ], + [ + 3, + 1, + 56500 + ], + [ + 3, + 2, + 100306 + ], + [ + 4, + 1, + 56500 + ], + [ + 5, + 1, + 70750 + ], + [ + 6, + 1, + 70750 + ], + [ + 7, + 1, + 70750 + ], + [ + 8, + 1, + 70750 + ], + [ + 9, + 1, + 70750 + ], + [ + 10, + 1, + 86750 + ], + [ + 11, + 1, + 86750 + ], + [ + 12, + 1, + 86750 + ], + [ + 13, + 1, + 86750 + ], + [ + 14, + 1, + 86750 + ] + ], + "bfpf_sched": 6200, + "tfpf_us": 56500, + "tffs_us": [ + 56500, + 88750, + null + ], + "offered": 55650, + "delivered": 23322, + "dropped": 31128, + "abandoned": 1200, + "never_sent_capped": 0, + "missed": 0, + "stall_us": [ + [ + 0, + 0 + ], + [ + 1, + 0 + ], + [ + 2, + 0 + ], + [ + 3, + 0 + ], + [ + 4, + 0 + ], + [ + 5, + 0 + ], + [ + 6, + 0 + ], + [ + 7, + 0 + ], + [ + 8, + 0 + ], + [ + 9, + 0 + ], + [ + 10, + 0 + ], + [ + 11, + 0 + ], + [ + 12, + 0 + ], + [ + 13, + 0 + ], + [ + 14, + 0 + ] + ] + } + }, + { + "name": "per-track/fifo-timeout-subgroup", + "stall_bound_us": 65000, + "max_stall_us": 34287, + "result": { + "objects": [ + { + "id": "g0-base", + "sent_first_us": 20000, + "arrived_us": 52500, + "state": "delivered" + }, + { + "id": "g0-l1-u0", + "sent_first_us": 32500, + "arrived_us": 56250, + "state": "delivered" + }, + { + "id": "g0-l1-u1", + "sent_first_us": 36250, + "arrived_us": 60093, + "state": "delivered" + }, + { + "id": "g0-l1-u2", + "sent_first_us": 40093, + "arrived_us": 64028, + "state": "delivered" + }, + { + "id": "g0-l1-u3", + "sent_first_us": 44028, + "arrived_us": 68056, + "state": "delivered" + }, + { + "id": "g0-l1-u4", + "sent_first_us": 48056, + "arrived_us": 72176, + "state": "delivered" + }, + { + "id": "g0-l2-u0", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g0-l2-u1", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g0-l2-u2", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g0-l2-u3", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g0-l2-u4", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-base", + "sent_first_us": 52176, + "arrived_us": 86426, + "state": "delivered" + }, + { + "id": "g1-l1-u0", + "sent_first_us": 66426, + "arrived_us": 90204, + "state": "delivered" + }, + { + "id": "g1-l1-u1", + "sent_first_us": 70204, + "arrived_us": 94074, + "state": "delivered" + }, + { + "id": "g1-l1-u2", + "sent_first_us": 74074, + "arrived_us": 98037, + "state": "delivered" + }, + { + "id": "g1-l1-u3", + "sent_first_us": 78037, + "arrived_us": null, + "state": "abandoned" + }, + { + "id": "g1-l1-u4", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l2-u0", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l2-u1", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l2-u2", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l2-u3", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g1-l2-u4", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-base", + "sent_first_us": 81037, + "arrived_us": 117037, + "state": "delivered" + }, + { + "id": "g2-l1-u0", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l1-u1", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l1-u2", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l1-u3", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l1-u4", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u0", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u1", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u2", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u3", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u4", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + } + ], + "presentations": [ + [ + 0, + 1, + 56500 + ], + [ + 0, + 2, + 58500 + ], + [ + 1, + 1, + 56500 + ], + [ + 1, + 2, + 62093 + ], + [ + 2, + 1, + 56500 + ], + [ + 2, + 2, + 66028 + ], + [ + 3, + 1, + 56500 + ], + [ + 3, + 2, + 70056 + ], + [ + 4, + 1, + 56500 + ], + [ + 4, + 2, + 74176 + ], + [ + 5, + 1, + 90426 + ], + [ + 5, + 2, + 92426 + ], + [ + 6, + 1, + 90426 + ], + [ + 6, + 2, + 96074 + ], + [ + 7, + 1, + 90426 + ], + [ + 7, + 2, + 100037 + ], + [ + 8, + 1, + 90426 + ], + [ + 9, + 1, + 90426 + ], + [ + 10, + 1, + 121037 + ], + [ + 11, + 1, + 121037 + ], + [ + 12, + 1, + 121037 + ], + [ + 13, + 1, + 121037 + ], + [ + 14, + 1, + 121037 + ] + ], + "bfpf_sched": 6500, + "tfpf_us": 56500, + "tffs_us": [ + 56500, + 58500, + null + ], + "offered": 55650, + "delivered": 29614, + "dropped": 24836, + "abandoned": 1200, + "never_sent_capped": 0, + "missed": 0, + "stall_us": [ + [ + 0, + 0 + ], + [ + 1, + 0 + ], + [ + 2, + 0 + ], + [ + 3, + 0 + ], + [ + 4, + 0 + ], + [ + 5, + 19676 + ], + [ + 6, + 19676 + ], + [ + 7, + 19676 + ], + [ + 8, + 19676 + ], + [ + 9, + 19676 + ], + [ + 10, + 34287 + ], + [ + 11, + 34287 + ], + [ + 12, + 34287 + ], + [ + 13, + 34287 + ], + [ + 14, + 34287 + ] + ] + } + }, + { + "name": "per-track/fifo-no-timeout", + "stall_bound_us": 62480, + "max_stall_us": 8000, + "result": { + "objects": [ + { + "id": "g0-base", + "sent_first_us": 20000, + "arrived_us": 42000, + "state": "delivered" + }, + { + "id": "g0-l1-u0", + "sent_first_us": 22000, + "arrived_us": 42600, + "state": "delivered" + }, + { + "id": "g0-l1-u1", + "sent_first_us": 22600, + "arrived_us": 43215, + "state": "delivered" + }, + { + "id": "g0-l1-u2", + "sent_first_us": 23215, + "arrived_us": 43845, + "state": "delivered" + }, + { + "id": "g0-l1-u3", + "sent_first_us": 23845, + "arrived_us": 44490, + "state": "delivered" + }, + { + "id": "g0-l1-u4", + "sent_first_us": 24490, + "arrived_us": 45150, + "state": "delivered" + }, + { + "id": "g0-l2-u0", + "sent_first_us": 25150, + "arrived_us": 45510, + "state": "delivered" + }, + { + "id": "g0-l2-u1", + "sent_first_us": 25510, + "arrived_us": 45885, + "state": "delivered" + }, + { + "id": "g0-l2-u2", + "sent_first_us": 25885, + "arrived_us": 46275, + "state": "delivered" + }, + { + "id": "g0-l2-u3", + "sent_first_us": 26275, + "arrived_us": 46680, + "state": "delivered" + }, + { + "id": "g0-l2-u4", + "sent_first_us": 26680, + "arrived_us": 47100, + "state": "delivered" + }, + { + "id": "g1-base", + "sent_first_us": 27100, + "arrived_us": 49380, + "state": "delivered" + }, + { + "id": "g1-l1-u0", + "sent_first_us": 29380, + "arrived_us": 49985, + "state": "delivered" + }, + { + "id": "g1-l1-u1", + "sent_first_us": 29985, + "arrived_us": 50605, + "state": "delivered" + }, + { + "id": "g1-l1-u2", + "sent_first_us": 30605, + "arrived_us": 51239, + "state": "delivered" + }, + { + "id": "g1-l1-u3", + "sent_first_us": 31239, + "arrived_us": 51888, + "state": "delivered" + }, + { + "id": "g1-l1-u4", + "sent_first_us": 31888, + "arrived_us": 52552, + "state": "delivered" + }, + { + "id": "g1-l2-u0", + "sent_first_us": 32552, + "arrived_us": 52917, + "state": "delivered" + }, + { + "id": "g1-l2-u1", + "sent_first_us": 32917, + "arrived_us": 53297, + "state": "delivered" + }, + { + "id": "g1-l2-u2", + "sent_first_us": 33297, + "arrived_us": 53691, + "state": "delivered" + }, + { + "id": "g1-l2-u3", + "sent_first_us": 33691, + "arrived_us": 54100, + "state": "delivered" + }, + { + "id": "g1-l2-u4", + "sent_first_us": 34100, + "arrived_us": 54524, + "state": "delivered" + }, + { + "id": "g2-base", + "sent_first_us": 34524, + "arrived_us": 57084, + "state": "delivered" + }, + { + "id": "g2-l1-u0", + "sent_first_us": 37084, + "arrived_us": 57693, + "state": "delivered" + }, + { + "id": "g2-l1-u1", + "sent_first_us": 37693, + "arrived_us": 58317, + "state": "delivered" + }, + { + "id": "g2-l1-u2", + "sent_first_us": 38317, + "arrived_us": 58956, + "state": "delivered" + }, + { + "id": "g2-l1-u3", + "sent_first_us": 38956, + "arrived_us": 59610, + "state": "delivered" + }, + { + "id": "g2-l1-u4", + "sent_first_us": 39610, + "arrived_us": 60278, + "state": "delivered" + }, + { + "id": "g2-l2-u0", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u1", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u2", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u3", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + }, + { + "id": "g2-l2-u4", + "sent_first_us": null, + "arrived_us": null, + "state": "dropped" + } + ], + "presentations": [ + [ + 0, + 1, + 46000 + ], + [ + 0, + 2, + 48000 + ], + [ + 0, + 3, + 86000 + ], + [ + 1, + 1, + 46000 + ], + [ + 1, + 2, + 50000 + ], + [ + 1, + 3, + 88000 + ], + [ + 2, + 1, + 46000 + ], + [ + 2, + 2, + 56000 + ], + [ + 2, + 3, + 90000 + ], + [ + 3, + 1, + 46000 + ], + [ + 3, + 2, + 58000 + ], + [ + 3, + 3, + 92000 + ], + [ + 4, + 1, + 46000 + ], + [ + 4, + 2, + 64000 + ], + [ + 4, + 3, + 94000 + ], + [ + 5, + 1, + 54000 + ], + [ + 5, + 2, + 66000 + ], + [ + 5, + 3, + 96000 + ], + [ + 6, + 1, + 54000 + ], + [ + 6, + 2, + 68000 + ], + [ + 6, + 3, + 98000 + ], + [ + 7, + 1, + 54000 + ], + [ + 7, + 2, + 70000 + ], + [ + 7, + 3, + 100000 + ], + [ + 8, + 1, + 54000 + ], + [ + 8, + 2, + 72000 + ], + [ + 8, + 3, + 102000 + ], + [ + 9, + 1, + 54000 + ], + [ + 9, + 2, + 74000 + ], + [ + 9, + 3, + 104000 + ], + [ + 10, + 1, + 62000 + ], + [ + 10, + 2, + 76000 + ], + [ + 11, + 1, + 62000 + ], + [ + 11, + 2, + 78000 + ], + [ + 12, + 1, + 62000 + ], + [ + 12, + 2, + 80000 + ], + [ + 13, + 1, + 62000 + ], + [ + 13, + 2, + 82000 + ], + [ + 14, + 1, + 62000 + ], + [ + 14, + 2, + 84000 + ] + ], + "bfpf_sched": 14707, + "tfpf_us": 46000, + "tffs_us": [ + 46000, + 48000, + 86000 + ], + "offered": 55650, + "delivered": 50670, + "dropped": 4980, + "abandoned": 0, + "never_sent_capped": 0, + "missed": 0, + "stall_us": [ + [ + 0, + 0 + ], + [ + 1, + 0 + ], + [ + 2, + 0 + ], + [ + 3, + 0 + ], + [ + 4, + 0 + ], + [ + 5, + 4000 + ], + [ + 6, + 4000 + ], + [ + 7, + 4000 + ], + [ + 8, + 4000 + ], + [ + 9, + 4000 + ], + [ + 10, + 8000 + ], + [ + 11, + 8000 + ], + [ + 12, + 8000 + ], + [ + 13, + 8000 + ], + [ + 14, + 8000 + ] + ] + } + }, { "name": "audio/strict-subgroup", "stall_bound_us": 2480, diff --git a/wai/staged-measure-conformance/sim_diff.py b/wai/staged-measure-conformance/sim_diff.py new file mode 100644 index 000000000..70cb51d35 --- /dev/null +++ b/wai/staged-measure-conformance/sim_diff.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""The delivery simulator, differentially: `wai_stage_measure sim` (wai-rs's +`stage_measure::sim`) against sim_verify.py, which shares no code with it, on seeded +random models and plans (extensions/staged-measurement.md §10, Appendix B). + +A model is one to five groups, each a base over one to four units and up to three +refinement layers, each layer one object over the group or one per unit. A refinement +object may carry its own delivery timeout (`timeout_us`: zero, small or large, or null) +or take the plan's. A plan is live or on demand, fifo or strict priority, by subgroup or +datagram, with or without a base and a refinement timeout, a layer cap, one or two +decode workers, base-first or fifo decoding, and late refinements discarded or +presented. For each case the two must agree on the refusal, or on the whole result, +the stall bound (§10.4) and the longest stall. Where the bound applies, every stall +must be within it. + + python3 sim_diff.py [seed] [count] + +Exit status 0 only if every case agrees and no stall exceeds its bound. +""" +import json +import os +import random +import subprocess +import sys +import tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import measure_verify as MV # noqa: E402 +import sim_verify as SV # noqa: E402 + + +def model(rng): + unit = rng.choice([1000, 20000, 40000]) + objs, u = [], 0 + for g in range(rng.randint(1, 5)): + n = rng.randint(1, 4) + cap = (u + n) * unit + rng.choice([0, 0, 5000]) + objs.append({"id": f"g{g}b", "base": g, "layer": 0, "units": [u, n], "bytes": rng.randint(1, 20000), "capture_us": cap}) + for k in range(1, rng.randint(1, 4)): + spans = [(u + i, 1) for i in range(n)] if rng.random() < 0.5 else [(u, n)] + for (f, c) in spans: + o = {"id": f"g{g}l{k}u{f}", "base": g, "layer": k, "units": [f, c], "bytes": rng.randint(1, 9000), "capture_us": cap} + r = rng.random() + if r < 0.6: + o["timeout_us"] = rng.choice([0, 1, 1000, 20000, 60000, 300000]) + elif r < 0.7: + o["timeout_us"] = None + objs.append(o) + u += n + return {"format": "wai-sim-model/1", "unit_us": unit, "objects": objs} + + +def plan(rng): + live = rng.random() < 0.5 + return {"plan": "wai-sim/1", "mode": "live" if live else "vod", + "link": {"rtt_us": rng.choice([0, 1000, 40000]), "bytes_per_ms": rng.choice([10, 150, 2500, 20000]), + "chunk_bytes": rng.choice([1200, 1500, 64000])}, + "sender": {"schedule": rng.choice(["fifo", "strict-priority"]), "carriage": rng.choice(["subgroup", "subgroup", "datagram"]), + "layer_cap": rng.choice([None, None, 1, 2]), + "delivery_timeout_us": {"base": rng.choice([None, None, None, 50000]), + "refinement": rng.choice([None, None, 30000, 100000])}, + "vod_window_us": None if live else rng.choice([None, 100000])}, + "sink": {"workers": rng.choice([1, 2]), "decode_order": rng.choice(["base-first", "base-first", "fifo"]), + "decode_us": {"base": rng.choice([1, 4000]), "refinement": rng.choice([1, 2000])}, + "playout_start_us": rng.choice([None, 215000]) if live else None, + "late_refinement": rng.choice(["discard", "present"])}} + + +def main(argv): + if len(argv) < 2: + print(__doc__) + return 2 + binary = argv[1] + seed = int(argv[2]) if len(argv) > 2 else 1 + count = int(argv[3]) if len(argv) > 3 else 1000 + rng = random.Random(seed) + agree = refused = bounded = 0 + fails = [] + with tempfile.TemporaryDirectory() as d: + mp, pp = os.path.join(d, "model.json"), os.path.join(d, "plan.json") + for i in range(count): + m, p = model(rng), plan(rng) + with open(mp, "w") as f: + json.dump(m, f) + with open(pp, "w") as f: + json.dump(p, f) + r = subprocess.run([binary, "sim", mp, pp], capture_output=True) + try: + res = SV.simulate(json.loads(json.dumps(m)), p) + py = {"stall_bound_us": SV.bound(m, p), "max_stall_us": SV.max_stall(res), "result": res} + except ValueError as e: + py = None + why = str(e) + if r.returncode != 0 or py is None: + if r.returncode != 0 and py is None: + refused += 1 + else: + fails.append(f"case {i}: wai_stage_measure {'refused' if r.returncode else 'simulated'}, " + f"sim_verify.py {'refused (' + why + ')' if py is None else 'simulated'}: " + f"{r.stderr.decode()[-200:]}") + continue + g = json.loads(r.stdout) + out = [] + MV.diff(py, {"stall_bound_us": g["stall_bound_us"], "max_stall_us": g["max_stall_us"], "result": g["result"]}, f"case {i}", out) + if out: + fails.append("; ".join(out[:3])) + continue + agree += 1 + if py["stall_bound_us"] is not None: + bounded += 1 + if py["max_stall_us"] is None or py["max_stall_us"] > py["stall_bound_us"]: + fails.append(f"case {i}: a stall of {py['max_stall_us']} past the bound {py['stall_bound_us']}") + for f in fails[:5]: + print(f"FAIL {f}") + print(f"sim_diff seed {seed}: {agree} agree, {refused} refused by both, {len(fails)} fail; " + f"the stall bound applied to {bounded}") + return 0 if not fails else 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/wai/staged-measure-conformance/sim_verify.py b/wai/staged-measure-conformance/sim_verify.py index 8c72c11bf..0f94038c7 100755 --- a/wai/staged-measure-conformance/sim_verify.py +++ b/wai/staged-measure-conformance/sim_verify.py @@ -41,6 +41,8 @@ def check_model(m): frm, cnt = o["units"] if o["bytes"] < 1 or cnt < 1: raise ValueError(f"{o['id']}: no bytes or no units") + if o["layer"] == 0 and o.get("timeout_us") is not None: + raise ValueError(f"{o['id']}: a base takes the plan's timeout") for u in range(frm, frm + cnt): if (u, o["layer"]) in covered: raise ValueError(f"{o['id']}: unit {u} covered twice") @@ -71,8 +73,13 @@ def run_link(m, p): raise ValueError(f"{o['id']}: no capture time") avail.append(max(h, o["capture_us"])) to = snd["delivery_timeout_us"] - deadline = [None if (to["base"] if o["layer"] == 0 else to["refinement"]) is None - else avail[i] + (to["base"] if o["layer"] == 0 else to["refinement"]) for i, o in enumerate(objs)] + + def timeout(o): + """A base's is the plan's; a refinement's its own where the model gives one.""" + if o["layer"] == 0: + return to["base"] + return o["timeout_us"] if o.get("timeout_us") is not None else to["refinement"] + deadline = [None if timeout(o) is None else avail[i] + timeout(o) for i, o in enumerate(objs)] datagram = snd["carriage"] == "datagram" if datagram and any(not capped[i] and o["bytes"] > lnk["chunk_bytes"] for i, o in enumerate(objs)): raise ValueError("datagram-too-large") @@ -232,15 +239,20 @@ def simulate(m, p): return full -def bound(p): +def bound(m, p): + """§10.4: under fifo, T is the longest timeout a refinement of the model takes, its + own or the plan's; none where one takes none.""" snd, sink = p["sender"], p["sink"] if snd["delivery_timeout_us"]["base"] is not None: return None chunk = ceil_div(p["link"]["chunk_bytes"] * 1000, p["link"]["bytes_per_ms"]) + plan_t = snd["delivery_timeout_us"]["refinement"] + ts = [o["timeout_us"] if o.get("timeout_us") is not None else plan_t for o in m["objects"] if o["layer"] >= 1] + t = None if None in ts else max(ts, default=plan_t if plan_t is not None else 0) if snd["schedule"] == "strict-priority": link = chunk - elif snd["delivery_timeout_us"]["refinement"] is not None: - link = snd["delivery_timeout_us"]["refinement"] + chunk + elif t is not None: + link = t + chunk else: return None if sink["decode_order"] != "base-first": @@ -288,7 +300,7 @@ def main(d): continue if r["offered"] != r["delivered"] + r["dropped"] + r["abandoned"]: fails.append(f"{name}: bytes are not conserved") - b, w = bound(p), max_stall(r) + b, w = bound(m, p), max_stall(r) claim = c["claim"] if claim == "within-bound": if b is None or w is None or w > b: @@ -298,7 +310,7 @@ def main(d): fails.append(f"{name}: the plan has a bound") elif claim.startswith("exceeds:"): other = json.load(open(os.path.join(d, "sim", "plans", claim.split(":", 1)[1] + ".json"))) - ob = bound(other) + ob = bound(m, other) if b is not None or ob is None or (w is not None and w <= ob): fails.append(f"{name}: stall {w} does not pass {claim}'s bound {ob}") else: diff --git a/wai/wai-pack/Cargo.lock b/wai/wai-pack/Cargo.lock index 11894f19c..6795569f9 100644 --- a/wai/wai-pack/Cargo.lock +++ b/wai/wai-pack/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "arrayvec" version = "0.7.8" @@ -311,6 +317,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "num-bigint" version = "0.4.8" @@ -674,6 +689,7 @@ version = "0.1.0" dependencies = [ "blake3", "ed25519-dalek", + "miniz_oxide", "serde_json", "sha2", "toml 0.8.23", diff --git a/wai/wai-pack/Cargo.toml b/wai/wai-pack/Cargo.toml index e9d59370f..cbe95c5fe 100644 --- a/wai/wai-pack/Cargo.toml +++ b/wai/wai-pack/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" rust-version = "1.97" license = "Apache-2.0" -description = "The reference packager of WAI's packaging extension: a declared policy and its digest, scene-cut segmentation with a bounded lookahead, piece classification, and the conformance corpus that pins them." +description = "The reference packager of WAI's packaging extension: a declared policy and its digest, scene-cut segmentation with a bounded lookahead, staged packages of refinement layers and stills, live windows, simulated delivery, and the conformance corpus that pins them." repository = "https://git.transaction.science/Transaction-Science/open-standards" publish = false @@ -31,9 +31,15 @@ toml = { version = "0.8", default-features = false, features = ["parse"] } serde_json = "1" blake3 = "1" sha2 = "0.10" +# Inflate, for the verifier's reader of a still (src/png.rs): an implementation apart from +# the packer's own deflate, so a still is decoded by code that did not write it. # Release builds check integer overflow as debug builds do, so the release tests in CI # catch a wrapped product (a field read as u16 and doubled) rather than run past it. +[dev-dependencies] +# An inflater independent of the reader's own, to read the writer's stills back in a test. +miniz_oxide = "0.8" + [profile.release] overflow-checks = true diff --git a/wai/wai-pack/src/bin/wai_pack.rs b/wai/wai-pack/src/bin/wai_pack.rs index 6748a5b7c..a0db30b7e 100644 --- a/wai/wai-pack/src/bin/wai_pack.rs +++ b/wai/wai-pack/src/bin/wai_pack.rs @@ -19,10 +19,42 @@ //! 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 +//! 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/ +//! then; each window of pieces written when it is sealed, as +//! windows//index-.wsix and then windows//package.receipt.json, chained to +//! the one before; at the end of the stream the last window, plan-.txt and +//! LISTING. Every file is written whole (a temporary file renamed into place), and its +//! listing line to a hidden file beside the package, sorted into LISTING at the end. +//! An object's bytes are not held once written; what is held is what no window has +//! sealed yet (those pieces' entry fields and their objects' content hashes), so with +//! --window 0 (the default), one window sealed at the end, every piece's until then, +//! and each piece's first frame for the plan (packaging §9.2). A source that fails +//! mid-stream (a short frame, a frame of another size) keeps every sealed window and +//! 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 listing -//! the listing (SPEC §3.1) of every file of but LISTING +//! 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 +//! windows/*/index-*.wsix) list as bases and refinements: what a live and an +//! on-demand pack of one source share (test 12) +//! wai-pack sim --policy --class --rate --startup +//! [--source ] [--rtt ] [--chunk ] [--enc ] [--cap ] [--fifo] +//! pack the source live and simulate its delivery to a subscriber of the class +//! (packaging §8.9, staged-measurement §10), printing the outcome per piece as JSON, +//! labelled simulated; --fifo sends without §8.5's priorities, to show what they +//! prevent +//! wai-pack stage-json +//! a conformance aid: reads JSON lines, each {"op": ...}, and writes one result per +//! line: "refine" {h,w,q,below,src} gives the WIR1 layer's hex and each reconstructed +//! frame's SHA-256 (§5.4); "still" {w,h,rgb} gives the still's hex (§5.7); "plan" +//! {policy, pieces:[{start,duration,kind,layer_bytes}]} gives each piece's rules, +//! moments, ladder rung ids, deadline and class plan (§5.7, §8.1); "deliver" {policy, +//! class_plan, published, plan} gives the simulated outcome (§8.9) //! wai-pack policy-verdict ... //! each policy's verdict (`ok`, or the reason a packager refuses it) and, when the //! policy itself parses, its digest (else `-`): `\t\t` @@ -55,8 +87,11 @@ fn usage() -> ExitCode { "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 live --policy --out --key [--signer-id ] [--window ]\n \ wai-pack verify \n \ - wai-pack listing \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 \ wai-pack policy-verdict ...\n \ wai-pack source-verdict ...\n \ wai-pack fixture gen [-o ] | fixture list\n \ @@ -189,15 +224,204 @@ fn cmd_verify(args: &[String]) -> Result<(), String> { } fn cmd_listing(args: &[String]) -> Result<(), String> { - let dir = args.first().ok_or("listing: a directory")?; + let dir = args.iter().find(|a| !a.starts_with("--")).ok_or("listing: a directory")?; let mut files = wai::pin::read_dir_set(Path::new(dir)).map_err(|e| e.to_string())?; files.remove("LISTING"); + if args.iter().any(|a| a == "--primary") { + files = wai_pack::live::primary_files(&files)?; + } let view: std::collections::BTreeMap = files.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); let l = wai::pin::listing_from(&view).map_err(|e| e.to_string())?; use std::io::Write as _; std::io::stdout().write_all(&l).map_err(|e| e.to_string()) } +/// What `wai-pack live` has written. It holds no record per file of the session: each +/// file's listing line goes to a hidden file beside the package as the file is written +/// (sorted into `LISTING` at the end), an object already written is known by its file, +/// and only the objects the pieces no window has sealed yet published are held, by +/// content hash and piece, for a session that ends in an error to remove. +struct LiveOut<'a> { + dir: &'a Path, + comp: String, + lines: std::fs::File, + open: Vec<(u64, [u8; 32])>, + objects: u64, + windows: u64, + sealed_pieces: u64, +} + +/// The hidden file the listing's lines are written to as the files are. +const LIVE_LINES: &str = ".LISTING.lines"; + +impl LiveOut<'_> { + fn new<'a>(dir: &'a Path, comp: String) -> Result, String> { + std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?; + let at = dir.join(LIVE_LINES); + let lines = std::fs::File::create(&at).map_err(|e| format!("{}: {e}", at.display()))?; + Ok(LiveOut { dir, comp, lines, open: Vec::new(), objects: 0, windows: 0, sealed_pieces: 0 }) + } + + /// Write a file of the package whole or not at all: its bytes to a hidden temporary + /// file beside it, then renamed into place; then its listing line. + fn write(&mut self, path: &str, bytes: &[u8]) -> Result<(), String> { + use std::io::Write as _; + let at = self.dir.join(path); + let parent = at.parent().ok_or_else(|| format!("{path}: no directory"))?; + std::fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?; + let name = at.file_name().and_then(|n| n.to_str()).ok_or_else(|| format!("{path}: no file name"))?; + let tmp = parent.join(format!(".{name}.tmp")); + std::fs::write(&tmp, bytes).map_err(|e| format!("{}: {e}", tmp.display()))?; + std::fs::rename(&tmp, &at).map_err(|e| format!("{}: {e}", at.display()))?; + writeln!(self.lines, "{} {path}", wai::pin::sha256_hex(bytes)).map_err(|e| format!("{LIVE_LINES}: {e}")) + } + + /// Each object as it is published; one already written (a repeated piece's) once. + fn publish(&mut self, published: Vec) -> Result<(), String> { + for x in published { + println!("piece {} layer {} at frame {}: {}", x.piece, x.layer, x.at_frame, hex(&x.hash)); + let path = wai_pack::objects::object_path(&x.hash); + if !self.dir.join(&path).exists() { + self.write(&path, &x.envelope)?; + self.open.push((x.piece, x.hash)); + self.objects += 1; + } + } + Ok(()) + } + + /// Each window as it is sealed: its index, then its receipt. The objects of the + /// pieces it holds are no longer open. + fn seal(&mut self, windows: Vec) -> Result<(), String> { + for w in windows { + let n = w.number; + self.write(&format!("windows/{n}/{}", wai::package::index_file(&self.comp)), &w.index)?; + self.write(&format!("windows/{n}/package.receipt.json"), w.receipt.to_json().as_bytes())?; + println!("window {n}: pieces {} to {}, receipt {}", w.first_piece, w.first_piece + w.pieces - 1, hex(&w.receipt.receipt_hash())); + self.windows = n + 1; + self.sealed_pieces = w.first_piece + w.pieces; + let through = self.sealed_pieces; + self.open.retain(|(p, _)| *p >= through); + } + Ok(()) + } + + /// The end of the source: the plan file, then `LISTING` from the lines written, sorted + /// by path (the one moment a line per file is held), and the lines' file removed. + fn finish(&mut self, starts: &[u64]) -> Result<(), String> { + use std::io::Write as _; + self.write(&format!("plan-{}.txt", self.comp), wai_pack::pack::plan_file(starts).as_bytes())?; + self.lines.flush().map_err(|e| format!("{LIVE_LINES}: {e}"))?; + let at = self.dir.join(LIVE_LINES); + let text = std::fs::read_to_string(&at).map_err(|e| format!("{}: {e}", at.display()))?; + let mut digests = std::collections::BTreeMap::new(); + for l in text.lines() { + let (d, p) = l.split_once(" ").ok_or_else(|| format!("{LIVE_LINES}: a line that is not a listing line"))?; + digests.insert(p.to_string(), d.to_string()); + } + let listing = wai::pin::listing_from_sha256(&digests).map_err(|e| e.to_string())?; + self.write("LISTING", &listing)?; + std::fs::remove_file(&at).map_err(|e| format!("{}: {e}", at.display())) + } + + /// The session ended in `why`: every sealed window and the objects they list are + /// kept; the objects only unsealed pieces published are removed, and no plan file or + /// LISTING is written. The error says what was kept. + fn abandon(&mut self, why: String) -> String { + let mut removed = 0; + for (_, h) in &self.open { + if std::fs::remove_file(self.dir.join(wai_pack::objects::object_path(h))).is_ok() { + removed += 1; + } + } + let _ = std::fs::remove_file(self.dir.join(LIVE_LINES)); + let kept = self.objects - removed; + let windows = match self.windows { + 0 => "no window".to_string(), + n => format!("windows 0 to {} (pieces 0 to {}) with the {kept} objects they list", n - 1, self.sealed_pieces - 1), + }; + format!("{why}; kept {windows} in {}; removed {removed} objects of pieces no window sealed; wrote no plan file and no LISTING", self.dir.display()) + } +} + +fn cmd_live(args: &[String]) -> Result<(), String> { + let path = opt(args, "--policy").ok_or("live: --policy")?; + let out = opt(args, "--out").ok_or("live: --out")?; + wai_pack::pack::out_dir_ready(Path::new(out)).map_err(|e| e.to_string())?; + let key = read_key(opt(args, "--key").ok_or("live: --key")?)?; + let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?; + 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::live::LiveOpts::new(key, signer_id); + o.window_pieces = opt(args, "--window").map_or(Ok(0), |w| w.parse::().map_err(|e| format!("live: --window: {e}")))?; + let mut r = source::y4m::Y4mReader::new(std::io::BufReader::new(std::io::stdin().lock())).map_err(|e| e.to_string())?; + let mut lp = wai_pack::live::LivePacker::new(&bytes, r.header.w, r.header.h, o).map_err(|e| e.to_string())?; + let tb = lp.policy().components[0].timebase; + if let Some((n, d)) = r.header.rate + && (n, d) != (tb.num, tb.den) + { + return Err("live: pack-source-rate: the source's frame rate is not the component's timebase".into()); + } + let mut w = LiveOut::new(Path::new(out), lp.policy().components[0].id.clone())?; + let mut frames = || -> Result<(), String> { + while let Some(f) = r.next_frame() { + let f = f.map_err(|e| e.to_string())?; + let p = lp.push(&f.rgb).map_err(|e| e.to_string())?; + w.publish(p)?; + w.seal(lp.take_sealed())?; + } + Ok(()) + }; + if let Err(e) = frames() { + return Err(w.abandon(format!("live: {e}"))); + } + let (p, windows, starts) = lp.finish().map_err(|e| w.abandon(format!("live: {e}")))?; + w.publish(p).map_err(|e| w.abandon(format!("live: {e}")))?; + w.seal(windows).map_err(|e| w.abandon(format!("live: {e}")))?; + w.finish(&starts) +} + +fn cmd_sim(args: &[String]) -> Result<(), String> { + let path = opt(args, "--policy").ok_or("sim: --policy")?; + let class = opt(args, "--class").ok_or("sim: --class")?; + let num = |name: &str, default: Option| -> Result { + match opt(args, name) { + Some(v) => v.parse::().map_err(|e| format!("sim: {name}: {e}")), + None => default.ok_or_else(|| format!("sim: {name}")), + } + }; + let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?; + let p = Policy::parse_bytes(&bytes).map_err(|e| e.to_string())?; + let c = p.components.first().ok_or("sim: no component")?; + let src = opt(args, "--source") + .map(str::to_string) + .or_else(|| c.source.as_ref().map(|s| Path::new(path).parent().unwrap_or(Path::new(".")).join(s).to_string_lossy().into_owned())) + .ok_or("sim: the component names no source; pass --source")?; + let y4m = std::fs::read(&src).map_err(|e| format!("{src}: {e}"))?; + let plan = wai_pack::delivery::DeliveryPlan { + rtt_us: num("--rtt", Some(0))?, + bytes_per_ms: num("--rate", None)?, + chunk_bytes: num("--chunk", Some(1200))?, + enc_us: num("--enc", Some(0))? * 1000, + startup_us: num("--startup", None)? * 1000, + layer_cap: opt(args, "--cap").map(|x| x.parse::().map_err(|e| format!("sim: --cap: {e}"))).transpose()?, + schedule: if args.iter().any(|a| a == "--fifo") { wai::stage_measure::sim::Schedule::Fifo } else { wai::stage_measure::sim::Schedule::StrictPriority }, + }; + let key = ed25519_dalek::SigningKey::from_bytes(&[0x5a; 32]); + let d = wai_pack::delivery::simulate_source(&bytes, &y4m, class, &plan, &key).map_err(|e| e.to_string())?; + println!("{}", serde_json::to_string_pretty(&d.to_json()).map_err(|e| e.to_string())?); + Ok(()) +} + +fn cmd_stage_json() -> Result<(), String> { + use std::io::BufRead as _; + for line in std::io::stdin().lock().lines() { + let line = line.map_err(|e| e.to_string())?; + let v: serde_json::Value = serde_json::from_str(&line).map_err(|e| e.to_string())?; + println!("{}", wai_pack::conformance::stage_json(&v)); + } + Ok(()) +} + fn cmd_policy_verdict(args: &[String]) -> Result<(), String> { for p in args { let b = std::fs::read(p).map_err(|e| format!("{p}: {e}"))?; @@ -312,6 +536,9 @@ fn main() -> ExitCode { "policy" => cmd_policy(rest), "plan" => cmd_plan(rest), "pack" => cmd_pack(rest), + "live" => cmd_live(rest), + "sim" => cmd_sim(rest), + "stage-json" => cmd_stage_json(), "verify" => cmd_verify(rest), "listing" => cmd_listing(rest), "policy-verdict" => cmd_policy_verdict(rest), diff --git a/wai/wai-pack/src/catalog.rs b/wai/wai-pack/src/catalog.rs index 3fdc025c3..47f3daa8c 100644 --- a/wai/wai-pack/src/catalog.rs +++ b/wai/wai-pack/src/catalog.rs @@ -89,6 +89,9 @@ pub struct CompView<'a> { /// Every object of the index by its content hash. pub objects: &'a BTreeMap<[u8; 32], Vec>, pub floor: Option, + /// The capability of the component's default base rung (§5.7): the base an HTTP view + /// carries (§8.3). + pub base_cap: String, } /// Why the catalogs cannot be written. @@ -156,11 +159,26 @@ impl CompView<'_> { self.index.piece_entries(p).iter().filter(|e| matches!(e.role, Role::Base | Role::Refinement) && e.layer <= v).collect() } + /// What an HTTP view of class `c` lists for piece `p` (§8.3): what the class lists, + /// where the piece's base is of the component's default base capability; nothing + /// otherwise, so the floor plays there. http-delivery §4.2 lets no variant list bases + /// with no capability in common, so a piece coded by a rule's ladder in another base + /// capability (a still, §5.7) is carried over HTTP by the floor alone. + pub fn http_listed(&self, c: usize, p: usize) -> Vec<&EntryRec> { + let base_ok = self.index.piece_entries(p).iter().find(|e| e.role == Role::Base).is_some_and(|b| self.index.capability(b) == self.base_cap); + if base_ok { self.listed(c, p) } else { Vec::new() } + } + + /// Whether an HTTP view of class `c` lists any WAI object. + pub fn http_lists(&self, c: usize) -> bool { + (0..self.index.pieces.len()).any(|p| !self.http_listed(c, p).is_empty()) + } + fn seq_objects(&self, c: usize) -> Result, CatalogError> { let mut out = Vec::new(); let mut seen = std::collections::BTreeSet::new(); for p in 0..self.index.pieces.len() { - for e in self.listed(c, p) { + 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()))?); @@ -182,8 +200,9 @@ impl CompView<'_> { fn wai_rates(&self, c: usize) -> (u64, u64) { let items: Vec<(u64, u64)> = (0..self.index.pieces.len()) - .filter(|&p| self.index.plan[c][p] != FLOOR_ONLY) - .map(|p| (self.listed(c, p).iter().map(|e| e.byte_len).sum(), self.index.pieces[p].duration as u64)) + .map(|p| (self.http_listed(c, p), p)) + .filter(|(l, _)| !l.is_empty()) + .map(|(l, p)| (l.iter().map(|e| e.byte_len).sum(), self.index.pieces[p].duration as u64)) .collect(); rates(&items, self.tb) } @@ -298,8 +317,9 @@ pub fn render_wai_media(v: &CompView, c: usize) -> Result if seg.n != base.byte_len { return Err(CatalogError(format!("object {} is not {} bytes", crate::hex(&base.content_hash), base.byte_len))); } - seg.gap = v.index.plan[c][p] == FLOOR_ONLY; - for e in v.listed(c, p).into_iter().filter(|e| e.role == Role::Refinement) { + let listed = v.http_listed(c, p); + seg.gap = listed.is_empty(); + for e in listed.into_iter().filter(|e| e.role == Role::Refinement) { seg.layers.push(WaiLayer::of(&object(v, e)?).map_err(|r| CatalogError(r.to_string()))?); } segs.push(seg); @@ -355,11 +375,11 @@ pub fn render_dash_classic(v: &CompView) -> Result { pub fn render_dash_class(v: &CompView, c: usize) -> Result, CatalogError> { let Some((union, floor)) = v.wai_codecs(c)? else { return Ok(None) }; let class = v.class_id(c); - let top = (0..v.index.pieces.len()).filter(|&p| v.index.plan[c][p] != FLOOR_ONLY).map(|p| v.index.plan[c][p]).max().unwrap_or(0); + let top = (0..v.index.pieces.len()).filter(|&p| !v.http_listed(c, p).is_empty()).map(|p| v.index.plan[c][p]).max().unwrap_or(0); let mut reps = Vec::new(); for k in 0..=top { let ps: Vec<(usize, &EntryRec)> = (0..v.index.pieces.len()) - .filter_map(|p| v.listed(c, p).into_iter().find(|e| e.layer == k && (k > 0 || e.role == Role::Base)).map(|e| (p, e))) + .filter_map(|p| v.http_listed(c, p).into_iter().find(|e| e.layer == k && (k > 0 || e.role == Role::Base)).map(|e| (p, e))) .collect(); let (peak, _) = rates(&ps.iter().map(|(p, e)| (e.byte_len, v.index.pieces[*p].duration as u64)).collect::>(), v.tb); reps.push(Representation { @@ -394,27 +414,78 @@ fn tier_label(t: u8) -> Option<&'static str> { } } -/// `moq/catalog.json`: RFC 8785 canonical. `layer_lag_ms[k]` is the policy's lag of layer -/// `k`, and `default_deadline` its `defaults.deadline_ms`. -/// -/// One track per (layer, deadline), by layer, then by deadline ascending; a refinement -/// track `depends` on every track that carries an entry its entries refine, in track -/// order: the layer below at the same deadline in a package whose layers share their -/// piece's deadline, and never a track the catalog does not hold. A subscriber receives -/// every track a track depends on (§8.4), so a dependency is all of them. -pub fn render_moq_catalog(v: &CompView, layer_lag_ms: &[u64], default_deadline: u64) -> Result { +/// The domain of a track's receipt alias (§8.4). +pub const DOMAIN_RECEIPT_ALIAS: &[u8] = b"wai:pack-receipt-alias\x01"; + +/// A track's `wai_receipt_alias` (§8.4): the first eight bytes of `BLAKE3("wai:pack- +/// receipt-alias\x01" ‖ namespace ‖ 0x00 ‖ name)` as a little-endian integer, less than +/// `2^53`. Distinct for distinct tracks with overwhelming probability, so two packages +/// published under one key keep apart receipt chains. +pub fn receipt_alias(namespace: &str, name: &str) -> u64 { + let mut h = blake3::Hasher::new(); + h.update(DOMAIN_RECEIPT_ALIAS); + h.update(namespace.as_bytes()); + h.update(&[0]); + h.update(name.as_bytes()); + u64::from_le_bytes(h.finalize().as_bytes()[..8].try_into().expect("8 bytes")) & ((1 << 53) - 1) +} + +/// One MoQ track of a component's stage set (§8.4): its layer and deadline, its name, and +/// the entries it carries. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MoqTrack { + pub layer: u8, + pub deadline_ms: u32, + pub name: String, + pub alias: u64, + /// `deadline − layer_lag_ms[layer]` for a refinement track (§8.6); none for the base. + pub timeout_ms: Option, +} + +/// The media tracks of a component (§8.4): one per (layer, deadline) of its bases and +/// refinements, by layer, then deadline ascending; named `/l`, or `/l@` for +/// a refinement whose deadline is not `default_deadline`. +pub fn moq_tracks(v: &CompView, namespace: &str, layer_lag_ms: &[u64], default_deadline: u64) -> Vec { + let id = v.id(); + let mut keys: Vec<(u8, u32)> = v.index.entries.iter().filter(|e| matches!(e.role, Role::Base | Role::Refinement)).map(|e| (e.layer, e.deadline_ms)).collect(); + keys.sort_unstable(); + keys.dedup(); + keys.into_iter() + .map(|(k, d)| { + let name = if k == 0 || d as u64 == default_deadline { format!("{id}/l{k}") } else { format!("{id}/l{k}@{d}") }; + MoqTrack { + layer: k, + deadline_ms: d, + alias: receipt_alias(namespace, &name), + timeout_ms: (k > 0).then(|| (d as u64).saturating_sub(layer_lag_ms.get(k as usize).copied().unwrap_or(0))), + name, + } + }) + .collect() +} + +/// `moq/catalog.json` (§8.4): RFC 8785 canonical, `{"tracks": [...], "version": 1}`. The +/// component's bases and refinements are a stage set of moq-streaming-format §4.1, written +/// by `wai::staged::moq::SetTrack`: one track per (layer, deadline) of namespace +/// `namespace` (the asset id), each with `wai_staged` (set the component's id, its layer, +/// `lead_ms` null: the package is on demand), `isLive` false, `role` video, `renderGroup` +/// 1, `depends` every track carrying an entry below one of its own along its chain to the +/// base (so a track of each layer below, none at or above), `wai_capabilities`, +/// `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. +/// 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}; let id = v.id(); let ix = v.index; let staged = |e: &EntryRec| matches!(e.role, Role::Base | Role::Refinement); - // The tracks, in order, and each staged entry's track. - let mut keys: Vec<(u8, u32)> = ix.entries.iter().filter(|e| staged(e)).map(|e| (e.layer, e.deadline_ms)).collect(); - keys.sort_unstable(); - keys.dedup(); - let name_of = |(k, d): (u8, u32)| if k == 0 || d as u64 == default_deadline { format!("{id}/l{k}") } else { format!("{id}/l{k}@{d}") }; - let track_of = |e: &EntryRec| keys.iter().position(|&x| x == (e.layer, e.deadline_ms)); - let mut tracks: Vec = Vec::new(); - for (ti, &(k, d)) in keys.iter().enumerate() { - let on: Vec<&EntryRec> = ix.entries.iter().filter(|e| staged(e) && (e.layer, e.deadline_ms) == (k, d)).collect(); + let tracks = moq_tracks(v, namespace, layer_lag_ms, default_deadline); + let track_of = |e: &EntryRec| tracks.iter().position(|t| (t.layer, t.deadline_ms) == (e.layer, e.deadline_ms)); + let mut out: Vec = Vec::new(); + for (ti, t) in tracks.iter().enumerate() { + let on: Vec<&EntryRec> = ix.entries.iter().filter(|e| staged(e) && (e.layer, e.deadline_ms) == (t.layer, t.deadline_ms)).collect(); let mut caps: Vec = Vec::new(); for e in &on { let c = ix.capability(e).to_string(); @@ -422,43 +493,61 @@ pub fn render_moq_catalog(v: &CompView, layer_lag_ms: &[u64], default_deadline: caps.push(c); } } - let weakest = on.iter().map(|e| e.prefix_tier).min().unwrap_or(0); - let mut t = json!({ - "name": name_of((k, d)), - "packaging": "wai", - "priority": PRIORITY_BASE + PRIORITY_STEP * k as u64, - "renderGroup": 1, - "wai_capabilities": caps, - "wai_layer": k, - "wai_receipts": false, - }); - if let Some(l) = tier_label(weakest) { - t["wai_determinism"] = json!(l); - } - if k == 0 { - if v.floor.is_some() { - t["altGroup"] = json!(1); - } - } else { - let mut deps: Vec = Vec::new(); - for e in &on { - let below = ix.entries.get(e.refines as usize).ok_or_else(|| CatalogError("a refinement refines no entry".into()))?; - let dt = track_of(below).filter(|&x| x < ti).ok_or_else(|| CatalogError("a refinement refines an entry on no earlier track".into()))?; + // Every track carrying an entry below one of this track's, down its chain. + let mut deps: Vec = Vec::new(); + for e in &on { + let mut cur = *e; + while cur.role == Role::Refinement { + cur = ix.entries.get(cur.refines as usize).ok_or_else(|| CatalogError("a refinement refines no entry".into()))?; + let dt = track_of(cur).filter(|&x| x < ti).ok_or_else(|| CatalogError("a refinement refines an entry on no earlier track".into()))?; if !deps.contains(&dt) { deps.push(dt); } } - deps.sort_unstable(); - t["depends"] = json!(deps.iter().map(|&x| name_of(keys[x])).collect::>()); - let lag = layer_lag_ms.get(k as usize).copied().unwrap_or(0); - t["wai_delivery_timeout_ms"] = json!((d as u64).saturating_sub(lag)); } - tracks.push(t); + deps.sort_unstable(); + let weakest = on.iter().map(|e| e.prefix_tier).min().unwrap_or(0); + let st = SetTrack { + namespace: namespace.to_string(), + name: t.name.clone(), + staged: StagedTrack { set: id.to_string(), layer: t.layer, lead_ms: None }, + is_live: false, + target_latency_ms: None, + role: Some("video".into()), + render_group: Some(1), + depends: deps.iter().map(|&x| tracks[x].name.clone()).collect(), + capabilities: caps, + determinism: tier_label(weakest).map(str::to_string), + receipts: true, + receipt_alias: Some(t.alias), + receipts_track: Some(format!("{}/receipts", t.name)), + }; + let mut j = st.to_catalog_json(); + if let Some(to) = t.timeout_ms { + j["wai_delivery_timeout_ms"] = json!(to); + } + if t.layer == 0 && v.floor.is_some() { + j["altGroup"] = json!(1); + } + out.push(j); } if let Some(f) = &v.floor { - tracks.push(json!({"altGroup": 1, "codec": f.codec, "height": f.h, "name": format!("{id}/floor"), "packaging": "cmaf", "renderGroup": 1, "width": f.w})); + out.push(json!({"altGroup": 1, "codec": f.codec, "height": f.h, "name": format!("{id}/floor"), "namespace": namespace, "packaging": "cmaf", "renderGroup": 1, "width": f.w})); } - wai::jcs::jcs(&json!({"tracks": tracks, "version": 1})).map(|s| s + "\n").map_err(|e| CatalogError(format!("moq/catalog.json: {e}"))) + for t in &tracks { + out.push(receipts_track_json(namespace, &format!("{}/receipts", t.name), false)); + } + let catalog = json!({"tracks": out, "version": 1}); + let aliases: std::collections::BTreeSet = tracks.iter().map(|t| t.alias).collect(); + if aliases.len() != tracks.len() { + return Err(CatalogError("two tracks' receipt aliases are equal".into())); + } + for (set, verdict) in wai::staged::moq::check_catalog_in(&catalog, Some(namespace)) { + if let Err(r) = verdict { + return Err(CatalogError(format!("moq/catalog.json: set {set} fails moq-streaming-format §4.1 ({})", r.code()))); + } + } + wai::jcs::jcs(&catalog).map(|s| s + "\n").map_err(|e| CatalogError(format!("moq/catalog.json: {e}"))) } /// `stages/.json`: RFC 8785 canonical. A start, a piece number or a length past @@ -487,7 +576,7 @@ pub fn render_stage_view(v: &CompView, c: usize, policy_digest: &[u8; 32]) -> Re } let class = v.class_id(c); let mut comp = json!({"pieces": pieces}); - if (0..v.index.pieces.len()).any(|p| !v.listed(c, p).is_empty()) { + if v.http_lists(c) { comp["media_playlist"] = json!(format!("../hls/{class}/wai-{}.m3u8", v.id())); } let mut components = serde_json::Map::new(); @@ -497,8 +586,8 @@ pub fn render_stage_view(v: &CompView, c: usize, policy_digest: &[u8; 32]) -> Re .map_err(|e| CatalogError(format!("stages/{class}.json: {e}"))) } -/// Every catalog file of a one-component package, by path. -pub fn render_all(v: &CompView, layer_lag_ms: &[u64], default_deadline: u64, policy_digest: &[u8; 32]) -> Result>, CatalogError> { +/// Every catalog file of a one-component package, by path, under `policy`. +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() { @@ -507,7 +596,7 @@ pub fn render_all(v: &CompView, layer_lag_ms: &[u64], default_deadline: u64, pol 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()); out.insert("dash/classic.mpd".into(), render_dash_classic(v)?.into_bytes()); - out.insert("moq/catalog.json".into(), render_moq_catalog(v, layer_lag_ms, default_deadline)?.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() { let class = v.class_id(c).to_string(); out.insert(format!("hls/{class}/master.m3u8"), render_class_master(v, c)?.into_bytes()); @@ -523,6 +612,12 @@ pub fn render_all(v: &CompView, layer_lag_ms: &[u64], default_deadline: u64, pol Ok(out) } +/// The capability of a component's default base rung: its first base rung in ladder order +/// (§5.7), which its HTTP views carry (§8.3). +pub fn default_base_cap(policy: &crate::policy::Policy, comp: &str) -> String { + policy.ladder.iter().find(|r| r.component == comp && r.role == crate::policy::Role::Base).map(|r| r.capability.clone()).unwrap_or_default() +} + #[cfg(test)] mod tests { use super::*; @@ -535,9 +630,19 @@ mod tests { 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)] }), + base_cap: "wai.video.int_motion".into(), } } + fn moq(v: &CompView) -> serde_json::Value { + serde_json::from_str(&render_moq_catalog(v, "asset", &[0, 200, 400], 1500).unwrap()).unwrap() + } + + /// The media tracks of a catalog, without the receipts tracks. + fn media_tracks(m: &serde_json::Value) -> Vec { + m["tracks"].as_array().unwrap().iter().filter(|t| t["packaging"] != "wai-receipts").cloned().collect() + } + /// A class listing layers: the codecs union carries the refinement after `+`; each /// listed layer is a `#WAI-LAYER` line http-delivery's parser reads, after its piece's /// segment; the WAI and floor media playlists are adjacent; and the DASH, MoQ and @@ -576,13 +681,20 @@ mod tests { let dash = render_dash_class(&v, 0).unwrap().unwrap(); assert!(dash.contains(" = moq["tracks"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect(); + let cat = moq(&v); + let tracks = media_tracks(&cat); + let names: Vec<&str> = tracks.iter().map(|t| t["name"].as_str().unwrap()).collect(); assert_eq!(names, vec!["v/l0", "v/l1", "v/l2", "v/floor"]); - assert_eq!(moq["tracks"][1]["depends"], serde_json::json!(["v/l0"])); - assert_eq!(moq["tracks"][1]["wai_delivery_timeout_ms"], 1300); - assert_eq!(moq["tracks"][2]["wai_delivery_timeout_ms"], 1100); - assert!(moq["tracks"][2]["priority"].as_u64() > moq["tracks"][1]["priority"].as_u64(), "a refinement after the track it depends on"); + assert_eq!(tracks[1]["depends"], serde_json::json!(["v/l0"])); + assert_eq!(tracks[2]["depends"], serde_json::json!(["v/l0", "v/l1"]), "a track of every layer below (moq-streaming-format §4.1)"); + assert_eq!(tracks[1]["wai_delivery_timeout_ms"], 1300); + assert_eq!(tracks[2]["wai_delivery_timeout_ms"], 1100); + assert_eq!(tracks[2]["wai_staged"], serde_json::json!({"set": "v", "layer": 2, "lead_ms": null})); + // Each media track has a receipts track of its own and an alias no other has. + let receipts: Vec<&str> = cat["tracks"].as_array().unwrap().iter().filter(|t| t["packaging"] == "wai-receipts").map(|t| t["name"].as_str().unwrap()).collect(); + assert_eq!(receipts, vec!["v/l0/receipts", "v/l1/receipts", "v/l2/receipts"]); + assert_eq!(tracks[0]["wai_receipt_alias"], receipt_alias("asset", "v/l0")); + assert!(wai::staged::moq::check_catalog_in(&cat, Some("asset")).iter().all(|(_, r)| r.is_ok())); let stages: serde_json::Value = serde_json::from_str(&render_stage_view(&v, 1, &[0; 32]).unwrap()).unwrap(); let p0 = &stages["components"]["v"]["pieces"][0]["stages"]; assert_eq!(p0.as_array().unwrap().len(), 2); @@ -601,13 +713,13 @@ mod tests { ix.check().unwrap(); let objects: BTreeMap<[u8; 32], Vec> = objs.iter().map(|o| (crate::objects::object_hash(o), o.clone())).collect(); let v = view(&ix, &objects); - let moq: serde_json::Value = serde_json::from_str(&render_moq_catalog(&v, &[0, 200, 400], 1500).unwrap()).unwrap(); - let tracks = moq["tracks"].as_array().unwrap(); + let cat = moq(&v); + let tracks = media_tracks(&cat); let names: Vec<&str> = tracks.iter().map(|t| t["name"].as_str().unwrap()).collect(); assert_eq!(names, vec!["v/l0", "v/l1@800", "v/l2", "v/floor"]); assert_eq!(tracks[1]["depends"], serde_json::json!(["v/l0"])); - assert_eq!(tracks[2]["depends"], serde_json::json!(["v/l1@800"])); - for t in tracks { + assert_eq!(tracks[2]["depends"], serde_json::json!(["v/l0", "v/l1@800"])); + for t in &tracks { for d in t["depends"].as_array().into_iter().flatten() { assert!(names.contains(&d.as_str().unwrap()), "{d} is a track of the catalog"); } @@ -627,7 +739,6 @@ mod tests { let objects: BTreeMap<[u8; 32], Vec> = objs.iter().map(|o| (crate::objects::object_hash(o), o.clone())).collect(); let v = view(&ix, &objects); assert!(render_stage_view(&v, 0, &[0; 32]).is_err()); - assert!(render_all(&v, &[0, 200, 400], 1500, &[0; 32]).is_err()); } #[test] diff --git a/wai/wai-pack/src/conformance.rs b/wai/wai-pack/src/conformance.rs index aee17c136..53fdbc50e 100644 --- a/wai/wai-pack/src/conformance.rs +++ b/wai/wai-pack/src/conformance.rs @@ -513,3 +513,122 @@ pub fn check(dir: &Path) -> std::io::Result> { out.extend(crate::golden::check(dir)?); Ok(out) } + +/// One line of `wai-pack stage-json` (the staged differential, `stage_diff.py`): the +/// result of the line's `op`, or `{"error": …}`. +/// +/// - `refine` `{h, w, q, below, src}`: the WIR1 layer that brings each frame of `below` +/// toward `src` (§5.4): `{"payload": hex, "recon": [SHA-256 of each frame]}`. +/// - `still` `{w, h, rgb}`: the still (§5.7), `{"png": hex}`, and its decode's SHA-256. +/// - `read-png` `{hex}`: what a still's reader (§5.7) makes of the bytes: +/// `{"verdict": "ok", "w", "h", "rgb_sha256"}`, or `{"verdict": "refused"}`. +/// - `plan` `{policy, pieces: [{start, duration, kind}], layer_bytes: [[…]]}`: the policy's +/// verdict, and per piece its rules and moments, its ladder's rung ids, its deadline and +/// each class's plan (§5.7, §8.1). +/// - `deliver` `{policy, class_plan, published: [{piece, start, duration, at_frame, +/// layer, deadline_ms, bytes}], plan: {rtt_us, bytes_per_ms, chunk_bytes, enc_us, +/// startup_us, layer_cap, fifo}}`: the simulated delivery (§8.9). +pub fn stage_json(v: &Value) -> Value { + match stage_op(v) { + Ok(r) => r, + Err(e) => json!({"error": e}), + } +} + +fn u64_of(v: &Value, k: &str) -> Result { + v[k].as_u64().ok_or_else(|| format!("{k}: not an unsigned integer")) +} + +fn bytes_of(v: &Value) -> Result, String> { + v.as_array().ok_or("not an array")?.iter().map(|x| x.as_u64().filter(|b| *b < 256).map(|b| b as u8).ok_or_else(|| "not a byte".to_string())).collect() +} + +fn frames_of(v: &Value) -> Result>, String> { + v.as_array().ok_or("frames: not an array")?.iter().map(bytes_of).collect() +} + +fn stage_op(v: &Value) -> Result { + match v["op"].as_str() { + Some("refine") => { + let (h, w, q) = (u64_of(v, "h")? as u32, u64_of(v, "w")? as u32, u64_of(v, "q")? as u32); + let (payload, recon) = crate::encode::encode_refinement(&frames_of(&v["below"])?, &frames_of(&v["src"])?, h, w, q)?; + Ok(json!({"payload": hex(&payload), "recon": recon.iter().map(|f| sha256_hex(f)).collect::>()})) + } + Some("still") => { + let (w, h) = (u64_of(v, "w")? as u32, u64_of(v, "h")? as u32); + let rgb = bytes_of(&v["rgb"])?; + if rgb.len() as u64 != 3 * w as u64 * h as u64 { + return Err("rgb: not 3·w·h bytes".into()); + } + let png = crate::png::encode(&rgb, w, h); + let (_, _, back) = crate::png::decode(&png)?; + Ok(json!({"png": hex(&png), "decoded_sha256": sha256_hex(&back)})) + } + Some("read-png") => { + let b = crate::unhex(v["hex"].as_str().ok_or("hex: not a string")?).ok_or("hex: not hexadecimal")?; + Ok(match crate::png::decode(&b) { + Ok((w, h, rgb)) => json!({"verdict": "ok", "w": w, "h": h, "rgb_sha256": sha256_hex(&rgb)}), + Err(_) => json!({"verdict": "refused"}), + }) + } + Some("plan") => { + let text = v["policy"].as_str().ok_or("policy: not a string")?; + let p = match Policy::parse(text) { + Ok(p) => p, + Err(e) => return Ok(json!({"verdict": e.code()})), + }; + let comp = &p.components[0]; + let mut out = Vec::new(); + for (pc, lb) in v["pieces"].as_array().ok_or("pieces")?.iter().zip(v["layer_bytes"].as_array().ok_or("layer_bytes")?) { + let kind = crate::classify::PieceKind::from_code(u64_of(pc, "kind")? as u8).ok_or("kind")?; + let (start, duration) = (u64_of(pc, "start")?, u64_of(pc, "duration")?); + let pp = crate::plan::plan_piece(&p, comp.timebase, start, duration, kind); + let ladder = crate::plan::piece_ladder(&p, &comp.id, &pp).ok_or("no base")?; + let mut ids = vec![ladder.base.id.clone()]; + ids.extend(ladder.refinements.iter().map(|r| r.id.clone())); + ids.extend(ladder.floors.iter().map(|r| r.id.clone())); + let lb: Vec = lb.as_array().ok_or("layer_bytes")?.iter().map(|x| x.as_u64().ok_or("a byte count")).collect::>()?; + let plan: Vec = p.classes.iter().map(|c| crate::plan::class_plan(c, &lb, pp.max_layer, duration, comp.timebase)).collect(); + out.push(json!({"rules": pp.rules, "moments": pp.moments, "ladder": ids, "deadline_ms": pp.deadline_ms, "class_plan": plan})); + } + Ok(json!({"verdict": "ok", "pieces": out})) + } + Some("deliver") => { + let text = v["policy"].as_str().ok_or("policy: not a string")?; + let p = Policy::parse(text).map_err(|e| e.code().to_string())?; + let row = bytes_of(&v["class_plan"])?; + let mut published = Vec::new(); + for x in v["published"].as_array().ok_or("published")? { + let env = vec![0u8; u64_of(x, "bytes")? as usize]; + published.push(crate::live::Published { + piece: u64_of(x, "piece")?, + start: u64_of(x, "start")?, + duration: u64_of(x, "duration")?, + at_frame: u64_of(x, "at_frame")?, + layer: u64_of(x, "layer")? as u8, + role: if u64_of(x, "layer")? == 0 { wai::stage_index::Role::Base } else { wai::stage_index::Role::Refinement }, + capability: String::new(), + deadline_ms: u64_of(x, "deadline_ms")? as u32, + hash: crate::objects::object_hash(&env), + envelope: env, + }); + } + let q = &v["plan"]; + let plan = crate::delivery::DeliveryPlan { + rtt_us: u64_of(q, "rtt_us")?, + bytes_per_ms: u64_of(q, "bytes_per_ms")?, + chunk_bytes: u64_of(q, "chunk_bytes")?, + enc_us: u64_of(q, "enc_us")?, + startup_us: u64_of(q, "startup_us")?, + layer_cap: q["layer_cap"].as_u64().map(|c| c as u8), + schedule: if q["fifo"].as_bool() == Some(true) { wai::stage_measure::sim::Schedule::Fifo } else { wai::stage_measure::sim::Schedule::StrictPriority }, + }; + let key = ed25519_dalek::SigningKey::from_bytes(&[0x5a; 32]); + let d = crate::delivery::simulate(&published, &p, &row, &plan, &key)?; + let mut j = d.to_json(); + j.as_object_mut().expect("an object").remove("plan"); + Ok(j) + } + _ => Err("op: refine, still, plan or deliver".into()), + } +} diff --git a/wai/wai-pack/src/delivery.rs b/wai/wai-pack/src/delivery.rs new file mode 100644 index 000000000..1b00738d2 --- /dev/null +++ b/wai/wai-pack/src/delivery.rs @@ -0,0 +1,247 @@ +//! A package's delivery over MoQ, simulated ([packaging] §8.9): the staged-delivery +//! simulator of staged-measurement §10 and Appendix B (`wai::stage_measure::sim`), run +//! over what a live packer publishes and what a class subscribes to. This module adds no +//! simulator of its own: it states the model and the plan, runs the one simulator, and +//! reads its presentations per piece. +//! +//! - **Model.** Each object a class lists of each piece (§8.1; a piece the class takes +//! as floor only contributes none), on the clip's frames as units: base `p` = the +//! piece, its layer, `units` the piece's frames, `bytes` the whole envelope, and its +//! capture time the instant it is published: the live packer publishes a piece's base +//! when the frame that closes the piece has arrived (frame `f` at `f · frame_us`), plus +//! `enc_us`, and layer `k` `layer_lag_ms[k]` after its base. Each refinement takes the +//! delivery timeout of its track, `(deadline_ms − layer_lag_ms[k]) · 1000` (§8.6). +//! - **Plan.** Live; the link's round trip, rate and chunk; strict priority, which is +//! §8.5's order (the base track's publisher priority below every layer's, a layer's +//! no greater than any above it); each (piece, layer) its own subgroup; no base +//! timeout; the subscriber's layer cap; one decode worker taking bases first, each +//! decode one microsecond (the simulation is of the link); playout from `startup_us`, +//! and a refinement that arrives after its piece's instant discarded (§8.7). +//! - **Outcome.** Per piece, at its first frame's presentation instant (`startup_us + +//! start · frame_us`): the highest layer presented by then, or none where even the base +//! arrived later (a stall, which §8.7's rule is there to prevent); the base's arrival; +//! and the layers dropped on the way. +//! +//! The publisher seals each track's group (a piece) with a jwp-receipts group receipt, +//! on the track's receipt chain (`wai_receipt_alias`, §8.4), and a subscriber accepts +//! every delivered object by its Merkle proof; an object dropped on the way is accounted +//! by inclusion alone (jwp-receipts §4.4), never as delivered. +//! +//! Every figure is simulated, labelled so, and never a measurement of a network. +//! +//! [packaging]: ../../../extensions/packaging.md + +use crate::catalog; +use crate::live::Published; +use crate::policy::Policy; +use ed25519_dalek::SigningKey; +use serde_json::{json, Value}; +use std::collections::BTreeMap; +use wai::stage_index::FLOOR_ONLY; +use wai::stage_measure::sim::{self, Carriage, DecodeOrder, LateRefinement, Mode, Model, ObjState, Plan, Schedule, SimObject}; + +/// The link, the encoder's delay and the subscriber of a simulated delivery. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeliveryPlan { + pub rtt_us: u64, + pub bytes_per_ms: u64, + pub chunk_bytes: u64, + /// From a piece's last needed frame to its base's publication. + pub enc_us: u64, + /// From the first frame's capture to its presentation. + pub startup_us: u64, + /// The highest layer the subscriber subscribes to (moq-streaming-format §4.1, + /// *Capping*); `None`: every layer track. + pub layer_cap: Option, + /// The sender's schedule: `StrictPriority` is §8.5's. `Fifo` is not a conforming + /// sender; it is here to show what §8.5 prevents. + pub schedule: Schedule, +} + +/// One piece's outcome. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PieceOutcome { + pub piece: u64, + /// The piece's first frame's presentation instant. + pub due_us: u64, + /// The highest layer presented at that instant; `None` where the base arrived later + /// (or the class takes the piece as floor only: `floor_only`). + pub presented: Option, + pub floor_only: bool, + pub base_arrival_us: Option, + /// The layers of the piece the link dropped or abandoned. + pub dropped: Vec, +} + +/// A simulated delivery. +#[derive(Debug, Clone)] +pub struct Delivery { + pub model: Model, + pub plan: Plan, + /// `None` where the class lists nothing of any piece: nothing is sent. + pub result: Option, + pub pieces: Vec, + /// Per object of the model, its track's group receipt and its own receipt. + pub receipts: Vec<(wai::jwp_delivery::GroupReceipt, wai::jwp_delivery::ObjectReceipt)>, +} + +impl Delivery { + /// The highest layer presented of each piece, `0xFF` for none: the vector a test + /// commits. + pub fn presented(&self) -> Vec { + self.pieces.iter().map(|p| p.presented.unwrap_or(FLOOR_ONLY)).collect() + } + + /// The outcome as JSON, labelled simulated, with its plan. + pub fn to_json(&self) -> Value { + json!({ + "label": "simulated", + "plan": self.plan.to_json(), + "pieces": self.pieces.iter().map(|p| json!({ + "piece": p.piece, "due_us": p.due_us, "presented": p.presented, "floor_only": p.floor_only, + "base_arrival_us": p.base_arrival_us, "dropped": p.dropped, + })).collect::>(), + "delivered": self.result.as_ref().map_or(0, |r| r.delivered), + "dropped": self.result.as_ref().map_or(0, |r| r.dropped), + "abandoned": self.result.as_ref().map_or(0, |r| r.abandoned), + "never_sent_capped": self.result.as_ref().map_or(0, |r| r.never_sent_capped), + }) + } +} + +/// `us` microseconds after 1970-01-01T00:00:00Z, as RFC 3339 to the microsecond: a group +/// receipt's start and end on the simulation's clock. +fn rfc3339(us: u64) -> String { + let (s, frac) = (us / 1_000_000, us % 1_000_000); + let (days, rem) = (s / 86_400, s % 86_400); + format!("1970-01-{:02}T{:02}:{:02}:{:02}.{frac:06}Z", 1 + days.min(30), rem / 3600, rem % 3600 / 60, rem % 60) +} + +/// Simulate the delivery to a subscriber of class `class` of what a live packer +/// published (`published`, in publication order) under `policy`, whose class plan per +/// piece is `class_plan` (the class's row of the windows' class plans, by absolute piece +/// number). `key` signs the group receipts. +pub fn simulate(published: &[Published], policy: &Policy, class_plan: &[u8], plan: &DeliveryPlan, key: &SigningKey) -> Result { + let comp = policy.components.first().ok_or("no component")?; + let tb = comp.timebase; + let frame_us = 1_000_000u64.checked_mul(tb.den as u64).filter(|x| x % tb.num as u64 == 0).map(|x| x / tb.num as u64).ok_or("a frame that is not a whole number of microseconds")?; + // §8.5: the base track's priority below every layer track's, each layer's no greater + // than any above it; strict priority by layer is that order. + let layers: Vec = (1..policy.layer_count()).map(|k| (catalog::PRIORITY_BASE + catalog::PRIORITY_STEP * k) as u8).collect(); + if !wai::staged::moq::priorities_ok(catalog::PRIORITY_BASE as u8, &layers) { + return Err("§8.5's priorities are not MoQ's order".into()); + } + let mut objects = Vec::new(); + let mut idx: Vec<&Published> = Vec::new(); + for p in published { + let listed = class_plan.get(p.piece as usize).copied().ok_or("a piece past the class plan")?; + if listed == FLOOR_ONLY || p.layer > listed { + continue; + } + let lag = policy.layer_lag_ms.get(p.layer as usize).copied().unwrap_or(0); + let at = p + .at_frame + .checked_mul(frame_us) + .and_then(|x| x.checked_add(plan.enc_us)) + .and_then(|x| x.checked_add(lag.checked_mul(1000)?)) + .ok_or("a time past 2^64 - 1")?; + objects.push(SimObject { + id: format!("p{}/l{}", p.piece, p.layer), + base: u32::try_from(p.piece).map_err(|_| "a piece past 2^32")?, + layer: p.layer, + from: p.start, + count: p.duration, + bytes: p.envelope.len() as u64, + capture_us: Some(at), + timeout_us: (p.layer > 0).then(|| (p.deadline_ms as u64).saturating_sub(lag) * 1000), + }); + idx.push(p); + } + let model = Model { unit_us: frame_us, objects }; + let sp = Plan { + mode: Mode::Live, + rtt_us: plan.rtt_us, + bytes_per_ms: plan.bytes_per_ms, + chunk_bytes: plan.chunk_bytes, + schedule: plan.schedule, + carriage: Carriage::Subgroup, + layer_cap: plan.layer_cap, + timeout_base_us: None, + timeout_refinement_us: None, + vod_window_us: None, + workers: 1, + decode_order: DecodeOrder::BaseFirst, + decode_base_us: 1, + decode_refinement_us: 1, + playout_start_us: Some(plan.startup_us), + late_refinement: LateRefinement::Discard, + }; + let result = if model.objects.is_empty() { None } else { Some(sim::simulate(&model, &sp).map_err(|e| e.to_string())?) }; + // Per piece, at its first frame's instant. + let mut pieces: BTreeMap = BTreeMap::new(); + for p in published { + let due = plan.startup_us + p.start * frame_us; + let floor_only = class_plan[p.piece as usize] == FLOOR_ONLY; + pieces.entry(p.piece).or_insert(PieceOutcome { piece: p.piece, due_us: due, presented: None, floor_only, base_arrival_us: None, dropped: Vec::new() }); + } + let events = result.as_ref().map_or(&[][..], |r| &r.objects[..]); + let presentations = result.as_ref().map_or(&[][..], |r| &r.presentations[..]); + for (o, ev) in model.objects.iter().zip(events) { + let po = pieces.get_mut(&(o.base as u64)).expect("a piece"); + if o.layer == 0 { + po.base_arrival_us = ev.arrived_us; + } + if matches!(ev.state, ObjState::Dropped | ObjState::Abandoned) { + po.dropped.push(o.layer); + } + } + for po in pieces.values_mut() { + let start = published.iter().find(|p| p.piece == po.piece).expect("a piece").start; + let at: Vec<&(u64, u16, u64)> = presentations.iter().filter(|(u, _, t)| *u == start && *t <= po.due_us).collect(); + if at.iter().any(|(_, l, _)| *l == 1) { + po.presented = at.iter().map(|(_, l, _)| (*l - 1) as u8).max(); + } + } + // The receipts: per track, per group, on the track's chain. + let tracks = |p: &Published| { + let name = if p.layer == 0 || p.deadline_ms as u64 == policy.deadline_ms { format!("{}/l{}", comp.id, p.layer) } else { format!("{}/l{}@{}", comp.id, p.layer, p.deadline_ms) }; + catalog::receipt_alias(&policy.asset_id, &name) + }; + let signer_id = wai::signer_id::did_key(&key.verifying_key().to_bytes()); + let mut heads: BTreeMap = BTreeMap::new(); + let mut receipts = Vec::with_capacity(idx.len()); + for (o, p) in model.objects.iter().zip(&idx) { + let alias = tracks(p); + let mut b = wai::jwp_delivery::GroupBuilder::default(); + b.add(p.hash, 0, "", None); + let at = o.capture_us.expect("live"); + let (g, mut objs) = b + .finish(key, &signer_id, alias, p.piece, &rfc3339(at), &rfc3339(at), heads.get(&alias).copied(), false) + .ok_or("a group receipt")?; + heads.insert(alias, g.link_hash()); + receipts.push((g, objs.remove(0))); + } + Ok(Delivery { model, plan: sp, result, pieces: pieces.into_values().collect(), receipts }) +} + +/// Pack the source `y4m` live under `policy_bytes` (one window) and simulate its delivery +/// to a subscriber of class `class` ([`simulate`]). +pub fn simulate_source(policy_bytes: &[u8], y4m: &[u8], class: &str, plan: &DeliveryPlan, key: &SigningKey) -> Result { + let (hd, frames) = crate::source::y4m::read_all(y4m).map_err(|e| e.to_string())?; + let signer_id = wai::signer_id::did_key(&key.verifying_key().to_bytes()); + let mut lp = crate::live::LivePacker::new(policy_bytes, hd.w, hd.h, crate::live::LiveOpts::new(key.clone(), signer_id)).map_err(|e| e.to_string())?; + let mut published = Vec::new(); + for f in &frames { + published.extend(lp.push(&f.rgb).map_err(|e| e.to_string())?); + } + let policy = lp.policy().clone(); + let (rest, windows, _) = lp.finish().map_err(|e| e.to_string())?; + published.extend(rest); + let ci = policy.classes.iter().position(|c| c.id == class).ok_or_else(|| format!("no class {class}"))?; + let mut row = Vec::new(); + for w in &windows { + let ix = wai::stage_index::StageIndex::parse(&w.index).map_err(|e| e.to_string())?; + row.extend_from_slice(&ix.plan[ci]); + } + simulate(&published, &policy, &row, plan, key) +} diff --git a/wai/wai-pack/src/encode.rs b/wai/wai-pack/src/encode.rs index 0c572bb62..a7dd4e099 100644 --- a/wai/wai-pack/src/encode.rs +++ b/wai/wai-pack/src/encode.rs @@ -69,10 +69,52 @@ pub fn choose_q(frames: &[Vec], h: u32, w: u32, p: &MotionParams) -> Result< unreachable!("the smallest step is always chosen when it is reached") } +/// The sum, over every sample, of the squared difference between two clips of RGB8 +/// frames (§6.6). +pub fn sse(a: &[Vec], b: &[Vec]) -> u64 { + a.iter().zip(b).flat_map(|(x, y)| x.iter().zip(y)).map(|(&x, &y)| (x as i64 - y as i64).unsigned_abs().pow(2)).sum() +} + +/// One refinement layer of a piece (§5.4): a `WIR1` of unit kind RGB8, one unit per frame, +/// the identity predictor, one table, at most 4095 table symbols, that brings each frame +/// of `below` (the stage the layer refines) toward its source frame in `src`, at step +/// `q`. This is `wai-rs`'s refinement encoder (`staged::wir1::encode`), the one `wai stage +/// refine` runs: the residual `rdiv(source − below, q)`, its table, its streams. Returns +/// the payload and the frames a sink reconstructs, which the encoder has decoded back. +pub fn encode_refinement(below: &[Vec], src: &[Vec], h: u32, w: u32, q: u32) -> Result<(Vec, Vec>), String> { + use wai::staged::wir1::{encode, Below, Next, Predictor, RefineParams, UnitKind}; + let belows: Vec = below.iter().map(|f| Below::U8(f)).collect(); + let targets: Vec = src.iter().map(|f| Below::U8(f)).collect(); + let p = RefineParams { q, predictor: Predictor::Identity, rate: (0, 0), table_per_unit: false, max_symbols: 4095 }; + let (payload, next) = encode(UnitKind::Rgb8, &belows, (h, w, 3), &targets, (h, w, 3), &p).map_err(|e| e.0)?; + let frames = next + .into_iter() + .map(|n| match n { + Next::U8(v) => Ok(v), + Next::I16(_) => Err("an RGB8 layer decoded to PCM".to_string()), + }) + .collect::, _>>()?; + Ok((payload, frames)) +} + #[cfg(test)] mod tests { use super::*; + /// A layer brings every frame closer to its source, and a finer step closer still. + #[test] + fn a_refinement_reduces_the_error() { + let src = clip(); + let base = encode(&src, 10, 12, &EncParams { block: 4, search: 1, mv_precision: 1, q: 24 }).unwrap(); + let (p1, r1) = encode_refinement(&base.recon, &src, 10, 12, 8).unwrap(); + let (_, r2) = encode_refinement(&r1, &src, 10, 12, 2).unwrap(); + assert_eq!(&p1[..4], b"WIR1"); + assert_eq!(sse(&base.recon, &src), base.sse); + assert!(sse(&r1, &src) < base.sse && sse(&r2, &src) < sse(&r1, &src), "{} {} {}", base.sse, sse(&r1, &src), sse(&r2, &src)); + // Step 1 reconstructs the source exactly. + assert_eq!(encode_refinement(&r1, &src, 10, 12, 1).unwrap().1, src); + } + fn clip() -> Vec> { (0..4).map(|t| (0..12 * 10 * 3).map(|i| ((i * 37 + t * 11) % 251) as u8).collect()).collect() } diff --git a/wai/wai-pack/src/fixture.rs b/wai/wai-pack/src/fixture.rs index 931aef49b..2fb1c1acb 100644 --- a/wai/wai-pack/src/fixture.rs +++ b/wai/wai-pack/src/fixture.rs @@ -21,7 +21,7 @@ //! v ^= v >> 16` (wrapping). Chroma is constant per shot. /// The fixtures this module generates. -pub const NAMES: &[&str] = &["cuts-01", "kinds-01", "video-base-01"]; +pub const NAMES: &[&str] = &["cuts-01", "kinds-01", "video-base-01", "video-staged-01"]; /// The fixtures the scene-cut, encode and step-choice corpora cover. pub const SCENECUT_NAMES: &[&str] = &["cuts-01", "kinds-01"]; @@ -119,6 +119,19 @@ fn spans(name: &str) -> Option<(u32, u32, Vec)> { s(40, ramp(16, 160, 1, 2, 1, true, 13, 128, 128)), ]) } + // The staged golden: a moving shot, the bumper, a still shot (no motion, no + // noise), the bumper again (the same frames both times), and a second moving + // shot. + "video-staged-01" => { + let bumper = Shot::Blocks { levels: [40, 120, 200], bw: 8, bh: 6, speed: 1, cb: 100, cr: 160 }; + (64, 36, vec![ + s(50, ramp(20, 200, 1, 1, 1, true, 21, 110, 140)), + local(25, bumper), + s(40, ramp(60, 180, 2, 1, 0, false, 0, 140, 116)), + local(25, bumper), + s(60, ramp(30, 120, 1, 2, 1, true, 22, 150, 120)), + ]) + } _ => return None, }) } @@ -173,7 +186,7 @@ mod tests { for n in NAMES { let b = generate(n).unwrap(); let (hd, frames) = crate::source::y4m::read_all(&b).unwrap(); - let dims = if *n == "video-base-01" { (64, 36) } else { (32, 18) }; + let dims = if n.starts_with("video-") { (64, 36) } else { (32, 18) }; assert_eq!((hd.w, hd.h, hd.rate), (dims.0, dims.1, Some((25, 1))), "{n}"); assert!(frames.len() >= 200, "{n}"); } @@ -181,6 +194,9 @@ mod tests { let (_, f) = crate::source::y4m::read_all(&generate("video-base-01").unwrap()).unwrap(); assert_eq!(f[60..85], f[135..160]); assert_ne!(f[60..85], f[85..110]); + let (_, f) = crate::source::y4m::read_all(&generate("video-staged-01").unwrap()).unwrap(); + assert_eq!(f[50..75], f[115..140]); + assert!(f[75..115].iter().all(|x| *x == f[75]), "the still shot does not change"); assert!(generate("nope").is_none()); } } diff --git a/wai/wai-pack/src/golden.rs b/wai/wai-pack/src/golden.rs index 9a916f8c1..f1e5804d7 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"]; +pub const PACKS: &[&str] = &["video-base-01", "video-staged-01"]; type Files = BTreeMap>; @@ -573,8 +573,8 @@ pub fn reseal_as(files: &mut Files, key: &SigningKey, policy_json: Option<&[u8]> files.extend(frags); } let (w, h) = dims.unwrap_or((0, 0)); - let view = CompView { index: &ix, tb, w, h, objects: &objects, floor: floor_view }; - if let Ok(cats) = crate::catalog::render_all(&view, &policy.layer_lag_ms, policy.deadline_ms, &policy_digest) { + let view = CompView { index: &ix, tb, w, h, objects: &objects, floor: floor_view, 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); } let parent = if ix.header.prev_index_digest != [0u8; 32] { Parent::LiveWindow([7u8; 32]) } else { Parent::None }; @@ -595,6 +595,9 @@ pub fn relist(files: &mut Files) { // ─────────────────────────────── catalogs ─────────────────────────────── +/// The namespace the catalog corpus's MoQ catalogs are written in. +pub const CATALOG_NAMESPACE: &str = "catalogs"; + /// MoQ catalogs (§8.4) of indexes no golden package has: layers on more than one /// deadline, so each refinement track's `depends` is read from what its entries refine. pub fn catalogs() -> (Files, Value) { @@ -619,11 +622,11 @@ pub fn catalogs() -> (Files, Value) { 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) }; - let moq = render_moq_catalog(&v, &lag, deadline).expect("a catalog"); + 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 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()); - cases.push(json!({"index": path, "layer_lag_ms": lag, "default_deadline": deadline, "floor": {"codec": "av01.0.00M.08", "w": 64, "h": 36}, "moq": moq})); + cases.push(json!({"index": path, "namespace": CATALOG_NAMESPACE, "layer_lag_ms": lag, "default_deadline": deadline, "floor": {"codec": "av01.0.00M.08", "w": 64, "h": 36}, "moq": moq})); } (files, json!({"cases": cases})) } @@ -936,6 +939,220 @@ pub fn package_mutations(g: &Files, key: &SigningKey) -> Vec { out } +/// The staged golden, which the staged mutations alter. +pub const STAGED_PACK: &str = "video-staged-01"; + +/// Entry `k`'s prefix as the reference sink gives it: its piece's base decoded, and each +/// refinement up to `k` applied. +fn prefix_frames(f: &Files, ix: &StageIndex, k: usize) -> Vec> { + let p = ix.pieces.iter().position(|p| (p.first_entry as usize..(p.first_entry + p.n_entries as u32) as usize).contains(&k)).expect("a piece"); + let first = ix.pieces[p].first_entry as usize; + let obj = |i: usize| f[&objects::object_path(&ix.entries[i].content_hash)].clone(); + let (info, units) = wai::staged::decode_base(&obj(first), &BTreeMap::new(), None, None).expect("the golden's base decodes"); + let mut c = wai::staged::StageChain::new(info, units, wai::staged::ChainConfig { + bound: wai::staged::HoldBound { max_envelopes: 0, max_age_ms: 0 }, + cost_limit: None, + budget: u64::MAX, + record_budget: 0, + live: None, + }); + for i in first + 1..=k { + c.offer(&obj(i), 0, wai::staged::ReceiptEvidence::None).expect("the golden's layer applies"); + } + c.units().iter().map(|u| match &u.buf.samples { + wai::staged::Samples::U8(v) => v.clone(), + wai::staged::Samples::I16(_) => unreachable!("frames"), + }).collect() +} + +/// 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 +/// 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 +/// the frame held for the piece (§5.7, §6.5). +pub fn staged_mutations(g: &Files, key: &SigningKey) -> Vec { + let sealed = |mut f: Files| { + reseal(&mut f, key).expect("the alteration re-seals"); + f + }; + 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() }); + 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"); + // The last piece's layers ride the event's deadline (800 ms): one on the default. + let mut f = g.clone(); + let mut x = golden_index(&f); + let k = top(&x, last); + assert_eq!(x.entries[k].deadline_ms, 800, "the event's deadline"); + x.entries[k].deadline_ms = 1500; + set_golden_index(&mut f, &x); + add("refinement-deadline", "index-policy-mismatch", sealed(f)); + // The last piece without its top layer: its entry and object gone, each class plan + // capped at the layer below. + let mut f = g.clone(); + let mut x = golden_index(&f); + let k = top(&x, last); + f.remove(&objects::object_path(&x.entries[k].content_hash)); + x.entries.remove(k); + x.pieces[last].n_entries -= 1; + let below = x.entries[k - 1].layer; + for row in x.plan.iter_mut() { + if row[last] != wsix::FLOOR_ONLY && row[last] > below { + row[last] = below; + } + } + set_golden_index(&mut f, &x); + add("layer-dropped", "index-policy-mismatch", sealed(f)); + // The static piece's rule giving it the default ladder: its still is no longer its + // ladder's base. + let mut f = g.clone(); + policy_edit(&mut f, "then = { ladder = [\"still\", \"f0\"] }", "then = { ladder = [\"b0\", \"r1\", \"f0\"] }"); + add("still-not-the-ladder", "index-policy-mismatch", sealed(f)); + // The last piece's top layer coded again at step 5 over the layer below: a well-formed + // layer whose output is not the prefix digest its entry states. + let mut f = g.clone(); + let x = golden_index(&f); + let k = top(&x, last); + let below = prefix_frames(&f, &x, k - 1); + let (_, frames) = crate::source::y4m::read_all(&crate::fixture::generate(STAGED_PACK).expect("the fixture")).expect("a Y4M"); + let pc = &x.pieces[last]; + let src: Vec> = frames[pc.start as usize..(pc.start + pc.duration as u64) as usize].iter().map(|fr| fr.rgb.clone()).collect(); + let (wd, ht) = (frames[0].w, frames[0].h); + let (payload, _) = crate::encode::encode_refinement(&below, &src, ht, wd, 5).expect("a layer encodes"); + let base = x.entries[pc.first_entry as usize].content_hash; + let env = objects::refinement_envelope("video", x.capability(&x.entries[k]), payload, &base, &x.entries[k - 1].content_hash, x.entries[k].layer); + replace_object(&mut f, k, env); + add("layer-replaced", "prefix-digest", sealed(f)); + // A piece past the policy's max_ms (§4.4), its rules, moments and class plan what its + // new span gives: the last piece, and the still as the last piece of a package cut + // after it. Refused before anything is decoded for it (index-policy-mismatch), so a + // still is never held for 2^32 − 1 frames. + let policy = crate::policy::Policy::parse_bytes(&g["policy.toml"]).expect("the golden's policy"); + let retime = |x: &mut StageIndex, p: usize, duration: u32| { + let tb = policy.components[0].timebase; + x.pieces[p].duration = duration; + let kind = crate::classify::PieceKind::from_code(x.pieces[p].kind).expect("a kind"); + let pp = crate::plan::plan_piece(&policy, tb, x.pieces[p].start, duration as u64, kind); + (x.pieces[p].rules, x.pieces[p].moments) = (pp.rules, pp.moments); + let bytes: Vec = x.piece_entries(p).iter().filter(|e| matches!(e.role, Role::Base | Role::Refinement)).map(|e| e.byte_len).collect(); + for (ci, c) in policy.classes.iter().enumerate() { + x.plan[ci][p] = crate::plan::class_plan(c, &bytes, pp.max_layer, duration as u64, tb); + } + }; + let mut f = g.clone(); + let mut x = golden_index(&f); + retime(&mut x, last, u32::MAX); + set_golden_index(&mut f, &x); + add("piece-past-max", "index-policy-mismatch", sealed(f)); + let mut f = g.clone(); + let mut x = golden_index(&f); + let still = x.entries.iter().position(|e| x.capability(e) == "wai.image.png").map(|k| x.entries[k].piece as usize).expect("the still"); + let keep = (x.pieces[still].first_entry + x.pieces[still].n_entries as u32) as usize; + x.pieces.truncate(still + 1); + x.entries.truncate(keep); + for row in x.plan.iter_mut() { + row.truncate(still + 1); + } + let starts: Vec = x.pieces.iter().map(|p| p.start).collect(); + x.header.plan_digest = wsix::plan_digest(&starts); + retime(&mut x, still, u32::MAX); + let named: std::collections::BTreeSet = x.entries.iter().map(|e| objects::object_path(&e.content_hash)).collect(); + f.retain(|k, _| !k.starts_with("objects/") || named.contains(k)); + f.insert("plan-v.txt".into(), crate::pack::plan_file(&starts).into_bytes()); + set_golden_index(&mut f, &x); + add("still-past-max", "index-policy-mismatch", sealed(f)); + // A policy past §4.4's longest piece: refused as a policy, before its index is read. + let mut f = g.clone(); + policy_edit(&mut f, "max_ms = 4000", "max_ms = 600001"); + relist(&mut f); + add("max-ms-past-cap", "policy-piece-bounds", f); + // The longest piece the policy allows (max_ms ten minutes) given to the still, as the + // last piece of a package cut after it, its manifest naming the frames: refused by + // the floor's unit count before a frame is decoded (§11.3), in either verifier. + let mut f = g.clone(); + policy_edit(&mut f, "max_ms = 4000", "max_ms = 600000"); + let at_cap = crate::policy::Policy::parse_bytes(&f["policy.toml"]).expect("a policy at the cap"); + let tb = at_cap.components[0].timebase; + let units = u32::try_from(tb.units_from_ms(at_cap.segmentation.max_ms)).expect("within 2^24"); + let mut x = golden_index(&f); + let still = x.entries.iter().position(|e| x.capability(e) == "wai.image.png").map(|k| x.entries[k].piece as usize).expect("the still"); + let keep = (x.pieces[still].first_entry + x.pieces[still].n_entries as u32) as usize; + x.pieces.truncate(still + 1); + x.entries.truncate(keep); + for row in x.plan.iter_mut() { + row.truncate(still + 1); + } + let starts: Vec = x.pieces.iter().map(|p| p.start).collect(); + x.header.plan_digest = wsix::plan_digest(&starts); + x.pieces[still].duration = units; + let kind = crate::classify::PieceKind::from_code(x.pieces[still].kind).expect("a kind"); + let pp = crate::plan::plan_piece(&at_cap, tb, x.pieces[still].start, units as u64, kind); + (x.pieces[still].rules, x.pieces[still].moments) = (pp.rules, pp.moments); + let bytes: Vec = x.piece_entries(still).iter().filter(|e| matches!(e.role, Role::Base | Role::Refinement)).map(|e| e.byte_len).collect(); + for (ci, c) in at_cap.classes.iter().enumerate() { + x.plan[ci][still] = crate::plan::class_plan(c, &bytes, pp.max_layer, units as u64, tb); + } + let named: std::collections::BTreeSet = x.entries.iter().map(|e| objects::object_path(&e.content_hash)).collect(); + f.retain(|k, _| !k.starts_with("objects/") || named.contains(k)); + f.insert("plan-v.txt".into(), crate::pack::plan_file(&starts).into_bytes()); + set_golden_index(&mut f, &x); + let k = x.entries.iter().position(|e| x.capability(e) == "wai.image.png").expect("the still"); + let w = Wai::from_bytes(&f[&objects::object_path(&x.entries[k].content_hash)]).expect("an envelope"); + let target = w.manifest.target.clone(); + let (h, wd) = (target["h"].as_u64().expect("h") as u32, target["w"].as_u64().expect("w") as u32); + replace_object(&mut f, k, objects::base_envelope("video", "wai.image.png", w.payload.clone(), units as u64, h, wd)); + add("still-at-cap", "floor-object-mismatch", sealed(f)); + // A base's and a refinement's manifest past §5.2 and §5.4: a still whose target holds + // one member more, and a top layer whose target is not null (`object-manifest`). + let mut f = g.clone(); + let x = golden_index(&f); + let k = x.entries.iter().position(|e| x.capability(e) == "wai.image.png").expect("the still"); + let mut w = Wai::from_bytes(&f[&objects::object_path(&x.entries[k].content_hash)]).expect("an envelope"); + w.manifest.target["x"] = json!(1); + replace_object(&mut f, k, w.to_bytes().expect("a manifest serialises")); + add("still-manifest-target", "object-manifest", sealed(f)); + let mut f = g.clone(); + let x = golden_index(&f); + let k = top(&x, last); + let mut w = Wai::from_bytes(&f[&objects::object_path(&x.entries[k].content_hash)]).expect("an envelope"); + w.manifest.target = json!({}); + replace_object(&mut f, k, w.to_bytes().expect("a manifest serialises")); + add("refinement-manifest-target", "object-manifest", sealed(f)); + // The still as a PNG §5.7 does not read: its one dynamic block's lone distance code is + // two bits long, an incomplete code (`prefix-digest`). + let mut f = g.clone(); + let x = golden_index(&f); + let k = x.entries.iter().position(|e| x.capability(e) == "wai.image.png").expect("the still"); + let w = Wai::from_bytes(&f[&objects::object_path(&x.entries[k].content_hash)]).expect("an envelope"); + let (pw, ph, rgb) = crate::png::decode(&w.payload).expect("a still"); + let mut raw = Vec::new(); + for row in rgb.chunks_exact(3 * pw as usize) { + raw.push(0); + raw.extend_from_slice(row); + } + let mut png = vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]; + crate::png::chunk(&mut png, b"IHDR", &[&pw.to_be_bytes()[..], &ph.to_be_bytes(), &[8, 2, 0, 0, 0]].concat()); + crate::png::chunk(&mut png, b"IDAT", &[&[0x78u8, 0x01][..], &dynamic_literals(&raw, 2)].concat()); + crate::png::chunk(&mut png, b"IEND", &[]); + assert!(crate::png::decode(&png).is_err()); + let target = &w.manifest.target; + let frames = target["frames"].as_u64().expect("frames"); + replace_object(&mut f, k, objects::base_envelope("video", "wai.image.png", png, frames, ph, pw)); + add("still-incomplete-dist", "prefix-digest", sealed(f)); + // The still's prefix digest of its one frame, not of the frame held for its piece. + let mut f = g.clone(); + let mut x = golden_index(&f); + let k = x.entries.iter().position(|e| x.capability(e) == "wai.image.png").expect("the still"); + let w = Wai::from_bytes(&f[&objects::object_path(&x.entries[k].content_hash)]).expect("an envelope"); + let (_, _, rgb) = crate::png::decode(&w.payload).expect("a still"); + x.entries[k].prefix_digest = crate::pack::clip_digest(&[rgb]); + set_golden_index(&mut f, &x); + add("still-held-digest", "prefix-digest", sealed(f)); + out +} + static CASE_DIRS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); fn set_unreadable(path: &Path, unreadable: bool) -> std::io::Result<()> { @@ -981,12 +1198,22 @@ pub fn package_case_verdict(files: &Files, empty_dirs: &[String], unreadable_dir /// removes, the directories it adds that hold nothing (`empty_dirs`) or cannot be read /// (`unreadable_dirs`), and its reason. pub fn package_overlays(g: &Files, key: &SigningKey) -> (Files, Value) { + overlays("package-mutations", MUTATED_PACK, g, package_mutations(g, key)) +} + +/// The staged mutations as overlays on the staged golden (`staged-mutations/`), in the +/// layout of [`package_overlays`]. +pub fn staged_overlays(g: &Files, key: &SigningKey) -> (Files, Value) { + overlays("staged-mutations", STAGED_PACK, g, staged_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(); - for m in package_mutations(g, key) { + for m in mutations { for (p, b) in &m.files { if g.get(p) != Some(b) { - files.insert(format!("package-mutations/{}/{p}", m.name), b.clone()); + files.insert(format!("{dir}/{}/{p}", m.name), b.clone()); } } let remove: Vec<&String> = g.keys().filter(|p| !m.files.contains_key(*p)).collect(); @@ -999,7 +1226,7 @@ pub fn package_overlays(g: &Files, key: &SigningKey) -> (Files, Value) { } cases.push(c); } - (files, json!({"pack": MUTATED_PACK, "cases": cases})) + (files, json!({"pack": pack, "cases": cases})) } /// A package case's files: the golden, less what it removes, with its overlay. @@ -1052,6 +1279,292 @@ pub fn v1_receipts() -> (Files, Value) { (files, json!({"cases": cases})) } +// ─────────────────────────────── stills ─────────────────────────────── + +/// `raw` as a deflate stream of one final dynamic block holding only literals and the +/// end of the block, then the Adler-32 of `raw`. The literal/length code is complete +/// (symbols 0 to 253 of eight bits; 254, 255, 256 and 257 of nine), and the distance +/// code is one code of `dist_bits` bits, used by no symbol. The code-length code gives +/// lengths 1, 2, 8 and 9 two bits each. +fn dynamic_literals(raw: &[u8], dist_bits: u8) -> Vec { + struct W { + out: Vec, + acc: u32, + n: u32, + } + impl W { + fn put(&mut self, v: u32, n: u32) { + for i in 0..n { + self.acc |= ((v >> i) & 1) << self.n; + self.n += 1; + if self.n == 8 { + self.out.push(self.acc as u8); + self.acc = 0; + self.n = 0; + } + } + } + /// A Huffman code, most significant bit first. + fn code(&mut self, c: u32, n: u32) { + for i in (0..n).rev() { + self.put((c >> i) & 1, 1); + } + } + } + let mut lit = [8u8; 258]; + lit[254..].fill(9); + // Canonical codes (RFC 1951 §3.2.2). + let codes = |lengths: &[u8]| -> Vec { + let mut count = [0u32; 16]; + lengths.iter().for_each(|&l| count[l as usize] += 1); + count[0] = 0; + let mut next = [0u32; 16]; + let mut code = 0; + for b in 1..16 { + code = (code + count[b - 1]) << 1; + next[b] = code; + } + lengths.iter().map(|&l| if l == 0 { 0 } else { let c = next[l as usize]; next[l as usize] += 1; c }).collect() + }; + let lc = codes(&lit); + // The code-length code: symbols 1, 2, 8 and 9 of two bits; in RFC 1951's order the + // last of them (1) is the 18th. + const ORDER: [usize; 19] = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; + let mut cl = [0u8; 19]; + for s in [1, 2, 8, 9] { + cl[s] = 2; + } + let cc = codes(&cl); + let mut w = W { out: Vec::new(), acc: 0, n: 0 }; + w.put(1, 1); + w.put(2, 2); + w.put(258 - 257, 5); + w.put(0, 5); + w.put(18 - 4, 4); + for &s in ORDER.iter().take(18) { + w.put(cl[s] as u32, 3); + } + for &l in lit.iter().chain([dist_bits].iter()) { + w.code(cc[l as usize], cl[l as usize] as u32); + } + for &b in raw { + w.code(lc[b as usize], lit[b as usize] as u32); + } + w.code(lc[256], lit[256] as u32); + if w.n > 0 { + w.out.push(w.acc as u8); + } + w.out.extend_from_slice(&crate::png::adler32(raw).to_be_bytes()); + w.out +} + +/// `stills/`: PNG files a still's reader (§5.7, "any PNG of 8-bit RGB without interlace") +/// reads or refuses, each with its verdict and, read, the SHA-256 of its RGB8 samples. +/// They are the module's still of a 5 × 3 picture rebuilt chunk by chunk: with a +/// suggested palette, ancillary chunks, the image data split in two; and with an +/// interlace, bytes after IEND, a short row, a palette after the image data, an unknown +/// critical chunk, no IEND, a chunk between the image data, a CRC, a sixth filter, +/// another colour type, a zlib window past 32 KiB, bytes after the zlib stream, a +/// wrong Adler-32, and a dynamic block whose lone distance code is two bits long (the +/// same block with a one-bit code is read). +pub fn stills() -> (Files, Value) { + use crate::png::{adler32, chunk, deflate_fixed}; + let (w, h) = (5u32, 3u32); + let rgb: Vec = (0..3 * w * h).map(|i| (crate::fixture::noise(9, i, 0, 0) % 256) as u8).collect(); + let rows = |filter: u8, cut: usize| -> Vec { + let stride = 3 * w as usize; + let mut raw = Vec::new(); + for row in rgb.chunks_exact(stride) { + raw.push(filter); + raw.extend_from_slice(row); + } + raw.truncate(raw.len() - cut); + raw + }; + let zlib = |raw: &[u8]| [&[0x78u8, 0x01][..], &deflate_fixed(raw), &adler32(raw).to_be_bytes()].concat(); + let ihdr = |w: u32, h: u32, colour: u8, interlace: u8| [&w.to_be_bytes()[..], &h.to_be_bytes(), &[8, colour, 0, 0, interlace]].concat(); + let png = |chunks: &[(&[u8; 4], Vec)]| { + let mut out = vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]; + for (k, d) in chunks { + chunk(&mut out, k, d); + } + out + }; + let z = zlib(&rows(0, 0)); + let (z1, z2) = z.split_at(z.len() / 2); + let good_ihdr = ihdr(w, h, 2, 0); + let plte = vec![1u8, 2, 3, 4, 5, 6]; + let text = b"Comment\0a still".to_vec(); + // CINFO 8, its check bits recomputed: a stream PNG does not allow, though it inflates. + let wide = [&[0x88u8, (31 - (0x88u32 * 256) % 31) as u8 % 31][..], &z[2..]].concat(); + let after = [&z[..], &z[z.len() - 4..]].concat(); + let mut sum = z.clone(); + *sum.last_mut().expect("adler") ^= 1; + // One dynamic block of literals: a lone distance code of one bit is the one incomplete + // code zlib takes; of two bits, it is refused (RFC 1951 §3.2.7, zlib's inflate). + let lone_one = [&[0x78u8, 0x01][..], &dynamic_literals(&rows(0, 0), 1)].concat(); + let lone_two = [&[0x78u8, 0x01][..], &dynamic_literals(&rows(0, 0), 2)].concat(); + let mut bad_crc = png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", z.clone()), (b"IEND", vec![])]); + bad_crc[8 + 8 + 13] ^= 1; // the first byte of IHDR's CRC + let cases: Vec<(&str, Vec, bool)> = vec![ + ("plain", png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", z.clone()), (b"IEND", vec![])]), true), + ("palette", png(&[(b"IHDR", good_ihdr.clone()), (b"PLTE", plte.clone()), (b"IDAT", z.clone()), (b"IEND", vec![])]), true), + ("ancillary", png(&[(b"IHDR", good_ihdr.clone()), (b"tEXt", text.clone()), (b"IDAT", z.clone()), (b"tEXt", text.clone()), (b"IEND", vec![])]), true), + ("data-split", png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", z1.to_vec()), (b"IDAT", z2.to_vec()), (b"IEND", vec![])]), true), + ("module-still", crate::png::encode(&rgb, w, h), true), + ("lone-one-bit-distance", png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", lone_one), (b"IEND", vec![])]), true), + ("still-incomplete-dist", png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", lone_two), (b"IEND", vec![])]), false), + ("interlace", png(&[(b"IHDR", ihdr(w, h, 2, 1)), (b"IDAT", z.clone()), (b"IEND", vec![])]), false), + ("bytes-after-end", [png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", z.clone()), (b"IEND", vec![])]), vec![0]].concat(), false), + ("short-row", png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", zlib(&rows(0, 1))), (b"IEND", vec![])]), false), + ("palette-after-data", png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", z.clone()), (b"PLTE", plte), (b"IEND", vec![])]), false), + ("unknown-critical", png(&[(b"IHDR", good_ihdr.clone()), (b"QRST", vec![1]), (b"IDAT", z.clone()), (b"IEND", vec![])]), false), + ("no-end", png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", z.clone())]), false), + ("chunk-between-data", png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", z1.to_vec()), (b"tEXt", text), (b"IDAT", z2.to_vec()), (b"IEND", vec![])]), false), + ("crc", bad_crc, false), + ("filter-5", png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", zlib(&rows(5, 0))), (b"IEND", vec![])]), false), + ("rgba", png(&[(b"IHDR", ihdr(w, h, 6, 0)), (b"IDAT", z), (b"IEND", vec![])]), false), + ("window-past-32k", png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", wide), (b"IEND", vec![])]), false), + ("bytes-after-stream", png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", after), (b"IEND", vec![])]), false), + ("stream-checksum", png(&[(b"IHDR", good_ihdr.clone()), (b"IDAT", sum), (b"IEND", vec![])]), false), + ]; + let mut files = BTreeMap::new(); + let mut out = Vec::new(); + // Each verdict is the case's, and a read file's samples are the picture's: what a + // reader gives is checked against them, never written into them. + for (name, bytes, ok) in cases { + let mut c = json!({"file": format!("{name}.png"), "verdict": if ok { "ok" } else { "refused" }}); + if ok { + c["rgb_sha256"] = json!(crate::sha256_hex(&rgb)); + } + files.insert(format!("stills/{name}.png"), bytes); + out.push(c); + } + (files, json!({"width": w, "height": h, "cases": out})) +} + +// ─────────────────────────────── delivery ─────────────────────────────── + +/// The delivery corpus's plans (§8.9, test 13): the staged golden packed live and sent to +/// a `tv` subscriber over a 40 bytes/ms link with chunks of 1200 bytes and 3.2 s of +/// start-up: under §8.5's priorities; without them (fifo); and capped at layer 1. +pub fn delivery_plans() -> Vec<(&'static str, crate::delivery::DeliveryPlan)> { + use wai::stage_measure::sim::Schedule; + let strict = crate::delivery::DeliveryPlan { rtt_us: 0, bytes_per_ms: 40, chunk_bytes: 1200, enc_us: 0, startup_us: 3_200_000, layer_cap: None, schedule: Schedule::StrictPriority }; + vec![ + ("strict", strict.clone()), + ("fifo", crate::delivery::DeliveryPlan { schedule: Schedule::Fifo, ..strict.clone() }), + ("capped", crate::delivery::DeliveryPlan { layer_cap: Some(1), ..strict }), + ] +} + +/// The class the delivery corpus's subscriber takes. +pub const DELIVERY_CLASS: &str = "tv"; + +/// `delivery/expected.json`: each plan of [`delivery_plans`] and its simulated outcome. +pub fn delivery(dir: &Path) -> std::io::Result { + let policy = std::fs::read(dir.join("packs").join(STAGED_PACK).join("policy.toml"))?; + let src = crate::fixture::generate(STAGED_PACK).ok_or_else(|| io("no fixture"))?; + let key = SigningKey::from_bytes(&[0x5a; 32]); + let mut cases = Vec::new(); + for (name, plan) in delivery_plans() { + let d = crate::delivery::simulate_source(&policy, &src, DELIVERY_CLASS, &plan, &key).map_err(io)?; + let mut outcome = d.to_json(); + let plan_json = outcome.as_object_mut().expect("an object").remove("plan").expect("a plan"); + cases.push(json!({ + "name": name, + "plan": {"rtt_us": plan.rtt_us, "bytes_per_ms": plan.bytes_per_ms, "chunk_bytes": plan.chunk_bytes, "enc_us": plan.enc_us, + "startup_us": plan.startup_us, "layer_cap": plan.layer_cap, "fifo": plan.schedule == wai::stage_measure::sim::Schedule::Fifo}, + "sim_plan": plan_json, + "presented": d.presented(), + "outcome": outcome, + })); + } + Ok(json!({"pack": STAGED_PACK, "class": DELIVERY_CLASS, "label": "simulated", "cases": cases})) +} + +// ─────────────────────────────── live windows ─────────────────────────────── + +/// The pieces a window of the live corpus holds. +pub const LIVE_WINDOW_PIECES: u64 = 2; + +/// `live/`: the staged golden's source packed live (§9.2) with its key, in windows of +/// [`LIVE_WINDOW_PIECES`] pieces: each window's stage index and the receipt that seals it +/// (`windows//`), and `expected.json`: each window's first piece and entry, its pieces, +/// its index digest and its receipt hash. +pub fn live_windows(dir: &Path) -> std::io::Result<(Files, Value)> { + let g = dir.join("packs").join(STAGED_PACK); + let policy = std::fs::read(g.join("policy.toml"))?; + let key = read_key(&g.join("key.hex"))?; + let id = wai::signer_id::did_key(&key.verifying_key().to_bytes()); + let src = crate::fixture::generate(STAGED_PACK).ok_or_else(|| io("no fixture"))?; + let (hd, frames) = crate::source::y4m::read_all(&src).map_err(io)?; + let mut o = crate::live::LiveOpts::new(key, id); + o.window_pieces = LIVE_WINDOW_PIECES; + let mut lp = crate::live::LivePacker::new(&policy, hd.w, hd.h, o).map_err(io)?; + let comp = lp.policy().components[0].id.clone(); + let mut windows = Vec::new(); + for f in &frames { + lp.push(&f.rgb).map_err(io)?; + windows.extend(lp.take_sealed()); + } + windows.extend(lp.finish().map_err(io)?.1); + let mut files = BTreeMap::new(); + let mut out = Vec::new(); + for w in &windows { + let ix = StageIndex::parse(&w.index).map_err(io)?; + files.insert(format!("live/windows/{}/{}", w.number, wai::package::index_file(&comp)), w.index.clone()); + files.insert(format!("live/windows/{}/package.receipt.json", w.number), w.receipt.to_json().into_bytes()); + out.push(json!({ + "window": w.number, + "first_piece": w.first_piece, + "pieces": w.pieces, + "first_entry": ix.header.first_entry, + "index_digest": hex(&ix.digest()), + "receipt_hash": hex(&w.receipt.receipt_hash()), + })); + } + Ok((files, json!({"pack": STAGED_PACK, "component": comp, "window_pieces": LIVE_WINDOW_PIECES, "windows": out}))) +} + +/// The chain of held live windows, as §9.2 states it: each receipt verifies against its +/// index; the first window's parent is none and its `prev_index_digest` zero; each +/// later one's parent is the window before's receipt, its `prev_index_digest` that +/// window's index digest, and its first piece and entry the next of the chain's. +fn live_chain(held: &Files, expected: &Value, out: &mut Vec) { + let comp = expected["component"].as_str().unwrap_or(""); + let mut prev: Option<(PackageReceiptV2, StageIndex)> = None; + for w in expected["windows"].as_array().into_iter().flatten() { + let n = w["window"].as_u64().unwrap_or(u64::MAX); + let index = held.get(&format!("windows/{n}/{}", wai::package::index_file(comp))); + let receipt = held.get(&format!("windows/{n}/package.receipt.json")).and_then(|b| std::str::from_utf8(b).ok()).and_then(|t| PackageReceiptV2::from_json(t).ok()); + let (Some(index), Some(r)) = (index, receipt) else { + out.push(format!("live window {n}: its index or receipt is not held")); + return; + }; + let Ok(ix) = StageIndex::parse(index) else { + out.push(format!("live window {n}: its index does not parse")); + return; + }; + if let Err(e) = r.verify_detailed(&[index]) { + out.push(format!("live window {n}: its receipt does not verify: {e}")); + } + let ok = match &prev { + None => r.parent == Parent::None && ix.header.prev_index_digest == [0; 32] && ix.header.first_piece == 0 && ix.header.first_entry == 0, + Some((pr, pix)) => { + r.parent == Parent::LiveWindow(pr.receipt_hash()) + && ix.header.prev_index_digest == pix.digest() + && ix.header.first_piece == pix.header.first_piece + pix.pieces.len() as u64 + && ix.header.first_entry == pix.header.first_entry + pix.entries.len() as u64 + } + }; + if !ok { + out.push(format!("live window {n}: not chained to the window before")); + } + prev = Some((r, ix)); + } +} + // ─────────────────────────────── regen and check ─────────────────────────────── fn write_files(dir: &Path, files: &Files) -> std::io::Result<()> { @@ -1071,6 +1584,10 @@ fn write_json(p: &Path, v: &Value) -> std::io::Result<()> { std::fs::write(p, s) } +fn tree_names(dir: &Path) -> std::io::Result> { + Ok(read_tree(dir)?.into_keys().collect()) +} + fn read_tree(dir: &Path) -> std::io::Result { wai::pin::read_dir_set(dir).map_err(io) } @@ -1111,6 +1628,32 @@ pub fn regen(dir: &Path) -> std::io::Result<()> { } write_files(dir, &files)?; write_json(&pm.join("expected.json"), &expected)?; + let (files, expected) = stills(); + let st = dir.join("stills"); + if st.exists() { + std::fs::remove_dir_all(&st)?; + } + write_files(dir, &files)?; + write_json(&st.join("expected.json"), &expected)?; + let (files, expected) = live_windows(dir)?; + let lv = dir.join("live"); + if lv.exists() { + std::fs::remove_dir_all(&lv)?; + } + write_files(dir, &files)?; + write_json(&lv.join("expected.json"), &expected)?; + 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)?; + } + write_files(dir, &files)?; + write_json(&sm.join("expected.json"), &expected)?; Ok(()) } @@ -1184,25 +1727,70 @@ pub fn check(dir: &Path) -> std::io::Result> { if expected_held != want { out.push("catalogs/expected.json: the corpus and this implementation differ".into()); } - // The package mutations, read strictly (their dot-names and `*.sha256` files are - // cases), each refused with its reason. - let golden = read_tree(&dir.join("packs").join(MUTATED_PACK).join("package"))?; - let key = read_key(&dir.join("packs").join(MUTATED_PACK).join("key.hex"))?; - let mut held = crate::verify::read_strict(&dir.join("package-mutations")).map_err(io)?; + // The stills (§5.7): the files, and each one's verdict from this reader. + let mut held = read_tree(&dir.join("stills"))?; let expected_held: Value = serde_json::from_slice(&held.remove("expected.json").unwrap_or_default()).map_err(io)?; - let (files, want) = package_overlays(&golden, &key); - let files: Files = files.into_iter().map(|(k, v)| (k.trim_start_matches("package-mutations/").to_string(), v)).collect(); - same_tree("package-mutations", &files, &held, &mut out); + let (files, want) = stills(); + let files: Files = files.into_iter().map(|(k, v)| (k.trim_start_matches("stills/").to_string(), v)).collect(); + same_tree("stills", &files, &held, &mut out); if expected_held != want { - out.push("package-mutations/expected.json: the corpus and this implementation differ".into()); + out.push("stills/expected.json: the corpus and this implementation differ".into()); } - let strings = |v: &Value| -> Vec { v.as_array().into_iter().flatten().filter_map(|p| p.as_str().map(String::from)).collect() }; for c in want["cases"].as_array().into_iter().flatten() { - let name = c["name"].as_str().unwrap_or(""); - let files = apply_overlay(&golden, &held, name, &strings(&c["remove"])); - let v = package_case_verdict(&files, &strings(&c["empty_dirs"]), &strings(&c["unreadable_dirs"]))?; - if v != c["reason"] { - out.push(format!("package-mutations {name}: {v}, the corpus says {}", c["reason"])); + let f = c["file"].as_str().unwrap_or(""); + let got = held.get(f).map(|b| crate::png::decode(b).map(|(w, h, px)| (w, h, crate::sha256_hex(&px)))); + let ok = match (got, c["verdict"].as_str()) { + (Some(Ok((w, h, sha))), Some("ok")) => c["rgb_sha256"] == sha && want["width"] == w && want["height"] == h, + (Some(Err(_)), Some("refused")) => true, + _ => false, + }; + if !ok { + out.push(format!("stills/{f}: not read as the corpus says")); + } + } + // The live windows (§9.2): the files, and their chain. + let mut held = read_tree(&dir.join("live"))?; + let expected_held: Value = serde_json::from_slice(&held.remove("expected.json").unwrap_or_default()).map_err(io)?; + let (files, want) = live_windows(dir)?; + let files: Files = files.into_iter().map(|(k, v)| (k.trim_start_matches("live/").to_string(), v)).collect(); + same_tree("live", &files, &held, &mut out); + if expected_held != want { + out.push("live/expected.json: the corpus and this implementation differ".into()); + } + live_chain(&held, &want, &mut out); + // The delivery cases (§8.9). + let held: Value = serde_json::from_slice(&std::fs::read(dir.join("delivery/expected.json"))?).map_err(io)?; + if held != delivery(dir)? { + out.push("delivery/expected.json: the corpus and this implementation differ".into()); + } + if tree_names(&dir.join("delivery"))? != ["expected.json"] { + out.push("delivery/: a file that is no case".into()); + } + // The package mutations, read strictly (their dot-names and `*.sha256` files are + // cases), each refused with its reason; then the staged ones. + for (sub, pack, f) in [ + ("package-mutations", MUTATED_PACK, package_overlays as fn(&Files, &SigningKey) -> (Files, Value)), + ("staged-mutations", STAGED_PACK, staged_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 mut held = crate::verify::read_strict(&dir.join(sub)).map_err(io)?; + let expected_held: Value = serde_json::from_slice(&held.remove("expected.json").unwrap_or_default()).map_err(io)?; + let (files, want) = f(&golden, &key); + let prefix = format!("{sub}/"); + let files: Files = files.into_iter().map(|(k, v)| (k.trim_start_matches(prefix.as_str()).to_string(), v)).collect(); + same_tree(sub, &files, &held, &mut out); + if expected_held != want { + out.push(format!("{sub}/expected.json: the corpus and this implementation differ")); + } + let strings = |v: &Value| -> Vec { v.as_array().into_iter().flatten().filter_map(|p| p.as_str().map(String::from)).collect() }; + for c in want["cases"].as_array().into_iter().flatten() { + let name = c["name"].as_str().unwrap_or(""); + let files = apply_overlay(&golden, &held, name, &strings(&c["remove"])); + let v = package_case_verdict(&files, &strings(&c["empty_dirs"]), &strings(&c["unreadable_dirs"]))?; + if v != c["reason"] { + out.push(format!("{sub} {name}: {v}, the corpus says {}", c["reason"])); + } } } Ok(out) diff --git a/wai/wai-pack/src/lib.rs b/wai/wai-pack/src/lib.rs index f868e5bcc..dfa81609d 100644 --- a/wai/wai-pack/src/lib.rs +++ b/wai/wai-pack/src/lib.rs @@ -3,10 +3,13 @@ //! //! What this revision builds: the policy and its digest ([`policy`]), what a packager //! may emit ([`capcheck`]), the source readers ([`source`]), `wai.pack.scenecut/1` -//! ([`scenecut`]), piece classification ([`classify`]), the synthetic fixtures -//! ([`fixture`]), and the conformance corpus's regeneration and check -//! ([`conformance`]), whose encode vectors run the integer video encoder -//! (`wai::int_video_enc`); and the base encode's choice of step ([`encode`]). +//! ([`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 +//! ([`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`]). //! //! No float decides anything here: every computation is integer, and float arithmetic //! is denied for the whole crate. @@ -19,13 +22,16 @@ pub mod catalog; pub mod classify; pub mod cmaf; pub mod conformance; +pub mod delivery; pub mod encode; pub mod fixture; pub mod floor; pub mod golden; +pub mod live; pub mod objects; pub mod pack; pub mod plan; +pub mod png; pub mod policy; pub mod scenecut; pub mod source; @@ -40,6 +46,14 @@ pub fn hex(b: &[u8]) -> String { }) } +/// The bytes of an even-length hexadecimal string, either case; `None` otherwise. +pub fn unhex(s: &str) -> Option> { + if !s.len().is_multiple_of(2) || !s.bytes().all(|c| c.is_ascii_hexdigit()) { + return None; + } + (0..s.len()).step_by(2).map(|i| s.get(i..i + 2).and_then(|d| u8::from_str_radix(d, 16).ok())).collect() +} + /// SHA-256 of `b`, as lowercase hex. pub fn sha256_hex(b: &[u8]) -> String { use sha2::Digest as _; diff --git a/wai/wai-pack/src/live.rs b/wai/wai-pack/src/live.rs new file mode 100644 index 000000000..b6751aa72 --- /dev/null +++ b/wai/wai-pack/src/live.rs @@ -0,0 +1,302 @@ +//! The live packer ([packaging] §9.2, §4.3): frames pushed one at a time, each piece +//! coded as the segmenter closes it, by the coder the on-demand packer uses +//! ([`crate::pack::Coder`]), so a piece is the same objects either way, and the pieces +//! gathered into windows, each a stage index (§6.1) sealed by a version-2 receipt that +//! names the previous window's (parent kind `live-window`, §9.2). +//! +//! `wai.pack.scenecut/1` decides a frame's cut once `lookahead_frames` more have arrived +//! (§4.3), so a live and an on-demand packer under one policy cut the same pieces and +//! write the same objects. A live packer writes no floor: a supplied floor is made from +//! the whole source after the fact (§7.3), so it is the on-demand package's alone, and a +//! window lists the primary objects, its bases and refinements. +//! +//! A piece's objects are published when the frame that closes it has been pushed: its +//! base then, and layer `k` `layer_lag_ms[k]` after it (§8.6), which [`Published`] states +//! for the delivery simulator (`crate::delivery`). +//! +//! What it holds. Each object's bytes go to the caller in its [`Published`] and are not +//! kept: an open piece keeps, per object, its entry's fields (hash, prefix digest, +//! capability, lengths, layer, tier, deadline, SSE), from which its window's index is +//! built. Besides those it holds the frames from the first of the piece being cut (at +//! most the policy's longest piece plus `lookahead_frames`), each piece's first frame +//! for the plan file (eight bytes a piece, for the session), and the windows sealed and +//! not yet taken ([`LivePacker::take_sealed`]). With windows of `n` pieces the open +//! records are at most `n` pieces'; with one window over the session (`window_pieces` +//! 0) they are every piece's, sealed by [`LivePacker::finish`]. +//! +//! [packaging]: ../../../extensions/packaging.md + +use crate::pack::{CodedPiece, Coder, PackError}; +use crate::policy::{Media, Policy}; +use crate::scenecut::{SegParams, Segmenter}; +use ed25519_dalek::SigningKey; +use wai::package::{PackageReceiptV2, Parent}; +use wai::quantum_ops::GrantRef; +use wai::stage_index::Role; + +/// What a live packer seals windows with. +pub struct LiveOpts { + pub signer: SigningKey, + pub signer_id: String, + /// Pieces per window; 0 for one window over the whole session. + pub window_pieces: u64, + pub grant: GrantRef, +} + +impl LiveOpts { + /// One window, unbounded grant `wai.video.package`. + pub fn new(signer: SigningKey, signer_id: impl Into) -> LiveOpts { + LiveOpts { signer, signer_id: signer_id.into(), window_pieces: 0, grant: GrantRef::unbounded("wai.video.package") } + } +} + +/// One object published. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Published { + /// The absolute piece number. + pub piece: u64, + /// The piece's first frame and its frames. + pub start: u64, + pub duration: u64, + /// The frame whose push closed the piece: the object is published once it has + /// arrived (and, for layer `k`, `layer_lag_ms[k]` after that). + pub at_frame: u64, + pub layer: u8, + pub role: Role, + pub capability: String, + /// A refinement's deadline (§8.6); 0 for a base. + pub deadline_ms: u32, + pub hash: [u8; 32], + pub envelope: Vec, +} + +/// What [`LivePacker::finish`] gives: the objects it published last, every window, and +/// the plan (each piece's first frame). +pub type Finished = (Vec, Vec, Vec); + +/// A window: its stage index and the receipt that seals it, and the pieces it holds. +#[derive(Debug, Clone)] +pub struct LiveWindow { + pub index: Vec, + pub receipt: PackageReceiptV2, + /// Its window number from 0, its first piece and its pieces. + pub number: u64, + pub first_piece: u64, + pub pieces: u64, +} + +/// The live packer of a one-component video policy. +pub struct LivePacker { + policy: Policy, + w: u32, + h: u32, + opts: LiveOpts, + seg: Option, + /// The frames from the first of the piece being cut, the first at `held_from`. + held: Vec>, + held_from: u64, + pushed: u64, + starts: Vec, + open: Vec, + first_piece: u64, + first_entry: u64, + prev_index: [u8; 32], + prev_receipt: Option<[u8; 32]>, + /// Windows sealed and not yet taken, and how many were sealed in all. + windows: Vec, + sealed: u64, +} + +impl LivePacker { + /// A live packer of frames of `w × h` under `policy_bytes`, refusing as the on-demand + /// packer does before any frame: the policy's own refusals, another medium, a rung it + /// does not code. + pub fn new(policy_bytes: &[u8], w: u32, h: u32, opts: LiveOpts) -> Result { + let policy = Policy::parse_bytes(policy_bytes).map_err(|e| PackError::new(e.code(), e.to_string()))?; + if let Some(r) = crate::verify::policy_refusal(&policy) { + return Err(PackError::new(r.code, r.detail)); + } + let [comp] = policy.components.as_slice() else { + return Err(PackError::new("pack-media-unsupported", "this revision packs one component")); + }; + if comp.media != Media::Video { + return Err(PackError::new("pack-media-unsupported", format!("component {} is {}", comp.id, comp.media.label()))); + } + Coder::new(&policy, comp, w, h)?; + let seg = Segmenter::new(SegParams::from_policy(&policy.segmentation, comp.timebase), w, h); + Ok(LivePacker { + policy, + w, + h, + opts, + seg: Some(seg), + held: Vec::new(), + held_from: 0, + pushed: 0, + starts: Vec::new(), + open: Vec::new(), + first_piece: 0, + first_entry: 0, + prev_index: [0u8; 32], + prev_receipt: None, + windows: Vec::new(), + sealed: 0, + }) + } + + /// The policy, every default written in. + pub fn policy(&self) -> &Policy { + &self.policy + } + + fn code(&mut self, pieces: Vec) -> Result, PackError> { + let mut out = Vec::new(); + let at_frame = self.pushed.saturating_sub(1); + for piece in pieces { + let comp = &self.policy.components[0]; + let coder = Coder { policy: &self.policy, comp, w: self.w, h: self.h }; + let from = (piece.start - self.held_from) as usize; + let mut cp = coder.code(&piece, &self.held[from..from + piece.len as usize])?; + let number = self.first_piece + self.open.len() as u64; + // Each object's bytes go to the caller; the piece keeps its entry's fields. + for s in &mut cp.stages { + out.push(Published { + piece: number, + start: piece.start, + duration: piece.len, + at_frame, + layer: s.layer, + role: s.role, + capability: s.capability.clone(), + deadline_ms: s.deadline_ms, + hash: s.hash, + envelope: std::mem::take(&mut s.envelope), + }); + } + // The frames before the next piece are no longer needed. + let end = (piece.start + piece.len - self.held_from) as usize; + self.held.drain(..end); + self.held_from = piece.start + piece.len; + self.starts.push(piece.start); + self.open.push(cp); + if self.opts.window_pieces > 0 && self.open.len() as u64 == self.opts.window_pieces { + self.close_window()?; + } + } + Ok(out) + } + + /// Push the next frame (RGB8, `3·w·h` bytes); the objects of every piece it closes. + pub fn push(&mut self, rgb: &[u8]) -> Result, PackError> { + if rgb.len() as u64 != 3 * self.w as u64 * self.h as u64 { + return Err(PackError::new("y4m-size-change", "a frame of another size")); + } + self.held.push(rgb.to_vec()); + self.pushed += 1; + let pieces = self.seg.as_mut().expect("open until finish").push(rgb); + self.code(pieces) + } + + /// Seal the open pieces as a window (a no-op with none open). + pub fn close_window(&mut self) -> Result<(), PackError> { + if self.open.is_empty() { + return Ok(()); + } + let comp = &self.policy.components[0]; + let w = crate::pack::window_index(&self.policy, comp, &self.open, self.first_piece, self.first_entry, self.prev_index)?; + let bytes = w.index.to_bytes(); + let parent = match self.prev_receipt { + None => Parent::None, + Some(h) => Parent::LiveWindow(h), + }; + let receipt = PackageReceiptV2::seal( + &self.opts.signer, + &self.opts.signer_id, + &self.policy.asset_id, + self.policy.digest(), + &[(comp.id.clone(), bytes.clone())], + &crate::pack::packager_id(), + wai::energy_meter::EnergyReading::unmetered(), + self.opts.grant.clone(), + parent, + ) + .map_err(|r| PackError::new(r.code, r.detail))?; + let window = LiveWindow { index: bytes, receipt, number: self.sealed, first_piece: self.first_piece, pieces: self.open.len() as u64 }; + self.first_piece += self.open.len() as u64; + self.first_entry += w.index.entries.len() as u64; + self.prev_index = w.index.digest(); + self.prev_receipt = Some(window.receipt.receipt_hash()); + self.open.clear(); + self.sealed += 1; + self.windows.push(window); + Ok(()) + } + + /// The windows sealed since the last call, in order; the packer keeps none of them. + pub fn take_sealed(&mut self) -> Vec { + std::mem::take(&mut self.windows) + } + + /// The end of the source: the last pieces coded and published, the last window + /// sealed. Returns the published objects, the windows not yet taken (every window, + /// if none was), and the plan (each piece's first frame). + pub fn finish(mut self) -> Result { + let pieces = self.seg.take().expect("open until finish").finish(); + let out = self.code(pieces)?; + self.close_window()?; + Ok((out, self.windows, self.starts)) + } +} + +/// What a live and an on-demand pack of one source share (test 12): of `files`, the plan +/// files (`plan-.txt`) and the objects the stage indexes (`index-.wsix` at the top, +/// and `windows//index-.wsix`) list as bases and refinements. A floor object, which +/// a live pack does not write, is left out. +pub fn primary_files(files: &std::collections::BTreeMap>) -> Result>, String> { + let is_index = |p: &str| { + let name = p.strip_prefix("windows/").and_then(|r| r.split_once('/')).map_or(p, |(_, n)| n); + name.starts_with("index-") && name.ends_with(".wsix") && (p == name || p.starts_with("windows/")) + }; + let mut out = std::collections::BTreeMap::new(); + for (p, b) in files { + if p.starts_with("plan-") && p.ends_with(".txt") && !p.contains('/') { + out.insert(p.clone(), b.clone()); + } + if is_index(p) { + let ix = wai::stage_index::StageIndex::parse(b).map_err(|e| format!("{p}: {e}"))?; + for e in ix.entries.iter().filter(|e| matches!(e.role, Role::Base | Role::Refinement)) { + let o = crate::objects::object_path(&e.content_hash); + let bytes = files.get(&o).ok_or_else(|| format!("{o}: listed by {p}, not held"))?; + out.insert(o, bytes.clone()); + } + } + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// What a live packer holds (§9.2): with one window over the session, every open + /// piece's records and none of its objects' bytes, which went out in its + /// publications; and a sealed window, once taken, not at all. + #[test] + fn a_published_object_is_not_held() { + let g = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../pack-conformance/packs/video-staged-01"); + let policy = std::fs::read(g.join("policy.toml")).expect("the golden's policy"); + let (hd, frames) = crate::source::y4m::read_all(&crate::fixture::generate("video-staged-01").expect("a fixture")).expect("its frames"); + let key = SigningKey::from_bytes(&[7; 32]); + let mut lp = LivePacker::new(&policy, hd.w, hd.h, LiveOpts::new(key, "test")).expect("a live packer"); + let mut published = 0u64; + for f in &frames { + published += lp.push(&f.rgb).expect("a frame").iter().map(|p| p.envelope.len() as u64).sum::(); + } + assert!(lp.open.len() >= 4, "every piece but the last is open: {}", lp.open.len()); + assert!(published > 0); + assert!(lp.open.iter().flat_map(|cp| &cp.stages).all(|s| s.envelope.is_empty() && s.lens.byte_len > 0), "a published object's bytes are held"); + lp.close_window().expect("a window"); + assert_eq!(lp.take_sealed().len(), 1); + assert!(lp.take_sealed().is_empty() && lp.open.is_empty(), "a taken window, or its pieces, are held"); + } +} diff --git a/wai/wai-pack/src/objects.rs b/wai/wai-pack/src/objects.rs index a7226d212..5e801dd63 100644 --- a/wai/wai-pack/src/objects.rs +++ b/wai/wai-pack/src/objects.rs @@ -4,8 +4,10 @@ //! content are one object. //! //! - a base: `media` the component's class, `intent` `replicate`, the rung's capability, -//! `conditioning.kind` its short codec id, and `target` the frames, height and width; -//! - a floor: the same for its capability, with `derived_from` naming the base it +//! `conditioning.kind` its short codec id, and `target` the frames, height and width +//! (a still's frames are those it is held for, §5.7); +//! - a refinement: the manifest `wai stage refine` writes, with `refines` (§5.4); +//! - a floor: a base's manifest for its capability, with `derived_from` naming the base it //! presents (SPEC §7.1, *Derived renditions*). //! //! [packaging]: ../../../extensions/packaging.md @@ -46,6 +48,19 @@ pub fn base_envelope(media: &str, cap: &str, payload: Vec, frames: u64, h: u Wai::new(m, payload).to_bytes().expect("a manifest serialises") } +/// A refinement object of `cap` (§5.4): `refines` names the piece's base `base`, the +/// object of the layer below as the producer of every unit (`prev`, the base itself for +/// layer 1) and its `layer`. Its manifest is the one `wai stage refine` writes: `media` +/// the component's, `intent` `replicate`, the capability with no fallback, +/// `conditioning.kind` `wir1` (the payload's form), `refines`, and no `target` (a +/// layer's shape is the stage below it). +pub fn refinement_envelope(media: &str, cap: &str, payload: Vec, base: &[u8; 32], prev: &[u8; 32], layer: u8) -> Vec { + use wai::container::{Prev, Refines}; + let refines = Refines { base: crate::hex(base), prev: Prev::One(crate::hex(prev)), layer, span: None }; + let m = Manifest::new(media, "replicate", ModelRequirement::new(cap), Conditioning::new("wir1")).with_refines(&refines); + Wai::new(m, payload).to_bytes().expect("a manifest serialises") +} + /// A floor object of `cap` presenting `derived_from.motif`. pub fn floor_envelope(media: &str, cap: &str, payload: Vec, frames: u64, h: u32, w: u32, derived_from: &DerivedFrom) -> Vec { let m = Manifest::new(media, "replicate", ModelRequirement::new(cap), Conditioning::new(kind_of(cap))) diff --git a/wai/wai-pack/src/pack.rs b/wai/wai-pack/src/pack.rs index b0ec71339..9a5fe6a6c 100644 --- a/wai/wai-pack/src/pack.rs +++ b/wai/wai-pack/src/pack.rs @@ -3,12 +3,15 @@ //! signing key and the packager's identity, except the energy figure and the measurement //! claim of a metered pack. //! -//! This revision packs one video component with a `wai.video.int_motion` base (its step -//! chosen per piece by [`crate::encode::choose_q`]) and a supplied AV1 floor. What it -//! does not pack yet it refuses by name rather than leaving out: another medium -//! (`pack-media-unsupported`), refinement rungs (`pack-refinement-unsupported`), a rule's -//! ladder subset (`pack-rule-ladder-unsupported`), a floor to encode -//! (`pack-floor-encode-unsupported`), a rung of a capability it does not encode +//! This revision packs one video component, each piece by its ladder ([packaging] §5.7, +//! [`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 +//! (`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 @@ -54,7 +57,7 @@ pub struct PackError { } impl PackError { - fn new(code: impl Into, detail: impl Into) -> PackError { + pub fn new(code: impl Into, detail: impl Into) -> PackError { PackError { code: code.into(), detail: detail.into() } } } @@ -160,6 +163,22 @@ pub fn clip_digest>(frames: &[F]) -> [u8; 32] { h.finalize().into() } +/// The clip digest of one RGB8 frame held for `n` frames (§5.7): the SHA-256 of `n` +/// copies of the frame's SHA-256, fed a block of them at a time, so no frame is repeated +/// in memory. `n` is at most `2^24` (§4.4). +pub fn held_digest(frame: &[u8], n: u64) -> [u8; 32] { + use sha2::Digest as _; + const BLOCK: u64 = 1024; + let one = wai::output_tree::out_digest(frame); + let block = one.repeat(BLOCK as usize); + let mut h = sha2::Sha256::new(); + for _ in 0..n / BLOCK { + h.update(&block); + } + h.update(&block[..(n % BLOCK) as usize * one.len()]); + h.finalize().into() +} + /// An object's manifest and payload lengths. Where WAI checks its payload's format, they /// are its counted bytes (staged-measurement §3, `measure_framing::count_object`), which /// also checks the payload parses as its capability's pinned format; a floor's payload, @@ -178,28 +197,6 @@ 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() } -/// An entry describing `env`. -#[allow(clippy::too_many_arguments)] -fn entry(env: &[u8], piece: u64, capability: u16, role: Role, prefix_tier: u8, prefix_digest: [u8; 32], sse: u64, flags: u8) -> Result { - let (man, payload_len) = object_lengths(env).map_err(|e| PackError::new("pack-encode", format!("an object the packer wrote is not countable: {e}")))?; - Ok(EntryRec { - content_hash: objects::object_hash(env), - byte_len: env.len() as u64, - piece, - refines: wsix::NO_REFINES, - capability, - role, - layer: 0, - prefix_tier, - flags, - deadline_ms: 0, - prefix_digest, - sse, - manifest_len: man, - payload_len, - }) -} - /// A component's CMAF files: the initialization segment's path and bytes, and each /// piece's fragment's. pub type CmafFiles = (String, Vec, Vec<(String, Vec)>); @@ -228,6 +225,304 @@ pub fn plan_file(starts: &[u64]) -> String { starts.iter().map(|s| format!("{s}\n")).collect() } +/// The capabilities this revision encodes, by role: a base of `wai.video.int_motion` +/// (§5.5, §5.6) or `wai.image.png` (§5.7), a refinement of `wai.video.int_refine` over an +/// `int_motion` base (§5.4), and a supplied AV1 floor (§7.3). +fn encodes(rung: &Rung, base_of: impl Fn(&Rung) -> Option) -> bool { + match rung.role { + RungRole::Base => matches!(rung.capability.as_str(), CAP_INT_MOTION | CAP_PNG), + RungRole::Refinement => rung.capability == CAP_INT_REFINE && base_of(rung).is_some_and(|b| b == CAP_INT_MOTION), + RungRole::Floor => matches!(rung.capability.as_str(), "wai.video.av1" | "wai.video.av1.lossless"), + } +} + +const CAP_INT_MOTION: &str = "wai.video.int_motion"; +const CAP_INT_REFINE: &str = "wai.video.int_refine"; +const CAP_PNG: &str = "wai.image.png"; + +/// An object's lengths as its entry states them (§6.2): the whole envelope, and its +/// manifest and payload by [`object_lengths`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Lens { + pub byte_len: u64, + pub manifest_len: u32, + pub payload_len: u64, +} + +impl Lens { + /// The lengths of an object the packer wrote; one it cannot count is `pack-encode`. + pub fn of(env: &[u8]) -> Result { + let (manifest_len, payload_len) = object_lengths(env).map_err(|e| PackError::new("pack-encode", format!("an object the packer wrote is not countable: {e}")))?; + Ok(Lens { byte_len: env.len() as u64, manifest_len, payload_len }) + } +} + +/// One object of a piece, with what its entry states (§6.2). Its envelope may have been +/// handed on (a live packer writes each object as it is published and keeps only this +/// record): the entry is built from `hash` and `lens`, never from `envelope`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Stage { + pub lens: Lens, + pub envelope: Vec, + pub hash: [u8; 32], + pub capability: String, + pub role: Role, + pub layer: u8, + pub prefix_tier: u8, + pub prefix_digest: [u8; 32], + pub sse: u64, + pub deadline_ms: u32, + /// `ENTRY_DERIVED_FROM` for a floor. + pub flags: u8, +} + +/// A piece coded: where it lies, its kind and plan, and its objects in entry order. +#[derive(Debug, Clone)] +pub struct CodedPiece { + pub piece: crate::scenecut::Piece, + pub kind: PieceKind, + pub plan: crate::plan::PiecePlan, + pub stages: Vec, +} + +impl CodedPiece { + /// The bytes of each layer's object: what the class plan reads (§8.1). + pub fn layer_bytes(&self) -> Vec { + self.stages.iter().filter(|s| matches!(s.role, Role::Base | Role::Refinement)).map(|s| s.lens.byte_len).collect() + } +} + +/// What codes a video component's pieces: its policy, its frame size, and the rungs it +/// codes, checked once. The on-demand packer ([`pack`]) and the live packer +/// (`crate::live`) code every piece with it, so a piece is the same objects either way. +pub struct Coder<'a> { + pub policy: &'a Policy, + pub comp: &'a crate::policy::Component, + pub w: u32, + pub h: u32, +} + +impl<'a> Coder<'a> { + /// A coder of `comp` under `policy`, refusing a rung of a capability it does not code + /// (`pack-capability-unsupported:`, the first in ladder order) before any piece. + pub fn new(policy: &'a Policy, comp: &'a crate::policy::Component, w: u32, h: u32) -> Result, PackError> { + let by_id: BTreeMap<&str, &Rung> = policy.ladder.iter().map(|r| (r.id.as_str(), r)).collect(); + let base_of = |r: &Rung| { + let mut cur = r; + while let Some(up) = cur.refines.as_deref().and_then(|id| by_id.get(id)) { + cur = up; + } + (cur.role == RungRole::Base).then(|| cur.capability.clone()) + }; + for r in policy.ladder.iter().filter(|r| r.component == comp.id) { + if !encodes(r, base_of) { + return Err(PackError::new(format!("pack-capability-unsupported:{}", r.capability), format!("ladder.{}", r.id))); + } + if r.capability == CAP_INT_MOTION && MotionParams::of(r).is_none() { + return Err(PackError::new(format!("pack-capability-unsupported:{}", r.capability), format!("ladder.{}: its parameters", r.id))); + } + } + Ok(Coder { policy, comp, w, h }) + } + + /// Code `piece`, whose source frames are `src`: its base, then each refinement of its + /// ladder (§5.4–§5.7). Each refinement is applied by the reference sink + /// (`wai::staged::StageChain`) to the decoded base before it is kept, and must give the + /// encoder's reconstruction. + pub fn code(&self, piece: &crate::scenecut::Piece, src: &[Vec]) -> Result { + let (w, h, tb) = (self.w, self.h, self.comp.timebase); + let kind: PieceKind = classify::classify(&piece.stats, &self.policy.classify); + let pp = plan_piece(self.policy, tb, piece.start, piece.len, kind); + let ladder = crate::plan::piece_ladder(self.policy, &self.comp.id, &pp) + .ok_or_else(|| PackError::new("policy-no-base", format!("component {}", self.comp.id)))?; + let n = src.len() as u64; + let enc = |e: String| PackError::new("pack-encode", e); + // The base and the frames a sink presents from it. + let (payload, recon): (Vec, Vec>) = match ladder.base.capability.as_str() { + CAP_INT_MOTION => { + let mp = MotionParams::of(ladder.base).expect("checked by Coder::new"); + let q = choose_q(src, h, w, &mp).map_err(|e| enc(e.to_string()))?; + (q.encoded.wiv1, q.encoded.recon) + } + // A still: the piece's first frame, held for the piece (§5.7). + _ => (crate::png::encode(&src[0], w, h), vec![src[0].clone(); src.len()]), + }; + let base_env = objects::base_envelope(self.comp.media.label(), &ladder.base.capability, payload, n, h, w); + let base_hash = objects::object_hash(&base_env); + let tier = wsix::capability_prefix_tier(&ladder.base.capability); + let mut stages = vec![Stage { + hash: base_hash, + capability: ladder.base.capability.clone(), + role: Role::Base, + layer: 0, + prefix_tier: tier, + prefix_digest: if tier == wsix::TIER_DECODE { clip_digest(&recon) } else { [0u8; 32] }, + sse: crate::encode::sse(&recon, src), + deadline_ms: 0, + flags: 0, + lens: Lens::of(&base_env)?, + envelope: base_env, + }]; + let deadline = u32::try_from(pp.deadline_ms).map_err(|_| enc("a deadline past 2^32 - 1".into()))?; + let mut below = recon; + let mut chain: Option = None; + for (k, rung) in ladder.refinements.iter().enumerate() { + let layer = u8::try_from(k + 1).map_err(|_| enc("a layer past 255".into()))?; + let q = rung.param_u64("q").and_then(|q| u32::try_from(q).ok()).ok_or_else(|| enc(format!("ladder.{}: its step", rung.id)))?; + let (payload, next) = crate::encode::encode_refinement(&below, src, h, w, q).map_err(enc)?; + let prev = stages.last().expect("the base").hash; + let env = objects::refinement_envelope(self.comp.media.label(), &rung.capability, payload, &base_hash, &prev, layer); + // The sink's own application of the layer to the decoded base. + if chain.is_none() { + let (info, units) = wai::staged::decode_base(&stages[0].envelope, &BTreeMap::new(), None, None).map_err(|e| enc(e.to_string()))?; + chain = Some(wai::staged::StageChain::new(info, units, sink_config())); + } + let c = chain.as_mut().expect("set"); + let reports = c.offer(&env, 0, wai::staged::ReceiptEvidence::None).map_err(|e| enc(e.to_string()))?; + if reports.iter().any(|r| r.envelope.is_some()) || c.clip_sha256() != Some(clip_digest(&next)) { + return Err(enc(format!("piece at {}: layer {layer} does not apply to its base as the encoder reconstructs it", piece.start))); + } + let below_tier = stages.last().expect("the base").prefix_tier; + let tier = if below_tier == wsix::TIER_DECODE && wsix::capability_prefix_tier(&rung.capability) == wsix::TIER_DECODE { wsix::TIER_DECODE } else { wsix::TIER_NONE }; + stages.push(Stage { + hash: objects::object_hash(&env), + capability: rung.capability.clone(), + role: Role::Refinement, + layer, + prefix_tier: tier, + prefix_digest: if tier == wsix::TIER_DECODE { clip_digest(&next) } else { [0u8; 32] }, + sse: crate::encode::sse(&next, src), + deadline_ms: deadline, + flags: 0, + lens: Lens::of(&env)?, + envelope: env, + }); + below = next; + } + Ok(CodedPiece { piece: piece.clone(), kind, plan: pp, stages }) + } +} + +/// The sink a coder applies its layers with: no hold, no cost limit, a budget no piece of +/// this revision reaches, no live schedule. +fn sink_config() -> wai::staged::ChainConfig { + wai::staged::ChainConfig { + bound: wai::staged::HoldBound { max_envelopes: 0, max_age_ms: 0 }, + cost_limit: None, + budget: u64::MAX, + record_budget: 0, + live: None, + } +} + +/// A window's stage index (§6) over coded pieces, and its objects by content hash (those +/// whose stage still holds its envelope). +pub struct Window { + pub index: StageIndex, + pub objects: BTreeMap<[u8; 32], Vec>, +} + +/// The stage index of `pieces` (each with its floor stage appended where it has one) as +/// one window: its first piece and entry are `first_piece` and `first_entry` of the +/// chain, `prev` the previous window's index digest (zero for on-demand content and a +/// first window). The string table is the component's id, the class ids, then each +/// capability at its first use; a content hash an earlier entry of the window has is +/// flagged a duplicate. +pub fn window_index(policy: &Policy, comp: &crate::policy::Component, pieces: &[CodedPiece], first_piece: u64, first_entry: u64, prev: [u8; 32]) -> Result { + let tb = comp.timebase; + let mut strings: Vec = vec![comp.id.clone()]; + strings.extend(policy.classes.iter().map(|c| c.id.clone())); + let fixed = strings.len(); + let mut objects: BTreeMap<[u8; 32], Vec> = BTreeMap::new(); + let mut entries: Vec = Vec::new(); + let mut piece_recs: Vec = Vec::new(); + for (p, cp) in pieces.iter().enumerate() { + let number = first_piece.checked_add(p as u64).ok_or_else(|| PackError::new("pack-piece-length", "a piece number past 2^64 - 1"))?; + let first = entries.len() as u32; + for s in &cp.stages { + let cap = match strings[fixed..].iter().position(|x| *x == s.capability) { + Some(i) => (fixed + i) as u16, + None => { + strings.push(s.capability.clone()); + (strings.len() - 1) as u16 + } + }; + let mut flags = s.flags; + if entries.iter().any(|x| x.content_hash == s.hash) { + flags |= wsix::ENTRY_DUP; + } + // A refinement refines the entry of the layer below it in its piece. + let refines = if s.role == Role::Refinement { entries.len() as u32 - 1 } else { wsix::NO_REFINES }; + entries.push(EntryRec { + content_hash: s.hash, + byte_len: s.lens.byte_len, + piece: number, + refines, + capability: cap, + role: s.role, + layer: s.layer, + prefix_tier: s.prefix_tier, + flags, + deadline_ms: s.deadline_ms, + prefix_digest: s.prefix_digest, + sse: s.sse, + manifest_len: s.lens.manifest_len, + payload_len: s.lens.payload_len, + }); + if !s.envelope.is_empty() { + objects.insert(s.hash, s.envelope.clone()); + } + } + piece_recs.push(PieceRec { + start: cp.piece.start, + duration: u32::try_from(cp.piece.len).map_err(|_| PackError::new("pack-piece-length", "a piece past 2^32 - 1 units"))?, + first_entry: first, + n_entries: u16::try_from(entries.len() as u32 - first).map_err(|_| PackError::new("pack-piece-length", "a piece of more than 65535 objects"))?, + kind: cp.kind as u8, + flags: cp.piece.flags, + rules: cp.plan.rules, + moments: cp.plan.moments, + }); + } + let plan: Vec> = policy + .classes + .iter() + .map(|c| pieces.iter().map(|cp| class_plan(c, &cp.layer_bytes(), cp.plan.max_layer, cp.piece.len, tb)).collect()) + .collect(); + let classes: Vec = policy + .classes + .iter() + .enumerate() + .map(|(i, c)| ClassRec { + name: (1 + i) as u16, + max_layer: c.max_layer.min(255) as u8, + flags: if c.floor_only { wsix::CLASS_FLOOR_ONLY } else { 0 }, + max_kbps: c.max_kbps.map_or(0, |k| k.min(u32::MAX as u64) as u32), + }) + .collect(); + let starts: Vec = pieces.iter().map(|cp| cp.piece.start).collect(); + let index = StageIndex { + header: Header { + media_class: MediaClass::Video, + tb_num: tb.num, + tb_den: tb.den, + policy_digest: policy.digest(), + prev_index_digest: prev, + first_piece, + first_entry, + plan_digest: wsix::plan_digest(&starts), + component: 0, + }, + pieces: piece_recs, + entries, + classes, + plan, + strings, + }; + index.check().map_err(|r| PackError::new(r.code, format!("the packer wrote an index that does not check: {}", r.detail)))?; + Ok(Window { index, objects }) +} + /// Pack `policy_bytes` over `sources` (each component's source bytes, by id). pub fn pack(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut PackOpts) -> Result { let policy = Policy::parse_bytes(policy_bytes).map_err(|e| PackError::new(e.code(), e.to_string()))?.effective(&opts.overrides); @@ -241,25 +536,12 @@ pub fn pack(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut if comp.media != Media::Video { return Err(PackError::new("pack-media-unsupported", format!("component {} is {}", comp.id, comp.media.label()))); } - if !rungs_of(&policy, &comp.id, RungRole::Refinement).is_empty() { - return Err(PackError::new("pack-refinement-unsupported", "refinement rungs are packed by a later revision")); - } - if policy.rules.iter().any(|r| r.then.ladder.is_some()) { - return Err(PackError::new("pack-rule-ladder-unsupported", "a rule's ladder subset is packed by a later revision")); - } - let bases = rungs_of(&policy, &comp.id, RungRole::Base); - let [base] = bases.as_slice() else { - let extra = bases.get(1).map_or("", |r| r.capability.as_str()); - return Err(PackError::new(format!("pack-capability-unsupported:{extra}"), "this revision packs one base rung")); - }; - let mp = MotionParams::of(base).ok_or_else(|| PackError::new(format!("pack-capability-unsupported:{}", base.capability), "the base rung"))?; + // 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 !matches!(floor_rung.capability.as_str(), "wai.video.av1" | "wai.video.av1.lossless") { - return Err(PackError::new(format!("pack-capability-unsupported:{}", floor_rung.capability), "the floor rung")); - } if floor_rung.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")); } @@ -287,101 +569,34 @@ pub fn pack(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut if let Some(m) = opts.meter.as_mut() { m.start(); } - - // The string table: the component, the classes, then capabilities by first use. - let mut strings: Vec = vec![comp.id.clone()]; - strings.extend(policy.classes.iter().map(|c| c.id.clone())); - let mut intern = |s: &str| -> u16 { - match strings.iter().position(|x| x == s) { - Some(i) => i as u16, - None => { - strings.push(s.to_string()); - (strings.len() - 1) as u16 - } - } - }; - let base_cap = intern(&base.capability); - let floor_cap = intern(&floor_rung.capability); - let mut objects: BTreeMap<[u8; 32], Vec> = BTreeMap::new(); - let mut entries: Vec = Vec::new(); - let mut piece_recs: Vec = Vec::new(); - let mut layer_bytes: Vec<(Vec, Option)> = Vec::new(); + let coder = Coder::new(&policy, comp, w, h)?; let (fw, fh) = (fl.seq.max_frame_width, fl.seq.max_frame_height); - let mut add = |env: Vec, entries: &mut Vec, e: EntryRec| { - let mut e = e; - if entries.iter().any(|x| x.content_hash == e.content_hash) { - e.flags |= wsix::ENTRY_DUP; - } - objects.insert(e.content_hash, env); - entries.push(e); - }; + let mut coded = Vec::with_capacity(pieces.len()); for (p, piece) in pieces.iter().enumerate() { - let kind: PieceKind = classify::classify(&piece.stats, &policy.classify); - let pp = plan_piece(&policy, tb, piece.start, piece.len, kind); let src_frames: Vec> = frames[piece.start as usize..(piece.start + piece.len) as usize].iter().map(|f| f.rgb.clone()).collect(); - let q = choose_q(&src_frames, h, w, &mp).map_err(|e| PackError::new("pack-encode", e.to_string()))?; - let n = src_frames.len() as u64; - let base_env = objects::base_envelope("video", &base.capability, q.encoded.wiv1, n, h, w); - let base_hash = objects::object_hash(&base_env); - let first = entries.len() as u32; - let tier = wsix::capability_prefix_tier(&base.capability); - let digest = if tier == wsix::TIER_DECODE { clip_digest(&q.encoded.recon) } else { [0u8; 32] }; - let be = entry(&base_env, p as u64, base_cap, Role::Base, tier, digest, q.encoded.sse, 0)?; - let base_len = be.byte_len; - add(base_env, &mut entries, be); - let df = objects::supplied_derived_from(&base_hash, &base.capability, &listing); + 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 floor_env = objects::floor_envelope("video", &floor_rung.capability, payload, n, fh, fw, &df); - let fe = entry(&floor_env, p as u64, floor_cap, Role::Floor, wsix::TIER_NONE, [0u8; 32], wsix::SSE_UNKNOWN, wsix::ENTRY_DERIVED_FROM)?; - add(floor_env, &mut entries, fe); - piece_recs.push(PieceRec { - start: piece.start, - duration: u32::try_from(piece.len).map_err(|_| PackError::new("pack-piece-length", "a piece past 2^32 - 1 units"))?, - first_entry: first, - n_entries: (entries.len() as u32 - first) as u16, - kind: kind as u8, - flags: piece.flags, - rules: pp.rules, - moments: pp.moments, + 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, }); - layer_bytes.push((vec![base_len], pp.max_layer)); + coded.push(cp); } - let plan: Vec> = policy - .classes - .iter() - .map(|c| piece_recs.iter().zip(&layer_bytes).map(|(pr, (lb, rm))| class_plan(c, lb, *rm, pr.duration as u64, tb)).collect()) - .collect(); - let classes: Vec = policy - .classes - .iter() - .enumerate() - .map(|(i, c)| ClassRec { - name: (1 + i) as u16, - max_layer: c.max_layer.min(255) as u8, - flags: if c.floor_only { wsix::CLASS_FLOOR_ONLY } else { 0 }, - max_kbps: c.max_kbps.map_or(0, |k| k.min(u32::MAX as u64) as u32), - }) - .collect(); + let Window { index, objects } = window_index(&policy, comp, &coded, 0, 0, [0u8; 32])?; let policy_digest = policy.digest(); - let index = StageIndex { - header: Header { - media_class: MediaClass::Video, - tb_num: tb.num, - tb_den: tb.den, - policy_digest, - prev_index_digest: [0u8; 32], - first_piece: 0, - first_entry: 0, - plan_digest: wsix::plan_digest(&starts), - component: 0, - }, - pieces: piece_recs, - entries, - classes, - plan, - strings, - }; - index.check().map_err(|r| PackError::new(r.code, format!("the packer wrote an index that does not check: {}", r.detail)))?; 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, @@ -390,8 +605,9 @@ pub fn pack(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut 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), }; - let catalogs = catalog::render_all(&view, &policy.layer_lag_ms, policy.deadline_ms, &policy_digest).map_err(|e| PackError::new("pack-catalog", e.0))?; + let catalogs = catalog::render_all(&view, &policy, &policy_digest).map_err(|e| PackError::new("pack-catalog", e.0))?; let index_bytes = index.to_bytes(); // 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. diff --git a/wai/wai-pack/src/plan.rs b/wai/wai-pack/src/plan.rs index 65418177a..a22a402b2 100644 --- a/wai/wai-pack/src/plan.rs +++ b/wai/wai-pack/src/plan.rs @@ -17,7 +17,7 @@ //! [packaging]: ../../../extensions/packaging.md use crate::classify::PieceKind; -use crate::policy::{Class, Policy, Timebase}; +use crate::policy::{Class, Policy, Role, Rung, Timebase}; use wai::stage_index::FLOOR_ONLY; /// A span in timebase units as milliseconds, rounded down. @@ -67,6 +67,41 @@ pub fn plan_piece(p: &Policy, tb: Timebase, start: u64, duration: u64, kind: Pie out } +/// The rungs a piece is coded in (§5.7): one base, the refinements over it in layer +/// order, and the component's floors. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PieceLadder<'a> { + pub base: &'a Rung, + /// `refinements[k − 1]` codes layer `k`. + pub refinements: Vec<&'a Rung>, + pub floors: Vec<&'a Rung>, +} + +/// The ladder of a piece of component `comp` whose plan is `pp`: the rungs the matching +/// rules' `then.ladder` names, where it names rungs of `comp` (§3.4 has checked that they +/// are one base, refinements over rungs it names, and every floor of the component); +/// otherwise the component's default ladder, its first base rung in ladder order, the +/// chain of refinements over it, and its floor rungs. `None` for a component with no base +/// rung (§3.4 refuses such a policy). +pub fn piece_ladder<'a>(p: &'a Policy, comp: &str, pp: &PiecePlan) -> Option> { + let mine = |r: &&Rung| r.component == comp; + let rungs: Vec<&Rung> = match &pp.ladder { + Some(ids) if ids.iter().all(|id| p.ladder.iter().any(|r| r.id == *id && r.component == comp)) => { + p.ladder.iter().filter(mine).filter(|r| ids.contains(&r.id)).collect() + } + _ => p.ladder.iter().filter(mine).collect(), + }; + let base = *rungs.iter().find(|r| r.role == Role::Base)?; + let mut refinements = Vec::new(); + let mut cur = base; + while let Some(next) = rungs.iter().find(|r| r.role == Role::Refinement && r.refines.as_deref() == Some(cur.id.as_str())) { + refinements.push(*next); + cur = next; + } + let floors = rungs.iter().filter(|r| r.role == Role::Floor).copied().collect(); + Some(PieceLadder { base, refinements, floors }) +} + /// The layer class `c` lists for a piece: `layer_bytes[k]` is the bytes of the piece's /// layer-`k` object(s), `rule_max` the rules' `max_layer`, and the piece lasts `duration` /// units of `tb`. diff --git a/wai/wai-pack/src/png.rs b/wai/wai-pack/src/png.rs new file mode 100644 index 000000000..82c6f6338 --- /dev/null +++ b/wai/wai-pack/src/png.rs @@ -0,0 +1,624 @@ +//! A piece's still ([packaging] §5.7): one RGB8 frame as a `wai.image.png` payload, every +//! byte a pure function of the frame, so an independent implementation writes the same +//! file, and the reader a verifier decodes any such payload with. +//! +//! The writer's choices: +//! +//! - the signature, then `IHDR` (width, height, bit depth 8, colour type 2, compression 0, +//! filter 0, interlace 0), one `IDAT` holding the whole compressed stream, and `IEND`; +//! each chunk a big-endian length, its type, its data and the CRC-32 of type and data; +//! - every row filtered `Sub` (filter byte 1): each byte less the byte three before it in +//! the row, modulo 256, the first pixel's bytes less 0; +//! - a zlib stream (RFC 1950): `0x78 0x01`, one final deflate block of the fixed Huffman +//! codes (RFC 1951 §3.2.6), and the Adler-32 of the filtered rows, big-endian; +//! - the block's symbols, from position 0 of the filtered rows: where at least three bytes +//! remain and an earlier position starts with the same three bytes, the latest such +//! position `j` within 32768 bytes is the match; its length is the bytes the two runs +//! share from there, at most 258, and it is written as that length and the distance to +//! `j`; otherwise one literal. Every position a symbol covers that has three bytes left +//! becomes the latest position of its three bytes. The block ends with symbol 256, and +//! the last byte is padded with zero bits. +//! +//! The reader takes any PNG of 8-bit RGB without interlace (PNG's chunk rules): every +//! chunk's type four letters and its CRC checked; `IHDR` first, of 8-bit RGB, compression +//! and filter method 0 and no interlace; an optional `PLTE` (a suggested palette, which an +//! RGB picture may carry) before the image data, read past; the `IDAT`s consecutive; +//! ancillary chunks read past; any other critical chunk refused; `IEND` last, empty, and +//! nothing after it. The zlib stream is inflated by the module's own inflater, written +//! to the zlib library's rules ([`inflate_zlib`]: no over-subscribed code, and no +//! incomplete one but a lone one-bit literal/length or distance code), with its Adler-32 +//! checked and no byte of the image data after it; its rows are exactly `1 + 3·width` +//! bytes each, and the five filters are undone. +//! +//! [packaging]: ../../../extensions/packaging.md + +use std::collections::BTreeMap; + +const SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]; + +/// The CRC-32 of ISO 3309 (PNG's chunk check), reflected, polynomial `0xEDB88320`. +pub fn crc32(parts: &[&[u8]]) -> u32 { + let mut table = [0u32; 256]; + for (n, t) in table.iter_mut().enumerate() { + let mut c = n as u32; + for _ in 0..8 { + c = if c & 1 != 0 { 0xedb8_8320 ^ (c >> 1) } else { c >> 1 }; + } + *t = c; + } + let mut c = 0xffff_ffffu32; + for p in parts { + for &b in *p { + c = table[((c ^ b as u32) & 0xff) as usize] ^ (c >> 8); + } + } + c ^ 0xffff_ffff +} + +/// Adler-32 (RFC 1950). +pub fn adler32(b: &[u8]) -> u32 { + let (mut a, mut s) = (1u32, 0u32); + for &x in b { + a = (a + x as u32) % 65521; + s = (s + a) % 65521; + } + (s << 16) | a +} + +/// Bits written least significant first, as deflate packs them. +struct Bits { + out: Vec, + acc: u64, + n: u32, +} + +impl Bits { + fn put(&mut self, v: u32, n: u32) { + self.acc |= (v as u64) << self.n; + self.n += n; + while self.n >= 8 { + self.out.push(self.acc as u8); + self.acc >>= 8; + self.n -= 8; + } + } + + /// A Huffman code, most significant bit first. + fn code(&mut self, code: u32, len: u32) { + let mut rev = 0u32; + for i in 0..len { + rev |= ((code >> i) & 1) << (len - 1 - i); + } + self.put(rev, len); + } + + fn finish(mut self) -> Vec { + if self.n > 0 { + self.out.push(self.acc as u8); + } + self.out + } +} + +/// The fixed literal/length code of symbol `s` (RFC 1951 §3.2.6). +fn lit_code(b: &mut Bits, s: u32) { + match s { + 0..=143 => b.code(0x30 + s, 8), + 144..=255 => b.code(0x190 + s - 144, 9), + 256..=279 => b.code(s - 256, 7), + _ => b.code(0xc0 + s - 280, 8), + } +} + +/// RFC 1951 §3.2.5: the length codes' base lengths and extra bits. +const LEN_BASE: [u32; 29] = [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258]; +const LEN_EXTRA: [u32; 29] = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0]; +const DIST_BASE: [u32; 30] = [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577]; +const DIST_EXTRA: [u32; 30] = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13]; + +fn put_match(b: &mut Bits, len: u32, dist: u32) { + let li = LEN_BASE.iter().rposition(|&x| x <= len).expect("a length of at least 3"); + lit_code(b, 257 + li as u32); + b.put(len - LEN_BASE[li], LEN_EXTRA[li]); + let di = DIST_BASE.iter().rposition(|&x| x <= dist).expect("a distance of at least 1"); + b.code(di as u32, 5); + b.put(dist - DIST_BASE[di], DIST_EXTRA[di]); +} + +fn key(d: &[u8], i: usize) -> u32 { + (d[i] as u32) << 16 | (d[i + 1] as u32) << 8 | d[i + 2] as u32 +} + +/// The module's deflate: one final block of fixed codes, the matches as the module states. +pub fn deflate_fixed(d: &[u8]) -> Vec { + let mut b = Bits { out: Vec::with_capacity(d.len() / 2 + 16), acc: 0, n: 0 }; + b.put(1, 1); // BFINAL + b.put(1, 2); // BTYPE 01, fixed codes + let mut last: BTreeMap = BTreeMap::new(); + let n = d.len(); + let mut i = 0usize; + while i < n { + let found = if i + 3 <= n { last.get(&key(d, i)).copied().filter(|&j| i - j <= 32768) } else { None }; + let len = match found { + Some(j) => { + let max = (n - i).min(258); + let mut l = 0; + while l < max && d[j + l] == d[i + l] { + l += 1; + } + put_match(&mut b, l as u32, (i - j) as u32); + l + } + None => { + lit_code(&mut b, d[i] as u32); + 1 + } + }; + for p in i..i + len { + if p + 3 <= n { + last.insert(key(d, p), p); + } + } + i += len; + } + lit_code(&mut b, 256); + b.finish() +} + +/// One chunk: its length, type, data and CRC. +pub fn chunk(out: &mut Vec, kind: &[u8; 4], data: &[u8]) { + out.extend_from_slice(&(data.len() as u32).to_be_bytes()); + out.extend_from_slice(kind); + out.extend_from_slice(data); + out.extend_from_slice(&crc32(&[kind, data]).to_be_bytes()); +} + +/// The rows of an RGB8 frame of `w × h`, each filtered `Sub`. +fn filtered(rgb: &[u8], w: u32, h: u32) -> Vec { + let stride = 3 * w as usize; + let mut out = Vec::with_capacity((stride + 1) * h as usize); + for row in rgb.chunks_exact(stride).take(h as usize) { + out.push(1); + for i in 0..stride { + out.push(row[i].wrapping_sub(if i >= 3 { row[i - 3] } else { 0 })); + } + } + out +} + +/// The still of an RGB8 frame of `w × h` (`rgb` holds `3·w·h` bytes). +pub fn encode(rgb: &[u8], w: u32, h: u32) -> Vec { + let raw = filtered(rgb, w, h); + let mut z = vec![0x78, 0x01]; + z.extend_from_slice(&deflate_fixed(&raw)); + z.extend_from_slice(&adler32(&raw).to_be_bytes()); + let mut ihdr = Vec::with_capacity(13); + ihdr.extend_from_slice(&w.to_be_bytes()); + ihdr.extend_from_slice(&h.to_be_bytes()); + ihdr.extend_from_slice(&[8, 2, 0, 0, 0]); + let mut out = SIGNATURE.to_vec(); + chunk(&mut out, b"IHDR", &ihdr); + chunk(&mut out, b"IDAT", &z); + chunk(&mut out, b"IEND", &[]); + out +} + +fn paeth(a: u8, b: u8, c: u8) -> u8 { + let p = a as i16 + b as i16 - c as i16; + let (pa, pb, pc) = ((p - a as i16).abs(), (p - b as i16).abs(), (p - c as i16).abs()); + if pa <= pb && pa <= pc { + a + } else if pb <= pc { + b + } else { + c + } +} + +/// The largest picture the reader decodes: the integer payloads' cap on one unit +/// (`2^26` samples). +pub const MAX_SAMPLES: u64 = 1 << 26; + +/// Decode a PNG of 8-bit RGB without interlace to its width, height and RGB8 samples. +pub fn decode(b: &[u8]) -> Result<(u32, u32, Vec), String> { + if b.len() < 8 || b[..8] != SIGNATURE { + return Err("not a PNG signature".into()); + } + let mut at = 8usize; + let mut ihdr: Option<(u32, u32)> = None; + let mut z = Vec::new(); + let mut ended = false; + let mut palette = false; + // 0: no IDAT yet; 1: in the IDAT run; 2: past it. + let mut idat = 0u8; + while at < b.len() { + if ended { + return Err("bytes after IEND".into()); + } + let len = u32::from_be_bytes(b.get(at..at + 4).ok_or("a chunk length past the end")?.try_into().expect("4")) as usize; + let kind: [u8; 4] = b.get(at + 4..at + 8).ok_or("a chunk type past the end")?.try_into().expect("4"); + let data = b.get(at + 8..at + 8 + len).ok_or("a chunk past the end")?; + let crc = u32::from_be_bytes(b.get(at + 8 + len..at + 12 + len).ok_or("a chunk CRC past the end")?.try_into().expect("4")); + if !kind.iter().all(u8::is_ascii_alphabetic) { + return Err("a chunk type that is not four letters".into()); + } + if crc != crc32(&[&kind, data]) { + return Err(format!("{}: its CRC", String::from_utf8_lossy(&kind))); + } + if idat == 1 && &kind != b"IDAT" { + idat = 2; + } + match &kind { + b"IHDR" => { + if ihdr.is_some() || at != 8 || data.len() != 13 { + return Err("IHDR is not one 13-byte chunk first".into()); + } + let (w, h) = (u32::from_be_bytes(data[0..4].try_into().expect("4")), u32::from_be_bytes(data[4..8].try_into().expect("4"))); + if data[8..] != [8, 2, 0, 0, 0] { + return Err("only 8-bit RGB, deflate, adaptive filtering, no interlace".into()); + } + if w == 0 || h == 0 || (w as u64) * (h as u64) * 3 > MAX_SAMPLES { + return Err("a size outside 1 to 2^26 samples".into()); + } + ihdr = Some((w, h)); + } + _ if ihdr.is_none() => return Err("a chunk before IHDR".into()), + b"PLTE" => { + if palette || idat != 0 || data.is_empty() || data.len() % 3 != 0 || data.len() > 768 { + return Err("PLTE: more than one, after the image data, or not 1 to 256 entries".into()); + } + palette = true; + } + b"IDAT" => { + if idat == 2 { + return Err("IDAT chunks that are not consecutive".into()); + } + idat = 1; + z.extend_from_slice(data); + } + b"IEND" => { + if !data.is_empty() { + return Err("IEND with data".into()); + } + ended = true; + } + _ if kind[0] & 0x20 == 0 => return Err(format!("an unknown critical chunk {}", String::from_utf8_lossy(&kind))), + _ => {} + } + at += 12 + len; + } + let (w, h) = ihdr.ok_or("no IHDR")?; + if !ended { + return Err("no IEND".into()); + } + let stride = 3 * w as usize; + let want = (stride + 1) * h as usize; + // The zlib stream: a 32 KiB window at most (PNG's CINFO ≤ 7), its Adler-32 checked, + // every byte of the image data in it, and exactly the rows the picture has. + let raw = inflate_zlib(&z, want)?; + if raw.len() != want { + return Err("the rows are not the picture's size".into()); + } + let mut out = vec![0u8; stride * h as usize]; + for y in 0..h as usize { + let f = raw[y * (stride + 1)]; + let row = &raw[y * (stride + 1) + 1..(y + 1) * (stride + 1)]; + for i in 0..stride { + let a = if i >= 3 { out[y * stride + i - 3] } else { 0 }; + let up = if y > 0 { out[(y - 1) * stride + i] } else { 0 }; + let c = if y > 0 && i >= 3 { out[(y - 1) * stride + i - 3] } else { 0 }; + let p = match f { + 0 => 0, + 1 => a, + 2 => up, + 3 => ((a as u16 + up as u16) / 2) as u8, + 4 => paeth(a, up, c), + _ => return Err(format!("row {y}: filter {f}")), + }; + out[y * stride + i] = row[i].wrapping_add(p); + } + } + Ok((w, h, out)) +} + +// ─────────────────────────────── inflate ─────────────────────────────── +// +// The reader's inflater, written from RFC 1950 and RFC 1951 with the rules of the zlib +// library's inflate (the one a PNG reader is built on, and the one the Python verifier +// calls), so the two accept the same streams: +// +// - the header: its check bits, method 8, a window of at most 32 KiB, no preset +// dictionary; the Adler-32 at the end, big-endian; nothing after it; +// - a stored block's length and its one's complement agree; +// - a dynamic block has at most 286 literal/length and 30 distance codes; its +// code-length code is complete; a repeat has a length before it and stays within +// the code lengths; the end-of-block symbol has a code; +// - the literal/length and distance codes are not over-subscribed, and are complete +// unless their longest code is one bit long (a single one-bit code, or none at all): +// the only incomplete code sets zlib takes; +// - a code with no symbol (in an incomplete set, or symbols 286 and 287 and distances +// 30 and 31 of the fixed codes) is refused where it is read, and so is a distance +// past the bytes written. + +struct BitReader<'a> { + b: &'a [u8], + at: usize, + bit: u32, +} + +impl BitReader<'_> { + fn bit(&mut self) -> Result { + let byte = *self.b.get(self.at).ok_or("the zlib stream ends early")?; + let v = (byte >> self.bit) & 1; + self.bit += 1; + if self.bit == 8 { + self.bit = 0; + self.at += 1; + } + Ok(v as u32) + } + + /// `n` bits, least significant first. + fn bits(&mut self, n: u32) -> Result { + let mut v = 0; + for i in 0..n { + v |= self.bit()? << i; + } + Ok(v) + } + + fn align(&mut self) { + if self.bit != 0 { + self.bit = 0; + self.at += 1; + } + } +} + +/// A canonical Huffman code (RFC 1951 §3.2.2): how many codes of each length, and the +/// symbols in code order. +struct Code { + count: [u16; 16], + symbol: Vec, +} + +/// The code of `lengths`, and how far from complete it is: 0 complete, above 0 +/// incomplete, below 0 over-subscribed; and its longest length. +fn code_of(lengths: &[u8]) -> (Code, i32, usize) { + let mut count = [0u16; 16]; + for &l in lengths { + count[l as usize] += 1; + } + let mut left: i32 = 1; + let mut longest = 0; + for (len, &c) in count.iter().enumerate().skip(1) { + left = (left << 1) - c as i32; + if c > 0 { + longest = len; + } + if left < 0 { + break; + } + } + let mut symbol = Vec::with_capacity(lengths.len()); + for len in 1..16 { + for (s, &l) in lengths.iter().enumerate() { + if l as usize == len { + symbol.push(s as u16); + } + } + } + count[0] = 0; + (Code { count, symbol }, left, longest) +} + +fn decode_symbol(r: &mut BitReader, c: &Code) -> Result { + let (mut code, mut first, mut index) = (0i32, 0i32, 0i32); + for len in 1..16 { + code |= r.bit()? as i32; + let n = c.count[len] as i32; + if code - n < first { + return Ok(c.symbol[(index + code - first) as usize]); + } + index += n; + first = (first + n) << 1; + code <<= 1; + } + Err("the zlib stream: a code with no symbol".into()) +} + +/// The literal/length and distance codes of a dynamic block, under zlib's rules. +fn dynamic_codes(r: &mut BitReader) -> Result<(Code, Code), String> { + const ORDER: [usize; 19] = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; + let nlen = r.bits(5)? as usize + 257; + let ndist = r.bits(5)? as usize + 1; + let ncode = r.bits(4)? as usize + 4; + if nlen > 286 || ndist > 30 { + return Err("the zlib stream: too many length or distance symbols".into()); + } + let mut cl = [0u8; 19]; + for &i in ORDER.iter().take(ncode) { + cl[i] = r.bits(3)? as u8; + } + let (clc, left, _) = code_of(&cl); + if left != 0 { + return Err("the zlib stream: a code-length code that is not complete".into()); + } + let mut lengths = vec![0u8; nlen + ndist]; + let mut have = 0; + while have < nlen + ndist { + let s = decode_symbol(r, &clc)?; + let (value, times) = match s { + 0..=15 => (s as u8, 1), + 16 => { + let prev = *lengths[..have].last().ok_or("the zlib stream: a repeat with no length before it")?; + (prev, 3 + r.bits(2)? as usize) + } + 17 => (0, 3 + r.bits(3)? as usize), + _ => (0, 11 + r.bits(7)? as usize), + }; + if have + times > nlen + ndist { + return Err("the zlib stream: a repeat past the code lengths".into()); + } + lengths[have..have + times].fill(value); + have += times; + } + if lengths[256] == 0 { + return Err("the zlib stream: no code for the end of the block".into()); + } + let (lit, left, longest) = code_of(&lengths[..nlen]); + if left < 0 || (left > 0 && longest > 1) { + return Err("the zlib stream: an over-subscribed or incomplete literal/length code".into()); + } + let (dist, left, longest) = code_of(&lengths[nlen..]); + if left < 0 || (left > 0 && longest > 1) { + return Err("the zlib stream: an over-subscribed or incomplete distance code".into()); + } + Ok((lit, dist)) +} + +/// The zlib stream `z` inflated, refused as zlib's inflate refuses it, and refused +/// when it would give more than `limit` bytes or when bytes follow its Adler-32. +pub fn inflate_zlib(z: &[u8], limit: usize) -> Result, String> { + let (cmf, flg) = match z { + [c, f, ..] => (*c as u32, *f as u32), + _ => return Err("the zlib stream: no header".into()), + }; + if (cmf * 256 + flg) % 31 != 0 || cmf & 15 != 8 || cmf >> 4 > 7 || flg & 0x20 != 0 { + return Err("the zlib stream: its header (check bits, method 8, a window of at most 32 KiB, no dictionary)".into()); + } + let mut r = BitReader { b: z, at: 2, bit: 0 }; + let mut out: Vec = Vec::new(); + let too_long = || "the zlib stream gives more than the picture's rows".to_string(); + loop { + let last = r.bit()?; + match r.bits(2)? { + 0 => { + r.align(); + let n = z.get(r.at..r.at + 4).ok_or("the zlib stream ends early")?; + let (len, nlen) = (u16::from_le_bytes([n[0], n[1]]), u16::from_le_bytes([n[2], n[3]])); + if len != !nlen { + return Err("the zlib stream: a stored block's length and its complement disagree".into()); + } + let data = z.get(r.at + 4..r.at + 4 + len as usize).ok_or("the zlib stream ends early")?; + if out.len() + data.len() > limit { + return Err(too_long()); + } + out.extend_from_slice(data); + r.at += 4 + len as usize; + } + t @ (1 | 2) => { + let (lit, dist) = if t == 1 { + let mut l = [8u8; 288]; + l[144..256].fill(9); + l[256..280].fill(7); + (code_of(&l).0, code_of(&[5u8; 32]).0) + } else { + dynamic_codes(&mut r)? + }; + loop { + let s = decode_symbol(&mut r, &lit)? as usize; + if s < 256 { + if out.len() == limit { + return Err(too_long()); + } + out.push(s as u8); + continue; + } + if s == 256 { + break; + } + let li = s - 257; + if li >= 29 { + return Err("the zlib stream: a length symbol past 285".into()); + } + let len = (LEN_BASE[li] + r.bits(LEN_EXTRA[li])?) as usize; + let di = decode_symbol(&mut r, &dist)? as usize; + if di >= 30 { + return Err("the zlib stream: a distance symbol past 29".into()); + } + let d = (DIST_BASE[di] + r.bits(DIST_EXTRA[di])?) as usize; + if d > out.len() { + return Err("the zlib stream: a distance past the bytes written".into()); + } + if out.len() + len > limit { + return Err(too_long()); + } + for _ in 0..len { + out.push(out[out.len() - d]); + } + } + } + _ => return Err("the zlib stream: block type 3".into()), + } + if last == 1 { + break; + } + } + r.align(); + let sum = z.get(r.at..r.at + 4).ok_or("the zlib stream ends before its Adler-32")?; + if u32::from_be_bytes(sum.try_into().expect("4")) != adler32(&out) { + return Err("the zlib stream: its Adler-32".into()); + } + if r.at + 4 != z.len() { + return Err("bytes follow the zlib stream".into()); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn frame(w: u32, h: u32, f: impl Fn(u32, u32, u32) -> u8) -> Vec { + (0..h).flat_map(|y| (0..w).flat_map(move |x| (0..3).map(move |c| (x, y, c)))).map(|(x, y, c)| f(x, y, c)).collect() + } + + /// Known answers computed independently (RFC 1950's and ISO 3309's examples). + #[test] + fn checksums() { + assert_eq!(crc32(&[b"IEND"]), 0xae42_6082); + assert_eq!(crc32(&[b"123456789"]), 0xcbf4_3926); + assert_eq!(adler32(b"Wikipedia"), 0x11e6_0398); + } + + /// The writer's output is a PNG another inflater reads back to the frame, for + /// pictures that compress well and pictures that do not. + #[test] + fn a_still_round_trips() { + for (w, h) in [(1, 1), (3, 2), (64, 36), (33, 17)] { + for f in [ + frame(w, h, |x, y, c| ((x * 7 + y * 3 + c * 50) % 256) as u8), + frame(w, h, |_, _, _| 77), + frame(w, h, |x, y, c| (crate::fixture::noise(5, x, y, c) % 256) as u8), + ] { + let png = encode(&f, w, h); + assert_eq!(decode(&png).unwrap(), (w, h, f.clone()), "{w}x{h}"); + let raw = miniz_oxide::inflate::decompress_to_vec_zlib(&png[8 + 25 + 8..png.len() - 12 - 4]).unwrap(); + assert_eq!(raw.len(), (3 * w as usize + 1) * h as usize); + } + } + } + + /// A flat picture compresses to a fraction of its samples; matches reach back across + /// rows and run to 258 bytes. + #[test] + fn matches_compress() { + let f = frame(64, 36, |_, _, _| 9); + assert!(encode(&f, 64, 36).len() < 300, "{}", encode(&f, 64, 36).len()); + } + + #[test] + fn the_reader_refuses_what_it_does_not_read() { + let f = frame(4, 3, |x, y, c| (x + y + c) as u8); + let good = encode(&f, 4, 3); + let mut bad = good.clone(); + bad[30] ^= 1; + assert!(decode(&bad).is_err(), "a CRC"); + let mut bad = good.clone(); + bad.push(0); + assert!(decode(&bad).is_err(), "bytes after IEND"); + assert!(decode(&good[..good.len() - 12]).is_err(), "no IEND"); + assert!(decode(b"not a png").is_err()); + } +} diff --git a/wai/wai-pack/src/policy.rs b/wai/wai-pack/src/policy.rs index f12d52c10..56946cc6e 100644 --- a/wai/wai-pack/src/policy.rs +++ b/wai/wai-pack/src/policy.rs @@ -53,6 +53,13 @@ pub const MAX_RUNGS: usize = 64; pub const MAX_CLASSES: usize = 32; pub const MAX_RULES: usize = 32; pub const MAX_MOMENTS: usize = 32; +/// The longest piece a policy may allow ([packaging] §4.4): `max_ms` at most ten minutes, +/// and at most `2^24` units on every component's timebase. A verifier's work for one +/// piece grows with its frames, so this bounds it. +/// +/// [packaging]: ../../../extensions/packaging.md +pub const MAX_PIECE_MS: u64 = 600_000; +pub const MAX_PIECE_UNITS: u64 = 1 << 24; pub const MAX_DEADLINES: usize = 4; /// Why a policy was refused. [`PolicyReason::code`] is the name the conformance corpus @@ -99,7 +106,8 @@ pub enum PolicyReason { Lead, /// `segmentation.closed` is false: every piece must be closed (§4.1). OpenGop, - /// Not `1 ≤ min_ms ≤ target_ms ≤ max_ms`. + /// Not `1 ≤ min_ms ≤ target_ms ≤ max_ms ≤ 600000`, or `max_ms` past `2^24` units + /// on a component's timebase. PieceBounds, /// A rung names a component the policy does not define. LadderComponent, @@ -936,6 +944,9 @@ impl Policy { if !(1 <= s.min_ms && s.min_ms <= s.target_ms && s.target_ms <= s.max_ms) { return e(P::PieceBounds, "segmentation"); } + if s.max_ms > MAX_PIECE_MS || self.components.iter().any(|c| c.timebase.units_from_ms(s.max_ms) > MAX_PIECE_UNITS) { + return e(P::PieceBounds, "segmentation.max_ms: past ten minutes, or past 2^24 units on a component's timebase"); + } let comps: BTreeSet<&str> = self.components.iter().map(|c| c.id.as_str()).collect(); if let Some(r) = self.ladder.iter().find(|r| !comps.contains(r.component.as_str())) { return e(P::LadderComponent, &format!("ladder.{}", r.id)); @@ -985,10 +996,17 @@ impl Policy { for r in &self.rules { if let Some(ids) = &r.then.ladder { let rungs: Option> = ids.iter().map(|id| by_id.get(id.as_str()).copied()).collect(); + // One component; each rung once; exactly one base; every refinement + // 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 ok = rungs.is_some_and(|rs| { !rs.is_empty() && rs.iter().all(|x| x.component == rs[0].component) - && rs.iter().any(|x| x.role == Role::Base) + && first_duplicate(ids.iter().map(String::as_str)).is_none() + && rs.iter().filter(|x| x.role == Role::Base).count() == 1 + && 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)) }); if !ok { return e(P::RuleLadder, &format!("rule.{}", r.id)); diff --git a/wai/wai-pack/src/verify.rs b/wai/wai-pack/src/verify.rs index 037d71dd5..2e3b12a71 100644 --- a/wai/wai-pack/src/verify.rs +++ b/wai/wai-pack/src/verify.rs @@ -24,7 +24,10 @@ //! verifies against them (§9.1's reasons, the index rules among them). //! 6. Per component: the index against the policy (`index-policy-mismatch`); the plan //! file (`plan-mismatch`); every object (`object-missing`, the object rules, -//! `object-lengths`); every decode-equivalent prefix (`prefix-digest`). Then, only when +//! `object-lengths`); each floor's unit count, before any frame is decoded +//! (`floor-object-mismatch`); every base and refinement in entry order, decoded or +//! applied (`prefix-digest`) and its manifest §5.2's or §5.4's (`object-manifest`); +//! every decode-equivalent prefix (`prefix-digest`). Then, only when //! all of that holds, the floor (`floor-object-mismatch`, `floor-motif-mismatch`, //! `floor-av1-malformed`, `floor-keyframe-mismatch`, `floor-cmaf-mismatch`) and the //! catalogs (`catalog-mismatch`). @@ -273,30 +276,35 @@ pub fn index_against_policy(policy: &Policy, comp: &Component, ix: &StageIndex) if !classes_ok { return bad("the class records are not the policy's classes".into()); } - // Each entry's role and capability are a rung's of its component. - let layers = policy.layers(); - for (k, e) in ix.entries.iter().enumerate() { - let want = match e.role { - Role::Base => Some(RungRole::Base), - Role::Refinement => Some(RungRole::Refinement), - Role::Floor => Some(RungRole::Floor), - _ => None, - }; - let cap = ix.capability(e); - let found = want.is_some_and(|w| { - policy.ladder.iter().any(|r| r.component == comp.id && r.role == w && r.capability == cap && layers.get(r.id.as_str()).copied() == Some(e.layer as u64)) - }); - if !found { - return bad(format!("entry {k}: {cap} as {:?} at layer {} is no rung of the component", e.role, e.layer)); - } + // Each piece: a kind a video piece has; its rules and moments (§8.1); its entries the + // rungs of its ladder (§5.7), a base, each refinement in layer order, each floor, with + // the plan's deadline on every refinement (§8.6); its class plan (§8.1). + // §4.4: no piece lasts longer than the policy's max_ms. Checked first, so a piece + // whose duration is past it is refused before anything is decoded for it (a still is + // held for its duration, §5.7). + let max = tb.units_from_ms(policy.segmentation.max_ms); + if let Some(p) = ix.pieces.iter().position(|pc| pc.duration as u64 > max) { + return bad(format!("piece {p}: {} units, past the policy's max_ms ({max} units, §4.4)", ix.pieces[p].duration)); } - // Each piece's rules, moments and class plan (§8.1). 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")) }; 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)); + } + 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)")); + } + if ix.piece_entries(p).iter().any(|e| e.role == Role::Refinement && e.deadline_ms as u64 != pp.deadline_ms) { + return bad(format!("piece {p}: a refinement's deadline is not its plan's (§8.6)")); + } let mut layer_bytes: Vec = Vec::new(); for e in ix.piece_entries(p).iter().filter(|e| matches!(e.role, Role::Base | Role::Refinement)) { if layer_bytes.len() <= e.layer as usize { @@ -355,18 +363,16 @@ fn verify_component( continue; } objects.insert(e.content_hash, b.clone()); - if e.prefix_tier == wsix::TIER_DECODE && e.role == Role::Base { - match decode_frames(ix.capability(e), b) { - Ok((frames, w, hgt)) => { - dims.get_or_insert((w, hgt)); - if crate::pack::clip_digest(&frames) != e.prefix_digest { - errs.push(reason("prefix-digest", format!("entry {k}: the decode is not its prefix digest"))); - } - } - Err(why) => errs.push(reason("prefix-digest", format!("entry {k}: {why}"))), - } - } else if e.prefix_tier == wsix::TIER_DECODE { - errs.push(reason("prefix-digest", format!("entry {k}: a refinement's prefix is checked by a later revision"))); + } + if errs.is_empty() + && let Some(r) = floor_unit_counts(policy, comp, ix, &objects) + { + return Err(vec![r]); + } + if errs.is_empty() { + match prefix_digests(comp.media.label(), ix, &objects) { + Ok(d) => dims = d, + Err(mut v) => errs.append(&mut v), } } // The floor and the catalogs are read only from objects that hold. @@ -384,19 +390,128 @@ 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 }; - catalog::render_all(&view, &policy.layer_lag_ms, policy.deadline_ms, policy_digest).map_err(|e| vec![reason("catalog-mismatch", format!("no catalog can be written: {}", e.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) }; + catalog::render_all(&view, policy, policy_digest).map_err(|e| vec![reason("catalog-mismatch", format!("no catalog can be written: {}", e.0))]) } -/// A base object decoded to its frames and dimensions. -fn decode_frames(cap: &str, env: &[u8]) -> Result<(Vec>, u32, u32), String> { +/// A base object decoded to the clip digest of the frames a sink presents for a piece of +/// `n` frames, and their dimensions: an integer payload's own frames, or a still's one +/// frame held for the piece (§5.7), whose digest is its frame's fed `n` times, so no +/// frame is repeated in memory. +fn decode_digest(cap: &str, env: &[u8], n: u32) -> Result<([u8; 32], u32, u32), String> { let w = wai::container::Wai::from_bytes(env).map_err(|e| e.to_string())?; + if cap == "wai.image.png" { + let (width, height, rgb) = crate::png::decode(&w.payload)?; + return Ok((crate::pack::held_digest(&rgb, n as u64), width, height)); + } match wai::int_payload::decode_int(cap, &w.payload, &wai::int_payload::IntParams::none()).map_err(|e| e.to_string())? { - wai::int_payload::IntDecoded::Video { frames, width, height } => Ok((frames, width, height)), + wai::int_payload::IntDecoded::Video { frames, width, height } => Ok((crate::pack::clip_digest(&frames), width, height)), _ => Err(format!("{cap} does not decode to frames here")), } } +/// An object's manifest bytes: the `ml` bytes after the magic and the manifest length. +fn manifest_bytes(env: &[u8]) -> Option<&[u8]> { + let ml = u32::from_le_bytes(env.get(4..8)?.try_into().ok()?) as usize; + env.get(8..8 + ml) +} + +/// Every base and refinement, in entry order (§11.3), then every decode-equivalent prefix +/// against its `prefix_digest` (§6.5). A base decodes by its capability (a still is §5.7's +/// PNG, 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 frame size (`object-manifest`). A +/// refinement's manifest is §5.4's (`object-manifest`), and it applies, in layer order, +/// to every frame of the stage below it by the reference sink (`wai::staged`) under the +/// reference tools' cost limit and chain budget, or it is `prefix-digest`. A base that +/// does not decode, or a layer that does not apply, ends the pass. Returns the frame size +/// of the first base. +fn prefix_digests(media: &str, ix: &StageIndex, objects: &BTreeMap<[u8; 32], Vec>) -> Result, Vec> { + let mut errs = Vec::new(); + let mut dims = None; + // Each entry's digest, for the comparisons after the pass. + let mut digests: Vec<(usize, [u8; 32])> = Vec::new(); + for p in 0..ix.pieces.len() { + let first = ix.pieces[p].first_entry as usize; + let es = ix.piece_entries(p); + let Some(bi) = es.iter().position(|e| e.role == Role::Base) else { continue }; + let base = &es[bi]; + let bytes = &objects[&base.content_hash]; + let at = |k: usize| format!("entry {}", first + k); + let cap = ix.capability(base); + let (digest, w, h) = match decode_digest(cap, bytes, ix.pieces[p].duration) { + Ok(d) => d, + Err(why) => { + errs.push(reason("prefix-digest", format!("{}: the base does not decode: {why}", at(bi)))); + return Err(errs); + } + }; + dims.get_or_insert((w, h)); + let want = crate::objects::base_envelope(media, cap, Vec::new(), ix.pieces[p].duration as u64, h, w); + if manifest_bytes(bytes) != manifest_bytes(&want) { + errs.push(reason("object-manifest", format!("{}: the base's manifest is not §5.2's", at(bi)))); + } + digests.push((first + bi, digest)); + let layers: Vec<(usize, &wsix::EntryRec)> = es.iter().enumerate().filter(|(_, e)| e.role == Role::Refinement).collect(); + if layers.is_empty() { + continue; + } + let limit = wai::staged::TOOL_DEFAULT_LIMIT; + let mut chain = match wai::staged::decode_base(bytes, &BTreeMap::new(), Some(&limit), None) { + Ok((info, units)) => wai::staged::StageChain::new(info, units, wai::staged::ChainConfig { + bound: wai::staged::HoldBound { max_envelopes: 0, max_age_ms: 0 }, + cost_limit: Some(wai::staged::layer_limit(&limit)), + budget: wai::staged::TOOL_DEFAULT_CHAIN_BUDGET, + record_budget: 0, + live: None, + }), + Err(e) => { + errs.push(reason("prefix-digest", format!("{}: the base its layers refine does not decode for the sink: {e}", at(bi)))); + return Err(errs); + } + }; + for (k, e) in layers { + let below = ix.entries.get(e.refines as usize).map(|x| x.content_hash).unwrap_or_default(); + let want = crate::objects::refinement_envelope(media, ix.capability(e), Vec::new(), &base.content_hash, &below, e.layer); + let obj = &objects[&e.content_hash]; + if manifest_bytes(obj) != manifest_bytes(&want) { + errs.push(reason("object-manifest", format!("{}: the refinement's manifest is not §5.4's", at(k)))); + } + let applied = chain.offer(obj, 0, wai::staged::ReceiptEvidence::None); + let ok = applied.as_ref().is_ok_and(|r| r.iter().all(|x| x.envelope.is_none() && x.applied.len() as u32 == chain.n_units())); + if !ok { + errs.push(reason("prefix-digest", format!("{}: the layer does not apply to every frame of the stage below it", at(k)))); + return Err(errs); + } + digests.push((first + k, chain.clip_sha256().unwrap_or_default())); + } + } + for (k, digest) in digests { + let e = &ix.entries[k]; + if e.prefix_tier == wsix::TIER_DECODE && digest != e.prefix_digest { + errs.push(reason("prefix-digest", format!("entry {k}: the prefix does not decode to its prefix digest"))); + } + } + 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 +/// [`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))); + } + } + None +} + type FloorFiles = (Option, Vec<(String, Vec)>); /// The floor of a component as its objects give it (§7.2, §7.3, §7.6), and the CMAF files diff --git a/wai/wai-pack/tests/refusals.rs b/wai/wai-pack/tests/refusals.rs index f7c68978b..275891504 100644 --- a/wai/wai-pack/tests/refusals.rs +++ b/wai/wai-pack/tests/refusals.rs @@ -97,7 +97,8 @@ fn every_floor_reason() { } /// §11.4: what this revision does not pack yet is refused by name, every -/// `pack-capability-unsupported` naming its capability. +/// `pack-capability-unsupported` naming its capability; what it packs now (refinement +/// rungs, a rule's ladder subset, a still) packs. #[test] fn what_this_revision_does_not_pack() { let fl = floor_of(golden_floor()); @@ -108,26 +109,34 @@ fn what_this_revision_does_not_pack() { run(&|p: String| p.replace("[segmentation]", "[[component]]\nid = \"a\"\nmedia = \"audio\"\n\n[component.timebase]\nnum = 48000\nden = 1\n\n[[ladder]]\nid = \"ab\"\ncomponent = \"a\"\ncapability = \"wai.audio.flac\"\nrole = \"base\"\n\n[segmentation]")), "pack-media-unsupported" ); - // A refinement rung. - assert_eq!( - run(&|p: String| p.replace("[floor]", "[[ladder]]\nid = \"r1\"\ncomponent = \"v\"\ncapability = \"wai.video.int_refine\"\nrole = \"refinement\"\nrefines = \"b0\"\nparams = { q = 6 }\n\n[live]\nlayer_lag_ms = [0, 200]\n\n[floor]")), - "pack-refinement-unsupported" - ); - // A rule's ladder subset. - assert_eq!( - run(&|p: String| format!("{p}\n[[rule]]\nid = \"s\"\nwhen = {{ piece_kind = [\"static\"] }}\nthen = {{ ladder = [\"b0\", \"f0\"] }}\n")), - "pack-rule-ladder-unsupported" - ); - // A floor to encode. - assert_eq!(run(&|p: String| p.replace("params = { source = \"supplied\" }", "params = { source = \"encode\" }")), "pack-floor-encode-unsupported"); - // A base of another capability; two bases; a floor of another capability. + // A refinement rung, and a rule's ladder subset, pack. + let refinement = |cap: &str, over: &str| { + let (cap, over) = (cap.to_string(), over.to_string()); + move |p: String| p.replace("[floor]", &format!("[[ladder]]\nid = \"r1\"\ncomponent = \"v\"\ncapability = \"{cap}\"\nrole = \"refinement\"\nrefines = \"{over}\"\nparams = {{ q = 6 }}\n\n[live]\nlayer_lag_ms = [0, 200]\n\n[floor]")) + }; + assert_eq!(run(&refinement("wai.video.int_refine", "b0")), "ok"); + assert_eq!(run(&|p: String| format!("{p}\n[[rule]]\nid = \"s\"\nwhen = {{ piece_kind = [\"static\"] }}\nthen = {{ ladder = [\"b0\", \"f0\"] }}\n")), "ok"); + // A still as the base, alone or as a second base for a rule to choose: it packs. assert_eq!( run(&|p: String| p.replace("capability = \"wai.video.int_motion\"\nrole = \"base\"\nparams = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 }", "capability = \"wai.image.png\"\nrole = \"base\"")), - "pack-capability-unsupported:wai.image.png" + "ok" + ); + assert_eq!(run(&|p: String| p.replace("[floor]", "[[ladder]]\nid = \"b1\"\ncomponent = \"v\"\ncapability = \"wai.image.png\"\nrole = \"base\"\n\n[floor]")), "ok"); + // A floor to encode. + assert_eq!(run(&|p: String| p.replace("params = { source = \"supplied\" }", "params = { source = \"encode\" }")), "pack-floor-encode-unsupported"); + // A base of a capability it does not code; a refinement of one, or over a base it does + // not refine; a floor of another capability. + assert_eq!( + run(&|p: String| p.replace("[floor]", "[[ladder]]\nid = \"b1\"\ncomponent = \"v\"\ncapability = \"wai.video.av1\"\nrole = \"base\"\n\n[floor]")), + "pack-capability-unsupported:wai.video.av1" ); assert_eq!( - run(&|p: String| p.replace("[floor]", "[[ladder]]\nid = \"b1\"\ncomponent = \"v\"\ncapability = \"wai.image.png\"\nrole = \"base\"\n\n[floor]")), - "pack-capability-unsupported:wai.image.png" + run(&|p: String| p.replace("[floor]", "[[ladder]]\nid = \"r1\"\ncomponent = \"v\"\ncapability = \"wai.image.int_refine\"\nrole = \"refinement\"\nrefines = \"b0\"\n\n[live]\nlayer_lag_ms = [0, 200]\n\n[floor]")), + "pack-capability-unsupported:wai.image.int_refine" + ); + assert_eq!( + run(&|p: String| refinement("wai.video.int_refine", "b1")(p.replace("[floor]", "[[ladder]]\nid = \"b1\"\ncomponent = \"v\"\ncapability = \"wai.image.png\"\nrole = \"base\"\n\n[floor]"))), + "pack-capability-unsupported:wai.video.int_refine" ); assert_eq!( run(&|p: String| p.replace("capability = \"wai.video.av1\"\nrole = \"floor\"\nparams = { source = \"supplied\" }", "capability = \"wai.image.png\"\nrole = \"floor\"")), diff --git a/wai/wai-pack/tests/staged.rs b/wai/wai-pack/tests/staged.rs new file mode 100644 index 000000000..0f73afd9e --- /dev/null +++ b/wai/wai-pack/tests/staged.rs @@ -0,0 +1,434 @@ +//! Staged packages, end to end, over the golden `video-staged-01` of +//! `wai/pack-conformance`: byte-identical packages with refinements and a still (test 5), +//! deduplication of a repeated piece and its layers (test 7), every prefix decoded by the +//! reference sink, the still over HTTP and MoQ, live packing equal to on-demand packing +//! (test 12), and delivery over a simulated link (test 13). + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use wai::stage_index::{Role, StageIndex}; +use wai_pack::golden; + +const PACK: &str = "video-staged-01"; + +fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../pack-conformance") +} + +fn held(p: &Path) -> BTreeMap> { + wai::pin::read_dir_set(p).unwrap() +} + +fn golden_package() -> BTreeMap> { + held(&corpus().join("packs").join(PACK).join("package")) +} + +fn policy() -> Vec { + std::fs::read(corpus().join("packs").join(PACK).join("policy.toml")).unwrap() +} + +fn key() -> ed25519_dalek::SigningKey { + let t = std::fs::read_to_string(corpus().join("packs").join(PACK).join("key.hex")).unwrap(); + let b: Vec = (0..32).map(|i| u8::from_str_radix(&t.trim()[2 * i..2 * i + 2], 16).unwrap()).collect(); + ed25519_dalek::SigningKey::from_bytes(&b.try_into().unwrap()) +} + +fn index(files: &BTreeMap>) -> StageIndex { + StageIndex::parse(&files["index-v.wsix"]).unwrap() +} + +/// Test 5: the staged golden's inputs pack to the committed package byte for byte, and +/// again to the same bytes; the package verifies; its pieces carry two refinement layers, +/// a still, and layers on the event's deadline. +#[test] +fn the_staged_golden_is_byte_identical() { + let a = golden::pack_golden(&corpus(), PACK).unwrap(); + let want = golden_package(); + assert_eq!(a.files.keys().collect::>(), want.keys().collect::>()); + for (p, b) in &want { + assert!(a.files[p] == *b, "{p} differs from the committed golden"); + } + let b = golden::pack_golden(&corpus(), PACK).unwrap(); + assert_eq!(a.files, b.files, "a second pack writes the same bytes"); + wai_pack::verify::verify_files(&want).unwrap(); + let ix = index(&want); + let shape: Vec> = (0..ix.pieces.len()).map(|p| ix.piece_entries(p).iter().map(|e| (e.role, e.layer, ix.capability(e))).collect()).collect(); + let (b0, r, still, f) = ("wai.video.int_motion", "wai.video.int_refine", "wai.image.png", "wai.video.av1"); + let staged = vec![(Role::Base, 0, b0), (Role::Refinement, 1, r), (Role::Refinement, 2, r), (Role::Floor, 0, f)]; + assert_eq!(shape, vec![staged.clone(), staged.clone(), vec![(Role::Base, 0, still), (Role::Floor, 0, f)], staged.clone(), staged]); + let deadlines: Vec = ix.entries.iter().filter(|e| e.role == Role::Refinement).map(|e| e.deadline_ms).collect(); + assert_eq!(deadlines, vec![1500, 1500, 1500, 1500, 1500, 1500, 800, 800], "the event's moment puts the last piece's layers on 800 ms"); +} + +/// Test 7: the bumper played twice is one base and one object per layer, so the package +/// stores fewer bytes than it references; the counts are the index's. +#[test] +fn a_repeated_piece_is_one_object_with_its_layers() { + let pkg = golden_package(); + let ix = index(&pkg); + let hashes = |p: usize| ix.piece_entries(p).iter().map(|e| e.content_hash).collect::>(); + assert_eq!(hashes(1), hashes(3), "the bumper's base, layers and floor are the same objects"); + assert!(ix.piece_entries(3).iter().all(|e| e.flags & wai::stage_index::ENTRY_DUP != 0)); + let r = wai::package::PackageReceiptV2::from_json(std::str::from_utf8(&pkg["package.receipt.json"]).unwrap()).unwrap(); + let mut first: BTreeMap<[u8; 32], u64> = BTreeMap::new(); + for e in &ix.entries { + first.entry(e.content_hash).or_insert(e.byte_len); + } + assert_eq!((r.unique_objects, r.stored_bytes), (first.len() as u64, first.values().sum::())); + assert_eq!(r.total_entries - r.unique_objects, 4, "the second bumper's four objects"); +} + +/// Every decode-equivalent prefix decodes to its digest through the reference sink: each +/// base by its capability, each layer applied by `wai::staged::StageChain`; the still's +/// prefix is its frame held for the piece, and every layer brings its piece closer to the +/// source (its `sse` falls). +#[test] +fn every_prefix_decodes_to_its_digest() { + let pkg = golden_package(); + let ix = index(&pkg); + let obj = |e: &wai::stage_index::EntryRec| pkg[&wai_pack::objects::object_path(&e.content_hash)].clone(); + for p in 0..ix.pieces.len() { + let es = ix.piece_entries(p); + if ix.capability(&es[0]) == "wai.image.png" { + let w = wai::container::Wai::from_bytes(&obj(&es[0])).unwrap(); + let (_, _, rgb) = wai_pack::png::decode(&w.payload).unwrap(); + let held = vec![rgb; ix.pieces[p].duration as usize]; + assert_eq!(wai_pack::pack::clip_digest(&held), es[0].prefix_digest, "piece {p}: the still held for its piece"); + assert_eq!(wai_pack::pack::held_digest(&held[0], held.len() as u64), es[0].prefix_digest, "and its digest fed n times"); + assert_eq!(es[0].sse, 0, "a still shot is its first frame"); + continue; + } + let (info, units) = wai::staged::decode_base(&obj(&es[0]), &BTreeMap::new(), None, None).unwrap(); + let mut c = wai::staged::StageChain::new(info, units, wai::staged::ChainConfig { + bound: wai::staged::HoldBound { max_envelopes: 0, max_age_ms: 0 }, + cost_limit: None, + budget: u64::MAX, + record_budget: 0, + live: None, + }); + assert_eq!(c.clip_sha256(), Some(es[0].prefix_digest)); + let mut sse = es[0].sse; + for e in es.iter().filter(|e| e.role == Role::Refinement) { + let r = c.offer(&obj(e), 0, wai::staged::ReceiptEvidence::None).unwrap(); + assert!(r.iter().all(|x| x.envelope.is_none() && x.applied.len() as u32 == c.n_units()), "piece {p} layer {}", e.layer); + assert_eq!(c.clip_sha256(), Some(e.prefix_digest), "piece {p} layer {}", e.layer); + assert!(e.sse < sse, "piece {p} layer {}: {} after {sse}", e.layer, e.sse); + sse = e.sse; + } + } +} + +/// §8.3, §8.4: the still rides the MoQ base track, and HTTP, whose variants cannot mix a +/// still with motion bases (http-delivery §4.2), plays the floor for its piece; the stage +/// view still lists it. The MoQ catalog is a stage set moq-streaming-format §4.1 accepts, +/// a receipts track per media track. +#[test] +fn a_still_rides_moq_and_the_floor_over_http() { + let pkg = golden_package(); + let ix = index(&pkg); + let still = (0..ix.pieces.len()).find(|&p| ix.capability(&ix.piece_entries(p)[0]) == "wai.image.png").unwrap(); + let media = String::from_utf8(pkg["hls/tv/wai-v.m3u8"].clone()).unwrap(); + let read = wai::http_binding::hls::parse_media_playlist(&media).unwrap(); + let gaps: Vec = read.segments.iter().enumerate().filter(|(_, s)| s.extra.gap).map(|(i, _)| i).collect(); + assert_eq!(gaps, vec![still], "tv plays the floor for the still's piece only"); + let master = String::from_utf8(pkg["hls/tv/wai.m3u8"].clone()).unwrap(); + assert!(master.contains("CODECS=\"wai1.video.int_motion+video.int_refine\""), "{master}"); + assert!(!master.contains("image.png")); + let view: serde_json::Value = serde_json::from_slice(&pkg["stages/tv.json"]).unwrap(); + assert_eq!(view["components"]["v"]["pieces"][still]["stages"].as_array().unwrap().len(), 1, "the stage view lists the still"); + let cat: serde_json::Value = serde_json::from_slice(&pkg["moq/catalog.json"]).unwrap(); + let l0 = cat["tracks"].as_array().unwrap().iter().find(|t| t["name"] == "v/l0").unwrap(); + assert_eq!(l0["wai_capabilities"], serde_json::json!(["wai.video.int_motion", "wai.image.png"])); + let verdicts = wai::staged::moq::check_catalog_in(&cat, Some(PACK)); + assert_eq!(verdicts, vec![("v".to_string(), Ok(()))]); + let media_tracks = cat["tracks"].as_array().unwrap().iter().filter(|t| t["packaging"] == "wai").count(); + let receipts = cat["tracks"].as_array().unwrap().iter().filter(|t| t["packaging"] == "wai-receipts").count(); + assert_eq!((media_tracks, receipts), (5, 5), "l0, l1@800, l1, l2@800, l2, each with its receipts track"); +} + +/// The frames of a fixture, pushed one at a time into a live packer. +fn live(policy: &[u8], fixture: &str, window_pieces: u64) -> (Vec, Vec, Vec) { + let (hd, frames) = wai_pack::source::y4m::read_all(&wai_pack::fixture::generate(fixture).unwrap()).unwrap(); + let k = key(); + let id = wai::signer_id::did_key(&k.verifying_key().to_bytes()); + let mut o = wai_pack::live::LiveOpts::new(k, id); + o.window_pieces = window_pieces; + let mut lp = wai_pack::live::LivePacker::new(policy, hd.w, hd.h, o).unwrap(); + let mut out = Vec::new(); + for f in &frames { + out.extend(lp.push(&f.rgb).unwrap()); + } + let (rest, windows, starts) = lp.finish().unwrap(); + out.extend(rest); + (out, windows, starts) +} + +fn listing_of(files: &BTreeMap>) -> Vec { + let view: BTreeMap = files.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); + wai::pin::listing_from(&view).unwrap() +} + +/// Test 12: a live pack of the staged golden's source, a frame at a time, writes the +/// on-demand package's plan and every one of its bases and refinements, byte for byte +/// (the primary listing; a live pack has no floor), in windows of two pieces that each +/// verify against their receipt and chain to the window before. +#[test] +fn live_is_on_demand() { + let (published, windows, starts) = live(&policy(), PACK, 2); + let mut files: BTreeMap> = published.iter().map(|p| (wai_pack::objects::object_path(&p.hash), p.envelope.clone())).collect(); + files.insert("plan-v.txt".into(), wai_pack::pack::plan_file(&starts).into_bytes()); + let vod = wai_pack::live::primary_files(&golden_package()).unwrap(); + assert_eq!(listing_of(&files), listing_of(&vod), "the primary listing"); + assert_eq!(windows.len(), 3, "five pieces in windows of two"); + let mut prev: Option<(&wai_pack::live::LiveWindow, StageIndex)> = None; + let mut piece = 0; + for w in &windows { + let ix = StageIndex::parse(&w.index).unwrap(); + ix.check().unwrap(); + w.receipt.verify_detailed(&[&w.index]).unwrap(); + assert_eq!(ix.header.first_piece, piece); + match &prev { + None => { + assert_eq!(w.receipt.parent, wai::package::Parent::None); + assert_eq!(ix.header.prev_index_digest, [0; 32]); + } + Some((pw, pix)) => { + assert_eq!(w.receipt.parent, wai::package::Parent::LiveWindow(pw.receipt.receipt_hash())); + assert_eq!(ix.header.prev_index_digest, pix.digest()); + assert_eq!(ix.header.first_entry, pix.header.first_entry + pix.entries.len() as u64); + } + } + piece += ix.pieces.len() as u64; + prev = Some((w, ix)); + } + // A piece is published once the frame that decides its end has arrived: the next + // piece's start plus the lookahead (6 frames), or the source's last frame. + let at: Vec<(u64, u64)> = published.iter().filter(|p| p.layer == 0).map(|p| (p.piece, p.at_frame)).collect(); + assert_eq!(at, vec![(0, 56), (1, 81), (2, 121), (3, 146), (4, 199)]); +} + +/// Test 12 over the command line: `wai-pack live` fed the source on standard input +/// writes the on-demand package's primary listing. +#[test] +fn live_over_a_pipe_is_on_demand() { + use std::io::Write as _; + let dir = std::env::temp_dir().join(format!("wai-pack-live-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let g = corpus().join("packs").join(PACK); + let mut child = std::process::Command::new(env!("CARGO_BIN_EXE_wai-pack")) + .args(["live", "--policy", g.join("policy.toml").to_str().unwrap(), "--out", dir.to_str().unwrap(), "--key", g.join("key.hex").to_str().unwrap()]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::null()) + .spawn() + .unwrap(); + child.stdin.take().unwrap().write_all(&wai_pack::fixture::generate(PACK).unwrap()).unwrap(); + assert!(child.wait().unwrap().success()); + let listing = |d: &Path| std::process::Command::new(env!("CARGO_BIN_EXE_wai-pack")).args(["listing", d.to_str().unwrap(), "--primary"]).output().unwrap().stdout; + let got = listing(&dir); + assert!(!got.is_empty()); + assert_eq!(got, listing(&g.join("package"))); + let _ = std::fs::remove_dir_all(&dir); +} + +/// The staged fixture as a Y4M stream: its bytes, the header's length, and each frame's +/// length (its `FRAME` line and samples). +fn y4m_stream() -> (Vec, usize, usize) { + let y = wai_pack::fixture::generate(PACK).unwrap(); + let (_, frames) = wai_pack::source::y4m::read_all(&y).unwrap(); + let header = y.iter().position(|&b| b == b'\n').unwrap() + 1; + assert_eq!((y.len() - header) % frames.len(), 0); + let frame = (y.len() - header) / frames.len(); + (y, header, frame) +} + +fn live_child(dir: &Path, window: &str) -> std::process::Child { + let g = corpus().join("packs").join(PACK); + std::process::Command::new(env!("CARGO_BIN_EXE_wai-pack")) + .args(["live", "--policy", g.join("policy.toml").to_str().unwrap(), "--out", dir.to_str().unwrap(), "--key", g.join("key.hex").to_str().unwrap(), "--window", window]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap() +} + +/// The objects a window's index lists as bases and refinements, by path. +fn listed_objects(files: &BTreeMap>, window: u64) -> Vec { + let ix = StageIndex::parse(&files[&format!("windows/{window}/index-v.wsix")]).unwrap(); + ix.entries.iter().filter(|e| matches!(e.role, Role::Base | Role::Refinement)).map(|e| wai_pack::objects::object_path(&e.content_hash)).collect() +} + +/// Every name below `dir`, hidden ones too: a temporary file left behind shows here. +fn every_name(dir: &Path) -> Vec { + let mut out = Vec::new(); + for e in std::fs::read_dir(dir).unwrap() { + let e = e.unwrap(); + let name = e.file_name().into_string().unwrap(); + if e.file_type().unwrap().is_dir() { + out.extend(every_name(&e.path()).into_iter().map(|n| format!("{name}/{n}"))); + } else { + out.push(name); + } + } + out +} + +/// `wai-pack live` writes as it goes (packaging §9.2): with the stream still open, each +/// window sealed so far is on disk with its receipt and every object it lists, and +/// nothing the end of the stream writes is; at the end, no file written early has +/// changed, and LISTING lists every file. +#[test] +fn live_writes_each_window_as_it_is_sealed() { + use std::io::Write as _; + let dir = std::env::temp_dir().join(format!("wai-pack-live-early-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let (y, header, frame) = y4m_stream(); + let mut child = live_child(&dir, "2"); + let mut stdin = child.stdin.take().unwrap(); + // Window 1 holds pieces 2 and 3; piece 3 closes when frame 146 arrives (test 12). + let upto = header + 151 * frame; + stdin.write_all(&y[..upto]).unwrap(); + stdin.flush().unwrap(); + // Wait for the packer with the stream open. The bound only keeps a broken packer from + // hanging the test; nothing is asserted about how long it takes. + let receipt1 = dir.join("windows/1/package.receipt.json"); + let mut polls = 0; + while !receipt1.exists() { + assert!(child.try_wait().unwrap().is_none(), "the packer ended with the stream open"); + assert!(polls < 6000, "window 1 is not written while the stream is open"); + std::thread::sleep(std::time::Duration::from_millis(100)); + polls += 1; + } + let early = held(&dir); + for n in 0..2u64 { + let ix = &early[&format!("windows/{n}/index-v.wsix")]; + let r = wai::package::PackageReceiptV2::from_json(std::str::from_utf8(&early[&format!("windows/{n}/package.receipt.json")]).unwrap()).unwrap(); + r.verify_detailed(&[ix]).unwrap(); + for o in listed_objects(&early, n) { + assert!(early.contains_key(&o), "window {n}: {o} not written with it"); + } + } + assert!(!early.keys().any(|p| p.starts_with("windows/2/") || p == "LISTING" || p.starts_with("plan-")), "{:?}", early.keys().collect::>()); + stdin.write_all(&y[upto..]).unwrap(); + drop(stdin); + let out = child.wait_with_output().unwrap(); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + let done = held(&dir); + for (p, b) in &early { + assert_eq!(done.get(p), Some(b), "{p} changed after it was written"); + } + let mut rest = done.clone(); + let listing = rest.remove("LISTING").unwrap(); + assert_eq!(listing, listing_of(&rest), "LISTING lists every file"); + assert!(done.contains_key("windows/2/package.receipt.json") && done.contains_key("plan-v.txt")); + assert!(every_name(&dir).iter().all(|n| !n.split('/').any(|s| s.starts_with('.'))), "a temporary file was left: {:?}", every_name(&dir)); + let _ = std::fs::remove_dir_all(&dir); +} + +/// A stream cut short inside a frame: `wai-pack live` exits 1 and says what it kept. The +/// window sealed before the cut stays, with its receipt and every object it lists; the +/// objects of the piece no window sealed (the still, published at frame 121) are +/// removed; no plan file or LISTING is written. +#[test] +fn a_truncated_stream_keeps_its_sealed_windows() { + use std::io::Write as _; + let dir = std::env::temp_dir().join(format!("wai-pack-live-cut-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let (y, header, frame) = y4m_stream(); + let mut child = live_child(&dir, "2"); + let mut stdin = child.stdin.take().unwrap(); + stdin.write_all(&y[..header + 126 * frame + frame / 2]).unwrap(); + drop(stdin); + let out = child.wait_with_output().unwrap(); + let err = String::from_utf8_lossy(&out.stderr); + assert_eq!(out.status.code(), Some(1), "{err}"); + assert!(err.contains("kept windows 0 to 0 (pieces 0 to 1)"), "{err}"); + let kept = held(&dir); + let r = wai::package::PackageReceiptV2::from_json(std::str::from_utf8(&kept["windows/0/package.receipt.json"]).unwrap()).unwrap(); + r.verify_detailed(&[&kept["windows/0/index-v.wsix"]]).unwrap(); + let mut want: Vec = listed_objects(&kept, 0); + want.extend(["windows/0/index-v.wsix".to_string(), "windows/0/package.receipt.json".to_string()]); + want.sort(); + want.dedup(); + assert_eq!(kept.keys().cloned().collect::>(), want, "the sealed window and its objects, and nothing else"); + let removed: usize = err.split("removed ").nth(1).and_then(|s| s.split(' ').next()).and_then(|n| n.parse().ok()).unwrap(); + assert!(removed > 0, "the still's objects were written, then removed: {err}"); + let _ = std::fs::remove_dir_all(&dir); +} + +/// Test 12's counter-case: with one frame less of lookahead than the on-demand cut, the +/// live packer cuts `cuts-01`'s flash, so the two plans part. +#[test] +fn a_shorter_lookahead_cuts_the_flash() { + let p = std::fs::read(corpus().join("scenecut/policy.toml")).unwrap(); + let (_, _, six) = live(&p, "cuts-01", 0); + let five = String::from_utf8(p.clone()).unwrap().replace("lookahead_frames = 6", "lookahead_frames = 5"); + let (_, _, short) = live(five.as_bytes(), "cuts-01", 0); + let want: Vec = serde_json::from_slice::(&std::fs::read(corpus().join("scenecut/expected.json")).unwrap()).unwrap()["fixtures"] + .as_array() + .unwrap() + .iter() + .find(|f| f["name"] == "cuts-01") + .unwrap()["pieces"] + .as_array() + .unwrap() + .iter() + .map(|p| p["start"].as_u64().unwrap()) + .collect(); + assert_eq!(six, want, "the live cut is the on-demand one"); + assert!(short.contains(&90), "the flash (frames 90 to 95) cuts at L = 5: {short:?}"); + assert_ne!(short, six); +} + +/// Test 13: the staged golden packed live and delivered to a `tv` subscriber over a +/// simulated 40 bytes/ms link, as `delivery/expected.json` states it: every base arrives +/// by its piece's instant under §8.5's priorities, and refinements are dropped on their +/// tracks' timeouts; every stall is within staged-measurement §10.4's bound; every object +/// delivered is accepted by its group receipt's Merkle proof, and a dropped one is held +/// by inclusion alone. Without §8.5's priorities (fifo), the second piece loses its layer; +/// capped at layer 1, no layer-2 object is sent. +#[test] +fn delivery_over_a_simulated_link() { + let expected: serde_json::Value = serde_json::from_slice(&std::fs::read(corpus().join("delivery/expected.json")).unwrap()).unwrap(); + let src = wai_pack::fixture::generate(PACK).unwrap(); + let k = ed25519_dalek::SigningKey::from_bytes(&[0x5a; 32]); + let mut seen = Vec::new(); + for (name, plan) in golden::delivery_plans() { + let d = wai_pack::delivery::simulate_source(&policy(), &src, golden::DELIVERY_CLASS, &plan, &k).unwrap(); + let case = expected["cases"].as_array().unwrap().iter().find(|c| c["name"] == name).unwrap(); + assert_eq!(serde_json::json!(d.presented()), case["presented"], "{name}"); + let mut got = d.to_json(); + got.as_object_mut().unwrap().remove("plan"); + assert_eq!(got, case["outcome"], "{name}"); + wai::stage_measure::sim::check_stall(&d.model, &d.plan).unwrap_or_else(|v| panic!("{name}: {v:?}")); + if name != "fifo" { + assert!(d.pieces.iter().all(|p| p.base_arrival_us.is_some_and(|a| a <= p.due_us)), "{name}: every base by its instant"); + } + // The receipts: a chain per track, every delivered object accepted, a dropped + // one by inclusion only. + let published = { + let (out, _, _) = live(&policy(), PACK, 0); + out + }; + let by_id: BTreeMap = published.iter().map(|p| (format!("p{}/l{}", p.piece, p.layer), p)).collect(); + let mut heads: BTreeMap = BTreeMap::new(); + for ((o, ev), (g, r)) in d.model.objects.iter().zip(&d.result.as_ref().unwrap().objects).zip(&d.receipts) { + g.check_parent(heads.get(&g.track_alias).copied()).unwrap(); + heads.insert(g.track_alias, g); + let env = &by_id[&o.id].envelope; + match ev.state { + wai::stage_measure::sim::ObjState::Delivered => { + r.accept(g, env).unwrap(); + let mut bad = env.clone(); + *bad.last_mut().unwrap() ^= 1; + assert!(r.accept(g, &bad).is_err()); + } + _ => assert_eq!(r.verify_against(g, None).unwrap(), wai::jwp_delivery::Evidence::InclusionVerified), + } + } + seen.push((name, d.presented(), d.result.as_ref().unwrap().never_sent_capped)); + } + assert_eq!(seen[0].1, vec![0, 1, 0, 1, 0]); + assert_eq!(seen[1].1, vec![0, 0, 0, 1, 0], "fifo: the second piece's layer waits behind the first's and is dropped"); + assert!(seen[2].2 > 0 && seen[2].1.iter().all(|&l| l <= 1), "capped at layer 1"); +} diff --git a/wai/wai-rs/src/bin/wai_cli/http_vectors.rs b/wai/wai-rs/src/bin/wai_cli/http_vectors.rs index 9191a48f6..fed4d5604 100644 --- a/wai/wai-rs/src/bin/wai_cli/http_vectors.rs +++ b/wai/wai-rs/src/bin/wai_cli/http_vectors.rs @@ -1065,6 +1065,10 @@ fn cases(objs: &BTreeMap>, extra: &mut BTreeMap> v.push(refused("parse_head_kind_number", "parse_head", json!({ "file": "bad/OKN.wai" }), "head_malformed")); // Item 2: layers. v.push(case("union_O10_layers", "union_floor", json!({ "objects": ["O10", "O10L1", "O10L2"] }))); + // §4.2: a base listed twice is one object, with every layer it has, whichever + // occurrence a layer is listed beside: the floor keeps the layer. + v.push(case("union_O10_repeated_layer_after_first", "union_floor", json!({ "objects": ["O10", "O10L1", "O10"] }))); + v.push(case("union_O10_repeated_layer_after_second", "union_floor", json!({ "objects": ["O10", "O10", "O10L1"] }))); v.push(refused("union_O10_layers_unbound", "union_floor", json!({ "objects": ["O10L1", "O10L2"] }), "codecs_layer_unbound")); v.push(case("fetch_O10_base", "fetch", json!({ "object": "O10", "advertises": ["wai.audio.flac"] }))); v.push(case("fetch_O10L1_discovered", "fetch", json!({ "object": "O10L1", "fragment": frag("O10L1", Some(1009)), "advertises": [] }))); diff --git a/wai/wai-rs/src/bin/wai_stage_measure.rs b/wai/wai-rs/src/bin/wai_stage_measure.rs index c26d78025..b8febfb41 100644 --- a/wai/wai-rs/src/bin/wai_stage_measure.rs +++ b/wai/wai-rs/src/bin/wai_stage_measure.rs @@ -613,16 +613,31 @@ fn gop3() -> Model { for g in 0..3u32 { let from = g as u64 * 5; let cap = Some((from + 5) * unit_us); - objects.push(SimObject { id: format!("g{g}-base"), base: g, layer: 0, from, count: 5, bytes: 5000 + 700 * g as u64, capture_us: cap }); + objects.push(SimObject { id: format!("g{g}-base"), base: g, layer: 0, from, count: 5, bytes: 5000 + 700 * g as u64, capture_us: cap, timeout_us: None }); for (k, size) in [(1u8, 1500u64), (2, 900)] { for u in 0..5u64 { - objects.push(SimObject { id: format!("g{g}-l{k}-u{u}"), base: g, layer: k, from: from + u, count: 1, bytes: size + 37 * u + 11 * g as u64, capture_us: cap }); + objects.push(SimObject { id: format!("g{g}-l{k}-u{u}"), base: g, layer: k, from: from + u, count: 1, bytes: size + 37 * u + 11 * g as u64, capture_us: cap, timeout_us: None }); } } } Model { unit_us, objects } } +/// `gop3` with a delivery timeout on each refinement of its own, one per layer, as +/// MoQ tracks of one layer each set theirs (packaging §8.6): 60 ms on layer 1, 20 ms on +/// layer 2. +fn per_track() -> Model { + let mut m = gop3(); + for o in m.objects.iter_mut() { + o.timeout_us = match o.layer { + 0 => None, + 1 => Some(60_000), + _ => Some(20_000), + }; + } + m +} + /// The plans: two bounded link schedules over both carriages, a counter-plan for each /// unbounded part of §10.4's bound, a layer cap, and a live plan with playout /// deadlines. @@ -693,6 +708,11 @@ fn sim_cases() -> Vec<(String, String, &'static str, String)> { for (name, ..) in reorder_cases() { out.push((name.to_string(), format!("models/{name}.json"), name, "within-bound".to_string())); } + // Refinements with timeouts of their own: a fifo plan with no refinement timeout is + // bounded by theirs. + for plan in ["strict-slow-timeout", "fifo-timeout-subgroup", "fifo-no-timeout"] { + out.push((format!("per-track/{plan}"), "models/per-track.json".to_string(), plan, "within-bound".to_string())); + } for f in ["audio", "clip", "splat", "still"] { for (plan, claim) in [("strict-subgroup", "within-bound"), ("fifo-timeout-subgroup", "within-bound"), ("fifo-timeout-datagram", "within-bound"), ("fifo-no-timeout", "unbounded")] { out.push((format!("{f}/{plan}"), format!("fixture:{f}"), plan, claim.to_string())); @@ -707,7 +727,7 @@ fn sim_cases() -> Vec<(String, String, &'static str, String)> { /// decode overtook a large one's under base-first decode (`reorder-decode`). Each comes /// with its plan. fn reorder_cases() -> Vec<(&'static str, Model, Plan)> { - let o = |id: &str, base, layer, from, bytes, cap| SimObject { id: id.into(), base, layer, from, count: 1, bytes, capture_us: cap }; + let o = |id: &str, base, layer, from, bytes, cap| SimObject { id: id.into(), base, layer, from, count: 1, bytes, capture_us: cap, timeout_us: None }; let link = Plan { mode: Mode::Live, rtt_us: 0, @@ -737,6 +757,7 @@ fn reorder_cases() -> Vec<(&'static str, Model, Plan)> { fn sim_inputs() -> BTreeMap> { let mut files = BTreeMap::new(); files.insert("sim/models/gop3.json".to_string(), pretty(&gop3().to_json())); + files.insert("sim/models/per-track.json".to_string(), pretty(&per_track().to_json())); for (name, m, p) in reorder_cases() { files.insert(format!("sim/models/{name}.json"), pretty(&m.to_json())); files.insert(format!("sim/plans/{name}.json"), pretty(&p.to_json())); @@ -782,7 +803,7 @@ fn sim_results(c: &Corpus, evals: &[(Title, Evaluation)]) -> Result Result { - let b = sim::stall_bound(&read_plan(c, &format!("plans/{other}.json"))?).ok_or(format!("{name}: {other} has no bound"))?; + let b = sim::stall_bound_for(&m, &read_plan(c, &format!("plans/{other}.json"))?).ok_or(format!("{name}: {other} has no bound"))?; if bound.is_some() || worst.is_some_and(|w| w <= b as i64) { return Err(format!("{name}: the stall {worst:?} does not pass {other}'s bound {b}")); } @@ -1052,7 +1073,7 @@ fn sim_one(model: &str, plan: &str) -> Result<(), String> { let v: Value = serde_json::from_slice(&std::fs::read(plan).map_err(|e| format!("{plan}: {e}"))?).map_err(|e| e.to_string())?; let p = Plan::from_json(&v).map_err(|e| e.to_string())?; let r = sim::simulate(&m, &p).map_err(|e| e.to_string())?; - let out = json!({"label": "simulated", "plan": p.to_json(), "stall_bound_us": sim::stall_bound(&p), "max_stall_us": r.max_stall(), "result": r.to_json()}); + let out = json!({"label": "simulated", "plan": p.to_json(), "stall_bound_us": sim::stall_bound_for(&m, &p), "max_stall_us": r.max_stall(), "result": r.to_json()}); print!("{}", String::from_utf8(pretty(&out)).expect("UTF-8")); Ok(()) } diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index 31dc32e2e..ce6f4678e 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -519,7 +519,6 @@ pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[ wire("science.transaction.wai.stages", WireKind::DataId, PACKAGING, "§8", WireStatus::Defined), wire("tag:transaction.science,2026:wai/class", WireKind::Scheme, PACKAGING, "§8", WireStatus::Defined), wire("tag:transaction.science,2026:wai/stages", WireKind::Scheme, PACKAGING, "§8", WireStatus::Defined), - wire("wai_layer", WireKind::Catalog, PACKAGING, "§8", WireStatus::Defined), wire("wai_delivery_timeout_ms", WireKind::Catalog, PACKAGING, "§8", WireStatus::Defined), ]; diff --git a/wai/wai-rs/src/http_binding/codecs.rs b/wai/wai-rs/src/http_binding/codecs.rs index 568a3c5ae..a4d35e39b 100644 --- a/wai/wai-rs/src/http_binding/codecs.rs +++ b/wai/wai-rs/src/http_binding/codecs.rs @@ -305,10 +305,14 @@ fn base_capability(b: &SeqObject, base: &[u8; 32]) -> Option { /// capability after `+` in the alt of the base its `refines.base` names: a `WAI1` /// object of the sequence, through its capability (staged delivery's base is never /// decoded through a fallback), or a `WAI2` component, through the component's -/// capability. Layers join in sequence order, after the object's own capabilities. +/// capability. Layers join in sequence order, after the object's own capabilities. An +/// object the sequence lists more than once is one object of it, described once with +/// every layer it has, and a layer joins every object of the sequence that is its base. fn described(objects: &[SeqObject]) -> Result, CodecsError> { + let mut seen = std::collections::BTreeSet::new(); + let objects: Vec<&SeqObject> = objects.iter().filter(|o| seen.insert(o.cid)).collect(); let mut els: Vec> = Vec::new(); - for o in objects { + for o in &objects { els.push(if o.head.is_layer() { None } else { Some(element_of(&o.head)?) }); } for o in objects.iter().filter(|o| o.head.is_layer()) { @@ -319,14 +323,19 @@ fn described(objects: &[SeqObject]) -> Result, CodecsError> { .ok_or_else(|| CodecsError::Malformed(format!("the layer wai.{cap} carries no refines"))) .and_then(|v| Refines::from_value(v, RefForm::ContentHash).map_err(|e| CodecsError::Malformed(format!("refines: {}", e.0))))?; let base = super::unhex32(&refines.base).ok_or_else(|| CodecsError::Malformed("refines.base".into()))?; - let target: Option<(usize, String)> = objects.iter().enumerate() + let targets: Vec<(usize, String)> = objects.iter().enumerate() .filter(|(_, b)| !b.head.is_layer()) - .find_map(|(k, b)| base_capability(b, &base).map(|c| (k, c))); - let (k, base_cap) = target.ok_or_else(|| CodecsError::LayerUnbound(format!("wai.{cap} refines {}", refines.base)))?; - let el = els[k].as_mut().expect("a base is not a layer"); - let alt = el.alts.iter_mut().find(|a| a.cap == base_cap) - .ok_or_else(|| CodecsError::LayerUnbound(format!("wai.{cap}: its base's capability is not dispatched")))?; - push_unique(&mut alt.plus, cap); + .filter_map(|(k, b)| base_capability(b, &base).map(|c| (k, c))) + .collect(); + if targets.is_empty() { + return Err(CodecsError::LayerUnbound(format!("wai.{cap} refines {}", refines.base))); + } + for (k, base_cap) in targets { + let el = els[k].as_mut().expect("a base is not a layer"); + let alt = el.alts.iter_mut().find(|a| a.cap == base_cap) + .ok_or_else(|| CodecsError::LayerUnbound(format!("wai.{cap}: its base's capability is not dispatched")))?; + push_unique(&mut alt.plus, cap.clone()); + } } Ok(els.into_iter().flatten().collect()) } diff --git a/wai/wai-rs/src/pin.rs b/wai/wai-rs/src/pin.rs index dfbba4eb7..9c2fa314b 100644 --- a/wai/wai-rs/src/pin.rs +++ b/wai/wai-rs/src/pin.rs @@ -157,12 +157,22 @@ pub fn parse_listing(bytes: &[u8]) -> Result, PinRefusal /// Write the listing of the files `files` (path → bytes). The paths must obey the /// listing rules; the lines come out sorted. pub fn listing_from(files: &BTreeMap) -> Result, PinRefusal> { + listing_from_sha256(&files.iter().map(|(p, b)| (p.clone(), sha256_hex(b))).collect()) +} + +/// The listing [`listing_from`] writes, of files given by their SHA-256 (path → 64 +/// lowercase hexadecimal digits): for a writer that hashed each file as it wrote it and +/// no longer holds the bytes. +pub fn listing_from_sha256(files: &BTreeMap) -> Result, PinRefusal> { let mut paths: Vec<&str> = files.keys().map(String::as_str).collect(); paths.sort_unstable_by(|a, b| a.as_bytes().cmp(b.as_bytes())); check_paths(&paths)?; let mut out = Vec::new(); for p in paths { - out.extend_from_slice(sha256_hex(files[p]).as_bytes()); + if !is_sha256_hex(&files[p]) { + return Err(malformed(format!("{p}: its digest is not 64 lowercase hexadecimal digits"))); + } + out.extend_from_slice(files[p].as_bytes()); out.extend_from_slice(b" "); out.extend_from_slice(p.as_bytes()); out.push(b'\n'); @@ -518,6 +528,18 @@ mod tests { assert!(line.contains("supersedes \"a"), "{line}"); } + /// A listing from digests is the listing from bytes, and a digest outside WAI's + /// grammar is refused, not written. + #[test] + fn a_listing_from_digests_is_the_listing_from_bytes() { + let files: BTreeMap = [("z".to_string(), &b"z"[..]), ("a/c.onnx".into(), b"c"), ("a.b".into(), b"a")].into_iter().collect(); + let digests: BTreeMap = files.iter().map(|(p, b)| (p.clone(), sha256_hex(b))).collect(); + assert_eq!(listing_from_sha256(&digests).unwrap(), listing_from(&files).unwrap()); + let mut upper = digests.clone(); + upper.insert("z".into(), sha256_hex(b"z").to_uppercase()); + assert!(listing_from_sha256(&upper).is_err()); + } + #[test] fn listing_rules() { let ok: Vec = [line(b"a", "a.b"), line(b"c", "a/c.onnx"), line(b"z", "z")].concat(); diff --git a/wai/wai-rs/src/stage_measure/sim.rs b/wai/wai-rs/src/stage_measure/sim.rs index f613b91a9..6a2f71e64 100644 --- a/wai/wai-rs/src/stage_measure/sim.rs +++ b/wai/wai-rs/src/stage_measure/sim.rs @@ -8,7 +8,7 @@ //! units and counted bytes ([`Model::from_evaluation`] takes them from an evaluated //! title). A [`Plan`] (`wai-sim/1`) is the link, the sender's schedule, carriage, //! layer cap and delivery timeouts, and the sink's decode workers, order, times and -//! playout deadlines. [`simulate`] runs one against the other; [`stall_bound`] states +//! playout deadlines. [`simulate`] runs one against the other; [`stall_bound_for`] states //! the bound §10.4 gives a plan on how far its refinements may delay a base, and //! [`check_stall`] checks a plan against it. @@ -67,6 +67,10 @@ pub struct SimObject { pub bytes: u64, /// When its content is captured, for a live plan. pub capture_us: Option, + /// For a refinement, its own delivery timeout, in place of the plan's: what a + /// track's `DELIVERY_TIMEOUT` sets for each object on it (packaging §8.6). `None`: + /// the plan's timeout for its layer. + pub timeout_us: Option, } /// A title's objects, in the order the title lists them (the object index). @@ -103,9 +107,14 @@ fn mul(a: u64, b: u64) -> Result { /// Refuse members a section does not define, so a misspelt member is not ignored. fn only(v: &Value, members: &[&str], at: &str) -> Result<(), SimError> { + only_with(v, members, &[], at) +} + +/// [`only`], with `optional` members a section may leave out. +fn only_with(v: &Value, members: &[&str], optional: &[&str], at: &str) -> Result<(), SimError> { let o = v.as_object().ok_or_else(|| invalid(format!("{at}: not an object")))?; for k in o.keys() { - if !members.contains(&k.as_str()) { + if !members.contains(&k.as_str()) && !optional.contains(&k.as_str()) { return Err(invalid(format!("{at}: unknown member `{k}`"))); } } @@ -127,7 +136,7 @@ impl Model { let mut objects = Vec::new(); for (i, o) in v["objects"].as_array().ok_or_else(|| invalid("model: no objects"))?.iter().enumerate() { let at = format!("model object {i}"); - only(o, &["id", "base", "layer", "units", "bytes", "capture_us"], &at)?; + only_with(o, &["id", "base", "layer", "units", "bytes", "capture_us"], &["timeout_us"], &at)?; let units = o["units"].as_array().filter(|a| a.len() == 2).ok_or_else(|| invalid(format!("{at}: units is not [from, count]")))?; objects.push(SimObject { id: o["id"].as_str().ok_or_else(|| invalid(format!("{at}: id")))?.to_string(), @@ -137,6 +146,7 @@ impl Model { count: units[1].as_u64().ok_or_else(|| invalid(format!("{at}: units")))?, bytes: u64_of(o, "bytes", &at)?, capture_us: opt_u64(o, "capture_us", &at)?, + timeout_us: opt_u64(o, "timeout_us", &at)?, }); } let m = Model { unit_us: u64_of(v, "unit_us", "model")?, objects }; @@ -144,14 +154,20 @@ impl Model { Ok(m) } - /// The model as JSON. + /// The model as JSON; `timeout_us` only on an object that has its own. pub fn to_json(&self) -> Value { json!({ "format": MODEL_FORMAT, "unit_us": self.unit_us, - "objects": self.objects.iter().map(|o| json!({ - "id": o.id, "base": o.base, "layer": o.layer, "units": [o.from, o.count], "bytes": o.bytes, "capture_us": o.capture_us, - })).collect::>(), + "objects": self.objects.iter().map(|o| { + let mut j = json!({ + "id": o.id, "base": o.base, "layer": o.layer, "units": [o.from, o.count], "bytes": o.bytes, "capture_us": o.capture_us, + }); + if let Some(t) = o.timeout_us { + j["timeout_us"] = json!(t); + } + j + }).collect::>(), }) } @@ -169,6 +185,7 @@ impl Model { count: o.count, bytes: o.counted.total, capture_us: e.bases.get(o.base).and_then(|b| b.1), + timeout_us: None, }) .collect(); Model { unit_us: e.unit_us, objects } @@ -180,9 +197,9 @@ impl Model { Model { unit_us: self.unit_us, objects: self.objects.iter().filter(|o| o.layer == 0).cloned().collect() } } - /// A model is well formed when every object has bytes and units; no two objects - /// cover one unit at one layer; and every unit a layer `k ≥ 1` covers is covered at - /// layer `k − 1` by an object of the same base. + /// A model is well formed when every object has bytes and units; no base carries a + /// timeout of its own; no two objects cover one unit at one layer; and every unit a + /// layer `k ≥ 1` covers is covered at layer `k − 1` by an object of the same base. pub fn check(&self) -> Result<(), SimError> { if self.unit_us == 0 { return Err(invalid("model: unit_us is 0")); @@ -195,6 +212,9 @@ impl Model { if o.bytes == 0 || o.count == 0 { return Err(invalid(format!("{}: no bytes or no units", o.id))); } + if o.layer == 0 && o.timeout_us.is_some() { + return Err(invalid(format!("{}: a base takes the plan's timeout, never its own", o.id))); + } let end = o.from.checked_add(o.count).ok_or_else(|| invalid(format!("{}: units past u64", o.id)))?; if o.count > 1 << 24 { return Err(invalid(format!("{}: too many units", o.id))); @@ -488,7 +508,7 @@ fn run_link(m: &Model, p: &Plan) -> Result { .objects .iter() .zip(&avail) - .map(|(o, &a)| if o.layer == 0 { p.timeout_base_us } else { p.timeout_refinement_us }.map(|t| add(a, t)).transpose()) + .map(|(o, &a)| if o.layer == 0 { p.timeout_base_us } else { o.timeout_us.or(p.timeout_refinement_us) }.map(|t| add(a, t)).transpose()) .collect::>()?; if p.carriage == Carriage::Datagram { if let Some(o) = m.objects.iter().zip(&capped).find(|(o, c)| !**c && o.bytes > p.chunk_bytes) { @@ -719,24 +739,38 @@ fn simulate_once(m: &Model, p: &Plan) -> Result { }) } -/// §10.4's bound on how far a plan's refinements may delay a base's presentation: -/// `S_link + S_decode`, or `None` where either part is unbounded. +/// §10.4's bound on how far a plan's refinements may delay a base's presentation over a +/// model: `S_link + S_decode`, or `None` where either part is unbounded. /// -/// - `S_link`: one chunk's serialisation under `strict-priority`; under `fifo` with a -/// refinement timeout `T`, `T` plus one chunk; otherwise unbounded. +/// - `S_link`: one chunk's serialisation under `strict-priority`; under `fifo`, `T` plus +/// one chunk, where `T` is the longest timeout any refinement of `m` takes (its own, +/// where the model gives it one, or the plan's), and unbounded where one takes none. /// - `S_decode`: under `base-first` decode, one refinement's decode time; otherwise /// unbounded. /// /// A plan with a base timeout has no bound: a base the refinements delay past its own /// timeout is not presented at all. §10.4 proves the bound for every other plan with a /// bounded link schedule and `base-first` decode, given that bases are sent and decoded -/// in the order they became available, as [`simulate`] sends and decodes them. -pub fn stall_bound(p: &Plan) -> Option { +/// in the order they became available, as [`simulate`] sends and decodes them. There is +/// no bound of a plan alone: a refinement's own timeout is the model's. +pub fn stall_bound_for(m: &Model, p: &Plan) -> Option { + let mut t: Option = None; + for o in m.objects.iter().filter(|o| o.layer >= 1) { + let own = o.timeout_us.or(p.timeout_refinement_us); + match own { + None => return bound_with(p, None), + Some(x) => t = Some(t.map_or(x, |y| y.max(x))), + } + } + bound_with(p, t.or(p.timeout_refinement_us).or(Some(0))) +} + +fn bound_with(p: &Plan, refinement_timeout: Option) -> Option { if p.timeout_base_us.is_some() { return None; } let chunk = p.serialise_us(p.chunk_bytes)?; - let link = match (p.schedule, p.timeout_refinement_us) { + let link = match (p.schedule, refinement_timeout) { (Schedule::StrictPriority, _) => chunk, (Schedule::Fifo, Some(t)) => t.checked_add(chunk)?, (Schedule::Fifo, None) => return None, @@ -760,7 +794,7 @@ pub struct StallViolation { /// Check `plan` over `model` against its bound; `Ok` for a plan with none. pub fn check_stall(model: &Model, plan: &Plan) -> Result<(), StallViolation> { - let Some(bound) = stall_bound(plan) else { return Ok(()) }; + let Some(bound) = stall_bound_for(model, plan) else { return Ok(()) }; let r = simulate(model, plan).map_err(|_| StallViolation { unit: 0, stall_us: None, bound_us: bound })?; for &(unit, s) in &r.stall_us { if s.is_none_or(|s| s > bound as i64) { diff --git a/wai/wai-rs/src/stage_measure/sim/tests.rs b/wai/wai-rs/src/stage_measure/sim/tests.rs index 66537ecb5..5f54a8c0a 100644 --- a/wai/wai-rs/src/stage_measure/sim/tests.rs +++ b/wai/wai-rs/src/stage_measure/sim/tests.rs @@ -23,8 +23,13 @@ impl Rng { } } +/// §10.4's bound of a plan over a model whose refinements carry no timeout of their own. +fn plan_only_bound(p: &Plan) -> Option { + bound_with(p, p.timeout_refinement_us) +} + fn obj(id: &str, base: u32, layer: u8, from: u64, count: u64, bytes: u64, capture_us: Option) -> SimObject { - SimObject { id: id.into(), base, layer, from, count, bytes, capture_us } + SimObject { id: id.into(), base, layer, from, count, bytes, capture_us, timeout_us: None } } /// `bases` groups of `per` units, each a base and `layers` refinement layers, each @@ -216,7 +221,7 @@ fn strict_priority_bound() { let carriage = if r.coin() { Carriage::Datagram } else { Carriage::Subgroup }; let t = if r.coin() { Some(r.range(0, 300_000)) } else { None }; let p = plan(&mut r, Schedule::StrictPriority, carriage, t, DecodeOrder::BaseFirst); - assert!(stall_bound(&p).is_some()); + assert!(plan_only_bound(&p).is_some()); conserved(&simulate(&m, &p).unwrap()); check_stall(&m, &p).unwrap_or_else(|v| panic!("plan {i}: {v:?} {p:?}")); } @@ -262,7 +267,7 @@ fn bound_holds_on_wild_plans() { let mut r = Rng(0x5eed_0000 + seed); let m = wild_model(&mut r); let p = wild_plan(&mut r, &m); - assert!(stall_bound(&p).is_some()); + assert!(plan_only_bound(&p).is_some()); let res = simulate(&m, &p).unwrap_or_else(|e| panic!("seed {seed}: {e}")); conserved(&res); check_stall(&m, &p).unwrap_or_else(|v| panic!("seed {seed}: {v:?}\n{p:?}\n{}", m.to_json())); @@ -356,7 +361,7 @@ fn bound_holds_on_targeted_plans() { let mut r = Rng(0x7a76_e7ed); for i in 0..TARGETED_DRAWS { let (m, p) = targeted(&mut r); - assert!(stall_bound(&p).is_some()); + assert!(plan_only_bound(&p).is_some()); let res = simulate(&m, &p).unwrap_or_else(|e| panic!("draw {i}: {e}\n{p:?}\n{}", m.to_json())); conserved(&res); check_stall(&m, &p).unwrap_or_else(|v| panic!("draw {i}: {v:?}\n{p:?}\n{}", m.to_json())); @@ -455,8 +460,8 @@ fn fifo_no_timeout_violates() { let m = counter_model(); let fifo = counter_plan(Schedule::Fifo, None, DecodeOrder::BaseFirst); let strict = counter_plan(Schedule::StrictPriority, None, DecodeOrder::BaseFirst); - assert_eq!(stall_bound(&fifo), None); - let bound = stall_bound(&strict).unwrap(); + assert_eq!(plan_only_bound(&fifo), None); + let bound = plan_only_bound(&strict).unwrap(); check_stall(&m, &strict).unwrap(); let worst = simulate(&m, &fifo).unwrap().max_stall().unwrap(); assert!(worst > bound as i64, "fifo with no timeout stalls {worst} µs, within the strict bound {bound}"); @@ -475,9 +480,9 @@ fn fifo_decode_violates() { let fifo = Plan { vod_window_us: Some(180_000), ..counter_plan(Schedule::StrictPriority, None, DecodeOrder::Fifo) }; let base_first = Plan { decode_order: DecodeOrder::BaseFirst, ..fifo.clone() }; check_stall(&m, &base_first).unwrap(); - assert_eq!(stall_bound(&fifo), None); + assert_eq!(plan_only_bound(&fifo), None); let worst = simulate(&m, &fifo).unwrap().max_stall().unwrap(); - assert!(worst > stall_bound(&base_first).unwrap() as i64, "fifo decode stalls {worst} µs"); + assert!(worst > plan_only_bound(&base_first).unwrap() as i64, "fifo decode stalls {worst} µs"); } #[test] @@ -563,7 +568,7 @@ fn sums_past_64_bits_are_refused() { // A decode that would end past 64 bits. let small = Model { unit_us: 40_000, objects: vec![obj("B0", 0, 0, 0, 1, 10, None)] }; assert_eq!(simulate(&small, &Plan { chunk_bytes: 1000, bytes_per_ms: 1000, decode_base_us: u64::MAX, ..p.clone() }), Err(SimError::Overflow)); - assert_eq!(stall_bound(&p), None, "a bound past 64 bits is none"); + assert_eq!(plan_only_bound(&p), None, "a bound past 64 bits is none"); // The review's model, read from its JSON: its byte counts pass 2^53 - 1. let o = serde_json::json!({"format": "wai-sim-model/1", "unit_us": 40000, "objects": [ {"id": "B0", "base": 0, "layer": 0, "units": [0, 1], "bytes": 18446744073709551u64, "capture_us": null}, @@ -574,3 +579,68 @@ fn sums_past_64_bits_are_refused() { let q = Plan { chunk_bytes: MAX_FIGURE, rtt_us: MAX_FIGURE, vod_window_us: Some(0), ..p }; assert!(matches!(simulate(&edge, &q), Err(SimError::Overflow))); } + +/// A refinement's own timeout replaces the plan's for it alone (a MoQ track's +/// `DELIVERY_TIMEOUT`, packaging §8.6): of two refinements behind a base on a slow link, +/// the one whose own timeout passes first is dropped and the other delivered. A base +/// takes no timeout of its own, and the member reads and writes back. +#[test] +fn a_refinement_s_own_timeout() { + let p = Plan { + mode: Mode::Vod, + rtt_us: 0, + bytes_per_ms: 1000, + chunk_bytes: 1000, + schedule: Schedule::StrictPriority, + carriage: Carriage::Datagram, + layer_cap: None, + timeout_base_us: None, + timeout_refinement_us: Some(10_000), + vod_window_us: None, + workers: 1, + decode_order: DecodeOrder::BaseFirst, + decode_base_us: 1, + decode_refinement_us: 1, + playout_start_us: None, + late_refinement: LateRefinement::Present, + }; + // The base takes 1 ms; each refinement 1 ms after it. + let mut m = Model { unit_us: 40_000, objects: vec![obj("b", 0, 0, 0, 2, 1000, None), obj("l0", 0, 1, 0, 1, 1000, None), obj("l1", 0, 1, 1, 1, 1000, None)] }; + m.objects[1].timeout_us = Some(500); + let r = simulate(&m, &p).unwrap(); + assert_eq!((r.objects[1].state, r.objects[2].state), (ObjState::Dropped, ObjState::Delivered), "l0's own 0.5 ms passed; l1 has the plan's 10 ms"); + let back = Model::from_json(&m.to_json()).unwrap(); + assert_eq!(back, m); + assert!(m.to_json()["objects"][2].get("timeout_us").is_none(), "written only where an object has its own"); + let mut bad = m.to_json(); + bad["objects"][0]["timeout_us"] = serde_json::json!(5); + assert!(Model::from_json(&bad).is_err(), "a base takes the plan's timeout"); + // Under fifo, the bound's T is the longest refinement timeout, its own or the plan's; + // with every refinement timed, a plan with none of its own is bounded. + let fifo = Plan { schedule: Schedule::Fifo, timeout_refinement_us: None, ..p.clone() }; + assert_eq!(plan_only_bound(&fifo), None); + m.objects[2].timeout_us = Some(7000); + assert_eq!(stall_bound_for(&m, &fifo), Some(7000 + 1000 + 1)); + assert_eq!(stall_bound_for(&m, &Plan { timeout_refinement_us: Some(9000), ..fifo.clone() }), Some(7000 + 1000 + 1), "own timeouts replace the plan's"); +} + +/// §10.4's bound on [`targeted`] draws whose refinements take timeouts of their own, +/// some of them, under either schedule. +#[test] +fn bound_holds_with_object_timeouts() { + let mut r = Rng(0x0b1e_c7ed); + for i in 0..TARGETED_DRAWS / 4 { + let (mut m, p) = targeted(&mut r); + let c = p.serialise_us(p.chunk_bytes).unwrap(); + let all = r.coin(); + for o in m.objects.iter_mut().filter(|o| o.layer >= 1) { + if all || r.coin() { + o.timeout_us = Some([0, 1, c, 3 * c, 10 * c][r.range(0, 4) as usize]); + } + } + let p = if all && r.coin() { Plan { timeout_refinement_us: None, ..p } } else { p }; + let res = simulate(&m, &p).unwrap_or_else(|e| panic!("draw {i}: {e}\n{p:?}\n{}", m.to_json())); + conserved(&res); + check_stall(&m, &p).unwrap_or_else(|v| panic!("draw {i}: {v:?}\n{p:?}\n{}", m.to_json())); + } +} diff --git a/wai/wai-rs/tests/staged_resources.rs b/wai/wai-rs/tests/staged_resources.rs index b91bff0e6..3e5991480 100644 --- a/wai/wai-rs/tests/staged_resources.rs +++ b/wai/wai-rs/tests/staged_resources.rs @@ -1,6 +1,9 @@ //! The staged sink's own work and memory (`extensions/staged-delivery.md` §4.5 and -//! Appendix A.5), measured with a global allocator that counts the bytes allocated, -//! the allocations, the bytes live and their peak. +//! Appendix A.5), measured with a global allocator that counts, for each thread, the +//! bytes it allocated, its allocations, the bytes live and their peak. The counts are +//! the measuring thread's own: another thread's allocations (the test harness's, which +//! allocates a message on its main thread when a test passes 60 seconds) never land in +//! a window. //! //! - Work per layer is linear in its units: an offer of a layer over 4096, 16384 and //! 65536 units (one layer alone; a layer whose `prev` is a run per unit, held; a @@ -15,12 +18,12 @@ //! many layers that conflict, or that expire, leaves the sink holding what it held. //! - The parse holds no more than the declared working bytes beyond the payload. //! -//! Its own test binary with a single test, so that no other test's allocations land in -//! the windows it measures. +//! Its own test binary with a single test, and the counts per thread, so that no other +//! test's or thread's allocations land in the windows it measures. #![cfg(all(feature = "staged", feature = "video_int"))] use std::alloc::{GlobalAlloc, Layout, System}; -use std::sync::atomic::{AtomicUsize, Ordering::Relaxed}; +use std::cell::Cell; use std::time::Duration; use wai::codecs::{DeterminismTier, CAP_VIDEO_INT_MOTION, CAP_VIDEO_INT_REFINE}; @@ -28,17 +31,48 @@ use wai::container::{Conditioning, Manifest, ModelRequirement, Prev, PrevRun, Re use wai::staged::wir1::{self, Below, Predictor, RefineParams, UnitKind}; use wai::staged::{BaseInfo, ChainConfig, HoldBound, ReceiptEvidence, Samples, StageRefusal, StageSink, UnitBuf}; -/// The system allocator, counting bytes allocated, allocations, bytes live and peak. +/// The system allocator, counting for the calling thread the bytes it allocated, its +/// allocations, the bytes live and their peak. Live bytes are signed: a thread may free +/// what another allocated. struct Count; -static ALLOCATED: AtomicUsize = AtomicUsize::new(0); -static CALLS: AtomicUsize = AtomicUsize::new(0); -static CUR: AtomicUsize = AtomicUsize::new(0); -static PEAK: AtomicUsize = AtomicUsize::new(0); + +/// One thread's counts. `const`-initialised `Cell`s with no destructor, so reaching them +/// from the allocator allocates nothing; `try_with` skips a thread being torn down. +struct Counts { + allocated: Cell, + calls: Cell, + cur: Cell, + peak: Cell, +} + +thread_local! { + static COUNTS: Counts = const { Counts { allocated: Cell::new(0), calls: Cell::new(0), cur: Cell::new(0), peak: Cell::new(0) } }; +} fn grow(n: usize) { - ALLOCATED.fetch_add(n, Relaxed); - CALLS.fetch_add(1, Relaxed); - PEAK.fetch_max(CUR.fetch_add(n, Relaxed) + n, Relaxed); + let _ = COUNTS.try_with(|c| { + c.allocated.set(c.allocated.get().wrapping_add(n)); + c.calls.set(c.calls.get().wrapping_add(1)); + let cur = c.cur.get().wrapping_add(n as isize); + c.cur.set(cur); + if cur > c.peak.get() { + c.peak.set(cur); + } + }); +} + +fn shrink(n: usize) { + let _ = COUNTS.try_with(|c| c.cur.set(c.cur.get().wrapping_sub(n as isize))); +} + +/// The calling thread's live bytes. +fn cur() -> isize { + COUNTS.with(|c| c.cur.get()) +} + +/// The calling thread's live bytes past `before`, or 0 below it. +fn grown_since(before: isize) -> usize { + (cur() - before).max(0) as usize } unsafe impl GlobalAlloc for Count { @@ -54,12 +88,12 @@ unsafe impl GlobalAlloc for Count { if new >= l.size() { grow(new - l.size()); } else { - CUR.fetch_sub(l.size() - new, Relaxed); + shrink(l.size() - new); } unsafe { System.realloc(p, l, new) } } unsafe fn dealloc(&self, p: *mut u8, l: Layout) { - CUR.fetch_sub(l.size(), Relaxed); + shrink(l.size()); unsafe { System.dealloc(p, l) } } } @@ -110,13 +144,19 @@ fn now() -> Duration { } fn measure(f: impl FnOnce() -> T) -> (T, Window) { - let start = CUR.load(Relaxed); - PEAK.store(start, Relaxed); - let (a0, c0) = (ALLOCATED.load(Relaxed), CALLS.load(Relaxed)); + let (start, a0, c0) = COUNTS.with(|c| { + c.peak.set(c.cur.get()); + (c.cur.get(), c.allocated.get(), c.calls.get()) + }); let t = now(); let out = f(); let time = now() - t; - let w = Window { allocated: ALLOCATED.load(Relaxed) - a0, calls: CALLS.load(Relaxed) - c0, peak: PEAK.load(Relaxed) - start, time }; + let w = COUNTS.with(|c| Window { + allocated: c.allocated.get().wrapping_sub(a0), + calls: c.calls.get().wrapping_sub(c0), + peak: (c.peak.get() - start).max(0) as usize, + time, + }); (out, w) } @@ -249,6 +289,15 @@ fn offers(n: u32) -> [Window; 5] { #[test] fn staged_sink_resources() { + // ── The counts are the measuring thread's own ── + // A megabyte another thread allocates inside a window is not this thread's; what this + // thread allocates is. + let (_, other) = measure(|| std::thread::spawn(|| std::hint::black_box(vec![1u8; 1 << 20])).join().unwrap().len()); + let (_, own) = measure(|| std::hint::black_box(vec![1u8; 1 << 20]).len()); + println!("counts: another thread's megabyte counts {} bytes here, this thread's {}", other.peak, own.peak); + assert!(other.peak < 1 << 16, "another thread's allocation counted: {}", other.peak); + assert!(own.peak >= 1 << 20, "this thread's allocation not counted: {}", own.peak); + // ── Work per layer, linear in its units ── let sizes = [4096u32, 16384, 65536]; // The fastest of three runs at each size, so a busy machine does not decide. @@ -337,19 +386,19 @@ fn staged_sink_resources() { let layers: Vec> = (0..48).map(|i| noisy(&bh, Prev::One(hex(&bh)), 1, &units, i)).collect(); let after_first = { sink.offer(&layers[0], 0, ReceiptEvidence::None).unwrap(); - CUR.load(Relaxed) + cur() }; for (i, l) in layers.iter().enumerate().skip(1) { let r = sink.offer(l, i as u64, ReceiptEvidence::None).unwrap(); assert_eq!(r[0].refused, vec![(0, StageRefusal::LayerConflict)]); } - let grew = CUR.load(Relaxed).saturating_sub(after_first); + let grew = grown_since(after_first); println!("retention: 47 refused layers of {} bytes each; the sink grew {grew} bytes", layers[1].len()); assert!(grew < layers[1].len() / 2, "the sink kept {grew} bytes of layers no unit holds"); // Held layers that expire are dropped too. let mut sink = StageSink::new(cfg(64, u64::MAX)); sink.add_base(info.clone(), units.clone()); - let before = CUR.load(Relaxed); + let before = cur(); for (i, l) in layers.iter().enumerate().take(16) { let mut l2 = Wai::from_bytes(l).unwrap(); let r = Refines { base: hex(&bh), prev: Prev::One(hex(&[i as u8 + 1; 32])), layer: 2, span: None }; @@ -357,11 +406,11 @@ fn staged_sink_resources() { let r = sink.offer(&l2.to_bytes().unwrap(), 0, ReceiptEvidence::None).unwrap(); assert_eq!(r[0].held, vec![0]); } - assert!(CUR.load(Relaxed) > before + 16 * layers[1].len() / 2, "held layers are kept"); + assert!(cur() > before + (16 * layers[1].len() / 2) as isize, "held layers are kept"); let r = sink.expire(10_000); assert_eq!(r.len(), 16); drop(r); - let grew = CUR.load(Relaxed).saturating_sub(before); + let grew = grown_since(before); assert!(grew < layers[1].len() / 2, "the sink kept {grew} bytes after every held layer expired"); // ── Records of dropped chains (§4.4) ── @@ -370,20 +419,20 @@ fn staged_sink_resources() { // holds for them within a small multiple of it. let units: Vec = base(4096, 1, [0; 32]).1; let mut sink = StageSink::new(cfg(16, u64::MAX)); - let before = CUR.load(Relaxed); + let before = cur(); for i in 0..20u8 { let mut info = base(1, 1, [0x40 + i; 32]).0; info.content_hash = [0x40 + i; 32]; sink.add_base(info, units.clone()); sink.remove_base(&[0x40 + i; 32]); } - let grew = CUR.load(Relaxed).saturating_sub(before); + let grew = grown_since(before); println!("records: 20 dropped bases of 4096 units with nothing recorded keep {} bytes; the sink grew {grew} bytes", sink.kept_bytes()); assert_eq!(sink.kept_bytes(), 0); assert!(grew < 16 << 10, "the sink kept {grew} bytes for bases with nothing recorded"); let record_budget = 1 << 20; let mut sink = StageSink::new(ChainConfig { record_budget, ..cfg(16, u64::MAX) }); - let before = CUR.load(Relaxed); + let before = cur(); for i in 0..20u8 { let h = [0x60 + i; 32]; let mut info = base(1, 1, h).0; @@ -394,7 +443,7 @@ fn staged_sink_resources() { sink.remove_base(&h); assert!(sink.kept_bytes() <= record_budget); } - let grew = CUR.load(Relaxed).saturating_sub(before); + let grew = grown_since(before); println!("records: 20 dropped bases of 4096 refined units keep {} counted bytes within {record_budget}; the sink grew {grew} bytes", sink.kept_bytes()); assert!(grew as u64 <= 4 * record_budget, "the sink holds {grew} bytes for a record budget of {record_budget}"); @@ -453,13 +502,13 @@ fn staged_sink_resources() { let (info, units) = base(16, 256, bh); let out: u64 = units.iter().map(|u| u.samples.byte_len()).sum(); let mut sink = StageSink::new(cfg(16, out / 2)); - let before = CUR.load(Relaxed); + let before = cur(); let r = sink.add_base(info, units); assert_eq!(r.len(), 1); assert_eq!(r[0].envelope, Some(StageRefusal::ChainOverLimit)); drop(r); assert!(sink.chain(&bh).is_none()); - let left = CUR.load(Relaxed).saturating_sub(before); + let left = grown_since(before); println!("budget: a base of {out} output bytes in a {}-byte budget is refused; the sink holds {left} bytes of it", out / 2); assert!(left < 4096, "the sink kept {left} bytes of a base it refused"); } diff --git a/wai/wai-web/src/measure.rs b/wai/wai-web/src/measure.rs index d94426c76..0687eded5 100644 --- a/wai/wai-web/src/measure.rs +++ b/wai/wai-web/src/measure.rs @@ -123,7 +123,7 @@ pub(crate) fn simulated(model: &Model, plan_json: &str) -> Result