Skip to main content

WAI Extension: Channel Lineup and Guide

Mirrored from the canonical text at commit 260cbd09 ().

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.

This revision adds the cue tags of a Media Playlist of the channel (§7.4): a packager that writes the channel’s segments, MPEG-2 transport stream segments among them, marks each break of the receipt’s schedule with EXT-X-CUE-OUT and the break’s duration, EXT-X-CUE-OUT-CONT on each later segment of the break and EXT-X-CUE-IN on its return, and, for a break whose cue carries its SCTE-35 section, with the EXT-X-DATERANGE tags of RFC 8216 §4.3.2.7.1; a verifier given the receipt, its cues and the playlist recomputes the tags and compares (§8.4). An import, a lineup and a guide are unchanged.

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 8216 (HTTP Live Streaming): §4.2 (attribute lists), §4.3.2.1 (EXTINF), §4.3.2.6 (EXT-X-PROGRAM-DATE-TIME), §4.3.2.7 (EXT-X-DATERANGE), §4.3.2.7.1 (SCTE-35 in a date range), §4.3.3.1, §4.3.3.2 and §4.3.3.4 (EXT-X-TARGETDURATION, EXT-X-MEDIA-SEQUENCE, EXT-X-ENDLIST), §6.2.1 and §6.3.1 (the server’s and the client’s general rules), §7 (protocol versions) and §8.10 (its example of a date range carrying SCTE-35);
  • 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), and the cue tags of a Media Playlist of the channel (§7.4); verification of a receipt, a lineup and a guide together (§8), and of a playlist’s cue tags (§8.4); 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; how a packager cuts and encodes segments, which §7.4 takes as given; 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

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:

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:

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:

membervalue
kindthe string wai-channel-feed
channel_namea string
programmesan array of objects, each with exactly start_ms and stop_ms (integers) and asset_id (a string)
assetsan array of objects, each with exactly the members of the next table
breaksan array of objects, each with exactly id (a string), start_ms and duration_ms (integers) and scte35 (a string, or null)
asset membervalue
asset_id, title, description, content_uristrings
categoriesan array of strings
ratingnull, or an object with exactly system and value, strings
artworkan array of objects, each with exactly uri and sha256, strings
geonull (shown everywhere), or an object with exactly one member, allow (shown only in) or deny (shown everywhere but), an array of strings
rights_holdera string, or null
duration_msan 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:

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

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

sizefieldmeaning
4magicWLU1 (ASCII)
1version1 in this revision
shortchannel_idthe channel: a short text
shortchannel_nameits name, as a guide shows it: a short text
32schedule_rootthe root of the schedule it describes (§5)
8schedule_start_msu64: the schedule’s first start
8schedule_end_msu64: the schedule’s last end
4pu32: the programmes’ count, 1 to 10 000
8 × pprogrammeseach programme’s start (u64), in order
4au32: the assets’ count, 1 to 10 000
…assetsa assets, §6.2, in the order of their asset ids’ bytes
32signerthe operator’s Ed25519 key
64sig§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

sizefieldmeaning
shortasset_idthe schedule’s asset id: a short text
longtitlea long text of 1 byte or more
longdescriptiona long text
1cthe categories’ count, 0 to 16
short × ccategorieseach a short text
shortrating_systemthe rating’s system, or no bytes
shortrating_valuethe rating, or no bytes for none
1kthe artworks’ count, 0 to 8
…artworkk 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)
1geowhere it may be shown: 0 everywhere; 1 only in the countries listed; 2 everywhere but them
1gthe countries’ count
2 × gcountrieseach an ISO 3166-1 alpha-2 code, two of A–Z, in strictly increasing order
shortrights_holderthe party that holds its rights, as the operator names it: a short text
longcontent_urithe asset’s media, or no bytes
8duration_msu64: 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>

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:

membervalue
kindwai-now-next
channel, channel_namethe lineup’s channel_id and channel_name
schedule_rootthe receipt’s, 64 lowercase hexadecimal digits
lineupthe lineup’s digest, 64 lowercase hexadecimal digits
at_msat
nowthe programme whose span holds at, or null
nextwhen 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.

7.4 The Media Playlist’s cue tags

A receipt also gives the ad markers of a Media Playlist of the channel (RFC 8216), so that a packager that writes the channel’s segments, MPEG-2 transport stream segments among them, marks each break where the signed schedule places it, and anyone holding the receipt and its cues recomputes the markers and compares (§8.4). Each break is marked in two forms:

A client ignores a tag it does not recognise (RFC 8216 §6.3.1), so a client that reads one form plays a playlist that carries both.

The breaks are the receipt’s ad entries of a duration above 0, in schedule order. A break’s id is its entry’s asset id, its start the entry’s start, its duration the entry’s duration, and its end its start plus its duration. Each break has its cue, the one an import wrote for it (§4.8, the cues.json of §9); its section is the cue’s message when the cue is of form 1, and it has none when the cue is of form 0. The receipt signs each break’s id, start and duration, which are all the cue tags are written from. It does not sign the section a date range carries: that is the cue’s, which §8.4 holds to its break’s id and duration and no further.

The window is what a Media Playlist lists: its media sequence number (RFC 8216 §4.3.3.2); the instant its first segment begins; its segments in order, each a URI and a duration, each beginning where the one before it ends; and whether it is ended (EXT-X-ENDLIST, RFC 8216 §4.3.3.4). A segment’s URI is 1 to 2 048 bytes of ASCII from ! to ~, the first of which is not #, and its duration 1 to 2^32 − 1 milliseconds; the media sequence number is at most 2^53 − 1, and the last segment ends before 253 402 300 800 000, the first instant of the year 10000. A break in the window starts at the latest where the window ends, so every date below has four digits of year. A window that is not so is refused playlist_malformed.

A segment starts in a break when it starts at or after the break’s start and before its end. A break’s last segment before it is the segment that ends at its start, and its return the segment that starts at its end. A break is in the window when the window holds its last segment before it, a segment that starts in it, or its return. The tags are refused:

Otherwise the playlist of the window is these lines, each ended by a line feed:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:TARGET
#EXT-X-MEDIA-SEQUENCE:SEQUENCE
#EXT-X-PROGRAM-DATE-TIME:FIRST

then, for each segment of the window in order, its cue lines (below), #EXTINF:DURATION, and its URI; and then #EXT-X-ENDLIST when the window is ended.

A segment’s cue lines are, in this order:

  1. The date range tags written at it, break by break in schedule order, of each break that has a section: its out tag at the first segment of the window that puts it in the window, and its in tag at its return, when the window holds it:

    #EXT-X-DATERANGE:ID="ID",START-DATE="START",PLANNED-DURATION=SPAN,SCTE35-OUT=0xSECTION
    #EXT-X-DATERANGE:ID="ID",START-DATE="START",DURATION=SPAN

    ID is the break’s id, @ and its start in decimal; START its start as a date; SPAN its duration as a span; and SECTION its section in uppercase hexadecimal, a hexadecimal-sequence (RFC 8216 §4.2).

  2. #EXT-X-CUE-IN, when a break ends at the segment’s start.

  3. #EXT-X-CUE-OUT:SPAN, SPAN the break’s duration, when a break starts at the segment’s start; otherwise #EXT-X-CUE-OUT-CONT:ElapsedTime=ELAPSED,Duration=SPAN, when the segment starts in a break, ELAPSED the time from the break’s start to the segment’s as a span.

So:

For brk-1 of the corpus’s sample (§11), 90 seconds from 18:12:00 UTC, in ten-second segments, the last segment before it, its first and its return carry:

#EXT-X-DATERANGE:ID="brk-1@1793297520000",START-DATE="2026-10-29T18:12:00.000Z",PLANNED-DURATION=90.000,SCTE35-OUT=0xFC30…
#EXTINF:10.000,
seg-71.ts
#EXT-X-CUE-OUT:90.000
#EXTINF:10.000,
seg-72.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=10.000,Duration=90.000
#EXTINF:10.000,
seg-73.ts
…
#EXT-X-DATERANGE:ID="brk-1@1793297520000",START-DATE="2026-10-29T18:12:00.000Z",DURATION=90.000
#EXT-X-CUE-IN
#EXTINF:10.000,
seg-81.ts

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

8.4 A playlist’s cue tags

A verifier given a receipt in its JSON carriage (§5), a cues document and a Media Playlist’s bytes gets one verdict, verified or the reason of the first step it fails:

  1. The receipt reads, as §8.2 step 1: channel_receipt_malformed.
  2. Its signature verifies, as §8.2 step 2: bad_key, bad_signature.
  3. Its rules hold, as §8.2 step 3: channel_receipt_invalid.
  4. The cues read: one JSON text (RFC 8259) in UTF-8, nested at most 64 deep, whose objects each name a member once, an array of objects of exactly break (a string), start_ms (an integer of at most 2^53 − 1, written as §4.4 writes one) and payload (an even count of lowercase hexadecimal digits), as channel.import writes cues.json (§9). Otherwise cues_malformed.
  5. They are the breaks’ cues (§7.4): there are as many as there are breaks, and the cue at each index is its break’s: its break the break’s id and its start_ms the break’s start, and its payload the one §4.8 yields for a break of that id and duration and of the section the payload carries, when it carries a message of form 1 that reads as §4.5 step 3 reads a section, or of none, when it is of form 0. Otherwise cues_mismatch, at the first index that is not, a cue missing or added at the first index past the shorter list. A break whose duration is above 2^32 − 1 milliseconds has no cue of §4.8, and its index fails.
  6. The playlist reads: UTF-8 text whose lines, split at each line feed, the last ended by one, begin with #EXTM3U; with exactly one line #EXT-X-MEDIA-SEQUENCE: and a decimal integer with no leading zero, and a line #EXT-X-PROGRAM-DATE-TIME: and an HLS date-time (§3.4) before its first segment; whose segments are each a line #EXTINF:, a duration (§3.4) and ,, followed by a line, the segment’s URI, at least one of them. It is ended when one of its lines is #EXT-X-ENDLIST. Its window is that media sequence number, the first such date-time’s instant as its first segment’s start, its segments in order and whether it is ended, which MUST hold to §7.4’s bounds. Otherwise playlist_malformed. No other line is read here: step 8 compares every line.
  7. Its window’s tags can be written (§7.4): cue_unaligned, at the segment’s index; cue_unwritable, at the break’s index among the breaks.
  8. Its bytes are the playlist of §7.4 of its window: playlist_mismatch.

Step 8 catches a marker the schedule does not place, at a segment or in a date range: a break another duration, another start or another section than its cue’s, a tag left out, added or moved.

A verification states its verdict and the part it is about (receipt for steps 1 to 3, cues for steps 4 and 5, playlist for steps 6 to 8, none when verified); the receipt’s identity, when it reads; for a playlist that reads, its window (its media sequence number, its first segment’s start, how many segments it holds and whether it is ended) and the breaks in it, each with its id, its start, its duration and whether it has a date range; and the index of step 5 or step 7.

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.

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:

reasonraised 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
cues_malformed§8.4 step 4
cues_mismatch§8.4 step 5
playlist_malformed§7.4, §8.4 step 6
cue_unaligned§7.4, §8.4 step 7
cue_unwritable§7.4, §8.4 step 7
playlist_mismatch§8.4 step 8

A verification also gives live-events’ bad_key and bad_signature (§8.2 steps 2 and 5, §8.4 step 2).

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:

A conforming implementation gives every import its outcome (the receipt, the lineup and the cues, or the refusal), every verification its verdict and facts, every playlist window its playlist or its refusal, and every playlist 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 renders and reads a Media Playlist’s cue tags itself (§7.4, §8.4); 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