diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index e45d98694..feafd20cc 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -274,11 +274,14 @@ jobs: # tests/cli_http.rs runs `wai http publish`, `codecs`, `origin` and `bench` as a user # does: objects named by their content hash with their head proofs, a sequence's union # and floor, the origin's headers, ranges and If-Range on loopback, and a fetch by - # the byte-range procedure whose record states energy as not measured. Cargo.toml - # declares it with required-features = ["http"]. + # the byte-range procedure whose record states energy as not measured; and `hls`, + # `mpd`, `seal-playlist` and `check-playlist`, whose receipts need video_manifest. + # Cargo.toml declares it with required-features = ["http"]. tests/dash_read_bounds.rs + # holds Appendix I's reader to the document's size under a counting allocator, and an + # element's attributes to linear CPU time; a binary of its own, one test. - name: The wai http commands working-directory: wai/wai-rs - run: cargo test --no-default-features --features http --test cli_http + run: cargo test --no-default-features --features http,video_manifest --test cli_http --test dash_read_bounds # tests/staged_moq.rs runs the MoQ trace of staged-conformance/moq/ (moq-streaming-format # §4.1) with signed receipts: a chain for each track, a receipt held for its parent and # admitted when it arrives, the relay's per-hop delivery timeout, dropped objects checked @@ -348,7 +351,7 @@ jobs: working-directory: wai/wai-rs run: | set -euo pipefail - F=world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery,video_int,audio_int,splat_int,staged,stage_measure,telemetry,http + F=world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery,video_int,audio_int,splat_int,staged,stage_measure,telemetry,http,video_manifest v() { echo "::group::$1"; cargo run -q --no-default-features --features "$F" --bin "$2" -- verify "$3"; echo "::endgroup::"; } v worlds wai_world_vectors ../world-conformance v spatial-audio wai_audio_vectors ../audio-conformance @@ -399,7 +402,7 @@ jobs: shell: pwsh working-directory: wai/wai-rs run: | - $F = "world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery,video_int,audio_int,splat_int,staged,stage_measure,telemetry,http" + $F = "world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery,video_int,audio_int,splat_int,staged,stage_measure,telemetry,http,video_manifest" function v($grp, $bin, $path) { Write-Output "::group::$grp" cargo run -q --no-default-features --features $F --bin $bin -- verify $path @@ -515,7 +518,9 @@ jobs: run: python3 staged-conformance/staged_verify.py staged-conformance # The HTTP delivery corpus's second implementation (http-conformance/README): the head, # the head proof (every committed proof rebuilt from the text), response checks, plans, - # selection, the byte-range procedure and its fallback, and the codecs union and floor, + # selection, the byte-range procedure and its fallback, the codecs union and floor, the + # playlists (read, rendered, held adjacent, the classical master unchanged), playlist + # receipts (Ed25519 from RFC 8032) and the live edge, and the adaptation policies, # checked first against published BLAKE3 vectors. Standard library only. Its # mutations and the differential against wai-rs run in wai-http.yml's falsify job. - name: http_verify.py @@ -525,7 +530,9 @@ jobs: # Outside the corpus: wai_http_vectors fuzz writes seeded cases (Content-Range values, # codecs values, #WAI-LAYER lines, manifests and their member types, encryption, # refines, layers declared against their bases, layers over WAI2 components, objects - # past the first request through a tampering origin) with wai-rs's outcomes, and + # past the first request through a tampering origin; playlists, adjacent pairs, + # published masters, renderings, adaptation states, the integer log and changed + # receipt files) with wai-rs's outcomes, and # http_verify.py must give every one, a refusal by the same code. It prints each # family's count of cases and of refusals. Two fixed seeds, so a run is reproducible. - name: The HTTP differential outside the corpus (wai-rs against http_verify.py) @@ -533,7 +540,7 @@ jobs: shell: bash run: | set -euo pipefail - (cd wai-rs && cargo build --no-default-features --features http --bin wai_http_vectors) + (cd wai-rs && cargo build --no-default-features --features http,video_manifest --bin wai_http_vectors) for seed in 11 12; do wai-rs/target/debug/wai_http_vectors fuzz http-conformance --seed "$seed" --count 400 --out "$RUNNER_TEMP/http-fuzz-$seed.json" python3 http-conformance/http_verify.py cases "$RUNNER_TEMP/http-fuzz-$seed.json" http-conformance diff --git a/.github/workflows/wai-http.yml b/.github/workflows/wai-http.yml index 8b815fd61..45f658044 100644 --- a/.github/workflows/wai-http.yml +++ b/.github/workflows/wai-http.yml @@ -9,7 +9,9 @@ name: WAI HTTP delivery # union, layers' place in it and their dispatch, the wai prefix's case, duplicate # capabilities, a record beside a fallback, the trailing-byte rule, the missing-digest # plan, the class merge, the base-only threshold, refused refinement responses, stale -# bytes after a mismatch, an unanswered base, the selection digest test); it +# bytes after a mismatch, an unanswered base, the selection digest test; and on the +# playlist side adjacency, rendering, bandwidths, durations, the classical master, the +# playlist receipt and its chain, listed cids, and the adaptation policies); it # is applied to a fresh worktree, the build must succeed, and `wai_http_vectors verify` # must fail naming the cases on the patch's first line. Then each `http_verify.py # --mutate` must fail, and the differential: wai-rs writes random envelopes' covered @@ -41,6 +43,13 @@ on: - 'wai/wai-rs/src/bin/wai_http_vectors.rs' - 'wai/wai-rs/src/bin/wai_cli/http.rs' - 'wai/wai-rs/src/bin/wai_cli/http_vectors.rs' + - 'wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs' + - 'wai/wai-rs/src/bin/wai_cli/http_playlists.rs' + # The playlists render through hls_render and are sealed by wai_video_manifest. + - 'wai/wai-rs/src/hls_render.rs' + - 'wai/wai-rs/src/wai_video_manifest.rs' + # Playlist receipts verify Ed25519 through the shared strict rule. + - 'wai/wai-rs/src/sig.rs' - 'wai/wai-rs/Cargo.toml' - 'wai/http-conformance/**' - 'wai/cache-conformance/**' @@ -61,6 +70,13 @@ on: - 'wai/wai-rs/src/bin/wai_http_vectors.rs' - 'wai/wai-rs/src/bin/wai_cli/http.rs' - 'wai/wai-rs/src/bin/wai_cli/http_vectors.rs' + - 'wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs' + - 'wai/wai-rs/src/bin/wai_cli/http_playlists.rs' + # The playlists render through hls_render and are sealed by wai_video_manifest. + - 'wai/wai-rs/src/hls_render.rs' + - 'wai/wai-rs/src/wai_video_manifest.rs' + # Playlist receipts verify Ed25519 through the shared strict rule. + - 'wai/wai-rs/src/sig.rs' - 'wai/wai-rs/Cargo.toml' - 'wai/http-conformance/**' - 'wai/cache-conformance/**' diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 409d82ca6..2335e2aa3 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -372,6 +372,68 @@ wire format may change and such changes are recorded here. negative-control proxies (which must each be caught) and against labelled caches where an environment provides them; reports name a cache by label, version and configuration digest only, and CI uploads them. +- **Playlists and live** (http-delivery §8.6, §10.4, §11, Appendices G and H): + - `hls_render::render_hls_with` renders on-demand, live and low-latency WAI media + playlists (media and discontinuity sequences, a fixed target duration, parts with + their server control, discontinuities, program date-times, `#WAI-LAYER` lines with + `N` only, the open segment and its preload hint) and, under the legacy parameters + with no extras, gives `render_hls`'s bytes; the module needs no dependency, and + `wai_video_manifest` re-exports it under its old paths; + - the playlist receipt (`ManifestReceipt`) carries `render` and `extras`; its + signature binds a render digest only when a rendering is present, so every receipt + sealed before verifies unchanged (one sealed by the earlier tree is pinned), and a + rendering has one form; `seal_with`, the receipt file's strict JSON form and + `mr/.json` are new, and a live playlist's revisions chain; + - `http_binding::hls` reads media playlists, holds a WAI playlist adjacent to its + classical one (durations to the millisecond, rounded half up, read exactly), renders + the WAI media and multivariant playlists (bandwidths computed from the objects), + and keeps the classical master to its one session-data line (`classical_changed`); + `http_binding::dash` writes `wai.mpd` on the classical timeline; + `http_binding::playlist_receipt` seals and checks playlists and live chains + (`playlist_receipt_malformed`, `_mismatch`, `_invalid`, `_unchained`; + `playlist_malformed`); + - `wai http hls|mpd|seal-playlist|check-playlist`; the origin serves playlists to be + revalidated on every use; + - the corpus gains a staged pair with a layer over its second segment (O11), a + low-latency set of two segments of twelve parts each, every part its own object + (O12), a player fixture's WAI segments (O13) beside a playable classical + presentation, the WAI playlists beside their classical ones, their receipts (the + live revisions chained) and the published classical master. O10 was already the + layer set, so these took the next numbers. +- **Adaptation** (http-delivery Appendix F): `http_binding::abr`'s four reference + policies (throughput, buffer, utility over buffer, look-ahead) in integers, the budget + rule that never uses an unlabelled figure, an entry order the session applies on both + paths (the selector's order now sees the verified head), and layer depth; seeded + vectors per policy, the integer log, entry orders and depths, which `http_verify.py` + reproduces. +- **Playlists and adaptation, after review** (http-delivery §8.6, §11, Appendices F, H + and I): + - a live client walks back to the head it holds across revisions it skipped: every + receipt is also served at `mr/.json`, the head fetched again is + unchanged, and a walk is bounded (`playlist_receipt_parent_missing`, + `playlist_receipt_walk_exceeded`); no revision lists another cid for an alias or a + media sequence number an earlier one bound (`playlist_listing_rebound`); + - Ed25519 is verified strictly (`wai::sig::verify_strict`: a canonical key of large + order, `S` < L, `R` of large order compared byte for byte), by the playlist receipt + and `http_verify.py`; + - DASH adjacency is checked over an XML subset (Appendix I); `wai.mpd` takes the + classical MPD's period, timescale, `t` and `d` and leaves a gap out; MPD values are + escaped and checked as XML; + - adaptation states have stated bounds (`abr_state_out_of_bounds`) and are computed + exactly within them; under a budget only a labelled option within it is chosen; + - a gap segment carries no parts or layer lines; the session-data line has one place + in the classical master; a receipt file holds no member twice, no lone surrogate + and no number past 2^53 − 1, a grant of no joule ceiling written `null`; + `seal-playlist --grant`; the origin answers a far-future `_HLS_msn` with 400. + - an MPD is held as written: timelines in runs, never expanding `@r`, compared run by + run, at most 256 per document, and an element's attributes read in linear time + (Appendix I); + - a budget never raises the policy's choice: with nothing at or below it within the + budget, option 0 and the budget `unmet` (replacing `floor`); + - past the walk bound, a revision ahead of the head re-anchors the chain + (`reanchored`), keeping its bindings, the revisions between unsigned; bindings key a + line by its URL resolved against the playlist's (RFC 3986), a gap never withdraws a + listed segment, and a binding is kept while one of the last 256 revisions lists it. - **What review found, fixed** (http-delivery §3.1, §4, §7, §7.1, §10, §13, Appendix D): - a layer object (a `WAI1` whose capability is refinement-only) is never dispatched: it is fetched whole at refinement urgency, verified against its cid and handed to diff --git a/wai/SPEC.md b/wai/SPEC.md index 0efd4e2a2..83ab61760 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -619,7 +619,7 @@ registration and makes no conforming envelope non-conforming (§8). | `wai.neural.dcvc_fm` | DCVC-FM (feature modulation), wide single-model quality range (RGB + YUV) | a wide single-model quality range; shares the inter-frame route | research code, `neural/video_dcvc.rs` | | `wai.video.int_motion` | DETERMINISTIC INTEGER inter-frame — I-frame + integer block motion-compensated P-frames + integer-quantized residual over rANS, no float/ONNX; motion is whole-pel or **half-pel** (`mv_precision`), the latter predicting at the half-grid by integer bilinear interpolation (`(a+b+1)>>1`, `(a+b+c+d+2)>>2`) so real footage — which pans a non-integer number of pixels per frame — costs fewer residual bits while staying byte-exact | byte-identical frame-by-frame on every machine (*frame-state-equivalence*, a per-frame portable hash); the temporal extension of the integer contract, where a float inter-frame decode desyncs down the GOP | payload `WIV1`, keyframe 0; `int_video.rs` (the encode tools write a JSON form a converter reads) | | `wai.video.int_hyper` | NEURAL-KEYFRAME inter-frame — the I-frame is the LEARNED integer codec `int_hyper` decoded byte-exact, the P-frames are integer block-MC residuals on top | a learned keyframe you can put in a receipt + integer inter-frames; whole decode integer, byte-identical everywhere | payload `WIV1`, keyframe 1 (a nested `WIH1`); `int_video::decode_pframes_typed` ∘ `int_hyper_synth::decode_conditional_checked` | -| `wai.video.manifest` | ATTESTED PER-SESSION MANIFEST — the OTT delivery layer, not a codec: a personalised HLS manifest (ads stitched at `EXT-X-DISCONTINUITY`, blackouts + geo enforced) rendered as a **pure function of the session *class*** `(content, entitlement, geo, blackout set, ad pod, window)` rather than the viewer, and sealed in an Ed25519 receipt that binds the JCP grant (capability, joule ceiling, `funds_ceiling` = ad budget), the composition, the stitched ads and the policy applied. A receipt over a WAI playlist MAY carry `render` and `extras`, the rendering parameters and per-segment lines that playlist adds, both bound through `manifest_hash` ([http-delivery](extensions/http-delivery.md) §8.6) | **manifest-equivalence**: `manifest_hash == hash(render(segments))` and `session_class_hash` are recomputable by anyone holding the bytes, so the ads, blackouts and compose-work stop being dashboard claims; class-determinism means one generation serves every viewer of the class, and `uj_per_viewer` is the amortised energy that a 1:1-unique generator cannot report below its per-viewer cost. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_manifest.rs` | +| `wai.video.manifest` | ATTESTED PER-SESSION MANIFEST — the OTT delivery layer, not a codec: a personalised HLS manifest (ads stitched at `EXT-X-DISCONTINUITY`, blackouts + geo enforced) rendered as a **pure function of the session *class*** `(content, entitlement, geo, blackout set, ad pod, window)` rather than the viewer, and sealed in an Ed25519 receipt that binds the JCP grant (capability, joule ceiling, `funds_ceiling` = ad budget), the composition, the stitched ads and the policy applied. A receipt over a WAI playlist MAY carry `render` and `extras`, the rendering parameters and per-segment lines that playlist adds, both bound through `manifest_hash` and through a render digest the signature covers ([http-delivery](extensions/http-delivery.md) §8.6) | **manifest-equivalence**: `manifest_hash == hash(render(segments))` and `session_class_hash` are recomputable by anyone holding the bytes, so the ads, blackouts and compose-work stop being dashboard claims; class-determinism means one generation serves every viewer of the class, and `uj_per_viewer` is the amortised energy that a 1:1-unique generator cannot report below its per-viewer cost. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_manifest.rs` | | `wai.video.ssai` | ATTESTED SERVER-SIDE AD INSERTION — a **deterministic** ad decision (greedy by price within the avail duration and the funds remaining) plus a per-impression Ed25519 receipt binding the candidate pod, the manifest it belongs to, the served `creative_hash`es and the VAST quartile each reached | **spend-equivalence**: `budget_spent_micros` is recomputed from the *billable* impressions and rejected if it exceeds the grant's `funds_ceiling` — an overspent receipt fails verification; every creative and quartile is checkable, so a buyer and seller reconcile impressions from the same bytes instead of two logs that only a dashboard joins. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_ssai.rs` | | `wai.video.keys` | ATTESTED CONTENT-KEY RELEASE — the DRM licensing decision as a record: every key release, and every refusal, is an Ed25519 receipt binding the KID, the DRM system, the entitlement demanded and the window enforced, with a BLAKE3 `key_commit` proving *which* key was released without carrying it | **decision-equivalence**: `verify()` re-runs the release policy over the receipt's own `(grant capability, entitlement, issued, expiry)` and rejects any misreported outcome, and enforces granted-iff-committed — so "keys went only to entitled, unexpired sessions" is checkable against the signed log, not asserted. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_keys.rs` | | `wai.video.package` | DETERMINISTIC PACKAGING + ATTESTED SEGMENTMAP — a source sliced into content-hashed segments on GOP boundaries; the ordered `(index → content_hash)` map is a signed Merkle commitment, and identical content (ads, bumpers, replays, simulcast overlap) dedups to one stored/encoded copy | **map-equivalence + verifiable dedup**: `segment_map_root` lets a `wai.video.manifest` reference be checked against the package that produced it, and `verify()` recomputes `unique_segments`/`stored_bytes` from the segments so the storage/encode saving cannot be overstated; `uj_per_unique_segment` is the encode-once energy. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable. Version 2 ([packaging](extensions/packaging.md) §9) binds the policy digest and one stage index per component, names each object by its jwp-receipts §2.1 content hash, and covers every media class a packager packs; the string keeps `video` for history | `wai_video_package.rs`, `package.rs` | diff --git a/wai/extensions/http-delivery.md b/wai/extensions/http-delivery.md index 38dd2506b..1b907d1e0 100644 --- a/wai/extensions/http-delivery.md +++ b/wai/extensions/http-delivery.md @@ -5,9 +5,11 @@ > component of a `WAI2`, receipt, key and playlist sidecars, cache rules, delivery > order and deadlines, and HLS / DASH adjacency for on-demand, live and low-latency > live. No envelope byte and no manifest member changes. The reference implementation -> is in `wai-rs`: `http_binding` (feature `http`) for §3–§7 and the origin side of §5 -> and §9, and the `wai http` commands of the `wai` tool. Keywords **MUST**, -> **MUST NOT**, **SHOULD**, **MAY** are RFC 2119 / RFC 8174. +> is in `wai-rs`: `http_binding` (feature `http`) for §3–§7, the origin side of §5 +> and §9, the playlists of §10.4 and §11 and the adaptation policies of Appendix F; +> `wai_video_manifest` (feature `video_manifest`) for the playlist receipts of §8.6; +> and the `wai http` commands of the `wai` tool. Keywords **MUST**, **MUST NOT**, +> **SHOULD**, **MAY** are RFC 2119 / RFC 8174. ## 1. Scope @@ -456,16 +458,64 @@ held, and dispatch continues as [SPEC](../SPEC.md) §4 and §7.1 state. ### 8.6 Playlist receipts -A WAI playlist (§11) is authenticated by a manifest receipt (`wai.video.manifest`, +A WAI media playlist (§11) is authenticated by a manifest receipt (`wai.video.manifest`, [SPEC](../SPEC.md) §5) whose `manifest_hash` is the BLAKE3 of the playlist bytes, -served immutable at `mr/.json` resolved against the playlist URI. Each -revision of a live playlist has its own receipt, whose `parent_receipt_hash` names the -previous one. A playlist receipt MAY carry `render`, the rendering parameters of the -playlist it signs (version, media sequence, discontinuity sequence, part target, -whether it ends), and `extras`, the per-segment lines that rendering adds -(discontinuities, program date-time, parts, layer lines). When both are absent, the -playlist is rendered as before. `manifest_hash` covers the rendered bytes, so both are -bound by the signature through it. +served immutable at `mr/.json` resolved against the playlist URI, in the +form of Appendix H, and with the same bytes at `mr/.json`, its receipt +hash (Appendix H), so a receipt can be fetched by the link a later one names. Each +revision of a live playlist has its own receipt, whose `parent_receipt_hash` is the +receipt hash of the previous revision's receipt, under the same key. A playlist receipt MAY carry `render`, the rendering parameters of the +playlist it signs (version, media sequence, discontinuity sequence, target duration, +part target, part hold-back, blocking reload, whether it ends), and `extras`, the +per-segment lines that rendering adds (discontinuities, program date-times, parts, +layer lines, a preload hint), rendered as Appendix G states. When both are absent, the +playlist is rendered as before. `manifest_hash` covers the rendered bytes, and the +signature also covers a digest of `render` and `extras` themselves (Appendix H), so a +rendering has one form. A public playlist is sealed with the entitlement `public`, no +geography, blackout or ad pod, and its media sequence as the window, under the grant +its publisher holds (`grant`: a grant of no ceilings when it holds none). + +This revision signs media playlists only. A receipt binds neither the multivariant +playlist that names the media playlist nor a `wai.mpd`, and nothing binds +`class.content_id` to the presentation: it is the publisher's label, signed as given, +so a client learns from it only what the signer chose to write. A client that needs the +multivariant playlist's variants, or the content's identity, authenticated holds them +from elsewhere. + +A client checks a playlist against a receipt file in this order: + +1. the file reads as Appendix H's form (`playlist_receipt_malformed`); +2. its name is `.json` (`receipt_name_mismatch`); +3. the playlist's BLAKE3 is `manifest_hash`, and rendering the receipt's segments, + `render` and `extras` gives the playlist's bytes (`playlist_receipt_mismatch`); +4. the receipt verifies (`playlist_receipt_invalid`): its signature, and every value it + recomputes (Appendix H). + +A client of a live playlist holds a chain: its head, the latest receipt that verified +and chained, and what the receipts in it bound. The first receipt it verifies is the +head. A later revision's receipt that is the head itself (a reload with no new +revision) leaves the chain as it is. Any other becomes the head only when it chains to +the head: it is under the head's key (`playlist_receipt_unchained`), and following +`parent_receipt_hash` from it reaches the head's receipt hash. Each parent on the way is +fetched from `mr/.json` and checked in order: it is served +(`playlist_receipt_parent_missing`); it reads as Appendix H's form +(`playlist_receipt_malformed`); its receipt hash is the link it was fetched by +(`receipt_name_mismatch`); it verifies (`playlist_receipt_invalid`); and it is under the +same key (`playlist_receipt_unchained`). A walk that reaches a receipt with no parent +before the head does not chain (`playlist_receipt_unchained`), so a revision older than +the head never does. A client fetches at most 64 parents for one revision, and MAY hold +a lower bound. When the head is further back than that, the client re-anchors the chain +at the revision in hand if that revision is ahead of the head (its media sequence number +after its last complete segment is greater than the head's): the revision becomes the +head (`reanchored`), the chain keeps what it bound, and the revisions between the old +head and the new one are not signed: no part they alone list is held at **signed**. A +revision past the bound and not ahead of the head is refused +(`playlist_receipt_walk_exceeded`), so an older or stale revision never moves the chain +back; the head fetched again, however often, changes nothing. Then the walked receipts, +oldest first, and the new head bind what they list (§11.5), a re-anchored head alone; one +that lists another cid for a URL or a media sequence number the chain bound, or a gap +where it bound an object, is refused (`playlist_listing_rebound`). A refused revision +leaves the chain as it was. ## 9. Cache rules @@ -566,7 +616,9 @@ request abandoned at its deadline, or whose response §7.1 refuses, leaves the layer not delivered (§10.5). A WAI media playlist lists the layer objects of a segment or part after its `EXTINF` -or `EXT-X-PART` line, in the order a sink applies them: +or `EXT-X-PART` line, in the order a sink applies them: directly after it, before the +segment's URI, with only other `#WAI-LAYER` lines (and blank lines) between, and `N` +increasing from line to line (`playlist_malformed` otherwise): ``` #WAI-LAYER:N=<1..255>,URI=".wai#n=…" @@ -606,14 +658,49 @@ arrived after its units were due (staged-delivery §10.2). not a format of RFC 8216 §3.1, so it is not an HLS presentation and MUST NOT be offered to a client that does not implement this extension. Each `EXT-X-STREAM-INF` carries `CODECS` (the union, §4.2), `X-WAI-FLOOR` (the floor), and `BANDWIDTH` and - `AVERAGE-BANDWIDTH` computed from the objects' `envelope_len` over their durations - (peak and mean of `8·envelope_len/duration`), never stated by hand. + `AVERAGE-BANDWIDTH` computed from the objects' `envelope_len` over their durations, + never stated by hand. A segment's bytes are the `envelope_len` of its object and of the + layer objects listed with it; a segment marked `EXT-X-GAP` counts nothing, and a live + variant with no segment yet is counted by its parts. With each duration in seconds as + exactly as the packager holds it, `BANDWIDTH` is the largest `⌈8·bytes/duration⌉` of + any segment, which is never below RFC 8216's peak over a run of segments, and + `AVERAGE-BANDWIDTH` is `⌈8·Σbytes/Σduration⌉` (from a playlist's milliseconds, + `⌈8000·bytes/duration_ms⌉`). Nothing in `wai.m3u8` needs `EXT-X-VERSION`; a publisher + MAY write one, `EXT-X-INDEPENDENT-SEGMENTS` when every segment it lists decodes + without the one before it, and further `EXT-X-SESSION-DATA` lines of its own. +- A WAI media playlist takes the classical one's version, target duration, playlist + type and `EXT-X-INDEPENDENT-SEGMENTS`, and never its `EXT-X-MAP`: a WAI object has no + initialization section. A segment a publisher does not offer as a WAI object at that + point (a class that takes it from the classical presentation) is marked `EXT-X-GAP`, + and a sink takes the classical segment there. - `wai.mpd` is a separate MPD with one `AdaptationSet` per WAI stream, `mimeType="application/wai"`, `@codecs` (the union), an `EssentialProperty schemeIdUri="tag:transaction.science,2026:wai/http-delivery"` so a client that does not know it ignores the set, and a `SupplementalProperty schemeIdUri="tag:transaction.science,2026:wai/floor"` whose - `value` is the floor. + `value` is the floor. The reference `wai.mpd` is static: one `Period` with the + classical one's `@id` (none when it has none) and `@start` (in seconds, without a + fraction when whole and with three decimals otherwise), `mediaPresentationDuration` + the timeline's length in seconds with three decimals, `minBufferTime` its longest + segment rounded up to whole seconds, and in each `Representation` a `SegmentList` with + the classical timeline's `@timescale` whose `SegmentTimeline` has one `S` per segment + it offers, with the classical `t` and `d`, and one `SegmentURL` per such segment naming + its object. A segment the WAI media playlist marks `EXT-X-GAP` is left out, `S` and + `SegmentURL` both: a `Representation` whose timeline leaves out a segment offers none + there. A layer's `Representation` names the one below it by `@dependencyId`. The + classical timeline is the classical MPD's, read as Appendix I states: by default its + first `Period` and that period's first `AdaptationSet` with a timeline. Beside a + presentation with no classical MPD, it is the classical HLS media playlist's at + timescale 1000, `t` from 0, in a `Period` starting at 0. Attribute values are escaped, + tab, LF and CR as character references, and a value holding a character that is not + an XML `Char` is not written (`playlist_malformed`). +- The published classical multivariant playlist is its original, byte for byte, or the + original with the session-data line (its own line end, LF or CR LF) inserted at one + place: after the first line and the `EXT-X-VERSION` and `EXT-X-INDEPENDENT-SEGMENTS` + lines that directly follow it, before any other line. A packager MUST NOT publish one + that differs otherwise (`classical_changed`): the line anywhere else, between an + `EXT-X-STREAM-INF` and its URI among them, is refused, as is an original whose line + before that place does not end. ### 11.2 Alignment @@ -621,12 +708,25 @@ A WAI media playlist is **adjacent** to a classical media playlist when, segment segment, they have the same `EXT-X-MEDIA-SEQUENCE`, `EXT-X-DISCONTINUITY-SEQUENCE`, `EXTINF` durations to the millisecond, discontinuities and `EXT-X-PROGRAM-DATE-TIME` values, and, where both carry parts, the same `EXT-X-PART-INF:PART-TARGET` and the same -part durations to the millisecond in each segment. In DASH: the same `Period@id`s and -`@start`s, and a `SegmentTimeline` with the same timescale, `t` and `d`. The WAI +part durations to the millisecond in each segment. In DASH, both read as Appendix I +states: each WAI `Period` has a classical `Period` with the same `@id` and `@start` +(compared in milliseconds), and has an `AdaptationSet`; each WAI `AdaptationSet` has a +timeline; and each of its timelines is not empty, has the `@timescale` of a timeline of +that classical period, and lists only that timeline's segments, each with its `t` and +`d`, in order (a segment left out is a gap). The WAI segment (or part) with a sequence number covers exactly the presentation interval of the classical one, so a sink can move between them at any boundary. A packager MUST NOT publish a WAI playlist that breaks this (`adjacency_misaligned`). `#WAI-LAYER` lines -do not affect alignment. +do not affect alignment, nor does `EXT-X-GAP`: a gap segment keeps its interval. + +A duration is compared in milliseconds: its decimal value, read exactly, rounded to the +nearest millisecond with a half rounded up (so `4.00008` and `4.000` are both 4000). A +missing `EXT-X-MEDIA-SEQUENCE` or `EXT-X-DISCONTINUITY-SEQUENCE` is 0, and either after a +segment is malformed. Part durations are compared in each segment both playlists list +parts for, and in the open segments (§11.4) when both have one; one side listing parts +the other has stopped listing is aligned. A playlist that does not read as a media +playlist — a delta update (`EXT-X-SKIP`) and a multivariant playlist among them — is not +compared (`playlist_malformed`). ### 11.3 Live at segment granularity (Live-S) @@ -652,7 +752,10 @@ Each part is its own object, served under an alias, and listed with its cid: ``` - The alias of a part names those bytes for its lifetime (§5.2). A preload hint names - the alias before the part exists; the origin holds the request until it does. + the alias before the part exists; the origin holds the request until it does. An + origin answers at once with 400 a blocking reload whose `_HLS_msn` is more than two + past the last segment of its current playlist, or that has `_HLS_part` without + `_HLS_msn`, as the HLS second edition requires, rather than holding it. - A client presents a part only after its bytes hash to the cid that a later playlist revision lists for that alias. Issued together, the hint request and a blocking reload (`_HLS_msn`, `_HLS_part`) complete when the part is complete, so the floor is @@ -665,12 +768,35 @@ Each part is its own object, served under an alias, and listed with its cid: it serves every client behind the live edge. Parts stop being listed as the HLS specification permits, and an origin MAY then delete them. - An `INDEPENDENT=YES` part is one whose envelope decodes without another object. +- A WAI Live-P playlist takes the classical playlist's target duration, part target and + blocking reload, and its part hold-back (three part targets when it states none). A + segment's parts precede its `EXTINF`, its discontinuity and date-time precede its + first part, the open segment's parts follow the last segment's URI, and the preload + hint is the last line (Appendix G), as the HLS second edition places them. A hint + with no open part yet follows the last segment. ### 11.5 Live to on demand Every object is immutable from the moment it is listed, so a live playlist becomes on-demand by appending `EXT-X-ENDLIST`; no object is rewritten. +A revision MUST NOT list another cid for a URL an earlier revision listed (an alias, or +any part, segment or layer URI), nor another object at a media sequence number an +earlier revision listed an object at, nor a gap there: a listed segment is not +withdrawn (a gap segment lists no object at its number). A URL is the line's URI +resolved against the playlist's URL (RFC 3986 §5.2) and normalized (§6.2.2: the scheme +and host in lower case, percent-encodings' hexadecimal digits in upper case, unreserved +characters decoded, dot segments removed), without its fragment, so two spellings of +one URL are one binding. A client keeps both bindings from the revisions it accepts, a +URL's cid being the first one listed with it (§5.2), and refuses a revision that lists +another (`playlist_listing_rebound`), taking none of its listings. A chain of receipts +binds the same way over the receipts in it (§8.6). + +A client keeps a binding while one of the last 256 revisions it took lists it, and MAY +keep it longer; what it holds is then bounded by what those revisions list, however +long the playlist runs. A rebinding of a URL or a sequence number that none of the last +256 revisions listed is not detected. + ### 11.6 Playlist authentication A WAI playlist SHOULD be authenticated by its receipt (§8.6). A client that verifies it @@ -681,7 +807,10 @@ each object is still bound to the cid the playlist names. A client reports which multivariant playlist names), **signed** (that playlist's receipt, or the object's receipt, verified), and the confirmation level of [jwp-receipts](jwp-receipts.md) §7.4. In Live-P a client MAY present at **listed** and verify the playlist receipt after -presenting; it MUST NOT report the part as signed until it has. +presenting; it MUST NOT report the part as signed until it has. A part is listed by the +first accepted revision whose playlist names its alias with a `cid`; it is signed when a +receipt in the chain the client holds (§8.6), the parents walked to it included, binds +the alias to that cid. A WAI segment whose payload is a classical segment's media relates to the classical package's segment map (`wai.video.package`) by sequence number: the package's @@ -760,11 +889,16 @@ is a share of sessions, not of devices or people. | `component_digest_absent` | §7 step 5 (a reason to fetch whole, never a skip) | | `component_digest_mismatch` | §7 step 6 ([SPEC](../SPEC.md) §7.1's reason) | | `object_digest_mismatch` | §7 step 7, or any whole fetch | -| `receipt_name_mismatch`, `receipt_not_about_object` | §8.1, §8.2 | +| `receipt_name_mismatch`, `receipt_not_about_object` | §8.1, §8.2, §8.6 | | `codecs_malformed`, `codecs_not_media`, `codecs_candidate`, `codecs_no_common_capability`, `codecs_undeclared`, `codecs_layer_unbound` | §4 | | `base_unanswered` | §10.1 | | `not_a_layer`, `layer_tag_malformed` | §10.4 | | `adjacency_misaligned` | §11.2 | +| `playlist_malformed` | §10.4, §11.2, Appendix G, Appendix I | +| `classical_changed` | §11.1 | +| `playlist_receipt_malformed`, `playlist_receipt_mismatch`, `playlist_receipt_invalid`, `playlist_receipt_unchained`, `playlist_receipt_parent_missing`, `playlist_receipt_walk_exceeded` | §8.6 | +| `playlist_listing_rebound` | §8.6, §11.5 | +| `abr_state_out_of_bounds` | Appendix F | Selection, pin, receipt, key and module reasons are those of [SPEC](../SPEC.md) §3.1 and §7.1 and [jwp-receipts](jwp-receipts.md) §3, §7.7 and §7.9, and the layer reasons @@ -780,7 +914,13 @@ step 5 states. code with `wai-rs`, report the same outcome for every case, and for every case of a seeded differential over inputs outside the corpus (`wai_http_vectors fuzz`): every `Content-Range`, `codecs` value, `#WAI-LAYER` line, manifest, member type, encryption, - `refines` and layer sequence it generates. + `refines` and layer sequence it generates. For §8.6, §10.4, §11 and Appendix F the + corpus holds WAI playlists rendered from their inputs beside their classical ones, + their receipts (a live playlist's chained, and walked across revisions a client + skipped or fetched again), a published classical master and a + playable classical presentation for players, and the policies' choices; the + differential adds playlists, adjacent pairs, published masters, renderings, receipt + files, adaptation states and MPD pairs. - [`cache-conformance/`](../cache-conformance/) checks §9 against HTTP caches and reports, per cache label and version, the configuration that meets it. Products are not named in published reports. @@ -900,9 +1040,222 @@ request ends at `covered_len`. Without `h`, the first request is 16,369 bytes. | RFC 9530 §2, §3 · RFC 9218 §4.1, §4.2, §5 | read | | RFC 8216 §3.1, §3.3, §4.3.4.2, §4.3.4.4 · the HLS second-edition draft (its date only) | read | | RFC 8006 `MI.*` objects | read | -| The HLS second-edition draft's `EXT-X-PART`, `EXT-X-PRELOAD-HINT` and blocking-reload clauses | not re-read here: §11.4 is to be checked against them | +| The HLS second-edition draft (draft 22): `EXT-X-PART` (its placement before the parent segment's URI, and the media segment tags before its first part), `EXT-X-PART-INF`, `EXT-X-SERVER-CONTROL` (`PART-HOLD-BACK` required with parts, at least twice the part target), `EXT-X-PRELOAD-HINT` | read | +| The HLS second-edition draft's blocking-reload clauses and when parts stop being listed | not re-read here: §11.4 is to be checked against them | | CTA-5005-B | title and date only | | The BLAKE3 specification | the tree rule is checked by the reference implementation against whole-input hashing | | RFC 8615, RFC 8785, RFC 4151, RFC 5234, ISO/IEC 14496-12 and -15, ISO/IEC 23000-19, ISO/IEC 23009-1, the Fetch Standard (CORS-safelisted `Range`, `priority`), WebCodecs, Media Capabilities | not re-read here | Every row not read is read before this extension leaves Draft. + +## Appendix F — Reference adaptation policies (informative) + +A client MAY order a `WAI2`'s entries by deployer policy ([SPEC](../SPEC.md) §7.1 +step 2) and choose how many layers it fetches (§10.4). The reference client's policies +choose among options of increasing size from measured delivery, in integers only, so +every implementation chooses the same; the corpus pins their choices. + +**Inputs.** A state holds the buffer `b` (ms), samples `(at, bytes, ms)` oldest first, +the last choice, each option's bytes `S_k`, each option's energy figure where a receipt +labels it with its acquisition class ([energy-measurement](energy-measurement.md) §2; +none otherwise), a joule budget if held, and the media duration `D` one option covers +(ms). Divisions round down unless stated; `⌈x/y⌉` rounds up. + +**Bounds.** Each value lies within its field's type: `b`, `D`, and the policies' `R`, +`C`, `v` and `M` below 2^32; `n` and `h` below 2^8; `p` below 2^16; each sample's +`at`, `bytes` and `ms`, the last choice, each `S_k`, each figure and the budget below +2^64. A state holds at most 256 options (and at most 256 figures), and a layer depth is +chosen over at most 256 objects, the bytes and the figure through each depth below 2^64. +A state or a policy beyond these is refused (`abr_state_out_of_bounds`). Within them +every quantity below is computed exactly, never saturated: a rate or a throughput +reaches 2^85, the look-ahead's stall times `max_k u_k + 65536` reaches 2^103, and the +utility-over-buffer cross-multiplication reaches 2^141, so an implementation in fixed +widths compares those products in 192 bits. + +- The throughput of the last `n` samples (`n` 0 taken as 1): `⌊8000·Σbytes / Σmax(ms, 1)⌋` + b/s, 0 with no sample. +- An option's rate: `⌈8000·S_k / max(D, 1)⌉` b/s. "The highest option under `r`" is the + largest index whose rate is at most `r`, else 0. +- `log2_q16(a, b)`, `log2(a/b)` in units of 1/65536: 0 when `a ≤ b` (`b` taken as at + least 1); else with `x = ⌊65536·a/b⌋`, `e = ⌊log2 x⌋ − 16`, `f = (x >> e) − 65536`, + `i = f >> 12`, `r = f & 4095` and `T = [0, 5732, 11136, 16248, 21098, 25711, 30109, + 34312, 38336, 42196, 45904, 49472, 52911, 56229, 59434, 62534, 65536]` + (`T[i] = round(65536·log2(1 + i/16))`), it is `65536·e + T[i] + ⌊(T[i+1] − T[i])·r / 4096⌋`. +- An option's utility: `u_k = log2_q16(max(S_k, 1), max(S_0, 1))`. + +**Policies.** Each chooses an option index (0 when there is none): + +- *Throughput* `(n, p)`: the highest option under `⌊throughput(n)·p/1000⌋`. +- *Buffer* `(R, C)`: 0 when `b ≤ R`; the highest index when `C = 0` or `b ≥ R + C`; else + the highest option under `rate_0 + ⌊max(rate_top − rate_0, 0)·(b − R)/C⌋`. +- *Utility over buffer* `(v, M)`: with `q = ⌊65536·b/D'⌋`, `q_max = ⌊65536·M/D'⌋` + (`D' = max(D, 1)`), `V_n = max(q_max − 65536, 0)` and `V_d = max_k u_k + 65536`, each + option scores `N_k = ⌊V_n·v·(u_k + 65536) / (V_d·1000)⌋ − q`. If any `N_k > 0`, the + option with the largest `N_k/S'_k` among them (`S'_k = max(S_k, 1)`, compared by + cross-multiplication); else the option with the largest `N_k`. Ties go to the lower + index. This is a rule of the utility-over-buffer kind; unlike the published algorithm + of that kind, it never waits for the buffer to drain. +- *Look-ahead* `(h, p)`: with `P = ⌊throughput(5)·p/1000⌋` (0 chooses 0), `h` taken in + 1 to 5 and lowered while `K^h > 65536` for `K` options, every sequence of `h` choices, + in increasing order of the sequence read as base-`K` digits, most significant first, + is played from the buffer `b`: each choice downloads in `⌈8000·S_k/P⌉` ms, stalling by + what passes the buffer, then adds `D`. Its score is `1000·Σu − 1000·Σ|u_t − u_{t−1}|` + (the first step against the last choice, when there is one below `K`) less the stall + in ms times `(max_k u_k + 65536)`. The first choice of the first best sequence is + taken. + +**Budget.** A budget never raises the policy's choice. Without a budget the choice +stands (`not_set`). When no option has a labelled figure, the choice stands and the +budget is not applied (`not_applied`): an unlabelled figure is never used. Otherwise the +choice is the highest option at or below the policy's whose labelled figure is within +the budget: the policy's own (`within`) or one below it (`lowered`), so an unlabelled +option never escapes the budget. When no option at or below the policy's has a labelled +figure within it, the choice is option 0 and the budget is not met (`unmet`): a client +fetches the least it can, and reports that the budget was not held. A figure past the +options names none. + +**Entry order.** Within a `WAI2`, the selectable entries whose capability the sink has +are grouped by capability in order of first appearance; a group's options are its +entries by component length (then table index), each option's figure the entry's. The +group is ordered as the choice under the budget, then the options below it from the +nearest down, then those above it from the nearest up; every other entry follows in +table order. Selection then runs unchanged on the head and on the whole envelope, so +the pick is the same on every path (§7.2). + +**Layer depth.** Over a base and its layers, option `k` fetches through layer `k`: its +bytes are the `envelope_len`s through it, and its figure the sum of the labelled figures +through it (none when any is unlabelled). + +## Appendix G — The rendering a playlist receipt signs + +A WAI media playlist is rendered from its segments (each a duration in milliseconds, a +URI, and whether it is an ad), the parameters of §8.6 and the extras, with lines ending +in LF and every duration written as seconds with three fractional digits: + +1. `#EXTM3U`; `#EXT-X-VERSION:`; `#EXT-X-TARGETDURATION:`, `t` the parameter or, + when it has none, the largest segment duration rounded up to whole seconds; + `#EXT-X-PART-INF:PART-TARGET=` with a part target; `#EXT-X-SERVER-CONTROL:` with + `CAN-BLOCK-RELOAD=YES` and `PART-HOLD-BACK=`, those that apply, in that order, + when either does; `#EXT-X-MEDIA-SEQUENCE:`; `#EXT-X-DISCONTINUITY-SEQUENCE:` + when it is not 0; `#EXT-X-PLAYLIST-TYPE:` with a type; + `#EXT-X-INDEPENDENT-SEGMENTS` when it applies; `#EXT-X-MAP:URI=""` with a map + (a classical playlist's; a WAI playlist has none). +2. Each segment: `#EXT-X-DISCONTINUITY` once, for an ad or a discontinuity; + `#EXT-X-PROGRAM-DATE-TIME:`; each part as + `#EXT-X-PART:DURATION=[,INDEPENDENT=YES],URI=""` followed by its layer lines; + `#EXT-X-GAP` for a gap; `#EXTINF:,`; the segment's layer lines; its URI. A layer + line is `#WAI-LAYER:N=,URI=""`. +3. The open segment, when there is one extra more than segments: its discontinuity, + date-time and parts, written as in step 2. +4. `#EXT-X-PRELOAD-HINT:TYPE=PART,URI=""` from the last extra; `#EXT-X-ENDLIST` + when the playlist ends. + +Extras are none, one per segment, or one more. A rendering is refused +(`playlist_malformed`) when it would not read back as its inputs: a URI that is empty, +begins with `#` or holds CR or LF (or `"`, where quoted, the map's among them); a +program date-time outside +ASCII letters, digits and `:.+-`; layer numbers outside 1 to 255 or not increasing; +parts with no part target; a part target with no part hold-back of at least twice it; +a preload hint on any but the last extra, or in a playlist that ends; a gap segment +with parts or layer lines (it offers no object to divide or refine); an open segment +in a playlist that ends, with layer lines or a gap of its own, with a discontinuity or date-time +before any part, or with neither a part nor a hint; version 0. + +## Appendix H — The playlist receipt file + +`mr/.json` is one JSON object with these members and no other: +`kind` (`"wai.video.manifest"`), `manifest_hash`, `session_class_hash`, `class` +(`content_id`, `entitlement`, `geo`, `blackouts`, `ad_pod_id`, `window`), `segments` +(each `index`, `kind` — `content` or `ad` —, `uri`, `duration_ms`, `ad_id`), +`merkle_root`, `work_compose_ops`, `viewers_served`, `joules_micro`, the acquisition +class's members of [energy-measurement](energy-measurement.md) §2.7 when the figure is +metered, `grant` (`grant_hash`, `capability`, `joule_ceiling_micro`, `funds_ceiling`), +`parent_receipt_hash`, `signer_pubkey`, `signer_id`, `sig`, and `render` (`version`, +`media_sequence`, `discontinuity_sequence`, `target_duration_s`, `part_target_ms`, +`part_hold_back_ms`, `can_block_reload`, `end_list`, `playlist_type` — `VOD`, `EVENT` or +null —, `independent_segments`, `map_uri`) with `extras` (each `discontinuity`, `gap`, +`program_date_time`, `parts` — each `duration_ms`, `independent`, +`uri`, `layers` —, `layers` — each `n`, `uri` —, `preload_hint`), the two together or +not at all. Hashes are lowercase hexadecimal; a number is an unsigned integer of at +most 2^53 − 1 written as digits alone, and a writer refuses to seal a receipt it cannot +write so. A grant's `joule_ceiling_micro` is `null` when the grant sets no joule ceiling +(it signs as 2^64 − 1), and `funds_ceiling` `null` when it sets no funds ceiling. The +file is read as I-JSON (RFC 7493): no object holds a member name +twice, at any depth and with names compared after their escapes are read, and no +string holds a `\u` escape that leaves a surrogate unpaired; either is +`playlist_receipt_malformed`, so no two readers can read two receipts from one file. + +Below, `‖` concatenates, integers are big-endian of the stated width in bytes, and a +string is its UTF-8 length in 4 bytes, then its bytes: + +- a segment's leaf: BLAKE3 of `"wai:video-manifest-seg\x01"` ‖ `index` (8) ‖ 1 for + content or 2 for an ad (1) ‖ `duration_ms` (8) ‖ `uri` ‖ (1 ‖ `ad_id`, or 0); +- `merkle_root`: [jwp-receipts](jwp-receipts.md) §2.2's root over the leaves; +- `session_class_hash`: BLAKE3 of `"wai:video-manifest-class\x01"` ‖ `content_id` ‖ + `entitlement` ‖ `geo` ‖ `ad_pod_id` ‖ the number of distinct `blackouts` (4) ‖ each, + in byte order ‖ `window` (8); +- the render digest: BLAKE3 of `"wai:video-manifest-render\x01"` ‖ the RFC 8785 form of + `{"render": …, "extras": […]}`; +- the signed bytes: `"wai:video-manifest-receipt\x01"` ‖ `manifest_hash` ‖ + `session_class_hash` ‖ `merkle_root` ‖ `work_compose_ops` (8) ‖ `viewers_served` (8) ‖ + `joules_micro` (8) ‖ the class's tag and evidence of + [energy-measurement](energy-measurement.md) §2.7, or 0 unmetered ‖ `grant_hash` ‖ + `capability` ‖ `joule_ceiling_micro` (8) ‖ (1 ‖ `funds_ceiling` (8), or 0) ‖ + (1 ‖ `parent_receipt_hash`, or 0) ‖ the number of segments (8) ‖ each leaf ‖ + `signer_pubkey` ‖ `signer_id`, and with a rendering, 1 ‖ the render digest; +- the receipt hash: BLAKE3 of `"wai:video-manifest-receipt-id\x01"` ‖ the signed bytes ‖ + `sig`. + +A receipt verifies when: the leaves give `merkle_root`; `class` gives +`session_class_hash`; without `render` there are no `extras`, and with it the rendering +reads back (Appendix G) and has an RFC 8785 form; the BLAKE3 of the rendering is +`manifest_hash`; `work_compose_ops` is the number of segments and `viewers_served` is +at least 1; the segment at position `i` has `index` `i`, and `ad_id` exactly when it is +an ad; `joules_micro` and the class are both present or the figure is the unmetered 0; +and `sig` is an Ed25519 signature (RFC 8032) by `signer_pubkey` over the signed bytes, +verified strictly: `signer_pubkey` decodes (§5.1.3) and is the one encoding of its point +(`y` < p, and no sign bit when `x` is 0), and neither it nor `R` is of small order +(`[8]P` is not the identity); `S` < L; and the encoding of `[S]B − [k]A` equals `R` +byte for byte, so an `R` that is not its point's one encoding never verifies. A verifier +that accepts a key or an `R` this refuses accepts a signature any party can make: under +a key of small order, `(R, S) = (B, 1)` verifies over every message. + +## Appendix I — Reading an MPD + +An MPD is read as XML 1.0 in this subset, and refused (`playlist_malformed`) outside it: + +- UTF-8, with an optional byte order mark; CR LF and a lone CR are read as LF (XML 1.0 + §2.11), and every character is an XML `Char`; +- an optional XML declaration first (``); before and after the root element, and among content, comments (with no + `--` inside) and white space; no document type declaration, CDATA section or other + processing instruction; +- names of ASCII letters, digits, `_`, `:`, `-` and `.`, not beginning with a digit, + `-` or `.`; an element's local name is its name after its last `:`; +- attribute values in `"` or `'`, holding no `<`, a literal tab or LF read as a space + (XML 1.0 §3.3.3); no attribute twice in an element; +- references, in values and in text, to `amp`, `lt`, `gt`, `quot` and `apos`, and + character references (`&#` and at most 10 decimal digits, or `&#x` and at most 8 + hexadecimal digits) to an XML `Char`; no `]]>` in text; +- elements nested at most 64 deep, and nothing after the root element but comments and + white space. + +Text is not read. The root's local name is `MPD`, with at least one `Period`. A +`Period`'s `@start` is an `xs:duration` of days, hours, minutes and seconds +(`P[nD][T[nH][nM][n[.n]S]]`, with at least one of them, in digits alone), read in +milliseconds as §11.2 reads a duration; absent, it is 0 for the first period and refused +for a later one. An `AdaptationSet`'s timelines are the `SegmentTimeline` of its own +`SegmentTemplate` or `SegmentList`, and of each of its `Representation`s'. A timeline's +`@timescale` is that element's, else that of the same kind of element in the +`AdaptationSet` and then the `Period`, else 1; 0 is refused. Each `S` has `@d`, a +positive integer, and may have `@t` (by default the end of the `S` before it, 0 for +the first) and `@r` (by default 0: the segment repeated `@r` more times). `@t`, `@d`, +`@r` and `@timescale` are digits alone; a segment whose end, `@t + @d·(@r + 1)` for the +last of a run, passes 2^64 − 1 is refused, and so is a document of more than 256 +timelines. Other elements and attributes are not read. + +A reader holds a timeline as it is written, one run per `S`, and never expands `@r`: +what it holds is bounded by the document's size, whatever `@r` says. §11.2's comparison +runs run by run: each step passes a WAI run or a classical one, so it is linear in the +runs of the two timelines. The names an element's attributes have are kept in a set, so +an element is read in time linear in its attributes. diff --git a/wai/extensions/packaging.md b/wai/extensions/packaging.md index 4abc02d86..56dbdc04b 100644 --- a/wai/extensions/packaging.md +++ b/wai/extensions/packaging.md @@ -654,14 +654,16 @@ codecs string *F* and maximum frame size `FW×FH`: - `hls/classic/master.m3u8`: `#EXTM3U`, `#EXT-X-VERSION:7`, `#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-STREAM-INF:BANDWIDTH=,AVERAGE-BANDWIDTH=,CODECS="F",RESOLUTION=FWxFH`, - `floor-.m3u8`. + `../classic/floor-.m3u8`: a URI that names the same playlist from every class's + directory, so each class's master differs from this one only by the session-data + line, as [http-delivery](http-delivery.md) §11.1 and §11.8 require. - `hls/classic/floor-.m3u8`: `#EXTM3U`, `#EXT-X-VERSION:7`, `#EXT-X-TARGETDURATION:`, `#EXT-X-MEDIA-SEQUENCE:0`, `#EXT-X-PLAYLIST-TYPE:VOD`, `#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-MAP:URI="../../floor//init.mp4"`, then per piece `#EXTINF:,` and `../../floor//.m4s`, then `#EXT-X-ENDLIST`. -- `hls//master.m3u8`: the classical master's lines, with, when the class lists a - WAI object, `#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8"` - after `#EXT-X-INDEPENDENT-SEGMENTS`, and the variant's URI `../classic/floor-.m3u8`. +- `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"` + after `#EXT-X-INDEPENDENT-SEGMENTS`, and no other change. - `hls//wai.m3u8`, when the class lists a WAI object: `#EXTM3U`, `#EXT-X-VERSION:7`, `#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE=""`, @@ -671,10 +673,14 @@ 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:,`, its base as - `../../objects/.wai#n=`, then each refinement the class lists, in - entry order, as `#WAI-LAYER:N=,URI="../../objects/.wai#n="` - ([http-delivery](http-delivery.md) §10.4). + class takes it as floor only, `#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). + +Every HLS file above and every MPD below is a rendering of +[http-delivery](http-delivery.md)'s (Appendix G for a media playlist): the reference +packager writes them with the binding's one HLS and one DASH renderer. DASH, the same as two MPDs ([http-delivery](http-delivery.md) §11.1), each ``, then diff --git a/wai/http-conformance/README.md b/wai/http-conformance/README.md index 1d4630b7d..31a54dda6 100644 --- a/wai/http-conformance/README.md +++ b/wai/http-conformance/README.md @@ -4,13 +4,15 @@ The conformance corpus of [http-delivery](../extensions/http-delivery.md) (§14) served as `.wai` with their `WHP1` head proofs (§6) and ETag sidecars, and cases for §4–§7 — the head, the head proof, the response checks, the plans, selection on the head and on the whole envelope, the byte-range procedure with its fallback, the -`codecs` grammar, union, floor and support. Two implementations check every case and -must agree: +`codecs` grammar, union, floor and support; and for §8.6, §10.4, §11 and Appendix F the +WAI playlists beside their classical ones, their receipts, the live edge, and the +reference adaptation policies. Two implementations check every case and must agree: - `wai_http_vectors verify .` (also `wai http vectors verify .`), in `wai-rs`, built - with the feature `http`; + with the features `http` and `video_manifest` (the playlist receipts); - `python3 http_verify.py .`, written from the extension text and SPEC §7.1, sharing no - code with `wai-rs`. It needs only the Python standard library, and reads + code with `wai-rs`. It needs only the Python standard library (its Ed25519 check of the + playlist receipts is written from RFC 8032), and reads `../claim-conformance/_blake3.py` (BLAKE3 from its specification) and `../registry/capabilities.json` (the published registry export). @@ -29,6 +31,17 @@ must agree: | `blake3_vectors.json` | a subset of the published BLAKE3 test vectors (CC0), which `http_verify.py` checks itself against first | | `http_verify.py` | the independent verifier | | `falsify/*.patch` | defects that `wai_http_vectors verify` must catch (below) | +| `src/live/`, `src/players/`, `src/playlists/` | frozen sources of the playlist side: the integer video payloads of O12 and O13, the classical playlists the WAI ones sit beside, and the player fixture's classical master as authored | +| `playlists/o9/`, `playlists/o10/`, `playlists/o11/` | WAI media and multivariant playlists (and MPDs) over O9 (and over O9 with its second segment a gap, which `wai-gap.mpd` leaves out), O10 with its layers, and O11, each beside its classical playlist in `src/playlists/`; O9's MPDs are on the timeline of `src/playlists/o9-classical.mpd` (90 kHz, written with `@r`, beside an audio set on its own timescale) | +| `live/` | four revisions of a low-latency WAI playlist over O12 beside `src/live/classical-r*.m3u8`, and its multivariant playlist; the parts are listed under aliases `720/..wai` with their cids (the alias files are not in the corpus: the cases serve the objects under them, and `wai http origin --root . --live-edge live/wai-720.m3u8=live/wai-r0.m3u8,live/wai-r1.m3u8,live/wai-r2.m3u8,live/wai-r3.m3u8` replays the edge over HTTP, blocking reloads and hinted parts included) | +| `players/` | the player fixture: `classical/` (frozen) is a playable classical presentation, `master.m3u8` its master as published with the session-data line, and the WAI multivariant and media playlists and MPD over O13 | +| `*/mr/.json` | each WAI media playlist's receipt (§8.6, Appendix H), sealed with the corpus's test key (seed 32 bytes of `0x57`, signer `did:example:http-vectors`; it attests nothing) under a grant of no ceilings, O10's under a test grant with both ceilings; the live revisions are chained | +| `cases/live-r2-orphan.json` | the third live revision sealed with no parent: a receipt that verifies and does not chain | +| `*/mr/.json` | each receipt again under its receipt hash, where a client walking back from a later revision fetches it by its link (§8.6) | +| `cases/other-key/` | the third live revision's receipt chained to the second by hash and signed by another key (`r2.json`, and in `mr/` under its receipt hash), and the fourth revision's receipt chained to it (`r3.json`) | +| `cases/relist/` | the third live revision listing the alias of part 0 of segment 1201 with part 1's object (`wai-r2.m3u8`), sealed and chained to the second (`r2.json`, and in `mr/`); the fourth revision's receipt chained to it (`r3-after.json`); the fourth revision listing segment 1201's object at sequence 1200 (`wai-r3.m3u8`, `r3.json`); the third revision listing that alias as `./720/1201.0.wai`, with part 1's object and with its own (`wai-r2-dot*.m3u8`, `r2-dot*.json`); and the fourth revision withdrawing segment 1200 as a gap (`wai-r3-gap.m3u8`, `r3-gap.json`) | +| `cases/walk-substituted/`, `cases/walk-invalid/` | parents a walk fetches by their links: the second revision's receipt served under the third's receipt hash; and the third revision changed after it was signed, served under its receipt hash, with the fourth revision's receipt chained to it (`r3.json`) | +| `cases/sig-*.json` | the O9 playlist's receipt re-signed at the edges of Ed25519 (Appendix H): the identity key, canonical and respelled `y` = p + 1, under `(R, S) = (B, 1)`; under the key B (scalar 1), `R` the identity, canonical and respelled, with `S` = k, so the cofactorless equation holds point for point; and under the key B a signature the strict rule accepts (`sig-key-base-point.json`) | ## Objects @@ -54,6 +67,16 @@ must agree: | OPf, OPe, OPw | a `WAI1` naming `wai.text.zstd` whose `refines` is `false`, `{}`, and well formed | a payload carrying `refines` is an ordinary object: dispatched, described by its capability, `not_a_layer` when listed as a layer (§10.4) | | ORN | an AV1 base most of the object and a refinement component stating no `sha256` | the refinement dropped (`component_digest_absent`) on the whole path and the range path alike | | `bad/OK.wai`, `bad/OKN.wai` | a string key id; a numeric `kind` | the manifest's member types | +| O11a, O11b, O11bL1 | the staged-delivery pair of `../staged-conformance/cases/live_multi_base/`: two `WAI1` int_motion bases of two frames each, and a layer over the second | `#WAI-LAYER` after one segment only; the layer joined to its own base in the union; depth over a base with one layer | +| O12s0, O12s1, O12p0_00 – O12p1_11 | a low-latency set: two segments of 3.84 s, each its own object, and their twenty-four 0.32 s parts, each its own object that decodes alone | Live-P adjacency, parts listed under aliases with cids, the live revision chain, parts verified against the cid the first revision listing them lists | +| O13a – O13c | the player fixture's WAI segments: one second each of the clip the classical presentation in `players/classical/` carries | a WAI playlist beside a playable classical one | + +O10 is the layer set of the byte-range side, so the playlist side's objects take the +next numbers: O11 is a layered sequence and O12 the low-latency set. The payloads of +O12 and O13 are `wai.video.int_motion` clips of a synthetic picture, encoded once by +this tree's integer video encoder, frozen in `src/` and checked to decode to the +encoder's reconstruction; every frame of each clip is distinct, so no two objects are +the same bytes. The HDR10+ block, the AVC bases and the LCEVC enhancements are synthetic: they carry the right start codes and decode to nothing, which transport does not need. OD's and @@ -77,6 +100,21 @@ outside this repository, and the AV1 streams were checked to decode. | `union_floor` | a sequence of objects | the union and the floor, or `error` | | `supports` | a floor, the sink's containers and capabilities | `supports` | | `layer_tag` | a `#WAI-LAYER` line | `n` and `uri`, or `error` | +| `media_playlist` | a `classical` playlist, the `segments` (each an `object`, its `layers`, its `parts` with their `alias`), an `open` segment, a `uri_prefix`, `end_list` and the `file` it is committed as | the rendering's BLAKE3 and length, and whether the file holds those bytes; or `error` | +| `multivariant` | each variant's `uri`, the WAI media `playlist` it names, `resolution`, `frame_rate_milli` | as `media_playlist`; the objects are found by the cids the playlist lists | +| `mpd` | a WAI media `playlist`, the classical timeline (`classical_mpd` and its `classical_set` and `period_id`, Appendix I; else the `classical` HLS playlist at timescale 1000), `set_id`, `rep_id` | as `media_playlist`; a gap segment is left out | +| `dash_adjacency` | a `wai` and a `classical` MPD (files, or `*_text`), optionally edited | `adjacent`, or `error` (§11.2 in DASH, Appendix I) | +| `playlist_parse` | a playlist `file` or `text` | its parameters, segments (duration, discontinuity, date-time, parts, layer numbers, URI), open segment and hint, and whether it is in the rendered form; or `error` | +| `adjacency` | a `wai` and a `classical` playlist (files, or `*_text`), optionally edited (`edit_wai`, `edit_classical`: `[from, to]` pairs, each replaced once) | `adjacent`, or `error` | +| `unchanged` | an `original` and a `published` classical master (files, or `*_text`), `edit_published` | `unchanged`, or `error` | +| `playlist_receipt` | a `playlist`, its `receipt` file and the `name` it is fetched as (the file's own by default), each optionally edited | the receipt hash and its parent, or `error` | +| `live` | `revisions` in the order a client fetched them (each a WAI playlist, its receipt, its classical playlist, `edit_wai`), `fetches` (an `alias` and the `object` served under it, optionally patched), the playlist's `url` (against which its lines resolve; `https://origin.example/live/wai-720.m3u8` by default), where parents are fetched from (`parents`, a directory holding `.json`; the first revision's `mr/` by default) and the walk's bound (`walk_bound`, 64 by default) | per revision its adjacency, its listing against what accepted revisions bound, its receipt, what it did to the chain (`true`, `unchanged`, `reanchored`, or the reason) and the parents walked; per fetch the accepted revision that first lists the alias, the outcome and what held (`listed`, or `signed` when a receipt in the chain binds the alias's URL to that cid) | +| `uri_resolve` | a `base` and a `reference` | the `url` (RFC 3986 §5.2 and §6.2.2, without the fragment) | +| `abr` | a `policy` and a `state` (Appendix F) | the `choice` and what the `budget` did, or `error` beyond Appendix F's bounds | +| `abr_order` | an object, what the sink `advertises`, a `policy`, a `state` | the entry `order` | +| `depth` | layer `objects` (or `layers_n`), a `policy`, a `state` | the `depth` and what the `budget` did, or `error` | +| `ilog2` | `a`, `b` | `log2_q16` | +| `render` | `segments`, `render` and `extras` written inline | the rendering's BLAKE3 and length, or `error` | An input names an object by id, a `file`, or an `envelope_hex` written inline; `union_floor` also takes `envelopes_hex`. A sink's `declared` is the union it was given @@ -85,6 +123,9 @@ An input names an object by id, a `file`, or an `envelope_hex` written inline; `alt` of that base's capability. A `declared` that does not parse, or a base that is not an object, refuses the case with its code. +`fetch`, `select` and `same_pick` also take `abr` (a `policy` and a `state`): the sink's +entry order is then the policy's over the verified head (Appendix F), on both paths. + `fetch` runs the reference client's policy: a first request of 16369 bytes unless `h` is given, never past `n`; the rest of the object when the planned base-class ranges reach 900 ‰ of the bytes after `covered_len` (`threshold_permille` changes it); and, @@ -100,7 +141,7 @@ dropped with why. ## Regenerating - cargo run --no-default-features --features http --bin wai_http_vectors -- gen ../http-conformance + cargo run --no-default-features --features http,video_manifest --bin wai_http_vectors -- gen ../http-conformance from `wai/wai-rs`. Every file is a function of `src/`, the two committed vectors the objects carry, and the code, so a second run writes the same bytes; `verify` @@ -116,7 +157,10 @@ each to a fresh worktree, requires the build to succeed, and requires --mutate NAME` does the same on the Python side (`left-half`, `no-root`, `counter-zero` fail the proof cases; `floor-as-codecs` fails `union_O9`; `blake3-iv` fails the self-test; `base-at-done`, `tick-noop` and `refinement-unclipped` fail the -`timed_*` cases), and the job checks a differential: `wai_http_vectors diff` +`timed_*` cases; `pdt-ignored`, `dseq-ignored`, `parts-after-extinf`, `average-floor`, +`render-digest-off`, `name-unchecked`, `chain-unchecked`, `ilog2-no-interp`, +`bola-waits`, `unlabelled-as-zero` and `round-half-down` fail the playlist, receipt and +adaptation cases they reach), and the job checks a differential: `wai_http_vectors diff` writes random envelopes' covered prefixes and proofs, `http_verify.py diff` verifies every one, and, with one chaining-value byte changed in ten of them, refuses exactly those ten. @@ -149,7 +193,16 @@ inputs where two readers most easily part, in families: or companion and an alternative each stating a right, a wrong or no digest, fetched through an origin tampered one way, at thresholds either side of the plan; `fuzz_whole_refine`: the whole path, its base's digest absent or the threshold low, - with a refinement whose digest is right, wrong or absent. + with a refinement whose digest is right, wrong or absent; +- `fuzz_playlist_parse`: playlists in a packager's order with at most one line changed, + and lines from every tag the reader reads, valid and not; `fuzz_adjacency`: one + timeline written as a classical and a WAI playlist, the WAI one perturbed one way or + not at all; `fuzz_unchanged`: a master and a published form of it; `fuzz_render`: + renderings from inline inputs, well and badly formed; `fuzz_receipt`: a committed + receipt file with one member changed; +- `fuzz_abr`: states and parameters of the four policies, sizes out of order, samples + of 0 ms, empty options and budgets; `fuzz_ilog2`: the integer log at random points + and the ends of `u64`. A refusal is an outcome like any other: `http_verify.py cases` must give the same code. A case wai-rs gives no outcome for stops the run, and `fuzz` prints, per family, diff --git a/wai/http-conformance/cases.json b/wai/http-conformance/cases.json index f9b0d6ca5..1765cf331 100644 --- a/wai/http-conformance/cases.json +++ b/wai/http-conformance/cases.json @@ -16,6 +16,38 @@ {"name":"head_proof_ok_O10","op":"head_proof","input":{"object":"O10"},"expect":{"covered_len":1009,"n_cv":1,"envelope_len":1825}}, {"name":"head_proof_ok_O10L1","op":"head_proof","input":{"object":"O10L1"},"expect":{"covered_len":1009,"n_cv":4,"envelope_len":12711}}, {"name":"head_proof_ok_O10L2","op":"head_proof","input":{"object":"O10L2"},"expect":{"covered_len":1009,"n_cv":5,"envelope_len":18367}}, +{"name":"head_proof_ok_O11a","op":"head_proof","input":{"object":"O11a"},"expect":{"covered_len":551,"n_cv":0,"envelope_len":551}}, +{"name":"head_proof_ok_O11b","op":"head_proof","input":{"object":"O11b"},"expect":{"covered_len":523,"n_cv":0,"envelope_len":523}}, +{"name":"head_proof_ok_O11bL1","op":"head_proof","input":{"object":"O11bL1"},"expect":{"covered_len":535,"n_cv":0,"envelope_len":535}}, +{"name":"head_proof_ok_O12p0_00","op":"head_proof","input":{"object":"O12p0_00"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3026}}, +{"name":"head_proof_ok_O12p0_01","op":"head_proof","input":{"object":"O12p0_01"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":2962}}, +{"name":"head_proof_ok_O12p0_02","op":"head_proof","input":{"object":"O12p0_02"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":2994}}, +{"name":"head_proof_ok_O12p0_03","op":"head_proof","input":{"object":"O12p0_03"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3018}}, +{"name":"head_proof_ok_O12p0_04","op":"head_proof","input":{"object":"O12p0_04"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3026}}, +{"name":"head_proof_ok_O12p0_05","op":"head_proof","input":{"object":"O12p0_05"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":2962}}, +{"name":"head_proof_ok_O12p0_06","op":"head_proof","input":{"object":"O12p0_06"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":2994}}, +{"name":"head_proof_ok_O12p0_07","op":"head_proof","input":{"object":"O12p0_07"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3018}}, +{"name":"head_proof_ok_O12p0_08","op":"head_proof","input":{"object":"O12p0_08"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3026}}, +{"name":"head_proof_ok_O12p0_09","op":"head_proof","input":{"object":"O12p0_09"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":2962}}, +{"name":"head_proof_ok_O12p0_10","op":"head_proof","input":{"object":"O12p0_10"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":2994}}, +{"name":"head_proof_ok_O12p0_11","op":"head_proof","input":{"object":"O12p0_11"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3014}}, +{"name":"head_proof_ok_O12p1_00","op":"head_proof","input":{"object":"O12p1_00"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3190}}, +{"name":"head_proof_ok_O12p1_01","op":"head_proof","input":{"object":"O12p1_01"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3042}}, +{"name":"head_proof_ok_O12p1_02","op":"head_proof","input":{"object":"O12p1_02"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3122}}, +{"name":"head_proof_ok_O12p1_03","op":"head_proof","input":{"object":"O12p1_03"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3190}}, +{"name":"head_proof_ok_O12p1_04","op":"head_proof","input":{"object":"O12p1_04"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3238}}, +{"name":"head_proof_ok_O12p1_05","op":"head_proof","input":{"object":"O12p1_05"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3146}}, +{"name":"head_proof_ok_O12p1_06","op":"head_proof","input":{"object":"O12p1_06"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3222}}, +{"name":"head_proof_ok_O12p1_07","op":"head_proof","input":{"object":"O12p1_07"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3222}}, +{"name":"head_proof_ok_O12p1_08","op":"head_proof","input":{"object":"O12p1_08"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3238}}, +{"name":"head_proof_ok_O12p1_09","op":"head_proof","input":{"object":"O12p1_09"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3170}}, +{"name":"head_proof_ok_O12p1_10","op":"head_proof","input":{"object":"O12p1_10"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3222}}, +{"name":"head_proof_ok_O12p1_11","op":"head_proof","input":{"object":"O12p1_11"},"expect":{"covered_len":1009,"n_cv":2,"envelope_len":3222}}, +{"name":"head_proof_ok_O12s0","op":"head_proof","input":{"object":"O12s0"},"expect":{"covered_len":1009,"n_cv":5,"envelope_len":16606}}, +{"name":"head_proof_ok_O12s1","op":"head_proof","input":{"object":"O12s1"},"expect":{"covered_len":1009,"n_cv":5,"envelope_len":19118}}, +{"name":"head_proof_ok_O13a","op":"head_proof","input":{"object":"O13a"},"expect":{"covered_len":1009,"n_cv":5,"envelope_len":21758}}, +{"name":"head_proof_ok_O13b","op":"head_proof","input":{"object":"O13b"},"expect":{"covered_len":1009,"n_cv":5,"envelope_len":20986}}, +{"name":"head_proof_ok_O13c","op":"head_proof","input":{"object":"O13c"},"expect":{"covered_len":1009,"n_cv":5,"envelope_len":21338}}, {"name":"head_proof_ok_O2","op":"head_proof","input":{"object":"O2"},"expect":{"covered_len":1009,"n_cv":5,"envelope_len":20250}}, {"name":"head_proof_ok_O3","op":"head_proof","input":{"object":"O3"},"expect":{"covered_len":1009,"n_cv":7,"envelope_len":98613}}, {"name":"head_proof_ok_O4","op":"head_proof","input":{"object":"O4"},"expect":{"covered_len":1009,"n_cv":6,"envelope_len":39361}}, @@ -318,5 +350,584 @@ {"name":"supports_union_only","op":"supports","input":{"floor":"wai2.video.av1","containers":["WAI1","WAI2"],"caps":["wai.video.int_hyper"]},"expect":{"supports":false}}, {"name":"supports_container_gate","op":"supports","input":{"floor":"wai2.video.av1","containers":["WAI1"],"caps":["wai.video.av1"]},"expect":{"supports":false}}, {"name":"supports_wai1","op":"supports","input":{"floor":"wai1.video.av1","containers":["WAI1"],"caps":["wai.video.av1"]},"expect":{"supports":true}}, -{"name":"supports_companion_ignored","op":"supports","input":{"floor":"wai2.video.av1+meta.hdr10plus","containers":["WAI1","WAI2"],"caps":["wai.video.av1"]},"expect":{"supports":true}} +{"name":"supports_companion_ignored","op":"supports","input":{"floor":"wai2.video.av1+meta.hdr10plus","containers":["WAI1","WAI2"],"caps":["wai.video.av1"]},"expect":{"supports":true}}, +{"name":"media_playlist_playlists_o9_wai_180_m3u8","op":"media_playlist","input":{"file":"playlists/o9/wai-180.m3u8","classical":"src/playlists/o9-classical.m3u8","uri_prefix":"../../objects/","segments":[{"object":"O9a"},{"object":"O9b"},{"object":"O9c"}]},"expect":{"blake3":"e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798","bytes":438,"same_as_file":true}}, +{"name":"media_playlist_playlists_o9_wai_180_gap_m3u8","op":"media_playlist","input":{"file":"playlists/o9/wai-180-gap.m3u8","classical":"src/playlists/o9-classical.m3u8","uri_prefix":"../../objects/","segments":[{"object":"O9a"},{"object":"O9b","gap":true},{"object":"O9c"}]},"expect":{"blake3":"0dae6b656b0b4928420241dab0f098591d3bcbc711b46a679f116742d78ea0ff","bytes":449,"same_as_file":true}}, +{"name":"media_playlist_playlists_o10_wai_a_m3u8","op":"media_playlist","input":{"file":"playlists/o10/wai-a.m3u8","classical":"src/playlists/o10-classical.m3u8","uri_prefix":"../../objects/","segments":[{"object":"O10","layers":["O10L1","O10L2"]}]},"expect":{"blake3":"24db5ea8900112df04426a7e28a68efa3ca77abdc07345af2c39724565715f77","bytes":417,"same_as_file":true}}, +{"name":"media_playlist_playlists_o11_wai_v_m3u8","op":"media_playlist","input":{"file":"playlists/o11/wai-v.m3u8","classical":"src/playlists/o11-classical.m3u8","uri_prefix":"../../objects/","segments":[{"object":"O11a"},{"object":"O11b","layers":["O11bL1"]}]},"expect":{"blake3":"044b6597093a22d2ae9cf86603fe564117bee5bb1b468424361da5ae48a81c79","bytes":406,"same_as_file":true}}, +{"name":"media_playlist_live_wai_r0_m3u8","op":"media_playlist","input":{"file":"live/wai-r0.m3u8","classical":"src/live/classical-r0.m3u8","uri_prefix":"../objects/","segments":[],"open":{"parts":[{"object":"O12p0_00","alias":"720/1200.0.wai","independent":true},{"object":"O12p0_01","alias":"720/1200.1.wai","independent":true},{"object":"O12p0_02","alias":"720/1200.2.wai","independent":true},{"object":"O12p0_03","alias":"720/1200.3.wai","independent":true},{"object":"O12p0_04","alias":"720/1200.4.wai","independent":true}],"preload_hint":"720/1200.5.wai"}},"expect":{"blake3":"6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9","bytes":975,"same_as_file":true}}, +{"name":"media_playlist_live_wai_r1_m3u8","op":"media_playlist","input":{"file":"live/wai-r1.m3u8","classical":"src/live/classical-r1.m3u8","uri_prefix":"../objects/","segments":[{"object":"O12s0","parts":[{"object":"O12p0_00","alias":"720/1200.0.wai","independent":true},{"object":"O12p0_01","alias":"720/1200.1.wai","independent":true},{"object":"O12p0_02","alias":"720/1200.2.wai","independent":true},{"object":"O12p0_03","alias":"720/1200.3.wai","independent":true},{"object":"O12p0_04","alias":"720/1200.4.wai","independent":true},{"object":"O12p0_05","alias":"720/1200.5.wai","independent":true},{"object":"O12p0_06","alias":"720/1200.6.wai","independent":true},{"object":"O12p0_07","alias":"720/1200.7.wai","independent":true},{"object":"O12p0_08","alias":"720/1200.8.wai","independent":true},{"object":"O12p0_09","alias":"720/1200.9.wai","independent":true},{"object":"O12p0_10","alias":"720/1200.10.wai","independent":true},{"object":"O12p0_11","alias":"720/1200.11.wai","independent":true}]}],"open":{"parts":[{"object":"O12p1_00","alias":"720/1201.0.wai","independent":true},{"object":"O12p1_01","alias":"720/1201.1.wai","independent":true},{"object":"O12p1_02","alias":"720/1201.2.wai","independent":true}],"preload_hint":"720/1201.3.wai"}},"expect":{"blake3":"091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271","bytes":2480,"same_as_file":true}}, +{"name":"media_playlist_live_wai_r2_m3u8","op":"media_playlist","input":{"file":"live/wai-r2.m3u8","classical":"src/live/classical-r2.m3u8","uri_prefix":"../objects/","segments":[{"object":"O12s0","parts":[{"object":"O12p0_00","alias":"720/1200.0.wai","independent":true},{"object":"O12p0_01","alias":"720/1200.1.wai","independent":true},{"object":"O12p0_02","alias":"720/1200.2.wai","independent":true},{"object":"O12p0_03","alias":"720/1200.3.wai","independent":true},{"object":"O12p0_04","alias":"720/1200.4.wai","independent":true},{"object":"O12p0_05","alias":"720/1200.5.wai","independent":true},{"object":"O12p0_06","alias":"720/1200.6.wai","independent":true},{"object":"O12p0_07","alias":"720/1200.7.wai","independent":true},{"object":"O12p0_08","alias":"720/1200.8.wai","independent":true},{"object":"O12p0_09","alias":"720/1200.9.wai","independent":true},{"object":"O12p0_10","alias":"720/1200.10.wai","independent":true},{"object":"O12p0_11","alias":"720/1200.11.wai","independent":true}]},{"object":"O12s1","parts":[{"object":"O12p1_00","alias":"720/1201.0.wai","independent":true},{"object":"O12p1_01","alias":"720/1201.1.wai","independent":true},{"object":"O12p1_02","alias":"720/1201.2.wai","independent":true},{"object":"O12p1_03","alias":"720/1201.3.wai","independent":true},{"object":"O12p1_04","alias":"720/1201.4.wai","independent":true},{"object":"O12p1_05","alias":"720/1201.5.wai","independent":true},{"object":"O12p1_06","alias":"720/1201.6.wai","independent":true},{"object":"O12p1_07","alias":"720/1201.7.wai","independent":true},{"object":"O12p1_08","alias":"720/1201.8.wai","independent":true},{"object":"O12p1_09","alias":"720/1201.9.wai","independent":true},{"object":"O12p1_10","alias":"720/1201.10.wai","independent":true},{"object":"O12p1_11","alias":"720/1201.11.wai","independent":true}]}],"open":{"parts":[],"preload_hint":"720/1202.0.wai"}},"expect":{"blake3":"3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900","bytes":3845,"same_as_file":true}}, +{"name":"media_playlist_live_wai_r3_m3u8","op":"media_playlist","input":{"file":"live/wai-r3.m3u8","classical":"src/live/classical-r3.m3u8","uri_prefix":"../objects/","segments":[{"object":"O12s0"},{"object":"O12s1"}],"end_list":true},"expect":{"blake3":"b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e","bytes":445,"same_as_file":true}}, +{"name":"media_playlist_players_wai_v0_m3u8","op":"media_playlist","input":{"file":"players/wai-v0.m3u8","classical":"players/classical/v0.m3u8","uri_prefix":"../objects/","segments":[{"object":"O13a"},{"object":"O13b"},{"object":"O13c"}]},"expect":{"blake3":"57bbf3d1af7c937c13ac72b729895532f68da3c5e55365cebdfa8d92dd73a9c5","bytes":422,"same_as_file":true}}, +{"name":"multivariant_playlists_o9_wai_m3u8","op":"multivariant","input":{"file":"playlists/o9/wai.m3u8","variants":[{"uri":"wai-180.m3u8","playlist":"playlists/o9/wai-180.m3u8","resolution":[320,180],"frame_rate_milli":25000}]},"expect":{"blake3":"5380eb2ce76df5d61a115dcb15ad64764731d80c3a8ced7c24ee19b680e99366","bytes":187,"same_as_file":true}}, +{"name":"multivariant_playlists_o9_wai_gap_m3u8","op":"multivariant","input":{"file":"playlists/o9/wai-gap.m3u8","variants":[{"uri":"wai-180-gap.m3u8","playlist":"playlists/o9/wai-180-gap.m3u8","resolution":[320,180]}]},"expect":{"blake3":"3c89015f4252faee1050a5efb2933bae4e70a4f38fbab8fbfb3a7dd7b589303a","bytes":173,"same_as_file":true}}, +{"name":"multivariant_playlists_o10_wai_m3u8","op":"multivariant","input":{"file":"playlists/o10/wai.m3u8","variants":[{"uri":"wai-a.m3u8","playlist":"playlists/o10/wai-a.m3u8"}]},"expect":{"blake3":"283a9a97c3f5d9818db6e9eed2e00c604599f8b35f694525adbde7927ce831b3","bytes":170,"same_as_file":true}}, +{"name":"multivariant_playlists_o11_wai_m3u8","op":"multivariant","input":{"file":"playlists/o11/wai.m3u8","variants":[{"uri":"wai-v.m3u8","playlist":"playlists/o11/wai-v.m3u8"}]},"expect":{"blake3":"aaba193bc0f8f3c81f49823bcfdad0f0e05daad81157697a840f5daaed6bbae1","bytes":161,"same_as_file":true}}, +{"name":"multivariant_live_wai_m3u8","op":"multivariant","input":{"file":"live/wai.m3u8","variants":[{"uri":"wai-720.m3u8","playlist":"live/wai-r3.m3u8","resolution":[32,18],"frame_rate_milli":25000}]},"expect":{"blake3":"43d8a9cac1a662298e42fe9f315a9a5101adfd51a23349d87d68f1c1f76d1b1a","bytes":181,"same_as_file":true}}, +{"name":"multivariant_players_wai_m3u8","op":"multivariant","input":{"file":"players/wai.m3u8","variants":[{"uri":"wai-v0.m3u8","playlist":"players/wai-v0.m3u8","resolution":[64,36],"frame_rate_milli":25000}]},"expect":{"blake3":"48967c4eb624a91ac9fb3a2849de4609f02fa093eced0aa31842189ea1793db7","bytes":182,"same_as_file":true}}, +{"name":"mpd_playlists_o9_wai_mpd","op":"mpd","input":{"file":"playlists/o9/wai.mpd","playlist":"playlists/o9/wai-180.m3u8","classical":"src/playlists/o9-classical.m3u8","classical_mpd":"src/playlists/o9-classical.mpd","classical_set":"1","set_id":1,"rep_id":"wai-180"},"expect":{"blake3":"d630fe02e4490e6ff4665fe145776d464299c4c8411fb2e5c16f382bdec6b397","bytes":1268,"same_as_file":true}}, +{"name":"mpd_playlists_o9_wai_gap_mpd","op":"mpd","input":{"file":"playlists/o9/wai-gap.mpd","playlist":"playlists/o9/wai-180-gap.m3u8","classical":"src/playlists/o9-classical.m3u8","classical_mpd":"src/playlists/o9-classical.mpd","classical_set":"1","set_id":1,"rep_id":"wai-180"},"expect":{"blake3":"c30a9c1e045155e75b6bf50349fddc0e72361da34b22c3e0a37a4b5b2852de6c","bytes":1108,"same_as_file":true}}, +{"name":"mpd_players_wai_mpd","op":"mpd","input":{"file":"players/wai.mpd","playlist":"players/wai-v0.m3u8","classical":"players/classical/v0.m3u8","period_id":"p0","set_id":1,"rep_id":"wai-v0"},"expect":{"blake3":"b09dfb6387b814f6e576274a41e630629bb3252e17d10d61c9a1730905ae1fb4","bytes":1239,"same_as_file":true}}, +{"name":"mpd_rep_id_white_space","op":"mpd","input":{"rep_id":"wai 180","playlist":"playlists/o9/wai-180.m3u8","classical":"src/playlists/o9-classical.m3u8","classical_mpd":"src/playlists/o9-classical.mpd","classical_set":"1","set_id":1},"expect":{"error":"playlist_malformed"}}, +{"name":"mpd_rep_id_not_a_char","op":"mpd","input":{"rep_id":"wai\u0001180","playlist":"playlists/o9/wai-180.m3u8","classical":"src/playlists/o9-classical.m3u8","classical_mpd":"src/playlists/o9-classical.mpd","classical_set":"1","set_id":1},"expect":{"error":"playlist_malformed"}}, +{"name":"mpd_rep_id_unicode_space","op":"mpd","input":{"rep_id":"wai 180","playlist":"playlists/o9/wai-180.m3u8","classical":"src/playlists/o9-classical.m3u8","classical_mpd":"src/playlists/o9-classical.mpd","classical_set":"1","set_id":1},"expect":{"error":"playlist_malformed"}}, +{"name":"media_playlist_short_O9","op":"media_playlist","input":{"classical":"src/playlists/o9-classical.m3u8","uri_prefix":"","segments":[{"object":"O9a"},{"object":"O9b"}]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"media_playlist_parts_beside_no_target","op":"media_playlist","input":{"classical":"src/playlists/o9-classical.m3u8","uri_prefix":"","segments":[{"object":"O9a","parts":[{"object":"O12p0_00","alias":"p0.wai","independent":true,"duration_ms":320},{"object":"O12p0_01","alias":"p1.wai","independent":true,"duration_ms":320}]},{"object":"O9b"},{"object":"O9c"}]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"media_playlist_gap_with_layers","op":"media_playlist","input":{"classical":"src/playlists/o11-classical.m3u8","uri_prefix":"","segments":[{"object":"O11a"},{"object":"O11b","gap":true,"layers":["O11bL1"]}]},"expect":{"error":"playlist_malformed"}}, +{"name":"render_gap_with_parts","op":"render","input":{"segments":[[640,"a.wai",false],[640,"b.wai",false]],"render":{"version":9,"media_sequence":0,"discontinuity_sequence":0,"target_duration_s":null,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":false,"end_list":true,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":true,"program_date_time":null,"parts":[{"duration_ms":320,"independent":true,"uri":"p.wai","layers":[]}],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[],"layers":[],"preload_hint":null}]},"expect":{"error":"playlist_malformed"}}, +{"name":"render_gap_with_layers","op":"render","input":{"segments":[[640,"a.wai",false],[640,"b.wai",false]],"render":{"version":9,"media_sequence":0,"discontinuity_sequence":0,"target_duration_s":null,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":false,"end_list":true,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":true,"program_date_time":null,"parts":[],"layers":[{"n":1,"uri":"l.wai"}],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[],"layers":[],"preload_hint":null}]},"expect":{"error":"playlist_malformed"}}, +{"name":"render_gap","op":"render","input":{"segments":[[640,"a.wai",false],[640,"b.wai",false]],"render":{"version":9,"media_sequence":0,"discontinuity_sequence":0,"target_duration_s":null,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":false,"end_list":true,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":true,"program_date_time":null,"parts":[],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[],"layers":[],"preload_hint":null}]},"expect":{"blake3":"eace1c6cc401981177eb98a6c981c02d13c9ad07f89db07bceeafc5592491695","bytes":218}}, +{"name":"media_playlist_layer_not_a_layer","op":"media_playlist","input":{"classical":"src/playlists/o9-classical.m3u8","uri_prefix":"","segments":[{"object":"O9a","layers":["O9b"]},{"object":"O9b"},{"object":"O9c"}]},"expect":{"error":"not_a_layer"}}, +{"name":"media_playlist_open_without_classical","op":"media_playlist","input":{"classical":"src/playlists/o9-classical.m3u8","uri_prefix":"","segments":[{"object":"O9a"},{"object":"O9b"},{"object":"O9c"}],"open":{"parts":[{"object":"O12p1_00","alias":"p.wai","independent":true,"duration_ms":320}]}},"expect":{"error":"adjacency_misaligned"}}, +{"name":"playlist_parse_playlists_o9_wai_180_m3u8","op":"playlist_parse","input":{"file":"playlists/o9/wai-180.m3u8"},"expect":{"version":7,"target_duration_s":1,"media_sequence":0,"discontinuity_sequence":0,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":true,"playlist_type":"VOD","independent_segments":false,"map_uri":null,"segments":[[640,false,null,[],[],"../../objects/31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3.wai#n=21034&h=1009",false],[640,false,null,[],[],"../../objects/b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb.wai#n=18004",false],[640,false,null,[],[],"../../objects/a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133.wai#n=18381",false]],"open":null,"preload_hint":null,"rendered_form":true}}, +{"name":"playlist_parse_playlists_o9_wai_180_gap_m3u8","op":"playlist_parse","input":{"file":"playlists/o9/wai-180-gap.m3u8"},"expect":{"version":7,"target_duration_s":1,"media_sequence":0,"discontinuity_sequence":0,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":true,"playlist_type":"VOD","independent_segments":false,"map_uri":null,"segments":[[640,false,null,[],[],"../../objects/31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3.wai#n=21034&h=1009",false],[640,false,null,[],[],"../../objects/b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb.wai#n=18004",true],[640,false,null,[],[],"../../objects/a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133.wai#n=18381",false]],"open":null,"preload_hint":null,"rendered_form":true}}, +{"name":"playlist_parse_playlists_o10_wai_a_m3u8","op":"playlist_parse","input":{"file":"playlists/o10/wai-a.m3u8"},"expect":{"version":7,"target_duration_s":1,"media_sequence":0,"discontinuity_sequence":0,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":true,"playlist_type":null,"independent_segments":false,"map_uri":null,"segments":[[50,false,null,[],[1,2],"../../objects/1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8.wai#n=1825",false]],"open":null,"preload_hint":null,"rendered_form":true}}, +{"name":"playlist_parse_playlists_o11_wai_v_m3u8","op":"playlist_parse","input":{"file":"playlists/o11/wai-v.m3u8"},"expect":{"version":7,"target_duration_s":1,"media_sequence":0,"discontinuity_sequence":0,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":true,"playlist_type":null,"independent_segments":false,"map_uri":null,"segments":[[200,false,null,[],[],"../../objects/8f3a701e2f904ad90663466c2c12cafed3410eeb84ad41db82acdc8f9a10adf7.wai#n=551",false],[200,false,null,[],[1],"../../objects/9e4ee50a011204c10f0538b8c89fdd41da51c85d970a6a54d850c55c255d5486.wai#n=523",false]],"open":null,"preload_hint":null,"rendered_form":true}}, +{"name":"playlist_parse_live_wai_r0_m3u8","op":"playlist_parse","input":{"file":"live/wai-r0.m3u8"},"expect":{"version":9,"target_duration_s":4,"media_sequence":1200,"discontinuity_sequence":0,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null,"segments":[],"open":[false,"2026-10-01T12:00:00.000Z",[[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]]]],"preload_hint":"720/1200.5.wai","rendered_form":true}}, +{"name":"playlist_parse_live_wai_r1_m3u8","op":"playlist_parse","input":{"file":"live/wai-r1.m3u8"},"expect":{"version":9,"target_duration_s":4,"media_sequence":1200,"discontinuity_sequence":0,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null,"segments":[[3840,false,"2026-10-01T12:00:00.000Z",[[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]]],[],"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606",false]],"open":[false,null,[[320,true,[]],[320,true,[]],[320,true,[]]]],"preload_hint":"720/1201.3.wai","rendered_form":true}}, +{"name":"playlist_parse_live_wai_r2_m3u8","op":"playlist_parse","input":{"file":"live/wai-r2.m3u8"},"expect":{"version":9,"target_duration_s":4,"media_sequence":1200,"discontinuity_sequence":0,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null,"segments":[[3840,false,"2026-10-01T12:00:00.000Z",[[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]]],[],"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606",false],[3840,false,null,[[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]]],[],"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118",false]],"open":null,"preload_hint":"720/1202.0.wai","rendered_form":true}}, +{"name":"playlist_parse_live_wai_r3_m3u8","op":"playlist_parse","input":{"file":"live/wai-r3.m3u8"},"expect":{"version":9,"target_duration_s":4,"media_sequence":1200,"discontinuity_sequence":0,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":true,"playlist_type":null,"independent_segments":false,"map_uri":null,"segments":[[3840,false,"2026-10-01T12:00:00.000Z",[],[],"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606",false],[3840,false,null,[],[],"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118",false]],"open":null,"preload_hint":null,"rendered_form":true}}, +{"name":"playlist_parse_players_wai_v0_m3u8","op":"playlist_parse","input":{"file":"players/wai-v0.m3u8"},"expect":{"version":7,"target_duration_s":1,"media_sequence":0,"discontinuity_sequence":0,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":true,"playlist_type":"VOD","independent_segments":false,"map_uri":null,"segments":[[1000,false,null,[],[],"../objects/0e1664a61fc504e04ff37dd2e4a15273791059a2626b78a2d1cd4d99ffbe6f59.wai#n=21758",false],[1000,false,null,[],[],"../objects/f7aebfa6c378a0b83fa3293be38962773a620b1fabb1d17acfc7f52693ce830a.wai#n=20986",false],[1000,false,null,[],[],"../objects/761e2e03083d601c1b824bf484ce60a286cbd20e478619944fbb385736c6e543.wai#n=21338",false]],"open":null,"preload_hint":null,"rendered_form":true}}, +{"name":"playlist_parse_src_live_classical_r1_m3u8","op":"playlist_parse","input":{"file":"src/live/classical-r1.m3u8"},"expect":{"version":9,"target_duration_s":4,"media_sequence":1200,"discontinuity_sequence":0,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":"init.mp4","segments":[[3840,false,"2026-10-01T12:00:00.000Z",[[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]],[320,true,[]]],[],"1200.m4s",false]],"open":[false,null,[[320,true,[]],[320,true,[]],[320,true,[]]]],"preload_hint":"1201.3.m4s","rendered_form":false}}, +{"name":"playlist_parse_players_classical_v0_m3u8","op":"playlist_parse","input":{"file":"players/classical/v0.m3u8"},"expect":{"version":7,"target_duration_s":1,"media_sequence":0,"discontinuity_sequence":0,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":true,"playlist_type":"VOD","independent_segments":false,"map_uri":"init.mp4","segments":[[1000,false,null,[],[],"seg0.m4s",false],[1000,false,null,[],[],"seg1.m4s",false],[1000,false,null,[],[],"seg2.m4s",false]],"open":null,"preload_hint":null,"rendered_form":false}}, +{"name":"playlist_parse_src_playlists_o9_classical_m3u8","op":"playlist_parse","input":{"file":"src/playlists/o9-classical.m3u8"},"expect":{"version":7,"target_duration_s":1,"media_sequence":0,"discontinuity_sequence":0,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":true,"playlist_type":"VOD","independent_segments":false,"map_uri":"init-180.mp4","segments":[[640,false,null,[],[],"seg-180-0.m4s",false],[640,false,null,[],[],"seg-180-1.m4s",false],[640,false,null,[],[],"seg-180-2.m4s",false]],"open":null,"preload_hint":null,"rendered_form":false}}, +{"name":"playlist_parse_no_header","op":"playlist_parse","input":{"text":"#EXT-X-VERSION:3\n#EXTINF:1,\na\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_extinf_no_uri","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXTINF:1,\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_uri_no_extinf","op":"playlist_parse","input":{"text":"#EXTM3U\na.wai\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_skip","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXT-X-SKIP:SKIPPED-SEGMENTS=3\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_variant","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\na.m3u8\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_sequence_late","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXTINF:1,\na\n#EXT-X-MEDIA-SEQUENCE:3\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_layer_after_uri","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXTINF:1,\na\n#WAI-LAYER:N=1,URI=\"l.wai\"\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_layer_order","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXTINF:1,\n#WAI-LAYER:N=2,URI=\"l.wai\"\n#WAI-LAYER:N=1,URI=\"m.wai\"\na\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_layer_comment_between","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXTINF:1,\n# a note\n#WAI-LAYER:N=1,URI=\"l.wai\"\na\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_part_no_uri","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXT-X-PART:DURATION=0.5\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_two_hints","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"a\"\n#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"b\"\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_negative","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXTINF:-1,\na\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_exponent","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXTINF:1e3,\na\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_block_maybe","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=MAYBE\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_type_live","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXT-X-PLAYLIST-TYPE:LIVE\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_map_no_uri","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXT-X-MAP:BYTERANGE=\"1@0\"\n"},"expect":{"error":"playlist_malformed"}}, +{"name":"playlist_parse_layer_zero","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXTINF:1,\n#WAI-LAYER:N=0,URI=\"l.wai\"\na\n"},"expect":{"error":"layer_tag_malformed"}}, +{"name":"playlist_parse_rounding","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXTINF:3.9995,\na\n#EXTINF:3.99949,\nb\n#EXTINF:4.00008,\nc\n#EXTINF:4,\nd\n"},"expect":{"version":null,"target_duration_s":null,"media_sequence":0,"discontinuity_sequence":0,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null,"segments":[[4000,false,null,[],[],"a",false],[3999,false,null,[],[],"b",false],[4000,false,null,[],[],"c",false],[4000,false,null,[],[],"d",false]],"open":null,"preload_hint":null,"rendered_form":false}}, +{"name":"playlist_parse_crlf_title","op":"playlist_parse","input":{"text":"#EXTM3U\r\n#EXTINF:0.64,a title\r\nseg.m4s\r\n#EXT-X-DISCONTINUITY\r\n#EXTINF:1,\r\nseg2.m4s\r\n"},"expect":{"version":null,"target_duration_s":null,"media_sequence":0,"discontinuity_sequence":0,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null,"segments":[[640,false,null,[],[],"seg.m4s",false],[1000,true,null,[],[],"seg2.m4s",false]],"open":null,"preload_hint":null,"rendered_form":false}}, +{"name":"playlist_parse_hint_only","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXT-X-PART-INF:PART-TARGET=1\n#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"p.0\"\n#EXT-X-PRELOAD-HINT:TYPE=MAP,URI=\"i\"\n"},"expect":{"version":null,"target_duration_s":null,"media_sequence":0,"discontinuity_sequence":0,"part_target_ms":1000,"part_hold_back_ms":null,"can_block_reload":false,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null,"segments":[],"open":null,"preload_hint":"p.0","rendered_form":false}}, +{"name":"playlist_parse_packager_lines","op":"playlist_parse","input":{"text":"#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-TARGETDURATION:3\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-PLAYLIST-TYPE:EVENT\n#EXT-X-INDEPENDENT-SEGMENTS\n#EXT-X-MAP:URI=\"i.mp4\"\n#EXT-X-GAP\n#EXTINF:2.400,\na.m4s\n#EXT-X-MAP:URI=\"j.mp4\"\n#EXTINF:1.000,\nb.m4s\n"},"expect":{"version":7,"target_duration_s":3,"media_sequence":0,"discontinuity_sequence":0,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":false,"playlist_type":"EVENT","independent_segments":true,"map_uri":"i.mp4","segments":[[2400,false,null,[],[],"a.m4s",true],[1000,false,null,[],[],"b.m4s",false]],"open":null,"preload_hint":null,"rendered_form":false}}, +{"name":"adjacency_playlists_o9_wai_180_m3u8","op":"adjacency","input":{"wai":"playlists/o9/wai-180.m3u8","classical":"src/playlists/o9-classical.m3u8"},"expect":{"adjacent":true}}, +{"name":"adjacency_playlists_o9_wai_180_gap_m3u8","op":"adjacency","input":{"wai":"playlists/o9/wai-180-gap.m3u8","classical":"src/playlists/o9-classical.m3u8"},"expect":{"adjacent":true}}, +{"name":"adjacency_playlists_o10_wai_a_m3u8","op":"adjacency","input":{"wai":"playlists/o10/wai-a.m3u8","classical":"src/playlists/o10-classical.m3u8"},"expect":{"adjacent":true}}, +{"name":"adjacency_playlists_o11_wai_v_m3u8","op":"adjacency","input":{"wai":"playlists/o11/wai-v.m3u8","classical":"src/playlists/o11-classical.m3u8"},"expect":{"adjacent":true}}, +{"name":"adjacency_live_wai_r0_m3u8","op":"adjacency","input":{"wai":"live/wai-r0.m3u8","classical":"src/live/classical-r0.m3u8"},"expect":{"adjacent":true}}, +{"name":"adjacency_live_wai_r1_m3u8","op":"adjacency","input":{"wai":"live/wai-r1.m3u8","classical":"src/live/classical-r1.m3u8"},"expect":{"adjacent":true}}, +{"name":"adjacency_live_wai_r2_m3u8","op":"adjacency","input":{"wai":"live/wai-r2.m3u8","classical":"src/live/classical-r2.m3u8"},"expect":{"adjacent":true}}, +{"name":"adjacency_live_wai_r3_m3u8","op":"adjacency","input":{"wai":"live/wai-r3.m3u8","classical":"src/live/classical-r3.m3u8"},"expect":{"adjacent":true}}, +{"name":"adjacency_players_wai_v0_m3u8","op":"adjacency","input":{"wai":"players/wai-v0.m3u8","classical":"players/classical/v0.m3u8"},"expect":{"adjacent":true}}, +{"name":"adjacency_extinf_shifted","op":"adjacency","input":{"wai":"playlists/o9/wai-180.m3u8","classical":"src/playlists/o9-classical.m3u8","edit_wai":[["#EXTINF:0.640,","#EXTINF:0.641,"]]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"adjacency_discontinuity_one_side","op":"adjacency","input":{"wai":"playlists/o9/wai-180.m3u8","classical":"src/playlists/o9-classical.m3u8","edit_wai":[["#EXTINF:0.640,","#EXT-X-DISCONTINUITY\n#EXTINF:0.640,"]]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"adjacency_media_sequence","op":"adjacency","input":{"wai":"playlists/o9/wai-180.m3u8","classical":"src/playlists/o9-classical.m3u8","edit_wai":[["#EXT-X-MEDIA-SEQUENCE:0","#EXT-X-MEDIA-SEQUENCE:1"]]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"adjacency_discontinuity_sequence","op":"adjacency","input":{"wai":"playlists/o9/wai-180.m3u8","classical":"src/playlists/o9-classical.m3u8","edit_wai":[["#EXT-X-MEDIA-SEQUENCE:0\n","#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-DISCONTINUITY-SEQUENCE:1\n"]]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"adjacency_segment_missing","op":"adjacency","input":{"wai":"playlists/o9/wai-180.m3u8","classical":"src/playlists/o9-classical.m3u8","edit_wai":[["#EXTINF:0.640,\n../../objects/","#EXT-X-BYTERANGE:1\n#EXT-X-GAP\n#EXTINF:0.640,\n../../objects/"],["#EXT-X-ENDLIST","#EXTINF:0.640,\nextra.wai\n#EXT-X-ENDLIST"]]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"adjacency_classical_discontinuity","op":"adjacency","input":{"wai":"playlists/o9/wai-180.m3u8","classical":"src/playlists/o9-classical.m3u8","edit_classical":[["#EXTINF:0.64,\nseg-180-2","#EXT-X-DISCONTINUITY\n#EXTINF:0.64,\nseg-180-2"]]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"adjacency_part_duration","op":"adjacency","input":{"wai":"live/wai-r1.m3u8","classical":"src/live/classical-r1.m3u8","edit_wai":[["#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI=\"720/1200.4.wai","#EXT-X-PART:DURATION=0.319,INDEPENDENT=YES,URI=\"720/1200.4.wai"]]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"adjacency_open_part_duration","op":"adjacency","input":{"wai":"live/wai-r1.m3u8","classical":"src/live/classical-r1.m3u8","edit_wai":[["#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI=\"720/1201.2.wai","#EXT-X-PART:DURATION=0.330,INDEPENDENT=YES,URI=\"720/1201.2.wai"]]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"adjacency_part_target","op":"adjacency","input":{"wai":"live/wai-r1.m3u8","classical":"src/live/classical-r1.m3u8","edit_wai":[["PART-TARGET=0.320","PART-TARGET=0.330"]]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"adjacency_program_date_time","op":"adjacency","input":{"wai":"live/wai-r1.m3u8","classical":"src/live/classical-r1.m3u8","edit_wai":[["2026-10-01T12:00:00.000Z","2026-10-01T12:00:00.000+00:00"]]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"adjacency_layer_line_ignored","op":"adjacency","input":{"wai":"live/wai-r1.m3u8","classical":"src/live/classical-r1.m3u8","edit_wai":[["#EXTINF:3.840,\n","#EXTINF:3.840,\n#WAI-LAYER:N=1,URI=\"../objects/l.wai#n=9\"\n"]]},"expect":{"adjacent":true}}, +{"name":"adjacency_live_s_beside_live_p","op":"adjacency","input":{"wai":"live/wai-r3.m3u8","classical":"src/live/classical-r3.m3u8","edit_wai":[["#EXT-X-ENDLIST\n",""]],"edit_classical":[["#EXT-X-ENDLIST\n",""]]},"expect":{"adjacent":true}}, +{"name":"adjacency_malformed_wai","op":"adjacency","input":{"wai":"live/wai-r1.m3u8","classical":"src/live/classical-r1.m3u8","edit_wai":[["#EXTM3U","#EXTM3X"]]},"expect":{"error":"playlist_malformed"}}, +{"name":"dash_adjacency_o9","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd"},"expect":{"adjacent":true}}, +{"name":"dash_adjacency_o9_gap","op":"dash_adjacency","input":{"wai":"playlists/o9/wai-gap.mpd","classical":"src/playlists/o9-classical.mpd"},"expect":{"adjacent":true}}, +{"name":"dash_adjacency_period_id","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[["",""]],"edit_classical":[]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"dash_adjacency_t","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[["",""]],"edit_classical":[]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"dash_adjacency_extra_segment","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[["",""]],"edit_classical":[]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"dash_adjacency_out_of_order","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[["\n ","\n "]],"edit_classical":[]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"dash_adjacency_written_with_r","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[["\n \n ",""]],"edit_classical":[]},"expect":{"adjacent":true}}, +{"name":"dash_adjacency_classical_written_out","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[],"edit_classical":[["",""]]},"expect":{"adjacent":true}}, +{"name":"dash_adjacency_classical_shorter","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[],"edit_classical":[["d=\"57600\" r=\"2\"","d=\"57600\" r=\"1\""]]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"dash_adjacency_no_timeline","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[["",""]],"edit_classical":[]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"dash_adjacency_crlf_and_comment","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[["\r\n \n",""]],"edit_classical":[]},"expect":{"error":"playlist_malformed"}}, +{"name":"dash_adjacency_unclosed","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[["",""]],"edit_classical":[]},"expect":{"error":"playlist_malformed"}}, +{"name":"dash_adjacency_r_negative","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[],"edit_classical":[["r=\"2\"","r=\"-1\""]]},"expect":{"error":"playlist_malformed"}}, +{"name":"dash_adjacency_classical_r_huge","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[],"edit_classical":[["d=\"57600\" r=\"2\"","d=\"57600\" r=\"300000000000000\""]]},"expect":{"adjacent":true}}, +{"name":"dash_adjacency_r_end_past_u64","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[],"edit_classical":[["d=\"57600\" r=\"2\"","d=\"57600\" r=\"18446744073709551614\""]]},"expect":{"error":"playlist_malformed"}}, +{"name":"dash_adjacency_wai_r_huge","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[["",""]],"edit_classical":[]},"expect":{"error":"adjacency_misaligned"}}, +{"name":"dash_adjacency_timelines_past_the_subset","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[],"edit_classical":[["",""]]},"expect":{"error":"playlist_malformed"}}, +{"name":"dash_adjacency_start_in_months","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[["start=\"PT0S\"","start=\"P0M\""]],"edit_classical":[]},"expect":{"error":"playlist_malformed"}}, +{"name":"dash_adjacency_not_an_mpd","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[["",""]],"edit_classical":[]},"expect":{"error":"playlist_malformed"}}, +{"name":"dash_adjacency_zero_d","op":"dash_adjacency","input":{"wai":"playlists/o9/wai.mpd","classical":"src/playlists/o9-classical.mpd","edit_wai":[],"edit_classical":[["d=\"30720\"","d=\"0\""]]},"expect":{"error":"playlist_malformed"}}, +{"name":"uri_resolve_rfc_00","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g:h"},"expect":{"url":"g:h"}}, +{"name":"uri_resolve_rfc_01","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g"},"expect":{"url":"http://a/b/c/g"}}, +{"name":"uri_resolve_rfc_02","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"./g"},"expect":{"url":"http://a/b/c/g"}}, +{"name":"uri_resolve_rfc_03","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g/"},"expect":{"url":"http://a/b/c/g/"}}, +{"name":"uri_resolve_rfc_04","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"/g"},"expect":{"url":"http://a/g"}}, +{"name":"uri_resolve_rfc_05","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"//g"},"expect":{"url":"http://g"}}, +{"name":"uri_resolve_rfc_06","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"?y"},"expect":{"url":"http://a/b/c/d;p?y"}}, +{"name":"uri_resolve_rfc_07","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g?y"},"expect":{"url":"http://a/b/c/g?y"}}, +{"name":"uri_resolve_rfc_08","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"#s"},"expect":{"url":"http://a/b/c/d;p?q"}}, +{"name":"uri_resolve_rfc_09","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g#s"},"expect":{"url":"http://a/b/c/g"}}, +{"name":"uri_resolve_rfc_10","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g?y#s"},"expect":{"url":"http://a/b/c/g?y"}}, +{"name":"uri_resolve_rfc_11","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":";x"},"expect":{"url":"http://a/b/c/;x"}}, +{"name":"uri_resolve_rfc_12","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g;x"},"expect":{"url":"http://a/b/c/g;x"}}, +{"name":"uri_resolve_rfc_13","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g;x?y#s"},"expect":{"url":"http://a/b/c/g;x?y"}}, +{"name":"uri_resolve_rfc_14","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":""},"expect":{"url":"http://a/b/c/d;p?q"}}, +{"name":"uri_resolve_rfc_15","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"."},"expect":{"url":"http://a/b/c/"}}, +{"name":"uri_resolve_rfc_16","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"./"},"expect":{"url":"http://a/b/c/"}}, +{"name":"uri_resolve_rfc_17","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":".."},"expect":{"url":"http://a/b/"}}, +{"name":"uri_resolve_rfc_18","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"../"},"expect":{"url":"http://a/b/"}}, +{"name":"uri_resolve_rfc_19","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"../g"},"expect":{"url":"http://a/b/g"}}, +{"name":"uri_resolve_rfc_20","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"../.."},"expect":{"url":"http://a/"}}, +{"name":"uri_resolve_rfc_21","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"../../"},"expect":{"url":"http://a/"}}, +{"name":"uri_resolve_rfc_22","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"../../g"},"expect":{"url":"http://a/g"}}, +{"name":"uri_resolve_rfc_23","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"../../../g"},"expect":{"url":"http://a/g"}}, +{"name":"uri_resolve_rfc_24","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"../../../../g"},"expect":{"url":"http://a/g"}}, +{"name":"uri_resolve_rfc_25","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"/./g"},"expect":{"url":"http://a/g"}}, +{"name":"uri_resolve_rfc_26","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"/../g"},"expect":{"url":"http://a/g"}}, +{"name":"uri_resolve_rfc_27","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g."},"expect":{"url":"http://a/b/c/g."}}, +{"name":"uri_resolve_rfc_28","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":".g"},"expect":{"url":"http://a/b/c/.g"}}, +{"name":"uri_resolve_rfc_29","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g.."},"expect":{"url":"http://a/b/c/g.."}}, +{"name":"uri_resolve_rfc_30","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"..g"},"expect":{"url":"http://a/b/c/..g"}}, +{"name":"uri_resolve_rfc_31","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"./../g"},"expect":{"url":"http://a/b/g"}}, +{"name":"uri_resolve_rfc_32","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"./g/."},"expect":{"url":"http://a/b/c/g/"}}, +{"name":"uri_resolve_rfc_33","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g/./h"},"expect":{"url":"http://a/b/c/g/h"}}, +{"name":"uri_resolve_rfc_34","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g/../h"},"expect":{"url":"http://a/b/c/h"}}, +{"name":"uri_resolve_rfc_35","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g;x=1/./y"},"expect":{"url":"http://a/b/c/g;x=1/y"}}, +{"name":"uri_resolve_rfc_36","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g;x=1/../y"},"expect":{"url":"http://a/b/c/y"}}, +{"name":"uri_resolve_rfc_37","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g?y/./x"},"expect":{"url":"http://a/b/c/g?y/./x"}}, +{"name":"uri_resolve_rfc_38","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g?y/../x"},"expect":{"url":"http://a/b/c/g?y/../x"}}, +{"name":"uri_resolve_rfc_39","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g#s/./x"},"expect":{"url":"http://a/b/c/g"}}, +{"name":"uri_resolve_rfc_40","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"g#s/../x"},"expect":{"url":"http://a/b/c/g"}}, +{"name":"uri_resolve_rfc_41","op":"uri_resolve","input":{"base":"http://a/b/c/d;p?q","reference":"http:g"},"expect":{"url":"http:g"}}, +{"name":"uri_resolve_alias_00","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"720/1201.0.wai#cid=00"},"expect":{"url":"https://origin.example/live/720/1201.0.wai"}}, +{"name":"uri_resolve_alias_01","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"./720/1201.0.wai"},"expect":{"url":"https://origin.example/live/720/1201.0.wai"}}, +{"name":"uri_resolve_alias_02","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"720/x/../1201.0.wai"},"expect":{"url":"https://origin.example/live/720/1201.0.wai"}}, +{"name":"uri_resolve_alias_03","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"%37%32%30/1201.0.wai"},"expect":{"url":"https://origin.example/live/720/1201.0.wai"}}, +{"name":"uri_resolve_alias_04","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"720/1201%2e0.wai"},"expect":{"url":"https://origin.example/live/720/1201.0.wai"}}, +{"name":"uri_resolve_alias_05","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"720/%2E/1201.0.wai"},"expect":{"url":"https://origin.example/live/720/1201.0.wai"}}, +{"name":"uri_resolve_alias_06","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"720%2F1201.0.wai"},"expect":{"url":"https://origin.example/live/720%2F1201.0.wai"}}, +{"name":"uri_resolve_alias_07","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"HTTPS://ORIGIN.EXAMPLE/live/720/1201.0.wai"},"expect":{"url":"https://origin.example/live/720/1201.0.wai"}}, +{"name":"uri_resolve_alias_08","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"//Origin.Example:443/live/720/1201.0.wai"},"expect":{"url":"https://origin.example:443/live/720/1201.0.wai"}}, +{"name":"uri_resolve_alias_09","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"720/1201.0.wai?x=%7e%41"},"expect":{"url":"https://origin.example/live/720/1201.0.wai?x=~A"}}, +{"name":"uri_resolve_alias_10","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"../../../720/1201.0.wai"},"expect":{"url":"https://origin.example/720/1201.0.wai"}}, +{"name":"uri_resolve_alias_11","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"a:b/c"},"expect":{"url":"a:b/c"}}, +{"name":"uri_resolve_alias_12","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"./a:b"},"expect":{"url":"https://origin.example/live/a:b"}}, +{"name":"uri_resolve_alias_13","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"user@x"},"expect":{"url":"https://origin.example/live/user@x"}}, +{"name":"uri_resolve_alias_14","op":"uri_resolve","input":{"base":"https://origin.example/live/wai-720.m3u8","reference":"//u%41@Host.example/p"},"expect":{"url":"https://uA@host.example/p"}}, +{"name":"unchanged_players","op":"unchanged","input":{"original":"src/players/master.m3u8","published":"players/master.m3u8","edit_published":[]},"expect":{"unchanged":true}}, +{"name":"unchanged_original_itself","op":"unchanged","input":{"original":"src/players/master.m3u8","published":"src/players/master.m3u8"},"expect":{"unchanged":true}}, +{"name":"unchanged_two_lines","op":"unchanged","input":{"original":"src/players/master.m3u8","published":"players/master.m3u8","edit_published":[["#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n","#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n"]]},"expect":{"error":"classical_changed"}}, +{"name":"unchanged_bandwidth_changed","op":"unchanged","input":{"original":"src/players/master.m3u8","published":"players/master.m3u8","edit_published":[["BANDWIDTH=10840","BANDWIDTH=10841"]]},"expect":{"error":"classical_changed"}}, +{"name":"unchanged_other_data_id","op":"unchanged","input":{"original":"src/players/master.m3u8","published":"players/master.m3u8","edit_published":[["science.transaction.wai.multivariant","science.transaction.wai.other"]]},"expect":{"error":"classical_changed"}}, +{"name":"unchanged_trailing_space","op":"unchanged","input":{"original":"src/players/master.m3u8","published":"players/master.m3u8","edit_published":[["VALUE=\"wai.m3u8\"\n","VALUE=\"wai.m3u8\" \n"]]},"expect":{"error":"classical_changed"}}, +{"name":"unchanged_first_line","op":"unchanged","input":{"original":"src/players/master.m3u8","published":"players/master.m3u8","edit_published":[["#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n",""],["#EXTM3U\n","#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n#EXTM3U\n"]]},"expect":{"error":"classical_changed"}}, +{"name":"unchanged_before_version","op":"unchanged","input":{"original":"src/players/master.m3u8","published":"players/master.m3u8","edit_published":[["#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n",""],["#EXTM3U\n","#EXTM3U\n#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n"]]},"expect":{"error":"classical_changed"}}, +{"name":"unchanged_between_variant_and_uri","op":"unchanged","input":{"original":"src/players/master.m3u8","published":"players/master.m3u8","edit_published":[["#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n",""],["classical/v0.m3u8","#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\nclassical/v0.m3u8"]]},"expect":{"error":"classical_changed"}}, +{"name":"unchanged_at_the_end","op":"unchanged","input":{"original":"src/players/master.m3u8","published":"players/master.m3u8","edit_published":[["#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n",""],["classical/v0.m3u8\n","classical/v0.m3u8\n#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n"]]},"expect":{"error":"classical_changed"}}, +{"name":"unchanged_blank_line_too","op":"unchanged","input":{"original":"src/players/master.m3u8","published":"players/master.m3u8","edit_published":[["#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n","#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n\n"]]},"expect":{"error":"classical_changed"}}, +{"name":"unchanged_crlf_line","op":"unchanged","input":{"original":"src/players/master.m3u8","published":"players/master.m3u8","edit_published":[["#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n","#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\r\n"]]},"expect":{"unchanged":true}}, +{"name":"unchanged_after_the_first_line","op":"unchanged","input":{"original_text":"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\nv.m3u8\n","published_text":"#EXTM3U\n#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n#EXT-X-STREAM-INF:BANDWIDTH=1\nv.m3u8\n"},"expect":{"unchanged":true}}, +{"name":"unchanged_bare_after_the_variant","op":"unchanged","input":{"original_text":"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\nv.m3u8\n","published_text":"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\n#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\nv.m3u8\n"},"expect":{"error":"classical_changed"}}, +{"name":"unchanged_no_place","op":"unchanged","input":{"original_text":"#EXTM3U\n#EXT-X-VERSION:7","published_text":"#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n"},"expect":{"error":"classical_changed"}}, +{"name":"playlist_receipt_playlists_o9_wai_180_m3u8","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json"},"expect":{"signed":"4b4657cea62905884efdfc59c357fd259910c5ddfd6c5314d6ed6c9894c80f13","parent":null}}, +{"name":"playlist_receipt_playlists_o10_wai_a_m3u8","op":"playlist_receipt","input":{"playlist":"playlists/o10/wai-a.m3u8","receipt":"playlists/o10/mr/24db5ea8900112df04426a7e28a68efa3ca77abdc07345af2c39724565715f77.json"},"expect":{"signed":"7c3b6480fcffa821e3d3d0e93594772159e18cdbffea5352ff72831d5260a1ae","parent":null}}, +{"name":"playlist_receipt_playlists_o11_wai_v_m3u8","op":"playlist_receipt","input":{"playlist":"playlists/o11/wai-v.m3u8","receipt":"playlists/o11/mr/044b6597093a22d2ae9cf86603fe564117bee5bb1b468424361da5ae48a81c79.json"},"expect":{"signed":"461a7871f56c0e69017be36a7dce1ca89f4501b3c546280a48b4de0e729398ea","parent":null}}, +{"name":"playlist_receipt_live_wai_r0_m3u8","op":"playlist_receipt","input":{"playlist":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json"},"expect":{"signed":"3ce1b1f8805024633d89312232438e6ff46022b5a7ba48c994338876b6a7c364","parent":null}}, +{"name":"playlist_receipt_live_wai_r1_m3u8","op":"playlist_receipt","input":{"playlist":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json"},"expect":{"signed":"eab14aa7350c082b1cc52b78c74eb5f2660f80ca6267a73a2ae9cd186df824ed","parent":"3ce1b1f8805024633d89312232438e6ff46022b5a7ba48c994338876b6a7c364"}}, +{"name":"playlist_receipt_live_wai_r2_m3u8","op":"playlist_receipt","input":{"playlist":"live/wai-r2.m3u8","receipt":"live/mr/3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900.json"},"expect":{"signed":"4ed29942f6a294bf03d422a1b76b93b7c0b84ff123b99ee2c26505649c68bdf6","parent":"eab14aa7350c082b1cc52b78c74eb5f2660f80ca6267a73a2ae9cd186df824ed"}}, +{"name":"playlist_receipt_live_wai_r3_m3u8","op":"playlist_receipt","input":{"playlist":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json"},"expect":{"signed":"dbc00c5ff81292325d272cbd6b83fbd4230a4fd626ffccdba54047bf0f6573ae","parent":"4ed29942f6a294bf03d422a1b76b93b7c0b84ff123b99ee2c26505649c68bdf6"}}, +{"name":"playlist_receipt_players_wai_v0_m3u8","op":"playlist_receipt","input":{"playlist":"players/wai-v0.m3u8","receipt":"players/mr/57bbf3d1af7c937c13ac72b729895532f68da3c5e55365cebdfa8d92dd73a9c5.json"},"expect":{"signed":"493155241cd03b53886e9bd1a210860033667f9cea89b45ca1849f0e54fc654d","parent":null}}, +{"name":"playlist_receipt_name","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","name":"0000.json"},"expect":{"error":"receipt_name_mismatch"}}, +{"name":"playlist_receipt_playlist_changed","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_playlist":[["#EXTINF:0.640,","#EXTINF:0.641,"]]},"expect":{"error":"playlist_receipt_mismatch"}}, +{"name":"playlist_receipt_viewers","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"viewers_served\":1","\"viewers_served\":2"]]},"expect":{"error":"playlist_receipt_invalid"}}, +{"name":"playlist_receipt_class","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"entitlement\":\"public\"","\"entitlement\":\"svod\""]]},"expect":{"error":"playlist_receipt_invalid"}}, +{"name":"playlist_receipt_extras_equivalent","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"extras\":[]","\"extras\":[{\"discontinuity\":false,\"gap\":false,\"program_date_time\":null,\"parts\":[],\"layers\":[],\"preload_hint\":null},{\"discontinuity\":false,\"gap\":false,\"program_date_time\":null,\"parts\":[],\"layers\":[],\"preload_hint\":null},{\"discontinuity\":false,\"gap\":false,\"program_date_time\":null,\"parts\":[],\"layers\":[],\"preload_hint\":null}]"]]},"expect":{"error":"playlist_receipt_invalid"}}, +{"name":"playlist_receipt_kind","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"kind\":\"wai.video.manifest\"","\"kind\":\"wai.video.package\""]]},"expect":{"error":"playlist_receipt_malformed"}}, +{"name":"playlist_receipt_unknown_member","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["{\"kind\"","{\"note\":1,\"kind\""]]},"expect":{"error":"playlist_receipt_malformed"}}, +{"name":"playlist_receipt_duplicate_first","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["{\"kind\"","{\"signer_id\":\"did:example:other\",\"kind\""]]},"expect":{"error":"playlist_receipt_malformed"}}, +{"name":"playlist_receipt_duplicate_last","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"signer_id\":\"did:example:http-vectors\"","\"signer_id\":\"did:example:http-vectors\",\"signer_id\":\"did:example:other\""]]},"expect":{"error":"playlist_receipt_malformed"}}, +{"name":"playlist_receipt_duplicate_same_value","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"signer_id\":\"did:example:http-vectors\"","\"signer_id\":\"did:example:http-vectors\",\"signer_id\":\"did:example:http-vectors\""]]},"expect":{"error":"playlist_receipt_malformed"}}, +{"name":"playlist_receipt_duplicate_in_class","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"geo\":\"\"","\"geo\":\"\",\"geo\":\"\""]]},"expect":{"error":"playlist_receipt_malformed"}}, +{"name":"playlist_receipt_duplicate_escaped_name","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["{\"kind\"","{\"\\u0073igner_id\":\"did:example:other\",\"kind\""]]},"expect":{"error":"playlist_receipt_malformed"}}, +{"name":"playlist_receipt_lone_surrogate","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"geo\":\"\"","\"geo\":\"\\ud800\""]]},"expect":{"error":"playlist_receipt_malformed"}}, +{"name":"playlist_receipt_lone_low_surrogate","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"signer_id\":\"did:example:http-vectors\"","\"signer_id\":\"did:example:http-vectors\\udfff\""]]},"expect":{"error":"playlist_receipt_malformed"}}, +{"name":"playlist_receipt_surrogate_pair","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"geo\":\"\"","\"geo\":\"\\ud83d\\ude00\""]]},"expect":{"error":"playlist_receipt_invalid"}}, +{"name":"playlist_receipt_ceiling_past_2_53","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"joule_ceiling_micro\":null","\"joule_ceiling_micro\":9007199254740992"]]},"expect":{"error":"playlist_receipt_malformed"}}, +{"name":"playlist_receipt_ceiling_written_out","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"joule_ceiling_micro\":null","\"joule_ceiling_micro\":18446744073709551615"]]},"expect":{"error":"playlist_receipt_malformed"}}, +{"name":"playlist_receipt_ceiling_number_for_none","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"joule_ceiling_micro\":null","\"joule_ceiling_micro\":9007199254740991"]]},"expect":{"error":"playlist_receipt_invalid"}}, +{"name":"playlist_receipt_window_past_2_53","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"playlists/o9/mr/e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798.json","edit_receipt":[["\"window\":0","\"window\":9007199254740992"]]},"expect":{"error":"playlist_receipt_malformed"}}, +{"name":"playlist_receipt_grant_ceiling_changed","op":"playlist_receipt","input":{"playlist":"playlists/o10/wai-a.m3u8","receipt":"playlists/o10/mr/24db5ea8900112df04426a7e28a68efa3ca77abdc07345af2c39724565715f77.json","edit_receipt":[["\"joule_ceiling_micro\":5000000","\"joule_ceiling_micro\":5000001"]]},"expect":{"error":"playlist_receipt_invalid"}}, +{"name":"playlist_receipt_grant_funds_dropped","op":"playlist_receipt","input":{"playlist":"playlists/o10/wai-a.m3u8","receipt":"playlists/o10/mr/24db5ea8900112df04426a7e28a68efa3ca77abdc07345af2c39724565715f77.json","edit_receipt":[["\"funds_ceiling\":250","\"funds_ceiling\":null"]]},"expect":{"error":"playlist_receipt_invalid"}}, +{"name":"playlist_receipt_sig_key_identity","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"cases/sig-key-identity.json","name":false},"expect":{"error":"playlist_receipt_invalid"}}, +{"name":"playlist_receipt_sig_key_identity_long","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"cases/sig-key-identity-long.json","name":false},"expect":{"error":"playlist_receipt_invalid"}}, +{"name":"playlist_receipt_sig_r_small_order","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"cases/sig-r-small-order.json","name":false},"expect":{"error":"playlist_receipt_invalid"}}, +{"name":"playlist_receipt_sig_r_non_canonical","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"cases/sig-r-non-canonical.json","name":false},"expect":{"error":"playlist_receipt_invalid"}}, +{"name":"playlist_receipt_sig_key_base_point","op":"playlist_receipt","input":{"playlist":"playlists/o9/wai-180.m3u8","receipt":"cases/sig-key-base-point.json","name":false},"expect":{"signed":"9cec31c4957646fe2bb595b69590f102377d9a6754db72d613cbfad041d55d06","parent":null}}, +{"name":"playlist_receipt_part_independent","op":"playlist_receipt","input":{"playlist":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","edit_playlist":[["DURATION=0.320,INDEPENDENT=YES,URI=\"720/1200.3","DURATION=0.320,URI=\"720/1200.3"]]},"expect":{"error":"playlist_receipt_mismatch"}}, +{"name":"playlist_receipt_parts_reordered","op":"playlist_receipt","input":{"playlist":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","edit_receipt":[["\"uri\":\"720/1200.3.wai","\"uri\":\"720/1200.X.wai"],["\"uri\":\"720/1200.4.wai","\"uri\":\"720/1200.3.wai"],["\"uri\":\"720/1200.X.wai","\"uri\":\"720/1200.4.wai"]]},"expect":{"error":"playlist_receipt_mismatch"}}, +{"name":"live_edge","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"live/wai-r2.m3u8","receipt":"live/mr/3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900.json","classical":"src/live/classical-r2.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1200.5.wai","object":"O12p0_05"},{"alias":"720/1201.3.wai","object":"O12p1_03"},{"alias":"720/1201.7.wai","object":"O12p1_08"},{"alias":"720/1200.2.wai","object":"O12p0_02","patch_object":[[400,"00"]]},{"alias":"720/1202.0.wai","object":"O12p1_11"},{"alias":"1200.m4s","object":"O12s0"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1200.5.wai","listed_in":1,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":2,"outcome":"accepted","held":"signed"},{"alias":"720/1201.7.wai","listed_in":2,"outcome":"object_digest_mismatch","held":null},{"alias":"720/1200.2.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1202.0.wai","listed_in":null,"outcome":"not_listed","held":null},{"alias":"1200.m4s","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_edge_revision_changed","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"live/wai-r2.m3u8","receipt":"live/mr/3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900.json","classical":"src/live/classical-r2.m3u8","edit_wai":[["720/1201.7.wai#cid=173c227ba468e8809e9b7b016264bd86dc1310c7681a732e9d0886373411915f","720/1201.7.wai#cid=db17bd07b023ddcc31b057df9d0da1354b148c3920489e345201a8d3c2c72bed"]]},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"fetches":[{"alias":"720/1201.7.wai","object":"O12p1_08"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"playlist_receipt_mismatch","chained":null,"walked":null},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":1}],"fetches":[{"alias":"720/1201.7.wai","listed_in":2,"outcome":"accepted","held":"listed"},{"alias":"720/1201.3.wai","listed_in":2,"outcome":"accepted","held":"signed"}]}}, +{"name":"live_edge_unchained","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"live/wai-r2.m3u8","receipt":"cases/live-r2-orphan.json","classical":"src/live/classical-r2.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"fetches":[{"alias":"720/1201.3.wai","object":"O12p1_03"},{"alias":"720/1200.0.wai","object":"O12p0_00"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"playlist_receipt_unchained","walked":null},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":1}],"fetches":[{"alias":"720/1201.3.wai","listed_in":2,"outcome":"accepted","held":"signed"},{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"}]}}, +{"name":"live_edge_unchanged_reload","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"live/wai-r2.m3u8","receipt":"live/mr/3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900.json","classical":"src/live/classical-r2.m3u8"},{"wai":"live/wai-r2.m3u8","receipt":"live/mr/3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900.json","classical":"src/live/classical-r2.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"unchanged","walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"unchanged","walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":3,"outcome":"accepted","held":"signed"}]}}, +{"name":"live_edge_skip","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r2.m3u8","receipt":"live/mr/3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900.json","classical":"src/live/classical-r2.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":1},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":1,"outcome":"accepted","held":"signed"}]}}, +{"name":"live_edge_skip_two","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":2}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_edge_reanchored","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"walk_bound":1,"fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"reanchored","walked":1}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_edge_reanchored_then_reloaded","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"walk_bound":1,"fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"reanchored","walked":1},{"adjacent":true,"listing":true,"receipt":"signed","chained":"unchanged","walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"unchanged","walked":0}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_edge_reanchored_skipping","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r2.m3u8","receipt":"live/mr/3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900.json","classical":"src/live/classical-r2.m3u8"}],"walk_bound":0,"fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"reanchored","walked":0}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":1,"outcome":"accepted","held":"signed"}]}}, +{"name":"live_edge_reanchor_not_backwards","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"}],"walk_bound":0,"fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"reanchored","walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"playlist_receipt_walk_exceeded","walked":null}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_edge_parent_substituted","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"parents":"cases/walk-substituted/mr","fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"receipt_name_mismatch","walked":null}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_edge_parent_invalid","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"cases/walk-invalid/r3.json","classical":"src/live/classical-r3.m3u8"}],"parents":"cases/walk-invalid/mr","fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"playlist_receipt_invalid","walked":null}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_edge_walk_at_bound","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"walk_bound":2,"fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":2}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_edge_parent_missing","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"parents":"cases/no-receipts","fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"playlist_receipt_parent_missing","walked":null}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_edge_other_key","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"live/wai-r2.m3u8","receipt":"cases/other-key/r2.json","classical":"src/live/classical-r2.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"playlist_receipt_unchained","walked":null},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":1}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":2,"outcome":"accepted","held":"signed"}]}}, +{"name":"live_edge_other_key_walked","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"cases/other-key/r3.json","classical":"src/live/classical-r3.m3u8"}],"parents":"cases/other-key/mr","fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"playlist_receipt_unchained","walked":null}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_relisted_alias","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"cases/relist/wai-r2.m3u8","receipt":"cases/relist/r2.json","classical":"src/live/classical-r2.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"live/mr/b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e.json","classical":"src/live/classical-r3.m3u8"}],"fetches":[{"alias":"720/1201.0.wai","object":"O12p1_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":"playlist_listing_rebound","receipt":null,"chained":null,"walked":null},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":1}],"fetches":[{"alias":"720/1201.0.wai","listed_in":1,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_relisted_alias_walked","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"live/wai-r3.m3u8","receipt":"cases/relist/r3-after.json","classical":"src/live/classical-r3.m3u8"}],"parents":"cases/relist/mr","fetches":[{"alias":"720/1201.0.wai","object":"O12p1_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":"playlist_listing_rebound","walked":null}],"fetches":[{"alias":"720/1201.0.wai","listed_in":1,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_resequenced","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"live/wai-r2.m3u8","receipt":"live/mr/3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900.json","classical":"src/live/classical-r2.m3u8"},{"wai":"cases/relist/wai-r3.m3u8","receipt":"cases/relist/r3.json","classical":"src/live/classical-r3.m3u8"}],"fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":"playlist_listing_rebound","receipt":null,"chained":null,"walked":null}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":2,"outcome":"accepted","held":"signed"}]}}, +{"name":"live_relisted_dot_alias","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"cases/relist/wai-r2-dot.m3u8","receipt":"cases/relist/r2-dot.json","classical":"src/live/classical-r2.m3u8"}],"fetches":[{"alias":"720/1201.0.wai","object":"O12p1_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":"playlist_listing_rebound","receipt":null,"chained":null,"walked":null}],"fetches":[{"alias":"720/1201.0.wai","listed_in":1,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":null,"outcome":"not_listed","held":null}]}}, +{"name":"live_dot_alias_same_object","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"cases/relist/wai-r2-dot-same.m3u8","receipt":"cases/relist/r2-dot-same.json","classical":"src/live/classical-r2.m3u8"}],"fetches":[{"alias":"720/1201.0.wai","object":"O12p1_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0}],"fetches":[{"alias":"720/1201.0.wai","listed_in":1,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":2,"outcome":"accepted","held":"signed"}]}}, +{"name":"live_gap_withdraws_a_segment","op":"live","input":{"revisions":[{"wai":"live/wai-r0.m3u8","receipt":"live/mr/6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9.json","classical":"src/live/classical-r0.m3u8"},{"wai":"live/wai-r1.m3u8","receipt":"live/mr/091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271.json","classical":"src/live/classical-r1.m3u8"},{"wai":"live/wai-r2.m3u8","receipt":"live/mr/3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900.json","classical":"src/live/classical-r2.m3u8"},{"wai":"cases/relist/wai-r3-gap.m3u8","receipt":"cases/relist/r3-gap.json","classical":"src/live/classical-r3.m3u8"}],"fetches":[{"alias":"720/1200.0.wai","object":"O12p0_00"},{"alias":"720/1201.3.wai","object":"O12p1_03"}]},"expect":{"revisions":[{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":true,"receipt":"signed","chained":true,"walked":0},{"adjacent":true,"listing":"playlist_listing_rebound","receipt":null,"chained":null,"walked":null}],"fetches":[{"alias":"720/1200.0.wai","listed_in":0,"outcome":"accepted","held":"signed"},{"alias":"720/1201.3.wai","listed_in":2,"outcome":"accepted","held":"signed"}]}}, +{"name":"abr_throughput_00","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":28203,"samples":[[1000,1259305,570]],"last":2,"option_bytes":[229507,231557,344895],"option_joules_micro":[3913,null,3815],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_throughput_01","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":27557,"samples":[],"last":0,"option_bytes":[48543,69733,178222,189003,255156,340556],"option_joules_micro":[250,4083,4219,null,null,null],"joule_budget_micro":1274,"segment_ms":2000}},"expect":{"choice":0,"budget":"within"}}, +{"name":"abr_throughput_02","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":27339,"samples":[],"last":null,"option_bytes":[132854,232413],"option_joules_micro":[null,315],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_throughput_03","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":5325,"samples":[[1000,691100,1108],[2000,1798923,186],[3000,499287,1537],[4000,729119,142]],"last":null,"option_bytes":[47614],"option_joules_micro":[2441],"joule_budget_micro":75,"segment_ms":3840}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_throughput_04","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":29234,"samples":[[1000,96310,457],[2000,692823,1361],[3000,532395,2859],[4000,1569349,2674],[5000,309913,2047]],"last":2,"option_bytes":[31692,61260,114256,118871,321713],"option_joules_micro":[3464,1013,null,1819,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_throughput_05","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":38786,"samples":[[1000,1346651,872],[2000,1898768,937],[3000,1172638,1448]],"last":3,"option_bytes":[96715,141611,162979,171720,184391,300283],"option_joules_micro":[null,1397,3978,58,3341,null],"joule_budget_micro":3328,"segment_ms":1000}},"expect":{"choice":3,"budget":"lowered"}}, +{"name":"abr_throughput_06","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":39560,"samples":[],"last":null,"option_bytes":[82831,144942],"option_joules_micro":[null,1121],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_throughput_07","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":38581,"samples":[[1000,751117,26]],"last":null,"option_bytes":[237209],"option_joules_micro":[null],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_throughput_08","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":19262,"samples":[[1000,597546,2693],[2000,684159,2225]],"last":0,"option_bytes":[168294],"option_joules_micro":[null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_throughput_09","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":8381,"samples":[[1000,1385410,319],[2000,1460612,2797]],"last":1,"option_bytes":[65202,65696,184785,346631,371704],"option_joules_micro":[1624,3288,4389,3212,null],"joule_budget_micro":4577,"segment_ms":4000}},"expect":{"choice":3,"budget":"lowered"}}, +{"name":"abr_throughput_10","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":6439,"samples":[[1000,1642862,2072],[2000,1724810,2415]],"last":null,"option_bytes":[221303,254054,282799,397258],"option_joules_micro":[2984,4714,951,2746],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_throughput_11","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":12659,"samples":[[1000,1332812,1319],[2000,939957,2721],[3000,1367969,837],[4000,1599764,1867]],"last":2,"option_bytes":[80403,120456,245927],"option_joules_micro":[3147,null,2885],"joule_budget_micro":5230,"segment_ms":200}},"expect":{"choice":0,"budget":"lowered"}}, +{"name":"abr_throughput_12","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":16498,"samples":[[1000,495433,2847],[2000,1138099,1332],[3000,118438,752]],"last":null,"option_bytes":[5136,209878,237512,306265,314855],"option_joules_micro":[2083,1131,2370,3206,2827],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_throughput_13","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":23508,"samples":[],"last":null,"option_bytes":[106126,244473,290603,332362,353222,363306],"option_joules_micro":[null,2545,null,4788,4650,null],"joule_budget_micro":5986,"segment_ms":4000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_throughput_14","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":25907,"samples":[[1000,1865109,2358],[2000,1599056,2350]],"last":null,"option_bytes":[27081,34658],"option_joules_micro":[789,4649],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_throughput_15","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":34250,"samples":[],"last":null,"option_bytes":[35851,202585,220271,236085,248931,345739],"option_joules_micro":[1723,1554,null,199,781,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_throughput_16","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":23347,"samples":[],"last":null,"option_bytes":[200798,293314,359455],"option_joules_micro":[1339,null,2473],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_throughput_17","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":600,"samples":[[1000,1896617,2224],[2000,1558447,1692],[3000,765327,1419]],"last":1,"option_bytes":[163436,186366,272666,275011,340603],"option_joules_micro":[3991,4424,null,3790,3355],"joule_budget_micro":1411,"segment_ms":2000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_throughput_18","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":13891,"samples":[[1000,954290,1322],[2000,1359600,1307],[3000,579442,570],[4000,1316832,1762],[5000,1974497,2440]],"last":null,"option_bytes":[73894,79372,276740],"option_joules_micro":[2593,1518,null],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_throughput_19","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":24789,"samples":[[1000,560355,44],[2000,1168366,2494],[3000,1768747,727],[4000,1437641,603]],"last":null,"option_bytes":[163014,260410],"option_joules_micro":[null,1131],"joule_budget_micro":4590,"segment_ms":200}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_throughput_20","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":15721,"samples":[[1000,1211483,39],[2000,301375,2007]],"last":0,"option_bytes":[10369,280040],"option_joules_micro":[null,668],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_throughput_21","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":33334,"samples":[[1000,1447610,2507],[2000,1657478,2231],[3000,450998,1363]],"last":null,"option_bytes":[183723,251782,272819,337959,393865,395099],"option_joules_micro":[21,null,1077,4664,303,null],"joule_budget_micro":4277,"segment_ms":3840}},"expect":{"choice":4,"budget":"lowered"}}, +{"name":"abr_throughput_22","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":35031,"samples":[[1000,324127,2144],[2000,1092870,1089]],"last":null,"option_bytes":[352081],"option_joules_micro":[1053],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_throughput_23","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":19071,"samples":[[1000,287343,1171],[2000,964313,2942],[3000,1704757,2128],[4000,1385776,95]],"last":null,"option_bytes":[72845,114716,340475,374581,379279],"option_joules_micro":[4160,2173,704,null,4235],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_throughput_24","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":6320,"samples":[[1000,180442,1433],[2000,1296876,1896],[3000,1459973,2827],[4000,906413,1788],[5000,708548,914]],"last":2,"option_bytes":[18928,32680,119510,244405,348963],"option_joules_micro":[4323,null,3719,334,2024],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_throughput_25","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":21905,"samples":[[1000,1343712,1367],[2000,469695,2726],[3000,1904792,2871],[4000,1325799,1677],[5000,250625,1526]],"last":null,"option_bytes":[45241,215743,242203,249812],"option_joules_micro":[4983,null,4421,2631],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_throughput_26","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":12394,"samples":[[1000,1228723,1636],[2000,1510412,794],[3000,100188,1057]],"last":3,"option_bytes":[10898,111800,122856,238090],"option_joules_micro":[4652,null,null,3096],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_throughput_27","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":28905,"samples":[[1000,694916,82],[2000,112506,981],[3000,1300898,439],[4000,1895031,165]],"last":null,"option_bytes":[256221],"option_joules_micro":[1154],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_throughput_28","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":10426,"samples":[[1000,1429554,3],[2000,119924,2406],[3000,692890,1773],[4000,1863026,2411]],"last":null,"option_bytes":[118910,163595,350359],"option_joules_micro":[null,2770,3426],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_throughput_29","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":9070,"samples":[[1000,1012061,1185]],"last":null,"option_bytes":[18854,33308,77115,281011],"option_joules_micro":[1937,1674,2859,1446],"joule_budget_micro":371,"segment_ms":1000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_throughput_30","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":15557,"samples":[[1000,756365,795],[2000,1757679,1638]],"last":null,"option_bytes":[368004],"option_joules_micro":[null],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_throughput_31","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":26456,"samples":[[1000,1148772,1885],[2000,1378089,1925],[3000,1488056,561],[4000,615024,2153],[5000,203736,1988]],"last":null,"option_bytes":[25456,55188,175334,372052,395592],"option_joules_micro":[2982,null,1043,null,null],"joule_budget_micro":1948,"segment_ms":3840}},"expect":{"choice":2,"budget":"lowered"}}, +{"name":"abr_throughput_32","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":13482,"samples":[[1000,353535,461],[2000,100125,2264],[3000,1549597,940]],"last":null,"option_bytes":[71315,160507,171934,384830],"option_joules_micro":[null,4992,null,4405],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_throughput_33","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":33100,"samples":[[1000,1213885,1167],[2000,361434,1921]],"last":null,"option_bytes":[86339,106234,179258,247949],"option_joules_micro":[null,123,null,null],"joule_budget_micro":5271,"segment_ms":1000}},"expect":{"choice":1,"budget":"lowered"}}, +{"name":"abr_throughput_34","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":9326,"samples":[[1000,49874,1866],[2000,429238,1510],[3000,1332114,425],[4000,763830,1566],[5000,1149134,46]],"last":null,"option_bytes":[69890,101912,107255,206376,244677,356206],"option_joules_micro":[373,740,2549,383,2834,4872],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_throughput_35","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":2542,"samples":[[1000,293117,2313]],"last":0,"option_bytes":[63930,141450,153125,187898,230096,347497],"option_joules_micro":[58,3606,4416,null,null,null],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_throughput_36","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":33929,"samples":[[1000,59337,1305]],"last":2,"option_bytes":[160052,162794,223319,364293,366948],"option_joules_micro":[2111,2846,2816,774,null],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_throughput_37","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":23972,"samples":[[1000,1365119,332]],"last":1,"option_bytes":[26256,32808,162909,197504,315792,317227],"option_joules_micro":[null,1960,1542,null,1314,623],"joule_budget_micro":5741,"segment_ms":3840}},"expect":{"choice":5,"budget":"within"}}, +{"name":"abr_throughput_38","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":34202,"samples":[[1000,2498,1932],[2000,740286,1424],[3000,1964512,2166],[4000,1463791,994],[5000,833109,438]],"last":null,"option_bytes":[4006,177173,200028,293451,296992,351459],"option_joules_micro":[null,2760,3007,4566,1624,2408],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_throughput_39","op":"abr","input":{"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":6714,"samples":[[1000,1739427,69],[2000,1968256,622],[3000,1020785,2536],[4000,1578878,2178],[5000,456040,2675]],"last":null,"option_bytes":[113412,155507,211677,215466,331531,346524],"option_joules_micro":[4643,4087,1312,2992,null,4122],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_buffer_based_00","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":37166,"samples":[[1000,1979410,9]],"last":1,"option_bytes":[210016,308333,345583],"option_joules_micro":[null,2474,null],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_buffer_based_01","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":16121,"samples":[[1000,570802,767]],"last":5,"option_bytes":[204621,284313,289434,306386,318982,351290],"option_joules_micro":[null,1352,null,null,3924,null],"joule_budget_micro":4692,"segment_ms":200}},"expect":{"choice":4,"budget":"lowered"}}, +{"name":"abr_buffer_based_02","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":29777,"samples":[[1000,1565417,2877],[2000,1095405,1838],[3000,1213452,1830],[4000,1561082,2035],[5000,514815,2890]],"last":null,"option_bytes":[125752,240457],"option_joules_micro":[null,null],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_buffer_based_03","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":2271,"samples":[[1000,1844685,1561],[2000,1660953,1396],[3000,1046634,1653],[4000,572996,984]],"last":null,"option_bytes":[78556,128536,136533,216821,223315],"option_joules_micro":[null,null,null,563,4718],"joule_budget_micro":4470,"segment_ms":3840}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_buffer_based_04","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":10913,"samples":[[1000,1293814,1998],[2000,1252255,1484],[3000,443443,1964]],"last":null,"option_bytes":[170679],"option_joules_micro":[null],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_buffer_based_05","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":952,"samples":[[1000,787637,1089],[2000,1884870,2269],[3000,1428241,684],[4000,663641,1609],[5000,512905,917]],"last":null,"option_bytes":[27588,215824,225817,302822,314142,359134],"option_joules_micro":[286,2738,439,519,2188,null],"joule_budget_micro":3150,"segment_ms":200}},"expect":{"choice":0,"budget":"within"}}, +{"name":"abr_buffer_based_06","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":4978,"samples":[[1000,331389,564],[2000,827587,787],[3000,797362,361],[4000,253656,2761],[5000,772285,657]],"last":0,"option_bytes":[145164],"option_joules_micro":[2801],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_buffer_based_07","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":29335,"samples":[],"last":null,"option_bytes":[70464,132583,186315],"option_joules_micro":[2199,4129,null],"joule_budget_micro":662,"segment_ms":4000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_buffer_based_08","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":37749,"samples":[[1000,1709563,19]],"last":null,"option_bytes":[69175,85110,139703,199776,217765],"option_joules_micro":[4958,null,2129,null,2528],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_buffer_based_09","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":39078,"samples":[[1000,1551825,1508],[2000,654267,1691],[3000,338056,1616]],"last":null,"option_bytes":[2240,35831,107259,143553,219593],"option_joules_micro":[2163,238,3509,2438,3908],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_buffer_based_10","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":36561,"samples":[[1000,271282,2798],[2000,430309,868],[3000,46995,1685]],"last":null,"option_bytes":[27471,245810],"option_joules_micro":[1924,102],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_buffer_based_11","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":28890,"samples":[[1000,1226472,2092],[2000,737898,352],[3000,1925300,2584],[4000,270282,2632],[5000,1390372,242]],"last":null,"option_bytes":[31361,140603,176358,281536],"option_joules_micro":[397,2368,null,595],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_buffer_based_12","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":11976,"samples":[[1000,539711,1921],[2000,502892,1851],[3000,501448,2190],[4000,1799944,1647],[5000,1645324,2591]],"last":0,"option_bytes":[321858],"option_joules_micro":[2279],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_buffer_based_13","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":35463,"samples":[[1000,294543,579]],"last":null,"option_bytes":[62094,85789],"option_joules_micro":[4936,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_buffer_based_14","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":1866,"samples":[[1000,1377500,831],[2000,187295,2750],[3000,292627,1265],[4000,442366,702],[5000,1924865,1255]],"last":4,"option_bytes":[3210,213072,217510,287820,295523,356470],"option_joules_micro":[null,817,null,2337,3507,3573],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_buffer_based_15","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":27309,"samples":[[1000,111715,2526],[2000,231034,979]],"last":null,"option_bytes":[314026,391252],"option_joules_micro":[2964,4898],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_buffer_based_16","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":36242,"samples":[[1000,443958,2187],[2000,807690,546],[3000,208080,638]],"last":4,"option_bytes":[168938,185911,280205,302365,376909,391121],"option_joules_micro":[1840,3254,3948,2221,1166,2731],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_buffer_based_17","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":24891,"samples":[[1000,829665,2311],[2000,714803,2411],[3000,225237,1950],[4000,1067161,1421]],"last":null,"option_bytes":[59468,110431,164038,234549,259436,283866],"option_joules_micro":[null,null,null,3929,null,620],"joule_budget_micro":4362,"segment_ms":1000}},"expect":{"choice":5,"budget":"within"}}, +{"name":"abr_buffer_based_18","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":35958,"samples":[[1000,1431039,155],[2000,683213,535],[3000,513515,939],[4000,1101294,1059]],"last":3,"option_bytes":[8921,48253,59392,199835,323599],"option_joules_micro":[3123,null,22,589,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_buffer_based_19","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":20294,"samples":[[1000,551462,1444],[2000,1791244,1418],[3000,1245642,296],[4000,1987993,542],[5000,800295,2462]],"last":1,"option_bytes":[204577,289889],"option_joules_micro":[1025,2509],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_buffer_based_20","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":14345,"samples":[[1000,1036116,1218]],"last":5,"option_bytes":[83386,133160,160689,161213,221932,264986],"option_joules_micro":[null,3026,3427,null,989,1697],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_buffer_based_21","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":23761,"samples":[[1000,764827,1975],[2000,900986,2110],[3000,821380,785]],"last":2,"option_bytes":[9880,45476,64655,65611,85145,134840],"option_joules_micro":[90,null,2781,null,1005,3558],"joule_budget_micro":5306,"segment_ms":200}},"expect":{"choice":5,"budget":"within"}}, +{"name":"abr_buffer_based_22","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":21610,"samples":[[1000,1908393,1562],[2000,383657,466],[3000,663699,1533]],"last":5,"option_bytes":[3283,41158,118561,140465,210226,241740],"option_joules_micro":[null,null,2555,2681,435,1392],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_buffer_based_23","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":38379,"samples":[[1000,809805,2288],[2000,765096,882]],"last":null,"option_bytes":[86472,152234,248870,302446,366421,401120],"option_joules_micro":[null,1913,1140,1723,null,2002],"joule_budget_micro":5919,"segment_ms":200}},"expect":{"choice":5,"budget":"within"}}, +{"name":"abr_buffer_based_24","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":379,"samples":[[1000,1356046,2126],[2000,1074031,1592],[3000,999666,807],[4000,1433111,704]],"last":null,"option_bytes":[61493,112368,201303,359641],"option_joules_micro":[1956,1863,92,null],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_buffer_based_25","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":34646,"samples":[[1000,1525189,98],[2000,1749823,264],[3000,1935984,2297],[4000,1624790,2332],[5000,362616,1156]],"last":null,"option_bytes":[158338,237699,247119,263972,306843],"option_joules_micro":[1459,3809,1529,870,4339],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_buffer_based_26","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":16817,"samples":[[1000,1886518,1146]],"last":0,"option_bytes":[186622,302800,354224],"option_joules_micro":[null,null,4044],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_buffer_based_27","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":37746,"samples":[[1000,1825008,1445],[2000,1662378,2169],[3000,1309264,253]],"last":5,"option_bytes":[10202,49128,135356,301136,307742,318281],"option_joules_micro":[1963,647,null,4351,2310,null],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_buffer_based_28","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":27041,"samples":[[1000,875003,2914],[2000,475096,1811]],"last":0,"option_bytes":[190765,197897,220330,321218,332550],"option_joules_micro":[1384,628,null,5000,4239],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_buffer_based_29","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":28564,"samples":[[1000,1903050,1616],[2000,805741,715],[3000,1156735,928]],"last":0,"option_bytes":[132147,391510],"option_joules_micro":[3736,2596],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_buffer_based_30","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":21410,"samples":[[1000,1469834,659]],"last":null,"option_bytes":[283475,308777,311223],"option_joules_micro":[765,2079,4078],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_buffer_based_31","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":23415,"samples":[[1000,442019,2287],[2000,1783989,771],[3000,1466540,996]],"last":null,"option_bytes":[32947,125397,157859,246761,271145],"option_joules_micro":[null,null,2092,1535,4449],"joule_budget_micro":1292,"segment_ms":3840}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_buffer_based_32","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":11395,"samples":[[1000,343990,2406],[2000,777840,551]],"last":0,"option_bytes":[270717],"option_joules_micro":[2843],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_buffer_based_33","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":22365,"samples":[[1000,17516,1911],[2000,1837902,2638]],"last":0,"option_bytes":[102200,185038,186610],"option_joules_micro":[2128,4256,1647],"joule_budget_micro":2909,"segment_ms":1000}},"expect":{"choice":2,"budget":"within"}}, +{"name":"abr_buffer_based_34","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":2245,"samples":[],"last":null,"option_bytes":[137626,366867],"option_joules_micro":[2133,32],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_buffer_based_35","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":13932,"samples":[[1000,1775327,142],[2000,740968,1291],[3000,1979467,186]],"last":0,"option_bytes":[26057],"option_joules_micro":[null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_buffer_based_36","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":39383,"samples":[[1000,353676,1366],[2000,1490365,871],[3000,218806,2455],[4000,681126,1379],[5000,1439922,1626]],"last":0,"option_bytes":[45541,153009,213271,289249],"option_joules_micro":[3119,null,null,null],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_buffer_based_37","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":16367,"samples":[[1000,89442,713],[2000,695745,720],[3000,866081,2300],[4000,526205,617]],"last":null,"option_bytes":[44680,102214,198951,382909],"option_joules_micro":[null,null,null,null],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_buffer_based_38","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":19131,"samples":[[1000,353051,2579],[2000,1414361,57],[3000,577597,1301],[4000,1885009,776]],"last":1,"option_bytes":[135357,390901],"option_joules_micro":[null,2104],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_buffer_based_39","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":22639,"samples":[[1000,807717,2076],[2000,1374312,2528]],"last":0,"option_bytes":[72616],"option_joules_micro":[null],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_bola_like_00","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":33677,"samples":[[1000,829395,2963],[2000,1546059,2983]],"last":null,"option_bytes":[31524,86039,143379,169081],"option_joules_micro":[null,327,81,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_bola_like_01","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":3645,"samples":[[1000,1466360,26],[2000,1620323,2415],[3000,1649943,2525]],"last":0,"option_bytes":[345399],"option_joules_micro":[1409],"joule_budget_micro":3910,"segment_ms":1000}},"expect":{"choice":0,"budget":"within"}}, +{"name":"abr_bola_like_02","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":24831,"samples":[],"last":1,"option_bytes":[111510,190965],"option_joules_micro":[1410,null],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_bola_like_03","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":28818,"samples":[[1000,393581,121],[2000,1857427,184],[3000,1438715,1969],[4000,462861,507],[5000,1779704,732]],"last":null,"option_bytes":[55476,242754],"option_joules_micro":[null,1180],"joule_budget_micro":1617,"segment_ms":4000}},"expect":{"choice":1,"budget":"within"}}, +{"name":"abr_bola_like_04","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":24993,"samples":[[1000,628261,266],[2000,1311093,2490],[3000,1896231,2508],[4000,1899946,1549],[5000,1473270,521]],"last":3,"option_bytes":[12156,93174,102688,115312,200006],"option_joules_micro":[null,null,null,null,3620],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_bola_like_05","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":36964,"samples":[[1000,1635778,1330]],"last":0,"option_bytes":[180898,209837,230082,260672],"option_joules_micro":[2254,null,null,3133],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_bola_like_06","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":33333,"samples":[[1000,1928993,2256],[2000,1990635,1460],[3000,985446,157],[4000,1448177,1178]],"last":null,"option_bytes":[174914,277374,359614,400433],"option_joules_micro":[3301,2076,null,2638],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_bola_like_07","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":14770,"samples":[[1000,1194018,1373],[2000,259042,1066],[3000,1462106,1136]],"last":null,"option_bytes":[243135,292490],"option_joules_micro":[4054,null],"joule_budget_micro":1151,"segment_ms":4000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_bola_like_08","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":27980,"samples":[[1000,141644,2588],[2000,1022414,964],[3000,388313,362],[4000,900844,2246]],"last":null,"option_bytes":[35175,224251,379021],"option_joules_micro":[3222,3467,1675],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_bola_like_09","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":25379,"samples":[[1000,1891490,2917],[2000,1154608,571],[3000,919253,627],[4000,77454,1835]],"last":null,"option_bytes":[33791,62122,140623,207294,208269,277356],"option_joules_micro":[1522,2250,1056,null,3249,4928],"joule_budget_micro":854,"segment_ms":1000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_bola_like_10","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":32840,"samples":[[1000,1081027,2618]],"last":0,"option_bytes":[104494,230609],"option_joules_micro":[1662,62],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_bola_like_11","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":36145,"samples":[[1000,1966685,548]],"last":null,"option_bytes":[53981,203768],"option_joules_micro":[1104,777],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_bola_like_12","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":25512,"samples":[[1000,1037585,2300],[2000,1988690,1445],[3000,258576,2439]],"last":1,"option_bytes":[2816,214121,330531],"option_joules_micro":[3106,4288,2871],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_bola_like_13","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":15328,"samples":[[1000,494435,71],[2000,1770296,457],[3000,1901828,1981],[4000,1203235,2930],[5000,801814,2007]],"last":null,"option_bytes":[50866,121719,302006,333897,379200],"option_joules_micro":[346,null,1998,2201,2086],"joule_budget_micro":3860,"segment_ms":200}},"expect":{"choice":4,"budget":"within"}}, +{"name":"abr_bola_like_14","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":1929,"samples":[[1000,79637,15],[2000,875308,2757],[3000,4315,1389]],"last":1,"option_bytes":[49512,283176,330852],"option_joules_micro":[3828,1327,3321],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_bola_like_15","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":37908,"samples":[[1000,177163,2609],[2000,1224028,1052],[3000,1827069,573],[4000,92334,871],[5000,1925035,392]],"last":0,"option_bytes":[134154,163996],"option_joules_micro":[null,1026],"joule_budget_micro":1362,"segment_ms":3840}},"expect":{"choice":1,"budget":"within"}}, +{"name":"abr_bola_like_16","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":18539,"samples":[],"last":1,"option_bytes":[32923,149086,206071,219296,344716,350512],"option_joules_micro":[null,null,null,null,4330,3386],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_bola_like_17","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":18521,"samples":[[1000,1115263,2269],[2000,99390,476],[3000,1631954,879],[4000,980981,1528]],"last":null,"option_bytes":[63054,204900,241266,293744,294103,367915],"option_joules_micro":[null,3000,4574,2603,null,1817],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_bola_like_18","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":37732,"samples":[[1000,43283,2145]],"last":4,"option_bytes":[19044,89091,89950,155865,158194,354222],"option_joules_micro":[1653,null,926,null,1413,4822],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_bola_like_19","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":7048,"samples":[[1000,395218,2621],[2000,420999,1489],[3000,875357,2469],[4000,1590390,33]],"last":null,"option_bytes":[76058,110659],"option_joules_micro":[null,null],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_bola_like_20","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":6286,"samples":[[1000,1697883,490],[2000,653689,2821]],"last":2,"option_bytes":[93697,103897,166943,395655],"option_joules_micro":[808,2175,3720,1722],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_bola_like_21","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":1637,"samples":[[1000,1326660,453],[2000,623250,595],[3000,190373,496],[4000,545816,2879]],"last":null,"option_bytes":[45513,140712,285270],"option_joules_micro":[null,3444,2169],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_bola_like_22","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":33416,"samples":[[1000,1522501,2293],[2000,1367474,843],[3000,28876,258],[4000,348102,817]],"last":2,"option_bytes":[135561,164095,199476],"option_joules_micro":[null,542,2243],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_bola_like_23","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":25717,"samples":[[1000,1178437,1629],[2000,447799,476],[3000,630878,2096],[4000,1880066,1857]],"last":null,"option_bytes":[36369,174153,224525,369179,377169],"option_joules_micro":[null,1811,647,1106,1034],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_bola_like_24","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":10540,"samples":[],"last":null,"option_bytes":[71451,95462,143083,246735,253167,318686],"option_joules_micro":[4747,4813,4715,null,4515,135],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_bola_like_25","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":20951,"samples":[[1000,130708,887]],"last":null,"option_bytes":[156031],"option_joules_micro":[4035],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_bola_like_26","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":38816,"samples":[[1000,675201,2563],[2000,1747486,468],[3000,804995,2476],[4000,555929,1323]],"last":null,"option_bytes":[97255,144171,144983,212778,337879],"option_joules_micro":[1878,2427,3802,3359,3138],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_bola_like_27","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":29120,"samples":[[1000,205557,75],[2000,1600494,1251],[3000,615667,1422]],"last":1,"option_bytes":[15448,80716,314237],"option_joules_micro":[1783,null,983],"joule_budget_micro":2446,"segment_ms":1000}},"expect":{"choice":2,"budget":"within"}}, +{"name":"abr_bola_like_28","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":3895,"samples":[[1000,1642417,488]],"last":0,"option_bytes":[178847,197381],"option_joules_micro":[805,1330],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_bola_like_29","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":5043,"samples":[[1000,320516,856],[2000,455944,2400],[3000,1090390,307],[4000,1503545,2066],[5000,1571448,2156]],"last":3,"option_bytes":[118399,166307,207779,350673],"option_joules_micro":[3407,null,4989,4316],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_bola_like_30","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":10880,"samples":[[1000,496251,2984],[2000,1410869,227]],"last":5,"option_bytes":[53586,190914,212753,222066,293341,388656],"option_joules_micro":[null,null,2722,1228,2710,null],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_bola_like_31","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":32168,"samples":[[1000,1586751,2771],[2000,550958,2532],[3000,281722,1964],[4000,1084201,924]],"last":0,"option_bytes":[95722,138681,162936,182448,240430,352697],"option_joules_micro":[203,917,3136,4882,3788,null],"joule_budget_micro":2702,"segment_ms":4000}},"expect":{"choice":1,"budget":"lowered"}}, +{"name":"abr_bola_like_32","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":27832,"samples":[],"last":null,"option_bytes":[217137,290770],"option_joules_micro":[null,null],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_bola_like_33","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":1133,"samples":[[1000,1883348,310],[2000,1503581,219],[3000,257624,2456]],"last":null,"option_bytes":[376873],"option_joules_micro":[3600],"joule_budget_micro":2937,"segment_ms":1000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_bola_like_34","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":660,"samples":[],"last":2,"option_bytes":[35333,90913,214404,288921,307074,359893],"option_joules_micro":[null,1183,null,3405,4668,1069],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_bola_like_35","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":33019,"samples":[[1000,1196634,2933],[2000,1237240,1474]],"last":0,"option_bytes":[175597],"option_joules_micro":[1574],"joule_budget_micro":5374,"segment_ms":1000}},"expect":{"choice":0,"budget":"within"}}, +{"name":"abr_bola_like_36","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":9288,"samples":[[1000,834878,1601],[2000,423950,2676],[3000,1358696,1710],[4000,388812,1714]],"last":0,"option_bytes":[312330],"option_joules_micro":[null],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_bola_like_37","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":29838,"samples":[[1000,89024,23],[2000,1472164,2118],[3000,1201310,540]],"last":0,"option_bytes":[116439],"option_joules_micro":[2398],"joule_budget_micro":3382,"segment_ms":4000}},"expect":{"choice":0,"budget":"within"}}, +{"name":"abr_bola_like_38","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":18295,"samples":[[1000,546049,735],[2000,1918070,1160],[3000,347471,22],[4000,1299017,2171]],"last":0,"option_bytes":[34244,227894,279287,285354,307348,350156],"option_joules_micro":[null,null,2081,944,964,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_bola_like_39","op":"abr","input":{"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":15971,"samples":[[1000,102625,1856],[2000,266131,383],[3000,1974116,1755],[4000,1735286,1269]],"last":0,"option_bytes":[135545],"option_joules_micro":[2776],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_00","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":34246,"samples":[[1000,865389,2193],[2000,1385529,1216],[3000,1052936,2170]],"last":null,"option_bytes":[154175,157964],"option_joules_micro":[2967,null],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_mpc_01","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":1554,"samples":[[1000,36156,661],[2000,744758,716],[3000,20268,2358],[4000,989327,2256],[5000,1615968,16]],"last":null,"option_bytes":[155353,199999],"option_joules_micro":[2643,null],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_mpc_02","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":10836,"samples":[[1000,1951248,1523]],"last":null,"option_bytes":[9115,50612,284928,360073],"option_joules_micro":[912,2198,null,3974],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_mpc_03","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":12630,"samples":[],"last":null,"option_bytes":[343298],"option_joules_micro":[null],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_04","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":2863,"samples":[[1000,101317,1440]],"last":null,"option_bytes":[88174,103129,280755,300194,358240],"option_joules_micro":[4214,301,2260,4648,632],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_05","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":4727,"samples":[],"last":2,"option_bytes":[10157,104678,132143,203523,210330,220727],"option_joules_micro":[null,null,null,3069,896,null],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_06","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":32209,"samples":[[1000,1821999,2655],[2000,797338,1714]],"last":null,"option_bytes":[74005,89236,196978,306264,327164,352684],"option_joules_micro":[1377,null,null,3179,null,4221],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_mpc_07","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":30520,"samples":[[1000,517266,1308],[2000,434505,844],[3000,1638568,2657]],"last":null,"option_bytes":[21461,164937,178059,202715,217230],"option_joules_micro":[2138,3387,null,247,1764],"joule_budget_micro":3581,"segment_ms":3840}},"expect":{"choice":4,"budget":"within"}}, +{"name":"abr_mpc_08","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":12319,"samples":[[1000,340263,1392]],"last":null,"option_bytes":[12541,87847,186801,226775,309419,349923],"option_joules_micro":[null,3307,879,547,null,null],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_mpc_09","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":30313,"samples":[[1000,933644,1257],[2000,1814434,884],[3000,229375,266],[4000,446821,776],[5000,1102641,2517]],"last":null,"option_bytes":[291171,371171],"option_joules_micro":[1237,275],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_mpc_10","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":35123,"samples":[[1000,983211,2249]],"last":0,"option_bytes":[49079,310795],"option_joules_micro":[1996,2692],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_mpc_11","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":1570,"samples":[[1000,344542,446],[2000,1693018,1164]],"last":1,"option_bytes":[71246,95686,134267,155112,221156],"option_joules_micro":[null,3757,2827,2809,1525],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_mpc_12","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":35549,"samples":[[1000,1673989,696],[2000,1035206,2893],[3000,1016484,39]],"last":null,"option_bytes":[113914,156377],"option_joules_micro":[3044,1235],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_mpc_13","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":10338,"samples":[[1000,1224110,481],[2000,955881,1007]],"last":null,"option_bytes":[195816],"option_joules_micro":[null],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_14","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":17127,"samples":[[1000,1369275,1143]],"last":null,"option_bytes":[79050,79475,97585,272705,398504],"option_joules_micro":[null,null,2589,2622,4835],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_mpc_15","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":9841,"samples":[[1000,1168458,1567],[2000,1869218,627],[3000,138851,1938],[4000,1432156,427]],"last":null,"option_bytes":[78956,316949],"option_joules_micro":[4624,null],"joule_budget_micro":5428,"segment_ms":200}},"expect":{"choice":0,"budget":"lowered"}}, +{"name":"abr_mpc_16","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":34129,"samples":[[1000,696042,877]],"last":0,"option_bytes":[227388],"option_joules_micro":[2005],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_17","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":34612,"samples":[[1000,1113860,2847],[2000,1744199,781],[3000,1458008,2986],[4000,699431,503]],"last":null,"option_bytes":[87198,305540],"option_joules_micro":[535,2971],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":1,"budget":"not_set"}}, +{"name":"abr_mpc_18","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":11904,"samples":[[1000,1738597,2894],[2000,742606,673],[3000,758290,562],[4000,585883,266]],"last":1,"option_bytes":[175744,300621,311244],"option_joules_micro":[null,null,2800],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_mpc_19","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":19031,"samples":[],"last":0,"option_bytes":[218075],"option_joules_micro":[551],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_20","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":3015,"samples":[[1000,5944,953]],"last":0,"option_bytes":[209315,213815,256352],"option_joules_micro":[1099,2001,null],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_21","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":31340,"samples":[[1000,932838,1851],[2000,1898763,2714]],"last":null,"option_bytes":[31577,118773,121268],"option_joules_micro":[4672,null,2373],"joule_budget_micro":2542,"segment_ms":200}},"expect":{"choice":2,"budget":"within"}}, +{"name":"abr_mpc_22","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":27341,"samples":[[1000,1953438,659],[2000,1690928,406],[3000,119497,1548],[4000,1630137,1917],[5000,1253443,1424]],"last":0,"option_bytes":[9460,97210,282937,288947,344833,365350],"option_joules_micro":[4234,780,653,290,null,2585],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_mpc_23","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":38259,"samples":[[1000,788960,2831],[2000,697389,1164]],"last":4,"option_bytes":[58628,154371,256000,316213,364477],"option_joules_micro":[4711,1019,2212,1394,2744],"joule_budget_micro":3324,"segment_ms":4000}},"expect":{"choice":4,"budget":"within"}}, +{"name":"abr_mpc_24","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":16941,"samples":[[1000,879707,348]],"last":null,"option_bytes":[84996,97628,193104,223533],"option_joules_micro":[2748,870,4434,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_mpc_25","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":32826,"samples":[[1000,1940772,2321]],"last":3,"option_bytes":[111101,141461,173292,287950],"option_joules_micro":[263,null,2349,null],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_mpc_26","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":7985,"samples":[[1000,679797,1014],[2000,1388646,1629],[3000,1287532,995],[4000,673199,2771],[5000,1693506,6]],"last":null,"option_bytes":[148612,182814,263724],"option_joules_micro":[1240,3043,null],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_mpc_27","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":25285,"samples":[],"last":2,"option_bytes":[205132,209606,216912,263414,353879,372690],"option_joules_micro":[3514,1145,1369,4951,null,736],"joule_budget_micro":5625,"segment_ms":1000}},"expect":{"choice":0,"budget":"within"}}, +{"name":"abr_mpc_28","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":19124,"samples":[[1000,331098,1453],[2000,536721,2331]],"last":null,"option_bytes":[80660,128098,193362,237934,287501,324214],"option_joules_micro":[null,null,4493,null,3390,null],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":5,"budget":"not_set"}}, +{"name":"abr_mpc_29","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":13960,"samples":[[1000,1446941,305],[2000,1491488,581],[3000,765645,743],[4000,179479,1628],[5000,1937348,312]],"last":null,"option_bytes":[166082,190860],"option_joules_micro":[null,4831],"joule_budget_micro":457,"segment_ms":1000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_mpc_30","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":6763,"samples":[],"last":3,"option_bytes":[114132,121182,345843,346021,355368],"option_joules_micro":[3690,null,3581,1578,1526],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_31","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":12795,"samples":[[1000,904626,2824],[2000,712775,1599],[3000,884425,2063]],"last":null,"option_bytes":[28835,223843,227852,310268],"option_joules_micro":[null,893,3370,null],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_mpc_32","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":28372,"samples":[],"last":0,"option_bytes":[27801,54184],"option_joules_micro":[1568,null],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_33","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":976,"samples":[[1000,95753,2800],[2000,642094,319],[3000,927665,2262],[4000,179629,847]],"last":null,"option_bytes":[61929,144961,184676,221710,336112,398207],"option_joules_micro":[4302,3376,560,2370,4640,3510],"joule_budget_micro":null,"segment_ms":4000}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_mpc_34","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":23052,"samples":[],"last":null,"option_bytes":[80227,338392],"option_joules_micro":[3874,1539],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_35","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":34234,"samples":[],"last":0,"option_bytes":[14712,153962,374129],"option_joules_micro":[968,3807,1420],"joule_budget_micro":5207,"segment_ms":3840}},"expect":{"choice":0,"budget":"within"}}, +{"name":"abr_mpc_36","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":15197,"samples":[],"last":null,"option_bytes":[8555,15488,45559,129577,145333,393613],"option_joules_micro":[298,771,null,4210,null,null],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_37","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":22058,"samples":[[1000,1492222,2574],[2000,955765,723],[3000,44230,1292],[4000,1643204,1569]],"last":2,"option_bytes":[3419,61005,155199,251344,325134],"option_joules_micro":[2989,994,4448,1986,2340],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":4,"budget":"not_set"}}, +{"name":"abr_mpc_38","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":4614,"samples":[],"last":2,"option_bytes":[124032,147831,179787,264257,317602,373533],"option_joules_micro":[464,3627,null,2541,null,null],"joule_budget_micro":null,"segment_ms":3840}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_mpc_39","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":2169,"samples":[[1000,1664812,2029],[2000,1199226,396],[3000,1066747,1306],[4000,934893,1177],[5000,937707,462]],"last":2,"option_bytes":[69029,158033,286806,379267],"option_joules_micro":[null,null,1240,4653],"joule_budget_micro":null,"segment_ms":200}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_budget_not_set","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500000,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[10,50,500],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_budget_not_applied","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500000,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":100,"segment_ms":1000}},"expect":{"choice":2,"budget":"not_applied"}}, +{"name":"abr_budget_within","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500000,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[10,50,100],"joule_budget_micro":100,"segment_ms":1000}},"expect":{"choice":2,"budget":"within"}}, +{"name":"abr_budget_lowered","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500000,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[10,50,500],"joule_budget_micro":100,"segment_ms":1000}},"expect":{"choice":1,"budget":"lowered"}}, +{"name":"abr_budget_lowered_past_unlabelled","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500000,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[10,null,500],"joule_budget_micro":100,"segment_ms":1000}},"expect":{"choice":0,"budget":"lowered"}}, +{"name":"abr_budget_unlabelled_choice","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500000,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[10,50,null],"joule_budget_micro":100,"segment_ms":1000}},"expect":{"choice":1,"budget":"lowered"}}, +{"name":"abr_budget_unmet","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500000,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[200,null,500],"joule_budget_micro":100,"segment_ms":1000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_budget_unmet_not_the_least_figure","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500000,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,300,200],"joule_budget_micro":100,"segment_ms":1000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_budget_not_raised_to_labelled_options","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,1250,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,50,80],"joule_budget_micro":100,"segment_ms":1000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_budget_not_raised_past_the_budget","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,1250,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,7000],"joule_budget_micro":1000,"segment_ms":1000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_budget_not_raised_past_the_budget_no_budget","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,1250,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,7000],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_budget_not_raised_to_one_within","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,1250,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,900,null],"joule_budget_micro":1000,"segment_ms":1000}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_budget_figures_past_the_options","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500000,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null,5],"joule_budget_micro":100,"segment_ms":1000}},"expect":{"choice":2,"budget":"not_applied"}}, +{"name":"abr_extreme_throughput_0","op":"abr","input":{"policy":{"id":"throughput","last_n":255,"safety_permille":65535},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":0}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_extreme_throughput_1","op":"abr","input":{"policy":{"id":"throughput","last_n":255,"safety_permille":65535},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":1}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_extreme_throughput_4294967295","op":"abr","input":{"policy":{"id":"throughput","last_n":255,"safety_permille":65535},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":4294967295}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_extreme_buffer_based_0","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":0,"cushion_ms":4294967295},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":0}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_extreme_buffer_based_1","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":0,"cushion_ms":4294967295},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":1}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_extreme_buffer_based_4294967295","op":"abr","input":{"policy":{"id":"buffer_based","reservoir_ms":0,"cushion_ms":4294967295},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":4294967295}},"expect":{"choice":3,"budget":"not_set"}}, +{"name":"abr_extreme_bola_like_0","op":"abr","input":{"policy":{"id":"bola_like","v_milli":4294967295,"max_buffer_ms":4294967295},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":0}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_extreme_bola_like_1","op":"abr","input":{"policy":{"id":"bola_like","v_milli":4294967295,"max_buffer_ms":4294967295},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":1}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_extreme_bola_like_4294967295","op":"abr","input":{"policy":{"id":"bola_like","v_milli":4294967295,"max_buffer_ms":4294967295},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":4294967295}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_extreme_mpc_0","op":"abr","input":{"policy":{"id":"mpc","horizon":255,"safety_permille":65535},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":0}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_extreme_mpc_1","op":"abr","input":{"policy":{"id":"mpc","horizon":255,"safety_permille":65535},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":1}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_extreme_mpc_4294967295","op":"abr","input":{"policy":{"id":"mpc","horizon":255,"safety_permille":65535},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":4294967295}},"expect":{"choice":2,"budget":"not_set"}}, +{"name":"abr_extreme_bola_like_past_128_bits","op":"abr","input":{"policy":{"id":"bola_like","v_milli":4294967295,"max_buffer_ms":596001515},"state":{"buffer_ms":129,"samples":[],"last":null,"option_bytes":[3997960217448864544,16001797029153315873],"option_joules_micro":[null,null],"joule_budget_micro":null,"segment_ms":1}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_extreme_budget","op":"abr","input":{"policy":{"id":"bola_like","v_milli":4294967295,"max_buffer_ms":4294967295},"state":{"buffer_ms":4294967295,"samples":[[0,18446744073709551615,0],[1,18446744073709551614,2],[2,18446744073709551613,0],[3,18446744073709551612,2],[4,18446744073709551611,0],[5,18446744073709551610,2],[6,18446744073709551609,0],[7,18446744073709551608,2],[8,18446744073709551607,0],[9,18446744073709551606,2],[10,18446744073709551605,0],[11,18446744073709551604,2],[12,18446744073709551603,0],[13,18446744073709551602,2],[14,18446744073709551601,0],[15,18446744073709551600,2],[16,18446744073709551599,0],[17,18446744073709551598,2],[18,18446744073709551597,0],[19,18446744073709551596,2],[20,18446744073709551595,0],[21,18446744073709551594,2],[22,18446744073709551593,0],[23,18446744073709551592,2],[24,18446744073709551591,0],[25,18446744073709551590,2],[26,18446744073709551589,0],[27,18446744073709551588,2],[28,18446744073709551587,0],[29,18446744073709551586,2],[30,18446744073709551585,0],[31,18446744073709551584,2],[32,18446744073709551583,0],[33,18446744073709551582,2],[34,18446744073709551581,0],[35,18446744073709551580,2],[36,18446744073709551579,0],[37,18446744073709551578,2],[38,18446744073709551577,0],[39,18446744073709551576,2],[40,18446744073709551575,0],[41,18446744073709551574,2],[42,18446744073709551573,0],[43,18446744073709551572,2],[44,18446744073709551571,0],[45,18446744073709551570,2],[46,18446744073709551569,0],[47,18446744073709551568,2],[48,18446744073709551567,0],[49,18446744073709551566,2],[50,18446744073709551565,0],[51,18446744073709551564,2],[52,18446744073709551563,0],[53,18446744073709551562,2],[54,18446744073709551561,0],[55,18446744073709551560,2],[56,18446744073709551559,0],[57,18446744073709551558,2],[58,18446744073709551557,0],[59,18446744073709551556,2],[60,18446744073709551555,0],[61,18446744073709551554,2],[62,18446744073709551553,0],[63,18446744073709551552,2],[64,18446744073709551551,0],[65,18446744073709551550,2],[66,18446744073709551549,0],[67,18446744073709551548,2],[68,18446744073709551547,0],[69,18446744073709551546,2],[70,18446744073709551545,0],[71,18446744073709551544,2],[72,18446744073709551543,0],[73,18446744073709551542,2],[74,18446744073709551541,0],[75,18446744073709551540,2],[76,18446744073709551539,0],[77,18446744073709551538,2],[78,18446744073709551537,0],[79,18446744073709551536,2],[80,18446744073709551535,0],[81,18446744073709551534,2],[82,18446744073709551533,0],[83,18446744073709551532,2],[84,18446744073709551531,0],[85,18446744073709551530,2],[86,18446744073709551529,0],[87,18446744073709551528,2],[88,18446744073709551527,0],[89,18446744073709551526,2],[90,18446744073709551525,0],[91,18446744073709551524,2],[92,18446744073709551523,0],[93,18446744073709551522,2],[94,18446744073709551521,0],[95,18446744073709551520,2],[96,18446744073709551519,0],[97,18446744073709551518,2],[98,18446744073709551517,0],[99,18446744073709551516,2],[100,18446744073709551515,0],[101,18446744073709551514,2],[102,18446744073709551513,0],[103,18446744073709551512,2],[104,18446744073709551511,0],[105,18446744073709551510,2],[106,18446744073709551509,0],[107,18446744073709551508,2],[108,18446744073709551507,0],[109,18446744073709551506,2],[110,18446744073709551505,0],[111,18446744073709551504,2],[112,18446744073709551503,0],[113,18446744073709551502,2],[114,18446744073709551501,0],[115,18446744073709551500,2],[116,18446744073709551499,0],[117,18446744073709551498,2],[118,18446744073709551497,0],[119,18446744073709551496,2],[120,18446744073709551495,0],[121,18446744073709551494,2],[122,18446744073709551493,0],[123,18446744073709551492,2],[124,18446744073709551491,0],[125,18446744073709551490,2],[126,18446744073709551489,0],[127,18446744073709551488,2],[128,18446744073709551487,0],[129,18446744073709551486,2],[130,18446744073709551485,0],[131,18446744073709551484,2],[132,18446744073709551483,0],[133,18446744073709551482,2],[134,18446744073709551481,0],[135,18446744073709551480,2],[136,18446744073709551479,0],[137,18446744073709551478,2],[138,18446744073709551477,0],[139,18446744073709551476,2],[140,18446744073709551475,0],[141,18446744073709551474,2],[142,18446744073709551473,0],[143,18446744073709551472,2],[144,18446744073709551471,0],[145,18446744073709551470,2],[146,18446744073709551469,0],[147,18446744073709551468,2],[148,18446744073709551467,0],[149,18446744073709551466,2],[150,18446744073709551465,0],[151,18446744073709551464,2],[152,18446744073709551463,0],[153,18446744073709551462,2],[154,18446744073709551461,0],[155,18446744073709551460,2],[156,18446744073709551459,0],[157,18446744073709551458,2],[158,18446744073709551457,0],[159,18446744073709551456,2],[160,18446744073709551455,0],[161,18446744073709551454,2],[162,18446744073709551453,0],[163,18446744073709551452,2],[164,18446744073709551451,0],[165,18446744073709551450,2],[166,18446744073709551449,0],[167,18446744073709551448,2],[168,18446744073709551447,0],[169,18446744073709551446,2],[170,18446744073709551445,0],[171,18446744073709551444,2],[172,18446744073709551443,0],[173,18446744073709551442,2],[174,18446744073709551441,0],[175,18446744073709551440,2],[176,18446744073709551439,0],[177,18446744073709551438,2],[178,18446744073709551437,0],[179,18446744073709551436,2],[180,18446744073709551435,0],[181,18446744073709551434,2],[182,18446744073709551433,0],[183,18446744073709551432,2],[184,18446744073709551431,0],[185,18446744073709551430,2],[186,18446744073709551429,0],[187,18446744073709551428,2],[188,18446744073709551427,0],[189,18446744073709551426,2],[190,18446744073709551425,0],[191,18446744073709551424,2],[192,18446744073709551423,0],[193,18446744073709551422,2],[194,18446744073709551421,0],[195,18446744073709551420,2],[196,18446744073709551419,0],[197,18446744073709551418,2],[198,18446744073709551417,0],[199,18446744073709551416,2],[200,18446744073709551415,0],[201,18446744073709551414,2],[202,18446744073709551413,0],[203,18446744073709551412,2],[204,18446744073709551411,0],[205,18446744073709551410,2],[206,18446744073709551409,0],[207,18446744073709551408,2],[208,18446744073709551407,0],[209,18446744073709551406,2],[210,18446744073709551405,0],[211,18446744073709551404,2],[212,18446744073709551403,0],[213,18446744073709551402,2],[214,18446744073709551401,0],[215,18446744073709551400,2],[216,18446744073709551399,0],[217,18446744073709551398,2],[218,18446744073709551397,0],[219,18446744073709551396,2],[220,18446744073709551395,0],[221,18446744073709551394,2],[222,18446744073709551393,0],[223,18446744073709551392,2],[224,18446744073709551391,0],[225,18446744073709551390,2],[226,18446744073709551389,0],[227,18446744073709551388,2],[228,18446744073709551387,0],[229,18446744073709551386,2],[230,18446744073709551385,0],[231,18446744073709551384,2],[232,18446744073709551383,0],[233,18446744073709551382,2],[234,18446744073709551381,0],[235,18446744073709551380,2],[236,18446744073709551379,0],[237,18446744073709551378,2],[238,18446744073709551377,0],[239,18446744073709551376,2],[240,18446744073709551375,0],[241,18446744073709551374,2],[242,18446744073709551373,0],[243,18446744073709551372,2],[244,18446744073709551371,0],[245,18446744073709551370,2],[246,18446744073709551369,0],[247,18446744073709551368,2],[248,18446744073709551367,0],[249,18446744073709551366,2],[250,18446744073709551365,0],[251,18446744073709551364,2],[252,18446744073709551363,0],[253,18446744073709551362,2],[254,18446744073709551361,0],[255,18446744073709551360,2],[256,18446744073709551359,0],[257,18446744073709551358,2],[258,18446744073709551357,0],[259,18446744073709551356,2],[260,18446744073709551355,0],[261,18446744073709551354,2],[262,18446744073709551353,0],[263,18446744073709551352,2],[264,18446744073709551351,0],[265,18446744073709551350,2],[266,18446744073709551349,0],[267,18446744073709551348,2],[268,18446744073709551347,0],[269,18446744073709551346,2],[270,18446744073709551345,0],[271,18446744073709551344,2],[272,18446744073709551343,0],[273,18446744073709551342,2],[274,18446744073709551341,0],[275,18446744073709551340,2],[276,18446744073709551339,0],[277,18446744073709551338,2],[278,18446744073709551337,0],[279,18446744073709551336,2],[280,18446744073709551335,0],[281,18446744073709551334,2],[282,18446744073709551333,0],[283,18446744073709551332,2],[284,18446744073709551331,0],[285,18446744073709551330,2],[286,18446744073709551329,0],[287,18446744073709551328,2],[288,18446744073709551327,0],[289,18446744073709551326,2],[290,18446744073709551325,0],[291,18446744073709551324,2],[292,18446744073709551323,0],[293,18446744073709551322,2],[294,18446744073709551321,0],[295,18446744073709551320,2],[296,18446744073709551319,0],[297,18446744073709551318,2],[298,18446744073709551317,0],[299,18446744073709551316,2]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":18446744073709551614,"segment_ms":1}},"expect":{"choice":0,"budget":"unmet"}}, +{"name":"abr_extreme_mpc_stalls","op":"abr","input":{"policy":{"id":"mpc","horizon":255,"safety_permille":65535},"state":{"buffer_ms":0,"samples":[[1,1,1]],"last":1,"option_bytes":[1,6148914691236517205,18446744073709551614,18446744073709551615],"option_joules_micro":[18446744073709551615,null,1,18446744073709551614],"joule_budget_micro":null,"segment_ms":1}},"expect":{"choice":0,"budget":"not_set"}}, +{"name":"abr_out_of_bounds_buffer","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":4294967296,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"abr_out_of_bounds_segment","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":4294967296}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"abr_out_of_bounds_segment_fraction","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":1000.5}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"abr_out_of_bounds_budget","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":1.8446744073709552e+19,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"abr_out_of_bounds_last_negative","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":-1,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"abr_out_of_bounds_options","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"abr_out_of_bounds_figures","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"abr_out_of_bounds_sample","op":"abr","input":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1,1,1.8446744073709552e+19]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"abr_out_of_bounds_last_n","op":"abr","input":{"policy":{"id":"throughput","last_n":256,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"abr_out_of_bounds_safety","op":"abr","input":{"policy":{"id":"mpc","horizon":3,"safety_permille":65536},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"abr_out_of_bounds_v","op":"abr","input":{"policy":{"id":"bola_like","v_milli":4294967296,"max_buffer_ms":20000},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"abr_out_of_bounds_horizon","op":"abr","input":{"policy":{"id":"mpc","horizon":256,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"depth_out_of_bounds_bytes","op":"depth","input":{"layers_n":[18446744073709551615,1],"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"depth_out_of_bounds_figure","op":"depth","input":{"layers_n":[1,1],"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,1000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[18446744073709551615,1],"joule_budget_micro":10,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"depth_out_of_bounds_layers","op":"depth","input":{"layers_n":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"error":"abr_state_out_of_bounds"}}, +{"name":"depth_at_the_bound","op":"depth","input":{"layers_n":[18446744073709551614,1],"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[10000,100000,1000000],"option_joules_micro":[null,null,null],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":0,"budget":"not_set"}}, +{"name":"ilog2_1_1","op":"ilog2","input":{"a":1,"b":1},"expect":{"log2_q16":0}}, +{"name":"ilog2_2_1","op":"ilog2","input":{"a":2,"b":1},"expect":{"log2_q16":65536}}, +{"name":"ilog2_3_2","op":"ilog2","input":{"a":3,"b":2},"expect":{"log2_q16":38336}}, +{"name":"ilog2_17_16","op":"ilog2","input":{"a":17,"b":16},"expect":{"log2_q16":5732}}, +{"name":"ilog2_31_16","op":"ilog2","input":{"a":31,"b":16},"expect":{"log2_q16":62534}}, +{"name":"ilog2_33_32","op":"ilog2","input":{"a":33,"b":32},"expect":{"log2_q16":2866}}, +{"name":"ilog2_1000_1","op":"ilog2","input":{"a":1000,"b":1},"expect":{"log2_q16":653108}}, +{"name":"ilog2_1001_1000","op":"ilog2","input":{"a":1001,"b":1000},"expect":{"log2_q16":90}}, +{"name":"ilog2_65535_65536","op":"ilog2","input":{"a":65535,"b":65536},"expect":{"log2_q16":0}}, +{"name":"ilog2_65537_65536","op":"ilog2","input":{"a":65537,"b":65536},"expect":{"log2_q16":1}}, +{"name":"ilog2_18446744073709551615_1","op":"ilog2","input":{"a":18446744073709551615,"b":1},"expect":{"log2_q16":4194303}}, +{"name":"ilog2_18446744073709551615_18446744073709551614","op":"ilog2","input":{"a":18446744073709551615,"b":18446744073709551614},"expect":{"log2_q16":0}}, +{"name":"ilog2_5_0","op":"ilog2","input":{"a":5,"b":0},"expect":{"log2_q16":152170}}, +{"name":"ilog2_0_0","op":"ilog2","input":{"a":0,"b":0},"expect":{"log2_q16":0}}, +{"name":"ilog2_1099511627776_3","op":"ilog2","input":{"a":1099511627776,"b":3},"expect":{"log2_q16":2517544}}, +{"name":"ilog2_123456789_1000","op":"ilog2","input":{"a":123456789,"b":1000},"expect":{"log2_q16":1108445}}, +{"name":"ilog2_7_3","op":"ilog2","input":{"a":7,"b":3},"expect":{"log2_q16":80079}}, +{"name":"ilog2_400000_2000","op":"ilog2","input":{"a":400000,"b":2000},"expect":{"log2_q16":500948}}, +{"name":"ilog2_999999_2","op":"ilog2","input":{"a":999999,"b":2},"expect":{"log2_q16":1240685}}, +{"name":"ilog2_4097_4096","op":"ilog2","input":{"a":4097,"b":4096},"expect":{"log2_q16":22}}, +{"name":"ilog2_6143_4096","op":"ilog2","input":{"a":6143,"b":4096},"expect":{"log2_q16":38320}}, +{"name":"ilog2_12000_7000","op":"ilog2","input":{"a":12000,"b":7000},"expect":{"log2_q16":50945}}, +{"name":"ilog2_9223372036854775808_1","op":"ilog2","input":{"a":9223372036854775808,"b":1},"expect":{"log2_q16":4128768}}, +{"name":"ilog2_9223372036854775809_4611686018427387904","op":"ilog2","input":{"a":9223372036854775809,"b":4611686018427387904},"expect":{"log2_q16":65536}}, +{"name":"abr_order_O3_throughput_0","op":"abr_order","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2]}}, +{"name":"abr_order_O3_throughput_1","op":"abr_order","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_O3_throughput_2","op":"abr_order","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_O3_buffer_based_0","op":"abr_order","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2]}}, +{"name":"abr_order_O3_buffer_based_1","op":"abr_order","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2]}}, +{"name":"abr_order_O3_buffer_based_2","op":"abr_order","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_O3_bola_like_0","op":"abr_order","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2]}}, +{"name":"abr_order_O3_bola_like_1","op":"abr_order","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_O3_bola_like_2","op":"abr_order","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_O3_mpc_0","op":"abr_order","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2]}}, +{"name":"abr_order_O3_mpc_1","op":"abr_order","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_O3_mpc_2","op":"abr_order","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_OD_throughput_0","op":"abr_order","input":{"object":"OD","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2]}}, +{"name":"abr_order_OD_throughput_1","op":"abr_order","input":{"object":"OD","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_OD_throughput_2","op":"abr_order","input":{"object":"OD","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_OD_buffer_based_0","op":"abr_order","input":{"object":"OD","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2]}}, +{"name":"abr_order_OD_buffer_based_1","op":"abr_order","input":{"object":"OD","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2]}}, +{"name":"abr_order_OD_buffer_based_2","op":"abr_order","input":{"object":"OD","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_OD_bola_like_0","op":"abr_order","input":{"object":"OD","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2]}}, +{"name":"abr_order_OD_bola_like_1","op":"abr_order","input":{"object":"OD","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_OD_bola_like_2","op":"abr_order","input":{"object":"OD","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_OD_mpc_0","op":"abr_order","input":{"object":"OD","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2]}}, +{"name":"abr_order_OD_mpc_1","op":"abr_order","input":{"object":"OD","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_OD_mpc_2","op":"abr_order","input":{"object":"OD","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0,2]}}, +{"name":"abr_order_O7_throughput_0","op":"abr_order","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0]}}, +{"name":"abr_order_O7_throughput_1","op":"abr_order","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O7_throughput_2","op":"abr_order","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O7_buffer_based_0","op":"abr_order","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0]}}, +{"name":"abr_order_O7_buffer_based_1","op":"abr_order","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0]}}, +{"name":"abr_order_O7_buffer_based_2","op":"abr_order","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O7_bola_like_0","op":"abr_order","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O7_bola_like_1","op":"abr_order","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O7_bola_like_2","op":"abr_order","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O7_mpc_0","op":"abr_order","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[1,0]}}, +{"name":"abr_order_O7_mpc_1","op":"abr_order","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O7_mpc_2","op":"abr_order","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O9a_throughput_0","op":"abr_order","input":{"object":"O9a","advertises":["wai.video.av1","wai.video.int_hyper"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O9a_throughput_1","op":"abr_order","input":{"object":"O9a","advertises":["wai.video.av1","wai.video.int_hyper"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O9a_throughput_2","op":"abr_order","input":{"object":"O9a","advertises":["wai.video.av1","wai.video.int_hyper"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O9a_buffer_based_0","op":"abr_order","input":{"object":"O9a","advertises":["wai.video.av1","wai.video.int_hyper"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O9a_buffer_based_1","op":"abr_order","input":{"object":"O9a","advertises":["wai.video.av1","wai.video.int_hyper"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O9a_buffer_based_2","op":"abr_order","input":{"object":"O9a","advertises":["wai.video.av1","wai.video.int_hyper"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O9a_bola_like_0","op":"abr_order","input":{"object":"O9a","advertises":["wai.video.av1","wai.video.int_hyper"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O9a_bola_like_1","op":"abr_order","input":{"object":"O9a","advertises":["wai.video.av1","wai.video.int_hyper"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O9a_bola_like_2","op":"abr_order","input":{"object":"O9a","advertises":["wai.video.av1","wai.video.int_hyper"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O9a_mpc_0","op":"abr_order","input":{"object":"O9a","advertises":["wai.video.av1","wai.video.int_hyper"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O9a_mpc_1","op":"abr_order","input":{"object":"O9a","advertises":["wai.video.av1","wai.video.int_hyper"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O9a_mpc_2","op":"abr_order","input":{"object":"O9a","advertises":["wai.video.av1","wai.video.int_hyper"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1]}}, +{"name":"abr_order_O2_throughput_0","op":"abr_order","input":{"object":"O2","advertises":["wai.neural.int_mlicpp","wai.image.avif","wai.image.jxl","wai.image.png"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2,3]}}, +{"name":"abr_order_O2_throughput_1","op":"abr_order","input":{"object":"O2","advertises":["wai.neural.int_mlicpp","wai.image.avif","wai.image.jxl","wai.image.png"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2,3]}}, +{"name":"abr_order_O2_throughput_2","op":"abr_order","input":{"object":"O2","advertises":["wai.neural.int_mlicpp","wai.image.avif","wai.image.jxl","wai.image.png"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2,3]}}, +{"name":"abr_order_O2_buffer_based_0","op":"abr_order","input":{"object":"O2","advertises":["wai.neural.int_mlicpp","wai.image.avif","wai.image.jxl","wai.image.png"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2,3]}}, +{"name":"abr_order_O2_buffer_based_1","op":"abr_order","input":{"object":"O2","advertises":["wai.neural.int_mlicpp","wai.image.avif","wai.image.jxl","wai.image.png"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2,3]}}, +{"name":"abr_order_O2_buffer_based_2","op":"abr_order","input":{"object":"O2","advertises":["wai.neural.int_mlicpp","wai.image.avif","wai.image.jxl","wai.image.png"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2,3]}}, +{"name":"abr_order_O2_bola_like_0","op":"abr_order","input":{"object":"O2","advertises":["wai.neural.int_mlicpp","wai.image.avif","wai.image.jxl","wai.image.png"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2,3]}}, +{"name":"abr_order_O2_bola_like_1","op":"abr_order","input":{"object":"O2","advertises":["wai.neural.int_mlicpp","wai.image.avif","wai.image.jxl","wai.image.png"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2,3]}}, +{"name":"abr_order_O2_bola_like_2","op":"abr_order","input":{"object":"O2","advertises":["wai.neural.int_mlicpp","wai.image.avif","wai.image.jxl","wai.image.png"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2,3]}}, +{"name":"abr_order_O2_mpc_0","op":"abr_order","input":{"object":"O2","advertises":["wai.neural.int_mlicpp","wai.image.avif","wai.image.jxl","wai.image.png"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2,3]}}, +{"name":"abr_order_O2_mpc_1","op":"abr_order","input":{"object":"O2","advertises":["wai.neural.int_mlicpp","wai.image.avif","wai.image.jxl","wai.image.png"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2,3]}}, +{"name":"abr_order_O2_mpc_2","op":"abr_order","input":{"object":"O2","advertises":["wai.neural.int_mlicpp","wai.image.avif","wai.image.jxl","wai.image.png"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,1,2,3]}}, +{"name":"abr_order_O8_throughput_0","op":"abr_order","input":{"object":"O8","advertises":["wai.video.avc","wai.video.av1"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,2,1]}}, +{"name":"abr_order_O8_throughput_1","op":"abr_order","input":{"object":"O8","advertises":["wai.video.avc","wai.video.av1"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,2,1]}}, +{"name":"abr_order_O8_throughput_2","op":"abr_order","input":{"object":"O8","advertises":["wai.video.avc","wai.video.av1"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,2,1]}}, +{"name":"abr_order_O8_buffer_based_0","op":"abr_order","input":{"object":"O8","advertises":["wai.video.avc","wai.video.av1"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,2,1]}}, +{"name":"abr_order_O8_buffer_based_1","op":"abr_order","input":{"object":"O8","advertises":["wai.video.avc","wai.video.av1"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,2,1]}}, +{"name":"abr_order_O8_buffer_based_2","op":"abr_order","input":{"object":"O8","advertises":["wai.video.avc","wai.video.av1"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,2,1]}}, +{"name":"abr_order_O8_bola_like_0","op":"abr_order","input":{"object":"O8","advertises":["wai.video.avc","wai.video.av1"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,2,1]}}, +{"name":"abr_order_O8_bola_like_1","op":"abr_order","input":{"object":"O8","advertises":["wai.video.avc","wai.video.av1"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,2,1]}}, +{"name":"abr_order_O8_bola_like_2","op":"abr_order","input":{"object":"O8","advertises":["wai.video.avc","wai.video.av1"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,2,1]}}, +{"name":"abr_order_O8_mpc_0","op":"abr_order","input":{"object":"O8","advertises":["wai.video.avc","wai.video.av1"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":0,"samples":[[1000,7500,1000],[2000,7500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,2,1]}}, +{"name":"abr_order_O8_mpc_1","op":"abr_order","input":{"object":"O8","advertises":["wai.video.avc","wai.video.av1"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":8000,"samples":[[1000,187500,1000],[2000,187500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,2,1]}}, +{"name":"abr_order_O8_mpc_2","op":"abr_order","input":{"object":"O8","advertises":["wai.video.avc","wai.video.av1"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":30000,"samples":[[1000,5000000,1000],[2000,5000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"expect":{"order":[0,2,1]}}, +{"name":"depth_O10_throughput_0","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,2500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":0,"budget":"not_set"}}, +{"name":"depth_O10_throughput_1","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":6000,"samples":[[1000,37500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":2,"budget":"not_set"}}, +{"name":"depth_O10_throughput_2","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":30000,"samples":[[1000,625000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":2,"budget":"not_set"}}, +{"name":"depth_O10_buffer_based_0","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":0,"samples":[[1000,2500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":0,"budget":"not_set"}}, +{"name":"depth_O10_buffer_based_1","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":6000,"samples":[[1000,37500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":0,"budget":"not_set"}}, +{"name":"depth_O10_buffer_based_2","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":30000,"samples":[[1000,625000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":2,"budget":"not_set"}}, +{"name":"depth_O10_bola_like_0","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":0,"samples":[[1000,2500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":0,"budget":"not_set"}}, +{"name":"depth_O10_bola_like_1","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":6000,"samples":[[1000,37500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":1,"budget":"not_set"}}, +{"name":"depth_O10_bola_like_2","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":30000,"samples":[[1000,625000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":2,"budget":"not_set"}}, +{"name":"depth_O10_mpc_0","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":0,"samples":[[1000,2500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":0,"budget":"not_set"}}, +{"name":"depth_O10_mpc_1","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":6000,"samples":[[1000,37500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":2,"budget":"not_set"}}, +{"name":"depth_O10_mpc_2","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":30000,"samples":[[1000,625000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":2,"budget":"not_set"}}, +{"name":"depth_O11b_throughput_0","op":"depth","input":{"objects":["O11b","O11bL1"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,2500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":1,"budget":"not_set"}}, +{"name":"depth_O11b_throughput_1","op":"depth","input":{"objects":["O11b","O11bL1"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":6000,"samples":[[1000,37500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":1,"budget":"not_set"}}, +{"name":"depth_O11b_throughput_2","op":"depth","input":{"objects":["O11b","O11bL1"],"policy":{"id":"throughput","last_n":3,"safety_permille":900},"state":{"buffer_ms":30000,"samples":[[1000,625000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":1,"budget":"not_set"}}, +{"name":"depth_O11b_buffer_based_0","op":"depth","input":{"objects":["O11b","O11bL1"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":0,"samples":[[1000,2500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":0,"budget":"not_set"}}, +{"name":"depth_O11b_buffer_based_1","op":"depth","input":{"objects":["O11b","O11bL1"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":6000,"samples":[[1000,37500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":0,"budget":"not_set"}}, +{"name":"depth_O11b_buffer_based_2","op":"depth","input":{"objects":["O11b","O11bL1"],"policy":{"id":"buffer_based","reservoir_ms":5000,"cushion_ms":10000},"state":{"buffer_ms":30000,"samples":[[1000,625000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":1,"budget":"not_set"}}, +{"name":"depth_O11b_bola_like_0","op":"depth","input":{"objects":["O11b","O11bL1"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":0,"samples":[[1000,2500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":0,"budget":"not_set"}}, +{"name":"depth_O11b_bola_like_1","op":"depth","input":{"objects":["O11b","O11bL1"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":6000,"samples":[[1000,37500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":1,"budget":"not_set"}}, +{"name":"depth_O11b_bola_like_2","op":"depth","input":{"objects":["O11b","O11bL1"],"policy":{"id":"bola_like","v_milli":930,"max_buffer_ms":20000},"state":{"buffer_ms":30000,"samples":[[1000,625000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":1,"budget":"not_set"}}, +{"name":"depth_O11b_mpc_0","op":"depth","input":{"objects":["O11b","O11bL1"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":0,"samples":[[1000,2500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":1,"budget":"not_set"}}, +{"name":"depth_O11b_mpc_1","op":"depth","input":{"objects":["O11b","O11bL1"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":6000,"samples":[[1000,37500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":1,"budget":"not_set"}}, +{"name":"depth_O11b_mpc_2","op":"depth","input":{"objects":["O11b","O11bL1"],"policy":{"id":"mpc","horizon":3,"safety_permille":850},"state":{"buffer_ms":30000,"samples":[[1000,625000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":1,"budget":"not_set"}}, +{"name":"depth_O10_throughput_between","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,25000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":1000}},"expect":{"depth":1,"budget":"not_set"}}, +{"name":"depth_O10_budget_unlabelled_layer","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,10000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[5,null,20],"joule_budget_micro":30,"segment_ms":1000}},"expect":{"depth":0,"budget":"lowered"}}, +{"name":"depth_O10_budget_lowered","op":"depth","input":{"objects":["O10","O10L1","O10L2"],"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,10000000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[5,10,20],"joule_budget_micro":30,"segment_ms":1000}},"expect":{"depth":1,"budget":"lowered"}}, +{"name":"fetch_abr_O3_low","op":"fetch","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"applies":["wai.meta.hdr10plus"],"abr":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"fragment":"#h=1009"},"expect":{"role":"base","pick":0,"verified_by":"head-proof","why_whole":null,"body_bytes":21650,"requests":3,"companions":[],"refinements":[],"abandoned":[],"dropped":[],"skipped":[],"component_sha256":"24003cee2a7bf7fa45caa412ab4ea9f5f08b6bf44842a73d5a11c2bcf36df464"}}, +{"name":"same_pick_abr_O3_low","op":"same_pick","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"applies":["wai.meta.hdr10plus"],"abr":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"fragment":"#h=1009"},"expect":{"pick":0}}, +{"name":"same_pick_abr_O7_low","op":"same_pick","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"abr":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,12500,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}}},"expect":{"pick":1}}, +{"name":"fetch_abr_O3_high","op":"fetch","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"applies":["wai.meta.hdr10plus"],"abr":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,1250000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"fragment":"#h=1009"},"expect":{"role":"base","pick":1,"verified_by":"head-proof","why_whole":null,"body_bytes":78526,"requests":3,"companions":[2],"refinements":[],"abandoned":[],"dropped":[],"skipped":[],"component_sha256":"68a51be787c439226eb7bb1cf434e913fc1f0e221870da87f70615d667a7ab12"}}, +{"name":"same_pick_abr_O3_high","op":"same_pick","input":{"object":"O3","advertises":["wai.video.av1","wai.meta.hdr10plus"],"applies":["wai.meta.hdr10plus"],"abr":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,1250000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}},"fragment":"#h=1009"},"expect":{"pick":1}}, +{"name":"same_pick_abr_O7_high","op":"same_pick","input":{"object":"O7","advertises":["wai.video.av1","wai.meta.hdr10plus"],"abr":{"policy":{"id":"throughput","last_n":1,"safety_permille":900},"state":{"buffer_ms":0,"samples":[[1000,1250000,1000]],"last":null,"option_bytes":[],"option_joules_micro":[],"joule_budget_micro":null,"segment_ms":2000}}},"expect":{"pick":1}} ] diff --git a/wai/http-conformance/cases/live-r2-orphan.json b/wai/http-conformance/cases/live-r2-orphan.json new file mode 100644 index 000000000..1470a295c --- /dev/null +++ b/wai/http-conformance/cases/live-r2-orphan.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"40add72be1d163ade96031789b4d36ca59dfa6100305a6ad2769fd17b096d07d","work_compose_ops":2,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":null,"signer_pubkey":"c84069fa2216c6edc1d52ca83aa11c267dd5440afa526090ca43ebd0ca86a9cf","signer_id":"did:example:http-vectors","sig":"a6886d729353f79ca98a5520f217544c7f3d5f332b64031a1e2e02945cd5986f48d33efc6912394c3a62f9f7d4b848d63fd3687306ab8b421cf07adf5375ba01","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[{"duration_ms":320,"independent":true,"uri":"720/1200.0.wai#cid=b8637cf58c3a5a0c567115d9065e5ec6d228663a65e50cb0b95a8b0dfb4cc385&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.1.wai#cid=c4336fb0c48737e9e68b8fe5f4bc60aa8314878a602de85cfc9c3c0d33c0e03f&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.2.wai#cid=12f271588cd4463ab7a8c89a25902fac152de57ec5420330b9ba5afd1fe0873a&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.3.wai#cid=00af350c635dd361002588143eaac2171e7b3ba39e8f8354eb3ac3fabd3392d4&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.4.wai#cid=27432e8c8ded7a74a057d0ffa83768d249982ed842d3e8659d0e39f69e92c815&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.5.wai#cid=2f38e98c125601d888c9a719cf0ac74e0846fe09c2d9fb09ed88b54cabc3a778&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.6.wai#cid=0a749256ffcfd6cfb3b396f04cea3d73622b5c230325096eed3585aa5f813008&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.7.wai#cid=dd01430fa65add27f88d5de32bb2100a3491b6202e935a1d4101aae8062d9d6f&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.8.wai#cid=7095c2d1f03b11435df79189640f73303b9c1492fa302d1dcbdb26f1527d9ce6&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.9.wai#cid=55f3946ced44b2e8384e772b9ffb71f07acbb176d171508cf269d06f0cf69676&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.10.wai#cid=f04294c0f3f12e5e6309b39f1c336ef3257c8c755dbeb69314bce44fcc0947ac&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.11.wai#cid=62853bb69f462471f250a4140c254b7bb0af2cf0691e4e759b89cc320f5f8659&n=3014","layers":[]}],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[{"duration_ms":320,"independent":true,"uri":"720/1201.0.wai#cid=7ae66a04b663eaf965ee71c445af732893632ab0713cca8139969d3529e8ec8e&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.1.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.2.wai#cid=a002266b23a292284444fb753d53a3def273636f4bd7ca182b635326d9f74c7b&n=3122","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.3.wai#cid=a8ae3c937be0969d0e9a355e3afedd51eddb754dc3a95ed6fcd9386b2a6372cf&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.4.wai#cid=6f5e3d4204b88e143da63ef4dc22840e65839fdbf47d2a0c03d68695860c0c70&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.5.wai#cid=35f910e1a0a4acfbf8ed201e8ccd430302f395280aa79f9657b3cb16f23ba5b0&n=3146","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.6.wai#cid=cd095db1f4b9560782af8ba700fe5154e7ef541554a0550ea8edb796bde725ce&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.7.wai#cid=173c227ba468e8809e9b7b016264bd86dc1310c7681a732e9d0886373411915f&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.8.wai#cid=db17bd07b023ddcc31b057df9d0da1354b148c3920489e345201a8d3c2c72bed&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.9.wai#cid=48c691c6021b6b07ae5a7046dd04eaee0b8ebb7ce3a5b9aca275ec5015d54312&n=3170","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.10.wai#cid=fa7e77738aaf35251d277030fb58d1cd9a3f262ce9b44bbb16248142037bf32d&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.11.wai#cid=5f341dcfe19ad23b52d1c879fa2d6f52c60829e89d47cc544dfb0891cac41304&n=3222","layers":[]}],"layers":[],"preload_hint":"720/1202.0.wai"}]} diff --git a/wai/http-conformance/cases/other-key/mr/90cd319c8e785ef9377d032e4330aa6b1b4dbb84ce5ca2d7fcd9a1b866615cb2.json b/wai/http-conformance/cases/other-key/mr/90cd319c8e785ef9377d032e4330aa6b1b4dbb84ce5ca2d7fcd9a1b866615cb2.json new file mode 100644 index 000000000..301a52720 --- /dev/null +++ b/wai/http-conformance/cases/other-key/mr/90cd319c8e785ef9377d032e4330aa6b1b4dbb84ce5ca2d7fcd9a1b866615cb2.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"40add72be1d163ade96031789b4d36ca59dfa6100305a6ad2769fd17b096d07d","work_compose_ops":2,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"eab14aa7350c082b1cc52b78c74eb5f2660f80ca6267a73a2ae9cd186df824ed","signer_pubkey":"dac31ebca159d283f8341fd90d7fa1cd95bf5eccab657f7952662e67e56afbe3","signer_id":"did:example:http-vectors","sig":"f60bd29bb57eea95c41ee5b5219f2ee034076f500a8c23fef53c5e8de36315bd33867a5d19697a241b07defbe8f88947d20f5eeb119720af8a0416c3cafa2400","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[{"duration_ms":320,"independent":true,"uri":"720/1200.0.wai#cid=b8637cf58c3a5a0c567115d9065e5ec6d228663a65e50cb0b95a8b0dfb4cc385&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.1.wai#cid=c4336fb0c48737e9e68b8fe5f4bc60aa8314878a602de85cfc9c3c0d33c0e03f&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.2.wai#cid=12f271588cd4463ab7a8c89a25902fac152de57ec5420330b9ba5afd1fe0873a&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.3.wai#cid=00af350c635dd361002588143eaac2171e7b3ba39e8f8354eb3ac3fabd3392d4&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.4.wai#cid=27432e8c8ded7a74a057d0ffa83768d249982ed842d3e8659d0e39f69e92c815&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.5.wai#cid=2f38e98c125601d888c9a719cf0ac74e0846fe09c2d9fb09ed88b54cabc3a778&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.6.wai#cid=0a749256ffcfd6cfb3b396f04cea3d73622b5c230325096eed3585aa5f813008&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.7.wai#cid=dd01430fa65add27f88d5de32bb2100a3491b6202e935a1d4101aae8062d9d6f&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.8.wai#cid=7095c2d1f03b11435df79189640f73303b9c1492fa302d1dcbdb26f1527d9ce6&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.9.wai#cid=55f3946ced44b2e8384e772b9ffb71f07acbb176d171508cf269d06f0cf69676&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.10.wai#cid=f04294c0f3f12e5e6309b39f1c336ef3257c8c755dbeb69314bce44fcc0947ac&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.11.wai#cid=62853bb69f462471f250a4140c254b7bb0af2cf0691e4e759b89cc320f5f8659&n=3014","layers":[]}],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[{"duration_ms":320,"independent":true,"uri":"720/1201.0.wai#cid=7ae66a04b663eaf965ee71c445af732893632ab0713cca8139969d3529e8ec8e&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.1.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.2.wai#cid=a002266b23a292284444fb753d53a3def273636f4bd7ca182b635326d9f74c7b&n=3122","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.3.wai#cid=a8ae3c937be0969d0e9a355e3afedd51eddb754dc3a95ed6fcd9386b2a6372cf&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.4.wai#cid=6f5e3d4204b88e143da63ef4dc22840e65839fdbf47d2a0c03d68695860c0c70&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.5.wai#cid=35f910e1a0a4acfbf8ed201e8ccd430302f395280aa79f9657b3cb16f23ba5b0&n=3146","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.6.wai#cid=cd095db1f4b9560782af8ba700fe5154e7ef541554a0550ea8edb796bde725ce&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.7.wai#cid=173c227ba468e8809e9b7b016264bd86dc1310c7681a732e9d0886373411915f&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.8.wai#cid=db17bd07b023ddcc31b057df9d0da1354b148c3920489e345201a8d3c2c72bed&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.9.wai#cid=48c691c6021b6b07ae5a7046dd04eaee0b8ebb7ce3a5b9aca275ec5015d54312&n=3170","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.10.wai#cid=fa7e77738aaf35251d277030fb58d1cd9a3f262ce9b44bbb16248142037bf32d&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.11.wai#cid=5f341dcfe19ad23b52d1c879fa2d6f52c60829e89d47cc544dfb0891cac41304&n=3222","layers":[]}],"layers":[],"preload_hint":"720/1202.0.wai"}]} diff --git a/wai/http-conformance/cases/other-key/r2.json b/wai/http-conformance/cases/other-key/r2.json new file mode 100644 index 000000000..301a52720 --- /dev/null +++ b/wai/http-conformance/cases/other-key/r2.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"40add72be1d163ade96031789b4d36ca59dfa6100305a6ad2769fd17b096d07d","work_compose_ops":2,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"eab14aa7350c082b1cc52b78c74eb5f2660f80ca6267a73a2ae9cd186df824ed","signer_pubkey":"dac31ebca159d283f8341fd90d7fa1cd95bf5eccab657f7952662e67e56afbe3","signer_id":"did:example:http-vectors","sig":"f60bd29bb57eea95c41ee5b5219f2ee034076f500a8c23fef53c5e8de36315bd33867a5d19697a241b07defbe8f88947d20f5eeb119720af8a0416c3cafa2400","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[{"duration_ms":320,"independent":true,"uri":"720/1200.0.wai#cid=b8637cf58c3a5a0c567115d9065e5ec6d228663a65e50cb0b95a8b0dfb4cc385&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.1.wai#cid=c4336fb0c48737e9e68b8fe5f4bc60aa8314878a602de85cfc9c3c0d33c0e03f&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.2.wai#cid=12f271588cd4463ab7a8c89a25902fac152de57ec5420330b9ba5afd1fe0873a&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.3.wai#cid=00af350c635dd361002588143eaac2171e7b3ba39e8f8354eb3ac3fabd3392d4&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.4.wai#cid=27432e8c8ded7a74a057d0ffa83768d249982ed842d3e8659d0e39f69e92c815&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.5.wai#cid=2f38e98c125601d888c9a719cf0ac74e0846fe09c2d9fb09ed88b54cabc3a778&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.6.wai#cid=0a749256ffcfd6cfb3b396f04cea3d73622b5c230325096eed3585aa5f813008&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.7.wai#cid=dd01430fa65add27f88d5de32bb2100a3491b6202e935a1d4101aae8062d9d6f&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.8.wai#cid=7095c2d1f03b11435df79189640f73303b9c1492fa302d1dcbdb26f1527d9ce6&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.9.wai#cid=55f3946ced44b2e8384e772b9ffb71f07acbb176d171508cf269d06f0cf69676&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.10.wai#cid=f04294c0f3f12e5e6309b39f1c336ef3257c8c755dbeb69314bce44fcc0947ac&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.11.wai#cid=62853bb69f462471f250a4140c254b7bb0af2cf0691e4e759b89cc320f5f8659&n=3014","layers":[]}],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[{"duration_ms":320,"independent":true,"uri":"720/1201.0.wai#cid=7ae66a04b663eaf965ee71c445af732893632ab0713cca8139969d3529e8ec8e&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.1.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.2.wai#cid=a002266b23a292284444fb753d53a3def273636f4bd7ca182b635326d9f74c7b&n=3122","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.3.wai#cid=a8ae3c937be0969d0e9a355e3afedd51eddb754dc3a95ed6fcd9386b2a6372cf&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.4.wai#cid=6f5e3d4204b88e143da63ef4dc22840e65839fdbf47d2a0c03d68695860c0c70&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.5.wai#cid=35f910e1a0a4acfbf8ed201e8ccd430302f395280aa79f9657b3cb16f23ba5b0&n=3146","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.6.wai#cid=cd095db1f4b9560782af8ba700fe5154e7ef541554a0550ea8edb796bde725ce&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.7.wai#cid=173c227ba468e8809e9b7b016264bd86dc1310c7681a732e9d0886373411915f&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.8.wai#cid=db17bd07b023ddcc31b057df9d0da1354b148c3920489e345201a8d3c2c72bed&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.9.wai#cid=48c691c6021b6b07ae5a7046dd04eaee0b8ebb7ce3a5b9aca275ec5015d54312&n=3170","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.10.wai#cid=fa7e77738aaf35251d277030fb58d1cd9a3f262ce9b44bbb16248142037bf32d&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.11.wai#cid=5f341dcfe19ad23b52d1c879fa2d6f52c60829e89d47cc544dfb0891cac41304&n=3222","layers":[]}],"layers":[],"preload_hint":"720/1202.0.wai"}]} diff --git a/wai/http-conformance/cases/other-key/r3.json b/wai/http-conformance/cases/other-key/r3.json new file mode 100644 index 000000000..be750a341 --- /dev/null +++ b/wai/http-conformance/cases/other-key/r3.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"40add72be1d163ade96031789b4d36ca59dfa6100305a6ad2769fd17b096d07d","work_compose_ops":2,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"90cd319c8e785ef9377d032e4330aa6b1b4dbb84ce5ca2d7fcd9a1b866615cb2","signer_pubkey":"c84069fa2216c6edc1d52ca83aa11c267dd5440afa526090ca43ebd0ca86a9cf","signer_id":"did:example:http-vectors","sig":"eba7d6f7efd7d3b43d1b44eb82645a2736ad931fe99cc27af3de657bed2d6ee13f079ca53fb5ff8c08404ced7ced0769cbaf43c051ca9b3d217d26dbad3a7707","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":true,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[],"layers":[],"preload_hint":null}]} diff --git a/wai/http-conformance/cases/relist/mr/4fdc8b5cc9867641c9cf38517c942529a2bf185da6fd9797c3bab7abcc3f4fd7.json b/wai/http-conformance/cases/relist/mr/4fdc8b5cc9867641c9cf38517c942529a2bf185da6fd9797c3bab7abcc3f4fd7.json new file mode 100644 index 000000000..dec78301d --- /dev/null +++ b/wai/http-conformance/cases/relist/mr/4fdc8b5cc9867641c9cf38517c942529a2bf185da6fd9797c3bab7abcc3f4fd7.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"3d2caa315b4db835b377e56ccb5fd3f78bc039f618bfe766db613481a547a9d7","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"40add72be1d163ade96031789b4d36ca59dfa6100305a6ad2769fd17b096d07d","work_compose_ops":2,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"eab14aa7350c082b1cc52b78c74eb5f2660f80ca6267a73a2ae9cd186df824ed","signer_pubkey":"c84069fa2216c6edc1d52ca83aa11c267dd5440afa526090ca43ebd0ca86a9cf","signer_id":"did:example:http-vectors","sig":"ff79ca7c3a2525d4ba7cb27bfbe5fc353baecd5c322e53a6afd5c9dd68d8d1a9eb9b189d5115e8d07eca7d3e9d0c750811e347f15c24899086e1d6ae4ef12e03","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[{"duration_ms":320,"independent":true,"uri":"720/1200.0.wai#cid=b8637cf58c3a5a0c567115d9065e5ec6d228663a65e50cb0b95a8b0dfb4cc385&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.1.wai#cid=c4336fb0c48737e9e68b8fe5f4bc60aa8314878a602de85cfc9c3c0d33c0e03f&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.2.wai#cid=12f271588cd4463ab7a8c89a25902fac152de57ec5420330b9ba5afd1fe0873a&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.3.wai#cid=00af350c635dd361002588143eaac2171e7b3ba39e8f8354eb3ac3fabd3392d4&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.4.wai#cid=27432e8c8ded7a74a057d0ffa83768d249982ed842d3e8659d0e39f69e92c815&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.5.wai#cid=2f38e98c125601d888c9a719cf0ac74e0846fe09c2d9fb09ed88b54cabc3a778&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.6.wai#cid=0a749256ffcfd6cfb3b396f04cea3d73622b5c230325096eed3585aa5f813008&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.7.wai#cid=dd01430fa65add27f88d5de32bb2100a3491b6202e935a1d4101aae8062d9d6f&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.8.wai#cid=7095c2d1f03b11435df79189640f73303b9c1492fa302d1dcbdb26f1527d9ce6&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.9.wai#cid=55f3946ced44b2e8384e772b9ffb71f07acbb176d171508cf269d06f0cf69676&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.10.wai#cid=f04294c0f3f12e5e6309b39f1c336ef3257c8c755dbeb69314bce44fcc0947ac&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.11.wai#cid=62853bb69f462471f250a4140c254b7bb0af2cf0691e4e759b89cc320f5f8659&n=3014","layers":[]}],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[{"duration_ms":320,"independent":true,"uri":"720/1201.0.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.1.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.2.wai#cid=a002266b23a292284444fb753d53a3def273636f4bd7ca182b635326d9f74c7b&n=3122","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.3.wai#cid=a8ae3c937be0969d0e9a355e3afedd51eddb754dc3a95ed6fcd9386b2a6372cf&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.4.wai#cid=6f5e3d4204b88e143da63ef4dc22840e65839fdbf47d2a0c03d68695860c0c70&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.5.wai#cid=35f910e1a0a4acfbf8ed201e8ccd430302f395280aa79f9657b3cb16f23ba5b0&n=3146","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.6.wai#cid=cd095db1f4b9560782af8ba700fe5154e7ef541554a0550ea8edb796bde725ce&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.7.wai#cid=173c227ba468e8809e9b7b016264bd86dc1310c7681a732e9d0886373411915f&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.8.wai#cid=db17bd07b023ddcc31b057df9d0da1354b148c3920489e345201a8d3c2c72bed&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.9.wai#cid=48c691c6021b6b07ae5a7046dd04eaee0b8ebb7ce3a5b9aca275ec5015d54312&n=3170","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.10.wai#cid=fa7e77738aaf35251d277030fb58d1cd9a3f262ce9b44bbb16248142037bf32d&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.11.wai#cid=5f341dcfe19ad23b52d1c879fa2d6f52c60829e89d47cc544dfb0891cac41304&n=3222","layers":[]}],"layers":[],"preload_hint":"720/1202.0.wai"}]} diff --git a/wai/http-conformance/cases/relist/r2-dot-same.json b/wai/http-conformance/cases/relist/r2-dot-same.json new file mode 100644 index 000000000..32bf40b59 --- /dev/null +++ b/wai/http-conformance/cases/relist/r2-dot-same.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"ba060cc75a825f51a94e6e61fba988af8486254134d9452689a9d8340524a085","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"40add72be1d163ade96031789b4d36ca59dfa6100305a6ad2769fd17b096d07d","work_compose_ops":2,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"eab14aa7350c082b1cc52b78c74eb5f2660f80ca6267a73a2ae9cd186df824ed","signer_pubkey":"c84069fa2216c6edc1d52ca83aa11c267dd5440afa526090ca43ebd0ca86a9cf","signer_id":"did:example:http-vectors","sig":"b7bdf3a4ce93f17fcd7e3dd6e10ee519fcee492013c6fd289d6962460c8c28ccc60fc1f561275d1fd4eb5fc93032efbc5fa9ce607a554d48ca64be15fe42f50d","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[{"duration_ms":320,"independent":true,"uri":"720/1200.0.wai#cid=b8637cf58c3a5a0c567115d9065e5ec6d228663a65e50cb0b95a8b0dfb4cc385&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.1.wai#cid=c4336fb0c48737e9e68b8fe5f4bc60aa8314878a602de85cfc9c3c0d33c0e03f&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.2.wai#cid=12f271588cd4463ab7a8c89a25902fac152de57ec5420330b9ba5afd1fe0873a&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.3.wai#cid=00af350c635dd361002588143eaac2171e7b3ba39e8f8354eb3ac3fabd3392d4&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.4.wai#cid=27432e8c8ded7a74a057d0ffa83768d249982ed842d3e8659d0e39f69e92c815&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.5.wai#cid=2f38e98c125601d888c9a719cf0ac74e0846fe09c2d9fb09ed88b54cabc3a778&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.6.wai#cid=0a749256ffcfd6cfb3b396f04cea3d73622b5c230325096eed3585aa5f813008&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.7.wai#cid=dd01430fa65add27f88d5de32bb2100a3491b6202e935a1d4101aae8062d9d6f&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.8.wai#cid=7095c2d1f03b11435df79189640f73303b9c1492fa302d1dcbdb26f1527d9ce6&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.9.wai#cid=55f3946ced44b2e8384e772b9ffb71f07acbb176d171508cf269d06f0cf69676&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.10.wai#cid=f04294c0f3f12e5e6309b39f1c336ef3257c8c755dbeb69314bce44fcc0947ac&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.11.wai#cid=62853bb69f462471f250a4140c254b7bb0af2cf0691e4e759b89cc320f5f8659&n=3014","layers":[]}],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[{"duration_ms":320,"independent":true,"uri":"./720/1201.0.wai#cid=7ae66a04b663eaf965ee71c445af732893632ab0713cca8139969d3529e8ec8e&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.1.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.2.wai#cid=a002266b23a292284444fb753d53a3def273636f4bd7ca182b635326d9f74c7b&n=3122","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.3.wai#cid=a8ae3c937be0969d0e9a355e3afedd51eddb754dc3a95ed6fcd9386b2a6372cf&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.4.wai#cid=6f5e3d4204b88e143da63ef4dc22840e65839fdbf47d2a0c03d68695860c0c70&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.5.wai#cid=35f910e1a0a4acfbf8ed201e8ccd430302f395280aa79f9657b3cb16f23ba5b0&n=3146","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.6.wai#cid=cd095db1f4b9560782af8ba700fe5154e7ef541554a0550ea8edb796bde725ce&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.7.wai#cid=173c227ba468e8809e9b7b016264bd86dc1310c7681a732e9d0886373411915f&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.8.wai#cid=db17bd07b023ddcc31b057df9d0da1354b148c3920489e345201a8d3c2c72bed&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.9.wai#cid=48c691c6021b6b07ae5a7046dd04eaee0b8ebb7ce3a5b9aca275ec5015d54312&n=3170","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.10.wai#cid=fa7e77738aaf35251d277030fb58d1cd9a3f262ce9b44bbb16248142037bf32d&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.11.wai#cid=5f341dcfe19ad23b52d1c879fa2d6f52c60829e89d47cc544dfb0891cac41304&n=3222","layers":[]}],"layers":[],"preload_hint":"720/1202.0.wai"}]} diff --git a/wai/http-conformance/cases/relist/r2-dot.json b/wai/http-conformance/cases/relist/r2-dot.json new file mode 100644 index 000000000..0ba83f269 --- /dev/null +++ b/wai/http-conformance/cases/relist/r2-dot.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"2fb1c914fa5b8fd12fc710a8410fe6d5de290a84eb400aa91bd831fb65db659e","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"40add72be1d163ade96031789b4d36ca59dfa6100305a6ad2769fd17b096d07d","work_compose_ops":2,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"eab14aa7350c082b1cc52b78c74eb5f2660f80ca6267a73a2ae9cd186df824ed","signer_pubkey":"c84069fa2216c6edc1d52ca83aa11c267dd5440afa526090ca43ebd0ca86a9cf","signer_id":"did:example:http-vectors","sig":"9248ef288daff80485b0c057b82e90759c2f40f080a375eec628f817aa110a3a83e47b8956c02100c4aeac349894022a1b1104f89b5d94e66aa9fd394cfd3207","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[{"duration_ms":320,"independent":true,"uri":"720/1200.0.wai#cid=b8637cf58c3a5a0c567115d9065e5ec6d228663a65e50cb0b95a8b0dfb4cc385&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.1.wai#cid=c4336fb0c48737e9e68b8fe5f4bc60aa8314878a602de85cfc9c3c0d33c0e03f&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.2.wai#cid=12f271588cd4463ab7a8c89a25902fac152de57ec5420330b9ba5afd1fe0873a&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.3.wai#cid=00af350c635dd361002588143eaac2171e7b3ba39e8f8354eb3ac3fabd3392d4&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.4.wai#cid=27432e8c8ded7a74a057d0ffa83768d249982ed842d3e8659d0e39f69e92c815&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.5.wai#cid=2f38e98c125601d888c9a719cf0ac74e0846fe09c2d9fb09ed88b54cabc3a778&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.6.wai#cid=0a749256ffcfd6cfb3b396f04cea3d73622b5c230325096eed3585aa5f813008&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.7.wai#cid=dd01430fa65add27f88d5de32bb2100a3491b6202e935a1d4101aae8062d9d6f&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.8.wai#cid=7095c2d1f03b11435df79189640f73303b9c1492fa302d1dcbdb26f1527d9ce6&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.9.wai#cid=55f3946ced44b2e8384e772b9ffb71f07acbb176d171508cf269d06f0cf69676&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.10.wai#cid=f04294c0f3f12e5e6309b39f1c336ef3257c8c755dbeb69314bce44fcc0947ac&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.11.wai#cid=62853bb69f462471f250a4140c254b7bb0af2cf0691e4e759b89cc320f5f8659&n=3014","layers":[]}],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[{"duration_ms":320,"independent":true,"uri":"./720/1201.0.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.1.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.2.wai#cid=a002266b23a292284444fb753d53a3def273636f4bd7ca182b635326d9f74c7b&n=3122","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.3.wai#cid=a8ae3c937be0969d0e9a355e3afedd51eddb754dc3a95ed6fcd9386b2a6372cf&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.4.wai#cid=6f5e3d4204b88e143da63ef4dc22840e65839fdbf47d2a0c03d68695860c0c70&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.5.wai#cid=35f910e1a0a4acfbf8ed201e8ccd430302f395280aa79f9657b3cb16f23ba5b0&n=3146","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.6.wai#cid=cd095db1f4b9560782af8ba700fe5154e7ef541554a0550ea8edb796bde725ce&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.7.wai#cid=173c227ba468e8809e9b7b016264bd86dc1310c7681a732e9d0886373411915f&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.8.wai#cid=db17bd07b023ddcc31b057df9d0da1354b148c3920489e345201a8d3c2c72bed&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.9.wai#cid=48c691c6021b6b07ae5a7046dd04eaee0b8ebb7ce3a5b9aca275ec5015d54312&n=3170","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.10.wai#cid=fa7e77738aaf35251d277030fb58d1cd9a3f262ce9b44bbb16248142037bf32d&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.11.wai#cid=5f341dcfe19ad23b52d1c879fa2d6f52c60829e89d47cc544dfb0891cac41304&n=3222","layers":[]}],"layers":[],"preload_hint":"720/1202.0.wai"}]} diff --git a/wai/http-conformance/cases/relist/r2.json b/wai/http-conformance/cases/relist/r2.json new file mode 100644 index 000000000..dec78301d --- /dev/null +++ b/wai/http-conformance/cases/relist/r2.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"3d2caa315b4db835b377e56ccb5fd3f78bc039f618bfe766db613481a547a9d7","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"40add72be1d163ade96031789b4d36ca59dfa6100305a6ad2769fd17b096d07d","work_compose_ops":2,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"eab14aa7350c082b1cc52b78c74eb5f2660f80ca6267a73a2ae9cd186df824ed","signer_pubkey":"c84069fa2216c6edc1d52ca83aa11c267dd5440afa526090ca43ebd0ca86a9cf","signer_id":"did:example:http-vectors","sig":"ff79ca7c3a2525d4ba7cb27bfbe5fc353baecd5c322e53a6afd5c9dd68d8d1a9eb9b189d5115e8d07eca7d3e9d0c750811e347f15c24899086e1d6ae4ef12e03","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[{"duration_ms":320,"independent":true,"uri":"720/1200.0.wai#cid=b8637cf58c3a5a0c567115d9065e5ec6d228663a65e50cb0b95a8b0dfb4cc385&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.1.wai#cid=c4336fb0c48737e9e68b8fe5f4bc60aa8314878a602de85cfc9c3c0d33c0e03f&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.2.wai#cid=12f271588cd4463ab7a8c89a25902fac152de57ec5420330b9ba5afd1fe0873a&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.3.wai#cid=00af350c635dd361002588143eaac2171e7b3ba39e8f8354eb3ac3fabd3392d4&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.4.wai#cid=27432e8c8ded7a74a057d0ffa83768d249982ed842d3e8659d0e39f69e92c815&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.5.wai#cid=2f38e98c125601d888c9a719cf0ac74e0846fe09c2d9fb09ed88b54cabc3a778&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.6.wai#cid=0a749256ffcfd6cfb3b396f04cea3d73622b5c230325096eed3585aa5f813008&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.7.wai#cid=dd01430fa65add27f88d5de32bb2100a3491b6202e935a1d4101aae8062d9d6f&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.8.wai#cid=7095c2d1f03b11435df79189640f73303b9c1492fa302d1dcbdb26f1527d9ce6&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.9.wai#cid=55f3946ced44b2e8384e772b9ffb71f07acbb176d171508cf269d06f0cf69676&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.10.wai#cid=f04294c0f3f12e5e6309b39f1c336ef3257c8c755dbeb69314bce44fcc0947ac&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.11.wai#cid=62853bb69f462471f250a4140c254b7bb0af2cf0691e4e759b89cc320f5f8659&n=3014","layers":[]}],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[{"duration_ms":320,"independent":true,"uri":"720/1201.0.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.1.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.2.wai#cid=a002266b23a292284444fb753d53a3def273636f4bd7ca182b635326d9f74c7b&n=3122","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.3.wai#cid=a8ae3c937be0969d0e9a355e3afedd51eddb754dc3a95ed6fcd9386b2a6372cf&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.4.wai#cid=6f5e3d4204b88e143da63ef4dc22840e65839fdbf47d2a0c03d68695860c0c70&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.5.wai#cid=35f910e1a0a4acfbf8ed201e8ccd430302f395280aa79f9657b3cb16f23ba5b0&n=3146","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.6.wai#cid=cd095db1f4b9560782af8ba700fe5154e7ef541554a0550ea8edb796bde725ce&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.7.wai#cid=173c227ba468e8809e9b7b016264bd86dc1310c7681a732e9d0886373411915f&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.8.wai#cid=db17bd07b023ddcc31b057df9d0da1354b148c3920489e345201a8d3c2c72bed&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.9.wai#cid=48c691c6021b6b07ae5a7046dd04eaee0b8ebb7ce3a5b9aca275ec5015d54312&n=3170","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.10.wai#cid=fa7e77738aaf35251d277030fb58d1cd9a3f262ce9b44bbb16248142037bf32d&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.11.wai#cid=5f341dcfe19ad23b52d1c879fa2d6f52c60829e89d47cc544dfb0891cac41304&n=3222","layers":[]}],"layers":[],"preload_hint":"720/1202.0.wai"}]} diff --git a/wai/http-conformance/cases/relist/r3-after.json b/wai/http-conformance/cases/relist/r3-after.json new file mode 100644 index 000000000..2ec1d022d --- /dev/null +++ b/wai/http-conformance/cases/relist/r3-after.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"40add72be1d163ade96031789b4d36ca59dfa6100305a6ad2769fd17b096d07d","work_compose_ops":2,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"4fdc8b5cc9867641c9cf38517c942529a2bf185da6fd9797c3bab7abcc3f4fd7","signer_pubkey":"c84069fa2216c6edc1d52ca83aa11c267dd5440afa526090ca43ebd0ca86a9cf","signer_id":"did:example:http-vectors","sig":"0f6f640981fe8a69c0631835056547d825d21155ac95abc410baf8a990cea12a70d1c3db6835fec26b8e63ba26f78c2febba35605333c5f915fceb4f0f070b03","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":true,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[],"layers":[],"preload_hint":null}]} diff --git a/wai/http-conformance/cases/relist/r3-gap.json b/wai/http-conformance/cases/relist/r3-gap.json new file mode 100644 index 000000000..024f61bbc --- /dev/null +++ b/wai/http-conformance/cases/relist/r3-gap.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"9738e08a98c5867a031789c26269d72cbfabd153483128811d4feb0e33e4e136","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"40add72be1d163ade96031789b4d36ca59dfa6100305a6ad2769fd17b096d07d","work_compose_ops":2,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"4ed29942f6a294bf03d422a1b76b93b7c0b84ff123b99ee2c26505649c68bdf6","signer_pubkey":"c84069fa2216c6edc1d52ca83aa11c267dd5440afa526090ca43ebd0ca86a9cf","signer_id":"did:example:http-vectors","sig":"3bf9989633f662d7084a06d5698c98bf112c81041b2060627a10d2da4a141e20e58cda96925718090d6bb4800b52dab8745f8ce2a368ce279bb9b88578ff7a0e","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":true,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":true,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[],"layers":[],"preload_hint":null}]} diff --git a/wai/http-conformance/cases/relist/r3.json b/wai/http-conformance/cases/relist/r3.json new file mode 100644 index 000000000..84d66790f --- /dev/null +++ b/wai/http-conformance/cases/relist/r3.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"198e04e4e3aa8c149d7adbcc6c1b5ed0a56956ceb3c8d1f34e42104f318dcbdb","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"c5e68c2958aa68df360e32a66700bdff0c36c01e63d0d6bd80b9f03b2bd66a87","work_compose_ops":2,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"4ed29942f6a294bf03d422a1b76b93b7c0b84ff123b99ee2c26505649c68bdf6","signer_pubkey":"c84069fa2216c6edc1d52ca83aa11c267dd5440afa526090ca43ebd0ca86a9cf","signer_id":"did:example:http-vectors","sig":"b08962976e408f754a89d2cdc1a5ca5f1cae76a186e8a155ff97cbe3f327239113e9beec406d25a73013ec79f87b2ac5d53171b16e0203a4d5c06d29132a2e0c","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":true,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[],"layers":[],"preload_hint":null}]} diff --git a/wai/http-conformance/cases/relist/wai-r2-dot-same.m3u8 b/wai/http-conformance/cases/relist/wai-r2-dot-same.m3u8 new file mode 100644 index 000000000..2c840b6d0 --- /dev/null +++ b/wai/http-conformance/cases/relist/wai-r2-dot-same.m3u8 @@ -0,0 +1,36 @@ +#EXTM3U +#EXT-X-VERSION:9 +#EXT-X-TARGETDURATION:4 +#EXT-X-PART-INF:PART-TARGET=0.320 +#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.960 +#EXT-X-MEDIA-SEQUENCE:1200 +#EXT-X-PROGRAM-DATE-TIME:2026-10-01T12:00:00.000Z +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.0.wai#cid=b8637cf58c3a5a0c567115d9065e5ec6d228663a65e50cb0b95a8b0dfb4cc385&n=3026" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.1.wai#cid=c4336fb0c48737e9e68b8fe5f4bc60aa8314878a602de85cfc9c3c0d33c0e03f&n=2962" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.2.wai#cid=12f271588cd4463ab7a8c89a25902fac152de57ec5420330b9ba5afd1fe0873a&n=2994" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.3.wai#cid=00af350c635dd361002588143eaac2171e7b3ba39e8f8354eb3ac3fabd3392d4&n=3018" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.4.wai#cid=27432e8c8ded7a74a057d0ffa83768d249982ed842d3e8659d0e39f69e92c815&n=3026" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.5.wai#cid=2f38e98c125601d888c9a719cf0ac74e0846fe09c2d9fb09ed88b54cabc3a778&n=2962" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.6.wai#cid=0a749256ffcfd6cfb3b396f04cea3d73622b5c230325096eed3585aa5f813008&n=2994" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.7.wai#cid=dd01430fa65add27f88d5de32bb2100a3491b6202e935a1d4101aae8062d9d6f&n=3018" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.8.wai#cid=7095c2d1f03b11435df79189640f73303b9c1492fa302d1dcbdb26f1527d9ce6&n=3026" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.9.wai#cid=55f3946ced44b2e8384e772b9ffb71f07acbb176d171508cf269d06f0cf69676&n=2962" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.10.wai#cid=f04294c0f3f12e5e6309b39f1c336ef3257c8c755dbeb69314bce44fcc0947ac&n=2994" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.11.wai#cid=62853bb69f462471f250a4140c254b7bb0af2cf0691e4e759b89cc320f5f8659&n=3014" +#EXTINF:3.840, +../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606 +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="./720/1201.0.wai#cid=7ae66a04b663eaf965ee71c445af732893632ab0713cca8139969d3529e8ec8e&n=3190" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.1.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.2.wai#cid=a002266b23a292284444fb753d53a3def273636f4bd7ca182b635326d9f74c7b&n=3122" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.3.wai#cid=a8ae3c937be0969d0e9a355e3afedd51eddb754dc3a95ed6fcd9386b2a6372cf&n=3190" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.4.wai#cid=6f5e3d4204b88e143da63ef4dc22840e65839fdbf47d2a0c03d68695860c0c70&n=3238" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.5.wai#cid=35f910e1a0a4acfbf8ed201e8ccd430302f395280aa79f9657b3cb16f23ba5b0&n=3146" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.6.wai#cid=cd095db1f4b9560782af8ba700fe5154e7ef541554a0550ea8edb796bde725ce&n=3222" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.7.wai#cid=173c227ba468e8809e9b7b016264bd86dc1310c7681a732e9d0886373411915f&n=3222" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.8.wai#cid=db17bd07b023ddcc31b057df9d0da1354b148c3920489e345201a8d3c2c72bed&n=3238" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.9.wai#cid=48c691c6021b6b07ae5a7046dd04eaee0b8ebb7ce3a5b9aca275ec5015d54312&n=3170" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.10.wai#cid=fa7e77738aaf35251d277030fb58d1cd9a3f262ce9b44bbb16248142037bf32d&n=3222" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.11.wai#cid=5f341dcfe19ad23b52d1c879fa2d6f52c60829e89d47cc544dfb0891cac41304&n=3222" +#EXTINF:3.840, +../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118 +#EXT-X-PRELOAD-HINT:TYPE=PART,URI="720/1202.0.wai" diff --git a/wai/http-conformance/cases/relist/wai-r2-dot.m3u8 b/wai/http-conformance/cases/relist/wai-r2-dot.m3u8 new file mode 100644 index 000000000..12a3e67c0 --- /dev/null +++ b/wai/http-conformance/cases/relist/wai-r2-dot.m3u8 @@ -0,0 +1,36 @@ +#EXTM3U +#EXT-X-VERSION:9 +#EXT-X-TARGETDURATION:4 +#EXT-X-PART-INF:PART-TARGET=0.320 +#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.960 +#EXT-X-MEDIA-SEQUENCE:1200 +#EXT-X-PROGRAM-DATE-TIME:2026-10-01T12:00:00.000Z +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.0.wai#cid=b8637cf58c3a5a0c567115d9065e5ec6d228663a65e50cb0b95a8b0dfb4cc385&n=3026" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.1.wai#cid=c4336fb0c48737e9e68b8fe5f4bc60aa8314878a602de85cfc9c3c0d33c0e03f&n=2962" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.2.wai#cid=12f271588cd4463ab7a8c89a25902fac152de57ec5420330b9ba5afd1fe0873a&n=2994" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.3.wai#cid=00af350c635dd361002588143eaac2171e7b3ba39e8f8354eb3ac3fabd3392d4&n=3018" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.4.wai#cid=27432e8c8ded7a74a057d0ffa83768d249982ed842d3e8659d0e39f69e92c815&n=3026" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.5.wai#cid=2f38e98c125601d888c9a719cf0ac74e0846fe09c2d9fb09ed88b54cabc3a778&n=2962" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.6.wai#cid=0a749256ffcfd6cfb3b396f04cea3d73622b5c230325096eed3585aa5f813008&n=2994" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.7.wai#cid=dd01430fa65add27f88d5de32bb2100a3491b6202e935a1d4101aae8062d9d6f&n=3018" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.8.wai#cid=7095c2d1f03b11435df79189640f73303b9c1492fa302d1dcbdb26f1527d9ce6&n=3026" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.9.wai#cid=55f3946ced44b2e8384e772b9ffb71f07acbb176d171508cf269d06f0cf69676&n=2962" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.10.wai#cid=f04294c0f3f12e5e6309b39f1c336ef3257c8c755dbeb69314bce44fcc0947ac&n=2994" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.11.wai#cid=62853bb69f462471f250a4140c254b7bb0af2cf0691e4e759b89cc320f5f8659&n=3014" +#EXTINF:3.840, +../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606 +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="./720/1201.0.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.1.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.2.wai#cid=a002266b23a292284444fb753d53a3def273636f4bd7ca182b635326d9f74c7b&n=3122" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.3.wai#cid=a8ae3c937be0969d0e9a355e3afedd51eddb754dc3a95ed6fcd9386b2a6372cf&n=3190" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.4.wai#cid=6f5e3d4204b88e143da63ef4dc22840e65839fdbf47d2a0c03d68695860c0c70&n=3238" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.5.wai#cid=35f910e1a0a4acfbf8ed201e8ccd430302f395280aa79f9657b3cb16f23ba5b0&n=3146" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.6.wai#cid=cd095db1f4b9560782af8ba700fe5154e7ef541554a0550ea8edb796bde725ce&n=3222" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.7.wai#cid=173c227ba468e8809e9b7b016264bd86dc1310c7681a732e9d0886373411915f&n=3222" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.8.wai#cid=db17bd07b023ddcc31b057df9d0da1354b148c3920489e345201a8d3c2c72bed&n=3238" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.9.wai#cid=48c691c6021b6b07ae5a7046dd04eaee0b8ebb7ce3a5b9aca275ec5015d54312&n=3170" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.10.wai#cid=fa7e77738aaf35251d277030fb58d1cd9a3f262ce9b44bbb16248142037bf32d&n=3222" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.11.wai#cid=5f341dcfe19ad23b52d1c879fa2d6f52c60829e89d47cc544dfb0891cac41304&n=3222" +#EXTINF:3.840, +../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118 +#EXT-X-PRELOAD-HINT:TYPE=PART,URI="720/1202.0.wai" diff --git a/wai/http-conformance/cases/relist/wai-r2.m3u8 b/wai/http-conformance/cases/relist/wai-r2.m3u8 new file mode 100644 index 000000000..c66aa3ebe --- /dev/null +++ b/wai/http-conformance/cases/relist/wai-r2.m3u8 @@ -0,0 +1,36 @@ +#EXTM3U +#EXT-X-VERSION:9 +#EXT-X-TARGETDURATION:4 +#EXT-X-PART-INF:PART-TARGET=0.320 +#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.960 +#EXT-X-MEDIA-SEQUENCE:1200 +#EXT-X-PROGRAM-DATE-TIME:2026-10-01T12:00:00.000Z +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.0.wai#cid=b8637cf58c3a5a0c567115d9065e5ec6d228663a65e50cb0b95a8b0dfb4cc385&n=3026" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.1.wai#cid=c4336fb0c48737e9e68b8fe5f4bc60aa8314878a602de85cfc9c3c0d33c0e03f&n=2962" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.2.wai#cid=12f271588cd4463ab7a8c89a25902fac152de57ec5420330b9ba5afd1fe0873a&n=2994" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.3.wai#cid=00af350c635dd361002588143eaac2171e7b3ba39e8f8354eb3ac3fabd3392d4&n=3018" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.4.wai#cid=27432e8c8ded7a74a057d0ffa83768d249982ed842d3e8659d0e39f69e92c815&n=3026" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.5.wai#cid=2f38e98c125601d888c9a719cf0ac74e0846fe09c2d9fb09ed88b54cabc3a778&n=2962" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.6.wai#cid=0a749256ffcfd6cfb3b396f04cea3d73622b5c230325096eed3585aa5f813008&n=2994" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.7.wai#cid=dd01430fa65add27f88d5de32bb2100a3491b6202e935a1d4101aae8062d9d6f&n=3018" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.8.wai#cid=7095c2d1f03b11435df79189640f73303b9c1492fa302d1dcbdb26f1527d9ce6&n=3026" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.9.wai#cid=55f3946ced44b2e8384e772b9ffb71f07acbb176d171508cf269d06f0cf69676&n=2962" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.10.wai#cid=f04294c0f3f12e5e6309b39f1c336ef3257c8c755dbeb69314bce44fcc0947ac&n=2994" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1200.11.wai#cid=62853bb69f462471f250a4140c254b7bb0af2cf0691e4e759b89cc320f5f8659&n=3014" +#EXTINF:3.840, +../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606 +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.0.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.1.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.2.wai#cid=a002266b23a292284444fb753d53a3def273636f4bd7ca182b635326d9f74c7b&n=3122" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.3.wai#cid=a8ae3c937be0969d0e9a355e3afedd51eddb754dc3a95ed6fcd9386b2a6372cf&n=3190" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.4.wai#cid=6f5e3d4204b88e143da63ef4dc22840e65839fdbf47d2a0c03d68695860c0c70&n=3238" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.5.wai#cid=35f910e1a0a4acfbf8ed201e8ccd430302f395280aa79f9657b3cb16f23ba5b0&n=3146" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.6.wai#cid=cd095db1f4b9560782af8ba700fe5154e7ef541554a0550ea8edb796bde725ce&n=3222" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.7.wai#cid=173c227ba468e8809e9b7b016264bd86dc1310c7681a732e9d0886373411915f&n=3222" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.8.wai#cid=db17bd07b023ddcc31b057df9d0da1354b148c3920489e345201a8d3c2c72bed&n=3238" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.9.wai#cid=48c691c6021b6b07ae5a7046dd04eaee0b8ebb7ce3a5b9aca275ec5015d54312&n=3170" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.10.wai#cid=fa7e77738aaf35251d277030fb58d1cd9a3f262ce9b44bbb16248142037bf32d&n=3222" +#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI="720/1201.11.wai#cid=5f341dcfe19ad23b52d1c879fa2d6f52c60829e89d47cc544dfb0891cac41304&n=3222" +#EXTINF:3.840, +../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118 +#EXT-X-PRELOAD-HINT:TYPE=PART,URI="720/1202.0.wai" diff --git a/wai/http-conformance/cases/relist/wai-r3-gap.m3u8 b/wai/http-conformance/cases/relist/wai-r3-gap.m3u8 new file mode 100644 index 000000000..a55986f51 --- /dev/null +++ b/wai/http-conformance/cases/relist/wai-r3-gap.m3u8 @@ -0,0 +1,13 @@ +#EXTM3U +#EXT-X-VERSION:9 +#EXT-X-TARGETDURATION:4 +#EXT-X-PART-INF:PART-TARGET=0.320 +#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.960 +#EXT-X-MEDIA-SEQUENCE:1200 +#EXT-X-PROGRAM-DATE-TIME:2026-10-01T12:00:00.000Z +#EXT-X-GAP +#EXTINF:3.840, +../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606 +#EXTINF:3.840, +../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118 +#EXT-X-ENDLIST diff --git a/wai/http-conformance/cases/relist/wai-r3.m3u8 b/wai/http-conformance/cases/relist/wai-r3.m3u8 new file mode 100644 index 000000000..0c9177660 --- /dev/null +++ b/wai/http-conformance/cases/relist/wai-r3.m3u8 @@ -0,0 +1,12 @@ +#EXTM3U +#EXT-X-VERSION:9 +#EXT-X-TARGETDURATION:4 +#EXT-X-PART-INF:PART-TARGET=0.320 +#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.960 +#EXT-X-MEDIA-SEQUENCE:1200 +#EXT-X-PROGRAM-DATE-TIME:2026-10-01T12:00:00.000Z +#EXTINF:3.840, +../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118 +#EXTINF:3.840, +../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118 +#EXT-X-ENDLIST diff --git a/wai/http-conformance/cases/sig-key-base-point.json b/wai/http-conformance/cases/sig-key-base-point.json new file mode 100644 index 000000000..066cfbbbf --- /dev/null +++ b/wai/http-conformance/cases/sig-key-base-point.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798","session_class_hash":"20acdf15043e64cc0a0642ca48c3719647bd5f03c57d3bcf344d0c1f49466443","class":{"content_id":"o9","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":0},"segments":[{"index":0,"kind":"content","uri":"../../objects/31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3.wai#n=21034&h=1009","duration_ms":640,"ad_id":null},{"index":1,"kind":"content","uri":"../../objects/b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb.wai#n=18004","duration_ms":640,"ad_id":null},{"index":2,"kind":"content","uri":"../../objects/a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133.wai#n=18381","duration_ms":640,"ad_id":null}],"merkle_root":"323e355c473b98404de2b55855d3abcc843c0e67e07f40ac47fa1b5c30568f86","work_compose_ops":3,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":null,"signer_pubkey":"5866666666666666666666666666666666666666666666666666666666666666","signer_id":"did:example:http-vectors","sig":"58666666666666666666666666666666666666666666666666666666666666662f03c04a2605fd41de75215b9a872c6a05423724a0ed9ee29c5c6854cc8a7d05","render":{"version":7,"media_sequence":0,"discontinuity_sequence":0,"target_duration_s":1,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":true,"playlist_type":"VOD","independent_segments":false,"map_uri":null},"extras":[]} diff --git a/wai/http-conformance/cases/sig-key-identity-long.json b/wai/http-conformance/cases/sig-key-identity-long.json new file mode 100644 index 000000000..a7bfb766a --- /dev/null +++ b/wai/http-conformance/cases/sig-key-identity-long.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798","session_class_hash":"20acdf15043e64cc0a0642ca48c3719647bd5f03c57d3bcf344d0c1f49466443","class":{"content_id":"o9","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":0},"segments":[{"index":0,"kind":"content","uri":"../../objects/31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3.wai#n=21034&h=1009","duration_ms":640,"ad_id":null},{"index":1,"kind":"content","uri":"../../objects/b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb.wai#n=18004","duration_ms":640,"ad_id":null},{"index":2,"kind":"content","uri":"../../objects/a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133.wai#n=18381","duration_ms":640,"ad_id":null}],"merkle_root":"323e355c473b98404de2b55855d3abcc843c0e67e07f40ac47fa1b5c30568f86","work_compose_ops":3,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":null,"signer_pubkey":"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f","signer_id":"did:example:http-vectors","sig":"58666666666666666666666666666666666666666666666666666666666666660100000000000000000000000000000000000000000000000000000000000000","render":{"version":7,"media_sequence":0,"discontinuity_sequence":0,"target_duration_s":1,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":true,"playlist_type":"VOD","independent_segments":false,"map_uri":null},"extras":[]} diff --git a/wai/http-conformance/cases/sig-key-identity.json b/wai/http-conformance/cases/sig-key-identity.json new file mode 100644 index 000000000..8a0a7457e --- /dev/null +++ b/wai/http-conformance/cases/sig-key-identity.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798","session_class_hash":"20acdf15043e64cc0a0642ca48c3719647bd5f03c57d3bcf344d0c1f49466443","class":{"content_id":"o9","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":0},"segments":[{"index":0,"kind":"content","uri":"../../objects/31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3.wai#n=21034&h=1009","duration_ms":640,"ad_id":null},{"index":1,"kind":"content","uri":"../../objects/b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb.wai#n=18004","duration_ms":640,"ad_id":null},{"index":2,"kind":"content","uri":"../../objects/a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133.wai#n=18381","duration_ms":640,"ad_id":null}],"merkle_root":"323e355c473b98404de2b55855d3abcc843c0e67e07f40ac47fa1b5c30568f86","work_compose_ops":3,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":null,"signer_pubkey":"0100000000000000000000000000000000000000000000000000000000000000","signer_id":"did:example:http-vectors","sig":"58666666666666666666666666666666666666666666666666666666666666660100000000000000000000000000000000000000000000000000000000000000","render":{"version":7,"media_sequence":0,"discontinuity_sequence":0,"target_duration_s":1,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":true,"playlist_type":"VOD","independent_segments":false,"map_uri":null},"extras":[]} diff --git a/wai/http-conformance/cases/sig-r-non-canonical.json b/wai/http-conformance/cases/sig-r-non-canonical.json new file mode 100644 index 000000000..d034d4da9 --- /dev/null +++ b/wai/http-conformance/cases/sig-r-non-canonical.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798","session_class_hash":"20acdf15043e64cc0a0642ca48c3719647bd5f03c57d3bcf344d0c1f49466443","class":{"content_id":"o9","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":0},"segments":[{"index":0,"kind":"content","uri":"../../objects/31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3.wai#n=21034&h=1009","duration_ms":640,"ad_id":null},{"index":1,"kind":"content","uri":"../../objects/b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb.wai#n=18004","duration_ms":640,"ad_id":null},{"index":2,"kind":"content","uri":"../../objects/a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133.wai#n=18381","duration_ms":640,"ad_id":null}],"merkle_root":"323e355c473b98404de2b55855d3abcc843c0e67e07f40ac47fa1b5c30568f86","work_compose_ops":3,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":null,"signer_pubkey":"5866666666666666666666666666666666666666666666666666666666666666","signer_id":"did:example:http-vectors","sig":"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f080a137c08411f3c035f03a58f1b5131bcb5887f5082212f2caa4d827d8aed04","render":{"version":7,"media_sequence":0,"discontinuity_sequence":0,"target_duration_s":1,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":true,"playlist_type":"VOD","independent_segments":false,"map_uri":null},"extras":[]} diff --git a/wai/http-conformance/cases/sig-r-small-order.json b/wai/http-conformance/cases/sig-r-small-order.json new file mode 100644 index 000000000..fbab2e251 --- /dev/null +++ b/wai/http-conformance/cases/sig-r-small-order.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"e6fff6deff390739a13844a07178f756446079c7d95a327e14c85428ffb6c798","session_class_hash":"20acdf15043e64cc0a0642ca48c3719647bd5f03c57d3bcf344d0c1f49466443","class":{"content_id":"o9","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":0},"segments":[{"index":0,"kind":"content","uri":"../../objects/31abc1da2cdf927191f3f447f68508ead30d078690cb7bea2435ae3d1df5f4f3.wai#n=21034&h=1009","duration_ms":640,"ad_id":null},{"index":1,"kind":"content","uri":"../../objects/b7a711ab4b0a0939a561a16b3167039e7fe65c640d73a040ebc5d5cf86e4ecbb.wai#n=18004","duration_ms":640,"ad_id":null},{"index":2,"kind":"content","uri":"../../objects/a8ab2cd0cc277ef7109aacdee6bb55ff713aa0d4dc4c2e3dfe40aab759a90133.wai#n=18381","duration_ms":640,"ad_id":null}],"merkle_root":"323e355c473b98404de2b55855d3abcc843c0e67e07f40ac47fa1b5c30568f86","work_compose_ops":3,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":null,"signer_pubkey":"5866666666666666666666666666666666666666666666666666666666666666","signer_id":"did:example:http-vectors","sig":"0100000000000000000000000000000000000000000000000000000000000000f57a2a3a17cc1400dba50adbc733921a1b095514a8d91a25909f588c437ac10f","render":{"version":7,"media_sequence":0,"discontinuity_sequence":0,"target_duration_s":1,"part_target_ms":null,"part_hold_back_ms":null,"can_block_reload":false,"end_list":true,"playlist_type":"VOD","independent_segments":false,"map_uri":null},"extras":[]} diff --git a/wai/http-conformance/cases/walk-invalid/mr/532a6470fd1df4eb9eca528cb34215932108c4da3fde179d6cbaa3b91c296b1c.json b/wai/http-conformance/cases/walk-invalid/mr/532a6470fd1df4eb9eca528cb34215932108c4da3fde179d6cbaa3b91c296b1c.json new file mode 100644 index 000000000..dfb90cbab --- /dev/null +++ b/wai/http-conformance/cases/walk-invalid/mr/532a6470fd1df4eb9eca528cb34215932108c4da3fde179d6cbaa3b91c296b1c.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"3274f7d04d766ab2894b8da756aafe2d76e1cbb92ee9c88b803d0b21d2428900","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"40add72be1d163ade96031789b4d36ca59dfa6100305a6ad2769fd17b096d07d","work_compose_ops":2,"viewers_served":2,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"eab14aa7350c082b1cc52b78c74eb5f2660f80ca6267a73a2ae9cd186df824ed","signer_pubkey":"c84069fa2216c6edc1d52ca83aa11c267dd5440afa526090ca43ebd0ca86a9cf","signer_id":"did:example:http-vectors","sig":"d02f849c0a2476cd732b53039fa56de97d4aedf4f36c49696a0bad2349f8530c31deea9635825a8fc33ef0a16a55e55b15d90a9633e821bcdfed0a5eba7be803","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[{"duration_ms":320,"independent":true,"uri":"720/1200.0.wai#cid=b8637cf58c3a5a0c567115d9065e5ec6d228663a65e50cb0b95a8b0dfb4cc385&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.1.wai#cid=c4336fb0c48737e9e68b8fe5f4bc60aa8314878a602de85cfc9c3c0d33c0e03f&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.2.wai#cid=12f271588cd4463ab7a8c89a25902fac152de57ec5420330b9ba5afd1fe0873a&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.3.wai#cid=00af350c635dd361002588143eaac2171e7b3ba39e8f8354eb3ac3fabd3392d4&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.4.wai#cid=27432e8c8ded7a74a057d0ffa83768d249982ed842d3e8659d0e39f69e92c815&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.5.wai#cid=2f38e98c125601d888c9a719cf0ac74e0846fe09c2d9fb09ed88b54cabc3a778&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.6.wai#cid=0a749256ffcfd6cfb3b396f04cea3d73622b5c230325096eed3585aa5f813008&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.7.wai#cid=dd01430fa65add27f88d5de32bb2100a3491b6202e935a1d4101aae8062d9d6f&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.8.wai#cid=7095c2d1f03b11435df79189640f73303b9c1492fa302d1dcbdb26f1527d9ce6&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.9.wai#cid=55f3946ced44b2e8384e772b9ffb71f07acbb176d171508cf269d06f0cf69676&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.10.wai#cid=f04294c0f3f12e5e6309b39f1c336ef3257c8c755dbeb69314bce44fcc0947ac&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.11.wai#cid=62853bb69f462471f250a4140c254b7bb0af2cf0691e4e759b89cc320f5f8659&n=3014","layers":[]}],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[{"duration_ms":320,"independent":true,"uri":"720/1201.0.wai#cid=7ae66a04b663eaf965ee71c445af732893632ab0713cca8139969d3529e8ec8e&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.1.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.2.wai#cid=a002266b23a292284444fb753d53a3def273636f4bd7ca182b635326d9f74c7b&n=3122","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.3.wai#cid=a8ae3c937be0969d0e9a355e3afedd51eddb754dc3a95ed6fcd9386b2a6372cf&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.4.wai#cid=6f5e3d4204b88e143da63ef4dc22840e65839fdbf47d2a0c03d68695860c0c70&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.5.wai#cid=35f910e1a0a4acfbf8ed201e8ccd430302f395280aa79f9657b3cb16f23ba5b0&n=3146","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.6.wai#cid=cd095db1f4b9560782af8ba700fe5154e7ef541554a0550ea8edb796bde725ce&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.7.wai#cid=173c227ba468e8809e9b7b016264bd86dc1310c7681a732e9d0886373411915f&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.8.wai#cid=db17bd07b023ddcc31b057df9d0da1354b148c3920489e345201a8d3c2c72bed&n=3238","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.9.wai#cid=48c691c6021b6b07ae5a7046dd04eaee0b8ebb7ce3a5b9aca275ec5015d54312&n=3170","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.10.wai#cid=fa7e77738aaf35251d277030fb58d1cd9a3f262ce9b44bbb16248142037bf32d&n=3222","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.11.wai#cid=5f341dcfe19ad23b52d1c879fa2d6f52c60829e89d47cc544dfb0891cac41304&n=3222","layers":[]}],"layers":[],"preload_hint":"720/1202.0.wai"}]} diff --git a/wai/http-conformance/cases/walk-invalid/r3.json b/wai/http-conformance/cases/walk-invalid/r3.json new file mode 100644 index 000000000..659e8293f --- /dev/null +++ b/wai/http-conformance/cases/walk-invalid/r3.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"b6e2d55ea619bd811d877d9098ad02edf6d70de7823de8c1c876b41e202d133e","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null},{"index":1,"kind":"content","uri":"../objects/63c08b05659fd758074ff41a1378a391b7d449b0010a4ddd1894efb60026bb17.wai#n=19118","duration_ms":3840,"ad_id":null}],"merkle_root":"40add72be1d163ade96031789b4d36ca59dfa6100305a6ad2769fd17b096d07d","work_compose_ops":2,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"532a6470fd1df4eb9eca528cb34215932108c4da3fde179d6cbaa3b91c296b1c","signer_pubkey":"c84069fa2216c6edc1d52ca83aa11c267dd5440afa526090ca43ebd0ca86a9cf","signer_id":"did:example:http-vectors","sig":"f4e778d20184fa490c5e0d89da546a9131dbbf9c9f16edc5b0f845f80687103ca6fff7ea16c5d847bb667a26c1fe8adf44b10dcc1b9d54849939ff9f22fd890d","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":true,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[],"layers":[],"preload_hint":null}]} diff --git a/wai/http-conformance/cases/walk-substituted/mr/4ed29942f6a294bf03d422a1b76b93b7c0b84ff123b99ee2c26505649c68bdf6.json b/wai/http-conformance/cases/walk-substituted/mr/4ed29942f6a294bf03d422a1b76b93b7c0b84ff123b99ee2c26505649c68bdf6.json new file mode 100644 index 000000000..53e2c5d3b --- /dev/null +++ b/wai/http-conformance/cases/walk-substituted/mr/4ed29942f6a294bf03d422a1b76b93b7c0b84ff123b99ee2c26505649c68bdf6.json @@ -0,0 +1 @@ +{"kind":"wai.video.manifest","manifest_hash":"091740345f60047c283dfbb184cff89d86f93355c2162648dd032ac2263c4271","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[{"index":0,"kind":"content","uri":"../objects/9de886a61b983bfcb8118ec313551c5af730a176180151756f8071fbb7d5f716.wai#n=16606","duration_ms":3840,"ad_id":null}],"merkle_root":"00e2bc6cdc748c6ab760c8e867603fd4d9a72d3a58f985bd13810a8d77aedcd3","work_compose_ops":1,"viewers_served":1,"joules_micro":0,"grant":{"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","capability":"video.manifest","joule_ceiling_micro":null,"funds_ceiling":null},"parent_receipt_hash":"3ce1b1f8805024633d89312232438e6ff46022b5a7ba48c994338876b6a7c364","signer_pubkey":"c84069fa2216c6edc1d52ca83aa11c267dd5440afa526090ca43ebd0ca86a9cf","signer_id":"did:example:http-vectors","sig":"8186f3adb28113d8ad08c923d24df463eb898d7a95b04902d23ca208fc17fe677249dc30f280faef8ec5aeac81abdb6b6fcd577075d9e9346f445c3b8c13fa03","render":{"version":9,"media_sequence":1200,"discontinuity_sequence":0,"target_duration_s":4,"part_target_ms":320,"part_hold_back_ms":960,"can_block_reload":true,"end_list":false,"playlist_type":null,"independent_segments":false,"map_uri":null},"extras":[{"discontinuity":false,"gap":false,"program_date_time":"2026-10-01T12:00:00.000Z","parts":[{"duration_ms":320,"independent":true,"uri":"720/1200.0.wai#cid=b8637cf58c3a5a0c567115d9065e5ec6d228663a65e50cb0b95a8b0dfb4cc385&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.1.wai#cid=c4336fb0c48737e9e68b8fe5f4bc60aa8314878a602de85cfc9c3c0d33c0e03f&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.2.wai#cid=12f271588cd4463ab7a8c89a25902fac152de57ec5420330b9ba5afd1fe0873a&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.3.wai#cid=00af350c635dd361002588143eaac2171e7b3ba39e8f8354eb3ac3fabd3392d4&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.4.wai#cid=27432e8c8ded7a74a057d0ffa83768d249982ed842d3e8659d0e39f69e92c815&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.5.wai#cid=2f38e98c125601d888c9a719cf0ac74e0846fe09c2d9fb09ed88b54cabc3a778&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.6.wai#cid=0a749256ffcfd6cfb3b396f04cea3d73622b5c230325096eed3585aa5f813008&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.7.wai#cid=dd01430fa65add27f88d5de32bb2100a3491b6202e935a1d4101aae8062d9d6f&n=3018","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.8.wai#cid=7095c2d1f03b11435df79189640f73303b9c1492fa302d1dcbdb26f1527d9ce6&n=3026","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.9.wai#cid=55f3946ced44b2e8384e772b9ffb71f07acbb176d171508cf269d06f0cf69676&n=2962","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.10.wai#cid=f04294c0f3f12e5e6309b39f1c336ef3257c8c755dbeb69314bce44fcc0947ac&n=2994","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1200.11.wai#cid=62853bb69f462471f250a4140c254b7bb0af2cf0691e4e759b89cc320f5f8659&n=3014","layers":[]}],"layers":[],"preload_hint":null},{"discontinuity":false,"gap":false,"program_date_time":null,"parts":[{"duration_ms":320,"independent":true,"uri":"720/1201.0.wai#cid=7ae66a04b663eaf965ee71c445af732893632ab0713cca8139969d3529e8ec8e&n=3190","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.1.wai#cid=ab999eaf1dfdbdee1784b3de6b4f1c7edf311119358fdb59870ad9d51c369c3d&n=3042","layers":[]},{"duration_ms":320,"independent":true,"uri":"720/1201.2.wai#cid=a002266b23a292284444fb753d53a3def273636f4bd7ca182b635326d9f74c7b&n=3122","layers":[]}],"layers":[],"preload_hint":"720/1201.3.wai"}]} diff --git a/wai/http-conformance/falsify/abr-options-unbounded.patch b/wai/http-conformance/falsify/abr-options-unbounded.patch new file mode 100644 index 000000000..35ccb4fda --- /dev/null +++ b/wai/http-conformance/falsify/abr-options-unbounded.patch @@ -0,0 +1,15 @@ +# must fail: abr_out_of_bounds_options abr_out_of_bounds_figures +# options past the bound read (abr::check_state) +diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs +index 6fb8973c..9dace574 100644 +--- a/wai/wai-rs/src/http_binding/abr.rs ++++ b/wai/wai-rs/src/http_binding/abr.rs +@@ -53,7 +53,7 @@ pub const MAX_OPTIONS: usize = 256; + /// field below 2^32, each byte count, figure and budget below 2^64): at most + /// [`MAX_OPTIONS`] options and figures. + pub fn check_state(s: &AbrState) -> Result<(), AbrError> { +- if s.option_bytes.len() > MAX_OPTIONS || s.option_joules_micro.len() > MAX_OPTIONS { ++ if false { + return Err(AbrError("more options than 256")); + } + Ok(()) diff --git a/wai/http-conformance/falsify/abr-parameter-truncated.patch b/wai/http-conformance/falsify/abr-parameter-truncated.patch new file mode 100644 index 000000000..9191812a5 --- /dev/null +++ b/wai/http-conformance/falsify/abr-parameter-truncated.patch @@ -0,0 +1,15 @@ +# must fail: abr_out_of_bounds_last_n abr_out_of_bounds_horizon +# a parameter past its type truncated (the corpus tool's policy reading) +diff --git a/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs b/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs +index 3d4fecbb..b61542ec 100644 +--- a/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs ++++ b/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs +@@ -420,7 +420,7 @@ fn bounded_opt(v: &Value, max: u64, what: &str) -> Result, Stop> { + /// and the like, each parameter within its type (Appendix F). + pub(super) fn policy(v: &Value) -> Result, Stop> { + let u = |k: &str, max: u64| bounded(&v[k], max, &format!("policy.{k}")); +- let (u8m, u16m, u32m) = (u64::from(u8::MAX), u64::from(u16::MAX), u64::from(u32::MAX)); ++ let (u8m, u16m, u32m) = (u64::MAX, u64::MAX, u64::MAX); + Ok(match v["id"].as_str() { + Some("throughput") => Box::new(Throughput { last_n: u("last_n", u8m)? as u8, safety_permille: u("safety_permille", u16m)? as u16 }), + Some("buffer_based") => Box::new(BufferBased { reservoir_ms: u("reservoir_ms", u32m)? as u32, cushion_ms: u("cushion_ms", u32m)? as u32 }), diff --git a/wai/http-conformance/falsify/adjacency-dseq.patch b/wai/http-conformance/falsify/adjacency-dseq.patch new file mode 100644 index 000000000..641049b41 --- /dev/null +++ b/wai/http-conformance/falsify/adjacency-dseq.patch @@ -0,0 +1,15 @@ +# must fail: adjacency_discontinuity_sequence +# adjacency ignores a larger discontinuity sequence (hls::timelines_adjacent) +diff --git a/wai/wai-rs/src/http_binding/hls.rs b/wai/wai-rs/src/http_binding/hls.rs +index 2623644b..aa290009 100644 +--- a/wai/wai-rs/src/http_binding/hls.rs ++++ b/wai/wai-rs/src/http_binding/hls.rs +@@ -394,7 +394,7 @@ pub fn timelines_adjacent(wai: &MediaPlaylist, classical: &MediaPlaylist) -> Res + if wai.media_sequence != classical.media_sequence { + return Err(misaligned(format!("EXT-X-MEDIA-SEQUENCE {} beside {}", wai.media_sequence, classical.media_sequence))); + } +- if wai.discontinuity_sequence != classical.discontinuity_sequence { ++ if wai.discontinuity_sequence < classical.discontinuity_sequence { + return Err(misaligned(format!("EXT-X-DISCONTINUITY-SEQUENCE {} beside {}", wai.discontinuity_sequence, classical.discontinuity_sequence))); + } + if wai.segments.len() != classical.segments.len() { diff --git a/wai/http-conformance/falsify/adjacency-pdt.patch b/wai/http-conformance/falsify/adjacency-pdt.patch new file mode 100644 index 000000000..86aba2a34 --- /dev/null +++ b/wai/http-conformance/falsify/adjacency-pdt.patch @@ -0,0 +1,15 @@ +# must fail: adjacency_program_date_time +# adjacency ignores another program date-time (hls::timelines_adjacent) +diff --git a/wai/wai-rs/src/http_binding/hls.rs b/wai/wai-rs/src/http_binding/hls.rs +index 2623644b..e93d4430 100644 +--- a/wai/wai-rs/src/http_binding/hls.rs ++++ b/wai/wai-rs/src/http_binding/hls.rs +@@ -412,7 +412,7 @@ pub fn timelines_adjacent(wai: &MediaPlaylist, classical: &MediaPlaylist) -> Res + if w.extra.discontinuity != c.extra.discontinuity { + return Err(misaligned(format!("segment {seq}: a discontinuity on one side only"))); + } +- if w.extra.program_date_time != c.extra.program_date_time { ++ if w.extra.program_date_time.is_some() && c.extra.program_date_time.is_none() { + return Err(misaligned(format!("segment {seq}: another program date-time"))); + } + if both_parts && !w.extra.parts.is_empty() && !c.extra.parts.is_empty() && parts_ms(&w.extra.parts) != parts_ms(&c.extra.parts) { diff --git a/wai/http-conformance/falsify/bandwidth-average-floor.patch b/wai/http-conformance/falsify/bandwidth-average-floor.patch new file mode 100644 index 000000000..17bc96436 --- /dev/null +++ b/wai/http-conformance/falsify/bandwidth-average-floor.patch @@ -0,0 +1,15 @@ +# must fail: multivariant_playlists_o9_wai_m3u8 +# AVERAGE-BANDWIDTH rounded down (hls::bandwidths) +diff --git a/wai/wai-rs/src/http_binding/hls.rs b/wai/wai-rs/src/http_binding/hls.rs +index 2623644b..604ff92b 100644 +--- a/wai/wai-rs/src/http_binding/hls.rs ++++ b/wai/wai-rs/src/http_binding/hls.rs +@@ -608,7 +608,7 @@ pub fn bandwidths(objects: &[(u64, u64)]) -> Option<(u64, u64)> { + let rate = |n: u128, d: u128| u64::try_from((8000 * n).div_ceil(d)).unwrap_or(u64::MAX); + let peak = objects.iter().map(|&(n, d)| rate(u128::from(n), u128::from(d))).max()?; + let (sn, sd) = objects.iter().fold((0u128, 0u128), |(a, b), &(n, d)| (a + u128::from(n), b + u128::from(d))); +- Some((peak, rate(sn, sd))) ++ Some((peak, u64::try_from(8000 * sn / sd).unwrap_or(u64::MAX))) + } + + /// One variant of `wai.m3u8` (§11.1). diff --git a/wai/http-conformance/falsify/bola-narrow-product.patch b/wai/http-conformance/falsify/bola-narrow-product.patch new file mode 100644 index 000000000..25c2884bb --- /dev/null +++ b/wai/http-conformance/falsify/bola-narrow-product.patch @@ -0,0 +1,15 @@ +# must fail: abr_extreme_bola_like_past_128_bits +# the utility-over-buffer cross-multiplication in 128 bits, wrapping (abr::BolaLike::choose) +diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs +index 6fb8973c..0f72e915 100644 +--- a/wai/wai-rs/src/http_binding/abr.rs ++++ b/wai/wai-rs/src/http_binding/abr.rs +@@ -302,7 +302,7 @@ impl Policy for BolaLike { + for i in (0..k).filter(|&i| score[i] > 0) { + // score[i]/size(i) > score[b]/size(b), by cross-multiplication widened + // past 128 bits: a score times a size reaches 2^141. +- if best.is_none_or(|b| product_greater(score[i] as u128, size(b), score[b] as u128, size(i))) { ++ if best.is_none_or(|b| (score[i] as u128).wrapping_mul(u128::from(size(b))) > (score[b] as u128).wrapping_mul(u128::from(size(i)))) { + best = Some(i); + } + } diff --git a/wai/http-conformance/falsify/bola-waits.patch b/wai/http-conformance/falsify/bola-waits.patch new file mode 100644 index 000000000..dd5318b95 --- /dev/null +++ b/wai/http-conformance/falsify/bola-waits.patch @@ -0,0 +1,15 @@ +# must fail: abr_bola_like_00 abr_bola_like_02 +# the utility rule takes the lowest option where no option scores, as if it waited (abr::BolaLike::choose) +diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs +index a8046b34..86597807 100644 +--- a/wai/wai-rs/src/http_binding/abr.rs ++++ b/wai/wai-rs/src/http_binding/abr.rs +@@ -241,6 +241,8 @@ impl Policy for BolaLike { + best = Some(i); + } + } ++ } else if k > 0 { ++ return 0; + } else { + for i in 0..k { + if best.is_none_or(|b| score[i] > score[b]) { diff --git a/wai/http-conformance/falsify/budget-raises.patch b/wai/http-conformance/falsify/budget-raises.patch new file mode 100644 index 000000000..d22286616 --- /dev/null +++ b/wai/http-conformance/falsify/budget-raises.patch @@ -0,0 +1,15 @@ +# must fail: abr_budget_not_raised_to_labelled_options abr_budget_not_raised_to_one_within +# the budget raises the choice to the lowest labelled option above it within the budget (abr::clamp_to_budget) +diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs +index cc9cd96a..a0cb30b9 100644 +--- a/wai/wai-rs/src/http_binding/abr.rs ++++ b/wai/wai-rs/src/http_binding/abr.rs +@@ -398,7 +398,7 @@ pub fn clamp_to_budget(s: &AbrState, choice: usize) -> (usize, Budget) { + match (0..=choice).rev().find(|&k| fig(k).is_some_and(|j| j <= b)) { + Some(k) if k == choice => (k, Budget::Within), + Some(k) => (k, Budget::Lowered), +- None => (0, Budget::Unmet), ++ None => ((choice + 1..n).find(|&k| fig(k).is_some_and(|j| j <= b)).unwrap_or(0), Budget::Unmet), + } + } + diff --git a/wai/http-conformance/falsify/budget-unlabelled-escapes.patch b/wai/http-conformance/falsify/budget-unlabelled-escapes.patch new file mode 100644 index 000000000..291cb2288 --- /dev/null +++ b/wai/http-conformance/falsify/budget-unlabelled-escapes.patch @@ -0,0 +1,15 @@ +# must fail: abr_budget_unlabelled_choice +# an unlabelled choice leaves the budget unapplied (abr::clamp_to_budget) +diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs +index cc9cd96a..20028eea 100644 +--- a/wai/wai-rs/src/http_binding/abr.rs ++++ b/wai/wai-rs/src/http_binding/abr.rs +@@ -392,7 +392,7 @@ pub fn clamp_to_budget(s: &AbrState, choice: usize) -> (usize, Budget) { + let Some(b) = s.joule_budget_micro else { return (choice, Budget::NotSet) }; + let n = s.option_bytes.len().max(choice + 1); + let fig = |k: usize| if k < n { s.option_joules_micro.get(k).copied().flatten() } else { None }; +- if (0..n).all(|k| fig(k).is_none()) { ++ if (0..n).all(|k| fig(k).is_none()) || fig(choice).is_none() { + return (choice, Budget::NotApplied); + } + match (0..=choice).rev().find(|&k| fig(k).is_some_and(|j| j <= b)) { diff --git a/wai/http-conformance/falsify/budget-unlabelled.patch b/wai/http-conformance/falsify/budget-unlabelled.patch new file mode 100644 index 000000000..9d44a21b0 --- /dev/null +++ b/wai/http-conformance/falsify/budget-unlabelled.patch @@ -0,0 +1,15 @@ +# must fail: abr_budget_not_applied abr_budget_unlabelled_choice depth_O10_budget_unlabelled_layer +# an unlabelled figure taken as zero joules (abr::clamp_to_budget) +diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs +index cc9cd96a..18d9cbfe 100644 +--- a/wai/wai-rs/src/http_binding/abr.rs ++++ b/wai/wai-rs/src/http_binding/abr.rs +@@ -391,7 +391,7 @@ pub fn labelled_figure(joules_micro: u64, class: Option<&crate::energy_class::En + pub fn clamp_to_budget(s: &AbrState, choice: usize) -> (usize, Budget) { + let Some(b) = s.joule_budget_micro else { return (choice, Budget::NotSet) }; + let n = s.option_bytes.len().max(choice + 1); +- let fig = |k: usize| if k < n { s.option_joules_micro.get(k).copied().flatten() } else { None }; ++ let fig = |k: usize| if k < n { s.option_joules_micro.get(k).copied().flatten().or(Some(0)) } else { None }; + if (0..n).all(|k| fig(k).is_none()) { + return (choice, Budget::NotApplied); + } diff --git a/wai/http-conformance/falsify/budget-unmet-least-figure.patch b/wai/http-conformance/falsify/budget-unmet-least-figure.patch new file mode 100644 index 000000000..1c59ec854 --- /dev/null +++ b/wai/http-conformance/falsify/budget-unmet-least-figure.patch @@ -0,0 +1,15 @@ +# must fail: abr_budget_unmet_not_the_least_figure abr_budget_not_raised_past_the_budget +# with nothing within the budget, the option of the least labelled figure (abr::clamp_to_budget) +diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs +index cc9cd96a..a2ae9f56 100644 +--- a/wai/wai-rs/src/http_binding/abr.rs ++++ b/wai/wai-rs/src/http_binding/abr.rs +@@ -398,7 +398,7 @@ pub fn clamp_to_budget(s: &AbrState, choice: usize) -> (usize, Budget) { + match (0..=choice).rev().find(|&k| fig(k).is_some_and(|j| j <= b)) { + Some(k) if k == choice => (k, Budget::Within), + Some(k) => (k, Budget::Lowered), +- None => (0, Budget::Unmet), ++ None => ((0..n).filter_map(|k| fig(k).map(|j| (j, k))).min().map_or(0, |(_, k)| k), Budget::Unmet), + } + } + diff --git a/wai/http-conformance/falsify/chain-bindings-dropped.patch b/wai/http-conformance/falsify/chain-bindings-dropped.patch new file mode 100644 index 000000000..b1fa5fc37 --- /dev/null +++ b/wai/http-conformance/falsify/chain-bindings-dropped.patch @@ -0,0 +1,14 @@ +# must fail: live_edge +# what the chain's receipts list not bound (LiveChain::advance) +diff --git a/wai/wai-rs/src/http_binding/playlist_receipt.rs b/wai/wai-rs/src/http_binding/playlist_receipt.rs +index 6666c22a..14d8229f 100644 +--- a/wai/wai-rs/src/http_binding/playlist_receipt.rs ++++ b/wai/wai-rs/src/http_binding/playlist_receipt.rs +@@ -240,7 +240,6 @@ impl LiveChain { + for r in walked.iter().rev().chain(std::iter::once(cur)) { + bind(&mut b, r)?; + } +- self.bindings = b; + self.head = Some(cur.clone()); + Ok(Step::Chained { walked: walked.len() }) + } diff --git a/wai/http-conformance/falsify/chain-key-unchecked.patch b/wai/http-conformance/falsify/chain-key-unchecked.patch new file mode 100644 index 000000000..adba19af5 --- /dev/null +++ b/wai/http-conformance/falsify/chain-key-unchecked.patch @@ -0,0 +1,24 @@ +# must fail: live_edge_other_key live_edge_other_key_walked +# a receipt chained by hash under another key accepted (LiveChain::advance) +diff --git a/wai/wai-rs/src/http_binding/playlist_receipt.rs b/wai/wai-rs/src/http_binding/playlist_receipt.rs +index 6666c22a..58f23c43 100644 +--- a/wai/wai-rs/src/http_binding/playlist_receipt.rs ++++ b/wai/wai-rs/src/http_binding/playlist_receipt.rs +@@ -207,7 +207,7 @@ impl LiveChain { + if cur.receipt_hash() == target { + return Ok(Step::Unchanged); + } +- if cur.signer_pubkey != head.signer_pubkey { ++ if false { + return Err(ReceiptError::Unchained); + } + let mut walked: Vec = Vec::new(); +@@ -230,7 +230,7 @@ impl LiveChain { + if !r.verify() { + return Err(ReceiptError::Invalid); + } +- if r.signer_pubkey != cur.signer_pubkey { ++ if false { + return Err(ReceiptError::Unchained); + } + link = r.parent_receipt_hash; diff --git a/wai/http-conformance/falsify/chain-parent-unchecked.patch b/wai/http-conformance/falsify/chain-parent-unchecked.patch new file mode 100644 index 000000000..c8d1258a0 --- /dev/null +++ b/wai/http-conformance/falsify/chain-parent-unchecked.patch @@ -0,0 +1,18 @@ +# must fail: live_edge_unchained live_edge_skip live_edge_other_key_walked +# a live revision's parent links not walked (playlist_receipt::LiveChain::advance) +diff --git a/wai/wai-rs/src/http_binding/playlist_receipt.rs b/wai/wai-rs/src/http_binding/playlist_receipt.rs +index 6fb6141d..db167d4c 100644 +--- a/wai/wai-rs/src/http_binding/playlist_receipt.rs ++++ b/wai/wai-rs/src/http_binding/playlist_receipt.rs +@@ -228,9 +228,9 @@ impl LiveChain { + let mut link = cur.parent_receipt_hash; + loop { + let l = match link { +- None => return Err(ReceiptError::Unchained), ++ None => break, + Some(l) if l == target => break, +- Some(l) => l, ++ Some(_) => break, + }; + if walked.len() == self.bound { + // §8.6: past the bound, a revision ahead of the head (more segments diff --git a/wai/http-conformance/falsify/chain-reload-as-revision.patch b/wai/http-conformance/falsify/chain-reload-as-revision.patch new file mode 100644 index 000000000..21ca5a3cd --- /dev/null +++ b/wai/http-conformance/falsify/chain-reload-as-revision.patch @@ -0,0 +1,16 @@ +# must fail: live_edge_unchanged_reload +# the head fetched again taken as a new revision (LiveChain::advance) +diff --git a/wai/wai-rs/src/http_binding/playlist_receipt.rs b/wai/wai-rs/src/http_binding/playlist_receipt.rs +index 6666c22a..7acdf1be 100644 +--- a/wai/wai-rs/src/http_binding/playlist_receipt.rs ++++ b/wai/wai-rs/src/http_binding/playlist_receipt.rs +@@ -204,9 +204,6 @@ impl LiveChain { + return Ok(Step::First); + }; + let target = head.receipt_hash(); +- if cur.receipt_hash() == target { +- return Ok(Step::Unchanged); +- } + if cur.signer_pubkey != head.signer_pubkey { + return Err(ReceiptError::Unchained); + } diff --git a/wai/http-conformance/falsify/chain-walk-unbounded.patch b/wai/http-conformance/falsify/chain-walk-unbounded.patch new file mode 100644 index 000000000..36410a759 --- /dev/null +++ b/wai/http-conformance/falsify/chain-walk-unbounded.patch @@ -0,0 +1,15 @@ +# must fail: live_edge_reanchored live_edge_reanchor_not_backwards +# the walk back to the head not bounded (LiveChain::advance) +diff --git a/wai/wai-rs/src/http_binding/playlist_receipt.rs b/wai/wai-rs/src/http_binding/playlist_receipt.rs +index 6fb6141d..b3b67ab0 100644 +--- a/wai/wai-rs/src/http_binding/playlist_receipt.rs ++++ b/wai/wai-rs/src/http_binding/playlist_receipt.rs +@@ -232,7 +232,7 @@ impl LiveChain { + Some(l) if l == target => break, + Some(l) => l, + }; +- if walked.len() == self.bound { ++ if false && walked.len() == self.bound { + // §8.6: past the bound, a revision ahead of the head (more segments + // complete) re-anchors the chain, its bindings kept; one that is not + // ahead is refused, so a stale or older revision never rolls it back. diff --git a/wai/http-conformance/falsify/dash-order-unchecked.patch b/wai/http-conformance/falsify/dash-order-unchecked.patch new file mode 100644 index 000000000..e2f50cee2 --- /dev/null +++ b/wai/http-conformance/falsify/dash-order-unchecked.patch @@ -0,0 +1,14 @@ +# must fail: dash_adjacency_out_of_order +# the classical cursor reset for each WAI run: segments out of order accepted (dash_read::within) +diff --git a/wai/wai-rs/src/http_binding/dash_read.rs b/wai/wai-rs/src/http_binding/dash_read.rs +index 71e36cde..7a416543 100644 +--- a/wai/wai-rs/src/http_binding/dash_read.rs ++++ b/wai/wai-rs/src/http_binding/dash_read.rs +@@ -477,6 +477,7 @@ fn within(wai: &[Run], classical: &[Run]) -> bool { + // The classical cursor: a run, and the segments of it already passed. + let (mut ci, mut co) = (0usize, 0u128); + for w in wai { ++ (ci, co) = (0, 0); + let (mut t, mut left) = (u128::from(w.t), u128::from(w.count)); + while left > 0 { + // The first classical segment at or after the cursor whose time is `t`. diff --git a/wai/http-conformance/falsify/dash-r-ignored.patch b/wai/http-conformance/falsify/dash-r-ignored.patch new file mode 100644 index 000000000..be2c4a41e --- /dev/null +++ b/wai/http-conformance/falsify/dash-r-ignored.patch @@ -0,0 +1,15 @@ +# must fail: dash_adjacency_o9 mpd_playlists_o9_wai_mpd +# @r read as no repeat (dash_read::timeline_of) +diff --git a/wai/wai-rs/src/http_binding/dash_read.rs b/wai/wai-rs/src/http_binding/dash_read.rs +index 71e36cde..6611250e 100644 +--- a/wai/wai-rs/src/http_binding/dash_read.rs ++++ b/wai/wai-rs/src/http_binding/dash_read.rs +@@ -412,7 +412,7 @@ fn timeline_of(e: &Element, inherited: &[&Element]) -> Result, + if end > u128::from(u64::MAX) { + return Err(malformed("a segment time past 2^64 - 1")); + } +- runs.push(Run { t, d, count: r + 1 }); ++ runs.push(Run { t, d, count: 1 }); + next = end as u64; + } + Ok(Some(Timeline { timescale, runs })) diff --git a/wai/http-conformance/falsify/dash-run-end-unchecked.patch b/wai/http-conformance/falsify/dash-run-end-unchecked.patch new file mode 100644 index 000000000..457f8fcfb --- /dev/null +++ b/wai/http-conformance/falsify/dash-run-end-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: dash_adjacency_r_end_past_u64 +# a run whose end passes 2^64 - 1 read (dash_read::timeline_of) +diff --git a/wai/wai-rs/src/http_binding/dash_read.rs b/wai/wai-rs/src/http_binding/dash_read.rs +index 71e36cde..5b55d5a1 100644 +--- a/wai/wai-rs/src/http_binding/dash_read.rs ++++ b/wai/wai-rs/src/http_binding/dash_read.rs +@@ -409,7 +409,7 @@ fn timeline_of(e: &Element, inherited: &[&Element]) -> Result, + let r = s.attr("r").map(|v| unsigned(v, "@r")).transpose()?.unwrap_or(0); + // The run's end, t + d·(r + 1), at most 2^64 − 1: computed, never expanded. + let end = u128::from(t) + u128::from(d) * (u128::from(r) + 1); +- if end > u128::from(u64::MAX) { ++ if false && end > u128::from(u64::MAX) { + return Err(malformed("a segment time past 2^64 - 1")); + } + runs.push(Run { t, d, count: r + 1 }); diff --git a/wai/http-conformance/falsify/dash-start-unchecked.patch b/wai/http-conformance/falsify/dash-start-unchecked.patch new file mode 100644 index 000000000..1c0f951a0 --- /dev/null +++ b/wai/http-conformance/falsify/dash-start-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: dash_adjacency_period_start +# a Period@start not compared (dash_read::check_dash_adjacent) +diff --git a/wai/wai-rs/src/http_binding/dash_read.rs b/wai/wai-rs/src/http_binding/dash_read.rs +index 79d55df6..dd967c6c 100644 +--- a/wai/wai-rs/src/http_binding/dash_read.rs ++++ b/wai/wai-rs/src/http_binding/dash_read.rs +@@ -450,7 +450,7 @@ pub fn check_dash_adjacent(wai: &str, classical: &str) -> Result<(), PlaylistErr + let k = read_mpd(classical)?; + for wp in &w { + let kp = k.iter().find(|p| p.id == wp.id).ok_or_else(|| misaligned(format!("no classical Period with the @id {:?}", wp.id)))?; +- if kp.start_ms != wp.start_ms { ++ if false { + return Err(misaligned(format!("Period {:?} starts at {} ms, the classical one at {} ms", wp.id, wp.start_ms, kp.start_ms))); + } + if wp.sets.is_empty() { diff --git a/wai/http-conformance/falsify/dash-timelines-unbounded.patch b/wai/http-conformance/falsify/dash-timelines-unbounded.patch new file mode 100644 index 000000000..14c68ca6f --- /dev/null +++ b/wai/http-conformance/falsify/dash-timelines-unbounded.patch @@ -0,0 +1,15 @@ +# must fail: dash_adjacency_timelines_past_the_subset +# a document of more than 256 timelines read (dash_read::read_mpd) +diff --git a/wai/wai-rs/src/http_binding/dash_read.rs b/wai/wai-rs/src/http_binding/dash_read.rs +index 71e36cde..d08ed0fd 100644 +--- a/wai/wai-rs/src/http_binding/dash_read.rs ++++ b/wai/wai-rs/src/http_binding/dash_read.rs +@@ -435,7 +435,7 @@ pub fn read_mpd(text: &str) -> Result, PlaylistError> { + let mut held = 0usize; + let mut count = |timelines: &Vec| -> Result<(), PlaylistError> { + held += timelines.len(); +- if held > MAX_TIMELINES { Err(malformed("more SegmentTimelines than the subset's 256")) } else { Ok(()) } ++ if false && held > MAX_TIMELINES { Err(malformed("more SegmentTimelines than the subset's 256")) } else { Ok(()) } + }; + for (i, p) in root.children_named("Period").enumerate() { + let start_ms = match p.attr("start") { diff --git a/wai/http-conformance/falsify/dash-timescale-unchecked.patch b/wai/http-conformance/falsify/dash-timescale-unchecked.patch new file mode 100644 index 000000000..5efb3c3b3 --- /dev/null +++ b/wai/http-conformance/falsify/dash-timescale-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: dash_adjacency_timescale dash_adjacency_the_audio_timescale +# a timescale not compared (dash_read::check_dash_adjacent) +diff --git a/wai/wai-rs/src/http_binding/dash_read.rs b/wai/wai-rs/src/http_binding/dash_read.rs +index 71e36cde..6e7a5b83 100644 +--- a/wai/wai-rs/src/http_binding/dash_read.rs ++++ b/wai/wai-rs/src/http_binding/dash_read.rs +@@ -526,7 +526,7 @@ pub fn check_dash_adjacent(wai: &str, classical: &str) -> Result<(), PlaylistErr + } + for tl in &s.timelines { + let aligned = tl.segment_count() > 0 +- && kp.sets.iter().flat_map(|c| &c.timelines).any(|c| c.timescale == tl.timescale && within(&tl.runs, &c.runs)); ++ && kp.sets.iter().flat_map(|c| &c.timelines).any(|c| within(&tl.runs, &c.runs)); + if !aligned { + return Err(misaligned(format!("a timeline of AdaptationSet {:?} is not a classical one's", s.id))); + } diff --git a/wai/http-conformance/falsify/dash-zero-d.patch b/wai/http-conformance/falsify/dash-zero-d.patch new file mode 100644 index 000000000..048fce9d4 --- /dev/null +++ b/wai/http-conformance/falsify/dash-zero-d.patch @@ -0,0 +1,15 @@ +# must fail: dash_adjacency_zero_d +# an S of no duration read (dash_read::timeline_of) +diff --git a/wai/wai-rs/src/http_binding/dash_read.rs b/wai/wai-rs/src/http_binding/dash_read.rs +index 71e36cde..f4aec871 100644 +--- a/wai/wai-rs/src/http_binding/dash_read.rs ++++ b/wai/wai-rs/src/http_binding/dash_read.rs +@@ -402,7 +402,7 @@ fn timeline_of(e: &Element, inherited: &[&Element]) -> Result, + let mut next = 0u64; + for s in st.children_named("S") { + let d = unsigned(s.attr("d").ok_or_else(|| malformed("an S has no @d"))?, "@d")?; +- if d == 0 { ++ if false { + return Err(malformed("an S of no duration")); + } + let t = s.attr("t").map(|v| unsigned(v, "@t")).transpose()?.unwrap_or(next); diff --git a/wai/http-conformance/falsify/decimal-truncates.patch b/wai/http-conformance/falsify/decimal-truncates.patch new file mode 100644 index 000000000..e158be949 --- /dev/null +++ b/wai/http-conformance/falsify/decimal-truncates.patch @@ -0,0 +1,15 @@ +# must fail: playlist_parse_rounding +# durations truncated to the millisecond, not rounded (hls::decimal_ms) +diff --git a/wai/wai-rs/src/http_binding/hls.rs b/wai/wai-rs/src/http_binding/hls.rs +index 2623644b..552dbde8 100644 +--- a/wai/wai-rs/src/http_binding/hls.rs ++++ b/wai/wai-rs/src/http_binding/hls.rs +@@ -92,7 +92,7 @@ pub fn decimal_ms(s: &str) -> Option { + let digit = |i: usize| f.as_bytes().get(i).map_or(0, |b| u64::from(b - b'0')); + let whole: u64 = int.parse().ok()?; + let ms = whole.checked_mul(1000)?.checked_add(digit(0) * 100 + digit(1) * 10 + digit(2))?; +- if digit(3) >= 5 { ms.checked_add(1) } else { Some(ms) } ++ if digit(3) >= 10 { ms.checked_add(1) } else { Some(ms) } + } + + fn decimal_int(s: &str) -> Option { diff --git a/wai/http-conformance/falsify/depth-per-layer.patch b/wai/http-conformance/falsify/depth-per-layer.patch new file mode 100644 index 000000000..2673909c1 --- /dev/null +++ b/wai/http-conformance/falsify/depth-per-layer.patch @@ -0,0 +1,15 @@ +# must fail: depth_O10_throughput_between depth_O11b_bola_like_0 +# a depth's bytes taken as its own layer's, not the sum through it (abr::depth) +diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs +index 6fb8973c..c6472097 100644 +--- a/wai/wai-rs/src/http_binding/abr.rs ++++ b/wai/wai-rs/src/http_binding/abr.rs +@@ -432,7 +432,7 @@ pub fn depth(layers_n: &[u64], p: &dyn Policy, s: &AbrState) -> Result<(usize, B + let mut acc = 0u64; + st.option_bytes = Vec::with_capacity(layers_n.len()); + for &n in layers_n { +- acc = acc.checked_add(n).ok_or(AbrError("the bytes through a depth pass 2^64 - 1"))?; ++ acc = n; + st.option_bytes.push(acc); + } + st.option_joules_micro = Vec::with_capacity(layers_n.len()); diff --git a/wai/http-conformance/falsify/depth-sum-saturates.patch b/wai/http-conformance/falsify/depth-sum-saturates.patch new file mode 100644 index 000000000..4c59cde59 --- /dev/null +++ b/wai/http-conformance/falsify/depth-sum-saturates.patch @@ -0,0 +1,15 @@ +# must fail: depth_out_of_bounds_bytes +# the bytes through a depth saturate (abr::depth) +diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs +index 6fb8973c..42b3495d 100644 +--- a/wai/wai-rs/src/http_binding/abr.rs ++++ b/wai/wai-rs/src/http_binding/abr.rs +@@ -432,7 +432,7 @@ pub fn depth(layers_n: &[u64], p: &dyn Policy, s: &AbrState) -> Result<(usize, B + let mut acc = 0u64; + st.option_bytes = Vec::with_capacity(layers_n.len()); + for &n in layers_n { +- acc = acc.checked_add(n).ok_or(AbrError("the bytes through a depth pass 2^64 - 1"))?; ++ acc = acc.saturating_add(n); + st.option_bytes.push(acc); + } + st.option_joules_micro = Vec::with_capacity(layers_n.len()); diff --git a/wai/http-conformance/falsify/gap-carries.patch b/wai/http-conformance/falsify/gap-carries.patch new file mode 100644 index 000000000..b6767dde8 --- /dev/null +++ b/wai/http-conformance/falsify/gap-carries.patch @@ -0,0 +1,15 @@ +# must fail: render_gap_with_parts render_gap_with_layers media_playlist_gap_with_layers +# a gap segment with parts or layer lines rendered (hls_render::check_render) +diff --git a/wai/wai-rs/src/hls_render.rs b/wai/wai-rs/src/hls_render.rs +index ba3f11d4..ba588609 100644 +--- a/wai/wai-rs/src/hls_render.rs ++++ b/wai/wai-rs/src/hls_render.rs +@@ -359,7 +359,7 @@ pub fn check_render(segments: &[Segment], r: &HlsRender, extras: &[SegmentExtra] + check_layers(&p.layers, &at)?; + } + check_layers(&e.layers, &at)?; +- if i < n && e.gap && (!e.parts.is_empty() || !e.layers.is_empty()) { ++ if false && i < n && e.gap && (!e.parts.is_empty() || !e.layers.is_empty()) { + return Err(RenderError(format!("{at}: a gap carries parts or layer lines"))); + } + if let Some(h) = &e.preload_hint { diff --git a/wai/http-conformance/falsify/gap-not-rendered.patch b/wai/http-conformance/falsify/gap-not-rendered.patch new file mode 100644 index 000000000..e1c789cf4 --- /dev/null +++ b/wai/http-conformance/falsify/gap-not-rendered.patch @@ -0,0 +1,16 @@ +# must fail: media_playlist_playlists_o9_wai_180_gap_m3u8 +# a gap segment rendered as a segment (hls_render::render_hls_with) +diff --git a/wai/wai-rs/src/hls_render.rs b/wai/wai-rs/src/hls_render.rs +index 7e29b8dd..52cd436d 100644 +--- a/wai/wai-rs/src/hls_render.rs ++++ b/wai/wai-rs/src/hls_render.rs +@@ -259,9 +259,6 @@ pub fn render_hls_with(segments: &[Segment], r: &HlsRender, extras: &[SegmentExt + out.push_str(&format!("#EXT-X-PROGRAM-DATE-TIME:{pdt}\n")); + } + part_lines(&mut out, &e.parts); +- if e.gap { +- out.push_str("#EXT-X-GAP\n"); +- } + // Fixed 3-dp seconds so the byte output is stable across platforms. + out.push_str(&format!("#EXTINF:{},\n", seconds(s.duration_ms))); + layer_lines(&mut out, &e.layers); diff --git a/wai/http-conformance/falsify/gap-withdraws.patch b/wai/http-conformance/falsify/gap-withdraws.patch new file mode 100644 index 000000000..bbe7a0e5f --- /dev/null +++ b/wai/http-conformance/falsify/gap-withdraws.patch @@ -0,0 +1,15 @@ +# must fail: live_gap_withdraws_a_segment +# a gap at a sequence number bound to an object accepted (hls::Bindings::bind) +diff --git a/wai/wai-rs/src/http_binding/hls.rs b/wai/wai-rs/src/http_binding/hls.rs +index 1852df35..e38575cd 100644 +--- a/wai/wai-rs/src/http_binding/hls.rs ++++ b/wai/wai-rs/src/http_binding/hls.rs +@@ -982,7 +982,7 @@ impl Bindings { + for (i, s) in p.segments.iter().enumerate() { + let n = p.media_sequence.checked_add(i as u64).ok_or_else(|| malformed("the media sequence number passes 2^64"))?; + if s.extra.gap { +- if let Some((held, _)) = next.sequences.get(&n) { ++ if let Some((held, _)) = next.sequences.get(&n).filter(|_| false) { + return Err(PlaylistError::Rebound(format!("media sequence {n} was listed with {}, and is now a gap", hex(held)))); + } + continue; diff --git a/wai/http-conformance/falsify/grant-ceiling-written-out.patch b/wai/http-conformance/falsify/grant-ceiling-written-out.patch new file mode 100644 index 000000000..5d78400c1 --- /dev/null +++ b/wai/http-conformance/falsify/grant-ceiling-written-out.patch @@ -0,0 +1,15 @@ +# must fail: regeneration +# no joule ceiling written as 2^64 - 1 (wai_video_manifest::grant_json) +diff --git a/wai/wai-rs/src/wai_video_manifest.rs b/wai/wai-rs/src/wai_video_manifest.rs +index a7574e49..41275c34 100644 +--- a/wai/wai-rs/src/wai_video_manifest.rs ++++ b/wai/wai-rs/src/wai_video_manifest.rs +@@ -199,7 +199,7 @@ fn numbers_within(v: &serde_json::Value) -> bool { + pub fn grant_json(g: &GrantRef) -> serde_json::Value { + serde_json::json!({ + "grant_hash": hex(&g.grant_hash), "capability": g.capability, +- "joule_ceiling_micro": (g.joule_ceiling_micro != u64::MAX).then_some(g.joule_ceiling_micro), "funds_ceiling": g.funds_ceiling, ++ "joule_ceiling_micro": g.joule_ceiling_micro, "funds_ceiling": g.funds_ceiling, + }) + } + diff --git a/wai/http-conformance/falsify/ilog2-no-interp.patch b/wai/http-conformance/falsify/ilog2-no-interp.patch new file mode 100644 index 000000000..fad715202 --- /dev/null +++ b/wai/http-conformance/falsify/ilog2-no-interp.patch @@ -0,0 +1,15 @@ +# must fail: ilog2_1001_1000 ilog2_7_3 +# the integer log without its interpolation (abr::ilog2_q16) +diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs +index a8046b34..7fb9c495 100644 +--- a/wai/wai-rs/src/http_binding/abr.rs ++++ b/wai/wai-rs/src/http_binding/abr.rs +@@ -145,7 +145,7 @@ pub fn ilog2_q16(a: u64, b: u64) -> u64 { + let e = (127 - x.leading_zeros()) as u64 - 16; + let f = ((x >> e) - 65536) as u64; + let (i, r) = ((f >> 12) as usize, f & 4095); +- e * 65536 + LOG2_TABLE[i] + (((LOG2_TABLE[i + 1] - LOG2_TABLE[i]) * r) >> 12) ++ e * 65536 + LOG2_TABLE[i] + (r & 0) + } + + /// The throughput the last `n` samples give, in bits per second: `⌊8000·Σbytes / Σms⌋`, diff --git a/wai/http-conformance/falsify/layer-after-uri.patch b/wai/http-conformance/falsify/layer-after-uri.patch new file mode 100644 index 000000000..7a4d919e4 --- /dev/null +++ b/wai/http-conformance/falsify/layer-after-uri.patch @@ -0,0 +1,17 @@ +# must fail: media_playlist_playlists_o10_wai_a_m3u8 media_playlist_playlists_o11_wai_v_m3u8 +# layer lines rendered after the segment's URI (hls_render::render_hls_with) +diff --git a/wai/wai-rs/src/hls_render.rs b/wai/wai-rs/src/hls_render.rs +index 7e29b8dd..3c910dc0 100644 +--- a/wai/wai-rs/src/hls_render.rs ++++ b/wai/wai-rs/src/hls_render.rs +@@ -264,9 +264,9 @@ pub fn render_hls_with(segments: &[Segment], r: &HlsRender, extras: &[SegmentExt + } + // Fixed 3-dp seconds so the byte output is stable across platforms. + out.push_str(&format!("#EXTINF:{},\n", seconds(s.duration_ms))); +- layer_lines(&mut out, &e.layers); + out.push_str(&s.uri); + out.push('\n'); ++ layer_lines(&mut out, &e.layers); + } + if let Some(open) = extras.get(segments.len()) { + if open.discontinuity { diff --git a/wai/http-conformance/falsify/layers-not-counted.patch b/wai/http-conformance/falsify/layers-not-counted.patch new file mode 100644 index 000000000..f89667c6b --- /dev/null +++ b/wai/http-conformance/falsify/layers-not-counted.patch @@ -0,0 +1,16 @@ +# must fail: multivariant_playlists_o10_wai_m3u8 +# a variant's rates without the layer objects listed with its segments (the corpus tool's listed_objects) +diff --git a/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs b/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs +index 29d4d12c..bebf0a80 100644 +--- a/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs ++++ b/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs +@@ -315,9 +315,6 @@ fn listed_objects(c: &Corpus, p: &MediaPlaylist) -> Result<(Vec<(u64, u64)>, Vec + let b = resolve(&s.uri)?; + let mut n = b.len() as u64; + all.push(b); +- for l in &s.extra.layers { +- n += resolve(&l.uri)?.len() as u64; +- } + segs.push((n, s.duration_ms)); + extra(&s.extra, &mut parts, &mut all)?; + } diff --git a/wai/http-conformance/falsify/listed-open-parts.patch b/wai/http-conformance/falsify/listed-open-parts.patch new file mode 100644 index 000000000..e7fe6b64b --- /dev/null +++ b/wai/http-conformance/falsify/listed-open-parts.patch @@ -0,0 +1,16 @@ +# must fail: live_edge +# the open segment's parts not read for their cids (hls::listed_cid) +diff --git a/wai/wai-rs/src/http_binding/hls.rs b/wai/wai-rs/src/http_binding/hls.rs +index 2623644b..fc201603 100644 +--- a/wai/wai-rs/src/http_binding/hls.rs ++++ b/wai/wai-rs/src/http_binding/hls.rs +@@ -741,9 +741,6 @@ pub fn listed_cid(p: &MediaPlaylist, uri: &str) -> Option<[u8; 32]> { + lines.extend(s.extra.layers.iter().map(|l| l.uri.as_str())); + lines.push(&s.uri); + } +- if let Some(o) = &p.open { +- parts(o, &mut lines); +- } + let line = lines.into_iter().find(|l| super::parse_fragment(l).0 == uri)?; + let (base, frag) = super::parse_fragment(line); + frag.cid.or_else(|| { diff --git a/wai/http-conformance/falsify/mpc-no-stall.patch b/wai/http-conformance/falsify/mpc-no-stall.patch new file mode 100644 index 000000000..b5b6883a0 --- /dev/null +++ b/wai/http-conformance/falsify/mpc-no-stall.patch @@ -0,0 +1,15 @@ +# must fail: abr_mpc_04 abr_order_O3_mpc_0 depth_O10_mpc_0 +# the look-ahead does not count stalls (abr::Mpc::choose) +diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs +index 6fb8973c..852404d4 100644 +--- a/wai/wai-rs/src/http_binding/abr.rs ++++ b/wai/wai-rs/src/http_binding/abr.rs +@@ -364,7 +364,7 @@ impl Policy for Mpc { + } + prev = Some(uo); + } +- qoe -= rebuf as i128 * (u_top + 65536); ++ qoe -= rebuf as i128 * 0; + if best.is_none_or(|(q, _)| qoe > q) { + best = Some((qoe, seq[0])); + } diff --git a/wai/http-conformance/falsify/mpd-char-unchecked.patch b/wai/http-conformance/falsify/mpd-char-unchecked.patch new file mode 100644 index 000000000..e05d9572e --- /dev/null +++ b/wai/http-conformance/falsify/mpd-char-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: mpd_rep_id_not_a_char +# a value outside XML written (dash::esc) +diff --git a/wai/wai-rs/src/http_binding/dash.rs b/wai/wai-rs/src/http_binding/dash.rs +index 7dd63c36..1ee1165b 100644 +--- a/wai/wai-rs/src/http_binding/dash.rs ++++ b/wai/wai-rs/src/http_binding/dash.rs +@@ -158,7 +158,7 @@ fn esc(s: &str) -> Result { + '\t' => out.push_str(" "), + '\n' => out.push_str(" "), + '\r' => out.push_str(" "), +- c if xml_char(c) => out.push(c), ++ c if true || xml_char(c) => out.push(c), + c => return Err(PlaylistError::Malformed(format!("U+{:04X} is not an XML character", c as u32))), + } + } diff --git a/wai/http-conformance/falsify/mpd-gap-listed.patch b/wai/http-conformance/falsify/mpd-gap-listed.patch new file mode 100644 index 000000000..711e6e0e8 --- /dev/null +++ b/wai/http-conformance/falsify/mpd-gap-listed.patch @@ -0,0 +1,15 @@ +# must fail: mpd_playlists_o9_wai_gap_mpd +# a gap segment listed in wai.mpd (the corpus tool's mpd rendering) +diff --git a/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs b/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs +index be66d9a2..027a96e5 100644 +--- a/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs ++++ b/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs +@@ -365,7 +365,7 @@ fn render_mpd(c: &Corpus, input: &Value) -> Result { + DashTimeline::from_hls(&classical, input["period_id"].as_str().unwrap_or("p0"), 0).map_err(|e| pl_err(&e))? + } + }; +- let media = p.segments.iter().map(|s| (!s.extra.gap).then(|| s.uri.clone())).collect(); ++ let media = p.segments.iter().map(|s| Some(s.uri.clone())).collect(); + let rep = WaiRepresentation { id: input["rep_id"].as_str().ok_or("rep_id")?.into(), bandwidth: peak, media }; + wai_mpd(&t, input["set_id"].as_u64().unwrap_or(1) as u32, &union, &floor, &[rep]).map_err(|e| pl_err(&e)) + } diff --git a/wai/http-conformance/falsify/order-by-table.patch b/wai/http-conformance/falsify/order-by-table.patch new file mode 100644 index 000000000..a7d461a41 --- /dev/null +++ b/wai/http-conformance/falsify/order-by-table.patch @@ -0,0 +1,15 @@ +# must fail: abr_order_O7_throughput_0 +# a capability's entries left in table order, not ordered by size (abr::order_manifest) +diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs +index a8046b34..8c2cc113 100644 +--- a/wai/wai-rs/src/http_binding/abr.rs ++++ b/wai/wai-rs/src/http_binding/abr.rs +@@ -389,7 +389,7 @@ pub fn order_manifest(head: &Head, m: &ManifestV2, eligible: &dyn Fn(&str) -> bo + } + let mut order = Vec::with_capacity(e.len()); + for (_, mut g) in groups { +- g.sort_by_key(|&i| (len(i), i)); ++ g.sort_by_key(|&i| i); + let mut st = s.clone(); + st.option_bytes = g.iter().map(|&i| len(i)).collect(); + st.option_joules_micro = g.iter().map(|&i| s.option_joules_micro.get(i).copied().flatten()).collect(); diff --git a/wai/http-conformance/falsify/reanchor-backwards.patch b/wai/http-conformance/falsify/reanchor-backwards.patch new file mode 100644 index 000000000..91f3bf32f --- /dev/null +++ b/wai/http-conformance/falsify/reanchor-backwards.patch @@ -0,0 +1,15 @@ +# must fail: live_edge_reanchor_not_backwards +# past the bound, any revision re-anchors, an older one too (LiveChain::advance) +diff --git a/wai/wai-rs/src/http_binding/playlist_receipt.rs b/wai/wai-rs/src/http_binding/playlist_receipt.rs +index 6fb6141d..f71d65eb 100644 +--- a/wai/wai-rs/src/http_binding/playlist_receipt.rs ++++ b/wai/wai-rs/src/http_binding/playlist_receipt.rs +@@ -236,7 +236,7 @@ impl LiveChain { + // §8.6: past the bound, a revision ahead of the head (more segments + // complete) re-anchors the chain, its bindings kept; one that is not + // ahead is refused, so a stale or older revision never rolls it back. +- if last_segment(cur) <= last_segment(head) { ++ if false && last_segment(cur) <= last_segment(head) { + return Err(ReceiptError::WalkExceeded); + } + let mut b = self.bindings.clone(); diff --git a/wai/http-conformance/falsify/rebind-sequence.patch b/wai/http-conformance/falsify/rebind-sequence.patch new file mode 100644 index 000000000..0b1dc3448 --- /dev/null +++ b/wai/http-conformance/falsify/rebind-sequence.patch @@ -0,0 +1,15 @@ +# must fail: live_resequenced +# a media sequence number listed again with another object accepted (hls::Bindings::bind) +diff --git a/wai/wai-rs/src/http_binding/hls.rs b/wai/wai-rs/src/http_binding/hls.rs +index 1852df35..a6e2e383 100644 +--- a/wai/wai-rs/src/http_binding/hls.rs ++++ b/wai/wai-rs/src/http_binding/hls.rs +@@ -989,7 +989,7 @@ impl Bindings { + } + let Some((_, cid)) = uri_cid(&s.uri) else { continue }; + match next.sequences.get(&n) { +- Some((held, _)) if *held != cid => { ++ Some((held, _)) if false && *held != cid => { + return Err(PlaylistError::Rebound(format!("media sequence {n} was listed with {}, and is now listed with {}", hex(held), hex(&cid)))); + } + _ => { diff --git a/wai/http-conformance/falsify/rebind-spelling.patch b/wai/http-conformance/falsify/rebind-spelling.patch new file mode 100644 index 000000000..c7edcc06c --- /dev/null +++ b/wai/http-conformance/falsify/rebind-spelling.patch @@ -0,0 +1,15 @@ +# must fail: live_relisted_dot_alias +# bindings keyed by a line's spelling, not its resolved URL (hls::Bindings::bind) +diff --git a/wai/wai-rs/src/http_binding/hls.rs b/wai/wai-rs/src/http_binding/hls.rs +index 1852df35..3f2a43aa 100644 +--- a/wai/wai-rs/src/http_binding/hls.rs ++++ b/wai/wai-rs/src/http_binding/hls.rs +@@ -951,7 +951,7 @@ impl Bindings { + let base = self.base.clone(); + let mut uri = |line: &str| -> Result<(), PlaylistError> { + let Some((_, cid)) = uri_cid(line) else { return Ok(()) }; +- let key = super::uri::resolve(&base, line); ++ let key = super::parse_fragment(line).0; + match next.uris.get(&key) { + Some((held, _)) if *held != cid => Err(PlaylistError::Rebound(format!("{key} was listed with {}, and is now listed with {}", hex(held), hex(&cid)))), + _ => { diff --git a/wai/http-conformance/falsify/rebind-uri.patch b/wai/http-conformance/falsify/rebind-uri.patch new file mode 100644 index 000000000..9d795519c --- /dev/null +++ b/wai/http-conformance/falsify/rebind-uri.patch @@ -0,0 +1,15 @@ +# must fail: live_relisted_alias live_relisted_alias_walked +# a URL listed again with another cid accepted (hls::Bindings::bind) +diff --git a/wai/wai-rs/src/http_binding/hls.rs b/wai/wai-rs/src/http_binding/hls.rs +index 1852df35..867bafe5 100644 +--- a/wai/wai-rs/src/http_binding/hls.rs ++++ b/wai/wai-rs/src/http_binding/hls.rs +@@ -953,7 +953,7 @@ impl Bindings { + let Some((_, cid)) = uri_cid(line) else { return Ok(()) }; + let key = super::uri::resolve(&base, line); + match next.uris.get(&key) { +- Some((held, _)) if *held != cid => Err(PlaylistError::Rebound(format!("{key} was listed with {}, and is now listed with {}", hex(held), hex(&cid)))), ++ Some((held, _)) if false && *held != cid => Err(PlaylistError::Rebound(format!("{key} was listed with {}, and is now listed with {}", hex(held), hex(&cid)))), + _ => { + next.uris.insert(key, (cid, rev)); + Ok(()) diff --git a/wai/http-conformance/falsify/receipt-duplicate-names-raw.patch b/wai/http-conformance/falsify/receipt-duplicate-names-raw.patch new file mode 100644 index 000000000..27d334c07 --- /dev/null +++ b/wai/http-conformance/falsify/receipt-duplicate-names-raw.patch @@ -0,0 +1,15 @@ +# must fail: playlist_receipt_duplicate_escaped_name +# member names compared as written, not as read (ManifestReceipt::from_json) +diff --git a/wai/wai-rs/src/wai_video_manifest.rs b/wai/wai-rs/src/wai_video_manifest.rs +index b639ad0b..7658b047 100644 +--- a/wai/wai-rs/src/wai_video_manifest.rs ++++ b/wai/wai-rs/src/wai_video_manifest.rs +@@ -210,7 +210,7 @@ fn duplicate_member(text: &str) -> Option { + i += if b[i] == b'\\' { 2 } else { 1 }; + } + if let Some(Some((names, true))) = stack.last_mut() { +- let name: String = serde_json::from_str(&text[start..=i.min(b.len() - 1)]).ok()?; ++ let name: String = text[start..=i.min(b.len() - 1)].to_string(); + if !names.insert(name.clone()) { + return Some(name); + } diff --git a/wai/http-conformance/falsify/receipt-duplicates.patch b/wai/http-conformance/falsify/receipt-duplicates.patch new file mode 100644 index 000000000..fcae10b0e --- /dev/null +++ b/wai/http-conformance/falsify/receipt-duplicates.patch @@ -0,0 +1,15 @@ +# must fail: playlist_receipt_duplicate_first playlist_receipt_duplicate_last playlist_receipt_duplicate_in_class playlist_receipt_duplicate_escaped_name +# a receipt file with a member twice read (ManifestReceipt::from_json) +diff --git a/wai/wai-rs/src/wai_video_manifest.rs b/wai/wai-rs/src/wai_video_manifest.rs +index b639ad0b..368608bf 100644 +--- a/wai/wai-rs/src/wai_video_manifest.rs ++++ b/wai/wai-rs/src/wai_video_manifest.rs +@@ -579,7 +579,7 @@ impl ManifestReceipt { + // A member name twice in one object is refused, at any depth and however the + // names are escaped: a reader that keeps the first and one that keeps the last + // would read two receipts from one file (I-JSON, RFC 7493 §2.3). +- if let Some(name) = duplicate_member(text) { ++ if let Some(name) = duplicate_member(text).filter(|_| false) { + return Err(format!("the member {name:?} twice in one object")); + } + let o = v.as_object().ok_or("a receipt is a JSON object")?; diff --git a/wai/http-conformance/falsify/receipt-name-unchecked.patch b/wai/http-conformance/falsify/receipt-name-unchecked.patch new file mode 100644 index 000000000..89ad00d1f --- /dev/null +++ b/wai/http-conformance/falsify/receipt-name-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: playlist_receipt_name +# a receipt file accepted under another name (playlist_receipt::check_playlist) +diff --git a/wai/wai-rs/src/http_binding/playlist_receipt.rs b/wai/wai-rs/src/http_binding/playlist_receipt.rs +index a42d4379..5dedec6e 100644 +--- a/wai/wai-rs/src/http_binding/playlist_receipt.rs ++++ b/wai/wai-rs/src/http_binding/playlist_receipt.rs +@@ -101,7 +101,7 @@ pub fn seal_playlist( + pub fn check_playlist(playlist: &[u8], receipt: &[u8], name: Option<&str>) -> Result { + let text = std::str::from_utf8(receipt).map_err(|_| ReceiptError::Malformed("not UTF-8".into()))?; + let r = ManifestReceipt::from_json(text).map_err(ReceiptError::Malformed)?; +- if name.is_some_and(|n| n != r.receipt_file_name()) { ++ if name.is_some_and(|n| n.is_empty()) { + return Err(ReceiptError::NameMismatch); + } + if *blake3::hash(playlist).as_bytes() != r.manifest_hash || r.rendered().as_bytes() != playlist { diff --git a/wai/http-conformance/falsify/receipt-numbers-unbounded.patch b/wai/http-conformance/falsify/receipt-numbers-unbounded.patch new file mode 100644 index 000000000..30b0609f6 --- /dev/null +++ b/wai/http-conformance/falsify/receipt-numbers-unbounded.patch @@ -0,0 +1,24 @@ +# must fail: playlist_receipt_ceiling_past_2_53 playlist_receipt_ceiling_written_out playlist_receipt_window_past_2_53 +# numbers past 2^53 - 1 read (ManifestReceipt::from_json) +diff --git a/wai/wai-rs/src/wai_video_manifest.rs b/wai/wai-rs/src/wai_video_manifest.rs +index a7574e49..60e021ee 100644 +--- a/wai/wai-rs/src/wai_video_manifest.rs ++++ b/wai/wai-rs/src/wai_video_manifest.rs +@@ -186,7 +186,7 @@ pub const MAX_FILE_NUMBER: u64 = (1 << 53) - 1; + fn numbers_within(v: &serde_json::Value) -> bool { + use serde_json::Value; + match v { +- Value::Number(n) => n.as_u64().is_some_and(|x| x <= MAX_FILE_NUMBER), ++ Value::Number(n) => n.as_u64().is_some(), + Value::Array(a) => a.iter().all(numbers_within), + Value::Object(o) => o.values().all(numbers_within), + _ => true, +@@ -211,7 +211,7 @@ pub fn grant_from_json(v: &serde_json::Value) -> Result { + let ceiling = |k: &str| -> Result, String> { + match g.get(k) { + Some(Value::Null) => Ok(None), +- Some(n) => n.as_u64().filter(|&x| x <= MAX_FILE_NUMBER).map(Some).ok_or(format!("grant.{k} is a number of at most 2^53 - 1, or null")), ++ Some(n) => n.as_u64().map(Some).ok_or(format!("grant.{k} is a number of at most 2^53 - 1, or null")), + None => Err(format!("grant.{k}")), + } + }; diff --git a/wai/http-conformance/falsify/receipt-render-unsigned.patch b/wai/http-conformance/falsify/receipt-render-unsigned.patch new file mode 100644 index 000000000..456eb30e5 --- /dev/null +++ b/wai/http-conformance/falsify/receipt-render-unsigned.patch @@ -0,0 +1,18 @@ +# must fail: playlist_receipt_playlists_o9_wai_180_m3u8 live_edge +# the render digest left out of the signed payload (wai_video_manifest::ManifestReceipt::signing_payload) +diff --git a/wai/wai-rs/src/wai_video_manifest.rs b/wai/wai-rs/src/wai_video_manifest.rs +index fd25c628..fb9b1669 100644 +--- a/wai/wai-rs/src/wai_video_manifest.rs ++++ b/wai/wai-rs/src/wai_video_manifest.rs +@@ -273,10 +273,7 @@ impl ManifestReceipt { + // rendering has no canonical form). + if self.render.is_some() { + match self.render_digest() { +- Some(d) => { +- o.push(1); +- o.extend_from_slice(&d); +- } ++ Some(_) => {} + None => o.push(2), + } + } diff --git a/wai/http-conformance/falsify/render-parts-after-extinf.patch b/wai/http-conformance/falsify/render-parts-after-extinf.patch new file mode 100644 index 000000000..f0eaf9fdb --- /dev/null +++ b/wai/http-conformance/falsify/render-parts-after-extinf.patch @@ -0,0 +1,20 @@ +# must fail: media_playlist_live_wai_r1_m3u8 playlist_receipt_live_wai_r1_m3u8 +# a segment's parts rendered after its EXTINF, not before it (hls_render::render_hls_with) +diff --git a/wai/wai-rs/src/hls_render.rs b/wai/wai-rs/src/hls_render.rs +index 7e29b8dd..0d62afe4 100644 +--- a/wai/wai-rs/src/hls_render.rs ++++ b/wai/wai-rs/src/hls_render.rs +@@ -258,12 +258,12 @@ pub fn render_hls_with(segments: &[Segment], r: &HlsRender, extras: &[SegmentExt + if let Some(pdt) = &e.program_date_time { + out.push_str(&format!("#EXT-X-PROGRAM-DATE-TIME:{pdt}\n")); + } +- part_lines(&mut out, &e.parts); + if e.gap { + out.push_str("#EXT-X-GAP\n"); + } + // Fixed 3-dp seconds so the byte output is stable across platforms. + out.push_str(&format!("#EXTINF:{},\n", seconds(s.duration_ms))); ++ part_lines(&mut out, &e.parts); + layer_lines(&mut out, &e.layers); + out.push_str(&s.uri); + out.push('\n'); diff --git a/wai/http-conformance/falsify/run.sh b/wai/http-conformance/falsify/run.sh index 55437a71c..fd0b18100 100755 --- a/wai/http-conformance/falsify/run.sh +++ b/wai/http-conformance/falsify/run.sh @@ -8,7 +8,8 @@ # bash wai/http-conformance/falsify/run.sh [work-dir [patch-name…]] # # With patch names, only those patches run (and nothing else). -# Run from the repository root. CARGO_TARGET_DIR, if set, is shared by the builds. +# Run from the repository root. CARGO_TARGET_DIR, if set, is shared by the builds, which +# need the features http and video_manifest (the playlist receipts). set -uo pipefail ROOT="$(git rev-parse --show-toplevel)" WORK="${1:-${RUNNER_TEMP:-/tmp}/wai-http-falsify}" @@ -25,7 +26,7 @@ for patch in "$ROOT"/wai/http-conformance/falsify/*.patch; do must="$(head -1 "$patch" | sed 's/^# must fail: //')" if ! git -C "$wt" apply "$patch"; then echo "::error::$name: the patch does not apply"; fail=1 - elif ! (cd "$wt/wai/wai-rs" && cargo build --release --no-default-features --features http --bin wai_http_vectors) > "$WORK/$name.build.log" 2>&1; then + elif ! (cd "$wt/wai/wai-rs" && cargo build --release --no-default-features --features http,video_manifest --bin wai_http_vectors) > "$WORK/$name.build.log" 2>&1; then echo "::error::$name: the patched tree does not build"; tail -20 "$WORK/$name.build.log"; fail=1 else bin="${CARGO_TARGET_DIR:-$wt/wai/wai-rs/target}/release/wai_http_vectors" @@ -44,7 +45,15 @@ done [ -n "$ONLY" ] && exit $fail cd "$ROOT/wai/http-conformance" -for m in left-half no-root counter-zero floor-as-codecs blake3-iv base-at-done tick-noop refinement-unclipped; do +for m in left-half no-root counter-zero floor-as-codecs blake3-iv base-at-done tick-noop refinement-unclipped \ + pdt-ignored dseq-ignored parts-after-extinf average-floor render-digest-off name-unchecked chain-unchecked \ + ilog2-no-interp bola-waits unlabelled-as-zero round-half-down small-order-key small-order-r \ + rebind-uri rebind-sequence key-unchecked walk-unbounded reload-walked \ + xml-char-unchecked xml-attribute-twice dash-start-ignored dash-timescale-ignored \ + unlabelled-escapes abr-unbounded gap-carries receipt-duplicates receipt-surrogates session-data-anywhere \ + dash-r-expanded xml-attributes-scanned dash-timelines-unbounded budget-raises \ + uri-unresolved gap-withdraws reanchor-backwards walk-exceeded-wedges walk-name-unchecked walk-verify-unchecked bindings-unbounded \ + file-numbers-unbounded; do if python3 http_verify.py . --mutate "$m" > "$WORK/py-$m.log" 2>&1; then echo "::error::http_verify.py --mutate $m passed"; fail=1 else @@ -53,7 +62,7 @@ for m in left-half no-root counter-zero floor-as-codecs blake3-iv base-at-done t done # The differential runs the unpatched tool: build it again (the patched builds shared # its output path). -(cd "$ROOT/wai/wai-rs" && cargo build --release --no-default-features --features http --bin wai_http_vectors) > "$WORK/unpatched.build.log" 2>&1 || fail=1 +(cd "$ROOT/wai/wai-rs" && cargo build --release --no-default-features --features http,video_manifest --bin wai_http_vectors) > "$WORK/unpatched.build.log" 2>&1 || fail=1 bin="${CARGO_TARGET_DIR:-$ROOT/wai/wai-rs/target}/release/wai_http_vectors" if [ -x "$bin" ]; then "$bin" diff . --seed 7 --count 500 --out "$WORK/diff.jsonl" > /dev/null || fail=1 diff --git a/wai/http-conformance/falsify/session-data-more-lines.patch b/wai/http-conformance/falsify/session-data-more-lines.patch new file mode 100644 index 000000000..5597fc08f --- /dev/null +++ b/wai/http-conformance/falsify/session-data-more-lines.patch @@ -0,0 +1,15 @@ +# must fail: unchanged_two_lines unchanged_blank_line_too +# more than the one session-data line inserted (hls::classical_unchanged) +diff --git a/wai/wai-rs/src/http_binding/hls.rs b/wai/wai-rs/src/http_binding/hls.rs +index 107ae8a1..6a704c25 100644 +--- a/wai/wai-rs/src/http_binding/hls.rs ++++ b/wai/wai-rs/src/http_binding/hls.rs +@@ -847,7 +847,7 @@ pub fn classical_unchanged(original: &str, published: &str) -> Result<(), Playli + let (head, tail) = original.split_at(at); + let inserted = published.strip_prefix(head).and_then(|r| r.strip_suffix(tail)).ok_or_else(changed)?; + let bare = inserted.strip_suffix('\n').map(|x| x.strip_suffix('\r').unwrap_or(x)).ok_or_else(changed)?; +- if bare.contains('\n') || !is_session_data_line(bare) { ++ if !is_session_data_line(bare.lines().next().unwrap_or("")) { + return Err(changed()); + } + Ok(()) diff --git a/wai/http-conformance/falsify/session-data-position.patch b/wai/http-conformance/falsify/session-data-position.patch new file mode 100644 index 000000000..eec1654ae --- /dev/null +++ b/wai/http-conformance/falsify/session-data-position.patch @@ -0,0 +1,15 @@ +# must fail: unchanged_players unchanged_at_the_end +# the session-data line's place taken past every line (hls::session_data_position) +diff --git a/wai/wai-rs/src/http_binding/hls.rs b/wai/wai-rs/src/http_binding/hls.rs +index 107ae8a1..8f5f5bd9 100644 +--- a/wai/wai-rs/src/http_binding/hls.rs ++++ b/wai/wai-rs/src/http_binding/hls.rs +@@ -812,7 +812,7 @@ pub fn session_data_position(original: &str) -> Option { + let mut at = 0; + for (i, l) in original.split_inclusive('\n').enumerate() { + let bare = l.strip_suffix('\n').map(|x| x.strip_suffix('\r').unwrap_or(x)).unwrap_or(l); +- if i > 0 && !(bare.starts_with("#EXT-X-VERSION:") || bare == "#EXT-X-INDEPENDENT-SEGMENTS") { ++ if false && i > 0 && !(bare.starts_with("#EXT-X-VERSION:") || bare == "#EXT-X-INDEPENDENT-SEGMENTS") { + break; + } + if !l.ends_with('\n') { diff --git a/wai/http-conformance/falsify/sig-key-lenient.patch b/wai/http-conformance/falsify/sig-key-lenient.patch new file mode 100644 index 000000000..804c776ae --- /dev/null +++ b/wai/http-conformance/falsify/sig-key-lenient.patch @@ -0,0 +1,25 @@ +# must fail: playlist_receipt_sig_key_identity playlist_receipt_sig_key_identity_long playlist_receipt_sig_r_small_order +# keys read as the library reads them, and the equation checked leniently (wai::sig) +diff --git a/wai/wai-rs/src/sig.rs b/wai/wai-rs/src/sig.rs +index 46135933..01f85c39 100644 +--- a/wai/wai-rs/src/sig.rs ++++ b/wai/wai-rs/src/sig.rs +@@ -62,9 +62,6 @@ fn below_l(s: &[u8; 32]) -> bool { + /// large order. + pub fn strict_key(bytes: &[u8; 32]) -> Result { + let key = VerifyingKey::from_bytes(bytes).map_err(|_| SigError::BadKey)?; +- if key.to_edwards().compress().to_bytes() != *bytes || key.is_weak() { +- return Err(SigError::BadKey); +- } + Ok(key) + } + +@@ -75,7 +72,7 @@ pub fn verify_strict(pubkey: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> Result<() + if !below_l(s) { + return Err(SigError::BadSignature); + } +- key.verify_strict(msg, &Signature::from_bytes(sig)).map_err(|_| SigError::BadSignature) ++ ed25519_dalek::Verifier::verify(&key, msg, &Signature::from_bytes(sig)).map_err(|_| SigError::BadSignature) + } + + #[cfg(test)] diff --git a/wai/http-conformance/falsify/sig-verify-lenient.patch b/wai/http-conformance/falsify/sig-verify-lenient.patch new file mode 100644 index 000000000..2450c0fb0 --- /dev/null +++ b/wai/http-conformance/falsify/sig-verify-lenient.patch @@ -0,0 +1,15 @@ +# must fail: playlist_receipt_sig_r_small_order +# R of small order accepted (wai::sig::verify_strict checks the equation leniently) +diff --git a/wai/wai-rs/src/sig.rs b/wai/wai-rs/src/sig.rs +index 46135933..ebe58160 100644 +--- a/wai/wai-rs/src/sig.rs ++++ b/wai/wai-rs/src/sig.rs +@@ -75,7 +75,7 @@ pub fn verify_strict(pubkey: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> Result<() + if !below_l(s) { + return Err(SigError::BadSignature); + } +- key.verify_strict(msg, &Signature::from_bytes(sig)).map_err(|_| SigError::BadSignature) ++ ed25519_dalek::Verifier::verify(&key, msg, &Signature::from_bytes(sig)).map_err(|_| SigError::BadSignature) + } + + #[cfg(test)] diff --git a/wai/http-conformance/falsify/skip-read.patch b/wai/http-conformance/falsify/skip-read.patch new file mode 100644 index 000000000..e18a46d8a --- /dev/null +++ b/wai/http-conformance/falsify/skip-read.patch @@ -0,0 +1,14 @@ +# must fail: playlist_parse_skip +# a delta update read as a whole playlist (hls::parse_media_playlist) +diff --git a/wai/wai-rs/src/http_binding/hls.rs b/wai/wai-rs/src/http_binding/hls.rs +index 2623644b..87e34cce 100644 +--- a/wai/wai-rs/src/http_binding/hls.rs ++++ b/wai/wai-rs/src/http_binding/hls.rs +@@ -322,7 +322,6 @@ pub fn parse_media_playlist(text: &str) -> Result + } + } + "#EXT-X-ENDLIST" => p.end_list = true, +- "#EXT-X-SKIP" => return Err(malformed("a delta update (EXT-X-SKIP) is not compared")), + "#EXT-X-STREAM-INF" | "#EXT-X-I-FRAME-STREAM-INF" | "#EXT-X-MEDIA" => { + return Err(malformed(format!("{tag}: a multivariant playlist, not a media playlist"))) + } diff --git a/wai/http-conformance/falsify/uri-dots-kept.patch b/wai/http-conformance/falsify/uri-dots-kept.patch new file mode 100644 index 000000000..41160fcf3 --- /dev/null +++ b/wai/http-conformance/falsify/uri-dots-kept.patch @@ -0,0 +1,24 @@ +# must fail: uri_resolve_rfc_02 uri_resolve_rfc_17 live_relisted_dot_alias +# dot segments kept in a resolved URL (http_binding::uri::resolve) +diff --git a/wai/wai-rs/src/http_binding/uri.rs b/wai/wai-rs/src/http_binding/uri.rs +index 0f23cf96..140ead1a 100644 +--- a/wai/wai-rs/src/http_binding/uri.rs ++++ b/wai/wai-rs/src/http_binding/uri.rs +@@ -127,7 +127,7 @@ pub fn resolve(base: &str, reference: &str) -> String { + Parts { scheme: b.scheme, authority: b.authority, path: remove_dot_segments(&r.path), query: r.query } + } else { + let merged = merge(&b, &r.path); +- Parts { scheme: b.scheme, authority: b.authority, path: remove_dot_segments(&merged), query: r.query } ++ Parts { scheme: b.scheme, authority: b.authority, path: merged, query: r.query } + }; + let mut out = String::new(); + if let Some(s) = &t.scheme { +@@ -146,7 +146,7 @@ pub fn resolve(base: &str, reference: &str) -> String { + None => out.push_str(&a.to_ascii_lowercase()), + } + } +- out.push_str(&remove_dot_segments(&normalize_percent(&t.path))); ++ out.push_str(&normalize_percent(&t.path)); + if let Some(q) = &t.query { + out.push('?'); + out.push_str(&normalize_percent(q)); diff --git a/wai/http-conformance/falsify/uri-percent-kept.patch b/wai/http-conformance/falsify/uri-percent-kept.patch new file mode 100644 index 000000000..e9e50cd59 --- /dev/null +++ b/wai/http-conformance/falsify/uri-percent-kept.patch @@ -0,0 +1,15 @@ +# must fail: uri_resolve_alias_03 +# percent-encoded unreserved characters left encoded (http_binding::uri::resolve) +diff --git a/wai/wai-rs/src/http_binding/uri.rs b/wai/wai-rs/src/http_binding/uri.rs +index 0f23cf96..5f74c61a 100644 +--- a/wai/wai-rs/src/http_binding/uri.rs ++++ b/wai/wai-rs/src/http_binding/uri.rs +@@ -97,7 +97,7 @@ fn normalize_percent(s: &str) -> String { + while i < b.len() { + if b[i] == b'%' && i + 2 < b.len() && b[i + 1].is_ascii_hexdigit() && b[i + 2].is_ascii_hexdigit() { + let v = u8::from_str_radix(&s[i + 1..i + 3], 16).expect("two hexadecimal digits"); +- if v.is_ascii_alphanumeric() || b"-._~".contains(&v) { ++ if false { + out.push(v as char); + } else { + out.push('%'); diff --git a/wai/http-conformance/falsify/walk-exceeded-wedges.patch b/wai/http-conformance/falsify/walk-exceeded-wedges.patch new file mode 100644 index 000000000..b0e3f66d5 --- /dev/null +++ b/wai/http-conformance/falsify/walk-exceeded-wedges.patch @@ -0,0 +1,15 @@ +# must fail: live_edge_reanchored live_edge_reanchored_skipping +# past the bound, every revision refused, so the client is wedged (LiveChain::advance) +diff --git a/wai/wai-rs/src/http_binding/playlist_receipt.rs b/wai/wai-rs/src/http_binding/playlist_receipt.rs +index 6fb6141d..cd1d8dea 100644 +--- a/wai/wai-rs/src/http_binding/playlist_receipt.rs ++++ b/wai/wai-rs/src/http_binding/playlist_receipt.rs +@@ -236,7 +236,7 @@ impl LiveChain { + // §8.6: past the bound, a revision ahead of the head (more segments + // complete) re-anchors the chain, its bindings kept; one that is not + // ahead is refused, so a stale or older revision never rolls it back. +- if last_segment(cur) <= last_segment(head) { ++ if true || last_segment(cur) <= last_segment(head) { + return Err(ReceiptError::WalkExceeded); + } + let mut b = self.bindings.clone(); diff --git a/wai/http-conformance/falsify/walk-parent-name-unchecked.patch b/wai/http-conformance/falsify/walk-parent-name-unchecked.patch new file mode 100644 index 000000000..d613e82da --- /dev/null +++ b/wai/http-conformance/falsify/walk-parent-name-unchecked.patch @@ -0,0 +1,15 @@ +# must fail: live_edge_parent_substituted +# a parent fetched by its link not held to it (LiveChain::advance) +diff --git a/wai/wai-rs/src/http_binding/playlist_receipt.rs b/wai/wai-rs/src/http_binding/playlist_receipt.rs +index 6fb6141d..7d582b26 100644 +--- a/wai/wai-rs/src/http_binding/playlist_receipt.rs ++++ b/wai/wai-rs/src/http_binding/playlist_receipt.rs +@@ -248,7 +248,7 @@ impl LiveChain { + let bytes = parent(&l).ok_or(ReceiptError::ParentMissing)?; + let text = std::str::from_utf8(&bytes).map_err(|_| ReceiptError::Malformed("not UTF-8".into()))?; + let r = ManifestReceipt::from_json(text).map_err(ReceiptError::Malformed)?; +- if r.receipt_hash() != l { ++ if false && r.receipt_hash() != l { + return Err(ReceiptError::NameMismatch); + } + if !r.verify() { diff --git a/wai/http-conformance/falsify/walk-parent-unverified.patch b/wai/http-conformance/falsify/walk-parent-unverified.patch new file mode 100644 index 000000000..542208c79 --- /dev/null +++ b/wai/http-conformance/falsify/walk-parent-unverified.patch @@ -0,0 +1,15 @@ +# must fail: live_edge_parent_invalid +# a parent the walk fetches not verified (LiveChain::advance) +diff --git a/wai/wai-rs/src/http_binding/playlist_receipt.rs b/wai/wai-rs/src/http_binding/playlist_receipt.rs +index 6fb6141d..08eec4e6 100644 +--- a/wai/wai-rs/src/http_binding/playlist_receipt.rs ++++ b/wai/wai-rs/src/http_binding/playlist_receipt.rs +@@ -251,7 +251,7 @@ impl LiveChain { + if r.receipt_hash() != l { + return Err(ReceiptError::NameMismatch); + } +- if !r.verify() { ++ if false && !r.verify() { + return Err(ReceiptError::Invalid); + } + if r.signer_pubkey != cur.signer_pubkey { diff --git a/wai/http-conformance/falsify/xml-attribute-twice.patch b/wai/http-conformance/falsify/xml-attribute-twice.patch new file mode 100644 index 000000000..5a92b456f --- /dev/null +++ b/wai/http-conformance/falsify/xml-attribute-twice.patch @@ -0,0 +1,15 @@ +# must fail: dash_adjacency_attribute_twice +# an attribute twice read (dash_read::read_xml) +diff --git a/wai/wai-rs/src/http_binding/dash_read.rs b/wai/wai-rs/src/http_binding/dash_read.rs +index 71e36cde..ea32b008 100644 +--- a/wai/wai-rs/src/http_binding/dash_read.rs ++++ b/wai/wai-rs/src/http_binding/dash_read.rs +@@ -204,7 +204,7 @@ impl<'a> Reader<'a> { + } + self.space(); + let v = self.attr_value()?; +- if !names.insert(k.clone()) { ++ if !names.insert(k.clone()) && false { + return Err(malformed(format!("the attribute {k} twice"))); + } + attrs.push((k, v)); diff --git a/wai/http-conformance/falsify/xml-cdata-read.patch b/wai/http-conformance/falsify/xml-cdata-read.patch new file mode 100644 index 000000000..3174120ce --- /dev/null +++ b/wai/http-conformance/falsify/xml-cdata-read.patch @@ -0,0 +1,16 @@ +# must fail: dash_adjacency_cdata +# a CDATA section read (dash_read::read_xml) +diff --git a/wai/wai-rs/src/http_binding/dash_read.rs b/wai/wai-rs/src/http_binding/dash_read.rs +index 79d55df6..c268e915 100644 +--- a/wai/wai-rs/src/http_binding/dash_read.rs ++++ b/wai/wai-rs/src/http_binding/dash_read.rs +@@ -248,6 +248,9 @@ pub fn read_xml(text: &str) -> Result { + Some(parent) => parent.children.push(e), + None => done = Some(e), + } ++ } else if r.rest().starts_with("").unwrap_or(0); ++ r.at += end + 3; + } else if r.rest().starts_with(" Reader<'a> { + } else { + None + }; +- n.and_then(char::from_u32).filter(|&c| xml_char(c)).ok_or_else(|| malformed("a reference to an entity or character outside the subset"))? ++ n.and_then(char::from_u32).ok_or_else(|| malformed("a reference to an entity or character outside the subset"))? + } + }; + self.at += end + 1; diff --git a/wai/http-conformance/http_verify.py b/wai/http-conformance/http_verify.py index 5c9736f53..0ed418ed1 100644 --- a/wai/http-conformance/http_verify.py +++ b/wai/http-conformance/http_verify.py @@ -32,7 +32,7 @@ stops the verifier, never passes. Exit status 0 only when every case gives its committed outcome (or, for diff, every proof verifies, or exactly the flipped ones fail). """ -import hashlib, json, os, random, re, sys +import hashlib, json, os, random, re, sys, time HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance")) @@ -697,7 +697,7 @@ def parse_layer_tag(line): end = rest.find(b",") end = len(rest) if end < 0 else end value, quoted, after = rest[:end], False, rest[end:] - if not value or b'"' in value or re.search(rb"[ \t\n\r\x0b\x0c]", value): + if not value or b'"' in value or re.search(rb"[ \t\n\r\x0c]", value): raise bad if name == b"N": if quoted or len(value) > 3 or not re.fullmatch(rb"[0-9]+", value) or not 1 <= int(value) <= 255: @@ -942,7 +942,7 @@ class Session: self.to_rest("single_component", "base") else: entries = head["manifest"]["renditions"] - ordr = order if order is not None else list(range(len(entries))) + ordr = order(head) if callable(order) else (order if order is not None else list(range(len(entries)))) pick, skipped = select(head["manifest"], ordr, adv, held_sets, lambda i, e: True) wanted = [(pick, "base")] + self.applied(entries, pick) pl = plan(head, wanted, [(a, b) for a, b, _ in self.held], self.permille) @@ -1037,7 +1037,7 @@ class Session: return entries = m["renditions"] pay = [envb[s:s + ln] for s, ln in head["components"]] - ordr = order if order is not None else list(range(len(entries))) + ordr = order(head) if callable(order) else (order if order is not None else list(range(len(entries)))) pick, skipped = select(m, ordr, adv, held_sets, lambda i, e: payload_ok(e, pay[i])) comps, refs, dropped = [], [], [] for j, cl in self.applied(entries, pick): @@ -1230,7 +1230,2027 @@ def sink_of(d, inp, objs): if inp.get("layer_base") is not None or inp.get("layer_base_hex") is not None: b = bytes.fromhex(inp["layer_base_hex"]) if isinstance(inp.get("layer_base_hex"), str) else objs[inp["layer_base"]] base = seq_object(b) - return (inp.get("advertises") or [], inp.get("applies") or [], held, inp.get("order"), declared, base) + order = inp.get("order") + if inp.get("abr") is not None: + adv, pol, st = inp.get("advertises") or [], inp["abr"]["policy"], abr_state(inp["abr"]["state"]) + check_policy(pol) + check_state(st) + + def order(head): + try: + return order_entries(head, adv, pol, st) + except Refusal: + return list(range(len(head["manifest"]["renditions"]))) + return (inp.get("advertises") or [], inp.get("applies") or [], held, order, declared, base) + + +# ------------------------------------------------- adaptation (Appendix F) + +LOG2_TABLE = [0, 5732, 11136, 16248, 21098, 25711, 30109, 34312, 38336, 42196, 45904, 49472, 52911, 56229, 59434, 62534, 65536] + + +def ilog2_q16(a, b): + """log2(a/b) in Q16: the ratio's exponent, and the table interpolated on the + mantissa's top four fractional bits with the next twelve as the weight.""" + b = max(b, 1) + if a <= b: + return 0 + x = (a << 16) // b + e = x.bit_length() - 1 - 16 + f = (x >> e) - 65536 + i, r = f >> 12, f & 4095 + if MUTATE == "ilog2-no-interp": + r = 0 + return e * 65536 + LOG2_TABLE[i] + (((LOG2_TABLE[i + 1] - LOG2_TABLE[i]) * r) >> 12) + + +def throughput_bps(samples, n): + k = min(max(n, 1), len(samples)) + tail = samples[len(samples) - k:] + t = sum(max(s[2], 1) for s in tail) + return sum(s[1] for s in tail) * 8000 // t if t else 0 + + +def option_bps(nbytes, seg): + return -(-(nbytes * 8000) // max(seg, 1)) + + +def highest_under(st, budget): + for k in range(len(st["option_bytes"]) - 1, -1, -1): + if option_bps(st["option_bytes"][k], st["segment_ms"]) <= budget: + return k + return 0 + + +def utilities(st): + s0 = max(st["option_bytes"][0] if st["option_bytes"] else 1, 1) + return [ilog2_q16(max(b, 1), s0) for b in st["option_bytes"]] + + +def choose(p, st): + opts, seg, k = st["option_bytes"], st["segment_ms"], len(st["option_bytes"]) + if k == 0: + return 0 + if p["id"] == "throughput": + return highest_under(st, throughput_bps(st["samples"], p["last_n"]) * p["safety_permille"] // 1000) + if p["id"] == "buffer_based": + b, r, c = st["buffer_ms"], p["reservoir_ms"], p["cushion_ms"] + if b <= r: + return 0 + if c == 0 or b >= r + c: + return k - 1 + lo, hi = option_bps(opts[0], seg), option_bps(opts[-1], seg) + return highest_under(st, lo + max(hi - lo, 0) * (b - r) // c) + if p["id"] == "bola_like": + sg = max(seg, 1) + q, qmax = st["buffer_ms"] * 65536 // sg, p["max_buffer_ms"] * 65536 // sg + u = utilities(st) + vn, vd = max(qmax - 65536, 0), max(u) + 65536 + score = [vn * p["v_milli"] * (x + 65536) // (vd * 1000) - q for x in u] + size = [max(b, 1) for b in opts] + best = None + if any(x > 0 for x in score): + for i in range(k): + if score[i] > 0 and (best is None or score[i] * size[best] > score[best] * size[i]): + best = i + elif MUTATE == "bola-waits": + return 0 + else: + for i in range(k): + if best is None or score[i] > score[best]: + best = i + return best + if p["id"] == "mpc": + pred = throughput_bps(st["samples"], 5) * p["safety_permille"] // 1000 + if pred == 0: + return 0 + h = min(max(p["horizon"], 1), 5) + while h > 1 and k ** h > 65536: + h -= 1 + u = utilities(st) + u_top = max(u) + best = None + for code in range(k ** h): + seq, c = [0] * h, code + for t in range(h - 1, -1, -1): + seq[t], c = c % k, c // k + b, rebuf, qoe = st["buffer_ms"], 0, 0 + last = st["last"] + prev = u[last] if last is not None and last < k else None + for o in seq: + dl = -(-(opts[o] * 8000) // pred) + if dl > b: + rebuf, b = rebuf + dl - b, 0 + else: + b -= dl + b += seg + qoe += 1000 * u[o] + if prev is not None: + qoe -= 1000 * abs(u[o] - prev) + prev = u[o] + qoe -= rebuf * (u_top + 65536) + if best is None or qoe > best[0]: + best = (qoe, seq[0]) + return best[1] + raise SystemExit("http_verify: an unknown policy " + str(p["id"])) + + +def clamp_to_budget(st, choice): + """Appendix F's budget: it never raises the choice; the highest option at or below it + whose labelled figure is within it, else option 0 unmet; not applied only when no + option has a labelled figure.""" + budget = st["joule_budget_micro"] + if budget is None: + return choice, "not_set" + figs = st["option_joules_micro"] + n = max(len(st["option_bytes"]), choice + 1) + fig = lambda j: figs[j] if j < len(figs) and j < n else None + if MUTATE == "unlabelled-as-zero": + fig = lambda j: (figs[j] if j < len(figs) and j < n else None) or 0 + labelled = [(j, fig(j)) for j in range(n) if fig(j) is not None] + if not labelled or (MUTATE == "unlabelled-escapes" and fig(choice) is None): + return choice, "not_applied" + if fig(choice) is not None and fig(choice) <= budget: + return choice, "within" + below = [j for j, f in labelled if j < choice and f <= budget] + if below: + return below[-1], "lowered" + if MUTATE == "budget-raises": + above = [j for j, f in labelled if j > choice and f <= budget] + if above: + return above[0], "unmet" + return 0, "unmet" + + +MAX_OPTIONS = 256 +OUT_OF_BOUNDS = "abr_state_out_of_bounds" + + +def bounded(v, top): + """An unsigned integer of at most top; out of bounds for any other number.""" + if isinstance(v, bool) or not isinstance(v, (int, float)): + raise SystemExit("http_verify: an adaptation value is not a number") + if not isinstance(v, int) or not 0 <= v <= top: + raise Refusal(OUT_OF_BOUNDS) + return v + + +def check_policy(p): + """Appendix F: each parameter within its type.""" + tops = {"throughput": {"last_n": 255, "safety_permille": 65535}, "buffer_based": {"reservoir_ms": U32, "cushion_ms": U32}, + "bola_like": {"v_milli": U32, "max_buffer_ms": U32}, "mpc": {"horizon": 255, "safety_permille": 65535}} + if p.get("id") not in tops: + raise SystemExit("http_verify: an unknown policy " + str(p.get("id"))) + for k, top in tops[p["id"]].items(): + bounded(p[k], top) + + +def check_state(st): + if (len(st["option_bytes"]) > MAX_OPTIONS or len(st["option_joules_micro"]) > MAX_OPTIONS) and MUTATE != "abr-unbounded": + raise Refusal(OUT_OF_BOUNDS) + + +def decide(p, st): + check_policy(p) + check_state(st) + return clamp_to_budget(st, choose(p, st)) + + +def abr_state(v): + """A state from its JSON form, each value within its field's type (Appendix F).""" + opt = lambda x, top: None if x is None else bounded(x, top) + return {"buffer_ms": bounded(v["buffer_ms"], U32), "samples": [tuple(bounded(x, U64) for x in s[:3]) for s in v.get("samples") or []], + "last": opt(v.get("last"), U64), "option_bytes": [bounded(x, U64) for x in v.get("option_bytes") or []], + "option_joules_micro": [opt(x, U64) for x in v.get("option_joules_micro") or []], + "joule_budget_micro": opt(v.get("joule_budget_micro"), U64), "segment_ms": bounded(v["segment_ms"], U32)} + + +def depth(layers_n, p, st): + check_policy(p) + check_state(st) + if len(layers_n) > MAX_OPTIONS: + raise Refusal(OUT_OF_BOUNDS) + st = dict(st) + acc, cum, jcum, fsum = 0, [], [], 0 + figs = st["option_joules_micro"] + for k, n in enumerate(layers_n): + acc += n + if acc > U64 and MUTATE != "abr-unbounded": + raise Refusal(OUT_OF_BOUNDS) + cum.append(acc) + f = figs[k] if k < len(figs) else None + fsum = None if fsum is None or f is None else fsum + f + if fsum is not None and fsum > U64 and MUTATE != "abr-unbounded": + raise Refusal(OUT_OF_BOUNDS) + jcum.append(fsum) + st["option_bytes"], st["option_joules_micro"] = cum, jcum + if not layers_n: + return 0, clamp_to_budget(st, 0)[1] + return clamp_to_budget(st, choose(p, st)) + + +def order_entries(head, adv, p, st): + """SPEC §7.1 step 2's order under a policy: each advertised selectable capability's + entries by component length, the choice first, then those below it nearest first, + then those above; every other entry after, in table order.""" + check_policy(p) + check_state(st) + if head["version"] != "WAI2": + return [0] + entries = head["manifest"]["renditions"] + ln = lambda i: head["components"][i][1] + groups = [] + for i, e in enumerate(entries): + if not selectable(e) or e["capability"] not in adv: + continue + g = next((g for g in groups if g[0] == e["capability"]), None) + if g is None: + groups.append((e["capability"], [i])) + else: + g[1].append(i) + order = [] + for _, g in groups: + g = sorted(g, key=lambda i: (ln(i), i)) + s2 = dict(st) + figs = st["option_joules_micro"] + s2["option_bytes"] = [ln(i) for i in g] + s2["option_joules_micro"] = [figs[i] if i < len(figs) else None for i in g] + c = min(decide(p, s2)[0], len(g) - 1) + order += [g[c]] + list(reversed(g[:c])) + g[c + 1:] + return order + [i for i in range(len(entries)) if i not in order] + + +# ------------------------------------------------------ playlists (§10.4, §11) + +U64 = 2 ** 64 - 1 +U32 = 2 ** 32 - 1 +DIGITS3 = re.compile(r"[0-9]+(\.[0-9]+)?", re.ASCII) + + +def decimal_ms(s): + """A decimal number of seconds, exactly, in milliseconds rounded half up.""" + if not DIGITS3.fullmatch(s): + return None + whole, _, frac = s.partition(".") + f = (frac + "0000")[:4] + ms = int(whole) * 1000 + int(f[:3]) + (1 if int(f[3]) >= (6 if MUTATE == "round-half-down" else 5) else 0) + return ms if ms <= U64 else None + + +def decimal_int(s): + if not re.fullmatch(r"[0-9]+", s, re.ASCII): + return None + v = int(s) + return v if v <= U64 else None + + +def attributes(s): + """An HLS attribute list (RFC 8216 §4.2) as [(name, quoted, value)].""" + bad = Refusal("playlist_malformed") + out, rest = [], s + while True: + if "=" not in rest: + raise bad + name, after = rest.split("=", 1) + if not re.fullmatch(r"[A-Z0-9-]+", name, re.ASCII) or any(n == name for n, _, _ in out): + raise bad + if after.startswith('"'): + close = after.find('"', 1) + if close < 0: + raise bad + v = after[1:close] + if "\r" in v or "\n" in v: + raise bad + out.append((name, True, v)) + after = after[close + 1:] + else: + end = after.find(",") + end = len(after) if end < 0 else end + v = after[:end] + if not v or '"' in v or any(ch in " \t\n\r\x0c" for ch in v): + raise bad + out.append((name, False, v)) + after = after[end:] + if not after: + return out + if not after.startswith(","): + raise bad + rest = after[1:] + + +def attr(a, name): + return next(((q, v) for n, q, v in a if n == name), None) + + +def attr_ms(a, name): + x = attr(a, name) + if x is None: + return None + if x[0] or decimal_ms(x[1]) is None: + raise Refusal("playlist_malformed") + return decimal_ms(x[1]) + + +def attr_quoted(a, name): + x = attr(a, name) + if x is None: + return None + if not x[0]: + raise Refusal("playlist_malformed") + return x[1] + + +def attr_yes(a, name): + x = attr(a, name) + if x is None: + return False + if x[0] or x[1] not in ("YES", "NO"): + raise Refusal("playlist_malformed") + return x[1] == "YES" + + +def new_extra(): + return {"discontinuity": False, "gap": False, "pdt": None, "parts": [], "layers": [], "hint": None} + + +def push_layer(layers, l): + if layers and layers[-1]["n"] >= l["n"]: + raise Refusal("playlist_malformed") + layers.append(l) + + +def parse_media_playlist(text): + """A media playlist, classical or WAI, read as §11 needs it.""" + bad = Refusal("playlist_malformed") + lines = [l[:-1] if l.endswith("\r") else l for l in text.split("\n")] + lines = [l for l in lines if l] + if not lines or lines[0] != "#EXTM3U": + raise bad + p = {"version": None, "target": None, "seq": 0, "dseq": 0, "part_target": None, "hold_back": None, "block": False, + "segments": [], "open": None, "hint": None, "end": False, "ptype": None, "indep": False, "map": None} + pending, extinf, anchor, started = new_extra(), None, None, False + for line in lines[1:]: + if line.startswith("#WAI-LAYER:"): + l = parse_layer_tag(line) + if anchor == "extinf": + push_layer(pending["layers"], l) + elif anchor == "part": + push_layer(pending["parts"][-1]["layers"], l) + else: + raise bad + continue + anchor = None + if not line.startswith("#"): + if extinf is None: + raise bad + p["segments"].append({"ms": extinf, "uri": line, "extra": pending}) + pending, extinf, started = new_extra(), None, True + continue + if not line.startswith("#EXT"): + continue + tag, _, value = line.partition(":") + if tag == "#EXTM3U": + raise bad + elif tag == "#EXT-X-VERSION": + v = decimal_int(value) + if v is None or v > 255: + raise bad + p["version"] = v + elif tag == "#EXT-X-TARGETDURATION": + v = decimal_int(value) + if v is None: + raise bad + p["target"] = v + elif tag in ("#EXT-X-MEDIA-SEQUENCE", "#EXT-X-DISCONTINUITY-SEQUENCE"): + if started or extinf is not None or pending["parts"]: + raise bad + v = decimal_int(value) + if v is None: + raise bad + p["seq" if tag == "#EXT-X-MEDIA-SEQUENCE" else "dseq"] = v + elif tag == "#EXT-X-PART-INF": + a = attributes(value) + pt = attr_ms(a, "PART-TARGET") + if pt is None: + raise bad + p["part_target"] = pt + elif tag == "#EXT-X-SERVER-CONTROL": + a = attributes(value) + p["block"] = attr_yes(a, "CAN-BLOCK-RELOAD") + p["hold_back"] = attr_ms(a, "PART-HOLD-BACK") + elif tag == "#EXT-X-DISCONTINUITY": + pending["discontinuity"] = True + elif tag == "#EXT-X-GAP": + pending["gap"] = True + elif tag == "#EXT-X-INDEPENDENT-SEGMENTS": + p["indep"] = True + elif tag == "#EXT-X-PLAYLIST-TYPE": + if value not in ("VOD", "EVENT"): + raise bad + p["ptype"] = value + elif tag == "#EXT-X-MAP": + uri = attr_quoted(attributes(value), "URI") + if uri is None: + raise bad + if not started and extinf is None and pending == new_extra() and p["map"] is None: + p["map"] = uri + elif tag == "#EXT-X-PROGRAM-DATE-TIME": + pending["pdt"] = value + elif tag == "#EXT-X-PART": + a = attributes(value) + d, uri = attr_ms(a, "DURATION"), attr_quoted(a, "URI") + if d is None or uri is None: + raise bad + pending["parts"].append({"ms": d, "independent": attr_yes(a, "INDEPENDENT"), "uri": uri, "layers": []}) + anchor = "part" + elif tag == "#EXTINF": + if extinf is not None: + raise bad + extinf = decimal_ms(value.split(",", 1)[0]) + if extinf is None: + raise bad + anchor = "extinf" + elif tag == "#EXT-X-PRELOAD-HINT": + a = attributes(value) + t = attr(a, "TYPE") + if t is None or t[0]: + raise bad + if t[1] == "PART": + if p["hint"] is not None: + raise bad + h = attr_quoted(a, "URI") + if h is None: + raise bad + p["hint"] = h + elif tag == "#EXT-X-ENDLIST": + p["end"] = True + elif tag in ("#EXT-X-SKIP", "#EXT-X-STREAM-INF", "#EXT-X-I-FRAME-STREAM-INF", "#EXT-X-MEDIA"): + raise bad + if extinf is not None: + raise bad + if pending != new_extra(): + p["open"] = pending + return p + + +def secs(ms): + return "%d.%03d" % (ms // 1000, ms % 1000) + + +def render_hls(segs, r, extras): + """The rendering of §8.6: header, then each segment (discontinuity, date-time, parts + with their layer lines, EXTINF, its layer lines, URI), the open segment, the hint, + the end.""" + target = r["target"] if r["target"] is not None else max([-(-s["ms"] // 1000) for s in segs], default=0) + out = ["#EXTM3U", "#EXT-X-VERSION:%d" % r["version"], "#EXT-X-TARGETDURATION:%d" % target] + if r["part_target"] is not None: + out.append("#EXT-X-PART-INF:PART-TARGET=" + secs(r["part_target"])) + ctl = (["CAN-BLOCK-RELOAD=YES"] if r["block"] else []) + (["PART-HOLD-BACK=" + secs(r["hold_back"])] if r["hold_back"] is not None else []) + if ctl: + out.append("#EXT-X-SERVER-CONTROL:" + ",".join(ctl)) + out.append("#EXT-X-MEDIA-SEQUENCE:%d" % r["seq"]) + if r["dseq"]: + out.append("#EXT-X-DISCONTINUITY-SEQUENCE:%d" % r["dseq"]) + if r.get("ptype"): + out.append("#EXT-X-PLAYLIST-TYPE:" + r["ptype"]) + if r.get("indep"): + out.append("#EXT-X-INDEPENDENT-SEGMENTS") + if r.get("map") is not None: + out.append('#EXT-X-MAP:URI="%s"' % r["map"]) + + def parts(ps): + for q in ps: + out.append("#EXT-X-PART:DURATION=%s%s,URI=\"%s\"" % (secs(q["ms"]), ",INDEPENDENT=YES" if q["independent"] else "", q["uri"])) + out.extend("#WAI-LAYER:N=%d,URI=\"%s\"" % (l["n"], l["uri"]) for l in q["layers"]) + + for i, s in enumerate(segs): + e = extras[i] if i < len(extras) else new_extra() + if s.get("ad") or e["discontinuity"]: + out.append("#EXT-X-DISCONTINUITY") + if e["pdt"] is not None: + out.append("#EXT-X-PROGRAM-DATE-TIME:" + e["pdt"]) + if MUTATE == "parts-after-extinf": + out.append("#EXTINF:%s," % secs(s["ms"])) + parts(e["parts"]) + else: + parts(e["parts"]) + if e.get("gap"): + out.append("#EXT-X-GAP") + out.append("#EXTINF:%s," % secs(s["ms"])) + out.extend("#WAI-LAYER:N=%d,URI=\"%s\"" % (l["n"], l["uri"]) for l in e["layers"]) + out.append(s["uri"]) + if len(extras) > len(segs): + o = extras[len(segs)] + if o["discontinuity"]: + out.append("#EXT-X-DISCONTINUITY") + if o["pdt"] is not None: + out.append("#EXT-X-PROGRAM-DATE-TIME:" + o["pdt"]) + parts(o["parts"]) + if extras and extras[-1]["hint"] is not None: + out.append("#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"%s\"" % extras[-1]["hint"]) + if r["end"]: + out.append("#EXT-X-ENDLIST") + return "\n".join(out) + "\n" + + +def uri_ok(u, quoted): + return bool(u) and not u.startswith("#") and "\r" not in u and "\n" not in u and not (quoted and '"' in u) + + +def check_render(segs, r, extras): + bad = Refusal("playlist_malformed") + n = len(segs) + if len(extras) not in (0, n, n + 1) or r["version"] == 0: + raise bad + if r["part_target"] is not None and (r["hold_back"] is None or r["part_target"] == 0 or r["hold_back"] < 2 * r["part_target"]): + raise bad + if any(not uri_ok(s["uri"], False) for s in segs): + raise bad + if r.get("map") is not None and not uri_ok(r["map"], True): + raise bad + + def layers_ok(ls): + last = 0 + for l in ls: + if l["n"] == 0 or l["n"] <= last or not uri_ok(l["uri"], True): + raise bad + last = l["n"] + + for i, e in enumerate(extras): + if e["pdt"] is not None and (not e["pdt"] or not re.fullmatch(r"[A-Za-z0-9:.+-]+", e["pdt"], re.ASCII)): + raise bad + if e["parts"] and r["part_target"] is None: + raise bad + for q in e["parts"]: + if not uri_ok(q["uri"], True): + raise bad + layers_ok(q["layers"]) + layers_ok(e["layers"]) + if i < n and e.get("gap") and (e["parts"] or e["layers"]) and MUTATE != "gap-carries": + raise bad + if e["hint"] is not None and (i + 1 != len(extras) or r["end"] or not uri_ok(e["hint"], True)): + raise bad + if i == n: + if r["end"] or e["layers"] or e.get("gap") or (not e["parts"] and (e["discontinuity"] or e["pdt"] is not None)) or (not e["parts"] and e["hint"] is None): + raise bad + + +def render_inputs(p): + segs = [{"ms": s["ms"], "uri": s["uri"]} for s in p["segments"]] + r = {"version": p["version"] if p["version"] is not None else 1, "seq": p["seq"], "dseq": p["dseq"], "target": p["target"], + "part_target": p["part_target"], "hold_back": p["hold_back"], "block": p["block"], "end": p["end"], + "ptype": p["ptype"], "indep": p["indep"], "map": p["map"]} + extras = [json.loads(json.dumps(s["extra"])) for s in p["segments"]] + if p["open"] is not None: + extras.append(json.loads(json.dumps(p["open"]))) + if p["hint"] is not None: + if not extras: + extras.append(new_extra()) + extras[-1]["hint"] = p["hint"] + if all(e == new_extra() for e in extras): + extras = [] + return segs, r, extras + + +def is_rendered_form(p, text): + segs, r, extras = render_inputs(p) + try: + check_render(segs, r, extras) + except Refusal: + return False + return render_hls(segs, r, extras) == text + + +def misaligned(): + return Refusal("adjacency_misaligned") + + +def timelines_adjacent(w, c): + if w["seq"] != c["seq"] or (w["dseq"] != c["dseq"] and MUTATE != "dseq-ignored") or len(w["segments"]) != len(c["segments"]): + raise misaligned() + both = w["part_target"] is not None and c["part_target"] is not None + if both and w["part_target"] != c["part_target"]: + raise misaligned() + for a, b in zip(w["segments"], c["segments"]): + if a["ms"] != b["ms"] or a["extra"]["discontinuity"] != b["extra"]["discontinuity"]: + raise misaligned() + if a["extra"]["pdt"] != b["extra"]["pdt"] and MUTATE != "pdt-ignored": + raise misaligned() + pa, pb = [q["ms"] for q in a["extra"]["parts"]], [q["ms"] for q in b["extra"]["parts"]] + if both and pa and pb and pa != pb: + raise misaligned() + if w["open"] is not None and c["open"] is not None: + a, b = w["open"], c["open"] + if a["discontinuity"] != b["discontinuity"] or a["pdt"] != b["pdt"]: + raise misaligned() + if both and [q["ms"] for q in a["parts"]] != [q["ms"] for q in b["parts"]]: + raise misaligned() + + +def check_adjacent(wai, classical): + w = parse_media_playlist(wai) + c = parse_media_playlist(classical) + timelines_adjacent(w, c) + + +def wai_media_playlist(c, segs, open_, prefix, end): + """The WAI media playlist on classical timeline `c` (§11.1-§11.4).""" + if len(segs) != len(c["segments"]): + raise misaligned() + has_parts = any(s["parts"] for s in segs) or (open_ is not None and open_["parts"]) + if has_parts and c["part_target"] is None: + raise misaligned() + lay = lambda ls: [{"n": l["n"], "uri": "%s%s.wai#n=%d" % (prefix, l["cid"].hex(), l["len"])} for l in ls] + prt = lambda ps: [{"ms": q["ms"], "independent": q["independent"], "uri": "%s#cid=%s&n=%d" % (q["alias"], q["cid"].hex(), q["n"]), "layers": lay(q["layers"])} for q in ps] + out_segs, extras = [], [] + for i, (w, k) in enumerate(zip(segs, c["segments"])): + if w["ms"] != k["ms"]: + raise misaligned() + if w["parts"] and k["extra"]["parts"] and [q["ms"] for q in w["parts"]] != [q["ms"] for q in k["extra"]["parts"]]: + raise misaligned() + h = "&h=%d" % w["h"] if w["h"] is not None else "" + out_segs.append({"ms": w["ms"], "uri": "%s%s.wai#n=%d%s" % (prefix, w["cid"].hex(), w["n"], h)}) + e = new_extra() + e.update(discontinuity=k["extra"]["discontinuity"], gap=w.get("gap", False), pdt=k["extra"]["pdt"], parts=prt(w["parts"]), layers=lay(w["layers"])) + extras.append(e) + hint = None + if open_ is not None: + if not open_["parts"]: + hint = open_["hint"] + else: + ko = c["open"] + if ko is None: + raise misaligned() + if ko["parts"] and [q["ms"] for q in open_["parts"]] != [q["ms"] for q in ko["parts"]]: + raise misaligned() + e = new_extra() + e.update(discontinuity=ko["discontinuity"], pdt=ko["pdt"], parts=prt(open_["parts"]), hint=open_["hint"]) + extras.append(e) + if all(e == new_extra() for e in extras): + extras = [] + if hint is not None: + if not extras: + extras = [new_extra() for _ in range(max(len(segs), 1))] + extras[-1]["hint"] = hint + pt = c["part_target"] + r = {"version": c["version"] if c["version"] is not None else 6, "seq": c["seq"], "dseq": c["dseq"], "target": c["target"], + "part_target": pt, "hold_back": c["hold_back"] if c["hold_back"] is not None else (pt * 3 if pt is not None else None), + "block": c["block"], "end": end, "ptype": c["ptype"], "indep": c["indep"], "map": None} + check_render(out_segs, r, extras) + text = render_hls(out_segs, r, extras) + timelines_adjacent(parse_media_playlist(text), c) + return text + + +def bandwidths(objs): + if not objs or any(d == 0 for _, d in objs): + return None + rate = lambda n, d: min(-(-(8000 * n) // d), U64) + peak = max(rate(n, d) for n, d in objs) + sn, sd = sum(n for n, _ in objs), sum(d for _, d in objs) + return peak, (rate(sn, sd) if MUTATE != "average-floor" else 8000 * sn // sd) + + +SESSION_DATA = '#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="' + + +def session_data_position(original): + """§11.1: the offset after the first line and the EXT-X-VERSION and + EXT-X-INDEPENDENT-SEGMENTS lines that directly follow it; None when there is none.""" + at = 0 + lines = original.split("\n") + lines = [l + "\n" for l in lines[:-1]] + ([lines[-1]] if lines[-1] else []) + for i, l in enumerate(lines): + bare = l[:-1] if l.endswith("\n") else l + bare = bare[:-1] if l.endswith("\n") and bare.endswith("\r") else bare + if i > 0 and not (bare.startswith("#EXT-X-VERSION:") or bare == "#EXT-X-INDEPENDENT-SEGMENTS") and MUTATE != "session-data-anywhere": + break + if not l.endswith("\n"): + return None + at += len(l.encode("utf-8")) + if MUTATE == "session-data-anywhere": + break + return at if at > 0 else None + + +def classical_unchanged(original, published): + if original == published: + return + o, p = original.encode("utf-8"), published.encode("utf-8") + at = session_data_position(original) + if at is None or not (p.startswith(o[:at]) and p.endswith(o[at:]) and len(p) > len(o)): + raise Refusal("classical_changed") + inserted = p[at:len(p) - (len(o) - at)].decode("utf-8") + if not inserted.endswith("\n"): + raise Refusal("classical_changed") + bare = inserted[:-1] + bare = bare[:-1] if bare.endswith("\r") else bare + if "\n" in bare or not (bare.startswith(SESSION_DATA) and bare.endswith('"')): + raise Refusal("classical_changed") + uri = bare[len(SESSION_DATA):-1] + if not uri or any(ch in uri for ch in '"\r\n'): + raise Refusal("classical_changed") + + +def listing_lines(p): + """Every part, layer and segment line of a media playlist, in the order a client + reads them for a cid (§5.2, §11.4).""" + lines = [] + for s in p["segments"]: + for q in s["extra"]["parts"]: + lines.append(q["uri"]) + lines += [l["uri"] for l in q["layers"]] + lines += [l["uri"] for l in s["extra"]["layers"]] + lines.append(s["uri"]) + if p["open"] is not None: + for q in p["open"]["parts"]: + lines.append(q["uri"]) + lines += [l["uri"] for l in q["layers"]] + return lines + + +def uri_cid(line): + """The cid a listed URI names: its cid fragment, or the .wai its path ends in.""" + base, _, frag = line.partition("#") + for kv in frag.split("&") if frag else []: + k, eq, v = kv.partition("=") + if eq and k == "cid" and re.fullmatch(r"[0-9a-f]{64}", v): + return bytes.fromhex(v) + name = base.rsplit("/", 1)[-1] + if name.endswith(".wai") and re.fullmatch(r"[0-9a-f]{64}", name[:-4]): + return bytes.fromhex(name[:-4]) + return None + + +def listed_cid(p, uri): + line = next((l for l in listing_lines(p) if l.split("#", 1)[0] == uri), None) + return None if line is None else uri_cid(line) + + +RETENTION = 256 +URI_PARTS = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$", re.S) + + +def remove_dot_segments(path): + """RFC 3986 §5.2.4.""" + inp, out = path, "" + while inp: + if inp.startswith("../"): + inp = inp[3:] + elif inp.startswith("./"): + inp = inp[2:] + elif inp.startswith("/./"): + inp = inp[2:] + elif inp == "/.": + inp = "/" + elif inp.startswith("/../") or inp == "/..": + inp = "/" + inp[4:] if inp.startswith("/../") else "/" + out = out[:out.rfind("/")] if "/" in out else "" + elif inp in (".", ".."): + inp = "" + else: + j = inp.find("/", 1 if inp.startswith("/") else 0) + j = len(inp) if j < 0 else j + out, inp = out + inp[:j], inp[j:] + return out + + +def pct_normal(s): + """RFC 3986 §6.2.2.2: upper-case hexadecimal digits, unreserved characters decoded.""" + def one(m): + c = chr(int(m.group(1), 16)) + return c if re.fullmatch(r"[A-Za-z0-9._~-]", c) else "%" + m.group(1).upper() + return re.sub(r"%([0-9A-Fa-f]{2})", one, s) + + +def rfc3986_resolve(base, ref): + """ref resolved against base (RFC 3986 §5.2.2, §5.2.3) and normalized (§6.2.2), its + fragment dropped.""" + def parts(u): + m = URI_PARTS.match(u) + return m.group(2), m.group(4) if m.group(3) is not None else None, m.group(5), m.group(7) if m.group(6) is not None else None + bs, ba, bp, bq = parts(base) + rs, ra, rp, rq = parts(ref) + if MUTATE == "uri-unresolved": + return ref.split("#", 1)[0] + if rs is not None: + ts, ta, tp, tq = rs, ra, remove_dot_segments(rp), rq + elif ra is not None: + ts, ta, tp, tq = bs, ra, remove_dot_segments(rp), rq + elif rp == "": + ts, ta, tp, tq = bs, ba, bp, rq if rq is not None else bq + elif rp.startswith("/"): + ts, ta, tp, tq = bs, ba, remove_dot_segments(rp), rq + else: + if ba is not None and bp == "": + merged = "/" + rp + else: + merged = bp[:bp.rfind("/") + 1] + rp + ts, ta, tp, tq = bs, ba, remove_dot_segments(merged), rq + out = "" + if ts is not None: + out += ts.lower() + ":" + if ta is not None: + a = pct_normal(ta) + at = a.rfind("@") + out += "//" + (a[:at + 1] + a[at + 1:].lower() if at >= 0 else a.lower()) + out += remove_dot_segments(pct_normal(tp)) + if tq is not None: + out += "?" + pct_normal(tq) + return out + + +def new_bindings(url): + return {"base": url, "uris": {}, "seqs": {}, "rev": 0} + + +def bind(b, p): + """§5.2, §11.5: the bindings b with playlist p's added as one more revision: each URL + a line names (resolved against the playlist's URL) and each media sequence number to + its cid. Refusal when p lists another cid for one bound, or a gap on a bound sequence + number; then what none of the last RETENTION revisions listed is forgotten.""" + rev = b["rev"] + 1 + uris, seqs = dict(b["uris"]), dict(b["seqs"]) + for line in listing_lines(p): + cid = uri_cid(line) + if cid is None: + continue + key = rfc3986_resolve(b["base"], line) + if key in uris and uris[key][0] != cid and MUTATE != "rebind-uri": + raise Refusal("playlist_listing_rebound") + uris[key] = (cid, rev) + for i, sg in enumerate(p["segments"]): + n = p["seq"] + i + if n > U64: + raise Refusal("playlist_malformed") + if sg["extra"]["gap"]: + if n in seqs and MUTATE != "gap-withdraws": + raise Refusal("playlist_listing_rebound") + continue + cid = uri_cid(sg["uri"]) + if cid is None: + continue + if n in seqs and seqs[n][0] != cid and MUTATE != "rebind-sequence": + raise Refusal("playlist_listing_rebound") + seqs[n] = (cid, rev) + keep = (lambda last: True) if MUTATE == "bindings-unbounded" else (lambda last: last + RETENTION > rev) + return {"base": b["base"], "uris": {k: v for k, v in uris.items() if keep(v[1])}, "seqs": {k: v for k, v in seqs.items() if keep(v[1])}, "rev": rev} + + +def cid_of(b, uri): + v = b["uris"].get(rfc3986_resolve(b["base"], uri)) + return None if v is None else v[0] + + +def xml_char(c): + o = ord(c) + return o in (9, 10, 13) or 0x20 <= o <= 0xD7FF or 0xE000 <= o <= 0xFFFD or 0x10000 <= o <= 0x10FFFF + + +def esc_xml(s): + """An attribute value: & < > " as entities, tab, LF and CR as character references; + Refusal for a character that is not an XML Char.""" + out = [] + for c in s: + if c in '&<>"\t\n\r': + out.append({"&": "&", "<": "<", ">": ">", '"': """, "\t": " ", "\n": " ", "\r": " "}[c]) + elif xml_char(c) or MUTATE == "xml-char-unchecked": + out.append(c) + else: + raise Refusal("playlist_malformed") + return "".join(out) + + +# The characters of Unicode's White_Space property: an MPD @id holds none. +WHITE_SPACE = set("\t\n\x0b\x0c\r \x85\xa0\u1680\u2028\u2029\u202f\u205f\u3000") | {chr(c) for c in range(0x2000, 0x200B)} + + +def id_ok(s): + return s != "" and not any(c in WHITE_SPACE for c in s) + + +def xs_seconds(ms): + return "PT%dS" % (ms // 1000) if ms % 1000 == 0 else "PT%sS" % secs(ms) + + +def wai_mpd(t, set_id, union, floor, rep_id, bandwidth, media): + """wai.mpd (§11.1) on the timeline t = (period id or None, start ms, timescale, + [(t, d)]); media[i] None leaves segment i out (a gap).""" + period_id, start_ms, ts, segs = t + if not segs or ts == 0 or len(media) != len(segs) or union[0] != floor[0]: + raise Refusal("playlist_malformed") + if not id_ok(rep_id) or (period_id is not None and not id_ok(period_id)): + raise Refusal("playlist_malformed") + offered = [(sg, m) for sg, m in zip(segs, media) if m is not None] + if not offered: + raise Refusal("playlist_malformed") + to_ms = lambda d: min(d * 1000 // ts, U64) + total = min(sum(to_ms(d) for _, d in segs), U64) + longest = max(to_ms(d) for _, d in segs) + period = (' ' % (esc_xml(period_id), xs_seconds(start_ms))) if period_id is not None else (' ' % xs_seconds(start_ms)) + out = ['', + '' % (secs(total), -(-longest // 1000)), + period, + ' ' % (set_id, esc_xml(render(union))), + ' ', + ' ' % esc_xml(render(floor)), + ' ' % (esc_xml(rep_id), bandwidth), + ' ' % ts, ' '] + out += [' ' % sg for sg, _ in offered] + out += [' '] + [' ' % esc_xml(m) for _, m in offered] + out += [' ', ' ', ' ', ' ', ''] + return "\n".join(out) + "\n" + + +# ---------------------------------------------- reading an MPD (Appendix I) + +MAX_TIMELINES = 256 +MAX_DEPTH = 64 +NAME = re.compile(r"[A-Za-z_:][A-Za-z0-9_:.\-]*", re.ASCII) + + +class Xml: + """A reader of the XML subset of Appendix I over a normalised text.""" + + def __init__(self, s): + self.s, self.at = s, 0 + + def bad(self): + return Refusal("playlist_malformed") + + def rest(self): + return self.s[self.at:] + + def eat(self, lit): + if self.s.startswith(lit, self.at): + self.at += len(lit) + return True + return False + + def space(self): + n = 0 + while self.at + n < len(self.s) and self.s[self.at + n] in " \t\n": + n += 1 + self.at += n + return n > 0 + + def name(self): + m = NAME.match(self.s, self.at) + if m is None: + raise self.bad() + self.at = m.end() + return m.group(0) + + def reference(self): + end = self.s.find(";", self.at) + if end < 0: + raise self.bad() + body = self.s[self.at:end] + named = {"amp": "&", "lt": "<", "gt": ">", "quot": '"', "apos": "'"} + if body in named: + c = named[body] + else: + if body.startswith("#x") and re.fullmatch(r"[0-9A-Fa-f]{1,8}", body[2:], re.ASCII): + n = int(body[2:], 16) + elif body.startswith("#") and re.fullmatch(r"[0-9]{1,10}", body[1:], re.ASCII) and int(body[1:]) <= 0xFFFFFFFF: + n = int(body[1:]) + else: + raise self.bad() + if 0xD800 <= n <= 0xDFFF or n > 0x10FFFF or not xml_char(chr(n)): + raise self.bad() + c = chr(n) + self.at = end + 1 + return c + + def attr_value(self): + if self.at >= len(self.s) or self.s[self.at] not in "\"'": + raise self.bad() + q = self.s[self.at] + self.at += 1 + out = [] + while True: + if self.at >= len(self.s): + raise self.bad() + c = self.s[self.at] + self.at += 1 + if c == q: + return "".join(out) + if c == "<": + raise self.bad() + if c == "&": + out.append(self.reference()) + elif c in "\t\n": + out.append(" ") + else: + out.append(c) + + def comment(self): + self.at += 4 + end = self.s.find("--", self.at) + if end < 0 or not self.s.startswith("-->", end): + raise self.bad() + self.at = end + 3 + + def misc(self): + while True: + if self.space(): + continue + if self.s.startswith(" + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/pack_verify.py b/wai/pack-conformance/pack_verify.py index 8008ab14f..656cee808 100644 --- a/wai/pack-conformance/pack_verify.py +++ b/wai/pack-conformance/pack_verify.py @@ -2199,7 +2199,9 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): fb, fa = rates([(len(fr), pc["duration"]) for (_, fr), pc in zip(frags, pieces)]) head = ["#EXTM3U", "#EXT-X-VERSION:7", "#EXT-X-INDEPENDENT-SEGMENTS"] inf = f'#EXT-X-STREAM-INF:BANDWIDTH={fb},AVERAGE-BANDWIDTH={fa},CODECS="{F}",RESOLUTION={FW}x{FH}' - out["hls/classic/master.m3u8"] = text(head + [inf, f"floor-{c}.m3u8"]) + # The floor variant's URI resolves from hls/classic/ and from every class's directory + # alike, so a class's master is the classical one with at most one line added. + out["hls/classic/master.m3u8"] = text(head + [inf, f"../classic/floor-{c}.m3u8"]) media = ["#EXTM3U", "#EXT-X-VERSION:7", f"#EXT-X-TARGETDURATION:{tgt}", "#EXT-X-MEDIA-SEQUENCE:0", "#EXT-X-PLAYLIST-TYPE:VOD", "#EXT-X-INDEPENDENT-SEGMENTS"] lines = media + [f'#EXT-X-MAP:URI="../../floor/{c}/init.mp4"'] @@ -2273,10 +2275,12 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): base = next(e for e in E[p0:p0 + pc["n_entries"]] if e["role"] == BASE) if row[k] == 0xFF: lines.append("#EXT-X-GAP") - lines += [f"#EXTINF:{ms_text(pc['duration'], num, den)},", - f"../../objects/{base['hash'].hex()}.wai#n={base['byte_len']}"] + # 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] + 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 + [ f' .m3u8`: the classical -//! presentation, the floor alone; it names nothing of WAI. -//! - `hls//master.m3u8`: the classical presentation with, for a class offered WAI +//! presentation, the floor alone; it names nothing of WAI. Its variant is +//! `../classic/floor-.m3u8`, which resolves to the same playlist from every +//! class's directory, so a class's master is this one byte for byte. +//! - `hls//master.m3u8`: the classical master with, for a class offered WAI //! objects, the one line http-delivery §11.1 lets a publisher add, naming -//! `wai.m3u8`. +//! `wai.m3u8`, and no other change (http-delivery §11.1, §11.8). //! - `hls//wai.m3u8`: the class's WAI playlist (http-delivery §11.1): its class and //! stage view as `EXT-X-SESSION-DATA`, and one variant whose `CODECS` is the union and //! `X-WAI-FLOOR` the floor (http-delivery §4.2) of the objects it lists, with //! `BANDWIDTH` the peak and `AVERAGE-BANDWIDTH` the mean of `8 · bytes / duration`. //! - `hls//wai-.m3u8`: the WAI media playlist, adjacent to the floor's -//! (http-delivery §11.2): a piece's base as its segment, each layer the class lists as a -//! `#WAI-LAYER` line (http-delivery §10.4), and a piece the class takes as floor only -//! marked `#EXT-X-GAP`, so the sink takes the floor's segment there. +//! (http-delivery §11.2), rendered from the floor playlist and the objects by +//! `wai::http_binding::hls::wai_media_playlist`: a piece's base as its segment, each +//! layer the class lists as a `#WAI-LAYER` line after the segment's `EXTINF` +//! (http-delivery §10.4), and a piece the class takes as floor only marked +//! `#EXT-X-GAP`, so the sink takes the floor's segment there. //! - `dash/classic.mpd` and `dash/.mpd`: the same as two MPDs (http-delivery //! §11.1): the classical one, and the class's WAI MPD with `EssentialProperty` and the //! floor in a `SupplementalProperty`, one Representation per layer, each layer's @@ -30,14 +34,21 @@ //! milliseconds, written `S.mmm`; a target duration is the longest piece in whole //! seconds, rounded up. Rates: `ceil(8 · bytes · num / (d · den))` bits per second. //! +//! Every HLS and DASH file is written by the one HLS and the one DASH renderer of +//! `wai-rs` (`wai::hls_render::render_hls_with`, `wai::http_binding::hls::{ +//! wai_media_playlist, render_multivariant}`, `wai::http_binding::dash::render_mpd`); +//! this module says what each holds. +//! //! [packaging]: ../../../extensions/packaging.md //! [http-delivery]: ../../../extensions/http-delivery.md use crate::policy::Timebase; use serde_json::{json, Value}; use std::collections::BTreeMap; -use std::fmt::Write as _; +use wai::hls_render::{render_hls_with, HlsRender, PlaylistType, Segment, SegmentKind}; use wai::http_binding::codecs::{self, CodecsError, SeqObject}; +use wai::http_binding::dash::{self, render_mpd, AdaptationSet, Descriptor, Mpd, Representation}; +use wai::http_binding::hls::{self, parse_media_playlist, render_multivariant, Multivariant, SessionData, StreamInf, WaiLayer, WaiSegment}; use wai::stage_index::{EntryRec, Role, StageIndex, FLOOR_ONLY, TIER_DECODE, TIER_ENTROPY}; /// The SESSION-DATA identifier of a class's id. @@ -45,10 +56,10 @@ pub const DATA_ID_CLASS: &str = "science.transaction.wai.class"; /// The SESSION-DATA identifier of a class's stage view. pub const DATA_ID_STAGES: &str = "science.transaction.wai.stages"; /// http-delivery §11.1's SESSION-DATA identifier of the WAI playlist. -pub const DATA_ID_MULTIVARIANT: &str = "science.transaction.wai.multivariant"; +pub const DATA_ID_MULTIVARIANT: &str = hls::SESSION_DATA_ID; /// http-delivery §11.1's DASH schemes. -pub const SCHEME_HTTP: &str = "tag:transaction.science,2026:wai/http-delivery"; -pub const SCHEME_FLOOR: &str = "tag:transaction.science,2026:wai/floor"; +pub const SCHEME_HTTP: &str = dash::SCHEME_DELIVERY; +pub const SCHEME_FLOOR: &str = dash::SCHEME_FLOOR; /// The DASH schemes naming a class and its stage view. pub const SCHEME_CLASS: &str = "tag:transaction.science,2026:wai/class"; pub const SCHEME_STAGES: &str = "tag:transaction.science,2026:wai/stages"; @@ -98,6 +109,12 @@ impl From for CatalogError { } } +impl From for CatalogError { + fn from(e: hls::PlaylistError) -> Self { + CatalogError(e.to_string()) + } +} + /// `floor(units · den · 1000 / num)` milliseconds as `S.mmm`. pub fn seconds(units: u64, tb: Timebase) -> String { let ms = crate::plan::units_to_ms(units, tb); @@ -182,44 +199,66 @@ impl CompView<'_> { } } -fn media_head(ix: &StageIndex, tb: Timebase) -> String { - format!("#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-TARGETDURATION:{}\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXT-X-INDEPENDENT-SEGMENTS\n", target_duration(ix, tb)) +/// A piece's duration in milliseconds. +fn piece_ms(v: &CompView, p: usize) -> u64 { + crate::plan::units_to_ms(v.index.pieces[p].duration as u64, v.tb) +} + +/// The header every media playlist of a package carries: version 7, the target +/// duration, media sequence 0, on demand, independent segments. +fn media_render(v: &CompView, map_uri: Option) -> HlsRender { + HlsRender { + version: 7, + target_duration_s: Some(target_duration(v.index, v.tb)), + playlist_type: Some(PlaylistType::Vod), + independent_segments: true, + map_uri, + ..HlsRender::legacy() + } } /// `hls/classic/floor-.m3u8`. pub fn render_floor_media(v: &CompView) -> String { let f = v.floor.as_ref().expect("a floor"); - let mut o = media_head(v.index, v.tb); - let _ = writeln!(o, "#EXT-X-MAP:URI=\"../../{}\"", f.init); - for (p, (path, _)) in v.index.pieces.iter().zip(&f.fragments) { - let _ = writeln!(o, "#EXTINF:{},\n../../{path}", seconds(p.duration as u64, v.tb)); - } - o.push_str("#EXT-X-ENDLIST\n"); - o + let segments: Vec = f.fragments.iter().enumerate() + .map(|(p, (path, _))| Segment { index: p as u64, kind: SegmentKind::Content, uri: format!("../../{path}"), duration_ms: piece_ms(v, p), ad_id: None }) + .collect(); + render_hls_with(&segments, &media_render(v, Some(format!("../../{}", f.init))), &[]) } -fn floor_variant(v: &CompView, uri: &str) -> String { +/// The floor's variant, as every master names it: `../classic/floor-.m3u8`, the +/// same URI from `hls/classic/` and from a class's directory. +fn floor_variant(v: &CompView) -> StreamInf { let f = v.floor.as_ref().expect("a floor"); let (peak, mean) = v.floor_rates(); - format!("#EXT-X-STREAM-INF:BANDWIDTH={peak},AVERAGE-BANDWIDTH={mean},CODECS=\"{}\",RESOLUTION={}x{}\n{uri}\n", f.codec, f.w, f.h) + StreamInf { + uri: format!("../classic/floor-{}.m3u8", v.id()), + bandwidth: peak, + average_bandwidth: Some(mean), + codecs: f.codec.clone(), + wai_floor: None, + resolution: Some((f.w, f.h)), + frame_rate_milli: None, + } } -const MASTER_HEAD: &str = "#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-INDEPENDENT-SEGMENTS\n"; +fn master(session_data: Vec, variants: Vec) -> Result { + Ok(render_multivariant(&Multivariant { version: Some(7), independent_segments: true, session_data, variants })?) +} /// `hls/classic/master.m3u8`. pub fn render_classic_master(v: &CompView) -> String { - format!("{MASTER_HEAD}{}", floor_variant(v, &format!("floor-{}.m3u8", v.id()))) + master(Vec::new(), vec![floor_variant(v)]).expect("a floor variant renders") } -/// `hls//master.m3u8`: the classical presentation, and the line naming the class's +/// `hls//master.m3u8`: the classical master, and the line naming the class's /// WAI playlist when it has one. pub fn render_class_master(v: &CompView, c: usize) -> Result { - let mut o = MASTER_HEAD.to_string(); - if v.wai_codecs(c)?.is_some() { - let _ = writeln!(o, "#EXT-X-SESSION-DATA:DATA-ID=\"{DATA_ID_MULTIVARIANT}\",VALUE=\"wai.m3u8\""); - } - o.push_str(&floor_variant(v, &format!("../classic/floor-{}.m3u8", v.id()))); - Ok(o) + let data = match v.wai_codecs(c)? { + Some(_) => vec![SessionData::Value { data_id: DATA_ID_MULTIVARIANT.into(), value: "wai.m3u8".into() }], + None => Vec::new(), + }; + master(data, vec![floor_variant(v)]) } /// `hls//wai.m3u8`, or `None` when the class lists no WAI object. @@ -227,108 +266,124 @@ pub fn render_wai_multivariant(v: &CompView, c: usize) -> Result, let Some((union, floor)) = v.wai_codecs(c)? else { return Ok(None) }; let (peak, mean) = v.wai_rates(c); let class = v.class_id(c); - let mut o = MASTER_HEAD.to_string(); - let _ = writeln!(o, "#EXT-X-SESSION-DATA:DATA-ID=\"{DATA_ID_CLASS}\",VALUE=\"{class}\""); - let _ = writeln!(o, "#EXT-X-SESSION-DATA:DATA-ID=\"{DATA_ID_STAGES}\",URI=\"../../stages/{class}.json\""); - let _ = writeln!(o, "#EXT-X-STREAM-INF:BANDWIDTH={peak},AVERAGE-BANDWIDTH={mean},CODECS=\"{union}\",X-WAI-FLOOR=\"{floor}\",RESOLUTION={}x{}", v.w, v.h); - let _ = writeln!(o, "wai-{}.m3u8", v.id()); - Ok(Some(o)) + let data = vec![ + SessionData::Value { data_id: DATA_ID_CLASS.into(), value: class.into() }, + SessionData::Uri { data_id: DATA_ID_STAGES.into(), uri: format!("../../stages/{class}.json") }, + ]; + let variant = StreamInf { + uri: format!("wai-{}.m3u8", v.id()), + bandwidth: peak, + average_bandwidth: Some(mean), + codecs: union, + wai_floor: Some(floor), + resolution: Some((v.w, v.h)), + frame_rate_milli: None, + }; + Ok(Some(master(data, vec![variant])?)) } -fn object_uri(prefix: &str, e: &EntryRec) -> String { - format!("{prefix}objects/{}.wai#n={}", crate::hex(&e.content_hash), e.byte_len) +fn object(v: &CompView, e: &EntryRec) -> Result, CatalogError> { + v.objects.get(&e.content_hash).cloned().ok_or_else(|| CatalogError(format!("object {} is not held", crate::hex(&e.content_hash)))) } -/// `hls//wai-.m3u8`. -pub fn render_wai_media(v: &CompView, c: usize) -> String { - let mut o = media_head(v.index, v.tb); - for (p, piece) in v.index.pieces.iter().enumerate() { - let entries = v.index.piece_entries(p); - let base = entries.iter().find(|e| e.role == Role::Base).expect("a piece has a base"); - if v.index.plan[c][p] == FLOOR_ONLY { - o.push_str("#EXT-X-GAP\n"); +/// `hls//wai-.m3u8`: `wai_media_playlist` over the floor playlist, each piece's +/// base as its segment (a gap where the class takes the floor), with the layers it +/// lists. +pub fn render_wai_media(v: &CompView, c: usize) -> Result { + let floor = parse_media_playlist(&render_floor_media(v))?; + let mut segs = Vec::with_capacity(v.index.pieces.len()); + for p in 0..v.index.pieces.len() { + let base = v.index.piece_entries(p).iter().find(|e| e.role == Role::Base).expect("a piece has a base"); + let mut seg = WaiSegment::of(&object(v, base)?, piece_ms(v, p)).map_err(|e| CatalogError(e.to_string()))?; + if seg.n != base.byte_len { + return Err(CatalogError(format!("object {} is not {} bytes", crate::hex(&base.content_hash), base.byte_len))); } - let _ = writeln!(o, "#EXTINF:{},\n{}", seconds(piece.duration as u64, v.tb), object_uri("../../", base)); + 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 _ = writeln!(o, "#WAI-LAYER:N={},URI=\"{}\"", e.layer, object_uri("../../", e)); + seg.layers.push(WaiLayer::of(&object(v, e)?).map_err(|r| CatalogError(r.to_string()))?); } + segs.push(seg); } - o.push_str("#EXT-X-ENDLIST\n"); - o + let (_, _, _, text) = hls::wai_media_playlist(&floor, &segs, None, "../../objects/", true)?; + Ok(text) } -fn xml_head(duration: &str, min_buffer: u64) -> String { - format!( - "\n\n \n" - ) -} - -const XML_TAIL: &str = " \n\n"; - fn total_units(ix: &StageIndex) -> u64 { ix.pieces.iter().map(|p| p.duration as u64).sum() } -/// A `SegmentTimeline` over the pieces `ps`, each with its `t`. A time past `2^64 − 1` -/// has no timeline. -fn timeline(ix: &StageIndex, ps: &[usize], tb: Timebase, indent: &str) -> Result { - let mut o = format!("{indent}\n"); - for &p in ps { - let piece = &ix.pieces[p]; - let t = (piece.start - ix.pieces[0].start).checked_mul(tb.den as u64).ok_or_else(|| CatalogError("a segment time past 2^64 - 1".into()))?; - let _ = writeln!(o, "{indent} ", piece.duration as u64 * tb.den as u64); - } - let _ = writeln!(o, "{indent}"); - Ok(o) +/// The timeline over the pieces `ps`: each `t` from the first piece's start and its `d`, +/// in units of the timebase's numerator, multiplied checked (a time past 2^64 − 1 has +/// no timeline). +fn timeline(ix: &StageIndex, ps: &[usize], tb: Timebase) -> Result, CatalogError> { + let spans: Vec<(u64, u64)> = ps.iter().map(|&p| (ix.pieces[p].start, ix.pieces[p].duration as u64)).collect(); + Ok(dash::scaled_timeline(&spans, ix.pieces[0].start, tb.den as u64)?) +} + +fn mpd(v: &CompView, set: AdaptationSet) -> Result { + Ok(render_mpd(&Mpd { + duration_ms: crate::plan::units_to_ms(total_units(v.index), v.tb), + min_buffer_s: target_duration(v.index, v.tb), + period_id: Some("p0".into()), + period_start_ms: 0, + adaptation_sets: vec![set], + })?) } /// `dash/classic.mpd`. pub fn render_dash_classic(v: &CompView) -> Result { let f = v.floor.as_ref().expect("a floor"); let (peak, _) = v.floor_rates(); - let mut o = xml_head(&seconds(total_units(v.index), v.tb), target_duration(v.index, v.tb)); - let _ = writeln!(o, " ", f.codec, f.w, f.h); - let _ = writeln!(o, " ", v.id()); - let _ = writeln!(o, " ", v.tb.num); - let _ = writeln!(o, " ", f.init); let all: Vec = (0..v.index.pieces.len()).collect(); - o.push_str(&timeline(v.index, &all, v.tb, " ")?); - for (path, _) in &f.fragments { - let _ = writeln!(o, " "); - } - o.push_str(" \n \n \n"); - o.push_str(XML_TAIL); - Ok(o) + let rep = Representation { + id: format!("{}-floor", v.id()), + dependency_id: None, + bandwidth: peak, + timescale: v.tb.num as u64, + initialization: Some(format!("../{}", f.init)), + timeline: timeline(v.index, &all, v.tb)?, + media: f.fragments.iter().map(|(path, _)| format!("../{path}")).collect(), + }; + let set = AdaptationSet { + id: 1, content_type: Some("video".into()), mime_type: "video/mp4".into(), codecs: f.codec.clone(), size: Some((f.w, f.h)), + segment_alignment: true, start_with_sap: Some(1), essential: Vec::new(), supplemental: Vec::new(), representations: vec![rep], + }; + mpd(v, set) } /// `dash/.mpd`, or `None` when the class lists no WAI object. 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 mut o = xml_head(&seconds(total_units(v.index), v.tb), target_duration(v.index, v.tb)); - let _ = writeln!(o, " ", v.w, v.h); - let _ = writeln!(o, " "); - let _ = writeln!(o, " "); - let _ = writeln!(o, " "); - let _ = writeln!(o, " "); 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 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))) .collect(); let (peak, _) = rates(&ps.iter().map(|(p, e)| (e.byte_len, v.index.pieces[*p].duration as u64)).collect::>(), v.tb); - let dep = if k > 0 { format!(" dependencyId=\"{}-l{}\"", v.id(), k - 1) } else { String::new() }; - let _ = writeln!(o, " ", v.id()); - let _ = writeln!(o, " ", v.tb.num); - o.push_str(&timeline(v.index, &ps.iter().map(|(p, _)| *p).collect::>(), v.tb, " ")?); - for (_, e) in &ps { - let _ = writeln!(o, " ", crate::hex(&e.content_hash)); - } - o.push_str(" \n \n"); + reps.push(Representation { + id: format!("{}-l{k}", v.id()), + dependency_id: (k > 0).then(|| format!("{}-l{}", v.id(), k - 1)), + bandwidth: peak, + timescale: v.tb.num as u64, + initialization: None, + timeline: timeline(v.index, &ps.iter().map(|(p, _)| *p).collect::>(), v.tb)?, + media: ps.iter().map(|(_, e)| format!("../objects/{}.wai", crate::hex(&e.content_hash))).collect(), + }); } - o.push_str(" \n"); - o.push_str(XML_TAIL); - Ok(Some(o)) + let set = AdaptationSet { + id: 1, content_type: Some("video".into()), mime_type: wai::http_binding::MEDIA_TYPE.into(), codecs: union, size: Some((v.w, v.h)), + segment_alignment: false, start_with_sap: None, + essential: vec![Descriptor { scheme: SCHEME_HTTP.into(), value: None }], + supplemental: vec![ + Descriptor { scheme: SCHEME_FLOOR.into(), value: Some(floor) }, + Descriptor { scheme: SCHEME_CLASS.into(), value: Some(class.into()) }, + Descriptor { scheme: SCHEME_STAGES.into(), value: Some(format!("../stages/{class}.json")) }, + ], + representations: reps, + }; + Ok(Some(mpd(v, set)?)) } fn tier_label(t: u8) -> Option<&'static str> { @@ -458,7 +513,7 @@ pub fn render_all(v: &CompView, layer_lag_ms: &[u64], default_deadline: u64, pol out.insert(format!("hls/{class}/master.m3u8"), render_class_master(v, c)?.into_bytes()); if let Some(m) = render_wai_multivariant(v, c)? { out.insert(format!("hls/{class}/wai.m3u8"), m.into_bytes()); - out.insert(format!("hls/{class}/wai-{id}.m3u8"), render_wai_media(v, c).into_bytes()); + out.insert(format!("hls/{class}/wai-{id}.m3u8"), render_wai_media(v, c)?.into_bytes()); } if let Some(d) = render_dash_class(v, c)? { out.insert(format!("dash/{class}.mpd"), d.into_bytes()); @@ -496,7 +551,7 @@ 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_eq!(v.wai_codecs(0).unwrap().unwrap(), ("wai1.video.int_motion+video.int_refine".to_string(), "wai1.video.int_motion+video.int_refine".to_string())); - let media = render_wai_media(&v, 0); + let media = render_wai_media(&v, 0).unwrap(); let layers: Vec<&str> = media.lines().filter(|l| l.starts_with("#WAI-LAYER")).collect(); assert_eq!(layers.len(), 2); for (i, l) in layers.iter().enumerate() { @@ -504,11 +559,20 @@ mod tests { assert_eq!(t.n as usize, i + 1); assert!(t.uri.starts_with("../../objects/") && t.uri.contains(".wai#n=")); } - let m1 = render_wai_media(&v, 1); + let m1 = render_wai_media(&v, 1).unwrap(); assert_eq!(m1.lines().filter(|l| l.starts_with("#WAI-LAYER")).count(), 1, "class 1 lists layer 1 of piece 0 only"); assert_eq!(m1.lines().filter(|l| *l == "#EXT-X-GAP").count(), 1, "and piece 1 as floor only"); let extinf = |s: &str| s.lines().filter(|l| l.starts_with("#EXTINF") || l.starts_with("#EXT-X-TARGETDURATION") || l.starts_with("#EXT-X-MEDIA-SEQUENCE")).map(str::to_string).collect::>(); assert_eq!(extinf(&media), extinf(&render_floor_media(&v)), "adjacent: the same sequence and durations to the millisecond"); + // The layers sit after their segment's EXTINF, where http-delivery's reader finds + // them; the WAI playlist is adjacent to the floor's; a class's master is the + // classical master with the one line http-delivery §11.1 allows. + let read = wai::http_binding::hls::parse_media_playlist(&media).unwrap(); + assert_eq!(read.segments[0].extra.layers.len(), 2); + assert!(read.is_rendered_form(&media)); + wai::http_binding::hls::check_adjacent(&media, &render_floor_media(&v)).unwrap(); + wai::http_binding::hls::check_adjacent(&m1, &render_floor_media(&v)).unwrap(); + wai::http_binding::hls::classical_unchanged(&render_classic_master(&v), &render_class_master(&v, 0).unwrap()).unwrap(); let dash = render_dash_class(&v, 0).unwrap().unwrap(); assert!(dash.contains(" ExitCode { eprintln!("error: {msg}"); diff --git a/wai/wai-rs/src/bin/wai_cli/http.rs b/wai/wai-rs/src/bin/wai_cli/http.rs index 0fd7fee3e..1b0da2942 100644 --- a/wai/wai-rs/src/bin/wai_cli/http.rs +++ b/wai/wai-rs/src/bin/wai_cli/http.rs @@ -25,6 +25,24 @@ //! answers 400 to a //! path segment that is empty, `.` or `..`, or holds `\`, `:`, NUL or a control //! byte +//! wai http hls --objects --out [--master wai.m3u8] +//! wai http mpd --objects --out [--period-id ID] [--set-id N] +//! wai http seal-playlist --key [--signer-id ID] [--content-id ID] +//! [--parent ] [--out ] +//! wai http check-playlist --receipt [--parent ] +//! [--classical ] +//! WAI playlists beside a classical presentation, and their receipts (§8.6, §11; +//! wai_cli/http_playlists.rs) +//! wai http origin … --live-edge PATH=REVISION,REVISION,… [--live-step-ms MS] +//! [--live-swap ALIAS=OTHER]… +//! replay a live edge (§11.4): revision k of the playlist served at PATH is current +//! from k·MS after the start (1000 by default); a blocking reload (`_HLS_msn`, +//! `_HLS_part`) is held until a revision holds that segment or part (and answered +//! 400 when `_HLS_msn` is more than two past the current revision's last segment, +//! `_HLS_part` comes without it, or either is not a number), a part's +//! alias until the revision that first lists it with its cid, which names the +//! object served; what no revision lists is answered when the last is current (an +//! alias with 404). --live-swap serves OTHER's object under ALIAS (a test aid) //! wai http vectors gen|verify|diff … //! the corpus tool (as `wai_http_vectors`) //! wai http bench --url http://HOST:PORT/PATH[#frag] --runs N --advertise CAP… --out F @@ -71,7 +89,7 @@ fn positional(args: &[String], flags: &[&str]) -> Vec { } fn usage() -> String { - "usage: wai http publish … --out \n wai http codecs …\n wai http origin --root [--port P] [--delay-range FIRST-LAST:MS]\n wai http vectors gen|verify|diff …\n wai http bench --url URL --runs N --advertise CAP [--advertise CAP]… --out F".into() + "usage: wai http publish … --out \n wai http codecs …\n wai http origin --root [--port P] [--delay-range FIRST-LAST:MS]\n wai http hls --objects --out [--master NAME]\n wai http mpd --objects --out [--period-id ID] [--set-id N]\n wai http seal-playlist --key [--signer-id ID] [--content-id ID] [--parent ] [--out ]\n wai http check-playlist --receipt [--parent ] [--classical ]\n wai http vectors gen|verify|diff …\n wai http bench --url URL --runs N --advertise CAP [--advertise CAP]… --out F".into() } pub fn main(args: &[String]) -> Result { @@ -81,6 +99,10 @@ pub fn main(args: &[String]) -> Result { Some("origin") => origin_cmd(&args[1..]).map(|()| true), Some("vectors") => crate::http_vectors::main(&args[1..]), Some("bench") => bench(&args[1..]).map(|()| true), + Some("hls") => crate::http_playlists::hls(&args[1..]).map(|()| true), + Some("mpd") => crate::http_playlists::mpd(&args[1..]).map(|()| true), + Some("seal-playlist") => crate::http_playlists::seal(&args[1..]).map(|()| true), + Some("check-playlist") => crate::http_playlists::check(&args[1..]), _ => Err(usage()), } } @@ -247,6 +269,139 @@ struct OriginConfig { lifetime: Duration, active: std::sync::Arc, peers: std::sync::Arc>>, + /// A live edge (§11.4), when `--live-edge` gives one. + live_edge: Option>, +} + +/// A live edge (http-delivery §11.4) replayed from a playlist's revisions: revision `k` +/// is current from `k · step` after the origin started. The playlist is served at `path` +/// as its current revision; a blocking reload (`_HLS_msn`, `_HLS_part`) is held until a +/// revision holds that segment or part (or a later one), and a request for a part's alias +/// until the revision that first lists it with its cid, then answered with the object +/// that cid names. What no revision ever lists is answered once the last revision is +/// current: the playlist as it ends, an alias with 404. +struct LiveEdge { + path: String, + dir: String, + revisions: Vec<(Vec, wai::http_binding::hls::MediaPlaylist)>, + step: Duration, + start: Instant, + objects: std::collections::HashMap<[u8; 32], PathBuf>, + /// Aliases served with another alias's object (a test aid: bytes the cid refuses). + swap: Vec<(String, String)>, +} + +impl LiveEdge { + fn open(root: &std::path::Path, spec: &str, step_ms: u64, swaps: &[String]) -> Result { + let (path, revs) = spec.split_once('=').ok_or("--live-edge PATH=REVISION,REVISION,…")?; + let path = path.trim_start_matches('/').to_string(); + let dir = path.rsplit_once('/').map(|(d, _)| format!("{d}/")).unwrap_or_default(); + let mut revisions = Vec::new(); + for r in revs.split(',') { + let bytes = std::fs::read(root.join(r.trim_start_matches('/'))).map_err(|e| format!("{r}: {e}"))?; + let text = String::from_utf8(bytes.clone()).map_err(|_| format!("{r}: not UTF-8"))?; + let p = wai::http_binding::hls::parse_media_playlist(&text).map_err(|e| format!("{r}: {e}"))?; + revisions.push((bytes, p)); + } + if revisions.is_empty() { + return Err("--live-edge names no revision".into()); + } + let mut objects = std::collections::HashMap::new(); + let mut stack = vec![root.to_path_buf()]; + while let Some(d) = stack.pop() { + for e in std::fs::read_dir(&d).map_err(|e| format!("{}: {e}", d.display()))?.flatten() { + let p = e.path(); + if p.is_dir() { + stack.push(p); + } else if let Some(cid) = p.file_name().and_then(|n| n.to_str()).and_then(|n| n.strip_suffix(".wai")).and_then(unhex32) { + objects.entry(cid).or_insert(p); + } + } + } + let swap = swaps.iter().map(|s| s.split_once('=').map(|(a, b)| (a.to_string(), b.to_string())).ok_or("--live-swap ALIAS=OTHER")).collect::>()?; + Ok(LiveEdge { path, dir, revisions, step: Duration::from_millis(step_ms.max(1)), start: Instant::now(), objects, swap }) + } + + fn current(&self) -> usize { + let k = self.start.elapsed().as_millis() / self.step.as_millis().max(1); + usize::try_from(k).unwrap_or(usize::MAX).min(self.revisions.len() - 1) + } + + /// Wait until revision `k` is current, never past `until`. + fn wait_for(&self, k: usize, until: Instant) { + let at = (self.start + self.step * u32::try_from(k).unwrap_or(u32::MAX)).min(until); + let now = Instant::now(); + if at > now { + std::thread::sleep(at - now); + } + } + + /// Whether revision `k` holds segment `msn` complete, or (with `part`) that part of + /// it, or a later one; a playlist that ends holds everything it will. + fn has(&self, k: usize, msn: u64, part: Option) -> bool { + let p = &self.revisions[k].1; + let complete = p.media_sequence.saturating_add(p.segments.len() as u64); + if p.end_list || msn < complete { + return true; + } + match (part, &p.open) { + (Some(i), Some(o)) if msn == complete => (o.parts.len() as u64) > i, + _ => false, + } + } + + /// The playlist, an alias's object, `None` for what the edge does not serve, or + /// `Err` for a blocking reload the HLS second edition answers with 400: a directive + /// that is not a number, `_HLS_part` without `_HLS_msn`, or an `_HLS_msn` more than two + /// past the last segment of the current revision. + #[allow(clippy::type_complexity)] + fn serve(&self, path: &str, query: &str, until: Instant) -> Option, Vec<(&'static str, String)>), &'static str>> { + let rel = path.trim_start_matches('/'); + let last = self.revisions.len() - 1; + if rel == self.path { + let raw = |k: &str| query.split('&').find_map(|kv| kv.strip_prefix(k).and_then(|v| v.strip_prefix('='))); + let num = |k: &str| raw(k).map(|v| v.parse::().ok().filter(|_| !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit()))); + let (msn, part) = (num("_HLS_msn"), num("_HLS_part")); + if matches!(msn, Some(None)) || matches!(part, Some(None)) { + return Some(Err("a blocking-reload directive is not a decimal integer")); + } + let (msn, part) = (msn.flatten(), part.flatten()); + if part.is_some() && msn.is_none() { + return Some(Err("_HLS_part without _HLS_msn")); + } + let target = match msn { + Some(msn) => { + // RFC 8216bis §6.2.5.2: past the last segment of the current playlist + // plus two, the request is refused at once rather than held. + let p = &self.revisions[self.current()].1; + if msn > p.media_sequence.saturating_add(p.segments.len() as u64).saturating_add(1) { + return Some(Err("_HLS_msn is more than two past the last segment")); + } + (0..self.revisions.len()).find(|&k| self.has(k, msn, part)).unwrap_or(last) + } + None => 0, + }; + self.wait_for(target, until); + let k = self.current().max(target.min(last)); + return Some(Ok((self.revisions[k].0.clone(), origin::playlist_headers("application/vnd.apple.mpegurl")))); + } + let alias = rel.strip_prefix(self.dir.as_str()).filter(|a| a.ends_with(".wai"))?; + let looked = self.swap.iter().find(|(a, _)| a == alias).map_or(alias, |(_, b)| b.as_str()); + let first = self.revisions.iter().enumerate().find_map(|(k, (_, p))| wai::http_binding::hls::listed_cid(p, looked).map(|c| (k, c))); + match first { + Some((k, cid)) => { + self.wait_for(k, until); + let bytes = std::fs::read(self.objects.get(&cid)?).ok()?; + Some(Ok((bytes, origin::live_object_headers(&cid, None, self.step.as_secs().max(1) * self.revisions.len() as u64)))) + } + None if self.revisions.iter().any(|(b, _)| String::from_utf8_lossy(b).contains(&format!("URI=\"{alias}\""))) => { + // Hinted and never listed: held until the edge ends, then not found. + self.wait_for(last, until); + None + } + None => None, + } + } } /// What the origin serves at `path` under `root`: the file and its headers, by the @@ -281,6 +436,9 @@ fn resource(cfg: &OriginConfig, path: &str) -> Option<(Vec, Vec<(&'static st ("ETag", format!("\"{}\"", hex(&content_hash(&bytes)))), ("Access-Control-Allow-Origin", "*".to_string()), ] + } else if name.ends_with(".m3u8") || name.ends_with(".mpd") { + // A playlist changes at a live edge: revalidated on every use (§11). + origin::playlist_headers(if name.ends_with(".mpd") { "application/dash+xml" } else { "application/vnd.apple.mpegurl" }) } else if name.ends_with(".json") { origin::immutable_headers("application/json") } else { @@ -379,7 +537,12 @@ fn serve_one(cfg: &OriginConfig, stream: TcpStream, accepted: Instant) -> std::i if !path_ok(&req.path) { return respond(&s, until, 400, &[], b"bad path\n", req.method == "HEAD"); } - let Some((bytes, mut headers)) = resource(cfg, &req.path) else { + let live = match cfg.live_edge.as_ref().and_then(|e| e.serve(&req.path, &req.query, until)) { + Some(Err(why)) => return respond(&s, until, 400, &[("Cache-Control", "no-store".to_string())], format!("{why}\n").as_bytes(), req.method == "HEAD"), + Some(Ok(x)) => Some(x), + None => None, + }; + let Some((bytes, mut headers)) = live.or_else(|| resource(cfg, &req.path)) else { return respond(&s, until, 404, &[("Cache-Control", "no-store".to_string())], b"not found\n", req.method == "HEAD"); }; let etag = headers.iter().find(|(k, _)| *k == "ETag").map(|(_, v)| v.clone()).unwrap_or_default(); @@ -418,7 +581,15 @@ fn origin_cmd(args: &[String]) -> Result<(), String> { let lifetime = Duration::from_millis(opt(args, "--max-connection-ms").map_or(Ok(DEFAULT_MAX_CONNECTION_MS), str::parse).map_err(|e| format!("--max-connection-ms: {e}"))?); let max_per_peer = opt(args, "--max-per-peer").map(str::parse).transpose().map_err(|e| format!("--max-per-peer: {e}"))?; let active = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); - let cfg = OriginConfig { root: root.clone(), delay, live, hits, max_connections, max_per_peer, read_timeout, lifetime, active, peers: Default::default() }; + let swaps: Vec = args.iter().enumerate().filter(|(_, a)| *a == "--live-swap").filter_map(|(i, _)| args.get(i + 1).cloned()).collect(); + let live_edge = match opt(args, "--live-edge") { + Some(spec) => { + let step: u64 = opt(args, "--live-step-ms").map_or(Ok(1000), str::parse).map_err(|e| format!("--live-step-ms: {e}"))?; + Some(std::sync::Arc::new(LiveEdge::open(&root, spec, step, &swaps)?)) + } + None => None, + }; + let cfg = OriginConfig { root: root.clone(), delay, live, hits, max_connections, max_per_peer, read_timeout, lifetime, active, peers: Default::default(), live_edge }; let listener = TcpListener::bind(("127.0.0.1", port)).map_err(|e| format!("bind 127.0.0.1:{port}: {e}"))?; println!("serving {} on http://127.0.0.1:{}/", root.display(), listener.local_addr().map_err(|e| e.to_string())?.port()); for stream in listener.incoming().flatten() { diff --git a/wai/wai-rs/src/bin/wai_cli/http_playlists.rs b/wai/wai-rs/src/bin/wai_cli/http_playlists.rs new file mode 100644 index 000000000..ee62960ec --- /dev/null +++ b/wai/wai-rs/src/bin/wai_cli/http_playlists.rs @@ -0,0 +1,336 @@ +//! `wai http hls|mpd|seal-playlist|check-playlist` — WAI playlists beside a classical +//! HLS presentation (`extensions/http-delivery.md` §8.6, §11): +//! +//! wai http hls --objects --out [--master wai.m3u8] +//! write each variant's WAI media playlist, aligned with its classical media +//! playlist, and the WAI multivariant playlist; print the union and the floor +//! wai http mpd --objects --out [--classical-mpd ] +//! [--period-id ID] [--classical-set ID] [--set-id 1] +//! write wai.mpd on the classical timeline, one Representation per variant, a gap +//! left out: the classical MPD's period, timescale, t and d (its first period and +//! the first adaptation set with a timeline, or those named), checked adjacent to +//! it; with no classical MPD, the classical HLS timeline at timescale 1000 from 0 +//! wai http seal-playlist --key +//! [--signer-id ID] [--content-id ID] [--parent ] +//! [--grant ] [--out ] +//! seal a WAI playlist in its rendered form, under the publisher's grant (a JSON +//! object of grant_hash, capability, joule_ceiling_micro and funds_ceiling, each +//! ceiling a number up to 2^53 - 1 or null for none; with no --grant, a grant of +//! no ceilings); write /mr/.json +//! and /mr/.json (dir: the playlist's own by default) and +//! print its path and receipt hash +//! wai http check-playlist --receipt +//! [--parent ] [--classical ] +//! check the playlist against its receipt (and its chain to the receipt held as +//! --parent, walking any revisions between by their links from the receipt's +//! directory, and its adjacency to the classical playlist); print what held, or +//! the reason it did not +//! +//! The objects list, its paths relative to its own directory: +//! +//! { "uri_prefix": "objects/", "end_list": true, +//! "variants": [ { "uri": "wai-720.m3u8", "classical": "classical-720.m3u8", +//! "resolution": [1280, 720], "frame_rate_milli": 25000, +//! "segments": [ { "object": "a.wai", "layers": ["l1.wai"], "gap": false, +//! "parts": [ { "object": "p.wai", "alias": "live/720/1200.0.wai", +//! "independent": true, "layers": [] } ] } ], +//! "open": { "parts": [ … ], "preload_hint": "live/720/1201.2.wai" } } ] } +//! +//! A segment's and a part's duration is the classical playlist's (a part's own +//! `duration_ms` where the classical segment lists no parts); `end_list` defaults to +//! the classical playlist's. + +use std::path::{Path, PathBuf}; + +use serde_json::Value; + +use wai::http_binding::codecs::{self, SeqObject}; +use wai::http_binding::dash::{wai_mpd, DashTimeline, WaiRepresentation}; +use wai::http_binding::hls::{ + bandwidths, parse_media_playlist, wai_media_playlist, wai_multivariant, MediaPlaylist, VariantLine, WaiLayer, WaiOpen, + WaiPart, WaiSegment, +}; +use wai::http_binding::hex; + +fn opt<'a>(args: &'a [String], flag: &str) -> Option<&'a str> { + args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1).map(String::as_str)) +} + +fn read(p: &Path) -> Result, String> { + std::fs::read(p).map_err(|e| format!("{}: {e}", p.display())) +} + +fn write(p: &Path, b: &[u8]) -> Result<(), String> { + if let Some(d) = p.parent() { + std::fs::create_dir_all(d).map_err(|e| format!("{}: {e}", d.display()))?; + } + std::fs::write(p, b).map_err(|e| format!("{}: {e}", p.display())) +} + +/// One variant of an objects list, read: its classical playlist and the objects that +/// cover it, with every envelope's bytes (for the union and the floor). +struct Variant { + uri: String, + classical: MediaPlaylist, + resolution: Option<(u32, u32)>, + frame_rate_milli: Option, + segments: Vec, + open: Option, + envelopes: Vec>, +} + +struct List { + uri_prefix: String, + end_list: Option, + variants: Vec, +} + +fn strs(v: &Value) -> Vec { + v.as_array().into_iter().flatten().filter_map(|x| x.as_str().map(String::from)).collect() +} + +fn layers(dir: &Path, v: &Value, envelopes: &mut Vec>) -> Result, String> { + strs(v).iter().map(|p| { + let env = read(&dir.join(p))?; + let l = WaiLayer::of(&env).map_err(|e| format!("{p}: {e}"))?; + envelopes.push(env); + Ok(l) + }).collect() +} + +fn parts(dir: &Path, v: &Value, classical_ms: &[u64], envelopes: &mut Vec>) -> Result, String> { + v.as_array().into_iter().flatten().enumerate().map(|(j, p)| { + let path = p["object"].as_str().ok_or("a part names its object")?; + let alias = p["alias"].as_str().ok_or("a part names its alias")?; + let ms = classical_ms.get(j).copied().or(p["duration_ms"].as_u64()) + .ok_or(format!("{path}: no classical part {j} and no duration_ms"))?; + let env = read(&dir.join(path))?; + let mut part = WaiPart::of(&env, alias, ms, p["independent"].as_bool() == Some(true)).map_err(|e| format!("{path}: {e}"))?; + envelopes.push(env); + part.layers = layers(dir, &p["layers"], envelopes)?; + Ok(part) + }).collect() +} + +fn load(path: &Path) -> Result { + let dir = path.parent().unwrap_or(Path::new(".")); + let v: Value = serde_json::from_slice(&read(path)?).map_err(|e| format!("{}: {e}", path.display()))?; + let mut variants = Vec::new(); + for x in v["variants"].as_array().ok_or("the list has variants")? { + let cpath = x["classical"].as_str().ok_or("a variant names its classical playlist")?; + let ctext = String::from_utf8(read(&dir.join(cpath))?).map_err(|_| format!("{cpath}: not UTF-8"))?; + let classical = parse_media_playlist(&ctext).map_err(|e| format!("{cpath}: {e}"))?; + let mut envelopes = Vec::new(); + let mut segments = Vec::new(); + for (i, s) in x["segments"].as_array().ok_or("a variant has segments")?.iter().enumerate() { + let c = classical.segments.get(i).ok_or(format!("{cpath}: {} segments, and the list has more", classical.segments.len()))?; + let p = s["object"].as_str().ok_or("a segment names its object")?; + let env = read(&dir.join(p))?; + let mut seg = WaiSegment::of(&env, c.duration_ms).map_err(|e| format!("{p}: {e}"))?; + seg.gap = s["gap"].as_bool() == Some(true); + if !seg.gap { + envelopes.push(env); + } + seg.layers = layers(dir, &s["layers"], &mut envelopes)?; + let cms: Vec = c.extra.parts.iter().map(|q| q.duration_ms).collect(); + seg.parts = parts(dir, &s["parts"], &cms, &mut envelopes)?; + segments.push(seg); + } + let open = match &x["open"] { + Value::Null => None, + o => { + let cms: Vec = classical.open.as_ref().map(|c| c.parts.iter().map(|q| q.duration_ms).collect()).unwrap_or_default(); + Some(WaiOpen { parts: parts(dir, &o["parts"], &cms, &mut envelopes)?, preload_hint: o["preload_hint"].as_str().map(String::from) }) + } + }; + let resolution = x["resolution"].as_array().and_then(|r| Some((u32::try_from(r.first()?.as_u64()?).ok()?, u32::try_from(r.get(1)?.as_u64()?).ok()?))); + variants.push(Variant { + uri: x["uri"].as_str().ok_or("a variant names its URI")?.to_string(), + classical, + resolution, + frame_rate_milli: x["frame_rate_milli"].as_u64().and_then(|f| u32::try_from(f).ok()), + segments, + open, + envelopes, + }); + } + Ok(List { uri_prefix: v["uri_prefix"].as_str().unwrap_or("objects/").to_string(), end_list: v["end_list"].as_bool(), variants }) +} + +/// The union and floor of a variant's objects (§4.2): its segments, layers and parts. +fn union_floor(envs: &[Vec]) -> Result<(codecs::Element, codecs::Element), String> { + let objs: Vec = envs.iter().map(|e| SeqObject::from_envelope(e).map_err(|e| e.to_string())).collect::>()?; + Ok((codecs::union_of(&objs).map_err(|e| e.to_string())?, codecs::floor_of(&objs).map_err(|e| e.to_string())?)) +} + +/// The peak and average of a variant (§11.1): each segment that is not a gap with its +/// layer objects, or its parts when it has no segment yet. +fn rates(v: &Variant) -> Result<(u64, u64), String> { + let mut objs: Vec<(u64, u64)> = v.segments.iter().filter(|s| !s.gap) + .map(|s| (s.n + s.layers.iter().map(|l| l.len).sum::(), s.duration_ms)).collect(); + if objs.is_empty() { + objs = v.open.iter().flat_map(|o| o.parts.iter().map(|p| (p.n, p.duration_ms))).collect(); + } + bandwidths(&objs).ok_or(format!("{}: no object with a duration to compute a bandwidth from", v.uri)) +} + +pub fn hls(args: &[String]) -> Result<(), String> { + let list = load(Path::new(opt(args, "--objects").ok_or("hls needs --objects ")?))?; + let out = PathBuf::from(opt(args, "--out").ok_or("hls needs --out ")?); + let mut lines = Vec::new(); + for v in &list.variants { + let end = list.end_list.unwrap_or(v.classical.end_list); + let (_, _, _, text) = wai_media_playlist(&v.classical, &v.segments, v.open.as_ref(), &list.uri_prefix, end).map_err(|e| format!("{}: {e}", v.uri))?; + write(&out.join(&v.uri), text.as_bytes())?; + let (union, floor) = union_floor(&v.envelopes)?; + let (peak, avg) = rates(v)?; + println!("{}\tunion {}\tfloor {}\tbandwidth {peak}\taverage {avg}", out.join(&v.uri).display(), codecs::render(&union), codecs::render(&floor)); + lines.push(VariantLine { uri: v.uri.clone(), union, floor, peak_bps: peak, avg_bps: avg, resolution: v.resolution, frame_rate_milli: v.frame_rate_milli }); + } + let master = out.join(opt(args, "--master").unwrap_or("wai.m3u8")); + write(&master, wai_multivariant(&lines).map_err(|e| e.to_string())?.as_bytes())?; + println!("{}", master.display()); + Ok(()) +} + +pub fn mpd(args: &[String]) -> Result<(), String> { + use wai::http_binding::dash_read::check_dash_adjacent; + let list = load(Path::new(opt(args, "--objects").ok_or("mpd needs --objects ")?))?; + let out = PathBuf::from(opt(args, "--out").ok_or("mpd needs --out ")?); + let set_id: u32 = opt(args, "--set-id").unwrap_or("1").parse().map_err(|_| "--set-id is an unsigned integer")?; + let first = list.variants.first().ok_or("the list has no variant")?; + // §11.1, §11.2: the classical MPD's period, timescale, t and d; without one, the + // classical HLS timeline at timescale 1000 from 0. + let classical_mpd = opt(args, "--classical-mpd").map(|p| String::from_utf8(read(Path::new(p))?).map_err(|_| format!("{p}: not UTF-8"))).transpose()?; + let hls_timeline = |v: &Variant| DashTimeline::from_hls(&v.classical, opt(args, "--period-id").unwrap_or("p0"), 0).map_err(|e| format!("{}: {e}", v.uri)); + let timeline = match &classical_mpd { + Some(text) => { + // Expanded no further than the most segments a variant lists. + let most = list.variants.iter().map(|v| v.segments.len()).max().unwrap_or(0); + DashTimeline::from_mpd(text, opt(args, "--period-id"), opt(args, "--classical-set"), most).map_err(|e| format!("the classical MPD: {e}"))? + } + None => hls_timeline(first)?, + }; + let mut reps = Vec::new(); + let mut all = Vec::new(); + for v in &list.variants { + if v.segments.len() != timeline.segments.len() { + return Err(format!("{}: {} segments, and the classical timeline has {}", v.uri, v.segments.len(), timeline.segments.len())); + } + if classical_mpd.is_none() && hls_timeline(v)? != timeline { + return Err(format!("{}: its classical timeline is not the first variant's; one AdaptationSet shares one timeline", v.uri)); + } + let (peak, _) = rates(v)?; + let id = v.uri.trim_end_matches(".m3u8").rsplit('/').next().unwrap_or(&v.uri).to_string(); + // A gap offers no WAI object there: its S and SegmentURL are left out. + let media = v.segments.iter() + .map(|s| (!s.gap).then(|| format!("{}{}.wai#n={}{}", list.uri_prefix, hex(&s.cid), s.n, s.h.map(|h| format!("&h={h}")).unwrap_or_default()))) + .collect(); + reps.push(WaiRepresentation { id, bandwidth: peak, media }); + all.extend(v.envelopes.iter().cloned()); + } + let (union, floor) = union_floor(&all)?; + let text = wai_mpd(&timeline, set_id, &union, &floor, &reps).map_err(|e| e.to_string())?; + if let Some(c) = &classical_mpd { + check_dash_adjacent(&text, c).map_err(|e| format!("the WAI MPD is not adjacent to the classical one: {e}"))?; + } + write(&out, text.as_bytes())?; + println!("{}", out.display()); + Ok(()) +} + +/// A 32-byte seed: 64 hexadecimal digits, given or in a file. +#[cfg(feature = "video_manifest")] +fn seed(s: &str) -> Result<[u8; 32], String> { + let text = if wai::http_binding::unhex32(s.trim()).is_some() { s.to_string() } else { String::from_utf8_lossy(&read(Path::new(s))?).to_string() }; + wai::http_binding::unhex32(text.trim()).ok_or_else(|| "--key is 64 lowercase hexadecimal digits, or a file holding them".to_string()) +} + +#[cfg(feature = "video_manifest")] +pub fn seal(args: &[String]) -> Result<(), String> { + use wai::http_binding::playlist_receipt::seal_playlist; + use wai::wai_video_manifest::ManifestReceipt; + let path = args.first().filter(|a| !a.starts_with("--")).ok_or("seal-playlist needs the playlist")?; + let text = String::from_utf8(read(Path::new(path))?).map_err(|_| format!("{path}: not UTF-8"))?; + let key = ed25519_dalek::SigningKey::from_bytes(&seed(opt(args, "--key").ok_or("seal-playlist needs --key")?)?); + let parent = opt(args, "--parent").map(|p| ManifestReceipt::from_json(&String::from_utf8_lossy(&read(Path::new(p))?)).map_err(|e| format!("{p}: {e}"))).transpose()?; + let stem = Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or("playlist"); + // The publisher's grant (Appendix H's form), or none: a grant with no ceilings. + let grant = match opt(args, "--grant") { + Some(g) => { + let v: serde_json::Value = serde_json::from_slice(&read(Path::new(g))?).map_err(|e| format!("{g}: {e}"))?; + wai::wai_video_manifest::grant_from_json(&v).map_err(|e| format!("{g}: {e}"))? + } + None => wai::quantum_ops::GrantRef::unbounded("video.manifest"), + }; + let r = seal_playlist(&text, &key, opt(args, "--signer-id").unwrap_or("unconfirmed"), opt(args, "--content-id").unwrap_or(stem), + grant, parent.as_ref()).map_err(|e| format!("{path}: {e}"))?; + let dir = opt(args, "--out").map(PathBuf::from).unwrap_or_else(|| Path::new(path).parent().unwrap_or(Path::new(".")).to_path_buf()); + // §8.6: at its manifest hash, where a client checks the playlist, and at its receipt + // hash, where a client that skipped this revision fetches it by a later one's link. + let file = dir.join("mr").join(r.receipt_file_name()); + write(&file, r.to_json().as_bytes())?; + write(&dir.join("mr").join(format!("{}.json", hex(&r.receipt_hash()))), r.to_json().as_bytes())?; + println!("{}\treceipt {}", file.display(), hex(&r.receipt_hash())); + Ok(()) +} + +#[cfg(not(feature = "video_manifest"))] +pub fn seal(_: &[String]) -> Result<(), String> { + Err("built without video_manifest: sealing a playlist needs a build with the features http and video_manifest".into()) +} + +#[cfg(feature = "video_manifest")] +pub fn check(args: &[String]) -> Result { + use wai::http_binding::hls::check_adjacent; + use wai::http_binding::playlist_receipt::{check_playlist, LiveChain, Step}; + use wai::wai_video_manifest::ManifestReceipt; + let path = args.first().filter(|a| !a.starts_with("--")).ok_or("check-playlist needs the playlist")?; + let text = read(Path::new(path))?; + let rpath = opt(args, "--receipt").ok_or("check-playlist needs --receipt")?; + let name = Path::new(rpath).file_name().and_then(|n| n.to_str()); + let held = match check_playlist(&text, &read(Path::new(rpath))?, name) { + Ok(r) => r, + Err(e) => { + println!("refused\t{}", e.code()); + return Ok(false); + } + }; + let mut walked = None; + if let Some(p) = opt(args, "--parent") { + // The head the client holds; parents between it and this revision are fetched + // by their links from the receipt's own directory (§8.6). + let head = ManifestReceipt::from_json(&String::from_utf8_lossy(&read(Path::new(p))?)).map_err(|e| format!("{p}: {e}"))?; + let dir = Path::new(rpath).parent().unwrap_or(Path::new(".")).to_path_buf(); + // Lines resolve against the playlist's own path, the same for every revision. + let mut chain = LiveChain::for_playlist(path, wai::http_binding::playlist_receipt::WALK_BOUND); + chain.advance(&head, |_| None).map_err(|e| format!("{p}: {e}"))?; + match chain.advance(&held, |h| std::fs::read(dir.join(format!("{}.json", hex(h)))).ok()) { + Ok(Step::Chained { walked: n }) => walked = Some(n), + Ok(Step::Reanchored { walked: n }) => { + println!("reanchored\t{n} parents walked; the revisions between are not signed"); + walked = Some(n); + } + Ok(_) => walked = Some(0), + Err(e) => { + println!("refused\t{}", e.code()); + return Ok(false); + } + } + } + if let Some(c) = opt(args, "--classical") { + let ctext = String::from_utf8(read(Path::new(c))?).map_err(|_| format!("{c}: not UTF-8"))?; + if let Err(e) = check_adjacent(&String::from_utf8_lossy(&text), &ctext) { + println!("refused\t{}", e.code()); + return Ok(false); + } + } + let chain = walked.map(|n| format!("\tchained ({n} parents walked)")).unwrap_or_default(); + println!("signed\treceipt {}\tsigner {}{chain}\t(signer unconfirmed until anchored)", hex(&held.receipt_hash()), hex(&held.signer_pubkey)); + Ok(true) +} + +#[cfg(not(feature = "video_manifest"))] +pub fn check(_: &[String]) -> Result { + Err("built without video_manifest: checking a playlist receipt needs a build with the features http and video_manifest".into()) +} 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 338a90589..ad5b200b3 100644 --- a/wai/wai-rs/src/bin/wai_cli/http_vectors.rs +++ b/wai/wai-rs/src/bin/wai_cli/http_vectors.rs @@ -35,6 +35,9 @@ use wai::http_binding::session::{check_response, drive, FetchSession, Fetched, R use wai::http_binding::{content_hash, hex, parse_fragment, ByteRange, RangeRefusal}; use wai::pin::HeldBytes; +#[path = "http_vectors_playlists.rs"] +mod playlists; + /// The committed vectors the corpus carries objects of, relative to the corpus. const STREAM_WAI: &str = "../wai-rs/tests/vectors/int_mlicpp/stream.wai"; const MLICPP_SET: &str = "../wai-rs/tests/vectors/int_mlicpp/model.wmb"; @@ -393,7 +396,12 @@ fn selector(c: &Corpus, input: &Value) -> Result { }); if let Some(order) = input["order"].as_array() { let order: Vec = order.iter().filter_map(|v| v.as_u64().map(|x| x as usize)).collect(); - s.order = Some(Box::new(move |_| order.clone())); + s.order = Some(Box::new(move |_, _| order.clone())); + } + // A deployer policy's order (Appendix F): the policy over the state, on what the sink + // advertises. + if let Some(o) = playlists::abr_order(input, strs(&input["advertises"]))? { + s.order = Some(o); } if let Some(d) = input["declared"].as_str() { s.declared = Some(codecs::parse_element(d).map_err(|e| Stop::Refused(json!({ "error": e.code() })))?); @@ -417,11 +425,11 @@ fn head(b: &[u8]) -> Result { fn select(c: &Corpus, input: &Value) -> Result { let env = c.object(input)?; - whole_head(&env)?; + let h = whole_head(&env)?; let mut sel = selector(c, input)?; let w = WaiMulti::from_bytes(&env).map_err(|e| refusal_of(&RangeRefusal::HeadMalformed(e.to_string())))?; let order: Vec = match &sel.order { - Some(f) => f(&w.manifest), + Some(f) => f(&h, &w.manifest), None => (0..w.manifest.renditions.len()).collect(), }; let advertises = &sel.advertises; @@ -757,7 +765,10 @@ fn outcome(c: &Corpus, op: &str, input: &Value) -> Result { Ok(t) => json!({ "n": t.n, "uri": t.uri }), Err(e) => err(&e), }, - other => return Err(format!("unknown op {other}").into()), + other => match playlists::outcome(c, other, input) { + Some(r) => r?, + None => return Err(format!("unknown op {other}").into()), + }, }) } @@ -1153,7 +1164,8 @@ fn generate(dir: &Path) -> Result>, String> { let mut files = BTreeMap::new(); let mut objs = BTreeMap::new(); let mut listing = serde_json::Map::new(); - for (id, bytes, what) in objects(dir)? { + let all = objects(dir)?.into_iter().map(|(a, b, c)| (a.to_string(), b, c.to_string())).chain(playlists::objects(dir)?); + for (id, bytes, what) in all { let cid = content_hash(&bytes); let h = head(&bytes)?; let union = codecs::union_of(&[SeqObject::from_envelope(&bytes).map_err(|e| e.to_string())?]).map(|e| codecs::render(&e)).ok(); @@ -1189,8 +1201,11 @@ fn generate(dir: &Path) -> Result>, String> { for bad in ["bad/O6.wai", "bad/OK.wai", "bad/OKN.wai"] { extra.insert(bad.to_string(), files[bad].clone()); } - let list = cases(&objs, &mut extra)?; - let corpus = Corpus { dir: dir.to_path_buf(), objects: objs, extra: extra.clone() }; + let mut list = cases(&objs, &mut extra)?; + let mut corpus = Corpus { dir: dir.to_path_buf(), objects: objs, extra: extra.clone() }; + // The playlists, their receipts and their cases (§8.6, §10.4, §11, Appendix F). + let playlist_files = playlists::generate(&mut corpus)?; + list.extend(playlists::cases(&corpus)?); let mut out = Vec::new(); for c in &list { let expect = eval(&corpus, c.op, &c.input).map_err(|e| format!("{}: {e}", c.name))?; @@ -1264,6 +1279,7 @@ fn generate(dir: &Path) -> Result>, String> { files.insert(k, b); } } + files.extend(playlist_files); files.insert("cases.json".into(), json_lines(&out)); Ok(files) } @@ -1437,6 +1453,7 @@ fn security(s: &mut u64) -> Option<&'static str> { fn fuzz(dir: &Path, seed: u64, count: usize) -> Result<(Vec, usize, BTreeMap), String> { let mut s = seed; let corpus = Corpus { dir: dir.to_path_buf(), objects: BTreeMap::new(), extra: BTreeMap::new() }; + let fuzz_corpus = Corpus { dir: dir.to_path_buf(), objects: BTreeMap::new(), extra: BTreeMap::new() }; let mut out: Vec<(String, &str, Value, Value)> = Vec::new(); let mut stats: BTreeMap = BTreeMap::new(); let mut push = |name: String, op: &'static str, input: Value| -> Result<(), String> { @@ -1677,6 +1694,11 @@ fn fuzz(dir: &Path, seed: u64, count: usize) -> Result<(Vec, usize, BTreeMap push(format!("fuzz_whole_refine_{i}"), "fetch", json!({ "envelope_hex": hex(&raw_envelope(true, &m4, &[nb, nr])), "advertises": ["wai.video.av1"], "applies": ["wai.video.int_refine"], "threshold_permille": permille }))?; } + // The playlist side: playlists read, held adjacent, the classical master left + // unchanged, renderings, adaptation choices, the integer log, receipt files. + for (name, op, input) in playlists::fuzz_cases(&fuzz_corpus, &mut s, i)? { + push(name, op, input)?; + } } let n = out.len(); Ok((json_lines(&out), n, stats)) diff --git a/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs b/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs new file mode 100644 index 000000000..d75da24cc --- /dev/null +++ b/wai/wai-rs/src/bin/wai_cli/http_vectors_playlists.rs @@ -0,0 +1,1859 @@ +//! The playlist and adaptation side of the HTTP delivery corpus (http-delivery §8.6, +//! §10.4, §11, Appendix F): the layered, low-latency and player-fixture objects, the WAI +//! playlists rendered beside their classical playlists, their receipts, and the cases +//! that hold all of it — playlists rendered from their inputs, read, held adjacent, +//! the classical master left unchanged, receipts checked and chained, a live edge's +//! parts verified against the cids later revisions list, and the reference adaptation +//! policies' choices, orders and layer depths. + +use std::collections::BTreeMap; +use std::path::Path; + +use serde_json::{json, Value}; + +use wai::container::{Conditioning, Manifest, ModelRequirement, Wai}; +use wai::http_binding::abr::{self, AbrState, BolaLike, BufferBased, Mpc, Policy, Sample, Throughput}; +use wai::http_binding::codecs::{self, SeqObject}; +use wai::http_binding::dash::{wai_mpd, DashTimeline, WaiRepresentation}; +use wai::http_binding::head::{parse_head, HeadParse}; +use wai::http_binding::hls::{ + bandwidths, check_adjacent, classical_unchanged, listed_cid, parse_media_playlist, wai_media_playlist, wai_multivariant, + MediaPlaylist, PlaylistError, VariantLine, WaiLayer, WaiOpen, WaiPart, WaiSegment, +}; +use wai::http_binding::{content_hash, hex, parse_fragment, unhex32}; + +use super::{case, err, patch, read, refused, strs, Case, Corpus, Stop}; + +/// The staged-delivery stage set of a clip split into two bases, the second with a +/// layer: the corpus's layered sequence (O11). +const STAGED_PAIR: &str = "../staged-conformance/cases/live_multi_base"; + +/// The URL a live case's client fetches the playlist from (http-delivery §11.5: its lines +/// resolve against it); an example host, serving nothing. +const LIVE_URL: &str = "https://origin.example/live/wai-720.m3u8"; + +/// The seed of the corpus's playlist key: a published test key, attesting nothing. +pub(super) const PLAYLIST_SEED: [u8; 32] = [0x57; 32]; +/// The signer id the corpus's playlist receipts carry. +pub(super) const PLAYLIST_SIGNER: &str = "did:example:http-vectors"; + +fn int_motion(payload: Vec, w: u64, h: u64) -> Result, String> { + Wai::new( + Manifest::new("video", "replicate", ModelRequirement::new("wai.video.int_motion"), Conditioning::new("int_motion")) + .with_target(json!({ "w": w, "h": h })), + payload, + ).to_bytes().map_err(|e| e.to_string()) +} + +/// The id of live part `p` of segment `s`. +fn part_id(s: usize, p: usize) -> String { + format!("O12p{s}_{p:02}") +} + +/// The playlist side's objects, by id: `(id, bytes, what it covers)`. +pub(super) fn objects(dir: &Path) -> Result, String)>, String> { + let src = |name: &str| read(dir, &format!("src/{name}")); + let mut v: Vec<(String, Vec, String)> = vec![ + ("O11a".into(), read(dir, &format!("{STAGED_PAIR}/base_0.wai"))?, "a staged-delivery base: the first two frames of a clip (WAI1 int_motion), segment 1 of 2".into()), + ("O11b".into(), read(dir, &format!("{STAGED_PAIR}/base_1.wai"))?, "the next two frames, segment 2 of 2".into()), + ("O11bL1".into(), read(dir, &format!("{STAGED_PAIR}/L1_second.wai"))?, "layer 1 over O11b: listed after segment 2 only".into()), + ]; + for s in 0..2 { + v.push((format!("O12s{s}"), int_motion(src(&format!("live/s{s}.wiv1"))?, 32, 18)?, format!("live segment {} (WAI1 int_motion, 3.84 s): the object behind the live edge", 1200 + s))); + for p in 0..12 { + v.push((part_id(s, p), int_motion(src(&format!("live/s{s}p{p:02}.wiv1"))?, 32, 18)?, + format!("part {p} of live segment {} (0.32 s), served under its alias and decoded alone", 1200 + s))); + } + } + for (i, id) in ["O13a", "O13b", "O13c"].into_iter().enumerate() { + v.push((id.into(), int_motion(src(&format!("players/wai{i}.wiv1"))?, 64, 36)?, format!("player fixture: the WAI object for classical segment {i} (1 s)"))); + } + Ok(v) +} + +// ------------------------------------------------------------------- the inputs + +fn seg(object: &str) -> Value { + json!({ "object": object }) +} + +fn live_parts(s: usize, n: usize) -> Value { + Value::Array((0..n).map(|p| json!({ "object": part_id(s, p), "alias": format!("720/{}.{p}.wai", 1200 + s), "independent": true })).collect()) +} + +/// Each WAI media playlist the corpus renders: where it is written, the classical +/// playlist it sits beside, and the objects that cover it. +fn media_specs() -> Vec { + let live_seg = |s: usize, with_parts: bool| { + let mut v = seg(&format!("O12s{s}")); + if with_parts { + v["parts"] = live_parts(s, 12); + } + v + }; + vec![ + json!({ "file": "playlists/o9/wai-180.m3u8", "classical": "src/playlists/o9-classical.m3u8", "uri_prefix": "../../objects/", + "segments": [seg("O9a"), seg("O9b"), seg("O9c")] }), + json!({ "file": "playlists/o9/wai-180-gap.m3u8", "classical": "src/playlists/o9-classical.m3u8", "uri_prefix": "../../objects/", + "segments": [seg("O9a"), { "object": "O9b", "gap": true }, seg("O9c")] }), + json!({ "file": "playlists/o10/wai-a.m3u8", "classical": "src/playlists/o10-classical.m3u8", "uri_prefix": "../../objects/", + "segments": [{ "object": "O10", "layers": ["O10L1", "O10L2"] }] }), + json!({ "file": "playlists/o11/wai-v.m3u8", "classical": "src/playlists/o11-classical.m3u8", "uri_prefix": "../../objects/", + "segments": [seg("O11a"), { "object": "O11b", "layers": ["O11bL1"] }] }), + json!({ "file": "live/wai-r0.m3u8", "classical": "src/live/classical-r0.m3u8", "uri_prefix": "../objects/", + "segments": [], "open": { "parts": live_parts(0, 5), "preload_hint": "720/1200.5.wai" } }), + json!({ "file": "live/wai-r1.m3u8", "classical": "src/live/classical-r1.m3u8", "uri_prefix": "../objects/", + "segments": [live_seg(0, true)], "open": { "parts": live_parts(1, 3), "preload_hint": "720/1201.3.wai" } }), + json!({ "file": "live/wai-r2.m3u8", "classical": "src/live/classical-r2.m3u8", "uri_prefix": "../objects/", + "segments": [live_seg(0, true), live_seg(1, true)], "open": { "parts": [], "preload_hint": "720/1202.0.wai" } }), + json!({ "file": "live/wai-r3.m3u8", "classical": "src/live/classical-r3.m3u8", "uri_prefix": "../objects/", + "segments": [live_seg(0, false), live_seg(1, false)], "end_list": true }), + json!({ "file": "players/wai-v0.m3u8", "classical": "players/classical/v0.m3u8", "uri_prefix": "../objects/", + "segments": [seg("O13a"), seg("O13b"), seg("O13c")] }), + ] +} + +/// The WAI multivariant playlists: each variant's URI and the media playlist it names. +fn multivariant_specs() -> Vec { + vec![ + json!({ "file": "playlists/o9/wai.m3u8", "variants": [{ "uri": "wai-180.m3u8", "playlist": "playlists/o9/wai-180.m3u8", "resolution": [320, 180], "frame_rate_milli": 25000 }] }), + json!({ "file": "playlists/o9/wai-gap.m3u8", "variants": [{ "uri": "wai-180-gap.m3u8", "playlist": "playlists/o9/wai-180-gap.m3u8", "resolution": [320, 180] }] }), + json!({ "file": "playlists/o10/wai.m3u8", "variants": [{ "uri": "wai-a.m3u8", "playlist": "playlists/o10/wai-a.m3u8" }] }), + json!({ "file": "playlists/o11/wai.m3u8", "variants": [{ "uri": "wai-v.m3u8", "playlist": "playlists/o11/wai-v.m3u8" }] }), + json!({ "file": "live/wai.m3u8", "variants": [{ "uri": "wai-720.m3u8", "playlist": "live/wai-r3.m3u8", "resolution": [32, 18], "frame_rate_milli": 25000 }] }), + json!({ "file": "players/wai.m3u8", "variants": [{ "uri": "wai-v0.m3u8", "playlist": "players/wai-v0.m3u8", "resolution": [64, 36], "frame_rate_milli": 25000 }] }), + ] +} + +/// The WAI MPDs: the media playlist whose objects they list, the classical timeline +/// (the classical MPD's, `classical_mpd` and its `classical_set`; else the classical HLS +/// playlist's at timescale 1000), and the ids written. +fn mpd_specs() -> Vec { + vec![ + json!({ "file": "playlists/o9/wai.mpd", "playlist": "playlists/o9/wai-180.m3u8", "classical": "src/playlists/o9-classical.m3u8", + "classical_mpd": "src/playlists/o9-classical.mpd", "classical_set": "1", "set_id": 1, "rep_id": "wai-180" }), + json!({ "file": "playlists/o9/wai-gap.mpd", "playlist": "playlists/o9/wai-180-gap.m3u8", "classical": "src/playlists/o9-classical.m3u8", + "classical_mpd": "src/playlists/o9-classical.mpd", "classical_set": "1", "set_id": 1, "rep_id": "wai-180" }), + json!({ "file": "players/wai.mpd", "playlist": "players/wai-v0.m3u8", "classical": "players/classical/v0.m3u8", + "period_id": "p0", "set_id": 1, "rep_id": "wai-v0" }), + ] +} + +/// The receipts: each WAI media playlist, its content id, and (live) the revision whose +/// receipt is its parent. +fn receipt_specs() -> Vec<(&'static str, &'static str, Option)> { + vec![ + ("playlists/o9/wai-180.m3u8", "o9", None), + ("playlists/o10/wai-a.m3u8", "o10", None), + ("playlists/o11/wai-v.m3u8", "o11", None), + ("live/wai-r0.m3u8", "live/720", None), + ("live/wai-r1.m3u8", "live/720", Some(3)), + ("live/wai-r2.m3u8", "live/720", Some(4)), + ("live/wai-r3.m3u8", "live/720", Some(5)), + ("players/wai-v0.m3u8", "players/v0", None), + ] +} + +/// The published classical master of the player fixture: the original with the +/// session-data line in its place (§11.1). +fn published_master(original: &str) -> Result { + wai::http_binding::hls::insert_session_data(original, "wai.m3u8").map_err(|e| e.to_string()) +} + +// -------------------------------------------------------------- evaluation helpers + +/// `_text` written inline, or the file `` names. +fn inline_or_file(c: &Corpus, input: &Value, key: &str) -> Result { + match input[format!("{key}_text").as_str()].as_str() { + Some(t) => Ok(t.to_string()), + None => text(c, input[key].as_str().ok_or_else(|| Stop::Fault(format!("{key}")))?), + } +} + +/// A rendering's inputs written inline: `segments` (`[duration_ms, uri, ad]`), `render` +/// and `extras` in the receipt file's JSON forms. +fn render_inputs(input: &Value) -> Result<(Vec, wai::hls_render::HlsRender, Vec), Stop> { + use wai::hls_render::{HlsRender, LayerLine, PartLine, Segment, SegmentExtra, SegmentKind}; + let u = |v: &Value, what: &str| v.as_u64().ok_or_else(|| Stop::Fault(format!("{what} is an unsigned integer"))); + let opt = |v: &Value, what: &str| if v.is_null() { Ok(None) } else { u(v, what).map(Some) }; + let st = |v: &Value| v.as_str().map(String::from); + let segs = input["segments"].as_array().ok_or("segments")?.iter().enumerate().map(|(i, x)| { + let ad = x[2].as_bool() == Some(true); + Ok(Segment { index: i as u64, kind: if ad { SegmentKind::Ad } else { SegmentKind::Content }, uri: st(&x[1]).ok_or("uri")?, duration_ms: u(&x[0], "duration")?, + ad_id: ad.then(|| "ad".to_string()) }) + }).collect::, Stop>>()?; + let r = &input["render"]; + let render = HlsRender { + version: u(&r["version"], "version")? as u8, media_sequence: u(&r["media_sequence"], "media_sequence")?, + discontinuity_sequence: u(&r["discontinuity_sequence"], "discontinuity_sequence")?, target_duration_s: opt(&r["target_duration_s"], "target")?, + part_target_ms: opt(&r["part_target_ms"], "part target")?, part_hold_back_ms: opt(&r["part_hold_back_ms"], "hold back")?, + can_block_reload: r["can_block_reload"].as_bool() == Some(true), end_list: r["end_list"].as_bool() == Some(true), + playlist_type: match r["playlist_type"].as_str() { + Some("VOD") => Some(wai::hls_render::PlaylistType::Vod), + Some("EVENT") => Some(wai::hls_render::PlaylistType::Event), + _ => None, + }, + independent_segments: r["independent_segments"].as_bool() == Some(true), + map_uri: st(&r["map_uri"]), + }; + let layers = |v: &Value| -> Result, Stop> { + v.as_array().into_iter().flatten().map(|l| Ok(LayerLine { n: u(&l["n"], "n")? as u8, uri: st(&l["uri"]).ok_or("layer uri")? })).collect() + }; + let extras = input["extras"].as_array().into_iter().flatten().map(|e| { + Ok(SegmentExtra { + discontinuity: e["discontinuity"].as_bool() == Some(true), + gap: e["gap"].as_bool() == Some(true), + program_date_time: st(&e["program_date_time"]), + parts: e["parts"].as_array().into_iter().flatten().map(|q| Ok(PartLine { + duration_ms: u(&q["duration_ms"], "part duration")?, independent: q["independent"].as_bool() == Some(true), + uri: st(&q["uri"]).ok_or("part uri")?, layers: layers(&q["layers"])?, + })).collect::>()?, + layers: layers(&e["layers"])?, + preload_hint: st(&e["preload_hint"]), + }) + }).collect::, Stop>>()?; + Ok((segs, render, extras)) +} + +fn text(c: &Corpus, path: &str) -> Result { + String::from_utf8(c.file(path)?).map_err(|_| Stop::Fault(format!("{path}: not UTF-8"))) +} + +fn pl_err(e: &PlaylistError) -> Stop { + Stop::Refused(json!({ "error": e.code() })) +} + +/// Apply `[[from, to], …]` to a text, each once, in order; a `from` the text does not +/// hold is a fault in the case. +fn edit(mut t: String, edits: &Value) -> Result { + for e in edits.as_array().into_iter().flatten() { + let (from, to) = (e[0].as_str().ok_or("an edit's from")?, e[1].as_str().ok_or("an edit's to")?); + if !t.contains(from) { + return Err(Stop::Fault(format!("the edit's {from:?} is not in the text"))); + } + t = t.replacen(from, to, 1); + } + Ok(t) +} + +/// The corpus object whose content hash is `cid`. +fn by_cid<'a>(c: &'a Corpus, cid: &[u8; 32]) -> Option<&'a Vec> { + c.objects.values().find(|b| content_hash(b) == *cid) +} + +fn object_bytes(c: &Corpus, id: &str) -> Result, Stop> { + c.objects.get(id).cloned().ok_or_else(|| Stop::Fault(format!("no object {id}"))) +} + +fn wai_parts(c: &Corpus, v: &Value, classical_ms: &[u64]) -> Result, Stop> { + v.as_array().into_iter().flatten().enumerate().map(|(j, p)| { + let env = object_bytes(c, p["object"].as_str().ok_or("a part's object")?)?; + let ms = classical_ms.get(j).copied().or(p["duration_ms"].as_u64()).ok_or("a part's duration")?; + let mut part = WaiPart::of(&env, p["alias"].as_str().ok_or("a part's alias")?, ms, p["independent"].as_bool() == Some(true)).map_err(|e| err_stop(&e))?; + part.layers = wai_layers(c, &p["layers"])?; + Ok(part) + }).collect() +} + +fn err_stop(e: &wai::http_binding::RangeRefusal) -> Stop { + Stop::Refused(err(e)) +} + +fn wai_layers(c: &Corpus, v: &Value) -> Result, Stop> { + strs(v).iter().map(|id| WaiLayer::of(&object_bytes(c, id)?).map_err(|e| err_stop(&e))).collect() +} + +/// A media playlist spec rendered: its text, or the refusal. +fn render_media(c: &Corpus, input: &Value) -> Result { + let classical = parse_media_playlist(&text(c, input["classical"].as_str().ok_or("classical")?)?).map_err(|e| pl_err(&e))?; + let mut segs = Vec::new(); + for (i, s) in input["segments"].as_array().ok_or("segments")?.iter().enumerate() { + let ms = classical.segments.get(i).map_or(0, |x| x.duration_ms); + let env = object_bytes(c, s["object"].as_str().ok_or("a segment's object")?)?; + let mut w = WaiSegment::of(&env, s["duration_ms"].as_u64().unwrap_or(ms)).map_err(|e| err_stop(&e))?; + w.gap = s["gap"].as_bool() == Some(true); + w.layers = wai_layers(c, &s["layers"])?; + let cms: Vec = classical.segments.get(i).map(|x| x.extra.parts.iter().map(|p| p.duration_ms).collect()).unwrap_or_default(); + w.parts = wai_parts(c, &s["parts"], &cms)?; + segs.push(w); + } + let open = match &input["open"] { + Value::Null => None, + o => { + let cms: Vec = classical.open.as_ref().map(|x| x.parts.iter().map(|p| p.duration_ms).collect()).unwrap_or_default(); + Some(WaiOpen { parts: wai_parts(c, &o["parts"], &cms)?, preload_hint: o["preload_hint"].as_str().map(String::from) }) + } + }; + let end = input["end_list"].as_bool().unwrap_or(classical.end_list); + let (_, _, _, t) = wai_media_playlist(&classical, &segs, open.as_ref(), input["uri_prefix"].as_str().unwrap_or(""), end).map_err(|e| pl_err(&e))?; + Ok(t) +} + +/// The objects a WAI media playlist lists, resolved among the corpus's by cid: per +/// segment that is not a gap, the bytes of its object and its layers with its duration; +/// per part, its bytes with its duration; and every object a client may fetch (segments +/// that are not gaps, parts, layers), segment by segment. +fn listed_objects(c: &Corpus, p: &MediaPlaylist) -> Result<(Vec<(u64, u64)>, Vec<(u64, u64)>, Vec>), Stop> { + let resolve = |uri: &str| -> Result, Stop> { + let (base, frag) = parse_fragment(uri); + let cid = frag.cid.or_else(|| base.rsplit('/').next().and_then(|n| n.strip_suffix(".wai")).and_then(unhex32)) + .ok_or_else(|| Stop::Fault(format!("{uri}: no cid")))?; + by_cid(c, &cid).cloned().ok_or_else(|| Stop::Fault(format!("{uri}: no corpus object"))) + }; + let (mut segs, mut parts, mut all) = (Vec::new(), Vec::new(), Vec::new()); + let extra = |e: &wai::hls_render::SegmentExtra, parts: &mut Vec<(u64, u64)>, all: &mut Vec>| -> Result<(), Stop> { + for q in &e.parts { + let b = resolve(&q.uri)?; + parts.push((b.len() as u64, q.duration_ms)); + all.push(b); + for l in &q.layers { + all.push(resolve(&l.uri)?); + } + } + for l in &e.layers { + all.push(resolve(&l.uri)?); + } + Ok(()) + }; + for s in &p.segments { + if s.extra.gap { + continue; + } + let b = resolve(&s.uri)?; + let mut n = b.len() as u64; + all.push(b); + for l in &s.extra.layers { + n += resolve(&l.uri)?.len() as u64; + } + segs.push((n, s.duration_ms)); + extra(&s.extra, &mut parts, &mut all)?; + } + if let Some(o) = &p.open { + extra(o, &mut parts, &mut all)?; + } + Ok((segs, parts, all)) +} + +fn union_floor(all: &[Vec]) -> Result<(codecs::Element, codecs::Element), Stop> { + let objs: Vec = all.iter().map(|e| SeqObject::from_envelope(e).map_err(|e| err_stop(&e))).collect::>()?; + let u = codecs::union_of(&objs).map_err(|e| Stop::Refused(json!({ "error": e.code() })))?; + let f = codecs::floor_of(&objs).map_err(|e| Stop::Refused(json!({ "error": e.code() })))?; + Ok((u, f)) +} + +fn render_multivariant(c: &Corpus, input: &Value) -> Result { + let mut lines = Vec::new(); + for v in input["variants"].as_array().ok_or("variants")? { + let p = parse_media_playlist(&text(c, v["playlist"].as_str().ok_or("playlist")?)?).map_err(|e| pl_err(&e))?; + let (segs, parts, all) = listed_objects(c, &p)?; + let (union, floor) = union_floor(&all)?; + let (peak, avg) = bandwidths(if segs.is_empty() { &parts } else { &segs }).ok_or("no duration to compute a bandwidth from")?; + let resolution = v["resolution"].as_array().map(|r| (r[0].as_u64().unwrap_or(0) as u32, r[1].as_u64().unwrap_or(0) as u32)); + lines.push(VariantLine { uri: v["uri"].as_str().ok_or("uri")?.into(), union, floor, peak_bps: peak, avg_bps: avg, resolution, + frame_rate_milli: v["frame_rate_milli"].as_u64().map(|f| f as u32) }); + } + wai_multivariant(&lines).map_err(|e| pl_err(&e)) +} + +fn render_mpd(c: &Corpus, input: &Value) -> Result { + let p = parse_media_playlist(&text(c, input["playlist"].as_str().ok_or("playlist")?)?).map_err(|e| pl_err(&e))?; + let (segs, _, all) = listed_objects(c, &p)?; + let (union, floor) = union_floor(&all)?; + let (peak, _) = bandwidths(&segs).ok_or("no segment")?; + let t = match input["classical_mpd"].as_str() { + Some(f) => DashTimeline::from_mpd(&text(c, f)?, input["period_id"].as_str(), input["classical_set"].as_str(), p.segments.len()).map_err(|e| pl_err(&e))?, + None => { + let classical = parse_media_playlist(&text(c, input["classical"].as_str().ok_or("classical")?)?).map_err(|e| pl_err(&e))?; + DashTimeline::from_hls(&classical, input["period_id"].as_str().unwrap_or("p0"), 0).map_err(|e| pl_err(&e))? + } + }; + let media = p.segments.iter().map(|s| (!s.extra.gap).then(|| s.uri.clone())).collect(); + let rep = WaiRepresentation { id: input["rep_id"].as_str().ok_or("rep_id")?.into(), bandwidth: peak, media }; + wai_mpd(&t, input["set_id"].as_u64().unwrap_or(1) as u32, &union, &floor, &[rep]).map_err(|e| pl_err(&e)) +} + +/// A rendered file's outcome: its BLAKE3 and length, and whether the committed file +/// (when the case names one) holds exactly these bytes. +fn rendered_outcome(c: &Corpus, input: &Value, t: &str) -> Value { + let mut o = json!({ "blake3": blake3::hash(t.as_bytes()).to_hex().to_string(), "bytes": t.len() }); + if let Some(f) = input["file"].as_str() { + o["same_as_file"] = json!(c.file(f).is_ok_and(|b| b == t.as_bytes())); + } + o +} + +fn parsed(p: &MediaPlaylist, t: &str) -> Value { + let layers = |ls: &[wai::hls_render::LayerLine]| ls.iter().map(|l| l.n).collect::>(); + let parts = |ps: &[wai::hls_render::PartLine]| ps.iter().map(|q| json!([q.duration_ms, q.independent, layers(&q.layers)])).collect::>(); + json!({ + "version": p.version, "target_duration_s": p.target_duration_s, "media_sequence": p.media_sequence, + "discontinuity_sequence": p.discontinuity_sequence, "part_target_ms": p.part_target_ms, "part_hold_back_ms": p.part_hold_back_ms, + "can_block_reload": p.can_block_reload, "end_list": p.end_list, + "playlist_type": p.playlist_type.map(|t| t.label()), "independent_segments": p.independent_segments, "map_uri": p.map_uri, + "segments": p.segments.iter().map(|s| json!([s.duration_ms, s.extra.discontinuity, s.extra.program_date_time, parts(&s.extra.parts), layers(&s.extra.layers), s.uri, s.extra.gap])).collect::>(), + "open": p.open.as_ref().map(|o| json!([o.discontinuity, o.program_date_time, parts(&o.parts)])), + "preload_hint": p.preload_hint, + "rendered_form": p.is_rendered_form(t), + }) +} + +// ---------------------------------------------------------------- adaptation + +/// Appendix F's refusal of a value beyond its field's type. +fn out_of_bounds() -> Stop { + Stop::Refused(json!({ "error": "abr_state_out_of_bounds" })) +} + +/// An unsigned integer of at most `max`: a number beyond it (or negative, or with a +/// fraction) is out of bounds; anything else a fault in the case. +fn bounded(v: &Value, max: u64, what: &str) -> Result { + match v.as_u64() { + Some(x) if x <= max => Ok(x), + _ if v.is_number() => Err(out_of_bounds()), + _ => Err(Stop::Fault(format!("{what} is a number"))), + } +} + +fn bounded_opt(v: &Value, max: u64, what: &str) -> Result, Stop> { + if v.is_null() { Ok(None) } else { bounded(v, max, what).map(Some) } +} + +/// A policy from its JSON form: `{"id": "throughput", "last_n": …, "safety_permille": …}` +/// and the like, each parameter within its type (Appendix F). +pub(super) fn policy(v: &Value) -> Result, Stop> { + let u = |k: &str, max: u64| bounded(&v[k], max, &format!("policy.{k}")); + let (u8m, u16m, u32m) = (u64::from(u8::MAX), u64::from(u16::MAX), u64::from(u32::MAX)); + Ok(match v["id"].as_str() { + Some("throughput") => Box::new(Throughput { last_n: u("last_n", u8m)? as u8, safety_permille: u("safety_permille", u16m)? as u16 }), + Some("buffer_based") => Box::new(BufferBased { reservoir_ms: u("reservoir_ms", u32m)? as u32, cushion_ms: u("cushion_ms", u32m)? as u32 }), + Some("bola_like") => Box::new(BolaLike { v_milli: u("v_milli", u32m)? as u32, max_buffer_ms: u("max_buffer_ms", u32m)? as u32 }), + Some("mpc") => Box::new(Mpc { horizon: u("horizon", u8m)? as u8, safety_permille: u("safety_permille", u16m)? as u16 }), + _ => return Err("an unknown policy".into()), + }) +} + +/// A state from its JSON form, each value within its field's type (Appendix F). +pub(super) fn state(v: &Value) -> Result { + let (u32m, u64m) = (u64::from(u32::MAX), u64::MAX); + let list = |k: &str| v[k].as_array().cloned().unwrap_or_default(); + let samples = list("samples").iter().map(|s| { + Ok(Sample { at_ms: bounded(&s[0], u64m, "a sample's at_ms")?, bytes: bounded(&s[1], u64m, "a sample's bytes")?, ms: bounded(&s[2], u64m, "a sample's ms")? }) + }).collect::, Stop>>()?; + Ok(AbrState { + buffer_ms: bounded(&v["buffer_ms"], u32m, "state.buffer_ms")? as u32, + samples, + last: bounded_opt(&v["last"], u64m, "state.last")?.map(|l| l as usize), + option_bytes: list("option_bytes").iter().map(|x| bounded(x, u64m, "an option's bytes")).collect::>()?, + option_joules_micro: list("option_joules_micro").iter().map(|x| bounded_opt(x, u64m, "an option's figure")).collect::>()?, + joule_budget_micro: bounded_opt(&v["joule_budget_micro"], u64m, "state.joule_budget_micro")?, + segment_ms: bounded(&v["segment_ms"], u32m, "state.segment_ms")? as u32, + }) +} + +fn abr_err(e: abr::AbrError) -> Stop { + Stop::Refused(json!({ "error": e.code() })) +} + +// ------------------------------------------------------------------ the outcomes + +/// The outcome of a playlist or adaptation op; `None` for an op this side does not hold. +pub(super) fn outcome(c: &Corpus, op: &str, input: &Value) -> Option> { + Some(match op { + "media_playlist" => render_media(c, input).map(|t| rendered_outcome(c, input, &t)), + "multivariant" => render_multivariant(c, input).map(|t| rendered_outcome(c, input, &t)), + "mpd" => render_mpd(c, input).map(|t| rendered_outcome(c, input, &t)), + "playlist_parse" => (|| { + let t = match input["text"].as_str() { Some(t) => t.to_string(), None => text(c, input["file"].as_str().ok_or("file")?)? }; + let p = parse_media_playlist(&t).map_err(|e| pl_err(&e))?; + Ok(parsed(&p, &t)) + })(), + "uri_resolve" => (|| { + let (b, r) = (input["base"].as_str().ok_or("base")?, input["reference"].as_str().ok_or("reference")?); + Ok(json!({ "url": wai::http_binding::uri::resolve(b, r) })) + })(), + "dash_adjacency" => (|| { + let w = edit(inline_or_file(c, input, "wai")?, &input["edit_wai"])?; + let k = edit(inline_or_file(c, input, "classical")?, &input["edit_classical"])?; + wai::http_binding::dash_read::check_dash_adjacent(&w, &k).map_err(|e| pl_err(&e))?; + Ok(json!({ "adjacent": true })) + })(), + "adjacency" => (|| { + let w = edit(inline_or_file(c, input, "wai")?, &input["edit_wai"])?; + let k = edit(inline_or_file(c, input, "classical")?, &input["edit_classical"])?; + check_adjacent(&w, &k).map_err(|e| pl_err(&e))?; + Ok(json!({ "adjacent": true })) + })(), + "unchanged" => (|| { + let o = inline_or_file(c, input, "original")?; + let p = edit(inline_or_file(c, input, "published")?, &input["edit_published"])?; + classical_unchanged(&o, &p).map_err(|e| pl_err(&e))?; + Ok(json!({ "unchanged": true })) + })(), + "playlist_receipt" => receipt_outcome(c, input), + "live" => live_outcome(c, input), + "render" => (|| { + let (segs, r, extras) = render_inputs(input)?; + wai::hls_render::check_render(&segs, &r, &extras).map_err(|e| Stop::Refused(json!({ "error": e.code() })))?; + let t = wai::hls_render::render_hls_with(&segs, &r, &extras); + Ok(json!({ "blake3": blake3::hash(t.as_bytes()).to_hex().to_string(), "bytes": t.len() })) + })(), + "ilog2" => (|| { + let (a, b) = (input["a"].as_u64().ok_or("a")?, input["b"].as_u64().ok_or("b")?); + Ok(json!({ "log2_q16": abr::ilog2_q16(a, b) })) + })(), + "abr" => (|| { + let p = policy(&input["policy"])?; + let (k, b) = abr::decide(p.as_ref(), &state(&input["state"])?).map_err(abr_err)?; + Ok(json!({ "choice": k, "budget": b.label() })) + })(), + "abr_order" => (|| { + let env = c.object(input)?; + let h = match parse_head(&env, env.len() as u64) { + Ok(HeadParse::Head(h)) => h, + _ => return Err(Stop::Fault("abr_order needs a whole object".into())), + }; + let adv = strs(&input["advertises"]); + let order = abr::order_entries(&h, &|cap: &str| adv.iter().any(|a| a == cap), policy(&input["policy"])?.as_ref(), &state(&input["state"])?).map_err(abr_err)?; + Ok(json!({ "order": order })) + })(), + "depth" => (|| { + let layers: Vec = match input["objects"].as_array() { + Some(ids) => ids.iter().map(|id| Ok(object_bytes(c, id.as_str().ok_or("an object id")?)?.len() as u64)).collect::>()?, + None => input["layers_n"].as_array().ok_or("layers_n")?.iter().map(|x| x.as_u64().ok_or("layers_n")).collect::>()?, + }; + let (d, b) = abr::depth(&layers, policy(&input["policy"])?.as_ref(), &state(&input["state"])?).map_err(abr_err)?; + Ok(json!({ "depth": d, "budget": b.label() })) + })(), + _ => return None, + }) +} + +#[cfg(feature = "video_manifest")] +fn receipt_outcome(c: &Corpus, input: &Value) -> Result { + use wai::http_binding::playlist_receipt::check_playlist; + let pl = edit(text(c, input["playlist"].as_str().ok_or("playlist")?)?, &input["edit_playlist"])?; + let rpath = input["receipt"].as_str().ok_or("receipt")?; + let rc = edit(text(c, rpath)?, &input["edit_receipt"])?; + let name = match &input["name"] { + Value::String(n) => Some(n.clone()), + Value::Null => rpath.rsplit('/').next().map(String::from), + _ => None, + }; + let r = check_playlist(pl.as_bytes(), rc.as_bytes(), name.as_deref()).map_err(|e| Stop::Refused(json!({ "error": e.code() })))?; + Ok(json!({ "signed": hex(&r.receipt_hash()), "parent": r.parent_receipt_hash.map(|h| hex(&h)) })) +} + +#[cfg(not(feature = "video_manifest"))] +fn receipt_outcome(_: &Corpus, _: &Value) -> Result { + Err(Stop::Fault("built without video_manifest: playlist receipts are not checked".into())) +} + +/// §8.6, §11.4, §11.6 over a live playlist's revisions, in the order a client fetched +/// them: each revision's adjacency to its classical revision; its listing, held against +/// every URI and sequence number the revisions accepted before it bound; its receipt; +/// and the receipt taken into the chain the client holds, walking back by parent links +/// to the head through the receipts served under `parents` (`.json`) up to +/// `walk_bound`. Then each part fetched by alias, held against the cid the first +/// accepted revision that lists it lists, at `signed` when a receipt in the chain binds +/// the alias to that cid, else at `listed`. +#[cfg(feature = "video_manifest")] +fn live_outcome(c: &Corpus, input: &Value) -> Result { + use wai::http_binding::hls::Bindings; + use wai::http_binding::playlist_receipt::{check_playlist, LiveChain, Step, WALK_BOUND}; + let revisions = input["revisions"].as_array().ok_or("revisions")?; + let parents = match input["parents"].as_str() { + Some(d) => d.to_string(), + None => format!("{}/mr", revisions.first().and_then(|r| r["wai"].as_str()).and_then(|w| w.rsplit_once('/')).map_or("", |(d, _)| d)), + }; + let bound = input["walk_bound"].as_u64().map_or(WALK_BOUND, |b| b as usize); + // The URL the client fetches every revision from, against which their lines resolve. + let url = input["url"].as_str().unwrap_or(LIVE_URL); + let mut chain = LiveChain::for_playlist(url, bound); + let mut listed_bindings = Bindings::for_playlist(url); + let mut revs = Vec::new(); + let mut listed: Vec> = Vec::new(); + for r in revisions { + let w = edit(text(c, r["wai"].as_str().ok_or("wai")?)?, &r["edit_wai"])?; + let k = text(c, r["classical"].as_str().ok_or("classical")?)?; + let adjacent = match check_adjacent(&w, &k) { Ok(()) => json!(true), Err(e) => json!(e.code()) }; + let p = parse_media_playlist(&w).map_err(|e| pl_err(&e))?; + if let Err(e) = listed_bindings.bind(&p) { + revs.push(json!({ "adjacent": adjacent, "listing": e.code(), "receipt": null, "chained": null, "walked": null })); + listed.push(None); + continue; + } + listed.push(Some(p)); + let rpath = r["receipt"].as_str().ok_or("receipt")?; + let name = rpath.rsplit('/').next(); + let rr = match check_playlist(w.as_bytes(), &c.file(rpath)?, if rpath.starts_with("cases/") { None } else { name }) { + Ok(x) => x, + Err(e) => { + revs.push(json!({ "adjacent": adjacent, "listing": true, "receipt": e.code(), "chained": null, "walked": null })); + continue; + } + }; + let (chained, walked) = match chain.advance(&rr, |h| c.file(&format!("{parents}/{}.json", hex(h))).ok()) { + Ok(Step::First) => (json!(true), json!(0)), + Ok(Step::Chained { walked }) => (json!(true), json!(walked)), + Ok(Step::Unchanged) => (json!("unchanged"), json!(0)), + Ok(Step::Reanchored { walked }) => (json!("reanchored"), json!(walked)), + Err(e) => (json!(e.code()), Value::Null), + }; + revs.push(json!({ "adjacent": adjacent, "listing": true, "receipt": "signed", "chained": chained, "walked": walked })); + } + let mut fetches = Vec::new(); + for f in input["fetches"].as_array().into_iter().flatten() { + let alias = f["alias"].as_str().ok_or("alias")?; + let mut served = object_bytes(c, f["object"].as_str().ok_or("object")?)?; + patch(&mut served, &f["patch_object"])?; + let first = listed.iter().enumerate().find_map(|(k, p)| p.as_ref().and_then(|p| listed_cid(p, alias)).map(|cid| (k, cid))); + fetches.push(match first { + None => json!({ "alias": alias, "listed_in": null, "outcome": "not_listed", "held": null }), + Some((k, cid)) if content_hash(&served) == cid => { + let held = if chain.bindings().cid_of(alias) == Some(cid) { "signed" } else { "listed" }; + json!({ "alias": alias, "listed_in": k, "outcome": "accepted", "held": held }) + } + Some((k, _)) => json!({ "alias": alias, "listed_in": k, "outcome": "object_digest_mismatch", "held": null }), + }); + } + Ok(json!({ "revisions": revs, "fetches": fetches })) +} + +#[cfg(not(feature = "video_manifest"))] +fn live_outcome(_: &Corpus, _: &Value) -> Result { + Err(Stop::Fault("built without video_manifest: playlist receipts are not checked".into())) +} + +// ------------------------------------------------------------------ generation + +/// Every playlist-side file, by path: the WAI media playlists, multivariant playlists +/// and MPDs, the published classical master, and the receipts (each live revision's +/// chained to the one before), with the extra receipt a case needs. `files` holds the +/// files generated so far, and receives these. +pub(super) fn generate(c: &mut Corpus) -> Result>, String> { + let mut out = BTreeMap::new(); + let put = |c: &mut Corpus, out: &mut BTreeMap>, path: &str, b: Vec| { + c.extra.insert(path.to_string(), b.clone()); + out.insert(path.to_string(), b); + }; + let fault = |what: &str, s: Stop| match s { + Stop::Refused(v) => format!("{what}: refused {v}"), + Stop::Fault(e) => format!("{what}: {e}"), + }; + for spec in media_specs() { + let f = spec["file"].as_str().expect("file").to_string(); + let t = render_media(c, &spec).map_err(|s| fault(&f, s))?; + put(c, &mut out, &f, t.into_bytes()); + } + for spec in multivariant_specs() { + let f = spec["file"].as_str().expect("file").to_string(); + let t = render_multivariant(c, &spec).map_err(|s| fault(&f, s))?; + put(c, &mut out, &f, t.into_bytes()); + } + for spec in mpd_specs() { + let f = spec["file"].as_str().expect("file").to_string(); + let t = render_mpd(c, &spec).map_err(|s| fault(&f, s))?; + put(c, &mut out, &f, t.into_bytes()); + } + let original = String::from_utf8(c.file("src/players/master.m3u8")?).map_err(|_| "the master is not UTF-8")?; + put(c, &mut out, "players/master.m3u8", published_master(&original)?.into_bytes()); + seal_all(c, &mut out)?; + Ok(out) +} + +#[cfg(feature = "video_manifest")] +fn seal_all(c: &mut Corpus, out: &mut BTreeMap>) -> Result<(), String> { + use wai::http_binding::playlist_receipt::seal_playlist; + use wai::quantum_ops::GrantRef; + let key = ed25519_dalek::SigningKey::from_bytes(&PLAYLIST_SEED); + let mut sealed: Vec = Vec::new(); + for (path, content, parent) in receipt_specs() { + let t = String::from_utf8(c.file(path)?).map_err(|_| "not UTF-8")?; + // O10's playlist is sealed under a publisher's grant with both ceilings; the rest + // under a grant of none, whose joule ceiling the file writes null. + let grant = if path == "playlists/o10/wai-a.m3u8" { publisher_grant() } else { GrantRef::unbounded("video.manifest") }; + let r = seal_playlist(&t, &key, PLAYLIST_SIGNER, content, grant, parent.map(|k| &sealed[k])) + .map_err(|e| format!("{path}: {e}"))?; + let dir = path.rsplit_once('/').map_or("", |(d, _)| d); + publish(c, out, &format!("{dir}/mr"), &r); + sealed.push(r); + } + // Signatures a lenient verifier accepts and the strict rule refuses (crate::sig), each + // over the o9 playlist's receipt: the identity key in its canonical and its long + // spelling under (R, S) = (B, 1), which satisfies the equation for any message; and + // under the key B (scalar 1), R the identity, canonical and respelled, with S = k. + // The same key with R = B and S = 1 + k is a signature the rule accepts. + for (file, key_bytes, r, s_of_k) in crafted_signatures() { + let mut x = sealed[0].clone(); + x.signer_pubkey = key_bytes; + let k = k_of(&r, &key_bytes, &x.signing_payload()); + x.sig[..32].copy_from_slice(&r); + x.sig[32..].copy_from_slice(&s_of_k(&k)); + let f = format!("cases/{file}"); + c.extra.insert(f.clone(), x.to_json().into_bytes()); + out.insert(f, x.to_json().into_bytes()); + } + let live = |c: &Corpus, k: usize| String::from_utf8(c.file(&format!("live/wai-r{k}.m3u8"))?).map_err(|_| "not UTF-8".to_string()); + let seal = |t: &str, key: &ed25519_dalek::SigningKey, parent: Option<&wai::wai_video_manifest::ManifestReceipt>| { + seal_playlist(t, key, PLAYLIST_SIGNER, "live/720", GrantRef::unbounded("video.manifest"), parent).map_err(|e| e.to_string()) + }; + let put = |c: &mut Corpus, out: &mut BTreeMap>, f: &str, b: Vec| { + c.extra.insert(f.to_string(), b.clone()); + out.insert(f.to_string(), b); + }; + // Revision 2 sealed with no parent: a receipt that verifies and does not chain. + let orphan = seal(&live(c, 2)?, &key, None)?; + put(c, out, "cases/live-r2-orphan.json", orphan.to_json().into_bytes()); + // Revision 2 chained to revision 1 by hash and signed by another key; and revision 3 + // chained to that one, with the other key's receipt served by its receipt hash. + let other = ed25519_dalek::SigningKey::from_bytes(&[0x58; 32]); + let r2_other = seal(&live(c, 2)?, &other, Some(&sealed[4]))?; + put(c, out, "cases/other-key/r2.json", r2_other.to_json().into_bytes()); + publish_by_hash(c, out, "cases/other-key/mr", &r2_other); + put(c, out, "cases/other-key/r3.json", seal(&live(c, 3)?, &key, Some(&r2_other))?.to_json().into_bytes()); + // Revision 2 relisting the alias of part 0 of segment 1201 (listed by revision 1) with + // part 1's object, sealed and chained to revision 1; revision 3 chained to it; and + // revision 3 rewriting media sequence 1200 to segment 1201's object, chained to + // revision 2. + let part = |s: usize, p: usize| -> Result { + let b = c.objects.get(&part_id(s, p)).ok_or("a live part")?; + Ok(format!("720/{}.{p}.wai#cid={}&n={}", 1200 + s, hex(&content_hash(b)), b.len())) + }; + let (p0, p1) = (part(1, 0)?, part(1, 1)?); + let relisted = live(c, 2)?.replacen(&p0, &p1.replacen("1201.1.wai", "1201.0.wai", 1), 1); + if relisted == live(c, 2)? { + return Err("revision 2 does not list part 0 of segment 1201 as expected".into()); + } + put(c, out, "cases/relist/wai-r2.m3u8", relisted.clone().into_bytes()); + let r2_relisted = seal(&relisted, &key, Some(&sealed[4]))?; + put(c, out, "cases/relist/r2.json", r2_relisted.to_json().into_bytes()); + publish_by_hash(c, out, "cases/relist/mr", &r2_relisted); + put(c, out, "cases/relist/r3-after.json", seal(&live(c, 3)?, &key, Some(&r2_relisted))?.to_json().into_bytes()); + let seg = |s: usize| -> Result { + let b = c.objects.get(&format!("O12s{s}")).ok_or("a live segment")?; + Ok(format!("../objects/{}.wai#n={}", hex(&content_hash(b)), b.len())) + }; + let (s0, s1) = (seg(0)?, seg(1)?); + let r3 = live(c, 3)?; + let at = r3.find(&s0).ok_or("revision 3 does not list segment 1200 as expected")?; + let resequenced = format!("{}{s1}{}", &r3[..at], &r3[at + s0.len()..]); + put(c, out, "cases/relist/wai-r3.m3u8", resequenced.clone().into_bytes()); + put(c, out, "cases/relist/r3.json", seal(&resequenced, &key, Some(&sealed[5]))?.to_json().into_bytes()); + // Revision 2 relisting part 0 of segment 1201 under another spelling of its alias + // (./720/1201.0.wai, one URL with 720/1201.0.wai): with part 1's object, and with its + // own; each sealed and chained to revision 1. + let dotted = live(c, 2)?.replacen(&p0, &format!("./{}", p1.replacen("1201.1.wai", "1201.0.wai", 1)), 1); + put(c, out, "cases/relist/wai-r2-dot.m3u8", dotted.clone().into_bytes()); + put(c, out, "cases/relist/r2-dot.json", seal(&dotted, &key, Some(&sealed[4]))?.to_json().into_bytes()); + let same = live(c, 2)?.replacen(&p0, &format!("./{p0}"), 1); + put(c, out, "cases/relist/wai-r2-dot-same.m3u8", same.clone().into_bytes()); + put(c, out, "cases/relist/r2-dot-same.json", seal(&same, &key, Some(&sealed[4]))?.to_json().into_bytes()); + // Revision 3 withdrawing segment 1200, bound to its object, as a gap; chained to + // revision 2. + let at = r3.find("#EXTINF:").ok_or("revision 3 has no segment")?; + let withdrawn = format!("{}#EXT-X-GAP\n{}", &r3[..at], &r3[at..]); + put(c, out, "cases/relist/wai-r3-gap.m3u8", withdrawn.clone().into_bytes()); + put(c, out, "cases/relist/r3-gap.json", seal(&withdrawn, &key, Some(&sealed[5]))?.to_json().into_bytes()); + // The walk's per-parent checks: revision 2's link served with revision 1's receipt + // (another receipt under that name), and a revision 3 whose parent is revision 2 + // changed after it was signed (served under its own receipt hash, which it no longer + // verifies under). + put(c, out, &format!("cases/walk-substituted/mr/{}.json", hex(&sealed[5].receipt_hash())), sealed[4].to_json().into_bytes()); + let mut altered = sealed[5].clone(); + altered.viewers_served = 2; + publish_by_hash(c, out, "cases/walk-invalid/mr", &altered); + put(c, out, "cases/walk-invalid/r3.json", seal(&live(c, 3)?, &key, Some(&altered))?.to_json().into_bytes()); + Ok(()) +} + +/// The grant the corpus seals O10's playlist under: a test grant, attesting nothing. +#[cfg(feature = "video_manifest")] +pub(super) fn publisher_grant() -> wai::quantum_ops::GrantRef { + wai::quantum_ops::GrantRef { grant_hash: *blake3::hash(b"http-conformance: a publisher's test grant").as_bytes(), capability: "video.manifest".into(), + joule_ceiling_micro: 5_000_000, funds_ceiling: Some(250) } +} + +/// A receipt published in `dir` as §8.6 serves it: at `.json` and at +/// `.json`. +#[cfg(feature = "video_manifest")] +fn publish(c: &mut Corpus, out: &mut BTreeMap>, dir: &str, r: &wai::wai_video_manifest::ManifestReceipt) { + let f = format!("{dir}/{}", r.receipt_file_name()); + c.extra.insert(f.clone(), r.to_json().into_bytes()); + out.insert(f, r.to_json().into_bytes()); + publish_by_hash(c, out, dir, r); +} + +/// A receipt at `/.json`, where a walk fetches it by its link. +#[cfg(feature = "video_manifest")] +fn publish_by_hash(c: &mut Corpus, out: &mut BTreeMap>, dir: &str, r: &wai::wai_video_manifest::ManifestReceipt) { + let f = format!("{dir}/{}.json", hex(&r.receipt_hash())); + c.extra.insert(f.clone(), r.to_json().into_bytes()); + out.insert(f, r.to_json().into_bytes()); +} + +// -------------------------------------------------- signatures at the edges + +/// L = 2^252 + 27742317777372353535851937790883648493, in little-endian 64-bit limbs. +const L64: [u64; 4] = [0x5812_631a_5cf5_d3ed, 0x14de_f9de_a2f7_9cd6, 0, 0x1000_0000_0000_0000]; + +/// A little-endian integer of any length, reduced mod L, by binary long division. +fn mod_l(bytes: &[u8]) -> [u8; 32] { + let mut r = [0u64; 4]; + for i in (0..bytes.len() * 8).rev() { + // r < L < 2^253, so 2r + 1 < 2^254 fits in four limbs. + let mut carry = u64::from((bytes[i / 8] >> (i % 8)) & 1); + for limb in r.iter_mut() { + let top = *limb >> 63; + *limb = (*limb << 1) | carry; + carry = top; + } + if (0..4).rev().find(|&j| r[j] != L64[j]).is_none_or(|j| r[j] > L64[j]) { + let mut borrow = 0u64; + for (j, limb) in r.iter_mut().enumerate() { + let (d, b1) = limb.overflowing_sub(L64[j]); + let (d, b2) = d.overflowing_sub(borrow); + *limb = d; + borrow = u64::from(b1 || b2); + } + } + } + let mut out = [0u8; 32]; + for (j, limb) in r.iter().enumerate() { + out[8 * j..8 * j + 8].copy_from_slice(&limb.to_le_bytes()); + } + out +} + +/// k = SHA-512(R ‖ A ‖ M) mod L (RFC 8032 §5.1.7). +fn k_of(r: &[u8; 32], a: &[u8; 32], msg: &[u8]) -> [u8; 32] { + use sha2::{Digest, Sha512}; + let mut h = Sha512::new(); + h.update(r); + h.update(a); + h.update(msg); + mod_l(&h.finalize()) +} + +/// `n` as 32 little-endian bytes. +fn le32(n: u8) -> [u8; 32] { + let mut b = [0u8; 32]; + b[0] = n; + b +} + +/// p + `n` = 2^255 − 19 + `n`, a second spelling of the field element `n` (n < 19). +fn long_spelling(n: u8) -> [u8; 32] { + let mut b = [0xffu8; 32]; + b[0] = 0xed + n; + b[31] = 0x7f; + b +} + +/// The base point B. +const B_ENC: [u8; 32] = [0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, + 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66]; + +type SOfK = fn(&[u8; 32]) -> [u8; 32]; + +/// Each crafted receipt: its file under `cases/`, the key, R, and S as a function of k. +fn crafted_signatures() -> Vec<(&'static str, [u8; 32], [u8; 32], SOfK)> { + let one: SOfK = |_| le32(1); + let k: SOfK = |k| *k; + let one_plus_k: SOfK = |k| { + let mut wide = [0u8; 33]; + wide[..32].copy_from_slice(k); + let mut carry = 1u16; + for b in wide.iter_mut() { + let v = u16::from(*b) + carry; + *b = v as u8; + carry = v >> 8; + } + mod_l(&wide) + }; + vec![ + ("sig-key-identity.json", le32(1), B_ENC, one), + ("sig-key-identity-long.json", long_spelling(1), B_ENC, one), + ("sig-r-small-order.json", B_ENC, le32(1), k), + ("sig-r-non-canonical.json", B_ENC, long_spelling(1), k), + ("sig-key-base-point.json", B_ENC, B_ENC, one_plus_k), + ] +} + +#[cfg(not(feature = "video_manifest"))] +fn seal_all(_: &mut Corpus, _: &mut BTreeMap>) -> Result<(), String> { + Err("built without video_manifest: the corpus's playlist receipts need it (build with --features http,video_manifest)".into()) +} + +/// The receipt file of a playlist, as generated. +fn receipt_of(c: &Corpus, playlist: &str) -> Result { + let b = c.file(playlist)?; + let dir = playlist.rsplit_once('/').map_or("", |(d, _)| d); + Ok(format!("{dir}/mr/{}.json", blake3::hash(&b).to_hex())) +} + +// ------------------------------------------------------------------ the cases + +/// splitmix64, as the differential uses. +fn next(s: &mut u64) -> u64 { + super::next(s) +} + +fn abr_state(s: &mut u64, n_opts: usize, with_budget: bool) -> Value { + let mut sizes: Vec = (0..n_opts).map(|_| 2_000 + next(s) % 400_000).collect(); + sizes.sort_unstable(); + let samples: Vec = (0..next(s) % 6).map(|i| json!([1000 * (i + 1), next(s) % 2_000_000, next(s) % 3000])).collect(); + let figures: Vec = (0..n_opts).map(|_| if next(s) % 3 == 0 { Value::Null } else { json!(1 + next(s) % 5000) }).collect(); + let segment_ms = [200u64, 1000, 2000, 3840, 4000][(next(s) % 5) as usize]; + json!({ + "buffer_ms": next(s) % 40_000, + "samples": samples, + "last": if next(s) % 2 == 0 { Value::Null } else { json!(next(s) % n_opts as u64) }, + "option_bytes": sizes, + "option_joules_micro": figures, + "joule_budget_micro": if with_budget && next(s) % 2 == 0 { json!(next(s) % 6000) } else { Value::Null }, + "segment_ms": segment_ms, + }) +} + +fn policies() -> Vec { + vec![ + json!({ "id": "throughput", "last_n": 3, "safety_permille": 900 }), + json!({ "id": "buffer_based", "reservoir_ms": 5000, "cushion_ms": 10_000 }), + json!({ "id": "bola_like", "v_milli": 930, "max_buffer_ms": 20_000 }), + json!({ "id": "mpc", "horizon": 3, "safety_permille": 850 }), + ] +} + +pub(super) fn cases(c: &Corpus) -> Result, String> { + let mut v = Vec::new(); + // Every playlist renders from its inputs to its committed bytes. + for spec in media_specs() { + let f = spec["file"].as_str().expect("file"); + v.push(case(format!("media_playlist_{}", f.replace(['/', '.', '-'], "_")), "media_playlist", spec.clone())); + } + for spec in multivariant_specs() { + let f = spec["file"].as_str().expect("file"); + v.push(case(format!("multivariant_{}", f.replace(['/', '.', '-'], "_")), "multivariant", spec.clone())); + } + for spec in mpd_specs() { + let f = spec["file"].as_str().expect("file"); + v.push(case(format!("mpd_{}", f.replace(['/', '.', '-'], "_")), "mpd", spec.clone())); + } + // An MPD's ids and values are XML: white space in an @id, and a character outside + // XML, are refused. + for (name, id) in [("mpd_rep_id_white_space", "wai 180"), ("mpd_rep_id_not_a_char", "wai\u{1}180"), ("mpd_rep_id_unicode_space", "wai\u{2009}180")] { + let mut spec = mpd_specs()[0].clone(); + spec["rep_id"] = json!(id); + spec.as_object_mut().expect("object").remove("file"); + v.push(refused(name, "mpd", spec, "playlist_malformed")); + } + // Objects that do not cover a timeline. + let o9 = |segs: Value| json!({ "classical": "src/playlists/o9-classical.m3u8", "uri_prefix": "", "segments": segs }); + v.push(refused("media_playlist_short_O9", "media_playlist", o9(json!([seg("O9a"), seg("O9b")])), "adjacency_misaligned")); + v.push(refused("media_playlist_parts_beside_no_target", "media_playlist", + o9(json!([{ "object": "O9a", "parts": [{ "object": part_id(0, 0), "alias": "p0.wai", "independent": true, "duration_ms": 320 }, + { "object": part_id(0, 1), "alias": "p1.wai", "independent": true, "duration_ms": 320 }] }, + seg("O9b"), seg("O9c")])), "adjacency_misaligned")); + // A gap offers no object, so nothing refines or divides it (Appendix G). + v.push(refused("media_playlist_gap_with_layers", "media_playlist", json!({ "classical": "src/playlists/o11-classical.m3u8", "uri_prefix": "", + "segments": [seg("O11a"), { "object": "O11b", "gap": true, "layers": ["O11bL1"] }] }), "playlist_malformed")); + let gap_render = |extra: Value| json!({ + "segments": [[640, "a.wai", false], [640, "b.wai", false]], + "render": { "version": 9, "media_sequence": 0, "discontinuity_sequence": 0, "target_duration_s": null, "part_target_ms": 320, "part_hold_back_ms": 960, + "can_block_reload": false, "end_list": true, "playlist_type": null, "independent_segments": false, "map_uri": null }, + "extras": [extra, { "discontinuity": false, "gap": false, "program_date_time": null, "parts": [], "layers": [], "preload_hint": null }] }); + let part = json!({ "duration_ms": 320, "independent": true, "uri": "p.wai", "layers": [] }); + v.push(refused("render_gap_with_parts", "render", gap_render(json!({ "discontinuity": false, "gap": true, "program_date_time": null, "parts": [part], "layers": [], "preload_hint": null })), "playlist_malformed")); + v.push(refused("render_gap_with_layers", "render", gap_render(json!({ "discontinuity": false, "gap": true, "program_date_time": null, "parts": [], "layers": [{ "n": 1, "uri": "l.wai" }], "preload_hint": null })), "playlist_malformed")); + v.push(case("render_gap", "render", gap_render(json!({ "discontinuity": false, "gap": true, "program_date_time": null, "parts": [], "layers": [], "preload_hint": null })))); + v.push(refused("media_playlist_layer_not_a_layer", "media_playlist", + o9(json!([{ "object": "O9a", "layers": ["O9b"] }, seg("O9b"), seg("O9c")])), "not_a_layer")); + v.push(refused("media_playlist_open_without_classical", "media_playlist", json!({ + "classical": "src/playlists/o9-classical.m3u8", "uri_prefix": "", "segments": [seg("O9a"), seg("O9b"), seg("O9c")], + "open": { "parts": [{ "object": part_id(1, 0), "alias": "p.wai", "independent": true, "duration_ms": 320 }] } }), "adjacency_misaligned")); + + // Reading: every WAI and classical playlist, and malformed ones. + for f in ["playlists/o9/wai-180.m3u8", "playlists/o9/wai-180-gap.m3u8", "playlists/o10/wai-a.m3u8", "playlists/o11/wai-v.m3u8", "live/wai-r0.m3u8", "live/wai-r1.m3u8", + "live/wai-r2.m3u8", "live/wai-r3.m3u8", "players/wai-v0.m3u8", "src/live/classical-r1.m3u8", "players/classical/v0.m3u8", + "src/playlists/o9-classical.m3u8"] { + v.push(case(format!("playlist_parse_{}", f.replace(['/', '.', '-'], "_")), "playlist_parse", json!({ "file": f }))); + } + for (name, t) in [ + ("playlist_parse_no_header", "#EXT-X-VERSION:3\n#EXTINF:1,\na\n"), + ("playlist_parse_extinf_no_uri", "#EXTM3U\n#EXTINF:1,\n"), + ("playlist_parse_uri_no_extinf", "#EXTM3U\na.wai\n"), + ("playlist_parse_skip", "#EXTM3U\n#EXT-X-SKIP:SKIPPED-SEGMENTS=3\n"), + ("playlist_parse_variant", "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\na.m3u8\n"), + ("playlist_parse_sequence_late", "#EXTM3U\n#EXTINF:1,\na\n#EXT-X-MEDIA-SEQUENCE:3\n"), + ("playlist_parse_layer_after_uri", "#EXTM3U\n#EXTINF:1,\na\n#WAI-LAYER:N=1,URI=\"l.wai\"\n"), + ("playlist_parse_layer_order", "#EXTM3U\n#EXTINF:1,\n#WAI-LAYER:N=2,URI=\"l.wai\"\n#WAI-LAYER:N=1,URI=\"m.wai\"\na\n"), + ("playlist_parse_layer_comment_between", "#EXTM3U\n#EXTINF:1,\n# a note\n#WAI-LAYER:N=1,URI=\"l.wai\"\na\n"), + ("playlist_parse_part_no_uri", "#EXTM3U\n#EXT-X-PART:DURATION=0.5\n"), + ("playlist_parse_two_hints", "#EXTM3U\n#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"a\"\n#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"b\"\n"), + ("playlist_parse_negative", "#EXTM3U\n#EXTINF:-1,\na\n"), + ("playlist_parse_exponent", "#EXTM3U\n#EXTINF:1e3,\na\n"), + ("playlist_parse_block_maybe", "#EXTM3U\n#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=MAYBE\n"), + ("playlist_parse_type_live", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:LIVE\n"), + ("playlist_parse_map_no_uri", "#EXTM3U\n#EXT-X-MAP:BYTERANGE=\"1@0\"\n"), + ] { + v.push(refused(name, "playlist_parse", json!({ "text": t }), "playlist_malformed")); + } + v.push(refused("playlist_parse_layer_zero", "playlist_parse", json!({ "text": "#EXTM3U\n#EXTINF:1,\n#WAI-LAYER:N=0,URI=\"l.wai\"\na\n" }), "layer_tag_malformed")); + for (name, t) in [ + ("playlist_parse_rounding", "#EXTM3U\n#EXTINF:3.9995,\na\n#EXTINF:3.99949,\nb\n#EXTINF:4.00008,\nc\n#EXTINF:4,\nd\n"), + ("playlist_parse_crlf_title", "#EXTM3U\r\n#EXTINF:0.64,a title\r\nseg.m4s\r\n#EXT-X-DISCONTINUITY\r\n#EXTINF:1,\r\nseg2.m4s\r\n"), + ("playlist_parse_hint_only", "#EXTM3U\n#EXT-X-PART-INF:PART-TARGET=1\n#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"p.0\"\n#EXT-X-PRELOAD-HINT:TYPE=MAP,URI=\"i\"\n"), + ("playlist_parse_packager_lines", "#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-TARGETDURATION:3\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-PLAYLIST-TYPE:EVENT\n#EXT-X-INDEPENDENT-SEGMENTS\n#EXT-X-MAP:URI=\"i.mp4\"\n#EXT-X-GAP\n#EXTINF:2.400,\na.m4s\n#EXT-X-MAP:URI=\"j.mp4\"\n#EXTINF:1.000,\nb.m4s\n"), + ] { + v.push(case(name, "playlist_parse", json!({ "text": t }))); + } + + // Adjacency (§11.2): each WAI playlist beside its classical one, and every way it + // can be broken. + for (w, k) in [("playlists/o9/wai-180.m3u8", "src/playlists/o9-classical.m3u8"), ("playlists/o9/wai-180-gap.m3u8", "src/playlists/o9-classical.m3u8"), ("playlists/o10/wai-a.m3u8", "src/playlists/o10-classical.m3u8"), + ("playlists/o11/wai-v.m3u8", "src/playlists/o11-classical.m3u8"), ("live/wai-r0.m3u8", "src/live/classical-r0.m3u8"), + ("live/wai-r1.m3u8", "src/live/classical-r1.m3u8"), ("live/wai-r2.m3u8", "src/live/classical-r2.m3u8"), + ("live/wai-r3.m3u8", "src/live/classical-r3.m3u8"), ("players/wai-v0.m3u8", "players/classical/v0.m3u8")] { + v.push(case(format!("adjacency_{}", w.replace(['/', '.', '-'], "_")), "adjacency", json!({ "wai": w, "classical": k }))); + } + let o9a = |name: &str, edits: Value| refused(name, "adjacency", json!({ "wai": "playlists/o9/wai-180.m3u8", "classical": "src/playlists/o9-classical.m3u8", "edit_wai": edits }), "adjacency_misaligned"); + v.push(o9a("adjacency_extinf_shifted", json!([["#EXTINF:0.640,", "#EXTINF:0.641,"]]))); + v.push(o9a("adjacency_discontinuity_one_side", json!([["#EXTINF:0.640,", "#EXT-X-DISCONTINUITY\n#EXTINF:0.640,"]]))); + v.push(o9a("adjacency_media_sequence", json!([["#EXT-X-MEDIA-SEQUENCE:0", "#EXT-X-MEDIA-SEQUENCE:1"]]))); + v.push(o9a("adjacency_discontinuity_sequence", json!([["#EXT-X-MEDIA-SEQUENCE:0\n", "#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-DISCONTINUITY-SEQUENCE:1\n"]]))); + v.push(o9a("adjacency_segment_missing", json!([["#EXTINF:0.640,\n../../objects/", "#EXT-X-BYTERANGE:1\n#EXT-X-GAP\n#EXTINF:0.640,\n../../objects/"], ["#EXT-X-ENDLIST", "#EXTINF:0.640,\nextra.wai\n#EXT-X-ENDLIST"]]))); + v.push(refused("adjacency_classical_discontinuity", "adjacency", json!({ "wai": "playlists/o9/wai-180.m3u8", "classical": "src/playlists/o9-classical.m3u8", + "edit_classical": [["#EXTINF:0.64,\nseg-180-2", "#EXT-X-DISCONTINUITY\n#EXTINF:0.64,\nseg-180-2"]] }), "adjacency_misaligned")); + let r1 = |name: &str, edits: Value| refused(name, "adjacency", json!({ "wai": "live/wai-r1.m3u8", "classical": "src/live/classical-r1.m3u8", "edit_wai": edits }), "adjacency_misaligned"); + v.push(r1("adjacency_part_duration", json!([["#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI=\"720/1200.4.wai", "#EXT-X-PART:DURATION=0.319,INDEPENDENT=YES,URI=\"720/1200.4.wai"]]))); + v.push(r1("adjacency_open_part_duration", json!([["#EXT-X-PART:DURATION=0.320,INDEPENDENT=YES,URI=\"720/1201.2.wai", "#EXT-X-PART:DURATION=0.330,INDEPENDENT=YES,URI=\"720/1201.2.wai"]]))); + v.push(r1("adjacency_part_target", json!([["PART-TARGET=0.320", "PART-TARGET=0.330"]]))); + v.push(r1("adjacency_program_date_time", json!([["2026-10-01T12:00:00.000Z", "2026-10-01T12:00:00.000+00:00"]]))); + let layer_line = "#EXTINF:3.840,\n#WAI-LAYER:N=1,URI=\"../objects/l.wai#n=9\"\n"; + v.push(case("adjacency_layer_line_ignored", "adjacency", json!({ "wai": "live/wai-r1.m3u8", "classical": "src/live/classical-r1.m3u8", "edit_wai": [["#EXTINF:3.840,\n", layer_line]] }))); + v.push(case("adjacency_live_s_beside_live_p", "adjacency", json!({ "wai": "live/wai-r3.m3u8", "classical": "src/live/classical-r3.m3u8", + "edit_wai": [["#EXT-X-ENDLIST\n", ""]], "edit_classical": [["#EXT-X-ENDLIST\n", ""]] }))); + v.push(refused("adjacency_malformed_wai", "adjacency", json!({ "wai": "live/wai-r1.m3u8", "classical": "src/live/classical-r1.m3u8", "edit_wai": [["#EXTM3U", "#EXTM3X"]] }), "playlist_malformed")); + + // DASH adjacency (§11.2, Appendix I): wai.mpd beside the classical MPD it was rendered + // on, a gap left out, and every way it can be broken or fail to read. + let mpd_pair = |w: &str| json!({ "wai": w, "classical": "src/playlists/o9-classical.mpd" }); + v.push(case("dash_adjacency_o9", "dash_adjacency", mpd_pair("playlists/o9/wai.mpd"))); + v.push(case("dash_adjacency_o9_gap", "dash_adjacency", mpd_pair("playlists/o9/wai-gap.mpd"))); + let dash = |name: &str, ew: Value, ec: Value, code: Option<&'static str>| { + let mut i = mpd_pair("playlists/o9/wai.mpd"); + i["edit_wai"] = ew; + i["edit_classical"] = ec; + match code { Some(code) => refused(name, "dash_adjacency", i, code), None => case(name, "dash_adjacency", i) } + }; + let none = json!([]); + let mis = Some("adjacency_misaligned"); + let bad = Some("playlist_malformed"); + for (name, ew, ec, code) in [ + ("dash_adjacency_period_id", json!([["", ""]]), none.clone(), mis), + ("dash_adjacency_t", json!([["", ""]]), none.clone(), mis), + ("dash_adjacency_extra_segment", json!([["", ""]]), none.clone(), mis), + ("dash_adjacency_out_of_order", json!([["\n ", "\n "]]), none.clone(), mis), + ("dash_adjacency_written_with_r", json!([["\n \n ", ""]]), none.clone(), None), + ("dash_adjacency_classical_written_out", none.clone(), json!([["", ""]]), None), + ("dash_adjacency_classical_shorter", none.clone(), json!([["d=\"57600\" r=\"2\"", "d=\"57600\" r=\"1\""]]), mis), + ("dash_adjacency_no_timeline", json!([["", ""]]), none.clone(), mis), + ("dash_adjacency_crlf_and_comment", json!([["\r\n \n", ""]]), none.clone(), bad), + ("dash_adjacency_unclosed", json!([["", ""]]), none.clone(), bad), + ("dash_adjacency_r_negative", none.clone(), json!([["r=\"2\"", "r=\"-1\""]]), bad), + ("dash_adjacency_classical_r_huge", none.clone(), json!([["d=\"57600\" r=\"2\"", "d=\"57600\" r=\"300000000000000\""]]), None), + ("dash_adjacency_r_end_past_u64", none.clone(), json!([["d=\"57600\" r=\"2\"", "d=\"57600\" r=\"18446744073709551614\""]]), bad), + ("dash_adjacency_wai_r_huge", json!([["", ""]]), none.clone(), mis), + ("dash_adjacency_timelines_past_the_subset", none.clone(), json!([["".repeat(256))]]), bad), + ("dash_adjacency_t_past_u64", none.clone(), json!([["", ""]]), bad), + ("dash_adjacency_start_in_months", json!([["start=\"PT0S\"", "start=\"P0M\""]]), none.clone(), bad), + ("dash_adjacency_not_an_mpd", json!([["", ""]]), none.clone(), bad), + ("dash_adjacency_zero_d", none.clone(), json!([["d=\"30720\"", "d=\"0\""]]), bad), + ] { + v.push(dash(name, ew, ec, code)); + } + + // URLs (§11.5): RFC 3986 §5.4's examples against its base, and spellings of a live + // alias against a playlist's URL. + for (i, r) in ["g:h", "g", "./g", "g/", "/g", "//g", "?y", "g?y", "#s", "g#s", "g?y#s", ";x", "g;x", "g;x?y#s", "", ".", "./", "..", "../", + "../g", "../..", "../../", "../../g", "../../../g", "../../../../g", "/./g", "/../g", "g.", ".g", "g..", "..g", "./../g", "./g/.", + "g/./h", "g/../h", "g;x=1/./y", "g;x=1/../y", "g?y/./x", "g?y/../x", "g#s/./x", "g#s/../x", "http:g"].iter().enumerate() { + v.push(case(format!("uri_resolve_rfc_{i:02}"), "uri_resolve", json!({ "base": "http://a/b/c/d;p?q", "reference": r }))); + } + for (i, r) in ["720/1201.0.wai#cid=00", "./720/1201.0.wai", "720/x/../1201.0.wai", "%37%32%30/1201.0.wai", "720/1201%2e0.wai", "720/%2E/1201.0.wai", + "720%2F1201.0.wai", "HTTPS://ORIGIN.EXAMPLE/live/720/1201.0.wai", "//Origin.Example:443/live/720/1201.0.wai", "720/1201.0.wai?x=%7e%41", + "../../../720/1201.0.wai", "a:b/c", "./a:b", "user@x", "//u%41@Host.example/p"].iter().enumerate() { + v.push(case(format!("uri_resolve_alias_{i:02}"), "uri_resolve", json!({ "base": LIVE_URL, "reference": r }))); + } + // The classical master, published with at most the session-data line (§11.1). + let un = |edits: Value| json!({ "original": "src/players/master.m3u8", "published": "players/master.m3u8", "edit_published": edits }); + v.push(case("unchanged_players", "unchanged", un(json!([])))); + v.push(case("unchanged_original_itself", "unchanged", json!({ "original": "src/players/master.m3u8", "published": "src/players/master.m3u8" }))); + let sd = "#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"\n"; + for (name, edits) in [ + ("unchanged_two_lines", json!([[sd, format!("{sd}{sd}")]])), + ("unchanged_bandwidth_changed", json!([["BANDWIDTH=10840", "BANDWIDTH=10841"]])), + ("unchanged_other_data_id", json!([["science.transaction.wai.multivariant", "science.transaction.wai.other"]])), + ("unchanged_trailing_space", json!([["VALUE=\"wai.m3u8\"\n", "VALUE=\"wai.m3u8\" \n"]])), + ("unchanged_first_line", json!([[sd, ""], ["#EXTM3U\n", format!("{sd}#EXTM3U\n")]])), + ("unchanged_before_version", json!([[sd, ""], ["#EXTM3U\n", format!("#EXTM3U\n{sd}")]])), + ("unchanged_between_variant_and_uri", json!([[sd, ""], ["classical/v0.m3u8", format!("{sd}classical/v0.m3u8")]])), + ("unchanged_at_the_end", json!([[sd, ""], ["classical/v0.m3u8\n", format!("classical/v0.m3u8\n{sd}")]])), + ("unchanged_blank_line_too", json!([[sd, format!("{sd}\n")]])), + ] { + v.push(refused(name, "unchanged", un(edits), "classical_changed")); + } + v.push(case("unchanged_crlf_line", "unchanged", un(json!([[sd, sd.replace('\n', "\r\n")]])))); + // A master with neither EXT-X-VERSION nor EXT-X-INDEPENDENT-SEGMENTS: the line follows + // the first. + let bare = "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\nv.m3u8\n"; + v.push(case("unchanged_after_the_first_line", "unchanged", json!({ "original_text": bare, "published_text": format!("#EXTM3U\n{sd}#EXT-X-STREAM-INF:BANDWIDTH=1\nv.m3u8\n") }))); + v.push(refused("unchanged_bare_after_the_variant", "unchanged", json!({ "original_text": bare, "published_text": format!("#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1\n{sd}v.m3u8\n") }), "classical_changed")); + let unterminated = "#EXTM3U\n#EXT-X-VERSION:7"; + v.push(refused("unchanged_no_place", "unchanged", json!({ "original_text": unterminated, "published_text": format!("#EXTM3U\n#EXT-X-VERSION:7\n{sd}") }), "classical_changed")); + + // Playlist receipts (§8.6): each, and each way one is refused. + for (p, _, _) in receipt_specs() { + let r = receipt_of(c, p)?; + v.push(case(format!("playlist_receipt_{}", p.replace(['/', '.', '-'], "_")), "playlist_receipt", json!({ "playlist": p, "receipt": r }))); + } + let o9r = receipt_of(c, "playlists/o9/wai-180.m3u8")?; + let base = |extra: Value| { + let mut i = json!({ "playlist": "playlists/o9/wai-180.m3u8", "receipt": o9r }); + for (k, x) in extra.as_object().expect("object") { + i[k] = x.clone(); + } + i + }; + v.push(refused("playlist_receipt_name", "playlist_receipt", base(json!({ "name": "0000.json" })), "receipt_name_mismatch")); + v.push(refused("playlist_receipt_playlist_changed", "playlist_receipt", base(json!({ "edit_playlist": [["#EXTINF:0.640,", "#EXTINF:0.641,"]] })), "playlist_receipt_mismatch")); + v.push(refused("playlist_receipt_viewers", "playlist_receipt", base(json!({ "edit_receipt": [["\"viewers_served\":1", "\"viewers_served\":2"]] })), "playlist_receipt_invalid")); + v.push(refused("playlist_receipt_class", "playlist_receipt", base(json!({ "edit_receipt": [["\"entitlement\":\"public\"", "\"entitlement\":\"svod\""]] })), "playlist_receipt_invalid")); + let default_extra = "{\"discontinuity\":false,\"gap\":false,\"program_date_time\":null,\"parts\":[],\"layers\":[],\"preload_hint\":null}"; + v.push(refused("playlist_receipt_extras_equivalent", "playlist_receipt", base(json!({ "edit_receipt": [["\"extras\":[]", format!("\"extras\":[{default_extra},{default_extra},{default_extra}]")]] })), "playlist_receipt_invalid")); + v.push(refused("playlist_receipt_kind", "playlist_receipt", base(json!({ "edit_receipt": [["\"kind\":\"wai.video.manifest\"", "\"kind\":\"wai.video.package\""]] })), "playlist_receipt_malformed")); + v.push(refused("playlist_receipt_unknown_member", "playlist_receipt", base(json!({ "edit_receipt": [["{\"kind\"", "{\"note\":1,\"kind\""]] })), "playlist_receipt_malformed")); + // A member twice in one object, however placed or spelled, and a lone surrogate: a + // reader keeping the first and one keeping the last would read two receipts. + for (name, from, to) in [ + ("playlist_receipt_duplicate_first", "{\"kind\"", "{\"signer_id\":\"did:example:other\",\"kind\""), + ("playlist_receipt_duplicate_last", "\"signer_id\":\"did:example:http-vectors\"", "\"signer_id\":\"did:example:http-vectors\",\"signer_id\":\"did:example:other\""), + ("playlist_receipt_duplicate_same_value", "\"signer_id\":\"did:example:http-vectors\"", "\"signer_id\":\"did:example:http-vectors\",\"signer_id\":\"did:example:http-vectors\""), + ("playlist_receipt_duplicate_in_class", "\"geo\":\"\"", "\"geo\":\"\",\"geo\":\"\""), + ("playlist_receipt_duplicate_escaped_name", "{\"kind\"", "{\"\\u0073igner_id\":\"did:example:other\",\"kind\""), + ("playlist_receipt_lone_surrogate", "\"geo\":\"\"", "\"geo\":\"\\ud800\""), + ("playlist_receipt_lone_low_surrogate", "\"signer_id\":\"did:example:http-vectors\"", "\"signer_id\":\"did:example:http-vectors\\udfff\""), + ] { + v.push(refused(name, "playlist_receipt", base(json!({ "edit_receipt": [[from, to]] })), "playlist_receipt_malformed")); + } + v.push(case("playlist_receipt_surrogate_pair", "playlist_receipt", base(json!({ "edit_receipt": [["\"geo\":\"\"", "\"geo\":\"\\ud83d\\ude00\""]] })))); + // The grant's ceilings (Appendix H): none written null, a number at most 2^53 - 1. + for (name, from, to, code) in [ + ("playlist_receipt_ceiling_past_2_53", "\"joule_ceiling_micro\":null", "\"joule_ceiling_micro\":9007199254740992", "playlist_receipt_malformed"), + ("playlist_receipt_ceiling_written_out", "\"joule_ceiling_micro\":null", "\"joule_ceiling_micro\":18446744073709551615", "playlist_receipt_malformed"), + ("playlist_receipt_ceiling_number_for_none", "\"joule_ceiling_micro\":null", "\"joule_ceiling_micro\":9007199254740991", "playlist_receipt_invalid"), + ("playlist_receipt_window_past_2_53", "\"window\":0", "\"window\":9007199254740992", "playlist_receipt_malformed"), + ] { + v.push(refused(name, "playlist_receipt", base(json!({ "edit_receipt": [[from, to]] })), code)); + } + let o10r = receipt_of(c, "playlists/o10/wai-a.m3u8")?; + v.push(refused("playlist_receipt_grant_ceiling_changed", "playlist_receipt", json!({ "playlist": "playlists/o10/wai-a.m3u8", "receipt": o10r, + "edit_receipt": [["\"joule_ceiling_micro\":5000000", "\"joule_ceiling_micro\":5000001"]] }), "playlist_receipt_invalid")); + v.push(refused("playlist_receipt_grant_funds_dropped", "playlist_receipt", json!({ "playlist": "playlists/o10/wai-a.m3u8", "receipt": o10r, + "edit_receipt": [["\"funds_ceiling\":250", "\"funds_ceiling\":null"]] }), "playlist_receipt_invalid")); + // Ed25519 at its edges (§8.6 step 4, strictly): keys and R a lenient verifier accepts. + for (file, _, _, _) in crafted_signatures() { + let name = format!("playlist_receipt_{}", file.trim_end_matches(".json").replace('-', "_")); + let input = json!({ "playlist": "playlists/o9/wai-180.m3u8", "receipt": format!("cases/{file}"), "name": false }); + v.push(if file == "sig-key-base-point.json" { case(name, "playlist_receipt", input) } else { refused(&name, "playlist_receipt", input, "playlist_receipt_invalid") }); + } + let r1r = receipt_of(c, "live/wai-r1.m3u8")?; + v.push(refused("playlist_receipt_part_independent", "playlist_receipt", json!({ "playlist": "live/wai-r1.m3u8", "receipt": r1r, + "edit_playlist": [["DURATION=0.320,INDEPENDENT=YES,URI=\"720/1200.3", "DURATION=0.320,URI=\"720/1200.3"]] }), "playlist_receipt_mismatch")); + v.push(refused("playlist_receipt_parts_reordered", "playlist_receipt", json!({ "playlist": "live/wai-r1.m3u8", "receipt": r1r, + "edit_receipt": [["\"uri\":\"720/1200.3.wai", "\"uri\":\"720/1200.X.wai"], ["\"uri\":\"720/1200.4.wai", "\"uri\":\"720/1200.3.wai"], ["\"uri\":\"720/1200.X.wai", "\"uri\":\"720/1200.4.wai"]] }), "playlist_receipt_mismatch")); + + // A live edge (§11.4, §11.6): four revisions, each adjacent, signed and chained; parts + // fetched by alias before and after the revision that lists them. + let rev = |k: usize| -> Result { + let w = format!("live/wai-r{k}.m3u8"); + Ok(json!({ "wai": w, "receipt": receipt_of(c, &w)?, "classical": format!("src/live/classical-r{k}.m3u8") })) + }; + let revs_of = |ks: &[usize]| -> Result, String> { ks.iter().map(|&k| rev(k)).collect() }; + let with = |mut r: Value, extra: Value| { + for (k, x) in extra.as_object().expect("object") { + r[k] = x.clone(); + } + r + }; + let fetches = json!([ + { "alias": "720/1200.0.wai", "object": part_id(0, 0) }, + { "alias": "720/1200.5.wai", "object": part_id(0, 5) }, + { "alias": "720/1201.3.wai", "object": part_id(1, 3) }, + { "alias": "720/1201.7.wai", "object": part_id(1, 8) }, + { "alias": "720/1200.2.wai", "object": part_id(0, 2), "patch_object": [[400, "00"]] }, + { "alias": "720/1202.0.wai", "object": part_id(1, 11) }, + { "alias": "1200.m4s", "object": "O12s0" }, + ]); + v.push(case("live_edge", "live", json!({ "revisions": revs_of(&[0, 1, 2, 3])?, "fetches": fetches }))); + let cid8 = hex(&content_hash(&c.objects[&part_id(1, 8)])); + let cid7 = hex(&content_hash(&c.objects[&part_id(1, 7)])); + let mut changed = revs_of(&[0, 1, 2, 3])?; + changed[2]["edit_wai"] = json!([[format!("720/1201.7.wai#cid={cid7}"), format!("720/1201.7.wai#cid={cid8}")]]); + v.push(case("live_edge_revision_changed", "live", json!({ "revisions": changed, + "fetches": [{ "alias": "720/1201.7.wai", "object": part_id(1, 8) }, { "alias": "720/1201.3.wai", "object": part_id(1, 3) }] }))); + let mut orphaned = revs_of(&[0, 1, 2, 3])?; + orphaned[2]["receipt"] = json!("cases/live-r2-orphan.json"); + v.push(case("live_edge_unchained", "live", json!({ "revisions": orphaned, + "fetches": [{ "alias": "720/1201.3.wai", "object": part_id(1, 3) }, { "alias": "720/1200.0.wai", "object": part_id(0, 0) }] }))); + // What an origin's replay gives a polling client: a revision fetched again unchanged, + // and revisions skipped, whose receipts the client walks back by their links. + let few = json!([{ "alias": "720/1200.0.wai", "object": part_id(0, 0) }, { "alias": "720/1201.3.wai", "object": part_id(1, 3) }]); + v.push(case("live_edge_unchanged_reload", "live", json!({ "revisions": revs_of(&[0, 1, 1, 2, 2, 3])?, "fetches": few }))); + v.push(case("live_edge_skip", "live", json!({ "revisions": revs_of(&[0, 2, 3])?, "fetches": few }))); + v.push(case("live_edge_skip_two", "live", json!({ "revisions": revs_of(&[0, 3])?, "fetches": few }))); + // Past the bound, a revision ahead of the head re-anchors the chain; one that is not + // ahead is refused; a stale reload changes nothing. + v.push(case("live_edge_reanchored", "live", json!({ "revisions": revs_of(&[0, 3])?, "walk_bound": 1, "fetches": few }))); + v.push(case("live_edge_reanchored_then_reloaded", "live", json!({ "revisions": revs_of(&[0, 3, 3, 3])?, "walk_bound": 1, "fetches": few }))); + v.push(case("live_edge_reanchored_skipping", "live", json!({ "revisions": revs_of(&[0, 2])?, "walk_bound": 0, "fetches": few }))); + v.push(case("live_edge_reanchor_not_backwards", "live", json!({ "revisions": revs_of(&[0, 3, 1])?, "walk_bound": 0, "fetches": few }))); + // The walk's checks of each parent: another receipt under the link's name, and one + // that does not verify. + v.push(case("live_edge_parent_substituted", "live", json!({ "revisions": revs_of(&[0, 3])?, "parents": "cases/walk-substituted/mr", "fetches": few }))); + let invalid_parent = vec![rev(0)?, with(rev(3)?, json!({ "receipt": "cases/walk-invalid/r3.json" }))]; + v.push(case("live_edge_parent_invalid", "live", json!({ "revisions": invalid_parent, "parents": "cases/walk-invalid/mr", "fetches": few }))); + v.push(case("live_edge_walk_at_bound", "live", json!({ "revisions": revs_of(&[0, 3])?, "walk_bound": 2, "fetches": few }))); + v.push(case("live_edge_parent_missing", "live", json!({ "revisions": revs_of(&[0, 3])?, "parents": "cases/no-receipts", "fetches": few }))); + // Chained by hash, signed by another key: as the next revision, and as a parent walked. + let mut other = revs_of(&[0, 1, 2, 3])?; + other[2]["receipt"] = json!("cases/other-key/r2.json"); + v.push(case("live_edge_other_key", "live", json!({ "revisions": other, "fetches": few }))); + let walked_other = vec![rev(0)?, rev(1)?, with(rev(3)?, json!({ "receipt": "cases/other-key/r3.json" }))]; + v.push(case("live_edge_other_key_walked", "live", json!({ "revisions": walked_other, "parents": "cases/other-key/mr", "fetches": few }))); + // A revision that lists another cid for an alias or a media sequence number an earlier + // one bound (§5.2, §11.5): in its playlist, and in a receipt walked as a parent. + let relisted = vec![rev(0)?, rev(1)?, with(rev(2)?, json!({ "wai": "cases/relist/wai-r2.m3u8", "receipt": "cases/relist/r2.json" })), rev(3)?]; + let relist_fetches = json!([{ "alias": "720/1201.0.wai", "object": part_id(1, 0) }, { "alias": "720/1201.3.wai", "object": part_id(1, 3) }]); + v.push(case("live_relisted_alias", "live", json!({ "revisions": relisted, "fetches": relist_fetches }))); + let relisted_walked = vec![rev(0)?, rev(1)?, with(rev(3)?, json!({ "receipt": "cases/relist/r3-after.json" }))]; + v.push(case("live_relisted_alias_walked", "live", json!({ "revisions": relisted_walked, "parents": "cases/relist/mr", "fetches": relist_fetches }))); + let resequenced = vec![rev(0)?, rev(1)?, rev(2)?, with(rev(3)?, json!({ "wai": "cases/relist/wai-r3.m3u8", "receipt": "cases/relist/r3.json" }))]; + v.push(case("live_resequenced", "live", json!({ "revisions": resequenced, "fetches": few }))); + // An alias is bound by the URL it names, not its spelling: ./720/1201.0.wai with + // another object is refused; with its own, it is the same binding. + let dotted = vec![rev(0)?, rev(1)?, with(rev(2)?, json!({ "wai": "cases/relist/wai-r2-dot.m3u8", "receipt": "cases/relist/r2-dot.json" }))]; + v.push(case("live_relisted_dot_alias", "live", json!({ "revisions": dotted, "fetches": relist_fetches }))); + let same = vec![rev(0)?, rev(1)?, with(rev(2)?, json!({ "wai": "cases/relist/wai-r2-dot-same.m3u8", "receipt": "cases/relist/r2-dot-same.json" }))]; + v.push(case("live_dot_alias_same_object", "live", json!({ "revisions": same, "fetches": relist_fetches }))); + // A listed segment is not withdrawn as a gap. + let withdrawn = vec![rev(0)?, rev(1)?, rev(2)?, with(rev(3)?, json!({ "wai": "cases/relist/wai-r3-gap.m3u8", "receipt": "cases/relist/r3-gap.json" }))]; + v.push(case("live_gap_withdraws_a_segment", "live", json!({ "revisions": withdrawn, "fetches": few }))); + + // Adaptation (Appendix F): pinned states per policy, seeded; with and without a + // budget; and the budget's every outcome. + let mut s = 0x5741_495f_4142_5221u64; + for p in policies() { + let id = p["id"].as_str().expect("id").to_string(); + for i in 0..40 { + let n = 1 + (next(&mut s) % 6) as usize; + v.push(case(format!("abr_{id}_{i:02}"), "abr", json!({ "policy": p, "state": abr_state(&mut s, n, i % 2 == 1) }))); + } + } + let st = |buffer: u64, bps: u64, opts: Value, figs: Value, budget: Value| json!({ + "buffer_ms": buffer, "samples": [[1000, bps / 8, 1000]], "last": null, "option_bytes": opts, + "option_joules_micro": figs, "joule_budget_micro": budget, "segment_ms": 1000 }); + let opts = json!([10_000, 100_000, 1_000_000]); + let thr = json!({ "id": "throughput", "last_n": 1, "safety_permille": 900 }); + // A budget never raises the policy's choice: the highest option at or below it with + // a labelled figure within the budget, else option 0 unmet; not applied only when no + // option has a labelled figure. At 10 kb/s the throughput policy chooses option 0. + for (name, bps, figs, budget) in [ + ("abr_budget_not_set", 100_000_000u64, json!([10, 50, 500]), Value::Null), + ("abr_budget_not_applied", 100_000_000, json!([null, null, null]), json!(100)), + ("abr_budget_within", 100_000_000, json!([10, 50, 100]), json!(100)), + ("abr_budget_lowered", 100_000_000, json!([10, 50, 500]), json!(100)), + ("abr_budget_lowered_past_unlabelled", 100_000_000, json!([10, null, 500]), json!(100)), + ("abr_budget_unlabelled_choice", 100_000_000, json!([10, 50, null]), json!(100)), + ("abr_budget_unmet", 100_000_000, json!([200, null, 500]), json!(100)), + ("abr_budget_unmet_not_the_least_figure", 100_000_000, json!([null, 300, 200]), json!(100)), + ("abr_budget_not_raised_to_labelled_options", 10_000, json!([null, 50, 80]), json!(100)), + ("abr_budget_not_raised_past_the_budget", 10_000, json!([null, null, 7000]), json!(1000)), + ("abr_budget_not_raised_past_the_budget_no_budget", 10_000, json!([null, null, 7000]), Value::Null), + ("abr_budget_not_raised_to_one_within", 10_000, json!([null, 900, null]), json!(1000)), + ("abr_budget_figures_past_the_options", 100_000_000, json!([null, null, null, 5]), json!(100)), + ] { + v.push(case(name, "abr", json!({ "policy": thr, "state": st(0, bps, opts.clone(), figs, budget) }))); + } + // Appendix F's bounds: each value within its field's type, at most 256 options; and + // every policy at the extremes of those types, computed exactly. + let edge = json!({ "buffer_ms": u32::MAX, "samples": (0..300u64).map(|i| json!([i, u64::MAX - i, if i % 2 == 0 { 0 } else { 2 }])).collect::>(), + "last": 1, "option_bytes": [1, u64::MAX / 3, u64::MAX - 1, u64::MAX], "option_joules_micro": [u64::MAX, null, 1, u64::MAX - 1], + "joule_budget_micro": null, "segment_ms": 1 }); + let extreme_policies = [ + json!({ "id": "throughput", "last_n": 255, "safety_permille": 65_535 }), + json!({ "id": "buffer_based", "reservoir_ms": 0, "cushion_ms": u32::MAX }), + json!({ "id": "bola_like", "v_milli": u32::MAX, "max_buffer_ms": u32::MAX }), + json!({ "id": "mpc", "horizon": 255, "safety_permille": 65_535 }), + ]; + for p in &extreme_policies { + for seg in [0u64, 1, u64::from(u32::MAX)] { + let mut e = edge.clone(); + e["segment_ms"] = json!(seg); + v.push(case(format!("abr_extreme_{}_{seg}", p["id"].as_str().expect("id")), "abr", json!({ "policy": p, "state": e }))); + } + } + // Two options whose cross-multiplied scores pass 2^128: a 128-bit product wraps and + // picks the other. + v.push(case("abr_extreme_bola_like_past_128_bits", "abr", json!({ "policy": { "id": "bola_like", "v_milli": u32::MAX, "max_buffer_ms": 596_001_515u32 }, + "state": { "buffer_ms": 129, "samples": [], "last": null, "option_bytes": [3_997_960_217_448_864_544u64, 16_001_797_029_153_315_873u64], + "option_joules_micro": [null, null], "joule_budget_micro": null, "segment_ms": 1 } }))); + let mut budgeted = edge.clone(); + budgeted["joule_budget_micro"] = json!(u64::MAX - 1); + v.push(case("abr_extreme_budget", "abr", json!({ "policy": extreme_policies[2], "state": budgeted }))); + let mut buffered = edge.clone(); + buffered["buffer_ms"] = json!(0); + buffered["samples"] = json!([[1, 1, 1]]); + v.push(case("abr_extreme_mpc_stalls", "abr", json!({ "policy": extreme_policies[3], "state": buffered }))); + let base = st(0, 100_000, opts.clone(), json!([null, null, null]), Value::Null); + for (name, key, value) in [ + ("abr_out_of_bounds_buffer", "buffer_ms", json!(1u64 << 32)), + ("abr_out_of_bounds_segment", "segment_ms", json!(1u64 << 32)), + ("abr_out_of_bounds_segment_fraction", "segment_ms", json!(1000.5)), + ("abr_out_of_bounds_budget", "joule_budget_micro", json!(18_446_744_073_709_551_616.0)), + ("abr_out_of_bounds_last_negative", "last", json!(-1)), + ("abr_out_of_bounds_options", "option_bytes", json!(vec![1u64; 257])), + ("abr_out_of_bounds_figures", "option_joules_micro", json!(vec![1u64; 257])), + ("abr_out_of_bounds_sample", "samples", json!([[1, 1, 18_446_744_073_709_551_616.0]])), + ] { + let mut st = base.clone(); + st[key] = value; + v.push(refused(name, "abr", json!({ "policy": thr, "state": st }), "abr_state_out_of_bounds")); + } + for (name, p) in [ + ("abr_out_of_bounds_last_n", json!({ "id": "throughput", "last_n": 256, "safety_permille": 900 })), + ("abr_out_of_bounds_safety", json!({ "id": "mpc", "horizon": 3, "safety_permille": 65_536 })), + ("abr_out_of_bounds_v", json!({ "id": "bola_like", "v_milli": 1u64 << 32, "max_buffer_ms": 20_000 })), + ("abr_out_of_bounds_horizon", json!({ "id": "mpc", "horizon": 256, "safety_permille": 900 })), + ] { + v.push(refused(name, "abr", json!({ "policy": p, "state": base }), "abr_state_out_of_bounds")); + } + let deep = json!({ "buffer_ms": 0, "samples": [[1000, 1000, 1000]], "last": null, "option_bytes": [], "option_joules_micro": [u64::MAX, 1], + "joule_budget_micro": 10, "segment_ms": 1000 }); + v.push(refused("depth_out_of_bounds_bytes", "depth", json!({ "layers_n": [u64::MAX, 1], "policy": thr, "state": base }), "abr_state_out_of_bounds")); + v.push(refused("depth_out_of_bounds_figure", "depth", json!({ "layers_n": [1, 1], "policy": thr, "state": deep }), "abr_state_out_of_bounds")); + v.push(refused("depth_out_of_bounds_layers", "depth", json!({ "layers_n": vec![1u64; 257], "policy": thr, "state": base }), "abr_state_out_of_bounds")); + v.push(case("depth_at_the_bound", "depth", json!({ "layers_n": [u64::MAX - 1, 1], "policy": thr, "state": base }))); + // The integer log every utility rests on, at the table's knots, between them, and at + // the ends of u64. + for (a, b) in [(1u64, 1u64), (2, 1), (3, 2), (17, 16), (31, 16), (33, 32), (1000, 1), (1001, 1000), (65_535, 65_536), (65_537, 65_536), + (u64::MAX, 1), (u64::MAX, u64::MAX - 1), (5, 0), (0, 0), (1 << 40, 3), (123_456_789, 1_000), (7, 3), (400_000, 2_000), + (999_999, 2), (4_097, 4_096), (6_143, 4_096), (12_000, 7_000), (1 << 63, 1), ((1 << 63) + 1, 1 << 62)] { + v.push(case(format!("ilog2_{a}_{b}"), "ilog2", json!({ "a": a, "b": b }))); + } + // Entry orders over one WAI2 (SPEC §7.1 step 2) and layer depths (§10.4). + let av1 = json!(["wai.video.av1", "wai.meta.hdr10plus"]); + for (obj, adv) in [("O3", av1.clone()), ("OD", av1.clone()), ("O7", av1.clone()), ("O9a", json!(["wai.video.av1", "wai.video.int_hyper"])), + ("O2", json!(["wai.neural.int_mlicpp", "wai.image.avif", "wai.image.jxl", "wai.image.png"])), ("O8", json!(["wai.video.avc", "wai.video.av1"]))] { + for p in policies() { + for (k, (buffer, bps)) in [(0u64, 60_000u64), (8000, 1_500_000), (30_000, 40_000_000)].into_iter().enumerate() { + let state = json!({ "buffer_ms": buffer, "samples": [[1000, bps / 8, 1000], [2000, bps / 8, 1000]], "last": null, + "option_bytes": [], "option_joules_micro": [], "joule_budget_micro": null, "segment_ms": 2000 }); + v.push(case(format!("abr_order_{obj}_{}_{k}", p["id"].as_str().expect("id")), "abr_order", + json!({ "object": obj, "advertises": adv, "policy": p, "state": state }))); + } + } + } + for (name, objects) in [("O10", json!(["O10", "O10L1", "O10L2"])), ("O11b", json!(["O11b", "O11bL1"]))] { + for p in policies() { + for (k, (buffer, bps)) in [(0u64, 20_000u64), (6000, 300_000), (30_000, 5_000_000)].into_iter().enumerate() { + let state = json!({ "buffer_ms": buffer, "samples": [[1000, bps / 8, 1000]], "last": null, "option_bytes": [], + "option_joules_micro": [], "joule_budget_micro": null, "segment_ms": 1000 }); + v.push(case(format!("depth_{name}_{}_{k}", p["id"].as_str().expect("id")), "depth", json!({ "objects": objects, "policy": p, "state": state }))); + } + } + } + // At 200 kb/s less the 900 ‰ margin the bytes through depth 1 fit and those through + // depth 2 do not; layer 2 alone would. + v.push(case("depth_O10_throughput_between", "depth", json!({ "objects": ["O10", "O10L1", "O10L2"], "policy": thr, + "state": { "buffer_ms": 0, "samples": [[1000, 25_000, 1000]], "last": null, "option_bytes": [], "option_joules_micro": [], "joule_budget_micro": null, "segment_ms": 1000 } }))); + let rich = json!({ "buffer_ms": 0, "samples": [[1000, 10_000_000, 1000]], "last": null, "option_bytes": [], "option_joules_micro": [5, null, 20], + "joule_budget_micro": 30, "segment_ms": 1000 }); + v.push(case("depth_O10_budget_unlabelled_layer", "depth", json!({ "objects": ["O10", "O10L1", "O10L2"], "policy": thr, "state": rich }))); + let mut labelled = rich.clone(); + labelled["option_joules_micro"] = json!([5, 10, 20]); + v.push(case("depth_O10_budget_lowered", "depth", json!({ "objects": ["O10", "O10L1", "O10L2"], "policy": thr, "state": labelled }))); + // A fetch whose order is a policy's: the same pick as whole-envelope selection. + for (name, bps) in [("low", 100_000u64), ("high", 10_000_000)] { + let state = json!({ "buffer_ms": 0, "samples": [[1000, bps / 8, 1000]], "last": null, "option_bytes": [], "option_joules_micro": [], + "joule_budget_micro": null, "segment_ms": 2000 }); + let abr = json!({ "policy": thr, "state": state }); + v.push(case(format!("fetch_abr_O3_{name}"), "fetch", json!({ "object": "O3", "advertises": av1, "applies": ["wai.meta.hdr10plus"], "abr": abr, "fragment": "#h=1009" }))); + v.push(case(format!("same_pick_abr_O3_{name}"), "same_pick", json!({ "object": "O3", "advertises": av1, "applies": ["wai.meta.hdr10plus"], "abr": abr, "fragment": "#h=1009" }))); + v.push(case(format!("same_pick_abr_O7_{name}"), "same_pick", json!({ "object": "O7", "advertises": av1, "abr": abr }))); + } + Ok(v) +} + +/// The selector's order from a case's `abr` member: the policy over the state, on the +/// sink's advertised capabilities. +pub(super) fn abr_order(input: &Value, advertises: Vec) -> Result Vec>>, Stop> { + let a = &input["abr"]; + if a.is_null() { + return Ok(None); + } + Ok(Some(abr::selector_order(policy(&a["policy"])?, state(&a["state"])?, move |c| advertises.iter().any(|x| x == c)).map_err(abr_err)?)) +} + + +// ------------------------------------------------------------- the fuzz families + +fn pick<'a>(s: &mut u64, xs: &[&'a str]) -> &'a str { + xs[(next(s) % xs.len() as u64) as usize] +} + +const DECIMALS: &[&str] = &["4", "4.0", "4.000", "3.9995", "3.99949", "0.64", "0.640", "0.6399999", "1e3", "-1", "٤", "4.", ".5", " 4", + "18446744073709551.615", "18446744073709551.616", "0x10", "0.32", "0.5", "12", "0"]; + +/// A playlist's lines from a vocabulary that holds every tag the reader reads, valid +/// and not, and the layer, URI, comment and blank lines between them. +fn playlist_text(s: &mut u64) -> String { + if next(s) % 2 == 0 { + return structured_playlist(s); + } + let mut lines: Vec = Vec::new(); + if next(s) % 10 != 0 { + lines.push("#EXTM3U".into()); + } + for _ in 0..next(s) % 14 { + let d = pick(s, DECIMALS); + lines.push(match next(s) % 24 { + 0 => pick(s, &["#EXT-X-VERSION:9", "#EXT-X-VERSION:300", "#EXT-X-VERSION:x", "#EXT-X-TARGETDURATION:4"]).into(), + 1 => pick(s, &["#EXT-X-MEDIA-SEQUENCE:1200", "#EXT-X-DISCONTINUITY-SEQUENCE:2", "#EXT-X-MEDIA-SEQUENCE:+1"]).into(), + 2 => format!("#EXT-X-PART-INF:PART-TARGET={d}"), + 3 => pick(s, &["#EXT-X-PART-INF:PART-TARGET=\"0.5\"", "#EXT-X-PART-INF:X=1", "#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=1.5", + "#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=MAYBE", "#EXT-X-SERVER-CONTROL:PART-HOLD-BACK=1.5,PART-HOLD-BACK=2"]).into(), + 4 | 5 => "#EXT-X-DISCONTINUITY".into(), + 6 => pick(s, &["#EXT-X-PROGRAM-DATE-TIME:2026-10-01T12:00:00.000Z", "#EXT-X-PROGRAM-DATE-TIME", "#EXT-X-PROGRAM-DATE-TIME:a b"]).into(), + 7 | 8 => format!("#EXT-X-PART:DURATION={d}{},URI=\"{}\"", pick(s, &["", ",INDEPENDENT=YES", ",INDEPENDENT=NO", ",INDEPENDENT=yes"]), pick(s, &["p.wai", "p\u{b}q", "", "a,b"])), + 9 => pick(s, &["#EXT-X-PART:DURATION=0.5", "#EXT-X-PART:URI=\"p\"", "#EXT-X-PART:DURATION=0.5,URI=p", "#EXT-X-PART:DURATION=0.5,URI=\"p\",X=a\u{b}b"]).into(), + 10..=13 => format!("#EXTINF:{d}{}", pick(s, &[",", ",title", "", ",a,b"])), + 14..=16 => pick(s, &["seg.wai", "a/0123.wai#n=5", "x.m4s", "#", "seg two.wai"]).into(), + 17 => pick(s, &["#WAI-LAYER:N=1,URI=\"l1.wai\"", "#WAI-LAYER:N=2,URI=\"l2.wai\"", "#WAI-LAYER:N=0,URI=\"l.wai\"", "#WAI-LAYER:N=1"]).into(), + 18 => pick(s, &["#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"h.wai\"", "#EXT-X-PRELOAD-HINT:TYPE=MAP,URI=\"m\"", "#EXT-X-PRELOAD-HINT:URI=\"h\""]).into(), + 19 => "#EXT-X-ENDLIST".into(), + 20 => pick(s, &["#EXT-X-SKIP:SKIPPED-SEGMENTS=1", "#EXT-X-STREAM-INF:BANDWIDTH=1", "#EXTM3U", "#EXT-X-MEDIA:TYPE=AUDIO"]).into(), + 21 => pick(s, &["# a comment", "#WAI-LAYER", "#EXT-X-GAP", "#EXT-X-BYTERANGE:100@0", "#EXT-X-MAP:URI=\"i.mp4\"", "#EXT-X-PLAYLIST-TYPE:VOD", + "#EXT-X-PLAYLIST-TYPE:vod", "#EXT-X-INDEPENDENT-SEGMENTS", "#EXT-X-MAP:URI=i.mp4"]).into(), + _ => String::new(), + }); + } + let end = pick(s, &["\n", "\r\n"]); + let mut t = lines.join(end); + if next(s) % 2 == 0 { + t.push_str(end); + } + t +} + +/// A playlist in the order a packager writes one, with at most one line changed: the +/// readers' agreement on what is well formed, not only on what is not. +fn structured_playlist(s: &mut u64) -> String { + let parts = next(s) % 2 == 0; + let mut l: Vec = vec!["#EXTM3U".into(), "#EXT-X-VERSION:9".into(), "#EXT-X-TARGETDURATION:4".into()]; + if parts { + l.push(format!("#EXT-X-PART-INF:PART-TARGET={}", pick(s, &["0.5", "0.500", "0.4999"]))); + l.push(format!("#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD={},PART-HOLD-BACK=1.5", pick(s, &["YES", "NO"]))); + } + l.push(format!("#EXT-X-MEDIA-SEQUENCE:{}", next(s) % 3000)); + if next(s) % 2 == 0 { + l.push(pick(s, &["#EXT-X-PLAYLIST-TYPE:VOD", "#EXT-X-PLAYLIST-TYPE:EVENT"]).into()); + } + if next(s) % 2 == 0 { + l.push("#EXT-X-INDEPENDENT-SEGMENTS".into()); + } + if next(s) % 3 == 0 { + l.push("#EXT-X-MAP:URI=\"init.mp4\"".into()); + } + for k in 0..next(s) % 4 { + if next(s) % 3 == 0 { + l.push("#EXT-X-DISCONTINUITY".into()); + } + if next(s) % 3 == 0 { + l.push("#EXT-X-PROGRAM-DATE-TIME:2026-10-01T12:00:00.000Z".into()); + } + if parts { + for j in 0..next(s) % 3 { + l.push(format!("#EXT-X-PART:DURATION=0.5{},URI=\"p{k}.{j}.wai\"", pick(s, &["", ",INDEPENDENT=YES"]))); + if next(s) % 4 == 0 { + l.push(format!("#WAI-LAYER:N={},URI=\"pl{k}.{j}.wai\"", 1 + next(s) % 2)); + } + } + } + if next(s) % 4 == 0 { + l.push("#EXT-X-GAP".into()); + } + l.push(format!("#EXTINF:{},", pick(s, &["4", "3.84", "3.9995", "4.000"]))); + for n in 1..=next(s) % 3 { + l.push(format!("#WAI-LAYER:N={n},URI=\"l{k}.{n}.wai\"")); + } + l.push(format!("s{k}.wai#n=9")); + } + if parts && next(s) % 2 == 0 { + l.push("#EXT-X-PART:DURATION=0.5,URI=\"open.0.wai\"".into()); + l.push("#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"open.1.wai\"".into()); + } else if next(s) % 2 == 0 { + l.push("#EXT-X-ENDLIST".into()); + } + if next(s) % 2 == 0 { + let at = 1 + (next(s) as usize) % (l.len() - 1).max(1); + let d = pick(s, DECIMALS); + let swap = [format!("#EXTINF:{d},"), "#WAI-LAYER:N=1,URI=\"x.wai\"".to_string(), "x.wai".to_string(), "#EXT-X-MEDIA-SEQUENCE:1".to_string(), + format!("#EXT-X-PART:DURATION={d},URI=\"x\""), String::new(), "#EXT-X-SKIP:SKIPPED-SEGMENTS=1".to_string()]; + let at = at.min(l.len() - 1); + l[at] = swap[(next(s) % swap.len() as u64) as usize].clone(); + } + l.join("\n") + "\n" +} + +/// One timeline written twice: a classical playlist and a WAI one, the WAI one perturbed +/// one way or not at all. +fn adjacent_pair(s: &mut u64) -> (String, String) { + let seq = [0u64, 1200][(next(s) % 2) as usize]; + let parts = next(s) % 2 == 0; + let n = next(s) % 4; + let ms_forms: &[(u64, &[&str])] = &[(640, &["0.64", "0.640", "0.6399999", "0.6404"]), (2000, &["2", "2.0", "2.00002", "1.9996"]), (500, &["0.5", "0.500", "0.4995"])]; + let mut c = vec!["#EXTM3U".to_string(), "#EXT-X-TARGETDURATION:2".into()]; + let mut w = c.clone(); + if parts { + c.push("#EXT-X-PART-INF:PART-TARGET=0.5".into()); + w.push("#EXT-X-PART-INF:PART-TARGET=0.500".into()); + } + c.push(format!("#EXT-X-MEDIA-SEQUENCE:{seq}")); + w.push(format!("#EXT-X-MEDIA-SEQUENCE:{seq}")); + let perturb = next(s) % 9; + let at = next(s) % (n.max(1)); + for k in 0..n { + let (ms, forms) = ms_forms[(next(s) % ms_forms.len() as u64) as usize]; + if next(s) % 4 == 0 { + c.push("#EXT-X-DISCONTINUITY".into()); + if !(perturb == 1 && k == at) { + w.push("#EXT-X-DISCONTINUITY".into()); + } + } + if k == 0 && next(s) % 2 == 0 { + c.push("#EXT-X-PROGRAM-DATE-TIME:2026-10-01T12:00:00.000Z".into()); + w.push(if perturb == 2 { "#EXT-X-PROGRAM-DATE-TIME:2026-10-01T12:00:00Z".into() } else { "#EXT-X-PROGRAM-DATE-TIME:2026-10-01T12:00:00.000Z".into() }); + } + if parts { + for j in 0..(ms / 500).max(1) { + c.push(format!("#EXT-X-PART:DURATION={},URI=\"c{k}.{j}\"", pick(s, &["0.5", "0.5000", "0.49999"]))); + let d = if perturb == 3 && k == at && j == 0 { "0.499" } else { "0.500" }; + w.push(format!("#EXT-X-PART:DURATION={d},URI=\"w{k}.{j}.wai#cid={}&n=3\"", "ab".repeat(32))); + if perturb == 4 { + w.push("#WAI-LAYER:N=1,URI=\"l.wai\"".into()); + } + } + } + c.push(format!("#EXTINF:{},", pick(s, forms))); + c.push(format!("c{k}.m4s")); + let wd = if perturb == 5 && k == at { ms + 1 } else { ms }; + w.push(format!("#EXTINF:{}.{:03},", wd / 1000, wd % 1000)); + if perturb == 6 { + w.push("#WAI-LAYER:N=2,URI=\"l2.wai\"".into()); + } + w.push(format!("w{k}.wai#n=9")); + } + if perturb == 7 { + w.push("#EXTINF:0.640,".into()); + w.push("extra.wai".into()); + } + if perturb == 8 { + let at = w.iter().position(|l| l.starts_with("#EXT-X-MEDIA-SEQUENCE")).expect("a sequence"); + w[at] = format!("#EXT-X-MEDIA-SEQUENCE:{}", seq + 1); + } + (c.join("\n") + "\n", w.join("\n") + "\n") +} + +/// A classical master and a published form of it: unchanged, with the session-data line +/// at some boundary (well or badly formed, once or twice), or otherwise changed. +fn master_pair(s: &mut u64) -> (String, String) { + let end = pick(s, &["\n", "\r\n"]); + let body = ["#EXTM3U", "#EXT-X-VERSION:6", "#EXT-X-INDEPENDENT-SEGMENTS", "#EXT-X-STREAM-INF:BANDWIDTH=800000,CODECS=\"avc1.42e01e\"", "v0.m3u8"]; + let lines: Vec = body.iter().take(2 + (next(s) % 4) as usize).map(|l| format!("{l}{end}")).collect(); + let original: String = lines.concat(); + let sd = pick(s, &["#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\"", + "#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"\"", + "#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"a\"b\"", + "#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.multivariant\",VALUE=\"wai.m3u8\" ", + "#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.other\",VALUE=\"wai.m3u8\"", + "#EXT-X-SESSION-DATA:VALUE=\"wai.m3u8\",DATA-ID=\"science.transaction.wai.multivariant\""]); + let mut out = lines.clone(); + match next(s) % 5 { + 0 => {} + 1 | 2 => { + let at = (next(s) as usize) % (out.len() + 1); + out.insert(at, format!("{sd}{}", pick(s, &[end, "\n", "\r\n", ""]))); + } + 3 => { + let at = 1 + (next(s) as usize) % out.len(); + out.insert(at.min(out.len()), format!("{sd}{end}")); + out.insert(at.min(out.len()), format!("{sd}{end}")); + } + _ => { + let k = (next(s) as usize) % out.len(); + out[k] = out[k].replacen('0', "1", 1); + } + } + (original, out.concat()) +} + +fn render_case(s: &mut u64) -> Value { + let uris = ["a.wai", "b.wai", "c/d.wai#n=3", "ok/\u{1d518}.wai", "a b.wai", "e.wai", "a\"b", "", "#x", "a\nb"]; + let uris: &[&str] = if next(s) % 2 == 0 { &uris[..6] } else { &uris }; + let n = next(s) % 4; + let segs: Vec = (0..n).map(|_| json!([next(s) % 5000, pick(s, uris), next(s) % 5 == 0])).collect(); + let n_ex = [0, n, n, n + 1, n + 2][(next(s) % 5) as usize]; + let layers = |s: &mut u64| -> Vec { (0..next(s) % 3).map(|k| json!({ "n": if next(s) % 4 == 0 { next(s) % 4 } else { k + 1 }, "uri": pick(s, uris) })).collect() }; + let extras: Vec = (0..n_ex).map(|_| { + let parts: Vec = (0..next(s) % 3).map(|_| json!({ "duration_ms": next(s) % 600, "independent": next(s) % 2 == 0, "uri": pick(s, uris), "layers": layers(s) })).collect(); + let pdt = [Value::Null, json!("2026-10-01T12:00:00.000Z"), json!(""), json!("a b")][(next(s) % 4) as usize].clone(); + json!({ + "discontinuity": next(s) % 3 == 0, + "gap": next(s) % 4 == 0, + "program_date_time": if next(s) % 2 == 0 { Value::Null } else { pdt }, + "parts": parts, "layers": layers(s), + "preload_hint": if next(s) % 3 == 0 { json!(pick(s, uris)) } else { Value::Null }, + }) + }).collect(); + let o = |s: &mut u64, v: u64| if next(s) % 2 == 0 { Value::Null } else { json!(v) }; + let pt = [None, Some(500u64), Some(500), Some(320)][(next(s) % 4) as usize]; + let hold = pt.map(|t| t * [1u64, 2, 3, 3][(next(s) % 4) as usize] + next(s) % 2); + let (target, part_target, hold_back) = (o(s, 4), json!(pt), json!(hold.or(if next(s) % 4 == 0 { Some(900) } else { None }))); + let version = [0u64, 6, 7, 9, 9, 9][(next(s) % 6) as usize]; + let playlist_type = [Value::Null, json!("VOD"), json!("EVENT")][(next(s) % 3) as usize].clone(); + let map_uri = [Value::Null, Value::Null, json!("init.mp4"), json!("a\"b")][(next(s) % 4) as usize].clone(); + json!({ + "segments": segs, + "render": { "version": version, "media_sequence": next(s) % 3000, "discontinuity_sequence": next(s) % 3, + "target_duration_s": target, "part_target_ms": part_target, "part_hold_back_ms": hold_back, + "can_block_reload": next(s) % 2 == 0, "end_list": next(s) % 2 == 0, "playlist_type": playlist_type, + "independent_segments": next(s) % 2 == 0, "map_uri": map_uri }, + "extras": extras, + }) +} + +fn abr_case(s: &mut u64) -> Value { + let p = match next(s) % 4 { + 0 => json!({ "id": "throughput", "last_n": next(s) % 9, "safety_permille": next(s) % 1500 }), + 1 => { + let cushion = [0, next(s) % 20_000][(next(s) % 2) as usize]; + json!({ "id": "buffer_based", "reservoir_ms": next(s) % 20_000, "cushion_ms": cushion }) + } + 2 => json!({ "id": "bola_like", "v_milli": next(s) % 3000, "max_buffer_ms": next(s) % 60_000 }), + _ => json!({ "id": "mpc", "horizon": next(s) % 8, "safety_permille": next(s) % 1500 }), + }; + let k = next(s) % 8; + let opts: Vec = (0..k).map(|_| [0, 1, next(s) % 2_000_000, next(s) % (1 << 40)][(next(s) % 4) as usize]).collect(); + let samples: Vec = (0..next(s) % 7).map(|i| { + let ms = [0, 1, next(s) % 4000][(next(s) % 3) as usize]; + json!([i * 1000, next(s) % 5_000_000, ms]) + }).collect(); + let figs: Vec = (0..k + next(s) % 2).map(|_| if next(s) % 3 == 0 { Value::Null } else { json!(next(s) % 10_000) }).collect(); + let segment_ms = [0u64, 1, 40, 2000, 10_000][(next(s) % 5) as usize]; + let mut case = json!({ "policy": p, "state": { + "buffer_ms": next(s) % 70_000, "samples": samples, + "last": if next(s) % 2 == 0 { Value::Null } else { json!(next(s) % 10) }, + "option_bytes": opts, "option_joules_micro": figs, + "joule_budget_micro": if next(s) % 2 == 0 { Value::Null } else { json!(next(s) % 12_000) }, + "segment_ms": segment_ms, + } }); + // One case in three at the extremes of Appendix F's bounds, or just past them. + if next(s) % 3 == 0 { + let n_opts = case["state"]["option_bytes"].as_array().map_or(0, Vec::len); + let edge = |s: &mut u64, top: u64| -> Value { + match next(s) % 6 { + 0 => json!(top), + 1 => json!(top.saturating_sub(next(s) % 1000)), + 2 => json!(next(s) % (top / 2 + 1)), + 3 => json!(0), + 4 if top < u64::MAX => json!(top + 1), + _ => json!(next(s) >> (next(s) % 64) & top), + } + }; + for _ in 0..1 + next(s) % 4 { + match next(s) % 8 { + 0 => case["state"]["buffer_ms"] = edge(s, u64::from(u32::MAX)), + 1 => case["state"]["segment_ms"] = edge(s, u64::from(u32::MAX)), + 2 => { + let mut v: Vec = (0..n_opts.max(1)).map(|_| (next(s) | (1 << 63)) >> (next(s) % 40)).collect(); + v.sort_unstable(); + case["state"]["option_bytes"] = json!(v); + } + 3 => { + let samples: Vec = (0..1 + next(s) % 5).map(|i| { + let (bytes, ms) = (next(s) >> (next(s) % 8), [0, 1, u64::MAX][(next(s) % 3) as usize]); + json!([i, bytes, ms]) + }).collect(); + case["state"]["samples"] = json!(samples); + } + 4 => case["state"]["joule_budget_micro"] = edge(s, u64::MAX), + 5 => case["state"]["option_joules_micro"] = json!((0..n_opts).map(|_| if next(s) % 3 == 0 { Value::Null } else { edge(s, u64::MAX) }).collect::>()), + 6 => { + let p = &mut case["policy"]; + for (k, top) in [("last_n", 255u64), ("horizon", 255), ("safety_permille", 65_535), ("v_milli", u64::from(u32::MAX)), + ("max_buffer_ms", u64::from(u32::MAX)), ("reservoir_ms", u64::from(u32::MAX)), ("cushion_ms", u64::from(u32::MAX))] { + if !p[k].is_null() { + p[k] = edge(s, top); + } + } + } + _ => case["state"]["last"] = edge(s, u64::MAX), + } + } + } + case +} + +/// A receipt file and its playlist, with one member of the file changed one way. +fn receipt_case(c: &Corpus, s: &mut u64) -> Result { + let playlist = pick(s, &["playlists/o9/wai-180.m3u8", "live/wai-r1.m3u8", "playlists/o11/wai-v.m3u8"]); + let receipt = receipt_of(c, playlist)?; + let t = String::from_utf8(c.file(&receipt)?).map_err(|_| "not UTF-8")?; + let members = ["\"viewers_served\":1", "\"joules_micro\":0", "\"window\":", "\"version\":", "\"end_list\":", "\"kind\":\"wai.video.manifest\"", + "\"signer_id\":\"did:example:http-vectors\"", "\"extras\":[", "\"parent_receipt_hash\":", "\"funds_ceiling\":null", "\"index\":0", + "\"duration_ms\":", "\"independent\":true", "\"entitlement\":\"public\"", "\"blackouts\":[]"]; + let m = pick(s, &members); + if !t.contains(m) || next(s) % 5 == 0 { + return Ok(json!({ "playlist": playlist, "receipt": receipt })); + } + // A member whole (its name and its value), which can be written twice. + let whole = !m.ends_with(':') && !m.ends_with('['); + let escaped_name = || { + let first = m.chars().nth(1).unwrap_or('a'); + format!("\"\\u{:04x}{}", first as u32, &m[2..]) + }; + let to = match next(s) % 11 { + 0 => format!("{m}0"), + 1 => m.replacen(':', ":\"", 1) + "\"", + 2 => m.replacen("\":", "x\":", 1), + 3 => format!("\"note\":1,{m}"), + 4 => m.replacen(':', ":-", 1), + 5 => m.replacen(':', ":1.0e0,\"z\":", 1), + 6 => m.to_uppercase(), + // A member written twice, as it is or under an escaped name. + 8 if whole => format!("{m},{m}"), + 9 if whole => format!("{m},{}", escaped_name()), + // A lone surrogate at the end of a string value. + 10 if m.ends_with('"') => format!("{}\\u{:x}\"", &m[..m.len() - 1], [0xd800u32, 0xdbff, 0xdc00, 0xdfff][(next(s) % 4) as usize]), + _ => format!("{m}{m}").replacen("\":", "\":", 1), + }; + Ok(json!({ "playlist": playlist, "receipt": receipt, "edit_receipt": [[m, to]] })) +} + +/// One iteration of the playlist-side fuzz families. +pub(super) fn fuzz_cases(c: &Corpus, s: &mut u64, i: usize) -> Result, String> { + let mut v = vec![(format!("fuzz_playlist_parse_{i}"), "playlist_parse", json!({ "text": playlist_text(s) }))]; + let (k, w) = adjacent_pair(s); + v.push((format!("fuzz_adjacency_{i}"), "adjacency", json!({ "wai_text": w, "classical_text": k }))); + let (o, p) = master_pair(s); + v.push((format!("fuzz_unchanged_{i}"), "unchanged", json!({ "original_text": o, "published_text": p }))); + v.push((format!("fuzz_render_{i}"), "render", render_case(s))); + v.push((format!("fuzz_abr_{i}"), "abr", abr_case(s))); + let edge = [0u64, 1, 2, 3, 65_535, 65_536, 65_537, u64::MAX, u64::MAX - 1, 1 << 63]; + let num = |s: &mut u64| if next(s) % 3 == 0 { edge[(next(s) % edge.len() as u64) as usize] } else { next(s) >> (next(s) % 64) }; + let (a, b) = (num(s), num(s)); + v.push((format!("fuzz_ilog2_{i}"), "ilog2", json!({ "a": a, "b": b }))); + v.push((format!("fuzz_receipt_{i}"), "playlist_receipt", receipt_case(c, s)?)); + let parts = ["", ".", "..", "a", "b", "720", "%2E", "%2e%2E", "%41", "%7e", "%2F", "?", "#", ":", "@", "//", "/", "x:y", "%", "%4", "C", ";p"]; + let reference: String = (0..1 + next(s) % 6).map(|_| pick(s, &parts)).collect::>().join(if next(s) % 2 == 0 { "/" } else { "" }); + let base = pick(s, &[LIVE_URL, "http://a/b/c/d;p?q", "live/wai.m3u8", "", "HTTP://A.example", "x:/a/b", "//h/p/q"]); + v.push((format!("fuzz_uri_resolve_{i}"), "uri_resolve", json!({ "base": base, "reference": reference }))); + let (w, k) = dash_pair(c, s)?; + v.push((format!("fuzz_dash_adjacency_{i}"), "dash_adjacency", json!({ "wai_text": w, "classical_text": k }))); + Ok(v) +} + +/// A WAI MPD and its classical MPD, one or the other changed one to three ways: a number +/// of the timeline respelled, an `S` dropped or repeated, a timescale or a start +/// respelled, an attribute written twice, or markup at the edges of the XML subset put +/// before a tag (Appendix I). +fn dash_pair(c: &Corpus, s: &mut u64) -> Result<(String, String), String> { + let mut texts = [String::from_utf8(c.file("playlists/o9/wai.mpd")?).map_err(|_| "not UTF-8")?, + String::from_utf8(c.file("src/playlists/o9-classical.mpd")?).map_err(|_| "not UTF-8")?]; + const NUMBERS: &[&str] = &["0", "1", "57600", "57601", "115200", "172800", "", "-1", "+1", "01", "1e3", "18446744073709551615", "18446744073709551616", + "1048575", "1048576", "2", " 2", "90000", "48000", "1000", "0x10", "٣"]; + const DURATIONS: &[&str] = &["PT0S", "PT0.000S", "PT0.0004S", "PT0.0005S", "P0D", "P0DT0H0M0S", "PT", "P", "P1M", "P1Y", "PT1M", "PT60S", "-PT0S", + "PT0,5S", "pt0s", "PT0.S", "PT.5S", "P0DT", "PT0H0M0.0001S"]; + const MARKUP: &[&str] = &["", "", "", "", "&", "&bogus;", "�", "A", "A", "�", + "\r\n", "\r", "]]>", "", "", "\u{1}", "\u{FEFF}", "\u{FFFE}", "", "", "", + "", "", "", "<é/>", "\t\n"]; + for _ in 0..1 + next(s) % 3 { + let side = (next(s) % 2) as usize; + let t = &mut texts[side]; + match next(s) % 6 { + 0 => { + // A number of the timeline respelled. + let at: Vec = t.match_indices(['t', 'd', 'r']).map(|(i, _)| i).filter(|&i| t[i + 1..].starts_with("=\"") && t[..i].ends_with(' ')).collect(); + if let Some(&i) = at.get((next(s) % at.len().max(1) as u64) as usize) { + let start = i + 3; + let end = start + t[start..].find('"').unwrap_or(0); + t.replace_range(start..end, pick(s, NUMBERS)); + } + } + 1 => { + // An S dropped or repeated. + let at: Vec = t.match_indices("").map_or(0, |e| e + 2); + let el = t[i..end].to_string(); + if next(s) % 2 == 0 { t.replace_range(i..end, "") } else { t.insert_str(end, &el) } + } + } + 2 => { + if let Some(i) = t.find("timescale=\"") { + let start = i + 11; + let end = start + t[start..].find('"').unwrap_or(0); + t.replace_range(start..end, pick(s, NUMBERS)); + } + } + 3 => { + if let Some(i) = t.find("start=\"") { + let start = i + 7; + let end = start + t[start..].find('"').unwrap_or(0); + t.replace_range(start..end, pick(s, DURATIONS)); + } + } + 4 => { + // An attribute written twice. + let at: Vec = t.match_indices(" id=\"").map(|(i, _)| i).collect(); + if let Some(&i) = at.get((next(s) % at.len().max(1) as u64) as usize) { + let end = i + 5 + t[i + 5..].find('"').map_or(0, |e| e + 1); + let a = t[i..end].to_string(); + t.insert_str(end, &a); + } + } + _ => { + // Markup before a tag. + let at: Vec = t.match_indices('<').map(|(i, _)| i).collect(); + if let Some(&i) = at.get((next(s) % at.len().max(1) as u64) as usize) { + t.insert_str(i, pick(s, MARKUP)); + } + } + } + } + let [w, k] = texts; + Ok((w, k)) +} diff --git a/wai/wai-rs/src/hls_render.rs b/wai/wai-rs/src/hls_render.rs new file mode 100644 index 000000000..ba3f11d4d --- /dev/null +++ b/wai/wai-rs/src/hls_render.rs @@ -0,0 +1,389 @@ +//! HLS media playlist text (RFC 8216, and the second edition's partial segments): a +//! pure function of the ordered segments and the rendering parameters, so anyone +//! holding a playlist's bytes recomputes them, and a receipt over the bytes +//! (`wai.video.manifest`, http-delivery §8.6) binds every line. +//! +//! [`render_hls`] is the original rendering of `wai.video.manifest`: version 6, media +//! sequence 0, a derived target duration, a discontinuity before each ad and +//! `EXT-X-ENDLIST`. [`render_hls_with`] adds what an on-demand, live or low-latency WAI +//! playlist needs (http-delivery §10.4, §11): a media sequence and discontinuity +//! sequence, a fixed target duration, partial segments and their server control, +//! discontinuities and program date-times, `#WAI-LAYER` lines and a preload hint. With +//! [`HlsRender::legacy`] and no extras it gives [`render_hls`]'s bytes exactly. +//! +//! Pure text, no dependency: `http_binding` renders WAI playlists with it, and +//! `wai_video_manifest` seals them. + +/// Whether a segment carries programme content or a stitched ad. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SegmentKind { + Content, + Ad, +} + +impl SegmentKind { + /// `content` or `ad`. + pub fn label(self) -> &'static str { + match self { + SegmentKind::Content => "content", + SegmentKind::Ad => "ad", + } + } +} + +/// One entry in the ordered manifest composition. +/// +/// `ad_id.is_some()` iff `kind == Ad`; a manifest receipt's `verify` rejects any other +/// combination, so the monetisation audit (the ad-bearing segments) is a single source +/// of truth rather than a second list that could disagree. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Segment { + pub index: u64, + pub kind: SegmentKind, + /// The segment URI as it appears in the manifest (content or ad). + pub uri: String, + pub duration_ms: u64, + /// Present exactly for ad segments — the ad decision this break resolved to. + pub ad_id: Option, +} + +/// `EXT-X-PLAYLIST-TYPE`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PlaylistType { + Vod, + Event, +} + +impl PlaylistType { + /// `VOD` or `EVENT`. + pub fn label(self) -> &'static str { + match self { + PlaylistType::Vod => "VOD", + PlaylistType::Event => "EVENT", + } + } +} + +/// The playlist-wide rendering parameters (the header lines and the end). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HlsRender { + /// `EXT-X-VERSION`. + pub version: u8, + /// `EXT-X-MEDIA-SEQUENCE`. + pub media_sequence: u64, + /// `EXT-X-DISCONTINUITY-SEQUENCE`, written only when it is not 0. + pub discontinuity_sequence: u64, + /// `EXT-X-TARGETDURATION` in seconds; `None` derives it as the longest segment's + /// duration rounded up. A live playlist fixes it: it must not change between + /// revisions. + pub target_duration_s: Option, + /// `EXT-X-PART-INF:PART-TARGET`, in milliseconds. + pub part_target_ms: Option, + /// `EXT-X-SERVER-CONTROL:PART-HOLD-BACK`, in milliseconds: required with a part + /// target, and at least twice it. + pub part_hold_back_ms: Option, + /// `EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES`. + pub can_block_reload: bool, + /// `EXT-X-ENDLIST`. + pub end_list: bool, + /// `EXT-X-PLAYLIST-TYPE`. + pub playlist_type: Option, + /// `EXT-X-INDEPENDENT-SEGMENTS`: every segment decodes without the one before it. + pub independent_segments: bool, + /// `EXT-X-MAP:URI`, once, before the first segment: a classical playlist's + /// initialization section (a WAI playlist has none). + pub map_uri: Option, +} + +impl HlsRender { + /// [`render_hls`]'s parameters: version 6, media sequence 0, a derived target + /// duration, no parts, and the end of the list. + pub fn legacy() -> Self { + HlsRender { + version: 6, + media_sequence: 0, + discontinuity_sequence: 0, + target_duration_s: None, + part_target_ms: None, + part_hold_back_ms: None, + can_block_reload: false, + end_list: true, + playlist_type: None, + independent_segments: false, + map_uri: None, + } + } +} + +/// A `#WAI-LAYER` line (http-delivery §10.4): the layer object a client may fetch to +/// depth `n` over the segment or part it follows. `n` is a locator only; the +/// envelope's `refines.layer` decides. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LayerLine { + pub n: u8, + pub uri: String, +} + +/// An `EXT-X-PART` line and the layer lines that follow it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PartLine { + pub duration_ms: u64, + pub independent: bool, + pub uri: String, + pub layers: Vec, +} + +/// What a rendering adds to one segment: a discontinuity, a program date-time, the +/// partial segments that precede its URI, a gap, the layer lines after its `EXTINF`, and +/// a preload hint (only on the playlist's last entry). +/// +/// [`render_hls_with`] takes no extras, one per segment, or one per segment and one +/// more: the **open** segment at a live edge, whose parts are listed and whose +/// `EXTINF` and URI are not yet (it carries no layer lines of its own). +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct SegmentExtra { + pub discontinuity: bool, + /// `EXT-X-GAP`: the segment is absent here, and a sink takes the adjacent classical + /// segment (a class that takes the piece as floor only). + pub gap: bool, + pub program_date_time: Option, + pub parts: Vec, + pub layers: Vec, + pub preload_hint: Option, +} + +/// Why a rendering's inputs are refused (`playlist_malformed`, http-delivery §13): the +/// text would not read back as the lines it was given. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RenderError(pub String); + +impl RenderError { + /// `playlist_malformed`. + pub fn code(&self) -> &'static str { + "playlist_malformed" + } +} + +impl std::fmt::Display for RenderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "playlist_malformed: {}", self.0) + } +} + +impl std::error::Error for RenderError {} + +/// Milliseconds as a decimal number of seconds with three fractional digits, the same +/// on every platform. +pub fn seconds(ms: u64) -> String { + format!("{}.{:03}", ms / 1000, ms % 1000) +} + +/// Deterministic HLS rendering: the manifest is a pure function of the ordered +/// segments, so `hash(render(segments))` is a stable identity a verifier can +/// recompute. Ad segments carry an `EXT-X-DISCONTINUITY` — the real SSAI splice +/// marker — so the stitched breaks are visible in the bytes, not only the +/// receipt. Target duration is derived (ceil of the longest segment) rather than +/// supplied, so nothing outside `segments` can move the output. +pub fn render_hls(segments: &[Segment]) -> String { + render_hls_with(segments, &HlsRender::legacy(), &[]) +} + +fn layer_lines(out: &mut String, layers: &[LayerLine]) { + for l in layers { + out.push_str(&format!("#WAI-LAYER:N={},URI=\"{}\"\n", l.n, l.uri)); + } +} + +fn part_lines(out: &mut String, parts: &[PartLine]) { + for p in parts { + out.push_str(&format!("#EXT-X-PART:DURATION={}", seconds(p.duration_ms))); + if p.independent { + out.push_str(",INDEPENDENT=YES"); + } + out.push_str(&format!(",URI=\"{}\"\n", p.uri)); + layer_lines(out, &p.layers); + } +} + +/// The playlist `segments` render to under `r`, with `extras` (none, one per segment, +/// or one per segment and the open segment's). Each segment is written as: its +/// discontinuity (an ad's, or its extra's; one line), its program date-time, its +/// parts (each `EXT-X-PART` followed by its layer lines), its `EXT-X-GAP`, its +/// `EXTINF`, its layer lines, and its URI. The open segment follows with its +/// discontinuity, program date-time and parts; then the preload hint and +/// `EXT-X-ENDLIST`. The header is `EXTM3U`, `EXT-X-VERSION`, `EXT-X-TARGETDURATION`, +/// `EXT-X-PART-INF`, `EXT-X-SERVER-CONTROL`, `EXT-X-MEDIA-SEQUENCE`, and, those that +/// apply, `EXT-X-DISCONTINUITY-SEQUENCE` (when not 0), `EXT-X-PLAYLIST-TYPE`, +/// `EXT-X-INDEPENDENT-SEGMENTS` and `EXT-X-MAP`. Durations have three fractional digits. +/// +/// Rendering never fails; [`check_render`] says whether the inputs read back as +/// themselves, and a WAI playlist is published or sealed only when they do. +pub fn render_hls_with(segments: &[Segment], r: &HlsRender, extras: &[SegmentExtra]) -> String { + let target = r.target_duration_s.unwrap_or_else(|| segments.iter().map(|s| s.duration_ms.div_ceil(1000)).max().unwrap_or(0)); + let mut out = String::new(); + out.push_str(&format!("#EXTM3U\n#EXT-X-VERSION:{}\n", r.version)); + out.push_str(&format!("#EXT-X-TARGETDURATION:{target}\n")); + if let Some(pt) = r.part_target_ms { + out.push_str(&format!("#EXT-X-PART-INF:PART-TARGET={}\n", seconds(pt))); + } + let mut control: Vec = Vec::new(); + if r.can_block_reload { + control.push("CAN-BLOCK-RELOAD=YES".into()); + } + if let Some(h) = r.part_hold_back_ms { + control.push(format!("PART-HOLD-BACK={}", seconds(h))); + } + if !control.is_empty() { + out.push_str(&format!("#EXT-X-SERVER-CONTROL:{}\n", control.join(","))); + } + out.push_str(&format!("#EXT-X-MEDIA-SEQUENCE:{}\n", r.media_sequence)); + if r.discontinuity_sequence != 0 { + out.push_str(&format!("#EXT-X-DISCONTINUITY-SEQUENCE:{}\n", r.discontinuity_sequence)); + } + if let Some(t) = r.playlist_type { + out.push_str(&format!("#EXT-X-PLAYLIST-TYPE:{}\n", t.label())); + } + if r.independent_segments { + out.push_str("#EXT-X-INDEPENDENT-SEGMENTS\n"); + } + if let Some(m) = &r.map_uri { + out.push_str(&format!("#EXT-X-MAP:URI=\"{m}\"\n")); + } + let none = SegmentExtra::default(); + for (i, s) in segments.iter().enumerate() { + let e = extras.get(i).unwrap_or(&none); + if s.kind == SegmentKind::Ad || e.discontinuity { + out.push_str("#EXT-X-DISCONTINUITY\n"); + } + if let Some(pdt) = &e.program_date_time { + out.push_str(&format!("#EXT-X-PROGRAM-DATE-TIME:{pdt}\n")); + } + part_lines(&mut out, &e.parts); + if e.gap { + out.push_str("#EXT-X-GAP\n"); + } + // Fixed 3-dp seconds so the byte output is stable across platforms. + out.push_str(&format!("#EXTINF:{},\n", seconds(s.duration_ms))); + layer_lines(&mut out, &e.layers); + out.push_str(&s.uri); + out.push('\n'); + } + if let Some(open) = extras.get(segments.len()) { + if open.discontinuity { + out.push_str("#EXT-X-DISCONTINUITY\n"); + } + if let Some(pdt) = &open.program_date_time { + out.push_str(&format!("#EXT-X-PROGRAM-DATE-TIME:{pdt}\n")); + } + part_lines(&mut out, &open.parts); + } + if let Some(hint) = extras.last().and_then(|e| e.preload_hint.as_ref()) { + out.push_str(&format!("#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"{hint}\"\n")); + } + if r.end_list { + out.push_str("#EXT-X-ENDLIST\n"); + } + out +} + +fn uri_ok(u: &str, quoted: bool) -> bool { + !u.is_empty() && !u.starts_with('#') && !u.bytes().any(|b| b == b'\r' || b == b'\n' || (quoted && b == b'"')) +} + +fn check_layers(layers: &[LayerLine], at: &str) -> Result<(), RenderError> { + let mut last = 0u8; + for l in layers { + if l.n == 0 || l.n <= last { + return Err(RenderError(format!("{at}: layer numbers are 1 to 255 and strictly increasing"))); + } + if !uri_ok(&l.uri, true) { + return Err(RenderError(format!("{at}: a layer URI is empty or holds a quote, CR or LF"))); + } + last = l.n; + } + Ok(()) +} + +/// Whether `segments`, `r` and `extras` render to a playlist that reads back as them +/// (`playlist_malformed` otherwise): +/// +/// - `extras` is empty, one per segment, or one more (the open segment, which carries +/// no layer lines, needs a part before a discontinuity or a date-time, and is not +/// in a playlist that ends); +/// - a preload hint is on the last extra only, and not in a playlist that ends; +/// - parts need a part target, and a part target needs a part hold-back of at least +/// twice it (the HLS second edition requires both); +/// - every URI is non-empty, does not begin with `#`, and holds no CR or LF, and a +/// quoted one (a part's, a layer's, a hint's, the map's) no `"`; +/// - the open segment has no gap, and a gap segment carries no parts or layer lines +/// (it offers no object there, so nothing refines or divides one); +/// - a program date-time is ASCII letters, digits and `:.+-` only; +/// - layer numbers are 1 to 255 and strictly increasing after each segment or part; +/// - the version is at least 1. +pub fn check_render(segments: &[Segment], r: &HlsRender, extras: &[SegmentExtra]) -> Result<(), RenderError> { + let n = segments.len(); + if !(extras.is_empty() || extras.len() == n || extras.len() == n + 1) { + return Err(RenderError(format!("{} extras for {n} segments", extras.len()))); + } + if r.version == 0 { + return Err(RenderError("version 0".into())); + } + if r.map_uri.as_deref().is_some_and(|m| !uri_ok(m, true)) { + return Err(RenderError("the map URI is empty or holds a quote, CR or LF".into())); + } + if let Some(pt) = r.part_target_ms { + match r.part_hold_back_ms { + Some(h) if pt > 0 && h >= pt.saturating_mul(2) => {} + _ => return Err(RenderError("a part target needs a part hold-back of at least twice it".into())), + } + } + for (i, s) in segments.iter().enumerate() { + if !uri_ok(&s.uri, false) { + return Err(RenderError(format!("segment {i}: its URI is empty, begins with # or holds CR or LF"))); + } + } + for (i, e) in extras.iter().enumerate() { + let at = if i == n { "the open segment".to_string() } else { format!("segment {i}") }; + if let Some(p) = &e.program_date_time { + if p.is_empty() || !p.bytes().all(|b| b.is_ascii_alphanumeric() || b":.+-".contains(&b)) { + return Err(RenderError(format!("{at}: a program date-time outside letters, digits and :.+-"))); + } + } + if !e.parts.is_empty() && r.part_target_ms.is_none() { + return Err(RenderError(format!("{at}: parts with no part target"))); + } + for p in &e.parts { + if !uri_ok(&p.uri, true) { + return Err(RenderError(format!("{at}: a part URI is empty or holds a quote, CR or LF"))); + } + check_layers(&p.layers, &at)?; + } + check_layers(&e.layers, &at)?; + if i < n && e.gap && (!e.parts.is_empty() || !e.layers.is_empty()) { + return Err(RenderError(format!("{at}: a gap carries parts or layer lines"))); + } + if let Some(h) = &e.preload_hint { + if i + 1 != extras.len() || r.end_list { + return Err(RenderError(format!("{at}: a preload hint is on the last entry of a playlist that does not end"))); + } + if !uri_ok(h, true) { + return Err(RenderError(format!("{at}: the hint URI is empty or holds a quote, CR or LF"))); + } + } + if i == n { + if r.end_list { + return Err(RenderError("an open segment in a playlist that ends".into())); + } + if !e.layers.is_empty() || e.gap { + return Err(RenderError("the open segment carries layer lines or a gap of its own".into())); + } + if e.parts.is_empty() && (e.discontinuity || e.program_date_time.is_some()) { + return Err(RenderError("the open segment's discontinuity or date-time comes before a part".into())); + } + if e.parts.is_empty() && e.preload_hint.is_none() { + return Err(RenderError("an open segment with no part and no hint".into())); + } + } + } + Ok(()) +} diff --git a/wai/wai-rs/src/http_binding/abr.rs b/wai/wai-rs/src/http_binding/abr.rs new file mode 100644 index 000000000..cc9cd96a4 --- /dev/null +++ b/wai/wai-rs/src/http_binding/abr.rs @@ -0,0 +1,538 @@ +//! Adaptation (http-delivery Appendix F, informative): reference policies that choose +//! among options of increasing size from measured delivery — throughput, a buffer +//! level, a utility-over-buffer rule and a short look-ahead — in integer arithmetic +//! only, so every platform chooses the same, and corpus vectors pin each choice. +//! +//! They act in two places, both deployer policy and never a change to what a sink may +//! decode: +//! +//! - [`order_entries`] reorders the selectable entries of one `WAI2` by size within +//! each capability, putting the policy's choice first (SPEC §7.1 step 2 lets a +//! deployer order entries); selection then runs unchanged on the head and on the +//! whole envelope, so the pick is the same on every path (§7.2); +//! - [`depth`] chooses how many layer objects (§10.4) a client fetches over a base. +//! +//! An option's energy enters only as a figure a receipt labels with its acquisition +//! class ([`labelled_figure`]); a budget only ever lowers the policy's choice, to an +//! option with a labelled figure within it, and a budget with no labelled figure to act +//! on is reported as not applied ([`Budget::NotApplied`]). Nothing here reads a clock: samples carry the +//! caller's measurements. +//! +//! Every input has the bounds Appendix F states ([`check_state`], [`Policy::check`]): +//! within them each quantity is computed exactly, in 128-bit integers and a widened +//! cross-multiplication, never saturated; beyond them a state is refused +//! (`abr_state_out_of_bounds`). + +use super::head::{Head, Version}; +use crate::container::ManifestV2; + +/// A state or a policy beyond Appendix F's bounds (`abr_state_out_of_bounds`), with +/// what is out of bounds. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AbrError(pub &'static str); + +impl AbrError { + pub fn code(&self) -> &'static str { + "abr_state_out_of_bounds" + } +} + +impl std::fmt::Display for AbrError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "abr_state_out_of_bounds: {}", self.0) + } +} + +impl std::error::Error for AbrError {} + +/// The most options a state holds, and the most objects (a base and its layers) a depth +/// is chosen over (Appendix F). +/// cbindgen:ignore +pub const MAX_OPTIONS: usize = 256; + +/// Appendix F's bounds on a state beyond those of its fields' types (each `ms` and `u32` +/// field below 2^32, each byte count, figure and budget below 2^64): at most +/// [`MAX_OPTIONS`] options and figures. +pub fn check_state(s: &AbrState) -> Result<(), AbrError> { + if s.option_bytes.len() > MAX_OPTIONS || s.option_joules_micro.len() > MAX_OPTIONS { + return Err(AbrError("more options than 256")); + } + Ok(()) +} + +/// One measured request: `bytes` of body received in `ms` milliseconds, finished at +/// `at_ms` on the caller's clock. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Sample { + pub at_ms: u64, + pub bytes: u64, + pub ms: u64, +} + +/// What a policy chooses from. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct AbrState { + /// Media buffered ahead of the play point, in milliseconds. + pub buffer_ms: u32, + /// Measured requests, oldest first. + pub samples: Vec, + /// The option chosen last, if any. + pub last: Option, + /// Each option's bytes, in increasing order: a component's `len` from the head, or + /// the bytes through a layer depth. + pub option_bytes: Vec, + /// Each option's energy in microjoules, only from a receipt figure labelled with its + /// acquisition class ([`labelled_figure`]); `None` otherwise. + pub option_joules_micro: Vec>, + /// A budget in microjoules for one option, if the client holds one. + pub joule_budget_micro: Option, + /// The media duration one option covers, in milliseconds. + pub segment_ms: u32, +} + +/// A policy: a pure function from the state to an option index. +pub trait Policy { + /// The policy's identifier, as the vectors name it. + fn id(&self) -> &'static str; + /// The option chosen, before any budget: an index into `option_bytes` (0 when + /// there are none). Exact for every value of the state's field types. + fn choose(&self, s: &AbrState) -> usize; + /// Appendix F's bounds on the policy's parameters beyond their types; none for the + /// reference policies, whose every parameter value is in bounds. + fn check(&self) -> Result<(), AbrError> { + Ok(()) + } +} + +/// The highest option whose rate fits the measured throughput, less a safety margin. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Throughput { + pub last_n: u8, + pub safety_permille: u16, +} + +/// The lowest option while the buffer is at or below the reservoir, the highest once it +/// reaches the reservoir and the cushion, and in between the highest option under a rate +/// rising linearly from the lowest option's to the highest's. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BufferBased { + pub reservoir_ms: u32, + pub cushion_ms: u32, +} + +/// A utility-over-buffer rule in the manner of BOLA: each option's utility is the log +/// of its size over the smallest's, and the option with the best ratio of +/// `V·(utility + 1) − buffer` to its size is chosen, `V` set by `v_milli` and +/// `max_buffer_ms`. It is not the published algorithm: where that one waits (no option +/// scores above zero), this one chooses the option closest to scoring, so it always +/// returns an option. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BolaLike { + pub v_milli: u32, + pub max_buffer_ms: u32, +} + +/// A look-ahead: every sequence of `horizon` choices (fewer when the options are many) +/// is played against the measured throughput less a margin, scored by utility, less +/// stalls and less changes of utility, and the first choice of the best sequence is +/// taken. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Mpc { + pub horizon: u8, + pub safety_permille: u16, +} + +/// What a joule budget did to a choice. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Budget { + /// No budget is held. + NotSet, + /// No option has a labelled figure: the budget is not applied, and the choice + /// stands. + NotApplied, + /// The chosen option's labelled figure is within the budget. + Within, + /// The choice was lowered to the highest option below it with a labelled figure + /// within the budget. + Lowered, + /// No option at or below the choice has a labelled figure within the budget: the + /// lowest option, option 0, and the budget is not met. A budget never raises the + /// choice. + Unmet, +} + +impl Budget { + /// `not_set`, `not_applied`, `within`, `lowered` or `unmet`. + pub fn label(self) -> &'static str { + match self { + Budget::NotSet => "not_set", + Budget::NotApplied => "not_applied", + Budget::Within => "within", + Budget::Lowered => "lowered", + Budget::Unmet => "unmet", + } + } +} + +/// `round(65536 · log2(1 + i/16))` for `i` in `0..=16`. +/// +/// cbindgen:ignore +pub const LOG2_TABLE: [u64; 17] = [0, 5732, 11136, 16248, 21098, 25711, 30109, 34312, 38336, 42196, 45904, 49472, 52911, 56229, 59434, 62534, 65536]; + +/// `log2(a / b)` in Q16 (×65536), 0 when `a ≤ b` (`b` taken as at least 1): the ratio +/// `x = ⌊65536·a/b⌋`, its exponent `e = ⌊log2 x⌋ − 16`, the mantissa `m = x >> e` in +/// `[65536, 131072)`, and `e·65536` plus [`LOG2_TABLE`] interpolated linearly on the +/// mantissa's top four fractional bits, its next twelve the weight, rounded down. +pub fn ilog2_q16(a: u64, b: u64) -> u64 { + let b = b.max(1); + if a <= b { + return 0; + } + let x = (u128::from(a) << 16) / u128::from(b); + let e = (127 - x.leading_zeros()) as u64 - 16; + let f = ((x >> e) - 65536) as u64; + let (i, r) = ((f >> 12) as usize, f & 4095); + e * 65536 + LOG2_TABLE[i] + (((LOG2_TABLE[i + 1] - LOG2_TABLE[i]) * r) >> 12) +} + +/// The throughput the last `n` samples give, in bits per second: `⌊8000·Σbytes / Σms⌋`, +/// each sample's `ms` taken as at least 1; 0 with no sample. `n` 0 is taken as 1. Exact: +/// at most 255 samples of at most 2^64 − 1 bytes, below 2^85. +pub fn throughput_bps(samples: &[Sample], n: u8) -> u128 { + let k = usize::from(n.max(1)).min(samples.len()); + let tail = &samples[samples.len() - k..]; + let (b, t) = tail.iter().fold((0u128, 0u128), |(b, t), s| (b + u128::from(s.bytes), t + u128::from(s.ms.max(1)))); + if t == 0 { + return 0; + } + b * 8000 / t +} + +/// An option's rate in bits per second: `⌈8000·bytes / segment_ms⌉`, `segment_ms` taken +/// as at least 1. Exact: below 2^77. +pub fn option_bps(bytes: u64, segment_ms: u32) -> u128 { + (u128::from(bytes) * 8000).div_ceil(u128::from(segment_ms.max(1))) +} + +/// The highest index whose rate is at most `budget_bps`; 0 when none is. +fn highest_under(s: &AbrState, budget_bps: u128) -> usize { + s.option_bytes.iter().enumerate().rev().find(|(_, b)| option_bps(**b, s.segment_ms) <= budget_bps).map_or(0, |(k, _)| k) +} + +/// `⌊x·p/1000⌋`, exact for `x` below 2^111. +fn permille(x: u128, p: u16) -> u128 { + x * u128::from(p) / 1000 +} + +/// Whether `a·b > c·d` for `a`, `c` below 2^127 and `b`, `d` below 2^64: the products, +/// below 2^191, compared exactly in two 128-bit halves. +fn product_greater(a: u128, b: u64, c: u128, d: u64) -> bool { + // x·y = (x_hi·2^64 + x_lo)·y, with x_lo·y < 2^128 and x_hi·y < 2^127. + let wide = |x: u128, y: u64| -> (u128, u128) { + let (hi, lo) = (x >> 64, x & u128::from(u64::MAX)); + let low = lo * u128::from(y); + let mid = hi * u128::from(y); + let (sum, carry) = low.overflowing_add(mid << 64); + ((mid >> 64) + u128::from(carry), sum) + }; + wide(a, b) > wide(c, d) +} + +impl Policy for Throughput { + fn id(&self) -> &'static str { + "throughput" + } + fn choose(&self, s: &AbrState) -> usize { + if s.option_bytes.is_empty() { + return 0; + } + highest_under(s, permille(throughput_bps(&s.samples, self.last_n), self.safety_permille)) + } +} + +impl Policy for BufferBased { + fn id(&self) -> &'static str { + "buffer_based" + } + fn choose(&self, s: &AbrState) -> usize { + let k = s.option_bytes.len(); + if k == 0 { + return 0; + } + let (b, r, c) = (u64::from(s.buffer_ms), u64::from(self.reservoir_ms), u64::from(self.cushion_ms)); + if b <= r { + return 0; + } + if c == 0 || b >= r + c { + return k - 1; + } + let lo = option_bps(s.option_bytes[0], s.segment_ms); + let hi = option_bps(s.option_bytes[k - 1], s.segment_ms); + // (hi − lo)·(b − r) is below 2^109. + let target = lo + hi.saturating_sub(lo) * u128::from(b - r) / u128::from(c); + highest_under(s, target) + } +} + +/// Each option's utility, `ilog2_q16(bytes, the first option's bytes)`. +fn utilities(s: &AbrState) -> Vec { + let s0 = s.option_bytes.first().copied().unwrap_or(1).max(1); + s.option_bytes.iter().map(|&b| ilog2_q16(b.max(1), s0)).collect() +} + +impl Policy for BolaLike { + fn id(&self) -> &'static str { + "bola_like" + } + fn choose(&self, s: &AbrState) -> usize { + let k = s.option_bytes.len(); + if k == 0 { + return 0; + } + let seg = i128::from(s.segment_ms.max(1)); + let q = i128::from(s.buffer_ms) * 65536 / seg; + let qmax = i128::from(self.max_buffer_ms) * 65536 / seg; + let u = utilities(s); + let u_top = i128::from(*u.iter().max().unwrap_or(&0)); + let (vn, vd) = ((qmax - 65536).max(0), u_top + 65536); + // V_n·v·(u_k + 65536) is below 2^103, and a score below 2^77 in magnitude. + let score: Vec = u.iter().map(|&uk| vn * i128::from(self.v_milli) * (i128::from(uk) + 65536) / (vd * 1000) - q).collect(); + let size = |i: usize| s.option_bytes[i].max(1); + let mut best: Option = None; + if score.iter().any(|&n| n > 0) { + for i in (0..k).filter(|&i| score[i] > 0) { + // score[i]/size(i) > score[b]/size(b), by cross-multiplication widened + // past 128 bits: a score times a size reaches 2^141. + if best.is_none_or(|b| product_greater(score[i] as u128, size(b), score[b] as u128, size(i))) { + best = Some(i); + } + } + } else { + for i in 0..k { + if best.is_none_or(|b| score[i] > score[b]) { + best = Some(i); + } + } + } + best.unwrap_or(0) + } +} + +impl Policy for Mpc { + fn id(&self) -> &'static str { + "mpc" + } + fn choose(&self, s: &AbrState) -> usize { + let k = s.option_bytes.len(); + let pred = permille(throughput_bps(&s.samples, 5), self.safety_permille); + if k == 0 || pred == 0 { + return 0; + } + let mut h = u32::from(self.horizon.clamp(1, 5)); + while h > 1 && (k as u128).pow(h) > 65536 { + h -= 1; + } + let u = utilities(s); + let u_top = i128::from(*u.iter().max().unwrap_or(&0)); + let seg = u128::from(s.segment_ms); + let mut best: Option<(i128, usize)> = None; + let total = (k as u64).pow(h); + let mut seq = vec![0usize; h as usize]; + for code in 0..total { + // The sequence `code` names, most significant choice first. + let mut c = code; + for t in (0..h as usize).rev() { + seq[t] = (c % k as u64) as usize; + c /= k as u64; + } + // Exact in 128 bits: a download below 2^77 ms, the buffer below 2^35, the + // stall below 2^80, and the stall times the utility bound below 2^103. + let (mut b, mut rebuf, mut qoe) = (u128::from(s.buffer_ms), 0u128, 0i128); + let mut prev: Option = s.last.filter(|&l| l < k).map(|l| i128::from(u[l])); + for &o in &seq { + let dl = (u128::from(s.option_bytes[o]) * 8000).div_ceil(pred); + if dl > b { + rebuf += dl - b; + b = 0; + } else { + b -= dl; + } + b += seg; + let uo = i128::from(u[o]); + qoe += 1000 * uo; + if let Some(p) = prev { + qoe -= 1000 * (uo - p).abs(); + } + prev = Some(uo); + } + qoe -= rebuf as i128 * (u_top + 65536); + if best.is_none_or(|(q, _)| qoe > q) { + best = Some((qoe, seq[0])); + } + } + best.map_or(0, |(_, o)| o) + } +} + +/// The figure a receipt states, usable for a budget only when it carries its +/// acquisition class and the two cohere: an unlabelled figure, an unmetered one, or a +/// class beside a zero figure gives `None`. +pub fn labelled_figure(joules_micro: u64, class: Option<&crate::energy_class::EnergyProvenance>) -> Option { + (class.is_some() && joules_micro > 0 && crate::energy_class::figure_and_class_cohere(joules_micro, class)).then_some(joules_micro) +} + +/// Apply the state's joule budget to `choice` (Appendix F). A budget never raises the +/// choice: unchanged when no budget is held (`not_set`), or when no option has a +/// labelled figure (`not_applied`); otherwise the highest option at or below the choice +/// whose labelled figure is within the budget — the choice itself (`within`) or one below +/// it (`lowered`) — and, when there is none, option 0 with the budget unmet (`unmet`). +/// An option is one of `option_bytes` (or the choice, with none); a figure past them +/// names no option. +pub fn clamp_to_budget(s: &AbrState, choice: usize) -> (usize, Budget) { + let Some(b) = s.joule_budget_micro else { return (choice, Budget::NotSet) }; + let n = s.option_bytes.len().max(choice + 1); + let fig = |k: usize| if k < n { s.option_joules_micro.get(k).copied().flatten() } else { None }; + if (0..n).all(|k| fig(k).is_none()) { + return (choice, Budget::NotApplied); + } + match (0..=choice).rev().find(|&k| fig(k).is_some_and(|j| j <= b)) { + Some(k) if k == choice => (k, Budget::Within), + Some(k) => (k, Budget::Lowered), + None => (0, Budget::Unmet), + } +} + +/// The policy's choice under the state's budget; refused beyond Appendix F's bounds. +pub fn decide(p: &dyn Policy, s: &AbrState) -> Result<(usize, Budget), AbrError> { + p.check()?; + check_state(s)?; + Ok(clamp_to_budget(s, p.choose(s))) +} + +/// How many layers to fetch over a base (§10.4, the counterpart of a MoQ subscriber's +/// layer cap): `layers_n[0]` is the base's `envelope_len` and `layers_n[k]` layer `k`'s; +/// option `k` is fetching through layer `k`, its bytes the sum through it and its energy +/// the sum of the state's per-object labelled figures through it (`None` when any is +/// unlabelled). Returns the depth (0: the base alone) and what the budget did. Refused +/// beyond Appendix F's bounds: more than [`MAX_OPTIONS`] objects, or the bytes or the +/// figure through a depth past 2^64 − 1. +pub fn depth(layers_n: &[u64], p: &dyn Policy, s: &AbrState) -> Result<(usize, Budget), AbrError> { + p.check()?; + check_state(s)?; + if layers_n.len() > MAX_OPTIONS { + return Err(AbrError("more layer objects than 256")); + } + let mut st = s.clone(); + let mut acc = 0u64; + st.option_bytes = Vec::with_capacity(layers_n.len()); + for &n in layers_n { + acc = acc.checked_add(n).ok_or(AbrError("the bytes through a depth pass 2^64 - 1"))?; + st.option_bytes.push(acc); + } + st.option_joules_micro = Vec::with_capacity(layers_n.len()); + let mut fig = Some(0u64); + for k in 0..layers_n.len() { + fig = match (fig, s.option_joules_micro.get(k).copied().flatten()) { + (Some(a), Some(f)) => Some(a.checked_add(f).ok_or(AbrError("the figure through a depth passes 2^64 - 1"))?), + _ => None, + }; + st.option_joules_micro.push(fig); + } + if layers_n.is_empty() { + return Ok((0, clamp_to_budget(&st, 0).1)); + } + Ok(clamp_to_budget(&st, p.choose(&st))) +} + +/// SPEC §7.1 step 2's order for one `WAI2`, as a deployer policy: the selectable +/// entries whose capability `eligible` accepts, grouped by capability in the order each +/// first appears; within a group, the entries by component length (then table index) +/// are the options, `option_bytes` their lengths and `option_joules_micro` the state's +/// figure of each entry by its table index; the group is ordered as the policy's choice +/// under the budget, then the options below it from the nearest down, then those above +/// it from the nearest up. Every other entry follows in table order. A `WAI1` gives +/// `[0]`. Refused beyond Appendix F's bounds, a group of more than [`MAX_OPTIONS`] +/// entries among them. +pub fn order_entries(head: &Head, eligible: &dyn Fn(&str) -> bool, p: &dyn Policy, s: &AbrState) -> Result, AbrError> { + p.check()?; + check_state(s)?; + if head.version != Version::Wai2 { + return Ok(vec![0]); + } + match head.manifest_v2() { + Ok(m) => order_manifest(head, &m, eligible, p, s), + Err(_) => Ok((0..head.components.len()).collect()), + } +} + +/// [`order_entries`] with the manifest already read from `head`. +pub fn order_manifest(head: &Head, m: &ManifestV2, eligible: &dyn Fn(&str) -> bool, p: &dyn Policy, s: &AbrState) -> Result, AbrError> { + let e = &m.renditions; + let len = |i: usize| head.components.get(i).map_or(0, |c| c.len); + let mut groups: Vec<(String, Vec)> = Vec::new(); + for (i, r) in e.iter().enumerate() { + if !r.is_selectable() || !eligible(&r.capability) { + continue; + } + match groups.iter_mut().find(|(c, _)| *c == r.capability) { + Some((_, g)) => g.push(i), + None => groups.push((r.capability.clone(), vec![i])), + } + } + let mut order = Vec::with_capacity(e.len()); + for (_, mut g) in groups { + g.sort_by_key(|&i| (len(i), i)); + let mut st = s.clone(); + st.option_bytes = g.iter().map(|&i| len(i)).collect(); + st.option_joules_micro = g.iter().map(|&i| s.option_joules_micro.get(i).copied().flatten()).collect(); + let (c, _) = decide(p, &st)?; + let c = c.min(g.len() - 1); + order.push(g[c]); + order.extend(g[..c].iter().rev()); + order.extend(&g[c + 1..]); + } + let rest: Vec = (0..e.len()).filter(|i| !order.contains(i)).collect(); + order.extend(rest); + Ok(order) +} + +/// A [`super::session::Selector`] order from a policy and a state: [`order_manifest`] +/// over the head the session verified, on both paths. Refused when the policy or the +/// state is beyond Appendix F's bounds; a head whose group passes them keeps table +/// order, on which selection runs unchanged. +pub fn selector_order( + p: Box, + s: AbrState, + eligible: impl Fn(&str) -> bool + 'static, +) -> Result Vec>, AbrError> { + p.check()?; + check_state(&s)?; + Ok(Box::new(move |h: &Head, m: &ManifestV2| order_manifest(h, m, &eligible, p.as_ref(), &s).unwrap_or_else(|_| (0..m.renditions.len()).collect()))) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The widened cross-multiplication against products small enough for 128 bits, and + /// at the widths the utility-over-buffer rule reaches, where 128 bits wrap. + #[test] + fn product_greater_is_exact_past_128_bits() { + for (a, b, c, d) in [(3u128, 5u64, 4u128, 4u64), (4, 4, 3, 5), (7, 7, 7, 7), (0, 9, 0, 1), (1 << 100, 3, 1 << 101, 1)] { + assert_eq!(product_greater(a, b, c, d), a * u128::from(b) > c * u128::from(d), "{a}·{b} > {c}·{d}"); + } + // 2^76·(2^64 − 1) against (2^76 − 1)·(2^64 − 1) and 2^76·(2^64 − 2): both products + // pass 2^128, where a 128-bit product wraps. + let (big, top) = (1u128 << 76, u64::MAX); + assert!(product_greater(big, top, big - 1, top)); + assert!(product_greater(big, top, big, top - 1)); + assert!(!product_greater(big - 1, top, big, top)); + assert!(!product_greater(big, top, big, top)); + // Just past 2^128 against just below it: a 128-bit product wraps to the smaller. + let (a, b, c, d) = (1u128 << 65, (1u64 << 63) + 1, 1u128 << 64, u64::MAX); + assert!(product_greater(a, b, c, d) && !product_greater(c, d, a, b)); + assert!(a.wrapping_mul(u128::from(b)) < c.wrapping_mul(u128::from(d)), "128 bits would answer wrongly here"); + // The largest operands: (2^127 − 1)·(2^64 − 1). + assert!(product_greater(i128::MAX as u128, top, i128::MAX as u128 - 1, top)); + } +} diff --git a/wai/wai-rs/src/http_binding/dash.rs b/wai/wai-rs/src/http_binding/dash.rs new file mode 100644 index 000000000..6b2448c60 --- /dev/null +++ b/wai/wai-rs/src/http_binding/dash.rs @@ -0,0 +1,306 @@ +//! `wai.mpd` (http-delivery §11.1): a separate static MPD (ISO/IEC 23009-1) with one +//! `AdaptationSet` per WAI stream, `mimeType="application/wai"`, `@codecs` the union, an +//! `EssentialProperty` a client that does not implement the extension ignores the set +//! by, and a `SupplementalProperty` carrying the floor. Its timeline is the classical +//! one (§11.2: the same `Period@id` and `@start`, and a `SegmentTimeline` with the same +//! timescale, `t` and `d`), read from the classical MPD ([`DashTimeline::from_mpd`]), or, +//! beside a presentation with no MPD, from the classical HLS media playlist it shares a +//! timeline with ([`DashTimeline::from_hls`]). [`super::dash_read::check_dash_adjacent`] +//! checks the result against the classical MPD. +//! +//! [`render_mpd`] writes any static MPD of one period this binding's packagers need — +//! the classical one beside it too — and [`wai_mpd`] is `wai.mpd` on a classical +//! timeline. A DASH playlist is not authenticated in this revision: the playlist +//! receipt (§8.6) signs HLS renderings only. + +use super::codecs::{self, Element}; +use super::dash_read::{read_mpd, xml_char}; +use super::hls::{MediaPlaylist, PlaylistError}; + +/// The `EssentialProperty` scheme a client that does not implement the extension +/// ignores a WAI `AdaptationSet` by. +pub const SCHEME_DELIVERY: &str = "tag:transaction.science,2026:wai/http-delivery"; +/// The `SupplementalProperty` scheme whose `value` is the floor. +pub const SCHEME_FLOOR: &str = "tag:transaction.science,2026:wai/floor"; + +/// The classical timeline a WAI `AdaptationSet` is aligned with: one period's `@id` +/// (none when the classical period has none) and `@start` in milliseconds, and its +/// segments' `t` and `d` at `timescale`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DashTimeline { + pub period_id: Option, + pub period_start_ms: u64, + pub timescale: u64, + pub segments: Vec<(u64, u64)>, +} + +/// Segment times at a timescale: each `(start, duration)` in some unit, `t` counted from +/// `origin`, both multiplied by `per_unit` ticks. Refused (`playlist_malformed`) when a +/// start precedes `origin` or a product passes 2^64 − 1. +pub fn scaled_timeline(spans: &[(u64, u64)], origin: u64, per_unit: u64) -> Result, PlaylistError> { + spans.iter().map(|&(start, d)| { + let t = start.checked_sub(origin).ok_or_else(|| PlaylistError::Malformed("a segment before the timeline's start".into()))?; + let past = || PlaylistError::Malformed("a segment time past 2^64 - 1".into()); + Ok((t.checked_mul(per_unit).ok_or_else(past)?, d.checked_mul(per_unit).ok_or_else(past)?)) + }).collect() +} + +impl DashTimeline { + /// The timeline of a classical HLS media playlist at timescale 1000, for a + /// presentation with no classical MPD: `t` from 0, each `d` a segment's duration in + /// milliseconds. Refused (`playlist_malformed`) when a time passes 2^64 − 1. + pub fn from_hls(p: &MediaPlaylist, period_id: &str, period_start_ms: u64) -> Result { + let mut t = 0u64; + let mut segments = Vec::with_capacity(p.segments.len()); + for s in &p.segments { + segments.push((t, s.duration_ms)); + t = t.checked_add(s.duration_ms).ok_or_else(|| PlaylistError::Malformed("a segment time past 2^64 - 1".into()))?; + } + Ok(DashTimeline { period_id: Some(period_id.to_string()), period_start_ms, timescale: 1000, segments }) + } + + /// The timeline of a classical MPD (§11.1): the period with `@id` `period_id` (the + /// first by default), its adaptation set with `@id` `set_id` (by default the first + /// that has a timeline), and that set's first timeline (Appendix I), expanded to one + /// entry per segment. Refused (`playlist_malformed`) when the MPD does not read, + /// holds no such timeline, or lists more than `max_segments` segments there (the + /// caller's own count, so that no `@r` makes it hold more than it lists). + pub fn from_mpd(text: &str, period_id: Option<&str>, set_id: Option<&str>, max_segments: usize) -> Result { + let periods = read_mpd(text)?; + let p = match period_id { + Some(id) => periods.iter().find(|p| p.id.as_deref() == Some(id)), + None => periods.first(), + }.ok_or_else(|| PlaylistError::Malformed("no such Period in the classical MPD".into()))?; + let set = match set_id { + Some(id) => p.sets.iter().find(|s| s.id.as_deref() == Some(id)), + None => p.sets.iter().find(|s| !s.timelines.is_empty()), + }.ok_or_else(|| PlaylistError::Malformed("no such AdaptationSet in the classical MPD".into()))?; + let tl = set.timelines.first().ok_or_else(|| PlaylistError::Malformed("the classical AdaptationSet has no SegmentTimeline".into()))?; + let segments = tl.segments(max_segments) + .ok_or_else(|| PlaylistError::Malformed(format!("the classical timeline lists {} segments, more than {max_segments}", tl.segment_count())))?; + Ok(DashTimeline { period_id: p.id.clone(), period_start_ms: p.start_ms, timescale: tl.timescale, segments }) + } + + /// The timeline's length in milliseconds, and its longest segment in milliseconds. + fn lengths_ms(&self) -> (u64, u64) { + let to_ms = |ticks: u64| u64::try_from(u128::from(ticks) * 1000 / u128::from(self.timescale.max(1))).unwrap_or(u64::MAX); + (self.segments.iter().map(|&(_, d)| to_ms(d)).fold(0, u64::saturating_add), self.segments.iter().map(|&(_, d)| to_ms(d)).max().unwrap_or(0)) + } +} + +/// One `Representation` of the WAI `AdaptationSet`: its `@id`, its `@bandwidth` (the +/// peak of `hls::bandwidths`) and, per segment of the timeline, its object's URI with its +/// fragment, or `None` for a gap, which the representation leaves out (§11.1). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WaiRepresentation { + pub id: String, + pub bandwidth: u64, + pub media: Vec>, +} + +/// A descriptor (`EssentialProperty`, `SupplementalProperty`): its scheme and value. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Descriptor { + pub scheme: String, + pub value: Option, +} + +/// A `Representation` with a `SegmentList`: its `@id`, `@dependencyId` and +/// `@bandwidth`, the list's timescale, an `Initialization`, the `S` elements (`t`, +/// `d`) and one `SegmentURL` per segment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Representation { + pub id: String, + pub dependency_id: Option, + pub bandwidth: u64, + pub timescale: u64, + pub initialization: Option, + pub timeline: Vec<(u64, u64)>, + pub media: Vec, +} + +/// An `AdaptationSet`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AdaptationSet { + pub id: u32, + pub content_type: Option, + pub mime_type: String, + pub codecs: String, + pub size: Option<(u32, u32)>, + pub segment_alignment: bool, + pub start_with_sap: Option, + pub essential: Vec, + pub supplemental: Vec, + pub representations: Vec, +} + +/// A static MPD of one period. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Mpd { + /// `mediaPresentationDuration`, in milliseconds. + pub duration_ms: u64, + /// `minBufferTime`, in whole seconds. + pub min_buffer_s: u64, + /// `Period@id`, none when the classical period has none. + pub period_id: Option, + /// `Period@start`, in milliseconds. + pub period_start_ms: u64, + pub adaptation_sets: Vec, +} + +/// An XML attribute value, escaped: `&`, `<`, `>` and `"` as entities, and tab, LF and +/// CR as character references, which XML 1.0 §3.3.3 does not normalise to a space. +/// Refused (`playlist_malformed`) for a character that is not an XML `Char`. +fn esc(s: &str) -> Result { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '&' => out.push_str("&"), + '<' => out.push_str("<"), + '>' => out.push_str(">"), + '"' => out.push_str("""), + '\t' => out.push_str(" "), + '\n' => out.push_str(" "), + '\r' => out.push_str(" "), + c if xml_char(c) => out.push(c), + c => return Err(PlaylistError::Malformed(format!("U+{:04X} is not an XML character", c as u32))), + } + } + Ok(out) +} + +/// A duration of whole milliseconds as `xs:duration`: whole seconds without a fraction, +/// otherwise with three decimals. +fn xs_seconds(ms: u64) -> String { + if ms % 1000 == 0 { format!("PT{}S", ms / 1000) } else { format!("PT{}S", crate::hls_render::seconds(ms)) } +} + +fn id_ok(s: &str) -> bool { + !s.is_empty() && !s.chars().any(char::is_whitespace) +} + +/// An MPD's text: ``, `` (the DASH namespace, the full profile, `static`, +/// `mediaPresentationDuration` in seconds with three decimals, `minBufferTime` in whole +/// seconds), one `` (`@id` when it has one, `@start` in seconds, with three +/// decimals when not whole), each `AdaptationSet` +/// (`@id`, `@contentType`, `@mimeType`, `@codecs`, `@width`, `@height`, +/// `@segmentAlignment`, `@startWithSAP`, those given, in that order) with its essential +/// then supplemental descriptors and its representations (`@id`, `@dependencyId`, +/// `@bandwidth`), each a `SegmentList` (`@timescale`) of an `Initialization`, a +/// `SegmentTimeline` of one `S` per segment with its `t` and `d`, and one `SegmentURL` +/// per segment. Attribute values are escaped, elements are indented by two spaces a +/// level, and lines end in LF. Refused (`playlist_malformed`) for no adaptation set or +/// representation, an `@id` that is empty or holds white space, a timescale of 0, a +/// representation with no segment or whose timeline and segment URLs differ in number, +/// or a value holding a character that is not an XML `Char`. +pub fn render_mpd(m: &Mpd) -> Result { + let bad = |s: &str| PlaylistError::Malformed(s.to_string()); + if m.adaptation_sets.is_empty() || m.adaptation_sets.iter().any(|a| a.representations.is_empty()) || m.period_id.as_deref().is_some_and(|i| !id_ok(i)) { + return Err(bad("an MPD needs an adaptation set and a representation, and a period id without white space")); + } + let mut out = String::from("\n"); + out.push_str(&format!( + "\n", + crate::hls_render::seconds(m.duration_ms), m.min_buffer_s + )); + match &m.period_id { + Some(id) => out.push_str(&format!(" \n", esc(id)?, xs_seconds(m.period_start_ms))), + None => out.push_str(&format!(" \n", xs_seconds(m.period_start_ms))), + } + for a in &m.adaptation_sets { + out.push_str(&format!(" \n"); + for (tag, ds) in [("EssentialProperty", &a.essential), ("SupplementalProperty", &a.supplemental)] { + for d in ds { + out.push_str(&format!(" <{tag} schemeIdUri=\"{}\"", esc(&d.scheme)?)); + if let Some(v) = &d.value { + out.push_str(&format!(" value=\"{}\"", esc(v)?)); + } + out.push_str("/>\n"); + } + } + for r in &a.representations { + if !id_ok(&r.id) || r.dependency_id.as_deref().is_some_and(|d| !id_ok(d)) || r.timescale == 0 || r.timeline.is_empty() || r.timeline.len() != r.media.len() { + return Err(bad("a representation's id, dependency id, timescale or segment count")); + } + out.push_str(&format!(" \n \n", r.bandwidth, r.timescale)); + if let Some(i) = &r.initialization { + out.push_str(&format!(" \n", esc(i)?)); + } + out.push_str(" \n"); + for &(t, d) in &r.timeline { + out.push_str(&format!(" \n")); + } + out.push_str(" \n"); + for u in &r.media { + out.push_str(&format!(" \n", esc(u)?)); + } + out.push_str(" \n \n"); + } + out.push_str(" \n"); + } + out.push_str(" \n\n"); + Ok(out) +} + +/// `wai.mpd` (§11.1) as [`render_mpd`] writes it: one period on timeline `t`, its +/// length the timeline's and `minBufferTime` its longest segment rounded up to whole +/// seconds, and one WAI `AdaptationSet` (`@id` `set_id`, `application/wai`, the union as +/// `@codecs`, segment-aligned, the delivery `EssentialProperty` and the floor +/// `SupplementalProperty`), each representation listing the segments of the timeline it +/// offers and leaving out its gaps. Refused (`playlist_malformed`) as [`render_mpd`] +/// refuses, for no segment, a representation whose segments are not the timeline's in +/// number, or a floor whose container is not the union's. +pub fn wai_mpd(t: &DashTimeline, set_id: u32, union: &Element, floor: &Element, reps: &[WaiRepresentation]) -> Result { + if t.segments.is_empty() || t.timescale == 0 { + return Err(PlaylistError::Malformed("an MPD needs a segment and a timescale".into())); + } + if floor.prefix != union.prefix { + return Err(PlaylistError::Malformed("the floor is written with the union's prefix".into())); + } + let (total, longest) = t.lengths_ms(); + render_mpd(&Mpd { + duration_ms: total, + min_buffer_s: longest.div_ceil(1000), + period_id: t.period_id.clone(), + period_start_ms: t.period_start_ms, + adaptation_sets: vec![AdaptationSet { + id: set_id, + content_type: None, + mime_type: super::MEDIA_TYPE.into(), + codecs: codecs::render(union), + size: None, + segment_alignment: true, + start_with_sap: None, + essential: vec![Descriptor { scheme: SCHEME_DELIVERY.into(), value: None }], + supplemental: vec![Descriptor { scheme: SCHEME_FLOOR.into(), value: Some(codecs::render(floor)) }], + representations: reps.iter().map(|r| { + if r.media.len() != t.segments.len() { + return Err(PlaylistError::Malformed(format!("representation {}: {} segments on a timeline of {}", r.id, r.media.len(), t.segments.len()))); + } + let offered: Vec<(&(u64, u64), &String)> = t.segments.iter().zip(&r.media).filter_map(|(s, m)| m.as_ref().map(|m| (s, m))).collect(); + Ok(Representation { + id: r.id.clone(), dependency_id: None, bandwidth: r.bandwidth, timescale: t.timescale, initialization: None, + timeline: offered.iter().map(|(s, _)| **s).collect(), media: offered.iter().map(|(_, m)| (*m).clone()).collect(), + }) + }).collect::>()?, + }], + }) +} diff --git a/wai/wai-rs/src/http_binding/dash_read.rs b/wai/wai-rs/src/http_binding/dash_read.rs new file mode 100644 index 000000000..71e36cde2 --- /dev/null +++ b/wai/wai-rs/src/http_binding/dash_read.rs @@ -0,0 +1,537 @@ +//! Reading an MPD (http-delivery §11.2, Appendix I): the XML subset this binding reads, +//! an MPD's periods and the segment timelines of their adaptation sets, and the DASH +//! adjacency check — a `wai.mpd` beside its classical MPD has the same `Period@id`s and +//! `@start`s, and each of its timelines has a classical timeline's timescale and lists +//! only that timeline's segments, each with its `t` and `d`, in order. +//! +//! A timeline is held as it is written, one run per `S` (`@t`, `@d`, and `@r` + 1 +//! segments), never expanded: what a reader holds is bounded by the document's size, +//! whatever `@r` says, and timelines are compared run by run. A document holds at most +//! [`MAX_TIMELINES`] timelines, so comparing every WAI timeline with every classical one +//! is bounded too. +//! +//! The XML read is a subset of XML 1.0, stated in Appendix I, so that every conforming +//! reader refuses the same documents: no document type declaration, CDATA section or +//! processing instruction past the declaration; ASCII names; the five predefined +//! entities and character references to XML `Char`s; no attribute twice. + +use super::hls::{decimal_ms, PlaylistError}; + +/// The most `SegmentTimeline`s a document holds (Appendix I); a document past it is +/// refused. +/// cbindgen:ignore +pub const MAX_TIMELINES: usize = 256; + +/// The deepest element nesting read (Appendix I). +/// cbindgen:ignore +pub const MAX_DEPTH: usize = 64; + +fn malformed(s: impl Into) -> PlaylistError { + PlaylistError::Malformed(s.into()) +} + +fn misaligned(s: impl Into) -> PlaylistError { + PlaylistError::Misaligned(s.into()) +} + +/// An element read: its name, its attributes in document order, and its child elements +/// (text is not kept). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Element { + pub name: String, + pub attrs: Vec<(String, String)>, + pub children: Vec, +} + +impl Element { + /// The name after its last `:`. + pub fn local(&self) -> &str { + self.name.rsplit(':').next().unwrap_or(&self.name) + } + + pub fn attr(&self, name: &str) -> Option<&str> { + self.attrs.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str()) + } + + fn children_named<'a>(&'a self, local: &'a str) -> impl Iterator + 'a { + self.children.iter().filter(move |c| c.local() == local) + } +} + +/// Whether `c` is an XML `Char`. +pub fn xml_char(c: char) -> bool { + matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}') +} + +fn name_start(b: u8) -> bool { + b.is_ascii_alphabetic() || b == b'_' || b == b':' +} + +fn name_char(b: u8) -> bool { + name_start(b) || b.is_ascii_digit() || b == b'-' || b == b'.' +} + +struct Reader<'a> { + s: &'a str, + at: usize, +} + +impl<'a> Reader<'a> { + fn rest(&self) -> &'a str { + &self.s[self.at..] + } + + fn eat(&mut self, lit: &str) -> bool { + if self.rest().starts_with(lit) { + self.at += lit.len(); + true + } else { + false + } + } + + fn space(&mut self) -> bool { + let n = self.rest().bytes().take_while(|b| matches!(b, b' ' | b'\t' | b'\n')).count(); + self.at += n; + n > 0 + } + + fn name(&mut self) -> Result { + let r = self.rest().as_bytes(); + if r.first().is_none_or(|&b| !name_start(b)) { + return Err(malformed("a name")); + } + let n = r.iter().take_while(|&&b| name_char(b)).count(); + let out = self.rest()[..n].to_string(); + self.at += n; + Ok(out) + } + + /// A reference after its `&`, decoded. + fn reference(&mut self) -> Result { + let r = self.rest(); + let end = r.find(';').ok_or_else(|| malformed("an unterminated reference"))?; + let body = &r[..end]; + let c = match body { + "amp" => '&', + "lt" => '<', + "gt" => '>', + "quot" => '"', + "apos" => '\'', + _ => { + let n = if let Some(h) = body.strip_prefix("#x") { + (!h.is_empty() && h.len() <= 8 && h.bytes().all(|b| b.is_ascii_hexdigit())).then(|| u32::from_str_radix(h, 16).ok()).flatten() + } else if let Some(d) = body.strip_prefix('#') { + (!d.is_empty() && d.len() <= 10 && d.bytes().all(|b| b.is_ascii_digit())).then(|| d.parse::().ok()).flatten() + } else { + None + }; + n.and_then(char::from_u32).filter(|&c| xml_char(c)).ok_or_else(|| malformed("a reference to an entity or character outside the subset"))? + } + }; + self.at += end + 1; + Ok(c) + } + + fn attr_value(&mut self) -> Result { + let q = match self.rest().as_bytes().first() { + Some(b'"') => '"', + Some(b'\'') => '\'', + _ => return Err(malformed("an attribute value is quoted")), + }; + self.at += 1; + let mut out = String::new(); + loop { + let c = self.rest().chars().next().ok_or_else(|| malformed("an unterminated attribute value"))?; + self.at += c.len_utf8(); + match c { + _ if c == q => return Ok(out), + '<' => return Err(malformed("< in an attribute value")), + '&' => out.push(self.reference()?), + // XML 1.0 §3.3.3: white space in a value is normalised to a space. + '\t' | '\n' => out.push(' '), + c => out.push(c), + } + } + } + + /// Comments and white space; `false` at anything else. + fn misc(&mut self) -> Result<(), PlaylistError> { + loop { + if self.space() { + continue; + } + if self.rest().starts_with("") { + return Err(malformed("-- inside a comment")); + } + self.at += end + 3; + Ok(()) + } + + /// A start tag after its `<`: the element (children empty) and whether it closed + /// itself. + fn start_tag(&mut self) -> Result<(Element, bool), PlaylistError> { + let name = self.name()?; + let mut attrs: Vec<(String, String)> = Vec::new(); + // The names read so far, to refuse one twice in constant time per attribute. + let mut names = std::collections::HashSet::new(); + loop { + let spaced = self.space(); + if self.eat("/>") { + return Ok((Element { name, attrs, children: Vec::new() }, true)); + } + if self.eat(">") { + return Ok((Element { name, attrs, children: Vec::new() }, false)); + } + if !spaced { + return Err(malformed("an attribute is preceded by white space")); + } + let k = self.name()?; + self.space(); + if !self.eat("=") { + return Err(malformed("an attribute has a value")); + } + self.space(); + let v = self.attr_value()?; + if !names.insert(k.clone()) { + return Err(malformed(format!("the attribute {k} twice"))); + } + attrs.push((k, v)); + } + } +} + +/// Read an XML document in the subset of Appendix I: its root element. +pub fn read_xml(text: &str) -> Result { + // XML 1.0 §2.11: CR LF and a lone CR are read as LF. + let normalised = text.strip_prefix('\u{FEFF}').unwrap_or(text).replace("\r\n", "\n").replace('\r', "\n"); + if let Some(c) = normalised.chars().find(|&c| !xml_char(c)) { + return Err(malformed(format!("U+{:04X} is not an XML character", c as u32))); + } + let mut r = Reader { s: &normalised, at: 0 }; + if r.rest().starts_with("").ok_or_else(|| malformed("an unterminated declaration"))?; + r.at += end + 2; + } + r.misc()?; + if !r.eat("<") { + return Err(malformed("no root element")); + } + let mut stack: Vec = Vec::new(); + let (root, closed) = r.start_tag()?; + let mut done: Option = None; + if closed { + done = Some(root); + } else { + stack.push(root); + } + while done.is_none() { + let c = r.rest().chars().next().ok_or_else(|| malformed("an element is not closed"))?; + match c { + '<' => { + if r.rest().starts_with("text < \n\n"); + assert_eq!(e.attr("x"), Some("1&AB")); + assert_eq!(e.attr("y"), Some("t u v")); + assert_eq!(e.children.iter().map(|c| c.name.as_str()).collect::>(), ["b", "c"]); + for bad in ["", "", "", "", "", "", "", "", "", + "", "", "", "", "", "￾", "]]>", + "", "<é/>", "", "", "junk", "".repeat(MAX_DEPTH + 1), "".repeat(MAX_DEPTH + 1)); + assert!(read_xml(&deep).is_err()); + let ok_deep = format!("{}{}", "".repeat(MAX_DEPTH), "".repeat(MAX_DEPTH)); + assert!(read_xml(&ok_deep).is_ok()); + for (d, ms) in [("PT0S", Some(0)), ("PT1.5S", Some(1500)), ("PT0.0005S", Some(1)), ("PT0.0004S", Some(0)), ("P1D", Some(86_400_000)), ("PT1H1M", Some(3_660_000)), + ("P1DT1S", Some(86_401_000)), ("P", None), ("PT", None), ("P1DT", None), ("P1Y", None), ("P1M", None), ("PT1.5M", None), ("PT-1S", None), + ("PT1M1H", None), ("1S", None), ("PT1S ", None), ("PT+1S", None)] { + assert_eq!(duration_ms(d), ms, "{d}"); + } +} + +/// §11.2 in DASH: a WAI MPD beside its classical MPD, and each way it is not adjacent. +#[test] +fn dash_adjacency() { + use super::dash_read::check_dash_adjacent; + let classical = ""; + let wai = |period: &str, ts: u64, ss: &str| format!("{ss}"); + let code = |w: &str| check_dash_adjacent(w, classical).err().map(|e| e.code()); + assert_eq!(code(&wai("id=\"p\" start=\"PT2S\"", 25, "")), None); + assert_eq!(code(&wai("id=\"p\" start=\"PT2.000S\"", 25, "")), None); + assert_eq!(code(&wai("id=\"q\" start=\"PT2S\"", 25, "")), Some("adjacency_misaligned")); + assert_eq!(code(&wai("id=\"p\"", 25, "")), Some("adjacency_misaligned")); + assert_eq!(code(&wai("id=\"p\" start=\"PT2S\"", 50, "")), Some("adjacency_misaligned")); + assert_eq!(code(&wai("id=\"p\" start=\"PT2S\"", 25, "")), Some("adjacency_misaligned")); + assert_eq!(code(&wai("id=\"p\" start=\"PT2S\"", 25, "")), Some("adjacency_misaligned")); + assert_eq!(code(&wai("id=\"p\" start=\"PT2S\"", 25, "")), Some("adjacency_misaligned")); + assert_eq!(code(&wai("id=\"p\" start=\"PT2S\"", 25, "")), Some("adjacency_misaligned")); + assert_eq!(code(&wai("id=\"p\" start=\"PT2S\"", 25, "")), Some("playlist_malformed")); + // Run by run: a WAI run that spans two classical runs, a classical run entered + // part-way, and a run past the classical one's end. + let runs = ""; + let in_runs = |ss: &str| check_dash_adjacent(&wai("id=\"p\" start=\"PT2S\"", 25, ss), runs).err().map(|e| e.code()); + assert_eq!(in_runs(""), None, "across the two runs"); + assert_eq!(in_runs(""), None, "entered part-way, then the last"); + assert_eq!(in_runs(""), Some("adjacency_misaligned"), "one past the classical run"); + assert_eq!(in_runs(""), Some("adjacency_misaligned"), "between two classical segments"); + assert_eq!(in_runs(""), Some("adjacency_misaligned"), "back within a run"); + assert_eq!(in_runs(""), Some("playlist_malformed"), "an end past 2^64 - 1"); + assert_eq!(in_runs(""), Some("adjacency_misaligned"), "far past, in one step"); + // At most 256 timelines in a document. + let many = |n: usize| format!("{}", + "".repeat(n)); + assert!(super::dash_read::read_mpd(&many(super::dash_read::MAX_TIMELINES)).is_ok()); + assert_eq!(super::dash_read::read_mpd(&many(super::dash_read::MAX_TIMELINES + 1)).unwrap_err().code(), "playlist_malformed"); + assert_eq!(check_dash_adjacent(&wai("id=\"p\" start=\"PT2S\"", 25, ""), "").unwrap_err().code(), "playlist_malformed"); +} + +/// §5.2, §11.4: the cid a playlist lists for a part's alias (its `cid` fragment) and +/// for a segment or layer (the `.wai` name); nothing for a URI no line names. +#[test] +fn listed_cid_alias_and_name() { + let ll = parse_media_playlist(CLASSICAL_LL).unwrap(); + let mut s0 = seg(1, 2000); + s0.parts = (0..4).map(|i| wpart(1200, i, 500)).collect(); + s0.layers = vec![WaiLayer { n: 1, cid: cid(0xe1), len: 70 }]; + let open = WaiOpen { parts: (0..2).map(|i| wpart(1201, i, 500)).collect(), preload_hint: Some("live/1201.2.wai".into()) }; + let (_, _, _, text) = wai_media_playlist(&ll, &[s0], Some(&open), "objects/", false).unwrap(); + let p = parse_media_playlist(&text).unwrap(); + assert_eq!(listed_cid(&p, "live/1200.2.wai"), Some(cid(0x82))); + assert_eq!(listed_cid(&p, "live/1201.1.wai"), Some(cid(0x81))); + assert_eq!(listed_cid(&p, &format!("objects/{}.wai", super::hex(&cid(1)))), Some(cid(1))); + assert_eq!(listed_cid(&p, &format!("objects/{}.wai", super::hex(&cid(0xe1)))), Some(cid(0xe1))); + assert_eq!(listed_cid(&p, "live/1201.2.wai"), None, "a hint lists no cid"); + assert_eq!(listed_cid(&p, "live/9.9.wai"), None); +} + +#[cfg(feature = "video_manifest")] +mod receipts { + use super::super::hls::*; + use super::super::playlist_receipt::*; + use super::{cid, seg, wpart, CLASSICAL_LL, CLASSICAL_VOD}; + use crate::quantum_ops::GrantRef; + use crate::wai_video_manifest::ManifestReceipt; + use ed25519_dalek::SigningKey; + + fn key() -> SigningKey { + SigningKey::from_bytes(&[21u8; 32]) + } + + /// A WAI playlist sealed from its file and checked against its receipt file: the + /// name, the bytes, the signature, the file's form, each refused by its code. + #[test] + fn seal_and_check_playlist() { + let vod = parse_media_playlist(CLASSICAL_VOD).unwrap(); + let (_, _, _, text) = wai_media_playlist(&vod, &[seg(1, 640), seg(2, 640), seg(3, 640)], None, "../objects/", true).unwrap(); + let r = seal_playlist(&text, &key(), "did:example:test", "players/v0", GrantRef::unbounded("video.manifest"), None).unwrap(); + let file = r.to_json(); + let name = r.receipt_file_name(); + assert_eq!(name, format!("{}.json", blake3::hash(text.as_bytes()).to_hex())); + assert_eq!(check_playlist(text.as_bytes(), file.as_bytes(), Some(&name)).unwrap(), r); + assert_eq!(r.class, public_class("players/v0", 0)); + let code = |pl: &[u8], f: &[u8], n: Option<&str>| check_playlist(pl, f, n).unwrap_err().code(); + assert_eq!(code(text.as_bytes(), file.as_bytes(), Some("00.json")), "receipt_name_mismatch"); + let mut flipped = text.clone().into_bytes(); + flipped[40] ^= 1; + assert_eq!(code(&flipped, file.as_bytes(), Some(&name)), "playlist_receipt_mismatch"); + let sig = crate::http_binding::hex(&r.sig); + let mut bad_sig = sig.clone(); + bad_sig.replace_range(0..2, if &sig[0..2] == "00" { "01" } else { "00" }); + assert_eq!(code(text.as_bytes(), file.replace(&sig, &bad_sig).as_bytes(), Some(&name)), "playlist_receipt_invalid"); + assert_eq!(code(text.as_bytes(), b"{", Some(&name)), "playlist_receipt_malformed"); + assert_eq!(code(text.as_bytes(), file.replace("\"public\"", "\"svod\"").as_bytes(), Some(&name)), "playlist_receipt_invalid"); + // A playlist not in the rendered form is not sealed. + assert_eq!(seal_playlist(&text.replace('\n', "\r\n"), &key(), "o", "c", GrantRef::unbounded("video.manifest"), None).unwrap_err().code(), "playlist_malformed"); + } + + /// Live revisions sealed with their parents form a chain; one sealed against + /// another parent, or by another signer, breaks it at that revision. + #[test] + fn live_revision_chain() { + let ll = parse_media_playlist(CLASSICAL_LL).unwrap(); + let mut s0 = seg(1, 2000); + s0.parts = (0..4).map(|i| wpart(1200, i, 500)).collect(); + let revs: Vec = [1usize, 2].iter().map(|&k| { + let open = WaiOpen { parts: (0..2).map(|i| wpart(1201, i, 500)).collect(), preload_hint: Some(format!("live/1201.{}.wai", k + 1)) }; + wai_media_playlist(&ll, &[s0.clone()], Some(&open), "objects/", false).unwrap().3 + }).collect(); + let g = || GrantRef::unbounded("video.manifest"); + let a = seal_playlist(&revs[0], &key(), "k", "live", g(), None).unwrap(); + let b = seal_playlist(&revs[1], &key(), "k", "live", g(), Some(&a)).unwrap(); + check_chain(&[a.clone(), b.clone()]).unwrap(); + assert_eq!(a.class.window, 1200); + let orphan = seal_playlist(&revs[1], &key(), "k", "live", g(), None).unwrap(); + assert_eq!(check_chain(&[a.clone(), orphan]).unwrap_err(), (1, ReceiptError::Unchained)); + let other = seal_playlist(&revs[1], &SigningKey::from_bytes(&[22u8; 32]), "k", "live", g(), Some(&a)).unwrap(); + assert_eq!(check_chain(&[a.clone(), other]).unwrap_err().0, 1); + let _: Vec = vec![a, b]; + let _ = cid(0); + } + + /// A client's chain (§8.6): the head fetched again is unchanged; skipped revisions + /// are walked back by their links, up to the bound; a parent not served, served + /// under another receipt's name, or under another key is refused; an older revision + /// after a newer one does not chain; and a revision that lists another cid for an + /// alias or a sequence number is refused, in its own listing and as a parent walked. + /// Every refusal leaves the hold as it was. + #[test] + fn live_chain_walks_back_and_holds_bindings() { + let ll = parse_media_playlist(CLASSICAL_LL).unwrap(); + let mut s0 = seg(1, 2000); + s0.parts = (0..4).map(|i| wpart(1200, i, 500)).collect(); + let rev = |k: usize, parts: Vec| { + let open = WaiOpen { parts, preload_hint: Some(format!("live/1201.{}.wai", k + 1)) }; + wai_media_playlist(&ll, &[s0.clone()], Some(&open), "objects/", false).unwrap().3 + }; + let open = |n: u64| (0..n).map(|i| wpart(1201, i, 500)).collect::>(); // the classical open segment has two parts + let g = || GrantRef::unbounded("video.manifest"); + let a = seal_playlist(&rev(1, open(2)), &key(), "k", "live", g(), None).unwrap(); + // The second revision lists its parts under aliases of its own, so what it binds + // is seen only through it. + let b_parts: Vec = open(2).into_iter().map(|mut q| { q.alias = q.alias.replace("live/", "live/b/"); q }).collect(); + let b = seal_playlist(&rev(2, b_parts), &key(), "k", "live", g(), Some(&a)).unwrap(); + let c = seal_playlist(&rev(3, open(2)), &key(), "k", "live", g(), Some(&b)).unwrap(); + let served = [a.clone(), b.clone(), c.clone()]; + let fetch = |h: &[u8; 32]| served.iter().find(|x| x.receipt_hash() == *h).map(|x| x.to_json().into_bytes()); + let mut chain = LiveChain::new(); + assert_eq!(chain.advance(&a, fetch).unwrap(), Step::First); + assert_eq!(chain.advance(&a, fetch).unwrap(), Step::Unchanged); + assert_eq!(chain.advance(&c, fetch).unwrap(), Step::Chained { walked: 1 }); + assert_eq!(chain.head(), Some(&c)); + assert_eq!(chain.bindings().cid_of("objects/live/1201.1.wai"), None); + assert_eq!(chain.bindings().cid_of("live/1201.1.wai"), Some(cid(0x81))); + assert_eq!(chain.bindings().cid_of("live/b/1201.0.wai"), Some(cid(0x80)), "the walked parent's listing is bound"); + // Each refusal, with the hold unchanged after it. + let held = |k: &LiveChain| (k.head().cloned(), k.bindings().clone()); + let mut at_a = LiveChain::new(); + at_a.advance(&a, fetch).unwrap(); + let before = held(&at_a); + let mut tight = LiveChain::with_bound(0); + tight.advance(&a, fetch).unwrap(); + assert_eq!(tight.advance(&c, fetch).unwrap_err(), ReceiptError::WalkExceeded, "past the bound, and not ahead of the head"); + assert_eq!(at_a.advance(&c, |_| None).unwrap_err(), ReceiptError::ParentMissing); + assert_eq!(at_a.advance(&c, |_| Some(a.to_json().into_bytes())).unwrap_err(), ReceiptError::NameMismatch); + assert_eq!(at_a.advance(&c, |_| Some(b"{".to_vec())).unwrap_err().code(), "playlist_receipt_malformed"); + // A parent named by its link that does not verify. + let mut b_bad = b.clone(); + b_bad.viewers_served = 2; + let c_over_bad = seal_playlist(&rev(3, open(2)), &key(), "k", "live", g(), Some(&b_bad)).unwrap(); + assert_eq!(at_a.advance(&c_over_bad, |_| Some(b_bad.to_json().into_bytes())).unwrap_err(), ReceiptError::Invalid); + let other = SigningKey::from_bytes(&[22u8; 32]); + let b_other = seal_playlist(&rev(2, open(2)), &other, "k", "live", g(), Some(&a)).unwrap(); + let c_over_other = seal_playlist(&rev(3, open(2)), &key(), "k", "live", g(), Some(&b_other)).unwrap(); + assert_eq!(at_a.advance(&c_over_other, |_| Some(b_other.to_json().into_bytes())).unwrap_err(), ReceiptError::Unchained); + assert_eq!(at_a.advance(&b_other, fetch).unwrap_err(), ReceiptError::Unchained); + // Part 1201.0 listed again with part 1's object. + let mut relisted = open(2); + relisted[0].cid = cid(0x81); + let b_re = seal_playlist(&rev(2, relisted.clone()), &key(), "k", "live", g(), Some(&a)).unwrap(); + assert_eq!(at_a.advance(&b_re, fetch).unwrap_err(), ReceiptError::Rebound); + let c_over_re = seal_playlist(&rev(3, open(2)), &key(), "k", "live", g(), Some(&b_re)).unwrap(); + assert_eq!(at_a.advance(&c_over_re, |_| Some(b_re.to_json().into_bytes())).unwrap_err(), ReceiptError::Rebound); + assert_eq!(held(&at_a), before); + // The listing alone, without receipts. + let mut listed = Bindings::default(); + listed.bind(&parse_media_playlist(&rev(1, open(2))).unwrap()).unwrap(); + assert_eq!(listed.bind(&parse_media_playlist(&rev(2, relisted)).unwrap()).unwrap_err().code(), "playlist_listing_rebound"); + // Past the bound, a revision ahead of the head (a segment more) re-anchors the + // chain, keeping what it bound: a rebinding there is still refused; and a revision + // not ahead never re-anchors it backwards. + let parts = |seq: u64, n: u64| (0..n).map(|i| format!("#EXT-X-PART:DURATION=0.5,URI=\"c/{seq}.{i}.m4s\"\n")).collect::(); + let classical_ahead = format!("#EXTM3U\n#EXT-X-VERSION:9\n#EXT-X-TARGETDURATION:2\n#EXT-X-PART-INF:PART-TARGET=0.5\n#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=1.5\n\ + #EXT-X-MEDIA-SEQUENCE:1200\n#EXT-X-PROGRAM-DATE-TIME:2026-10-01T12:00:00.000Z\n{}#EXTINF:2.00002,\nc/1200.m4s\n{}#EXTINF:2,\nc/1201.m4s\n{}\ + #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"c/1202.2.m4s\"\n", parts(1200, 4), parts(1201, 4), parts(1202, 2)); + let ll_ahead = parse_media_playlist(&classical_ahead).unwrap(); + let (mut s0, mut s1) = (seg(1, 2000), seg(2, 2000)); + s0.parts = (0..4).map(|i| wpart(1200, i, 500)).collect(); + s1.parts = (0..4).map(|i| wpart(1201, i, 500)).collect(); + let open = WaiOpen { parts: (0..2).map(|i| wpart(1202, i, 500)).collect(), preload_hint: Some("live/1202.2.wai".into()) }; + let t = wai_media_playlist(&ll_ahead, &[s0, s1], Some(&open), "objects/", false).unwrap().3; + let d = seal_playlist(&t, &key(), "k", "live", g(), Some(&c)).unwrap(); + let mut far = LiveChain::with_bound(0); + far.advance(&a, fetch).unwrap(); + assert_eq!(far.advance(&d, fetch).unwrap(), Step::Reanchored { walked: 0 }); + assert_eq!(far.head(), Some(&d)); + assert_eq!(far.bindings().cid_of("live/b/1201.0.wai"), None, "the skipped revisions are not bound"); + assert_eq!(far.bindings().cid_of("live/1200.0.wai"), Some(cid(0x80)), "what the chain bound is kept"); + assert_eq!(far.advance(&d, fetch).unwrap(), Step::Unchanged); + assert_eq!(far.advance(&c, fetch).unwrap_err(), ReceiptError::WalkExceeded, "never backwards"); + // A re-anchoring revision that rebinds what the chain kept is refused. + let mut s0x = seg(1, 2000); + s0x.parts = (0..4).map(|i| wpart(1200, i, 500)).collect(); + s0x.parts[0].cid = cid(0x99); + let (mut s1x, openx) = (seg(2, 2000), WaiOpen { parts: (0..2).map(|i| wpart(1202, i, 500)).collect(), preload_hint: Some("live/1202.2.wai".into()) }); + s1x.parts = (0..4).map(|i| wpart(1201, i, 500)).collect(); + let tx = wai_media_playlist(&ll_ahead, &[s0x, s1x], Some(&openx), "objects/", false).unwrap().3; + let dx = seal_playlist(&tx, &key(), "k", "live", g(), Some(&c)).unwrap(); + let mut farx = LiveChain::with_bound(0); + farx.advance(&a, fetch).unwrap(); + assert_eq!(farx.advance(&dx, fetch).unwrap_err(), ReceiptError::Rebound); + // An older revision after a newer one walks back past the start and does not chain. + assert_eq!(check_chain(&[a.clone(), c.clone(), b.clone()]).unwrap_err(), (2, ReceiptError::Unchained)); + check_chain(&[a.clone(), a.clone(), b.clone(), c.clone(), c]).unwrap(); + } + + /// A media sequence number keeps its segment's cid across revisions; a gap binds + /// none, and does not withdraw one already bound. + #[test] + fn a_sequence_number_keeps_its_cid() { + let vod = parse_media_playlist(CLASSICAL_VOD).unwrap(); + let text = |segs: &[WaiSegment]| wai_media_playlist(&vod, segs, None, "../objects/", true).unwrap().3; + let mut gap = seg(9, 640); + gap.gap = true; + let mut b = Bindings::default(); + b.bind(&parse_media_playlist(&text(&[seg(1, 640), gap.clone(), seg(3, 640)])).unwrap()).unwrap(); + b.bind(&parse_media_playlist(&text(&[seg(1, 640), seg(2, 640), seg(3, 640)])).unwrap()).unwrap(); + assert_eq!(b.clone().bind(&parse_media_playlist(&text(&[seg(1, 640), gap, seg(3, 640)])).unwrap()).unwrap_err().code(), "playlist_listing_rebound", + "a segment listed is not withdrawn as a gap"); + assert_eq!(b.bind(&parse_media_playlist(&text(&[seg(1, 640), seg(9, 640), seg(3, 640)])).unwrap()).unwrap_err().code(), "playlist_listing_rebound"); + } + + /// An alias is bound by the URL it names: another spelling of it is the same + /// binding, refused with another cid and taken with its own. + #[test] + fn an_alias_is_bound_by_its_url() { + let ll = parse_media_playlist(CLASSICAL_LL).unwrap(); + let rev = |alias: &str, c: u8| { + let mut q = wpart(1201, 0, 500); + q.alias = alias.into(); + q.cid = cid(c); + let open = WaiOpen { parts: vec![q, wpart(1201, 1, 500)], preload_hint: None }; + let mut s0 = seg(1, 2000); + s0.parts = (0..4).map(|i| wpart(1200, i, 500)).collect(); + parse_media_playlist(&wai_media_playlist(&ll, &[s0], Some(&open), "objects/", false).unwrap().3).unwrap() + }; + let mut b = Bindings::for_playlist("https://origin.example/live/wai.m3u8"); + b.bind(&rev("live/1201.0.wai", 0x80)).unwrap(); + for spelling in ["./live/1201.0.wai", "live/x/../1201.0.wai", "/live/live/1201.0.wai", "HTTPS://Origin.Example/live/live/1201.0.wai", "live/1201%2E0.wai"] { + assert_eq!(b.clone().bind(&rev(spelling, 0x99)).unwrap_err().code(), "playlist_listing_rebound", "{spelling}"); + b.clone().bind(&rev(spelling, 0x80)).unwrap(); + } + assert_eq!(b.cid_of("./live/1201.0.wai"), Some(cid(0x80))); + b.bind(&rev("live/1201.0.wai?x", 0x99)).unwrap(); + } + + /// A binding is kept while one of the last RETENTION revisions lists it; a + /// rebinding of what none of them listed is not detected, and what is bound stays + /// bounded however long the playlist runs. + #[test] + fn bindings_are_retained_for_a_span() { + let vod = parse_media_playlist(CLASSICAL_VOD).unwrap(); + let text = |segs: &[WaiSegment]| parse_media_playlist(&wai_media_playlist(&vod, segs, None, "../objects/", true).unwrap().3).unwrap(); + let first = text(&[seg(1, 640), seg(2, 640), seg(3, 640)]); + let other = text(&[seg(4, 640), seg(5, 640), seg(6, 640)]); + let mut b = Bindings::default(); + b.bind(&first).unwrap(); + let held = b.len(); + for _ in 1..RETENTION { + let mut shifted = other.clone(); + shifted.media_sequence = 1000; + b.bind(&shifted).unwrap(); + } + // The last revision of the span: the first one's listings are still held. + let mut rebind = first.clone(); + rebind.segments[0].uri = rebind.segments[1].uri.clone(); + assert_eq!(b.clone().bind(&rebind).unwrap_err().code(), "playlist_listing_rebound"); + let mut shifted = other.clone(); + shifted.media_sequence = 1000; + b.bind(&shifted).unwrap(); + // Past the span: forgotten, so the same rebinding is no longer detected. + b.clone().bind(&rebind).unwrap(); + assert!(b.len() <= 2 * held, "{} held", b.len()); + // The same span for an alias, apart from any sequence number. + let ll = parse_media_playlist(CLASSICAL_LL).unwrap(); + let mut s0 = seg(1, 2000); + s0.parts = (0..4).map(|i| wpart(1200, i, 500)).collect(); + let open = WaiOpen { parts: (0..2).map(|i| wpart(1201, i, 500)).collect(), preload_hint: None }; + let live = parse_media_playlist(&wai_media_playlist(&ll, &[s0], Some(&open), "objects/", false).unwrap().3).unwrap(); + let mut a = Bindings::default(); + a.bind(&live).unwrap(); + let mut relisted = live.clone(); + let line = relisted.open.as_ref().unwrap().parts[0].uri.clone(); + relisted.open.as_mut().unwrap().parts[0].uri = line.replace(&super::super::hex(&cid(0x80)), &super::super::hex(&cid(0x99))); + relisted.segments.clear(); + let unrelated = text(&[seg(4, 640), seg(5, 640), seg(6, 640)]); + for _ in 1..RETENTION { + a.bind(&unrelated).unwrap(); + } + assert_eq!(a.clone().bind(&relisted).unwrap_err().code(), "playlist_listing_rebound", "the alias is still held"); + a.bind(&unrelated).unwrap(); + a.bind(&relisted).unwrap(); + } +} + + +/// The lines a packager's playlists carry beside the timeline: `EXT-X-PLAYLIST-TYPE`, +/// `EXT-X-INDEPENDENT-SEGMENTS` and `EXT-X-MAP` in the header, `EXT-X-GAP` before a +/// segment's `EXTINF`. Each renders and reads back; the WAI playlist takes the +/// classical one's type and independence and never its map; a gap does not move +/// alignment; the open segment has no gap. +#[test] +fn packager_lines_render_and_read_back() { + use crate::hls_render::{check_render, PlaylistType, Segment, SegmentKind}; + let classical = "#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-TARGETDURATION:3\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXT-X-INDEPENDENT-SEGMENTS\n#EXT-X-MAP:URI=\"init.mp4\"\n#EXTINF:2.400,\na.m4s\n#EXTINF:1.000,\nb.m4s\n#EXT-X-ENDLIST\n"; + let c = parse_media_playlist(classical).unwrap(); + assert_eq!((c.playlist_type, c.independent_segments, c.map_uri.as_deref()), (Some(PlaylistType::Vod), true, Some("init.mp4"))); + assert!(c.is_rendered_form(classical), "the classical playlist is in the rendered form"); + let mut b = seg(2, 1000); + b.gap = true; + let (_, r, _, w) = wai_media_playlist(&c, &[seg(1, 2400), b], None, "../../objects/", true).unwrap(); + assert_eq!(r.map_uri, None); + assert!(w.starts_with("#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-TARGETDURATION:3\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXT-X-INDEPENDENT-SEGMENTS\n#EXTINF:2.400,\n"), "{w}"); + assert!(w.contains("#EXT-X-GAP\n#EXTINF:1.000,\n../../objects/0202"), "{w}"); + check_adjacent(&w, classical).unwrap(); + let back = parse_media_playlist(&w).unwrap(); + assert!(back.segments[1].extra.gap && back.is_rendered_form(&w)); + for bad in ["#EXTM3U\n#EXT-X-PLAYLIST-TYPE:LIVE\n", "#EXTM3U\n#EXT-X-MAP:BYTERANGE=\"1@0\"\n"] { + assert_eq!(parse_media_playlist(bad).unwrap_err().code(), "playlist_malformed", "{bad:?}"); + } + // A map after the first segment is not the header's. + let late = parse_media_playlist("#EXTM3U\n#EXTINF:1,\na\n#EXT-X-MAP:URI=\"i.mp4\"\n#EXTINF:1,\nb\n").unwrap(); + assert_eq!(late.map_uri, None); + let segs = vec![Segment { index: 0, kind: SegmentKind::Content, uri: "a".into(), duration_ms: 1000, ad_id: None }]; + let mut r = crate::hls_render::HlsRender { end_list: false, part_target_ms: Some(500), part_hold_back_ms: Some(1000), ..crate::hls_render::HlsRender::legacy() }; + let gap_open = vec![SegmentExtra::default(), SegmentExtra { gap: true, parts: vec![crate::hls_render::PartLine { duration_ms: 500, independent: true, uri: "p".into(), layers: vec![] }], ..Default::default() }]; + assert!(check_render(&segs, &r, &gap_open).is_err(), "a gap on the open segment"); + r.map_uri = Some("i\"nit".into()); + assert!(check_render(&segs, &r, &[]).is_err(), "a quote in the map URI"); +} + +/// A multivariant playlist of either kind: a classical one with a version, independent +/// segments and the session-data line; a WAI one naming its class and stage view. +#[test] +fn multivariant_of_either_kind() { + let floor = StreamInf { uri: "../classic/floor-v.m3u8".into(), bandwidth: 10_295, average_bandwidth: Some(9_026), codecs: "av01.0.00M.08".into(), + wai_floor: None, resolution: Some((64, 36)), frame_rate_milli: None }; + let classic = Multivariant { version: Some(7), independent_segments: true, session_data: vec![], variants: vec![floor.clone()] }; + let c = render_multivariant(&classic).unwrap(); + assert_eq!(c, "#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-INDEPENDENT-SEGMENTS\n#EXT-X-STREAM-INF:BANDWIDTH=10295,AVERAGE-BANDWIDTH=9026,CODECS=\"av01.0.00M.08\",RESOLUTION=64x36\n../classic/floor-v.m3u8\n"); + let class = Multivariant { session_data: vec![SessionData::Value { data_id: SESSION_DATA_ID.into(), value: "wai.m3u8".into() }], ..classic.clone() }; + let k = render_multivariant(&class).unwrap(); + classical_unchanged(&c, &k).unwrap(); + let wai = Multivariant { + version: Some(7), independent_segments: true, + session_data: vec![SessionData::Value { data_id: "science.transaction.wai.class".into(), value: "tv".into() }, + SessionData::Uri { data_id: "science.transaction.wai.stages".into(), uri: "../../stages/tv.json".into() }], + variants: vec![StreamInf { uri: "wai-v.m3u8".into(), bandwidth: 143_528, average_bandwidth: Some(121_122), codecs: "wai1.video.int_motion".into(), + wai_floor: Some("wai1.video.int_motion".into()), resolution: Some((64, 36)), frame_rate_milli: None }], + }; + assert!(render_multivariant(&wai).unwrap().contains("#EXT-X-SESSION-DATA:DATA-ID=\"science.transaction.wai.stages\",URI=\"../../stages/tv.json\"\n#EXT-X-STREAM-INF:BANDWIDTH=143528,AVERAGE-BANDWIDTH=121122,CODECS=\"wai1.video.int_motion\",X-WAI-FLOOR=\"wai1.video.int_motion\",RESOLUTION=64x36\nwai-v.m3u8\n")); + let mut bad = class.clone(); + bad.session_data = vec![SessionData::Value { data_id: "a\"b".into(), value: "x".into() }]; + assert_eq!(render_multivariant(&bad).unwrap_err().code(), "playlist_malformed"); + let mut bad = classic.clone(); + bad.version = Some(0); + assert!(render_multivariant(&bad).is_err()); + let mut bad = classic; + bad.variants[0].codecs = "a\nb".into(); + assert!(render_multivariant(&bad).is_err()); +} + +/// The one DASH renderer writes a classical MPD (an initialization, `startWithSAP`) and a +/// layered WAI one (a dependency per layer, a timeline with a gap), with durations in +/// seconds with three decimals and the buffer and start in whole seconds. +#[test] +fn render_mpd_of_either_kind() { + use super::dash::*; + let rep = |id: &str, dep: Option<&str>, init: Option<&str>, tl: Vec<(u64, u64)>| Representation { + id: id.into(), dependency_id: dep.map(String::from), bandwidth: 100, timescale: 25, initialization: init.map(String::from), + media: tl.iter().map(|(t, _)| format!("s{t}.wai")).collect(), timeline: tl, + }; + let set = |mime: &str, align: bool, sap: Option, reps: Vec| AdaptationSet { + id: 1, content_type: Some("video".into()), mime_type: mime.into(), codecs: "c".into(), size: Some((64, 36)), segment_alignment: align, + start_with_sap: sap, essential: vec![], supplemental: vec![Descriptor { scheme: SCHEME_FLOOR.into(), value: Some("f&g".into()) }], representations: reps, + }; + let m = Mpd { duration_ms: 8000, min_buffer_s: 3, period_id: Some("p0".into()), period_start_ms: 0, adaptation_sets: vec![ + set("video/mp4", true, Some(1), vec![rep("v-floor", None, Some("../floor/v/init.mp4"), vec![(0, 60), (60, 25)])])] }; + let t = render_mpd(&m).unwrap(); + assert!(t.contains("mediaPresentationDuration=\"PT8.000S\" minBufferTime=\"PT3S\">\n \n")); + assert!(t.contains("")); + assert!(t.contains("value=\"f&g\"") && t.contains("\n \n \n")); + let w = Mpd { adaptation_sets: vec![set("application/wai", false, None, vec![ + rep("v-l0", None, None, vec![(0, 60), (60, 25), (85, 50)]), rep("v-l1", Some("v-l0"), None, vec![(0, 60), (85, 50)])])], ..m.clone() }; + let t = render_mpd(&w).unwrap(); + assert!(t.contains("") && !t.contains("segmentAlignment")); + let mut bad = w.clone(); + bad.adaptation_sets[0].representations[1].media.pop(); + assert_eq!(render_mpd(&bad).unwrap_err().code(), "playlist_malformed"); + let mut bad = w.clone(); + bad.adaptation_sets[0].representations[0].dependency_id = Some("a b".into()); + assert!(render_mpd(&bad).is_err()); + // A start with a fraction, a period with no id, and values escaped as XML reads them + // back: tab, LF and CR as references, a character outside XML refused. + let mut odd = w.clone(); + odd.period_start_ms = 2500; + odd.period_id = None; + odd.adaptation_sets[0].supplemental[0].value = Some("a\tb\nc\rd".into()); + let t = render_mpd(&odd).unwrap(); + assert!(t.contains(" \n") && t.contains("value=\"a b c d\""), "{t}"); + let back = super::dash_read::read_xml(&t).unwrap(); + let sup = &back.children[0].children[0].children[0]; + assert_eq!(sup.attr("value"), Some("a\tb\nc\rd")); + for c in ['\u{0}', '\u{1}', '\u{b}', '\u{1f}', '\u{fffe}', '\u{ffff}'] { + let mut bad = w.clone(); + bad.adaptation_sets[0].codecs = format!("a{c}b"); + assert_eq!(render_mpd(&bad).unwrap_err().code(), "playlist_malformed", "{c:?}"); + } +} + +/// §5.4: a segment's object is listed with its `n`, and with its `h` only for a `WAI2`. +#[test] +fn segment_hints_follow_the_container() { + use crate::container::{Rendition, WaiMulti}; + let w2 = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![Rendition::media("wai.video.av1", "av1", vec![7u8; 3000]).with_id("a").with_payload_digest()]).to_bytes().unwrap(); + let w1 = crate::container::Wai::new(crate::container::Manifest::new("video", "replicate", crate::container::ModelRequirement::new("wai.video.av1"), crate::container::Conditioning::new("av1")), vec![7u8; 3000]).to_bytes().unwrap(); + assert_eq!(WaiSegment::of(&w2, 1000).unwrap().h, Some(1009)); + assert_eq!(WaiSegment::of(&w1, 1000).unwrap().h, None); +} diff --git a/wai/wai-rs/src/http_binding/uri.rs b/wai/wai-rs/src/http_binding/uri.rs new file mode 100644 index 000000000..0f23cf969 --- /dev/null +++ b/wai/wai-rs/src/http_binding/uri.rs @@ -0,0 +1,201 @@ +//! A URI reference resolved against a base and normalized (RFC 3986 §5.2, §6.2.2), so +//! that two spellings of one URL are one key (http-delivery §11.5): what a playlist lists +//! is bound by the URL it names, not by how a line happens to write it. +//! +//! Resolution is §5.2.2's strict algorithm, with §5.2.3's merge and §5.2.4's removal of +//! dot segments. Normalization is §6.2.2's: the scheme and the host in lower case, the +//! hexadecimal digits of a percent-encoding in upper case, a percent-encoded unreserved +//! character decoded, and dot segments removed again from the result. The fragment is +//! dropped: it names nothing the server holds. A base without a scheme is used as it is, +//! so that relative names still resolve relative to each other. + +/// The five components of a reference (RFC 3986 Appendix B), each absent or present +/// (possibly empty). +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct Parts { + scheme: Option, + authority: Option, + path: String, + query: Option, +} + +/// Split `s` as Appendix B's expression does; the fragment is dropped. +fn split(s: &str) -> Parts { + let s = s.split_once('#').map_or(s, |(a, _)| a); + let mut rest = s; + let mut p = Parts::default(); + if let Some(i) = rest.find([':', '/', '?']) { + if rest.as_bytes()[i] == b':' && i > 0 { + p.scheme = Some(rest[..i].to_string()); + rest = &rest[i + 1..]; + } + } + if let Some(r) = rest.strip_prefix("//") { + let end = r.find(['/', '?']).unwrap_or(r.len()); + p.authority = Some(r[..end].to_string()); + rest = &r[end..]; + } + match rest.split_once('?') { + Some((path, q)) => { + p.path = path.to_string(); + p.query = Some(q.to_string()); + } + None => p.path = rest.to_string(), + } + p +} + +/// §5.2.4: remove `.` and `..` segments. +fn remove_dot_segments(path: &str) -> String { + let mut input = path; + let mut out = String::with_capacity(path.len()); + while !input.is_empty() { + if let Some(r) = input.strip_prefix("../") { + input = r; + } else if let Some(r) = input.strip_prefix("./") { + input = r; + } else if input.starts_with("/./") { + input = &input[2..]; + } else if input == "/." { + input = "/"; + } else if input.starts_with("/../") || input == "/.." { + input = if input == "/.." { "/" } else { &input[3..] }; + match out.rfind('/') { + Some(i) => out.truncate(i), + None => out.clear(), + } + } else if input == "." || input == ".." { + input = ""; + } else { + let start = usize::from(input.starts_with('/')); + let end = input[start..].find('/').map_or(input.len(), |i| i + start); + out.push_str(&input[..end]); + input = &input[end..]; + } + } + out +} + +/// §5.2.3: merge a relative path with the base's. +fn merge(base: &Parts, path: &str) -> String { + if base.authority.is_some() && base.path.is_empty() { + format!("/{path}") + } else { + match base.path.rfind('/') { + Some(i) => format!("{}{path}", &base.path[..=i]), + None => path.to_string(), + } + } +} + +/// §6.2.2.2: hexadecimal digits of a percent-encoding in upper case, and a +/// percent-encoded unreserved character decoded. +fn normalize_percent(s: &str) -> String { + let b = s.as_bytes(); + let mut out = String::with_capacity(s.len()); + let mut i = 0; + while i < b.len() { + if b[i] == b'%' && i + 2 < b.len() && b[i + 1].is_ascii_hexdigit() && b[i + 2].is_ascii_hexdigit() { + let v = u8::from_str_radix(&s[i + 1..i + 3], 16).expect("two hexadecimal digits"); + if v.is_ascii_alphanumeric() || b"-._~".contains(&v) { + out.push(v as char); + } else { + out.push('%'); + out.push_str(&s[i + 1..i + 3].to_ascii_uppercase()); + } + i += 3; + } else { + let c = s[i..].chars().next().expect("a character"); + out.push(c); + i += c.len_utf8(); + } + } + out +} + +/// `reference` resolved against `base` (§5.2.2) and normalized (§6.2.2), without its +/// fragment. +pub fn resolve(base: &str, reference: &str) -> String { + let (b, r) = (split(base), split(reference)); + let t = if r.scheme.is_some() { + Parts { scheme: r.scheme, authority: r.authority, path: remove_dot_segments(&r.path), query: r.query } + } else if r.authority.is_some() { + Parts { scheme: b.scheme, authority: r.authority, path: remove_dot_segments(&r.path), query: r.query } + } else if r.path.is_empty() { + Parts { scheme: b.scheme, authority: b.authority, path: b.path, query: r.query.or(b.query) } + } else if r.path.starts_with('/') { + Parts { scheme: b.scheme, authority: b.authority, path: remove_dot_segments(&r.path), query: r.query } + } else { + let merged = merge(&b, &r.path); + Parts { scheme: b.scheme, authority: b.authority, path: remove_dot_segments(&merged), query: r.query } + }; + let mut out = String::new(); + if let Some(s) = &t.scheme { + out.push_str(&s.to_ascii_lowercase()); + out.push(':'); + } + if let Some(a) = &t.authority { + out.push_str("//"); + // The host (after any userinfo) in lower case. + let a = normalize_percent(a); + match a.rfind('@') { + Some(i) => { + out.push_str(&a[..=i]); + out.push_str(&a[i + 1..].to_ascii_lowercase()); + } + None => out.push_str(&a.to_ascii_lowercase()), + } + } + out.push_str(&remove_dot_segments(&normalize_percent(&t.path))); + if let Some(q) = &t.query { + out.push('?'); + out.push_str(&normalize_percent(q)); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + /// RFC 3986 §5.4.1 and §5.4.2, every example, against its base. + #[test] + fn the_rfc_examples() { + let base = "http://a/b/c/d;p?q"; + for (r, want) in [ + ("g:h", "g:h"), ("g", "http://a/b/c/g"), ("./g", "http://a/b/c/g"), ("g/", "http://a/b/c/g/"), ("/g", "http://a/g"), + ("//g", "http://g"), ("?y", "http://a/b/c/d;p?y"), ("g?y", "http://a/b/c/g?y"), ("#s", "http://a/b/c/d;p?q"), + ("g#s", "http://a/b/c/g"), ("g?y#s", "http://a/b/c/g?y"), (";x", "http://a/b/c/;x"), ("g;x", "http://a/b/c/g;x"), + ("g;x?y#s", "http://a/b/c/g;x?y"), ("", "http://a/b/c/d;p?q"), (".", "http://a/b/c/"), ("./", "http://a/b/c/"), + ("..", "http://a/b/"), ("../", "http://a/b/"), ("../g", "http://a/b/g"), ("../..", "http://a/"), ("../../", "http://a/"), + ("../../g", "http://a/g"), + ("../../../g", "http://a/g"), ("../../../../g", "http://a/g"), ("/./g", "http://a/g"), ("/../g", "http://a/g"), + ("g.", "http://a/b/c/g."), (".g", "http://a/b/c/.g"), ("g..", "http://a/b/c/g.."), ("..g", "http://a/b/c/..g"), + ("./../g", "http://a/b/g"), ("./g/.", "http://a/b/c/g/"), ("g/./h", "http://a/b/c/g/h"), ("g/../h", "http://a/b/c/h"), + ("g;x=1/./y", "http://a/b/c/g;x=1/y"), ("g;x=1/../y", "http://a/b/c/y"), ("g?y/./x", "http://a/b/c/g?y/./x"), + ("g?y/../x", "http://a/b/c/g?y/../x"), ("g#s/./x", "http://a/b/c/g"), ("g#s/../x", "http://a/b/c/g"), ("http:g", "http:g"), + ] { + assert_eq!(resolve(base, r), want, "{r:?}"); + } + } + + /// Two spellings of one URL are one key; another URL is another. + #[test] + fn spellings_of_one_url() { + let base = "https://Origin.Example/live/wai-720.m3u8"; + let key = resolve(base, "720/1201.0.wai#cid=00"); + assert_eq!(key, "https://origin.example/live/720/1201.0.wai"); + for same in ["./720/1201.0.wai", "../live/720/1201.0.wai", "/live/720/1201.0.wai", "720/./1201.0.wai", "720/x/../1201.0.wai", + "%37%32%30/1201.0.wai", "HTTPS://ORIGIN.EXAMPLE/live/720/1201.0.wai", "//origin.example/live/720/1201.0.wai", + "720/1201%2E0.wai", "720/%2E/1201.0.wai"] { + assert_eq!(resolve(base, same), key, "{same:?}"); + } + for other in ["720/1201.1.wai", "720/1201.0.wai?x", "720/1201%2e0.WAI", "../720/1201.0.wai", "720%2F1201.0.wai"] { + assert_ne!(resolve(base, other), key, "{other:?}"); + } + assert_eq!(resolve(base, "a%2fb%7e"), "https://origin.example/live/a%2Fb~"); + // A base with no scheme: names resolve relative to each other. + assert_eq!(resolve("live/wai-720.m3u8", "./720/a.wai"), resolve("live/wai-720.m3u8", "720/a.wai")); + assert_eq!(resolve("", "./720/a.wai"), "720/a.wai"); + } +} diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index 330852b4b..1eb2b013e 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -311,6 +311,12 @@ pub mod energy_binding; #[cfg(feature = "c2pa_emit")] pub mod c2pa_emit; +// Ed25519 verified strictly (RFC 8032 §5.1.7 with a canonical key of large order, S < L, +// and R of large order compared byte for byte): the one routine every receipt family's +// signature check is to move onto. Needs ed25519-dalek, which `world` and `oer` bring. +#[cfg(any(feature = "world", feature = "oer"))] +pub mod sig; + // RFC 8785 canonical JSON over the subset WAI signs and digests (integers up to // 2^53 − 1, no floats). Needs serde_json alone, so it is ungated: claims, key logs, // energy figures and packaging policies canonicalise through this one function. @@ -474,6 +480,12 @@ pub mod ternary_receipt; #[cfg(feature = "quantum_ops")] pub use wai_quantum::quantum_ops; +// HLS media playlist text: a pure function of the segments and the rendering +// parameters (`render_hls`, and `render_hls_with` for on-demand, live and low-latency +// WAI playlists). No dependency, so the HTTP binding renders playlists with it alone +// and wai.video.manifest seals what it renders. +pub mod hls_render; + // Attested per-session manifest (wai.video.manifest): the incumbent OTT 1:1 // manifest, made deterministic (a pure function of the session *class*, so it // dedups across viewers), energy-accounted, and sealed in a signed receipt that diff --git a/wai/wai-rs/src/sig.rs b/wai/wai-rs/src/sig.rs new file mode 100644 index 000000000..461359334 --- /dev/null +++ b/wai/wai-rs/src/sig.rs @@ -0,0 +1,222 @@ +//! Ed25519 verification as WAI verifies it (RFC 8032 §5.1.7, strictly): one routine, so +//! every receipt family that moves onto it accepts exactly the signatures every other +//! conforming verifier accepts. +//! +//! The rule, in order: +//! +//! 1. The key `A` decodes, and is the one canonical encoding of its point: `y` < p, and +//! `x` = 0 never carries the sign bit. `VerifyingKey::from_bytes` reduces a `y` ≥ p +//! and accepts the sign bit on `x` = 0; both are refused here by re-encoding the point +//! and comparing bytes. +//! 2. `A` is not of small order (its order divides 8). Under such a key the equation can +//! hold for a signature made without any private key: under the identity key, every +//! `R` = [s]B with `S` = s verifies. +//! 3. `S` < L. Checked here as well as inside the library, whose own test shrinks to the +//! top three bits when any crate in a build enables its `legacy_compatibility` +//! feature, and S + L passes that test. +//! 4. `R` decodes and is not of small order, and the cofactorless equation holds with +//! [S]B − [k]A encoded and compared with `R` byte for byte, so a non-canonical `R` +//! never verifies (ed25519-dalek's `verify_strict`). + +use ed25519_dalek::{Signature, VerifyingKey}; + +/// Why a signature is refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SigError { + /// The key does not decode, is not its point's one canonical encoding, or is of small + /// order (steps 1 and 2). + BadKey, + /// `S` ≥ L, `R` does not decode or is of small order, or the equation does not hold + /// with `R` compared byte for byte (steps 3 and 4). + BadSignature, +} + +impl std::fmt::Display for SigError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + SigError::BadKey => "the key is not a canonical point of large order", + SigError::BadSignature => "the signature does not verify strictly", + }) + } +} + +impl std::error::Error for SigError {} + +/// The group order L = 2^252 + 27742317777372353535851937790883648493, little-endian. +const L: [u8; 32] = [ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10, +]; + +/// Whether the little-endian integer `s` is below L. +fn below_l(s: &[u8; 32]) -> bool { + for i in (0..32).rev() { + if s[i] != L[i] { + return s[i] < L[i]; + } + } + false +} + +/// Steps 1 and 2: the key, refused unless it is the canonical encoding of a point of +/// large order. +pub fn strict_key(bytes: &[u8; 32]) -> Result { + let key = VerifyingKey::from_bytes(bytes).map_err(|_| SigError::BadKey)?; + if key.to_edwards().compress().to_bytes() != *bytes || key.is_weak() { + return Err(SigError::BadKey); + } + Ok(key) +} + +/// The whole rule over `msg`: `Ok` only when every step holds. +pub fn verify_strict(pubkey: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> Result<(), SigError> { + let key = strict_key(pubkey)?; + let s: &[u8; 32] = sig[32..].try_into().expect("a signature is 64 bytes"); + if !below_l(s) { + return Err(SigError::BadSignature); + } + key.verify_strict(msg, &Signature::from_bytes(sig)).map_err(|_| SigError::BadSignature) +} + +#[cfg(test)] +mod tests { + use super::*; + use ed25519_dalek::{Signer, SigningKey}; + + /// The base point B, encoded. + const B: [u8; 32] = [ + 0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, + 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, + ]; + + fn le(mut n: [u8; 32], add: u8) -> [u8; 32] { + let mut carry = add as u16; + for b in n.iter_mut() { + let v = *b as u16 + carry; + *b = v as u8; + carry = v >> 8; + } + n + } + + /// p = 2^255 − 19, little-endian. + fn p() -> [u8; 32] { + let mut b = [0xffu8; 32]; + b[0] = 0xed; + b[31] = 0x7f; + b + } + + fn sig(r: [u8; 32], s: [u8; 32]) -> [u8; 64] { + let mut g = [0u8; 64]; + g[..32].copy_from_slice(&r); + g[32..].copy_from_slice(&s); + g + } + + #[test] + fn a_real_signature_verifies() { + let k = SigningKey::from_bytes(&[7u8; 32]); + let g = k.sign(b"m").to_bytes(); + verify_strict(&k.verifying_key().to_bytes(), b"m", &g).unwrap(); + assert_eq!(verify_strict(&k.verifying_key().to_bytes(), b"n", &g), Err(SigError::BadSignature)); + } + + #[test] + fn the_identity_key_is_refused_in_either_spelling() { + let one = le([0; 32], 1); + // (R, S) = (B, 1) satisfies the equation under the identity for any message. + let forged = sig(B, one); + assert_eq!(verify_strict(&one, b"any", &forged), Err(SigError::BadKey)); + // y = p + 1, a second spelling of the identity that from_bytes reduces. + let long = le(p(), 1); + assert!(VerifyingKey::from_bytes(&long).is_ok(), "the library reads the long spelling"); + assert_eq!(verify_strict(&long, b"any", &forged), Err(SigError::BadKey)); + // x = 0 with the sign bit set. + let mut signed = one; + signed[31] |= 0x80; + assert_eq!(verify_strict(&signed, b"any", &forged), Err(SigError::BadKey)); + } + + #[test] + fn a_non_canonical_key_of_large_order_is_refused() { + // A point of large order whose y is below 19 has the second spelling y + p, + // which the library reads as the same point; strictly, only y itself is a key. + let mut seen = 0; + for y in 2u8..19 { + let short = le([0; 32], y); + let Ok(k) = VerifyingKey::from_bytes(&short) else { continue }; + if k.is_weak() { + continue; + } + let long = le(p(), y); + assert!(VerifyingKey::from_bytes(&long).is_ok(), "the library reads y = p + {y}"); + assert_eq!(strict_key(&long).err(), Some(SigError::BadKey), "y = p + {y}"); + assert!(strict_key(&short).is_ok(), "y = {y}"); + seen += 1; + } + assert!(seen > 0, "some y below 19 is on the curve with large order"); + } + + #[test] + fn s_at_or_above_l_is_refused() { + let k = SigningKey::from_bytes(&[9u8; 32]); + let g = k.sign(b"m").to_bytes(); + let s: [u8; 32] = g[32..].try_into().unwrap(); + // S + L, as a 256-bit little-endian sum (it fits: S < L < 2^253). + let mut sum = [0u8; 32]; + let mut carry = 0u16; + for i in 0..32 { + let v = s[i] as u16 + L[i] as u16 + carry; + sum[i] = v as u8; + carry = v >> 8; + } + let r: [u8; 32] = g[..32].try_into().unwrap(); + assert_eq!(verify_strict(&k.verifying_key().to_bytes(), b"m", &sig(r, sum)), Err(SigError::BadSignature)); + assert_eq!(verify_strict(&k.verifying_key().to_bytes(), b"m", &sig(r, L)), Err(SigError::BadSignature)); + assert!(!below_l(&L) && below_l(&s)); + } + + /// `bytes` (little-endian, any length) mod L, by binary long division. + fn mod_l(bytes: &[u8]) -> [u8; 32] { + let mut r = [0u8; 33]; + for i in (0..bytes.len() * 8).rev() { + let mut carry = (bytes[i / 8] >> (i % 8)) & 1; + for b in r.iter_mut() { + let top = *b >> 7; + *b = (*b << 1) | carry; + carry = top; + } + let lx = |j: usize| if j < 32 { L[j] } else { 0 }; + if (0..33).rev().find(|&j| r[j] != lx(j)).is_none_or(|j| r[j] > lx(j)) { + let mut borrow = 0i16; + for j in 0..33 { + let v = r[j] as i16 - lx(j) as i16 - borrow; + r[j] = v.rem_euclid(256) as u8; + borrow = i16::from(v < 0); + } + } + } + r[..32].try_into().unwrap() + } + + #[test] + fn a_small_order_or_non_canonical_r_is_refused() { + use ed25519_dalek::Verifier; + use sha2::{Digest, Sha512}; + // Under the key B (scalar 1), R the identity and S = k satisfy the cofactorless + // equation point for point: [k]B = O + [k]B. A lenient check accepts it. + let msg = b"m"; + let k_of = |r: &[u8; 32]| mod_l(&Sha512::new().chain_update(r).chain_update(B).chain_update(msg).finalize()); + let one = le([0; 32], 1); + let g = sig(one, k_of(&one)); + let key = VerifyingKey::from_bytes(&B).unwrap(); + assert!(key.verify(msg, &Signature::from_bytes(&g)).is_ok(), "the lenient check accepts R of small order"); + assert_eq!(verify_strict(&B, msg, &g), Err(SigError::BadSignature)); + // The identity respelled y = p + 1, with S over those bytes: never byte-equal. + let long = le(p(), 1); + assert_eq!(verify_strict(&B, msg, &sig(long, k_of(&long))), Err(SigError::BadSignature)); + // R = B, S = 1 + k: a signature by the scalar 1, which the rule accepts. + verify_strict(&B, msg, &sig(B, mod_l(&le(k_of(&B), 1)))).unwrap(); + } +} diff --git a/wai/wai-rs/src/wai_video_manifest.rs b/wai/wai-rs/src/wai_video_manifest.rs index e151110c9..71bf92b43 100644 --- a/wai/wai-rs/src/wai_video_manifest.rs +++ b/wai/wai-rs/src/wai_video_manifest.rs @@ -34,7 +34,7 @@ //! theirs: [`GrantRef`] carries the capability, the joule ceiling and the //! `funds_ceiling` — here the ad budget. Reuse, not reinvention. -use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; +use ed25519_dalek::{Signer, SigningKey}; use crate::quantum_ops::GrantRef; use crate::world::receipt::merkle_root; @@ -43,6 +43,8 @@ const DOMAIN_RECEIPT: &[u8] = b"wai:video-manifest-receipt\x01"; const DOMAIN_RECEIPT_ID: &[u8] = b"wai:video-manifest-receipt-id\x01"; const DOMAIN_CLASS: &[u8] = b"wai:video-manifest-class\x01"; const DOMAIN_SEG: &[u8] = b"wai:video-manifest-seg\x01"; +/// The domain of a playlist receipt's render digest (http-delivery §8.6). +const DOMAIN_RENDER: &[u8] = b"wai:video-manifest-render\x01"; /// How an energy figure was acquired. The one definition lives in /// [`crate::energy_class`] (no pixel-decode dependency, so this module stays @@ -52,44 +54,23 @@ const DOMAIN_SEG: &[u8] = b"wai:video-manifest-seg\x01"; pub use crate::energy_class::EnergyProvenance; use crate::energy_class::{figure_and_class_cohere, write_optional}; -/// Whether a segment carries programme content or a stitched ad. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum SegmentKind { - Content, - Ad, -} - -impl SegmentKind { - fn tag(self) -> u8 { - match self { - SegmentKind::Content => 1, - SegmentKind::Ad => 2, - } - } -} - -/// One entry in the ordered manifest composition. -/// -/// `ad_id.is_some()` iff `kind == Ad`; [`ManifestReceipt::verify`] rejects any -/// other combination, so the monetisation audit (the ad-bearing segments) is a -/// single source of truth rather than a second list that could disagree. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Segment { - pub index: u64, - pub kind: SegmentKind, - /// The segment URI as it appears in the manifest (content or ad). - pub uri: String, - pub duration_ms: u64, - /// Present exactly for ad segments — the ad decision this break resolved to. - pub ad_id: Option, -} +// The segments and their rendering live in `crate::hls_render`, which needs no +// dependency, so the HTTP binding renders WAI playlists with it alone; they are +// re-exported here under their original paths. +pub use crate::hls_render::{ + check_render, render_hls, render_hls_with, HlsRender, LayerLine, PartLine, RenderError, Segment, SegmentExtra, + SegmentKind, +}; impl Segment { fn leaf(&self) -> [u8; 32] { let mut h = blake3::Hasher::new(); h.update(DOMAIN_SEG); h.update(&self.index.to_be_bytes()); - h.update(&[self.kind.tag()]); + h.update(&[match self.kind { + SegmentKind::Content => 1, + SegmentKind::Ad => 2, + }]); h.update(&self.duration_ms.to_be_bytes()); h.update(&(self.uri.len() as u32).to_be_bytes()); h.update(self.uri.as_bytes()); @@ -152,35 +133,6 @@ impl SessionClass { } } -/// Deterministic HLS rendering: the manifest is a pure function of the ordered -/// segments, so `hash(render(segments))` is a stable identity a verifier can -/// recompute. Ad segments carry an `EXT-X-DISCONTINUITY` — the real SSAI splice -/// marker — so the stitched breaks are visible in the bytes, not only the -/// receipt. Target duration is derived (ceil of the longest segment) rather than -/// supplied, so nothing outside `segments` can move the output. -pub fn render_hls(segments: &[Segment]) -> String { - let target = segments - .iter() - .map(|s| s.duration_ms.div_ceil(1000)) - .max() - .unwrap_or(0); - let mut out = String::new(); - out.push_str("#EXTM3U\n#EXT-X-VERSION:6\n"); - out.push_str(&format!("#EXT-X-TARGETDURATION:{target}\n")); - out.push_str("#EXT-X-MEDIA-SEQUENCE:0\n"); - for s in segments { - if s.kind == SegmentKind::Ad { - out.push_str("#EXT-X-DISCONTINUITY\n"); - } - // Fixed 3-dp seconds so the byte output is stable across platforms. - out.push_str(&format!("#EXTINF:{}.{:03},\n", s.duration_ms / 1000, s.duration_ms % 1000)); - out.push_str(&s.uri); - out.push('\n'); - } - out.push_str("#EXT-X-ENDLIST\n"); - out -} - /// A signed, energy-accounted per-session manifest. #[derive(Clone, Debug, PartialEq)] pub struct ManifestReceipt { @@ -215,10 +167,168 @@ pub struct ManifestReceipt { pub signer_pubkey: [u8; 32], pub signer_id: String, pub sig: [u8; 64], + /// The rendering parameters of the playlist this receipt signs, when it is a WAI + /// playlist (http-delivery §8.6). `None`: the playlist is [`render_hls`]'s, and the + /// receipt's signed bytes are exactly those it had before rendering parameters + /// existed. + pub render: Option, + /// The per-segment lines the rendering adds (http-delivery §8.6): none, one per + /// segment, or one more for the open segment at a live edge. Empty when `render` + /// is `None`. + pub extras: Vec, +} + +/// The largest number a receipt file writes (Appendix H): 2^53 − 1, so every JSON +/// reader reads each exactly (I-JSON, RFC 7493 §2.2). +/// cbindgen:ignore +pub const MAX_FILE_NUMBER: u64 = (1 << 53) - 1; + +/// Whether every number in `v` is an unsigned integer of at most [`MAX_FILE_NUMBER`]. +fn numbers_within(v: &serde_json::Value) -> bool { + use serde_json::Value; + match v { + Value::Number(n) => n.as_u64().is_some_and(|x| x <= MAX_FILE_NUMBER), + Value::Array(a) => a.iter().all(numbers_within), + Value::Object(o) => o.values().all(numbers_within), + _ => true, + } +} + +/// A grant as a receipt file writes it (Appendix H): `grant_hash`, `capability`, +/// `joule_ceiling_micro` (null for no ceiling, which signs as 2^64 − 1) and +/// `funds_ceiling` (null for none). +pub fn grant_json(g: &GrantRef) -> serde_json::Value { + serde_json::json!({ + "grant_hash": hex(&g.grant_hash), "capability": g.capability, + "joule_ceiling_micro": (g.joule_ceiling_micro != u64::MAX).then_some(g.joule_ceiling_micro), "funds_ceiling": g.funds_ceiling, + }) +} + +/// A grant read from [`grant_json`]'s form: exactly its four members, a ceiling a number +/// of at most 2^53 − 1 or null. +pub fn grant_from_json(v: &serde_json::Value) -> Result { + use serde_json::Value; + let g = v.as_object().filter(|g| g.len() == 4).ok_or("grant is an object of its four members")?; + let ceiling = |k: &str| -> Result, String> { + match g.get(k) { + Some(Value::Null) => Ok(None), + Some(n) => n.as_u64().filter(|&x| x <= MAX_FILE_NUMBER).map(Some).ok_or(format!("grant.{k} is a number of at most 2^53 - 1, or null")), + None => Err(format!("grant.{k}")), + } + }; + Ok(GrantRef { + grant_hash: j_hex32(g, "grant_hash")?, + capability: j_str(g, "capability")?, + joule_ceiling_micro: ceiling("joule_ceiling_micro")?.unwrap_or(u64::MAX), + funds_ceiling: ceiling("funds_ceiling")?, + }) +} + +/// The first member name an object of `text` (valid JSON) holds twice, names compared +/// after their escapes are read. +fn duplicate_member(text: &str) -> Option { + // Each open container: for an object, the names read so far and whether the next + // string is a name; for an array, none. + let mut stack: Vec, bool)>> = Vec::new(); + let b = text.as_bytes(); + let mut i = 0; + while i < b.len() { + match b[i] { + b'{' => stack.push(Some((std::collections::BTreeSet::new(), true))), + b'[' => stack.push(None), + b'}' | b']' => { + stack.pop(); + } + b',' => { + if let Some(Some((_, expect))) = stack.last_mut() { + *expect = true; + } + } + b':' => { + if let Some(Some((_, expect))) = stack.last_mut() { + *expect = false; + } + } + b'"' => { + let start = i; + i += 1; + while i < b.len() && b[i] != b'"' { + i += if b[i] == b'\\' { 2 } else { 1 }; + } + if let Some(Some((names, true))) = stack.last_mut() { + let name: String = serde_json::from_str(&text[start..=i.min(b.len() - 1)]).ok()?; + if !names.insert(name.clone()) { + return Some(name); + } + } + } + _ => {} + } + i += 1; + } + None +} + +/// The JSON of a rendering's parameters and extras, as [`ManifestReceipt::render_digest`] +/// canonicalises it and the receipt file carries it. +pub fn render_json(r: &HlsRender) -> serde_json::Value { + serde_json::json!({ + "version": r.version, + "media_sequence": r.media_sequence, + "discontinuity_sequence": r.discontinuity_sequence, + "target_duration_s": r.target_duration_s, + "part_target_ms": r.part_target_ms, + "part_hold_back_ms": r.part_hold_back_ms, + "can_block_reload": r.can_block_reload, + "end_list": r.end_list, + "playlist_type": r.playlist_type.map(|t| t.label()), + "independent_segments": r.independent_segments, + "map_uri": r.map_uri, + }) +} + +fn layers_json(ls: &[LayerLine]) -> serde_json::Value { + ls.iter().map(|l| serde_json::json!({ "n": l.n, "uri": l.uri })).collect() +} + +/// The JSON of one extra (http-delivery §8.6). +pub fn extra_json(e: &SegmentExtra) -> serde_json::Value { + serde_json::json!({ + "discontinuity": e.discontinuity, + "gap": e.gap, + "program_date_time": e.program_date_time, + "parts": e.parts.iter().map(|p| serde_json::json!({ + "duration_ms": p.duration_ms, "independent": p.independent, "uri": p.uri, "layers": layers_json(&p.layers), + })).collect::>(), + "layers": layers_json(&e.layers), + "preload_hint": e.preload_hint, + }) } impl ManifestReceipt { - fn signing_payload(&self) -> Vec { + /// The playlist bytes this receipt signs: [`render_hls_with`] over the segments + /// with `render` (or [`HlsRender::legacy`]) and `extras`. + pub fn rendered(&self) -> String { + render_hls_with(&self.segments, &self.render.clone().unwrap_or_else(HlsRender::legacy), &self.extras) + } + + /// BLAKE3 over the render domain and the canonical JSON (RFC 8785) of + /// `{"render": …, "extras": […]}`: what the signature binds of a rendering besides + /// its bytes, so a receipt has exactly one form. `None` without `render`, or when + /// a number is past the canonical subset. + pub fn render_digest(&self) -> Option<[u8; 32]> { + let r = self.render.as_ref()?; + let v = serde_json::json!({ "render": render_json(r), "extras": self.extras.iter().map(extra_json).collect::>() }); + let c = crate::jcs::jcs(&v).ok()?; + let mut h = blake3::Hasher::new(); + h.update(DOMAIN_RENDER); + h.update(c.as_bytes()); + Some(*h.finalize().as_bytes()) + } + + /// The bytes the signature covers (Appendix H of http-delivery): everything but + /// `sig`. + pub fn signing_payload(&self) -> Vec { let mut o = DOMAIN_RECEIPT.to_vec(); o.extend_from_slice(&self.manifest_hash); o.extend_from_slice(&self.session_class_hash); @@ -254,6 +364,19 @@ impl ManifestReceipt { let id = self.signer_id.as_bytes(); o.extend_from_slice(&(id.len() as u32).to_be_bytes()); o.extend_from_slice(id); + // A WAI playlist's rendering (http-delivery §8.6): nothing without one, so the + // payload of every receipt sealed before rendering parameters existed is + // unchanged; `1` and the render digest with one (`2`, never signed, when the + // rendering has no canonical form). + if self.render.is_some() { + match self.render_digest() { + Some(d) => { + o.push(1); + o.extend_from_slice(&d); + } + None => o.push(2), + } + } o } @@ -273,7 +396,56 @@ impl ManifestReceipt { grant: GrantRef, parent_receipt_hash: Option<[u8; 32]>, ) -> ManifestReceipt { - let manifest_hash = *blake3::hash(render_hls(&segments).as_bytes()).as_bytes(); + Self::seal_inner(signer, signer_id, class, segments, viewers_served, joules_micro, energy_provenance, grant, parent_receipt_hash, None, Vec::new()) + } + + /// [`Self::seal`] for a WAI playlist rendered by [`render_hls_with`] under `render` + /// with `extras` (http-delivery §8.6). Refused (`playlist_malformed`) when the + /// inputs do not read back as themselves ([`check_render`]) or have no canonical + /// form. + #[allow(clippy::too_many_arguments)] + pub fn seal_with( + signer: &SigningKey, + signer_id: impl Into, + class: SessionClass, + segments: Vec, + viewers_served: u64, + joules_micro: u64, + energy_provenance: Option, + grant: GrantRef, + parent_receipt_hash: Option<[u8; 32]>, + render: HlsRender, + extras: Vec, + ) -> Result { + check_render(&segments, &render, &extras)?; + let r = Self::seal_inner(signer, signer_id, class, segments, viewers_served, joules_micro, energy_provenance, grant, parent_receipt_hash, Some(render), extras); + if r.render_digest().is_none() { + return Err(RenderError("a number past the canonical subset (2^53 - 1)".into())); + } + // The file writes every number at most 2^53 − 1 (Appendix H); a joule ceiling of + // 2^64 − 1 (none) is written null. + let v: serde_json::Value = serde_json::from_str(&r.to_json()).map_err(|e| RenderError(e.to_string()))?; + if !numbers_within(&v) { + return Err(RenderError("a number past 2^53 - 1 in the receipt file".into())); + } + Ok(r) + } + + #[allow(clippy::too_many_arguments)] + fn seal_inner( + signer: &SigningKey, + signer_id: impl Into, + class: SessionClass, + segments: Vec, + viewers_served: u64, + joules_micro: u64, + energy_provenance: Option, + grant: GrantRef, + parent_receipt_hash: Option<[u8; 32]>, + render: Option, + extras: Vec, + ) -> ManifestReceipt { + let manifest_hash = *blake3::hash(render_hls_with(&segments, &render.clone().unwrap_or_else(HlsRender::legacy), &extras).as_bytes()).as_bytes(); let session_class_hash = class.class_hash(); let leaves: Vec<[u8; 32]> = segments.iter().map(Segment::leaf).collect(); let mut r = ManifestReceipt { @@ -291,6 +463,8 @@ impl ManifestReceipt { signer_pubkey: signer.verifying_key().to_bytes(), signer_id: signer_id.into(), sig: [0u8; 64], + render, + extras, }; r.sig = signer.sign(&r.signing_payload()).to_bytes(); r @@ -310,7 +484,18 @@ impl ManifestReceipt { if self.class.class_hash() != self.session_class_hash { return false; } - if *blake3::hash(render_hls(&self.segments).as_bytes()).as_bytes() != self.manifest_hash { + // A rendering is a WAI playlist's (http-delivery §8.6): it reads back as itself + // and has a canonical form; extras without one are not a rendering. + match &self.render { + None if !self.extras.is_empty() => return false, + None => {} + Some(r) => { + if check_render(&self.segments, r, &self.extras).is_err() || self.render_digest().is_none() { + return false; + } + } + } + if *blake3::hash(self.rendered().as_bytes()).as_bytes() != self.manifest_hash { return false; } if self.work_compose_ops != self.segments.len() as u64 { @@ -335,8 +520,9 @@ impl ManifestReceipt { if !figure_and_class_cohere(self.joules_micro, self.energy_provenance.as_ref()) { return false; } - let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false }; - k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok() + // Strictly (crate::sig): a canonical key of large order, S < L, and R of large + // order compared byte for byte, as every playlist verifier checks it. + crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok() } /// The independent check: the bytes a player received hash to @@ -348,7 +534,7 @@ impl ManifestReceipt { if *blake3::hash(rendered_manifest).as_bytes() != self.manifest_hash { return false; } - if render_hls(&self.segments).as_bytes() != rendered_manifest { + if self.rendered().as_bytes() != rendered_manifest { return false; } self.verify() @@ -385,6 +571,250 @@ impl ManifestReceipt { h.update(&self.sig); *h.finalize().as_bytes() } + + /// The receipt's file name beside the playlist it signs (http-delivery §8.6): + /// `.json`, served under `mr/`. + pub fn receipt_file_name(&self) -> String { + format!("{}.json", hex(&self.manifest_hash)) + } + + /// The receipt file (http-delivery §8.6): one JSON object, its members in a fixed + /// order, `render` and `extras` only when the receipt has a rendering. + pub fn to_json(&self) -> String { + use serde_json::{json, Map, Value}; + let mut o = Map::new(); + o.insert("kind".into(), json!(RECEIPT_KIND)); + o.insert("manifest_hash".into(), json!(hex(&self.manifest_hash))); + o.insert("session_class_hash".into(), json!(hex(&self.session_class_hash))); + o.insert("class".into(), json!({ + "content_id": self.class.content_id, "entitlement": self.class.entitlement, "geo": self.class.geo, + "blackouts": self.class.blackouts, "ad_pod_id": self.class.ad_pod_id, "window": self.class.window, + })); + o.insert("segments".into(), Value::Array(self.segments.iter().map(|s| json!({ + "index": s.index, "kind": s.kind.label(), "uri": s.uri, "duration_ms": s.duration_ms, "ad_id": s.ad_id, + })).collect())); + o.insert("merkle_root".into(), json!(hex(&self.merkle_root))); + o.insert("work_compose_ops".into(), json!(self.work_compose_ops)); + o.insert("viewers_served".into(), json!(self.viewers_served)); + o.insert("joules_micro".into(), json!(self.joules_micro)); + if let Some(p) = &self.energy_provenance { + for (k, v) in crate::energy_class::json_fields(p) { + o.insert(k.into(), v); + } + } + o.insert("grant".into(), grant_json(&self.grant)); + o.insert("parent_receipt_hash".into(), json!(self.parent_receipt_hash.map(|h| hex(&h)))); + o.insert("signer_pubkey".into(), json!(hex(&self.signer_pubkey))); + o.insert("signer_id".into(), json!(self.signer_id)); + o.insert("sig".into(), json!(hex(&self.sig))); + if let Some(r) = &self.render { + o.insert("render".into(), render_json(r)); + o.insert("extras".into(), Value::Array(self.extras.iter().map(extra_json).collect())); + } + let mut out = Value::Object(o).to_string(); + out.push('\n'); + out + } + + /// Read a receipt file ([`Self::to_json`]). Strict: every member present with its + /// type, no member this form does not name, hashes as lowercase hexadecimal, and + /// `render` and `extras` together or not at all. It does not verify: call + /// [`Self::verify`] or [`Self::verify_render`]. + pub fn from_json(text: &str) -> Result { + use serde_json::Value; + // RFC 8259 text; a `\u` escape that leaves a surrogate unpaired is refused by the + // parser (it denotes no Unicode text). + let v: Value = serde_json::from_str(text).map_err(|e| e.to_string())?; + // Every number is an unsigned integer of at most 2^53 − 1 (Appendix H). + if !numbers_within(&v) { + return Err("a number that is not an unsigned integer of at most 2^53 - 1".into()); + } + // A member name twice in one object is refused, at any depth and however the + // names are escaped: a reader that keeps the first and one that keeps the last + // would read two receipts from one file (I-JSON, RFC 7493 §2.3). + if let Some(name) = duplicate_member(text) { + return Err(format!("the member {name:?} twice in one object")); + } + let o = v.as_object().ok_or("a receipt is a JSON object")?; + const KNOWN: &[&str] = &[ + "kind", "manifest_hash", "session_class_hash", "class", "segments", "merkle_root", "work_compose_ops", + "viewers_served", "joules_micro", "grant", "parent_receipt_hash", "signer_pubkey", "signer_id", "sig", + "render", "extras", crate::energy_class::JSON_CLASS, crate::energy_class::JSON_UNCERTAINTY, + crate::energy_class::JSON_FILTERING, crate::energy_class::JSON_CALIBRATION_REF, + ]; + if let Some(k) = o.keys().find(|k| !KNOWN.contains(&k.as_str())) { + return Err(format!("an unknown member {k:?}")); + } + if o.get("kind").and_then(Value::as_str) != Some(RECEIPT_KIND) { + return Err(format!("kind is not {RECEIPT_KIND:?}")); + } + let class = o.get("class").and_then(Value::as_object).ok_or("class")?; + if class.len() != 6 { + return Err("class has other members than its six".into()); + } + let class = SessionClass { + content_id: j_str(class, "content_id")?, + entitlement: j_str(class, "entitlement")?, + geo: j_str(class, "geo")?, + blackouts: class.get("blackouts").and_then(Value::as_array).ok_or("class.blackouts")? + .iter().map(|b| b.as_str().map(String::from).ok_or("class.blackouts")).collect::>()?, + ad_pod_id: j_str(class, "ad_pod_id")?, + window: j_u64(class, "window")?, + }; + let mut segments = Vec::new(); + for s in o.get("segments").and_then(Value::as_array).ok_or("segments")? { + let s = s.as_object().ok_or("a segment is an object")?; + if s.len() != 5 { + return Err("a segment has other members than its five".into()); + } + let kind = match s.get("kind").and_then(Value::as_str) { + Some("content") => SegmentKind::Content, + Some("ad") => SegmentKind::Ad, + _ => return Err("a segment's kind is content or ad".into()), + }; + let ad_id = match s.get("ad_id") { + Some(Value::Null) => None, + Some(Value::String(a)) => Some(a.clone()), + _ => return Err("a segment's ad_id is a string or null".into()), + }; + segments.push(Segment { index: j_u64(s, "index")?, kind, uri: j_str(s, "uri")?, duration_ms: j_u64(s, "duration_ms")?, ad_id }); + } + let grant = grant_from_json(o.get("grant").ok_or("grant")?)?; + let parent_receipt_hash = match o.get("parent_receipt_hash") { + Some(Value::Null) => None, + Some(_) => Some(j_hex32(o, "parent_receipt_hash")?), + None => return Err("parent_receipt_hash".into()), + }; + let sig_hex = j_str(o, "sig")?; + let sig: [u8; 64] = unhex(&sig_hex).and_then(|b| b.try_into().ok()).ok_or("sig is 128 lowercase hexadecimal digits")?; + let energy_provenance = crate::energy_class::label_from_json(o).map_err(|e| format!("energy class: {e}"))?; + let (render, extras) = match (o.get("render"), o.get("extras")) { + (None, None) => (None, Vec::new()), + (Some(r), Some(Value::Array(es))) => (Some(render_from_json(r)?), es.iter().map(extra_from_json).collect::>()?), + _ => return Err("render and extras come together, extras an array".into()), + }; + Ok(ManifestReceipt { + manifest_hash: j_hex32(o, "manifest_hash")?, + session_class_hash: j_hex32(o, "session_class_hash")?, + class, + segments, + merkle_root: j_hex32(o, "merkle_root")?, + work_compose_ops: j_u64(o, "work_compose_ops")?, + viewers_served: j_u64(o, "viewers_served")?, + joules_micro: j_u64(o, "joules_micro")?, + energy_provenance, + grant, + parent_receipt_hash, + signer_pubkey: j_hex32(o, "signer_pubkey")?, + signer_id: j_str(o, "signer_id")?, + sig, + render, + extras, + }) + } +} + +/// The `kind` of a receipt file (http-delivery §8.6): the capability it is a record of. +pub const RECEIPT_KIND: &str = "wai.video.manifest"; + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +fn unhex(s: &str) -> Option> { + if s.len() % 2 != 0 || !s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) { + return None; + } + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect() +} + +type JObj = serde_json::Map; + +fn j_str(o: &JObj, k: &str) -> Result { + o.get(k).and_then(serde_json::Value::as_str).map(String::from).ok_or(format!("{k} is a string")) +} + +fn j_u64(o: &JObj, k: &str) -> Result { + o.get(k).and_then(serde_json::Value::as_u64).ok_or(format!("{k} is an unsigned integer")) +} + +fn j_bool(o: &JObj, k: &str) -> Result { + o.get(k).and_then(serde_json::Value::as_bool).ok_or(format!("{k} is true or false")) +} + +fn j_opt_u64(o: &JObj, k: &str) -> Result, String> { + match o.get(k) { + Some(serde_json::Value::Null) => Ok(None), + Some(v) => v.as_u64().map(Some).ok_or(format!("{k} is an unsigned integer or null")), + None => Err(format!("{k} is missing")), + } +} + +fn j_opt_str(o: &JObj, k: &str) -> Result, String> { + match o.get(k) { + Some(serde_json::Value::Null) => Ok(None), + Some(serde_json::Value::String(s)) => Ok(Some(s.clone())), + _ => Err(format!("{k} is a string or null")), + } +} + +fn j_hex32(o: &JObj, k: &str) -> Result<[u8; 32], String> { + unhex(&j_str(o, k)?).and_then(|b| b.try_into().ok()).ok_or(format!("{k} is 64 lowercase hexadecimal digits")) +} + +fn j_members(v: &serde_json::Value, n: usize, what: &str) -> Result { + match v.as_object() { + Some(o) if o.len() == n => Ok(o.clone()), + _ => Err(format!("{what} is an object of exactly {n} members")), + } +} + +/// The rendering parameters a receipt file carries ([`render_json`]). +pub fn render_from_json(v: &serde_json::Value) -> Result { + let o = j_members(v, 11, "render")?; + let playlist_type = match o.get("playlist_type") { + Some(serde_json::Value::Null) => None, + Some(serde_json::Value::String(t)) if t == "VOD" => Some(crate::hls_render::PlaylistType::Vod), + Some(serde_json::Value::String(t)) if t == "EVENT" => Some(crate::hls_render::PlaylistType::Event), + _ => return Err("render.playlist_type is VOD, EVENT or null".into()), + }; + Ok(HlsRender { + version: u8::try_from(j_u64(&o, "version")?).map_err(|_| "render.version is at most 255")?, + media_sequence: j_u64(&o, "media_sequence")?, + discontinuity_sequence: j_u64(&o, "discontinuity_sequence")?, + target_duration_s: j_opt_u64(&o, "target_duration_s")?, + part_target_ms: j_opt_u64(&o, "part_target_ms")?, + part_hold_back_ms: j_opt_u64(&o, "part_hold_back_ms")?, + can_block_reload: j_bool(&o, "can_block_reload")?, + end_list: j_bool(&o, "end_list")?, + playlist_type, + independent_segments: j_bool(&o, "independent_segments")?, + map_uri: j_opt_str(&o, "map_uri")?, + }) +} + +fn layers_from_json(v: Option<&serde_json::Value>) -> Result, String> { + v.and_then(serde_json::Value::as_array).ok_or("layers is an array")?.iter().map(|l| { + let o = j_members(l, 2, "a layer")?; + Ok(LayerLine { n: u8::try_from(j_u64(&o, "n")?).map_err(|_| "a layer's n is at most 255")?, uri: j_str(&o, "uri")? }) + }).collect() +} + +/// One extra a receipt file carries ([`extra_json`]). +pub fn extra_from_json(v: &serde_json::Value) -> Result { + let o = j_members(v, 6, "an extra")?; + let parts = o.get("parts").and_then(serde_json::Value::as_array).ok_or("parts is an array")?.iter().map(|p| { + let p = j_members(p, 4, "a part")?; + Ok(PartLine { duration_ms: j_u64(&p, "duration_ms")?, independent: j_bool(&p, "independent")?, uri: j_str(&p, "uri")?, layers: layers_from_json(p.get("layers"))? }) + }).collect::>()?; + Ok(SegmentExtra { + discontinuity: j_bool(&o, "discontinuity")?, + gap: j_bool(&o, "gap")?, + program_date_time: j_opt_str(&o, "program_date_time")?, + parts, + layers: layers_from_json(o.get("layers"))?, + preload_hint: j_opt_str(&o, "preload_hint")?, + }) } #[cfg(test)] @@ -586,4 +1016,261 @@ mod tests { .collect(); assert_eq!(got, want); } + + /// The original rendering is unchanged by the retrofit: `render_hls` gives the bytes + /// it gave before rendering parameters existed (captured from the tree before them), + /// and `render_hls_with` under the legacy parameters with no extras gives the same. + #[test] + fn render_hls_legacy_byte_identical() { + let before = "#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-TARGETDURATION:6\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:6.000,\nseg0.ts\n#EXT-X-DISCONTINUITY\n#EXTINF:4.000,\nad/xyz/0.ts\n#EXTINF:6.000,\nseg1.ts\n#EXT-X-ENDLIST\n"; + assert_eq!(render_hls(&segments()), before); + assert_eq!(render_hls_with(&segments(), &HlsRender::legacy(), &[]), before); + assert_eq!(render_hls(&[]), "#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-TARGETDURATION:0\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-ENDLIST\n"); + } + + fn unhex_n(s: &str) -> [u8; N] { + unhex(s).unwrap().try_into().unwrap() + } + + /// A receipt sealed by the tree before rendering parameters existed: its fields and + /// signature, captured then. It still verifies, against the bytes it signed, and has + /// the receipt hash it had. + #[test] + fn old_receipt_verifies() { + let r = ManifestReceipt { + manifest_hash: unhex_n("5da852a2dce832a13bee19f57d82ad29fc2c75e0c5d26c84435c1820b69972ca"), + session_class_hash: unhex_n("82baa70a290bef84777cbe22eadf2d74d93070c0bb5d01331bcab7d9d09fde6c"), + class: class(), + segments: segments(), + merkle_root: unhex_n("4ded33ff67ad7bdd0f6b3d61336909adefeb3e682c2bf371429705bf4c703a7e"), + work_compose_ops: 3, + viewers_served: 12_000, + joules_micro: 800, + energy_provenance: Some(EnergyProvenance::ModelBased), + grant: GrantRef { grant_hash: [9u8; 32], capability: "video.manifest".into(), joule_ceiling_micro: 5_000_000, funds_ceiling: Some(250) }, + parent_receipt_hash: Some([3u8; 32]), + signer_pubkey: unhex_n("ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c"), + signer_id: "did:key:origin".into(), + sig: unhex_n("1706dc6f3593069eff2d2241f0d46b6be3609b875ebe491076c39eb351215a7354f05dbd864025a707f5a21e860c592c6b43c2dfd1d86aaf256ff2c56059090b"), + render: None, + extras: Vec::new(), + }; + let signed = "#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-TARGETDURATION:6\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:6.000,\nseg0.ts\n#EXT-X-DISCONTINUITY\n#EXTINF:4.000,\nad/xyz/0.ts\n#EXTINF:6.000,\nseg1.ts\n#EXT-X-ENDLIST\n"; + assert!(r.verify()); + assert!(r.verify_render(signed.as_bytes())); + assert_eq!(hex(&r.receipt_hash()), "f94942be7cbad0aaae98cfb38571ed56219ea3c755ee3fb9872184cc9afaa0bb"); + // Its file round-trips and still verifies; the file carries no rendering. + let j = r.to_json(); + assert!(!j.contains("\"render\"")); + let back = ManifestReceipt::from_json(&j).unwrap(); + assert_eq!(back, r); + assert!(back.verify_render(signed.as_bytes())); + // Extras without a rendering are not a receipt this form has. + let mut t = r.clone(); + t.extras = vec![SegmentExtra::default(); 3]; + assert!(!t.verify()); + } + + fn public_class(window: u64) -> SessionClass { + SessionClass { content_id: "live/720".into(), entitlement: "public".into(), geo: String::new(), blackouts: Vec::new(), ad_pod_id: String::new(), window } + } + + fn part(seq: u64, i: u64) -> PartLine { + PartLine { duration_ms: 320, independent: true, uri: format!("live/720/{seq}.{i}.wai#cid={}&n=400", hex(&[i as u8; 32])), layers: Vec::new() } + } + + fn live_render(media_sequence: u64) -> HlsRender { + HlsRender { + version: 9, media_sequence, discontinuity_sequence: 0, target_duration_s: Some(4), part_target_ms: Some(320), + part_hold_back_ms: Some(960), can_block_reload: true, end_list: false, ..HlsRender::legacy() + } + } + + /// Three revisions of a low-latency playlist, each sealed with the previous one as + /// its parent: each verifies against its own bytes, names its parent, and refuses + /// another revision's bytes. A revision whose part lines are reordered after sealing + /// fails, and so does one whose bytes were rendered from reordered parts. + #[test] + fn live_chain() { + let key = key(11); + let seg = |i: u64| Segment { index: i, kind: SegmentKind::Content, uri: format!("objects/{}.wai#n=3000&h=1009", hex(&[0x40 + i as u8; 32])), duration_ms: 3840, ad_id: None }; + // r0: segment 1200 open with 5 parts; r1: open with 12; r2: 1200 complete, 1201 open with 3. + let open = |seq: u64, k: u64, hint: u64| SegmentExtra { + program_date_time: (seq == 1200).then(|| "2026-10-01T12:00:00.000Z".to_string()), + parts: (0..k).map(|i| part(seq, i)).collect(), + preload_hint: Some(format!("live/720/{seq}.{hint}.wai")), + ..Default::default() + }; + let revisions: Vec<(Vec, Vec)> = vec![ + (vec![], vec![open(1200, 5, 5)]), + (vec![], vec![open(1200, 12, 12)]), + (vec![seg(0)], vec![ + SegmentExtra { program_date_time: Some("2026-10-01T12:00:00.000Z".into()), parts: (0..12).map(|i| part(1200, i)).collect(), ..Default::default() }, + open(1201, 3, 3), + ]), + ]; + let mut sealed: Vec = Vec::new(); + for (segs, extras) in &revisions { + let parent = sealed.last().map(ManifestReceipt::receipt_hash); + let r = ManifestReceipt::seal_with(&key, "did:example:live", public_class(1200), segs.clone(), 1, 0, None, + GrantRef::unbounded("video.manifest"), parent, live_render(1200), extras.clone()).unwrap(); + sealed.push(r); + } + let bytes: Vec = sealed.iter().map(ManifestReceipt::rendered).collect(); + assert!(bytes[0].contains("#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"live/720/1200.5.wai\"\n")); + assert!(bytes[2].contains("#EXT-X-PART-INF:PART-TARGET=0.320\n#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.960\n")); + for (i, r) in sealed.iter().enumerate() { + assert!(r.verify_render(bytes[i].as_bytes()), "revision {i}"); + for (j, b) in bytes.iter().enumerate() { + if j != i { + assert!(!r.verify_render(b.as_bytes()), "revision {i} against {j}'s bytes"); + } + } + let back = ManifestReceipt::from_json(&r.to_json()).unwrap(); + assert_eq!(&back, r); + assert!(back.verify_render(bytes[i].as_bytes())); + } + assert_eq!(sealed[0].parent_receipt_hash, None); + assert_eq!(sealed[1].parent_receipt_hash, Some(sealed[0].receipt_hash())); + assert_eq!(sealed[2].parent_receipt_hash, Some(sealed[1].receipt_hash())); + // Parts reordered after sealing: the receipt no longer verifies. + let mut t = sealed[2].clone(); + t.extras[0].parts.swap(3, 4); + assert!(!t.verify()); + assert!(!t.verify_render(bytes[2].as_bytes())); + // The bytes of a reordering: not the bytes the receipt signed. + let mut parts = revisions[2].1.clone(); + parts[0].parts.swap(3, 4); + let reordered = render_hls_with(&revisions[2].0, &live_render(1200), &parts); + assert!(!sealed[2].verify_render(reordered.as_bytes())); + } + + /// The rendering is inside the signature twice over: through `manifest_hash`, and + /// through the render digest, so a rendering that renders the same bytes another way + /// (no extras against empty ones; no parameters against the legacy ones) is another + /// receipt that does not verify. + #[test] + fn render_and_extras_are_signed() { + let k = key(12); + let r = ManifestReceipt::seal_with(&k, "o", public_class(0), segments(), 1, 0, None, GrantRef::unbounded("video.manifest"), None, + HlsRender { version: 7, ..HlsRender::legacy() }, Vec::new()).unwrap(); + assert!(r.verify()); + let mut t = r.clone(); + t.render = Some(HlsRender { end_list: false, ..t.render.clone().unwrap() }); + assert!(!t.verify()); + let mut t = r.clone(); + t.extras = vec![SegmentExtra::default(); 3]; + assert_eq!(t.rendered(), r.rendered(), "the same bytes"); + assert!(!t.verify(), "another form of the same bytes"); + // The legacy parameters as an explicit rendering: the same bytes as no rendering, + // and a different receipt. + let a = ManifestReceipt::seal_with(&k, "o", public_class(0), segments(), 1, 0, None, GrantRef::unbounded("video.manifest"), None, HlsRender::legacy(), Vec::new()).unwrap(); + let b = ManifestReceipt::seal(&k, "o", public_class(0), segments(), 1, 0, None, GrantRef::unbounded("video.manifest"), None); + assert_eq!(a.manifest_hash, b.manifest_hash); + assert_ne!(a.receipt_hash(), b.receipt_hash()); + let mut c = a.clone(); + c.render = None; + assert!(!c.verify()); + assert!(a.verify() && b.verify()); + } + + /// A rendering that would not read back as its inputs is refused when sealing and + /// when verifying: a URI that breaks a line, a part with no part target, a hint + /// before the end, extras of the wrong count. + #[test] + fn malformed_renderings_are_refused() { + let k = key(13); + let seal = |segs: Vec, r: HlsRender, e: Vec| ManifestReceipt::seal_with(&k, "o", public_class(0), segs, 1, 0, None, GrantRef::unbounded("video.manifest"), None, r, e); + let mut bad = segments(); + bad[0].uri = "seg0.ts\n#EXT-X-ENDLIST".into(); + assert_eq!(seal(bad, HlsRender::legacy(), vec![]).unwrap_err().code(), "playlist_malformed"); + let parts = SegmentExtra { parts: vec![part(0, 0)], ..Default::default() }; + assert!(seal(segments(), HlsRender::legacy(), vec![parts.clone(), Default::default(), Default::default()]).is_err()); + let hint_early = SegmentExtra { preload_hint: Some("x.wai".into()), ..Default::default() }; + assert!(seal(segments(), HlsRender { end_list: false, ..HlsRender::legacy() }, vec![hint_early, Default::default(), Default::default()]).is_err()); + assert!(seal(segments(), HlsRender::legacy(), vec![Default::default(); 2]).is_err()); + let quoted = SegmentExtra { layers: vec![LayerLine { n: 1, uri: "a\".wai".into() }], ..Default::default() }; + assert!(seal(segments(), HlsRender::legacy(), vec![quoted, Default::default(), Default::default()]).is_err()); + let order = SegmentExtra { layers: vec![LayerLine { n: 2, uri: "a.wai".into() }, LayerLine { n: 1, uri: "b.wai".into() }], ..Default::default() }; + assert!(seal(segments(), HlsRender::legacy(), vec![order, Default::default(), Default::default()]).is_err()); + let past = HlsRender { media_sequence: 1 << 53, ..HlsRender::legacy() }; + assert!(seal(segments(), past, vec![]).is_err(), "no canonical form"); + let hold = HlsRender { part_target_ms: Some(320), part_hold_back_ms: Some(639), ..live_render(0) }; + assert!(seal(vec![], hold, vec![]).is_err()); + // A gap offers no object: no parts and no layer lines on it; a bare gap renders. + let live = HlsRender { part_target_ms: Some(320), part_hold_back_ms: Some(960), ..HlsRender::legacy() }; + let gap = SegmentExtra { gap: true, ..Default::default() }; + seal(segments(), live.clone(), vec![gap.clone(), Default::default(), Default::default()]).unwrap(); + let gap_parts = SegmentExtra { parts: vec![part(0, 0)], ..gap.clone() }; + assert_eq!(seal(segments(), live.clone(), vec![gap_parts, Default::default(), Default::default()]).unwrap_err().code(), "playlist_malformed"); + let gap_layers = SegmentExtra { layers: vec![LayerLine { n: 1, uri: "l.wai".into() }], ..gap }; + assert_eq!(seal(segments(), live, vec![gap_layers, Default::default(), Default::default()]).unwrap_err().code(), "playlist_malformed"); + } + + /// A grant's ceilings in the file: none is written null (and signs as 2^64 − 1), a + /// ceiling a number of at most 2^53 − 1; a receipt that cannot be written so is not + /// sealed, and a file with a number past it is refused. + #[test] + fn grant_ceilings_in_the_file() { + let none = GrantRef::unbounded("video.manifest"); + assert_eq!(grant_json(&none)["joule_ceiling_micro"], serde_json::Value::Null); + assert_eq!(grant_from_json(&grant_json(&none)).unwrap(), none); + let held = GrantRef { grant_hash: [3u8; 32], capability: "video.manifest".into(), joule_ceiling_micro: MAX_FILE_NUMBER, funds_ceiling: Some(7) }; + assert_eq!(grant_from_json(&grant_json(&held)).unwrap(), held); + let mut big = grant_json(&held); + big["joule_ceiling_micro"] = serde_json::json!(MAX_FILE_NUMBER + 1); + assert!(grant_from_json(&big).is_err()); + big["joule_ceiling_micro"] = serde_json::json!(u64::MAX); + assert!(grant_from_json(&big).is_err(), "no ceiling is null, not 2^64 - 1 written out"); + let seal = |g: GrantRef| ManifestReceipt::seal_with(&key(15), "o", public_class(0), segments(), 1, 0, None, g, None, HlsRender::legacy(), vec![]); + let r = seal(none).unwrap(); + assert!(r.to_json().contains("\"joule_ceiling_micro\":null")); + assert_eq!(ManifestReceipt::from_json(&r.to_json()).unwrap(), r); + seal(held.clone()).unwrap(); + assert_eq!(seal(GrantRef { joule_ceiling_micro: MAX_FILE_NUMBER + 1, ..held.clone() }).unwrap_err().code(), "playlist_malformed"); + assert_eq!(seal(GrantRef { funds_ceiling: Some(u64::MAX), ..held }).unwrap_err().code(), "playlist_malformed"); + assert!(ManifestReceipt::from_json(&r.to_json().replace("\"viewers_served\":1", "\"viewers_served\":9007199254740992")).is_err()); + } + + /// A receipt file is read strictly: an unknown member, a missing one, an upper-case + /// hash, `render` without `extras`, are each refused. + #[test] + fn receipt_file_is_strict() { + let r = ManifestReceipt::seal_with(&key(14), "o", public_class(0), segments(), 1, 0, None, GrantRef::unbounded("video.manifest"), None, + HlsRender { version: 7, ..HlsRender::legacy() }, vec![SegmentExtra::default(); 3]).unwrap(); + let j: serde_json::Value = serde_json::from_str(&r.to_json()).unwrap(); + assert_eq!(r.receipt_file_name(), format!("{}.json", hex(&r.manifest_hash))); + let edit = |f: &dyn Fn(&mut serde_json::Map)| { + let mut o = j.as_object().unwrap().clone(); + f(&mut o); + ManifestReceipt::from_json(&serde_json::Value::Object(o).to_string()) + }; + assert!(edit(&|_| {}).is_ok()); + assert!(edit(&|o| { o.insert("note".into(), serde_json::json!(1)); }).is_err()); + assert!(edit(&|o| { o.remove("merkle_root"); }).is_err()); + assert!(edit(&|o| { o.insert("sig".into(), serde_json::json!(hex(&r.sig).to_uppercase())); }).is_err()); + assert!(edit(&|o| { o.remove("extras"); }).is_err()); + assert!(edit(&|o| { o["segments"][0]["kind"] = serde_json::json!("Content"); }).is_err()); + // A member twice, at the top, inside the class and inside a segment, first or last, + // and under an escaped spelling of its name; and a lone surrogate. + let text = r.to_json(); + let twice = |from: &str, to: &str| { + assert!(text.contains(from), "{from}"); + ManifestReceipt::from_json(&text.replacen(from, to, 1)) + }; + assert!(twice("{\"kind\"", "{\"signer_id\":\"x\",\"kind\"").is_err()); + assert!(twice("\"sig\":", "\"signer_id\":\"x\",\"sig\":").is_err()); + assert!(twice("\"geo\":\"\"", "\"geo\":\"\",\"geo\":\"x\"").is_err()); + assert!(twice("\"duration_ms\":", "\"duration_ms\":1,\"duration_ms\":").is_err()); + assert!(twice("{\"kind\"", "{\"\\u0073igner_id\":\"x\",\"kind\"").is_err()); + assert!(twice("\"geo\":\"\"", "\"geo\":\"\\ud800\"").is_err()); + // The same names in different objects are not duplicates. + assert!(ManifestReceipt::from_json(&text).is_ok()); + assert_eq!(duplicate_member(r#"{"a":{"a":1},"b":[{"a":1},{"a":2}],"c":"{\"a\":1,\"a\":2}"}"#), None); + assert_eq!(duplicate_member(r#"{"a":1,"\u0061":2}"#), Some("a".into())); + // A quote escaped inside a name or a value does not end the string. + assert_eq!(duplicate_member(r#"{"a\"b":1,"a\"b":2}"#), Some("a\"b".into())); + assert_eq!(duplicate_member(r#"{"a":"x\",\"a\":1","b":2}"#), None); + assert_eq!(duplicate_member(r#"{"a":"\\","a":2}"#), Some("a".into())); + } } diff --git a/wai/wai-rs/tests/cli_http.rs b/wai/wai-rs/tests/cli_http.rs index ababcbebf..6396b36ea 100644 --- a/wai/wai-rs/tests/cli_http.rs +++ b/wai/wai-rs/tests/cli_http.rs @@ -418,3 +418,205 @@ fn bench_times_the_base_apart_from_its_refinement() { assert!(r["ms_to_complete"]["median"].as_f64().unwrap() < 1000.0); drop(origin); } + +/// A classical playlist for the O9 segments (three of 640 ms), and an objects list over +/// them, written in a scratch directory with the corpus objects beside them. +fn o9_presentation(name: &str) -> PathBuf { + let d = scratch(name); + std::fs::write(d.join("classical.m3u8"), "#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-TARGETDURATION:1\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:0.640,\nc0.m4s\n#EXTINF:0.640,\nc1.m4s\n#EXTINF:0.640,\nc2.m4s\n#EXT-X-ENDLIST\n").unwrap(); + let objs: Vec = ["O9a", "O9b", "O9c"].iter().map(|id| object(id).to_str().unwrap().to_string()).collect(); + let list = serde_json::json!({ + "uri_prefix": "objects/", + "variants": [{ "uri": "wai-180.m3u8", "classical": "classical.m3u8", "resolution": [320, 180], "frame_rate_milli": 25000, + "segments": objs.iter().map(|o| serde_json::json!({ "object": o })).collect::>() }], + }); + std::fs::write(d.join("list.json"), serde_json::to_vec_pretty(&list).unwrap()).unwrap(); + d +} + +/// `wai http hls` writes the WAI media playlist aligned with the classical one, and +/// `wai.m3u8` with the union, the floor and computed bandwidths; `wai http mpd` writes +/// `wai.mpd` on the same timeline. +#[test] +fn hls_and_mpd_write_aligned_playlists() { + let d = o9_presentation("hls"); + let run = wai(&["http", "hls", "--objects", d.join("list.json").to_str().unwrap(), "--out", d.to_str().unwrap()]); + assert!(run.status.success(), "{}", String::from_utf8_lossy(&run.stderr)); + let media = std::fs::read_to_string(d.join("wai-180.m3u8")).unwrap(); + let classical = std::fs::read_to_string(d.join("classical.m3u8")).unwrap(); + wai::http_binding::hls::check_adjacent(&media, &classical).unwrap(); + let cid = hex(&content_hash(&std::fs::read(object("O9b")).unwrap())); + // A WAI1 is listed with its n alone; a WAI2 with its h too (§5.4). + assert!(media.contains(&format!("#EXTINF:0.640,\nobjects/{cid}.wai#n=18004\n")), "{media}"); + assert!(media.contains("wai#n=21034&h=1009\n"), "{media}"); + let master = std::fs::read_to_string(d.join("wai.m3u8")).unwrap(); + assert_eq!(master, "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=262925,AVERAGE-BANDWIDTH=239246,CODECS=\"wai2.video.av1|video.int_hyper\",X-WAI-FLOOR=\"wai2.video.av1\",RESOLUTION=320x180,FRAME-RATE=25.000\nwai-180.m3u8\n"); + let run = wai(&["http", "mpd", "--objects", d.join("list.json").to_str().unwrap(), "--out", d.join("wai.mpd").to_str().unwrap()]); + assert!(run.status.success(), "{}", String::from_utf8_lossy(&run.stderr)); + let mpd = std::fs::read_to_string(d.join("wai.mpd")).unwrap(); + assert!(mpd.contains("") && mpd.contains(""), "{mpd}"); + // An object list that does not cover the classical timeline is refused: three + // objects for two classical segments. + std::fs::write(d.join("classical.m3u8"), classical.replacen("#EXTINF:0.640,\nc2.m4s\n", "", 1)).unwrap(); + let run = wai(&["http", "hls", "--objects", d.join("list.json").to_str().unwrap(), "--out", d.to_str().unwrap()]); + assert!(!run.status.success()); +} + +/// `wai http mpd --classical-mpd` takes the classical MPD's period, timescale, `t` and +/// `d`, leaves a gap's `S` and `SegmentURL` out, and checks the result adjacent to the +/// classical MPD; a list that does not cover the classical timeline is refused. +#[test] +fn mpd_takes_the_classical_mpd_timeline_and_leaves_gaps_out() { + let d = o9_presentation("mpd-classical"); + let classical = "\n\n \n \n \n \n \n \n \n \n\n"; + std::fs::write(d.join("classical.mpd"), classical).unwrap(); + let mut list: serde_json::Value = serde_json::from_slice(&std::fs::read(d.join("list.json")).unwrap()).unwrap(); + list["variants"][0]["segments"][1]["gap"] = serde_json::json!(true); + std::fs::write(d.join("list.json"), serde_json::to_vec(&list).unwrap()).unwrap(); + let args = |out: &str| vec!["http".to_string(), "mpd".into(), "--objects".into(), d.join("list.json").to_str().unwrap().into(), "--out".into(), + d.join(out).to_str().unwrap().into(), "--classical-mpd".into(), d.join("classical.mpd").to_str().unwrap().into()]; + let run = wai(&args("wai.mpd").iter().map(String::as_str).collect::>()); + assert!(run.status.success(), "{}", String::from_utf8_lossy(&run.stderr)); + let mpd = std::fs::read_to_string(d.join("wai.mpd")).unwrap(); + assert!(mpd.contains("") && mpd.contains(""), "{mpd}"); + assert!(mpd.contains("") && mpd.contains("") && !mpd.contains("t=\"57600\""), "{mpd}"); + assert_eq!(mpd.matches(">()); + assert!(!run.status.success()); + assert!(String::from_utf8_lossy(&run.stderr).contains("3 segments, and the classical timeline has 2"), "{}", String::from_utf8_lossy(&run.stderr)); +} + +/// `wai http seal-playlist` writes `mr/.json` and +/// `mr/.json`; `check-playlist` reports it signed, refuses a changed +/// playlist, and holds a revision to the head it chains to, walking the revisions +/// between. +#[cfg(feature = "video_manifest")] +#[test] +fn seal_and_check_playlist() { + let d = o9_presentation("seal"); + let run = wai(&["http", "hls", "--objects", d.join("list.json").to_str().unwrap(), "--out", d.to_str().unwrap()]); + assert!(run.status.success(), "{}", String::from_utf8_lossy(&run.stderr)); + let pl = d.join("wai-180.m3u8"); + let key = "07".repeat(32); + let run = wai(&["http", "seal-playlist", pl.to_str().unwrap(), "--key", &key, "--signer-id", "did:example:cli", "--content-id", "o9"]); + assert!(run.status.success(), "{}", String::from_utf8_lossy(&run.stderr)); + let bytes = std::fs::read(&pl).unwrap(); + let name = format!("{}.json", blake3::hash(&bytes).to_hex()); + let receipt = d.join("mr").join(&name); + assert!(String::from_utf8_lossy(&run.stdout).starts_with(receipt.to_str().unwrap())); + let ok = wai(&["http", "check-playlist", pl.to_str().unwrap(), "--receipt", receipt.to_str().unwrap(), "--classical", d.join("classical.m3u8").to_str().unwrap()]); + assert!(ok.status.success(), "{}", String::from_utf8_lossy(&ok.stdout)); + assert!(String::from_utf8_lossy(&ok.stdout).starts_with("signed\t")); + let mut changed = bytes.clone(); + let at = changed.len() - 20; + changed[at] ^= 1; + std::fs::write(d.join("changed.m3u8"), &changed).unwrap(); + let bad = wai(&["http", "check-playlist", d.join("changed.m3u8").to_str().unwrap(), "--receipt", receipt.to_str().unwrap()]); + assert_eq!(bad.status.code(), Some(1)); + assert_eq!(String::from_utf8_lossy(&bad.stdout), "refused\tplaylist_receipt_mismatch\n"); + // With no grant, one of no ceilings, written null; under the publisher's grant, its + // ceilings; a ceiling past 2^53 - 1 is not sealed. + assert!(String::from_utf8_lossy(&std::fs::read(&receipt).unwrap()).contains("\"joule_ceiling_micro\":null")); + let grant = d.join("grant.json"); + std::fs::write(&grant, format!("{{\"grant_hash\":\"{}\",\"capability\":\"video.manifest\",\"joule_ceiling_micro\":5000000,\"funds_ceiling\":null}}", "ab".repeat(32))).unwrap(); + let granted = wai(&["http", "seal-playlist", pl.to_str().unwrap(), "--key", &key, "--grant", grant.to_str().unwrap(), "--out", d.join("granted").to_str().unwrap()]); + assert!(granted.status.success(), "{}", String::from_utf8_lossy(&granted.stderr)); + let file = String::from_utf8(std::fs::read(d.join("granted/mr").join(&name)).unwrap()).unwrap(); + assert!(file.contains("\"joule_ceiling_micro\":5000000") && file.contains(&"ab".repeat(32)), "{file}"); + let ok = wai(&["http", "check-playlist", pl.to_str().unwrap(), "--receipt", d.join("granted/mr").join(&name).to_str().unwrap()]); + assert!(ok.status.success(), "{}", String::from_utf8_lossy(&ok.stdout)); + std::fs::write(&grant, format!("{{\"grant_hash\":\"{}\",\"capability\":\"video.manifest\",\"joule_ceiling_micro\":9007199254740992,\"funds_ceiling\":null}}", "ab".repeat(32))).unwrap(); + let past = wai(&["http", "seal-playlist", pl.to_str().unwrap(), "--key", &key, "--grant", grant.to_str().unwrap(), "--out", d.join("past").to_str().unwrap()]); + assert!(!past.status.success()); + // Each receipt is also written under its receipt hash. + let hash_of = |out: &std::process::Output| String::from_utf8_lossy(&out.stdout).trim_end().rsplit(' ').next().unwrap().to_string(); + let r1_hash = hash_of(&run); + assert_eq!(std::fs::read(d.join("mr").join(format!("{r1_hash}.json"))).unwrap(), std::fs::read(&receipt).unwrap()); + // A second revision sealed with the first as its parent checks against it; the + // first fetched again is the head unchanged. + let run = wai(&["http", "seal-playlist", pl.to_str().unwrap(), "--key", &key, "--parent", receipt.to_str().unwrap(), "--out", d.join("r2").to_str().unwrap()]); + assert!(run.status.success(), "{}", String::from_utf8_lossy(&run.stderr)); + let r2 = d.join("r2/mr").join(&name); + let ok = wai(&["http", "check-playlist", pl.to_str().unwrap(), "--receipt", r2.to_str().unwrap(), "--parent", receipt.to_str().unwrap()]); + assert!(ok.status.success(), "{}", String::from_utf8_lossy(&ok.stdout)); + assert!(String::from_utf8_lossy(&ok.stdout).contains("\tchained (0 parents walked)")); + let same = wai(&["http", "check-playlist", pl.to_str().unwrap(), "--receipt", receipt.to_str().unwrap(), "--parent", receipt.to_str().unwrap()]); + assert!(same.status.success(), "{}", String::from_utf8_lossy(&same.stdout)); + // A third revision, chained to the second, checked against the first: the second is + // fetched by its link from the third's directory; without it the walk stops. + let r2_file = d.join("r2/mr").join(format!("{}.json", hash_of(&run))); + let run = wai(&["http", "seal-playlist", pl.to_str().unwrap(), "--key", &key, "--parent", r2_file.to_str().unwrap(), "--out", d.join("r2").to_str().unwrap()]); + assert!(run.status.success(), "{}", String::from_utf8_lossy(&run.stderr)); + let ok = wai(&["http", "check-playlist", pl.to_str().unwrap(), "--receipt", r2.to_str().unwrap(), "--parent", receipt.to_str().unwrap()]); + assert!(String::from_utf8_lossy(&ok.stdout).contains("\tchained (1 parents walked)"), "{}", String::from_utf8_lossy(&ok.stdout)); + std::fs::remove_file(&r2_file).unwrap(); + let bad = wai(&["http", "check-playlist", pl.to_str().unwrap(), "--receipt", r2.to_str().unwrap(), "--parent", receipt.to_str().unwrap()]); + assert_eq!(String::from_utf8_lossy(&bad.stdout), "refused\tplaylist_receipt_parent_missing\n"); +} + +/// The origin serves a playlist to be revalidated on every use, and its receipt +/// immutable. +#[test] +fn origin_serves_playlists_revalidated() { + let d = o9_presentation("origin-playlists"); + let run = wai(&["http", "hls", "--objects", d.join("list.json").to_str().unwrap(), "--out", d.to_str().unwrap()]); + assert!(run.status.success()); + std::fs::write(d.join("wai.mpd"), "\n").unwrap(); + let o = Origin::start(&d, &[]); + let (h, body) = get(o.port, "/wai-180.m3u8", &[]); + assert!(h.contains("Content-Type: application/vnd.apple.mpegurl\r\n") && h.contains("Cache-Control: no-cache, no-transform\r\n"), "{h}"); + assert_eq!(body, std::fs::read(d.join("wai-180.m3u8")).unwrap()); + let (h, _) = get(o.port, "/wai.mpd", &[]); + assert!(h.contains("Content-Type: application/dash+xml\r\n") && h.contains("Cache-Control: no-cache, no-transform\r\n"), "{h}"); +} + +/// The replayed live edge (§11.4): the playlist as its current revision; a blocking +/// reload for a part answered by a revision that lists it; a hinted part's alias held +/// until a revision lists it, and answered with the object whose content hash is the cid +/// that revision lists; an alias served with another part's bytes, which that cid +/// refuses; and a hint no revision ever fills, not found once the edge ends. Nothing here +/// reads a clock: each answer is checked by what it holds. +#[test] +fn origin_replays_a_live_edge() { + let root = corpus(); + let revs: Vec = (0..4).map(|k| format!("live/wai-r{k}.m3u8")).collect(); + let texts: Vec> = revs.iter().map(|r| std::fs::read(root.join(r)).unwrap()).collect(); + let edge = format!("live/wai-720.m3u8={}", revs.join(",")); + let o = Origin::start(&root, &["--live-edge", &edge, "--live-step-ms", "400", "--live-swap", "720/1201.7.wai=720/1201.8.wai"]); + let (h, body) = get(o.port, "/live/wai-720.m3u8", &[]); + assert!(h.starts_with("HTTP/1.1 200") && h.contains("Cache-Control: no-cache, no-transform\r\n"), "{h}"); + assert!(texts.contains(&body), "the playlist is one of its revisions"); + // A blocking reload more than two segments past the edge, or a part with no segment, + // is refused at once (RFC 8216bis §6.2.5.2), not held. + // (A held reload is answered 200 with a revision; 400 is the refusal.) + for q in ["_HLS_msn=1300", "_HLS_msn=1300&_HLS_part=0", "_HLS_part=1", "_HLS_msn=x", "_HLS_msn=-1"] { + let (h, _) = get(o.port, &format!("/live/wai-720.m3u8?{q}"), &[]); + assert!(h.starts_with("HTTP/1.1 400"), "{q}: {h}"); + } + // Part 2 of segment 1201 is first listed by revision 1. + let (_, body) = get(o.port, "/live/wai-720.m3u8?_HLS_msn=1201&_HLS_part=2", &[]); + let p = wai::http_binding::hls::parse_media_playlist(&String::from_utf8(body.clone()).unwrap()).unwrap(); + assert!(wai::http_binding::hls::listed_cid(&p, "720/1201.2.wai").is_some(), "the blocking reload lists the part"); + assert!(texts[1..].contains(&body)); + // Revision 0 hints 1200.5; revision 1 lists it with its cid. + let r1 = wai::http_binding::hls::parse_media_playlist(&String::from_utf8(texts[1].clone()).unwrap()).unwrap(); + let (h, part) = get(o.port, "/live/720/1200.5.wai", &[]); + assert!(h.starts_with("HTTP/1.1 200"), "{h}"); + assert_eq!(Some(content_hash(&part)), wai::http_binding::hls::listed_cid(&r1, "720/1200.5.wai")); + // Served with another part's bytes: the cid the playlist lists refuses them. + let r2 = wai::http_binding::hls::parse_media_playlist(&String::from_utf8(texts[2].clone()).unwrap()).unwrap(); + let (_, swapped) = get(o.port, "/live/720/1201.7.wai", &[]); + assert_ne!(Some(content_hash(&swapped)), wai::http_binding::hls::listed_cid(&r2, "720/1201.7.wai")); + assert_eq!(Some(content_hash(&swapped)), wai::http_binding::hls::listed_cid(&r2, "720/1201.8.wai")); + // Revision 2 hints 1202.0, which no revision fills: not found when the edge ends, + // and the playlist is then its last revision, which ends. + let (h, _) = get(o.port, "/live/720/1202.0.wai", &[]); + assert!(h.starts_with("HTTP/1.1 404"), "{h}"); + let (_, body) = get(o.port, "/live/wai-720.m3u8", &[]); + assert_eq!(body, texts[3]); + // A byte range of a part, under its alias. + let (h, b) = get(o.port, "/live/720/1200.5.wai", &[("Range", "bytes=0-7")]); + assert!(h.starts_with("HTTP/1.1 206") && b == part[..8], "{h}"); +} diff --git a/wai/wai-rs/tests/dash_read_bounds.rs b/wai/wai-rs/tests/dash_read_bounds.rs new file mode 100644 index 000000000..3e9f53320 --- /dev/null +++ b/wai/wai-rs/tests/dash_read_bounds.rs @@ -0,0 +1,153 @@ +//! Reading an MPD (`extensions/http-delivery.md` Appendix I) holds what the document +//! writes, not what its `@r` says, and reads an element's attributes in linear time. +//! Measured with a global allocator that counts the bytes live and their peak, and the +//! calling thread's CPU time. +//! +//! - A few kilobytes of 32 timelines, each one `S` of 2^20 segments, are read and +//! compared with each other within a footprint bounded by the document's size: the +//! runs are held as written and compared run by run, never expanded. +//! - Comparing them takes CPU time that does not grow with the segments a run lists. +//! - One element of 40000 attributes is read in time linear in its attributes: the +//! names already read are kept in a set, not scanned again for each attribute. +//! +//! Its own test binary with a single test, so that no other test's allocations land in +//! the windows it measures. +#![cfg(feature = "http")] + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::sync::atomic::{AtomicUsize, Ordering::Relaxed}; +use std::time::Duration; + +use wai::http_binding::dash_read::{check_dash_adjacent, read_mpd, read_xml}; + +/// The system allocator, counting the bytes live and their peak. +struct Count; +static CUR: AtomicUsize = AtomicUsize::new(0); +static PEAK: AtomicUsize = AtomicUsize::new(0); + +fn grow(n: usize) { + PEAK.fetch_max(CUR.fetch_add(n, Relaxed) + n, Relaxed); +} + +unsafe impl GlobalAlloc for Count { + unsafe fn alloc(&self, l: Layout) -> *mut u8 { + grow(l.size()); + unsafe { System.alloc(l) } + } + unsafe fn alloc_zeroed(&self, l: Layout) -> *mut u8 { + grow(l.size()); + unsafe { System.alloc_zeroed(l) } + } + unsafe fn realloc(&self, p: *mut u8, l: Layout, new: usize) -> *mut u8 { + if new >= l.size() { + grow(new - l.size()); + } else { + CUR.fetch_sub(l.size() - new, Relaxed); + } + unsafe { System.realloc(p, l, new) } + } + unsafe fn dealloc(&self, p: *mut u8, l: Layout) { + CUR.fetch_sub(l.size(), Relaxed); + unsafe { System.dealloc(p, l) } + } +} + +#[global_allocator] +static ALLOC: Count = Count; + +/// The calling thread's CPU time: on a loaded host wall-clock time grows with the load, +/// the thread's own CPU time only with its work. +#[cfg(unix)] +fn now() -> Duration { + #[repr(C)] + struct Timespec { + tv_sec: i64, + tv_nsec: i64, + } + unsafe extern "C" { + fn clock_gettime(clock: i32, tp: *mut Timespec) -> i32; + } + #[cfg(target_vendor = "apple")] + const CLOCK_THREAD_CPUTIME_ID: i32 = 16; + #[cfg(not(target_vendor = "apple"))] + const CLOCK_THREAD_CPUTIME_ID: i32 = 3; + let mut ts = Timespec { tv_sec: 0, tv_nsec: 0 }; + assert_eq!(unsafe { clock_gettime(CLOCK_THREAD_CPUTIME_ID, &mut ts) }, 0, "clock_gettime"); + Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32) +} + +/// Elsewhere, wall-clock time from a fixed origin. +#[cfg(not(unix))] +fn now() -> Duration { + static ORIGIN: std::sync::OnceLock = std::sync::OnceLock::new(); + ORIGIN.get_or_init(std::time::Instant::now).elapsed() +} + +/// The most bytes live at once beyond the start, while `f` runs. +fn peak(f: impl FnOnce() -> T) -> (T, usize) { + let start = CUR.load(Relaxed); + PEAK.store(start, Relaxed); + let out = f(); + (out, PEAK.load(Relaxed) - start) +} + +/// The CPU time of the best of three runs of `f`. +fn cpu(f: impl Fn()) -> Duration { + (0..3).map(|_| { + let t = now(); + f(); + now() - t + }).min().expect("three runs") +} + +/// `n` Representations, each a timeline of one `S` of 2^20 segments. +fn bomb(n: usize) -> String { + bomb_of(n, 1 << 20) +} + +/// `n` Representations, each a timeline of one `S` of `segments` segments. +fn bomb_of(n: usize, segments: u64) -> String { + let r = segments - 1; + let reps: String = (0..n) + .map(|i| format!("")) + .collect(); + format!("{reps}") +} + +/// One element of `n` attributes. +fn wide(n: usize) -> String { + let attrs: String = (0..n).map(|i| format!(" a{i}=\"\"")).collect(); + format!("") +} + +#[test] +fn an_mpd_is_held_as_written_and_read_in_linear_time() { + // 32 timelines of 2^20 segments in about 5 KB: an expanded reader holds 2^25 + // segments (hundreds of megabytes); this one, a run each. + for n in [1usize, 8, 32] { + let text = bomb(n); + let (periods, held) = peak(|| read_mpd(&text).unwrap()); + let tls = &periods[0].sets[0].timelines; + assert_eq!(tls.len(), n); + assert!(tls.iter().all(|t| t.runs.len() == 1 && t.segment_count() == 1 << 20)); + let bound = 64 * text.len() + (1 << 20); + assert!(held <= bound, "{n} timelines in {} bytes held {held} bytes (bound {bound})", text.len()); + // Compared with themselves, each of 32 WAI timelines against 32 classical ones. + let (adjacent, held) = peak(|| check_dash_adjacent(&text, &text)); + adjacent.unwrap(); + assert!(held <= 2 * bound, "comparing {n} timelines held {held} bytes"); + } + // The comparison is run by run: 32 timelines of 2^20 segments each, compared with + // themselves, take about the CPU time of 32 timelines of 2^10; one that steps a + // segment at a time takes about a thousand times as long. + let (few, many) = (bomb_of(32, 1 << 10), bomb_of(32, 1 << 20)); + let (t_few, t_many) = (cpu(|| check_dash_adjacent(&few, &few).unwrap()), cpu(|| check_dash_adjacent(&many, &many).unwrap())); + assert!(t_many <= t_few * 4 + Duration::from_millis(20), "2^20 segments a timeline took {t_many:?}, 2^10 took {t_few:?}"); + // One element of 40000 attributes against one of 10000: linear, so about four times + // the CPU time; a reader that scans the names read so far for each one takes about + // sixteen times. The margin keeps a loaded host from deciding it. + let (short, long) = (wide(10_000), wide(40_000)); + read_xml(&long).unwrap(); + let (t_short, t_long) = (cpu(|| drop(read_xml(&short))), cpu(|| drop(read_xml(&long)))); + assert!(t_long <= t_short * 8 + Duration::from_millis(20), "40000 attributes took {t_long:?}, 10000 took {t_short:?}"); +}