wai: HTTP playlists, live delivery and rendition switching — one renderer, sealed playlists, strict signatures #175

Merged
dcharlot merged 61 commits from wai/w3-4-playlists-abr into main 2026-10-07 09:13:18 -04:00
328 changed files with 13759 additions and 294 deletions

View file

@ -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

View file

@ -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/**'

View file

@ -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/<manifest_hash>.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/<receipt_hash>.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

View file

@ -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` |

View file

@ -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/<manifest_hash>.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/<manifest_hash>.json` resolved against the playlist URI, in the
form of Appendix H, and with the same bytes at `mr/<receipt_hash>.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 `<manifest_hash>.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/<parent_receipt_hash>.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="<cid>.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:<v>`; `#EXT-X-TARGETDURATION:<t>`, `t` the parameter or,
when it has none, the largest segment duration rounded up to whole seconds;
`#EXT-X-PART-INF:PART-TARGET=<d>` with a part target; `#EXT-X-SERVER-CONTROL:` with
`CAN-BLOCK-RELOAD=YES` and `PART-HOLD-BACK=<d>`, those that apply, in that order,
when either does; `#EXT-X-MEDIA-SEQUENCE:<n>`; `#EXT-X-DISCONTINUITY-SEQUENCE:<n>`
when it is not 0; `#EXT-X-PLAYLIST-TYPE:<VOD or EVENT>` with a type;
`#EXT-X-INDEPENDENT-SEGMENTS` when it applies; `#EXT-X-MAP:URI="<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:<value>`; each part as
`#EXT-X-PART:DURATION=<d>[,INDEPENDENT=YES],URI="<uri>"` followed by its layer lines;
`#EXT-X-GAP` for a gap; `#EXTINF:<d>,`; the segment's layer lines; its URI. A layer
line is `#WAI-LAYER:N=<n>,URI="<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="<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/<manifest_hash>.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 (`<?xml` followed by white space or `?`, up to the
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.

View file

@ -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=<b>,AVERAGE-BANDWIDTH=<a>,CODECS="F",RESOLUTION=FWxFH`,
`floor-<c>.m3u8`.
`../classic/floor-<c>.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-<c>.m3u8`: `#EXTM3U`, `#EXT-X-VERSION:7`,
`#EXT-X-TARGETDURATION:<t>`, `#EXT-X-MEDIA-SEQUENCE:0`, `#EXT-X-PLAYLIST-TYPE:VOD`,
`#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-MAP:URI="../../floor/<c>/init.mp4"`, then per
piece `#EXTINF:<S.mmm>,` and `../../floor/<c>/<sha256>.m4s`, then `#EXT-X-ENDLIST`.
- `hls/<class>/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-<c>.m3u8`.
- `hls/<class>/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/<class>/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="<class>"`,
@ -671,10 +673,14 @@ codecs string *F* and maximum frame size `FW×FH`:
over the objects the class lists.
- `hls/<class>/wai-<c>.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:<S.mmm>,`, its base as
`../../objects/<hash>.wai#n=<byte_len>`, then each refinement the class lists, in
entry order, as `#WAI-LAYER:N=<layer>,URI="../../objects/<hash>.wai#n=<byte_len>"`
([http-delivery](http-delivery.md) §10.4).
class takes it as floor only, `#EXTINF:<S.mmm>,`, each refinement the class lists,
in entry order, as `#WAI-LAYER:N=<layer>,URI="../../objects/<hash>.wai#n=<byte_len>"`,
then its base as `../../objects/<hash>.wai#n=<byte_len>`: 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
`<?xml version="1.0" encoding="UTF-8"?>`, then

View file

@ -4,13 +4,15 @@ The conformance corpus of [http-delivery](../extensions/http-delivery.md) (§14)
served as `<cid>.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/<sequence>.<part>.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/<manifest_hash>.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/<receipt_hash>.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 `<receipt_hash>.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,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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}]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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}]}

View file

@ -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}]}

View file

@ -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}]}

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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":[]}

View file

@ -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":[]}

View file

@ -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":[]}

View file

@ -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":[]}

View file

@ -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":[]}

View file

@ -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}]}

View file

@ -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"}]}

View file

@ -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(())

View file

@ -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<Option<u64>, Stop> {
/// and the like, each parameter within its type (Appendix F).
pub(super) fn policy(v: &Value) -> Result<Box<dyn Policy>, 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 }),

View file

@ -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() {

View file

@ -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) {

View file

@ -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).

View file

@ -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);
}
}

View file

@ -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]) {

View file

@ -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),
}
}

View file

@ -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)) {

View file

@ -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);
}

View file

@ -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),
}
}

View file

@ -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() })
}

View file

@ -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<ManifestReceipt> = 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;

View file

@ -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

View file

@ -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);
}

View file

@ -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.

View file

@ -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`.

View file

@ -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<Option<Timeline>,
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 }))

View file

@ -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<Option<Timeline>,
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 });

View file

@ -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() {

View file

@ -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<Vec<PeriodTimelines>, PlaylistError> {
let mut held = 0usize;
let mut count = |timelines: &Vec<Timeline>| -> 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") {

View file

@ -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)));
}

View file

@ -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<Option<Timeline>,
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);

View file

@ -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<u64> {
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<u64> {

View file

@ -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());

View file

@ -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());

View file

@ -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 {

View file

@ -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);

View file

@ -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;

View file

@ -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,
})
}

View file

@ -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⌋`,

View file

@ -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 {

View file

@ -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)?;
}

View file

@ -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(|| {

View file

@ -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]));
}

View file

@ -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<String, PlaylistError> {
'\t' => out.push_str("&#9;"),
'\n' => out.push_str("&#10;"),
'\r' => out.push_str("&#13;"),
- 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))),
}
}

View file

@ -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<String, Stop> {
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))
}

View file

@ -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();

View file

@ -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();

View file

@ -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))));
}
_ => {

View file

@ -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)))),
_ => {

View file

@ -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(())

View file

@ -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<String> {
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);
}

View file

@ -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")?;

View file

@ -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<ManifestReceipt, ReceiptError> {
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 {

View file

@ -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<GrantRef, String> {
let ceiling = |k: &str| -> Result<Option<u64>, 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}")),
}
};

View file

@ -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),
}
}

View file

@ -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');

View file

@ -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

View file

@ -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(())

View file

@ -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<usize> {
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') {

View file

@ -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<VerifyingKey, SigError> {
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)]

View file

@ -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)]

View file

@ -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<MediaPlaylist, PlaylistError>
}
}
"#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")))
}

View file

@ -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));

View file

@ -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('%');

View file

@ -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();

View file

@ -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() {

View file

@ -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 {

View file

@ -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));

View file

@ -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<Element, PlaylistError> {
Some(parent) => parent.children.push(e),
None => done = Some(e),
}
+ } else if r.rest().starts_with("<![CDATA[") {
+ let end = r.rest().find("]]>").unwrap_or(0);
+ r.at += end + 3;
} else if r.rest().starts_with("<!") || r.rest().starts_with("<?") {
return Err(malformed("a declaration, CDATA section or processing instruction is outside the subset"));
} else {

View file

@ -0,0 +1,15 @@
# must fail: dash_adjacency_char_ref_not_a_char
# a character reference to a non-character 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..fdd384ae 100644
--- a/wai/wai-rs/src/http_binding/dash_read.rs
+++ b/wai/wai-rs/src/http_binding/dash_read.rs
@@ -120,7 +120,7 @@ impl<'a> 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;

File diff suppressed because it is too large Load diff

View file

@ -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"}]}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"kind":"wai.video.manifest","manifest_hash":"6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[],"merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","work_compose_ops":0,"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":"5d213b6e5ce07b2c7391f0d73501c7ed8e91d6b81ab418e6b1eff4169d127f6f943306b5e1dc44844f69505c34156c040686a6e4151b37ddda98a617f2ac7609","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":[]}],"layers":[],"preload_hint":"720/1200.5.wai"}]}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"kind":"wai.video.manifest","manifest_hash":"6d6933140ea58cce58ea7bd667f63c7518c7e6c1d89d81328a419f8d406c75d9","session_class_hash":"2d9add4fe530faab9a90f8195095117234c8d10472bac66237a1c2e771671759","class":{"content_id":"live/720","entitlement":"public","geo":"","blackouts":[],"ad_pod_id":"","window":1200},"segments":[],"merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","work_compose_ops":0,"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":"5d213b6e5ce07b2c7391f0d73501c7ed8e91d6b81ab418e6b1eff4169d127f6f943306b5e1dc44844f69505c34156c040686a6e4151b37ddda98a617f2ac7609","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":[]}],"layers":[],"preload_hint":"720/1200.5.wai"}]}

Some files were not shown because too many files have changed in this diff Show more