open-standards/wai/extensions/channel-lineup.md
Transaction Science 88deef4675
Some checks failed
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 13s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 23s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 21s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 13s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 28s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 29s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m23s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Successful in 57s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 8s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 5s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 6s
WAI operation catalogue / does this change touch it (pull_request) Successful in 6s
WAI conformance / does this change touch it (pull_request) Successful in 6s
WAI energy receipts / does this change touch it (pull_request) Successful in 6s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 6s
wai-fleet service / does this change touch it (pull_request) Successful in 7s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 2m41s
wai-key-proxy / does this change touch it (pull_request) Successful in 6s
wai-ledger service / does this change touch it (pull_request) Successful in 6s
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Successful in 6s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 10m10s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 6s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 39s
wai-quantum docs, course and locales / does this change touch it (pull_request) Successful in 7s
wai-quantum for JavaScript / does this change touch it (pull_request) Successful in 6s
wai-quantum Metal member / does this change touch it (pull_request) Successful in 6s
wai-quantum Metal member / member · Metal (macos) (pull_request) Has been skipped
wai-quantum Metal member / member · Metal absent (linux) (pull_request) Has been skipped
wai-quantum matrix-extension member / does this change touch it (pull_request) Successful in 6s
wai-quantum matrix-extension member / member · matrix extension, emulated (aarch64 Linux) (pull_request) Has been skipped
wai-quantum matrix-extension member / member · matrix extension absent (macos) (pull_request) Has been skipped
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 6s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 5s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 7s
wai-web wasm32 / does this change touch it (pull_request) Successful in 5s
WAI Studio / does this change touch it (pull_request) Successful in 7s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 1m35s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m50s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Successful in 1m18s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 3m25s
wai-key-proxy / proxy · linux (pull_request) Successful in 1m34s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 4m3s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m52s
wai-fleet service / service · linux (pull_request) Successful in 2m31s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 4m13s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 3m59s
wai-ledger service / service · linux (pull_request) Successful in 2m7s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 6m43s
wai-pack / wai-pack · linux (pull_request) Successful in 1m49s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (pull_request) Successful in 13s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 25s
wai-quantum docs, course and locales / the course and the command line's languages · linux (pull_request) Successful in 2m23s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 1m33s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 7m54s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 32s
wai-quantum for JavaScript / javascript · linux (pull_request) Successful in 2m54s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 56s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 7m3s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 9m10s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 4m35s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 2m47s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 24s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 43s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 6m18s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 10m27s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 10m39s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m11s
wai-fleet service / service · macos (pull_request) Successful in 41s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 6m23s
wai-ledger service / service · macos (pull_request) Successful in 1m4s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 16m5s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 18m37s
WAI energy receipts / energy receipts · linux (pull_request) Failing after 18m35s
wai-pack / wai-pack · macos (pull_request) Successful in 3m36s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 11m3s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 16m15s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 11m51s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 4m35s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 13m42s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 8m10s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 10m36s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 19m32s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 2m35s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 9m21s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 6m5s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 24m8s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 27s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m25s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 11m22s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m28s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 10m56s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 7m22s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h2m25s
wai: channel-lineup, spans written in words
The section-reference check pairs a bare [ with the next link, and §5 now links to
revenue-share, so an interval written [a, b) before it read as a link's text. Each
span is written from its start to its end instead; the meaning is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 19:58:39 -04:00

47 KiB
Raw Permalink Blame History

WAI Extension: Channel Lineup and Guide

Status: Draft. Registers the record wai.video.lineup: a linear channel's signed lineup, WLU1, which describes each programme asset of one channel schedule (its title, description, categories, rating, artwork by content hash, where it may be shown, and its rights holder) and where each programme of the guide begins, and which binds that schedule by its root. It defines how a channel as operators exchange it (an XMLTV schedule, Media RSS catalogues, a JSON form of both, and the SCTE-35 date ranges of the channel's HLS Media Playlist) is imported into a wai.video.channel receipt and a lineup; and the channel's guide, an XMLTV document or a now/next document for a player's guide row, rendered from a receipt and its lineup, naming both, so that anyone holding them recomputes the guide and compares. The wai.video.channel receipt keeps its signed bytes and travels in revenue-share's JSON carriage of it. No new transport. The reference implementation is the channel_lineup module of wai-rs (feature channel_lineup); the corpus is live-conformance/channels/, and its second implementation is live-conformance/channel_lineup_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision, each at its source:

  • the XMLTV DTD, xmltv.dtd: its date and time format, and the elements tv, channel, display-name, programme, title, desc, category, icon, episode-num, rating and value, with their attributes and the order the DTD gives a programme's children;
  • Media RSS Specification 1.5.1, December 2009: §4.1 (media:group), §4.2 (media:content), §5 (where its optional elements appear), §5.2 (media:rating), §5.3 (media:title), §5.4 (media:description), §5.6 (media:thumbnails), §5.7 (media:category) and §5.13 (media:restriction); and the RSS 2.0 elements it extends (rss, channel, item, title, description, category, guid);
  • draft-pantos-hls-rfc8216bis-22: §4.2 (attribute lists, decimal-floating-point and hexadecimal-sequence values), §4.4.5.1 (EXT-X-DATERANGE) and §4.4.5.1.1 (SCTE-35 in a date range);
  • ANSI/SCTE 35-1 2023r2: §9.6 (Table 5, splice_info_section()) and §9.7.3 (Table 10, splice_insert());
  • RFC 8259 (JSON), RFC 8785 (the JSON Canonicalization Scheme), and XML 1.0 for the subset of §3.3.

A section number of one of these texts is that revision's.

1. Scope

In scope: the bounds and value forms an import reads (§3); the import of a channel's schedule, catalogue and ad breaks into a schedule, its assets and its cues (§4); the JSON carriage a wai.video.channel receipt travels in, which is revenue-share's (§5); the lineup, its rules, its signature and how it binds its schedule (§6); the guide, in XMLTV and as a now/next document (§7); verification of a receipt, a lineup and a guide together (§8); the operations that do each, with a worked example (§9).

Out of scope: a transport; how feeds travel between operators; playout, which turns a schedule into segments; the ad decision of a break, which live-ads and wai.video.ssai make; the revenue an asset's rights holder is owed; the HLS cue tags that the HLS text does not define, and interstitial date ranges, which stitched-presentation reads; SCTE 224 audience and blackout rules (a lineup states where an asset may be shown, and says nothing about when); and the elements of the XMLTV DTD and of Media RSS that §4 does not name, which an import does not read.

Relationship to the core spec: wai.video.lineup is a record (SPEC §5): a signed record a WAI tool emits and verifies. It names no payload format and is not dispatchable. A guide is not a record: it is a rendering of two records that anyone recomputes, so it carries no signature of its own.

Relationship to wai.video.channel (SPEC §5): an import seals a channel receipt with ChannelReceipt::seal, whose signed bytes are unchanged here, and carries it in the JSON form revenue-share §4.1 defines for it; a lineup binds the receipt's schedule by its schedule_root. A receipt's program-as-run (aired) is what a verification sets beside a guide (§8.3).

Relationship to live-events: each ad break an import reads becomes a cue payload of live-events §4.3, unchanged, which a room's principal signs as a cue event when the channel is delivered as a room. Relationship to cue-injection: its injection record (§7 there) binds a cue to the SCTE 104 message it was translated from; a cue an import yields comes from a playlist's SCTE-35 date range, and an import makes no injection record.

2. Terms

  • Channel: a linear channel, named by a channel id.
  • Schedule: the entries of a wai.video.channel receipt, gapless and non-overlapping, each an asset id, a start, a duration and a kind: program, ad or filler.
  • Programme: a span of the schedule the guide lists as one item: an episode or a film, with the ad breaks inside it. A programme's asset is the asset id of its program entries.
  • Asset: what an asset id names; the lineup describes each programme asset (§6.2).
  • Catalogue: the assets a channel's content owners describe, in Media RSS or the JSON form.
  • Break: a span of a programme that ads fill, from a date range or the JSON form (§4.5).
  • Operator: the party that runs the channel and signs its receipt and its lineup.
  • Rights holder: the party an asset's lineup entry names as holding its rights.
  • Feed: a document an import reads: an XMLTV document, a Media RSS document, the JSON form, or a Media Playlist.
  • Guide: an XMLTV document (§7.2) or a now/next document (§7.3) rendered from a receipt and a lineup.

An instant is a count of milliseconds since 1970-01-01T00:00:00Z, leap seconds not counted. An instant or a duration an import reads, a lineup states or a guide writes is at most 2^53 − 1, the integers JSON carries exactly (RFC 8785 §3.2.2.3). A span from a to b holds the instants from a, included, to b, excluded. Integers of the lineup are little-endian. A digest is SHA-256 (FIPS 180-4), 32 bytes. Ed25519 is RFC 8032's, verified strictly (SPEC §7 Signatures): a key that fails step 1 or 2 is bad_key, and a signature that fails a later step is bad_signature. Hexadecimal written by this text is lowercase.

3. Reading

3.1 Bounds

An import refuses import_oversized a feed or an artwork file of more than 16 777 216 bytes, and a feed that holds more than 10 000 programmes of the channel (XMLTV, the JSON form), more than 10 000 items (Media RSS, all its documents together), more than 10 000 assets (the JSON form) or more than 10 000 breaks (the JSON form and the playlist together). The size of each feed is checked before it is read, and each count as soon as its document has been read as XML, JSON or lines, before any of its elements is read further. An XML document is read to a depth of 64 elements at most (§3.3). An import therefore reads a bounded amount for any input.

3.2 Text values

A text is a sequence of Unicode scalar values in UTF-8 with no character below U+0020 other than U+0009 (tab) and U+000A (line feed). A short text is a text of 1 to 255 bytes; a long text a text of 0 to 65 535 bytes. A value an import reads that must be one of these and is not is refused import_field_invalid. Text is taken as the feed writes it; nothing is normalised.

3.3 The XML subset

An XMLTV or Media RSS document is read over the XML subset of stitched-presentation §3.5, with its local names and its text of an element, and with one addition: before the root element, where that subset admits a comment, the document MAY hold one document type declaration with no internal subset, which is skipped. Such a declaration is <!DOCTYPE, white space, a name (an ASCII letter or _, then ASCII letters, digits, _, -, . and :), and then optionally white space and either SYSTEM, white space and a literal, or PUBLIC, white space, a literal, white space and a literal; then optional white space and >. A literal is ", bytes other than ", and "; or ', bytes other than ', and '. White space is one or more of space, tab, CR and LF. A declaration of any other form (one with an internal subset among them), or a second one, is outside the subset. A document outside the subset, or whose root element is not the one §4.2 or §4.3 names, is refused import_xml_malformed.

An element's first x child is its first child element of local name x. Elements are named by local name only, so a Media RSS element and an RSS 2.0 element of one local name (media:title and title, for one) are both title children; §4.3 says which one an import takes.

3.4 Times and durations

An XMLTV time is 14 ASCII digits YYYYMMDDhhmmss, optionally followed by one space and a numeric zone: + or - and four digits HHMM with HH at most 23 and MM at most 59. With no zone the time is UTC, as the DTD says. The instant is the UTC instant of the date and time, less the zone's offset. A value is refused:

  • import_timezone_unknown when its first 14 bytes are digits and what follows them is a space and anything other than a numeric zone (a zone named by letters, for one);
  • import_time_malformed otherwise when it is not of that form, when its year is below 1970, its month not 1 to 12, its day not a day of that month in the Gregorian calendar, its hour above 23, its minute above 59 or its second above 59, or when its instant is before 1970-01-01T00:00:00Z.

An HLS date-time is YYYY-MM-DDThh:mm:ss, optionally . and one or more digits, then Z or a zone +hh:mm or -hh:mm with hh at most 23 and mm at most 59, the fields as above. Its fraction is read to the millisecond, and every digit after the third MUST be 0. A value of this form without a zone is refused import_timezone_unknown; any other value that is not of this form, or breaks a rule above, is refused import_time_malformed.

A duration is a decimal-floating-point value (draft-pantos-hls-rfc8216bis-22 §4.2): one or more digits, optionally . and one or more digits, a number of seconds. It is read to the millisecond, every fractional digit after the third MUST be 0, and the count of milliseconds MUST be at most 2^53 − 1; otherwise it is refused import_time_malformed.

4. Import

4.1 Inputs

An import is given:

  • channel: the channel id, a short text;
  • feeds, one of these sets, each optionally with one Media Playlist:
    • an XMLTV document, and zero or more Media RSS documents;
    • one or more Media RSS documents, with no XMLTV document;
    • one JSON form (§4.4), and no XMLTV or Media RSS document;
  • for each Media RSS document, optionally its owner, the rights holder of its items: a short text;
  • owner: optionally, the rights holder of every asset that no other input gives one: a short text;
  • id_system: the XMLTV episode-num system that names a programme's asset (§4.2); when not given, wai-asset-id, the system a guide writes (§7.2);
  • xmltv_channel: the XMLTV channel id whose programmes are read; when not given, channel;
  • start: an instant, which a schedule made of Media RSS items alone requires (§4.6);
  • artwork: a set of files, each named by a file name (§4.6);
  • window: optionally, the receipt's window, a span from window_start to window_end (§4.9);
  • the operator's Ed25519 key and the receipt's signer_id (a short text), and optionally the receipt hash of the window before this one, its parent_receipt_hash.

An input set that is none of the sets above, a Media RSS owner given for some of the Media RSS documents and not for the others, a value above that is not of its form, or a schedule of Media RSS items alone with no start, is refused import_inputs_invalid.

4.2 An XMLTV schedule

The document's root element is tv. Its channel name is the text of the first display-name child of the first channel child of tv whose id attribute is xmltv_channel, when that text is not empty; it MUST then be a short text. The import's programmes are the programme children of tv whose channel attribute is xmltv_channel, in document order; when there are none, the import is refused import_empty. Each one is read, in document order, by these steps, and refused at the first that fails:

  1. Its start attribute is an XMLTV time (§3.4); an absent start is import_time_malformed.
  2. It has a stop attribute: otherwise import_no_stop. That attribute is an XMLTV time (§3.4), and its instant is after the start's: otherwise import_time_malformed.
  3. Its asset. It has an episode-num child whose system attribute is id_system (an absent system is onscreen, the DTD's default), and the text of the first such child is a short text: otherwise import_asset_unknown. That text is the programme's asset id.
  4. Its description, each value as §3.2 says (import_field_invalid): its title, the text of its first title child, a long text of 1 byte or more; its description, the text of its first desc child, or empty when it has none, a long text; its categories, the text of each category child, in order, each a short text, at most 16 of them; its artwork, the src attribute of each icon child, in order, each a long text of 1 byte or more, at most 8 of them (an icon with no src is refused); and its rating, when it has a rating child: the first one's system attribute, or empty when it has none, which when not empty is a short text; and the text of that child's first value child, a short text (a rating with no value, or an empty one, is refused).

Every instant an XMLTV document gives is a whole second.

4.3 A Media RSS catalogue

The document's root element is rss, with a channel child; the first channel child is read. Its channel name is the text of that channel's first title child, when not empty; it MUST then be a short text. Its items are that channel's item children, in document order. Each one is read, in document order, by these steps, and refused at the first that fails:

  1. Its asset id is the text of its first guid child (RSS 2.0's guid), a short text: otherwise import_asset_unknown. An asset id that an item read before it has, in this document or in an earlier one, is refused import_asset_duplicate.
  2. Its content is its first content child (media:content, §4.2 there), or, when it has none, the first content child of its first group child (media:group, §4.1 there). Its content URI is the content's url attribute, a long text, empty when absent or when it has no content. Its duration is the content's duration attribute read as a duration (§3.4; import_time_malformed), which MUST be at least 1 millisecond (import_time_malformed); with no such attribute it is unknown.
  3. Its description, each value as §3.2 says (import_field_invalid): its title, the text of its first title child, a long text of 1 byte or more; its description, the text of its first description child, or empty, a long text; its categories, the text of each category child, each a short text, at most 16; its artwork, the url attribute of each thumbnail child (media:thumbnail, §5.6 there), each a long text of 1 byte or more, at most 8 (a thumbnail with no url is refused); its rating, when it has a rating child (media:rating, §5.2 there): the first one's scheme attribute, or urn:simple when absent (§5.2's default), which when not empty is a short text; and its text, a short text.
  4. Where it may be shown, from its first restriction child whose type attribute is country (media:restriction, §5.13 there): its relationship attribute is allow or deny, and its text, split at white space (space, tab, CR, LF) into non-empty tokens, is either the one token all or the one token none, or one or more tokens of two ASCII letters each: otherwise import_field_invalid. Two letters name the ISO 3166-1 alpha-2 code they spell in upper case, and the codes are taken in increasing order, each once. allow with all, deny with none, or no such child: shown everywhere; allow with none, or deny with all: shown nowhere; allow with codes: shown only in those countries; deny with codes: shown everywhere but those.

The item's title is RSS 2.0's title or Media RSS's media:title, whichever the item writes first, and so for description and category. Elements that a media:group or a media:content holds, other than the content itself, are not read.

4.4 The JSON form

The JSON form carries a channel's schedule, its catalogue and its breaks in one document. It is one JSON text (RFC 8259) in UTF-8, nested at most 64 deep, whose objects each name a member once. Its value is an object with exactly these members:

member value
kind the string wai-channel-feed
channel_name a string
programmes an array of objects, each with exactly start_ms and stop_ms (integers) and asset_id (a string)
assets an array of objects, each with exactly the members of the next table
breaks an array of objects, each with exactly id (a string), start_ms and duration_ms (integers) and scte35 (a string, or null)
asset member value
asset_id, title, description, content_uri strings
categories an array of strings
rating null, or an object with exactly system and value, strings
artwork an array of objects, each with exactly uri and sha256, strings
geo null (shown everywhere), or an object with exactly one member, allow (shown only in) or deny (shown everywhere but), an array of strings
rights_holder a string, or null
duration_ms an integer: the asset's duration, 0 when unknown

An integer is written as decimal digits with no sign, fraction, exponent or leading zero, at most 2^53 − 1. A document that is not so is refused import_json_malformed. Then, in document order, each value is read as §3.2 and this paragraph say (import_field_invalid unless said otherwise): channel_name a short text; each programme's asset_id a short text, with stop_ms after start_ms (otherwise import_time_malformed); each asset's asset_id a short text (one that an asset before it has is import_asset_duplicate), its title a long text of 1 byte or more, its description and content_uri long texts, its categories as in §4.2, its rating's system a short text or empty and its value a short text, its artwork at most 8, each uri a long text of 1 byte or more and each sha256 64 lowercase hexadecimal digits, its geo codes each two upper-case ASCII letters in strictly increasing order (a deny with none is refused), and its rights_holder a short text when not null; each break as §4.5 says of a break.

4.5 Breaks from a Media Playlist

A Media Playlist is read as stitched-presentation §3.2 reads one: UTF-8, split at each line feed with a CR before it dropped, its first line #EXTM3U (otherwise import_playlist_malformed). Each line that begins #EXT-X-DATERANGE: is a date range tag, and the rest of the line its attribute list, read by the rules of stitched-presentation §3.2; a tag whose list does not read, or whose ID is not a quoted string, is refused import_playlist_malformed.

Tags are grouped by ID, each group in the order its first tag appears. Two tags of one group that carry one attribute MUST carry the same value, a quoted string or not as both write it (draft-pantos-hls-rfc8216bis-22 §4.4.5.1): otherwise import_playlist_malformed. A group is a break when one of its tags carries SCTE35-OUT; the other groups are not read. A break's attributes are those of its tags together, and it is read by these steps, each refused at the first that fails:

  1. Its id is its ID, a short text (import_field_invalid).
  2. Its START-DATE is a quoted string, an HLS date-time (§3.4): an absent one, or one that is not quoted, is import_playlist_malformed. Its instant is the break's start.
  3. Its SCTE35-OUT (and its SCTE35-IN, when it has one) is an unquoted hexadecimal-sequence (§4.2 there: 0x or 0X, then hexadecimal digits, read here in either case) of an even count of digits, whose bytes are a splice_info_section() as live-events §4.3 reads form 1: table_id 0xFC, and section_length (the low 12 bits of its bytes 1 and 2) plus 3 its length. Otherwise import_scte35_malformed. The bytes of SCTE35-OUT are the break's section.
  4. Its duration is its DURATION, unquoted, when it has one; otherwise its END-DATE, a quoted HLS date-time, less its start, when it has one; otherwise its PLANNED-DURATION, unquoted. A duration attribute that is quoted is import_playlist_malformed, and one that does not read is refused as §3.4 says. A break with none of the three, or whose duration is 0 or above 2^32 − 1 milliseconds, or an END-DATE before its start, is refused import_break_duration.

A break of the JSON form is its id (step 1); its start_ms; its scte35, when not null, an even count of hexadecimal digits (either case, no 0x) read as step 3 reads a section (import_scte35_malformed); and its duration_ms, refused as step 4 refuses one when 0 or above 2^32 − 1. A break of the JSON form with a null scte35 has no section.

4.6 The programmes and their assets

The programmes are, by the feeds:

  • XMLTV: those of §4.2;
  • the JSON form: its programmes, each the span from start_ms to stop_ms, with its asset_id; none is import_empty;
  • Media RSS alone: the items of the documents in the order given, each document's in document order, one programme an item, back to back from start: each programme begins where the one before it ends and lasts its item's duration. An item whose duration is unknown is refused import_duration_unknown, and an instant above 2^53 − 1 import_time_malformed. No item is import_empty.

The programmes are ordered by start (programmes of one start keep their order), and each next to the one after it: a programme that ends before the next begins is refused import_gap, and one that ends after the next begins import_overlap. Every programme's start, and the last one's end, MUST be a whole second, a multiple of 1000 (import_time_malformed): a guide's XMLTV times have no finer unit (§7.2).

Each distinct asset id of the programmes, in the order of the first programme that names it, gets its lineup entry (§6.2) by these steps, refused at the first that fails:

  1. Its description. With the JSON form, its assets entry, which MUST exist (import_asset_unknown). With XMLTV, the item of that asset id when a Media RSS document has one, otherwise the first programme that names it. With Media RSS alone, its item. The description is title, description, categories, artwork, rating, where it may be shown, content URI and duration; a programme gives no content URI, a duration of 0 and shown everywhere.
  2. Its rights holder: the JSON form's rights_holder when not null; otherwise the owner of the Media RSS document its item came from, when given; otherwise the import's owner, when given. None is refused import_owner_unknown.
  3. Its artwork. Each artwork URI of a description that came from XMLTV or Media RSS is bound by its content hash: its name is the part of the URI after its last /, once the first ? or # and everything after it are removed. The name MUST be 1 to 255 bytes of ASCII letters, digits, ., _ and -, other than . and .., and the artwork set MUST hold a file of that name; otherwise import_artwork_missing. A file of more than 16 777 216 bytes is import_oversized. The artwork's sha256 is the SHA-256 of that file's bytes. The JSON form gives its artwork's sha256 itself.

Programmes and assets are read once each: an asset's later programmes give it nothing, and catalogue items no programme names are read (§4.3) but enter no lineup.

4.7 The schedule

The breaks are those of the JSON form, then those of the playlist, ordered by start (breaks of one start keep their order). A break whose end (its start and its duration) is after the next break's start is refused import_overlap; a break that does not lie within one programme, a programme whose start is at most its start and whose end is at least its end, is refused import_break_outside.

The schedule is, for each programme in order: a program entry from the programme's start, or from the end of its previous break, to the start of its next break or to its end, of the programme's asset id, wherever that span is not empty; and an ad entry for each of its breaks, from its start to its start plus its duration, whose asset id is the break's id. The schedule is therefore gapless and non-overlapping, and every entry of a programme lies within it.

4.8 The cues

Each break, in the order of §4.7, yields one cue payload of live-events §4.3, unchanged: type 4 (break: out and back, its duration known), duration_ms the break's duration, and form 1 with the break's section verbatim, or form 0 for a break with no section. Its cue_id is the section's splice_event_id when the section's encrypted_packet bit (the high bit of byte 4) is 0, its splice_command_type (byte 13) is 0x05 (splice_insert(), SCTE 35-1 §9.7.3) and it is 18 bytes or longer: the big-endian integer of bytes 14 to 17 (SCTE 35-1 §9.6, Table 5, and §9.7.3, Table 10). Otherwise the cue_id is the big-endian integer of the first 4 bytes of SHA-256 over wai:channel-cue-id (ASCII), 0x01 and the break's id. A cue says, as its message does, that the break is out of the program and back.

4.9 What an import yields

  • The receipt: ChannelReceipt::seal (SPEC §5, wai.video.channel) by the operator's key, with the import's signer_id, channel_id the channel, the schedule of §4.7, the window (when not given, from the schedule's first start to its last end), joules_micro 0 and no class (the import meters nothing; energy-measurement §2), the grant reference with no ceilings (grant_hash 32 zero bytes, capability video.channel, joule_ceiling_micro 2^64 − 1, no funds_ceiling), and parent_receipt_hash as given. A window that is empty, or that does not lie within the schedule (its start before the schedule's first start, or its end after its last end), is refused import_window_outside.
  • The lineup (§6): the channel id, the channel name (the first of these that gives one: the XMLTV document, the first Media RSS document, the JSON form; when none does, the channel id), the receipt's schedule_root, the schedule's first start and last end, each programme's start, and the assets of §4.6 in the order of their asset ids' bytes, signed by the operator's key.
  • The cues of §4.8, each with its break's id and start.

4.10 The order of refusal

An import is refused for the first step that fails, in this order: §4.1; then each feed, the XMLTV document, the Media RSS documents in the order given, the JSON form and then the playlist, each first by its size (§3.1), then as XML, JSON or lines, then by its counts (§3.1), then by its own steps (§4.2 to §4.5), element by element in document order; then §4.6, §4.7 and §4.9. An import that is refused yields nothing.

5. The channel receipt's JSON carriage

A wai.video.channel receipt keeps its signed bytes, and this text defines no form of its own for it: an import writes a receipt, and a verifier reads one, in the JSON carriage of revenue-share §4.1, the one JSON form of a channel receipt. A file that holds a receipt holds one JSON text (RFC 8259) in UTF-8 of that carriage. A receipt reads when it is such a text and reads as revenue-share §4.1 says; its entries' leaves, its schedule root, its signing payload, its signature and its identity (its receipt hash) are revenue-share §4.2's; its rules are rules 1 to 5 of revenue-share §4.3, and its signature that section's rule 6. A receipt verifies when its signature verifies and its rules hold.

6. The lineup

6.1 Layout

size field meaning
4 magic WLU1 (ASCII)
1 version 1 in this revision
short channel_id the channel: a short text
short channel_name its name, as a guide shows it: a short text
32 schedule_root the root of the schedule it describes (§5)
8 schedule_start_ms u64: the schedule's first start
8 schedule_end_ms u64: the schedule's last end
4 p u32: the programmes' count, 1 to 10 000
8 × p programmes each programme's start (u64), in order
4 a u32: the assets' count, 1 to 10 000
… assets a assets, §6.2, in the order of their asset ids' bytes
32 signer the operator's Ed25519 key
64 sig §6.3

A short field is a u8 length and that many bytes; a long field a u16 length and that many bytes. Programme k runs from its start to the start of programme k + 1, the last one to schedule_end_ms.

6.2 An asset

size field meaning
short asset_id the schedule's asset id: a short text
long title a long text of 1 byte or more
long description a long text
1 c the categories' count, 0 to 16
short × c categories each a short text
short rating_system the rating's system, or no bytes
short rating_value the rating, or no bytes for none
1 k the artworks' count, 0 to 8
… artwork k artworks, each 32 bytes (the SHA-256 of the image's bytes) and a long field (its URI, a long text of 1 byte or more)
1 geo where it may be shown: 0 everywhere; 1 only in the countries listed; 2 everywhere but them
1 g the countries' count
2 × g countries each an ISO 3166-1 alpha-2 code, two of A–Z, in strictly increasing order
short rights_holder the party that holds its rights, as the operator names it: a short text
long content_uri the asset's media, or no bytes
8 duration_ms u64: the asset's duration, 0 when unknown

A rights holder is a name, not a key: a contract between the operator and the rights holder (a share contract of the revenue it earns, for one) names its parties itself, and a lineup states what the operator published.

6.3 Digest and signature

The digest of a lineup is SHA-256 over its bytes from magic through signer. The signature is Ed25519 by signer over wai:channel-lineup (ASCII) and 0x01, followed by the bytes the digest covers.

6.4 Reading and rules

A reader reads exactly the bytes it is given as one lineup, in the order of §6.1, and refuses it lineup_malformed when the bytes end before a field the layout requires or go on after sig, or when:

  1. its magic is not WLU1 or its version not 1;
  2. channel_id or channel_name is not a short text;
  3. schedule_start_ms is not below schedule_end_ms, either is not a multiple of 1000, or schedule_end_ms is above 2^53 − 1;
  4. p is 0 or above 10 000; or the programmes' starts are not strictly increasing, the first is not schedule_start_ms, the last is not below schedule_end_ms, or one is not a multiple of 1000;
  5. a is 0 or above 10 000, or the asset ids are not in strictly increasing order of their bytes;
  6. an asset breaks §6.2: an asset_id, a category or rights_holder that is not a short text; a title empty or not a text; a description, an artwork's URI or content_uri not a text; an empty artwork URI; c above 16 or k above 8; a rating_system that is not empty or a short text, or one that is not empty beside an empty rating_value; a rating_value that is not empty or a short text; geo above 2, geo 0 with g above 0, geo 2 with g 0, or countries not two of A–Z each in strictly increasing order; or duration_ms above 2^53 − 1.

6.5 A lineup and its schedule

A lineup describes a receipt's schedule when:

  1. its channel_id is the receipt's;
  2. its schedule_root is the receipt's, and its schedule_start_ms and schedule_end_ms are the start of the receipt's first entry and the end of its last;
  3. each programme's start is the start of an entry;
  4. each programme holds at least one program entry, and every program entry it holds (every entry that starts in its span) has one asset id, the programme's asset;
  5. the asset ids of its assets are exactly the programmes' assets.

A programme's ad and filler entries are its breaks; a guide lists the programme over them.

7. The guide

7.1 The programmes of a range

A guide is rendered from a receipt and a lineup that describes its schedule. The programmes of a range from from to to, with from below to, are the lineup's programmes whose span meets it (a programme that begins before to and ends after from), in order, each with its start, its end, its asset's id and its asset's entry (§6.2).

7.2 The XMLTV guide

The XMLTV guide of a range is these bytes, in UTF-8, each line ending with a line feed:

<?xml version="1.0" encoding="UTF-8"?>
<tv source-data-url="urn:wai:guide:ROOT:LINEUP:FROM:TO">
  <channel id="CHANNEL">
    <display-name>NAME</display-name>
  </channel>
  <programme start="START" stop="STOP" channel="CHANNEL">
    <title>TITLE</title>
    <desc>DESCRIPTION</desc>
    <category>CATEGORY</category>
    <icon src="URI"/>
    <episode-num system="wai-asset-id">ASSET</episode-num>
    <rating system="SYSTEM">
      <value>VALUE</value>
    </rating>
  </programme>
</tv>
  • ROOT is the receipt's schedule_root and LINEUP the lineup's digest, each 64 lowercase hexadecimal digits, and FROM and TO the range's ends in decimal, with no leading zero. urn:wai:guide: names a guide's sources; a guide names them in its source-data-url, an attribute the DTD gives tv for where the data came from.
  • CHANNEL is the lineup's channel_id and NAME its channel_name.
  • There is one programme element for each programme of the range, in order. START and STOP are its start and its end as XMLTV times (§3.4) in UTC with the zone written: YYYYMMDDhhmmss +0000.
  • Of a programme's children, in the order the DTD gives them: one title; one desc only when the description is not empty; one category for each category, in order; one icon for each artwork, in order, its src the URI; one episode-num, whose text is the asset id; and one rating only when rating_value is not empty, with a system attribute only when rating_system is not empty.
  • In text, &, < and > are written &amp;, &lt; and &gt;; in an attribute value, those and ", tab and line feed are written &amp;, &lt;, &gt;, &quot;, &#9; and &#10;. Every other character is written as itself.

The guide's episode-num system is wai-asset-id, the import's default id_system (§4.1), so a guide imports back into the programmes it lists.

7.3 The now/next document

The now/next document of an instant at is the JCS serialisation (RFC 8785) of this object, followed by one line feed:

member value
kind wai-now-next
channel, channel_name the lineup's channel_id and channel_name
schedule_root the receipt's, 64 lowercase hexadecimal digits
lineup the lineup's digest, 64 lowercase hexadecimal digits
at_ms at
now the programme whose span holds at, or null
next when now is a programme, the one after it, or null for the last; when at is before the schedule's first start, the first programme; otherwise null

A programme is an object with the members start_ms, stop_ms and asset_id; title, description and content_uri; categories, an array of strings; rating, null or {"system", "value"}; artwork, an array of {"sha256", "uri"}; geo, null for everywhere, {"allow": [...]} or {"deny": [...]}; rights_holder; and duration_ms.

8. Verification

8.1 Inputs

A verifier is given a receipt in its JSON carriage (§5), a lineup's bytes, and optionally a guide's bytes: an XMLTV guide when its first byte is <, a now/next document when it is {.

8.2 Steps

A verification gets one verdict, verified or the reason of the first step it fails:

  1. The receipt reads (§5): channel_receipt_malformed.
  2. Its signature verifies under signer_pubkey (§5): bad_key, bad_signature.
  3. Its rules hold (§5): channel_receipt_invalid.
  4. The lineup reads (§6.4): lineup_malformed.
  5. Its signature verifies under signer (§6.3): bad_key, bad_signature.
  6. Its signer is the receipt's signer_pubkey: one operator signs both. Otherwise lineup_signer_mismatch.
  7. It describes the receipt's schedule (§6.5): lineup_schedule_mismatch.
  8. The guide, when one is given, reads: guide_malformed. An XMLTV guide reads when it is in the subset of §3.3 with the root tv; its source-data-url is urn:wai:guide:, two runs of 64 lowercase hexadecimal digits and two decimal integers with no leading zero, the first below the second, separated by :; and each of its programme children has start and stop attributes that are XMLTV times (§3.4) and an episode-num child of system wai-asset-id, the text of the first of which is its asset id. A now/next document reads when it is one JSON object whose members are those of §7.3, with at_ms an integer of at most 2^53 − 1, schedule_root and lineup strings, and now and next each null or an object whose start_ms and stop_ms are integers and whose asset_id is a string.
  9. It names the receipt's schedule_root and the lineup's digest: guide_source_mismatch.
  10. Each of its programmes is the lineup's: for an XMLTV guide, each programme has the start, the end and the asset of one of the programmes of the guide's range (§7.1); for a now/next document, now and next have those of the programmes §7.3 names for its at_ms, and are null where those are. Otherwise guide_entry_unscheduled, at the first programme, in the guide's order, that is not (its index among the guide's programme elements; for a now/next document, now 0 and next 1).
  11. Its bytes are the guide of §7.2 for its range, or the document of §7.3 for its at_ms: guide_mismatch.

Step 10 catches a guide that places a programme where the schedule does not; step 11 every other difference: a title, a rating or a picture the lineup does not state, a programme left out.

8.3 What a verification states

  • its verdict, and the part it is about: receipt for steps 1 to 3, lineup for steps 4 to 7, guide for steps 8 to 11, none when verified;
  • the receipt's identity, when it reads; and the lineup's digest, when it reads;
  • for a guide that reads: its format (xmltv or now-next), its range or its at_ms, and its programmes, each with its start, its end, its asset id and whether it aired: whether an entry the receipt's aired lists meets its span. A guide shows what the schedule placed; the receipt states which of it was on air in its window;
  • for guide_entry_unscheduled, the index of step 10.

9. Operations and a worked example

Three operations of the WAI command line carry this text (the operation catalogue, wai/catalogue/ops.json), each with the toolchain's agent rules: --json for the envelope, --dry-run on an operation that writes, and --yes before a file is replaced.

  • channel.import (wai channel import): feeds in; the receipt (channel.json, its JSON carriage), the lineup (lineup.wlu) and the cues (cues.json) out, into a directory. A refusal's code is the error's code, and its exit status 1.
  • channel.guide (wai channel guide): a receipt and a lineup in; an XMLTV guide of a range (the whole schedule unless --from and --to say otherwise), or with --now-next --at a now/next document, out.
  • channel.verify (wai channel verify): a receipt, a lineup and optionally a guide in; what §8.3 states out, exit status 0 when verified and 1 otherwise.

The corpus's sample channel (§11) is imported, guided and verified so:

wai channel import --channel sample-1 --xmltv sample.xmltv.xml \
    --mrss sample-a.mrss.xml --mrss-owner owner-a --mrss sample-b.mrss.xml --mrss-owner owner-b \
    --owner owner-c --playlist sample.m3u8 --artwork . --key operator.key --signer-id sample-operator \
    --window-start 1793296800000 --window-end 1793300400000 --out-dir out
wai channel guide --receipt out/channel.json --lineup out/lineup.wlu --out guide.xml
wai channel guide --receipt out/channel.json --lineup out/lineup.wlu --now-next --at 1793299545000 --out now-next.json
wai channel verify --receipt out/channel.json --lineup out/lineup.wlu --guide guide.xml

The test tests/channel_lineup.rs runs these commands on the corpus's files and requires the corpus's receipt, lineup, cues and guides from them, byte for byte.

10. Reasons

The codes this extension adds, each with where it is raised:

reason raised at
import_inputs_invalid §4.1
import_oversized §3.1, §4.6 step 3
import_xml_malformed §3.3
import_json_malformed §4.4
import_playlist_malformed §4.5
import_time_malformed §3.4, §4.2, §4.3, §4.4, §4.6
import_timezone_unknown §3.4
import_no_stop §4.2 step 2
import_empty §4.2, §4.6
import_asset_unknown §4.2 step 3, §4.3 step 1, §4.6 step 1
import_asset_duplicate §4.3 step 1, §4.4
import_field_invalid §3.2, §4.2 to §4.5
import_scte35_malformed §4.5 step 3
import_break_duration §4.5 step 4
import_duration_unknown §4.6
import_gap §4.6
import_overlap §4.6, §4.7
import_owner_unknown §4.6 step 2
import_artwork_missing §4.6 step 3
import_break_outside §4.7
import_window_outside §4.9
channel_receipt_malformed §8.2 step 1
channel_receipt_invalid §8.2 step 3
lineup_malformed §6.4, §8.2 step 4
lineup_signer_mismatch §8.2 step 6
lineup_schedule_mismatch §8.2 step 7
guide_malformed §8.2 step 8
guide_source_mismatch §8.2 step 9
guide_entry_unscheduled §8.2 step 10
guide_mismatch §8.2 step 11

A verification also gives live-events' bad_key and bad_signature (§8.2 steps 2 and 5).

11. Conformance

live-conformance/channels/ holds the corpus, written from fixed seeds by wai_channel_lineup_vectors gen: vectors.json, which holds the operator's key and each vector's inputs and outcome; the feeds of a sample channel (an XMLTV document with a second channel's programmes beside it, two Media RSS documents of two content owners, a Media Playlist with SCTE-35 date ranges, and the JSON form of the same channel); the artwork files; the receipt and the guides the sample yields. Every document is a fixture written for its case, of programmes, owners and pictures made for it. The vectors:

  • imports: the sample from XMLTV, Media RSS and the playlist; the same channel from the JSON form, which yields the same receipt and lineup byte for byte; a channel made of one owner's Media RSS items alone, back to back; and a control of each refusal of §4, each the sample with one change (an edit the vector states, which a verifier applies to its copy);
  • verifications: the sample's receipt and lineup alone, with its XMLTV guide, with a guide of part of the schedule, and with now/next documents before, inside and after the schedule; and a control of each step of §8.2 (a receipt that does not read, a flipped signature, a key of small order, a program-as-run the window does not give, a lineup that does not read, one another key signed, one for each rule of §6.5 that it alone breaks, a guide that does not read, one naming another schedule, one that places a programme the schedule does not, and one whose title the lineup does not state).

A conforming implementation gives every import its outcome (the receipt, the lineup and the cues, or the refusal) and every verification its verdict and facts, as the corpus states. wai_channel_lineup_vectors verify and the test tests/channel_lineup.rs check that every file regenerates byte for byte, that wai-rs gives every vector its outcome, that §10 lists exactly the codes the reference adds, and that the corpus gives every reason of §10 and both of live-events'. live-conformance/channel_lineup_verify.py is the second implementation, written from this text: it shares no code with wai-rs; it verifies Ed25519 with WAI's strict rule (claim-conformance/_ed25519.py), BLAKE3 with claim-conformance/_blake3.py, and reads a channel receipt's carriage, its signed bytes, its identity and its rules as the revenue-share corpus's second implementation reads them (§5); it reads XML, JSON and playlists itself; and it mutates copies of the corpus and requires every copy to fail, and drops each of its own checks in turn and requires the corpus to catch the weakened verifier.

12. Security and privacy considerations

  • What a lineup proves. That the operator stated this description of each programme asset of one schedule, and where its guide's programmes begin. It does not prove that a rights holder agreed: a rights holder's name in a lineup is the operator's statement, and a party paid by it checks a contract it signed, not the lineup.
  • What a guide proves. Nothing by itself: it is recomputed from a receipt and a lineup and compared, so a guide that verifies says exactly what the two signed records say, and a guide a third party altered is refused. A guide is a schedule published ahead of air; which of it was on air in a window is the receipt's program-as-run (§8.3), and a guide covering a span the receipt's window does not is checked against the schedule alone.
  • Artwork. A lineup binds each picture by its SHA-256, so a player that fetches the URI and checks the hash shows the picture the operator stated, wherever it was fetched from.
  • Bounded reading. An import reads bounded documents (§3.1), XML to a bounded depth with no document type declaration that defines anything (§3.3), so no entity is ever expanded, and integers it reads are checked against their bounds before any arithmetic.
  • Availability. Where an asset may be shown is the operator's statement of the catalogue's restriction; a player enforces it, or a delivery that a wai.video.manifest describes does. A lineup holds no viewer's data.