# WAI Extension: Signal Health > Status: Draft. Registers the record `wai.live.signal_health`: a monitor's signed statement > of what it measured on one input over a window of time. A **signal-health record** > (`WSH1`, §4) carries up to six checks: black and frozen pictures, silence, when captions and > cues were last seen, and the first-priority transport-stream indicators of ETSI TR 101 290. > Each check states the thresholds it was measured against, the integer values it measured, > the digest of the media it read and its verdict, `ok` or `fault`. Every value is a pure > integer function of that media (§5), so a verifier that holds the same media recomputes the > record; one that does not holds each verdict to the values and thresholds the record states > and each threshold to the profile it expects (§6). An input switch acts on the verdicts of > the monitors it trusts (§7). The reference implementation is the `signal_health` module of > `wai-rs` (feature `signal_health`); the corpus is > [`live-conformance/signal-health/`](../live-conformance/signal-health/), and its second > implementation is `live-conformance/signal_health_verify.py`. > Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174. > > Texts read for this revision: ETSI TR 101 290 V1.4.1 (2020-06), clause 5.2.1 (the > first-priority indicators and the sync hysteresis it proposes); ISO/IEC 13818-1 (the > transport packet, the adaptation field, program-specific information); RFC 8032. A section > number of one of those texts here is that revision's. ## 1. Scope In scope: the checks and what each reads (§3); the record, its digest and its signature (§4); how each check's values are computed from its media (§5); what a verifier derives from a record, with or without the media (§6); and how an input switch chooses an input from the records it holds (§7). Out of scope: decoding. A monitor reads the pictures and samples a decoder of the input's capability outputs; for a capability whose class is `StandardDefined` every conforming decoder outputs the same pictures (SPEC §5), so two monitors that decode one recording read the same luma, and either one's record recomputes from the other's reading. Also out of scope: how records travel, how a monitor raises an alert, how media reaches a monitor, and the second- and third-priority indicators of TR 101 290. Relationship to the core spec: `wai.live.signal_health` is a **record** (SPEC §5). It names no payload format and is not dispatchable. ## 2. Terms - **Instant**: a count of milliseconds since 1970-01-01T00:00:00Z, leap seconds not counted, as [holdings](holdings.md) writes one. - **Window**: the instants from `from_ms` up to, not including, `to_ms`, that a record covers. - **Input**: one contribution to a channel or a room, named by the operator: a primary and a backup feed of one channel are two inputs. - **Monitor**: the party that measures an input and signs the record, named by its Ed25519 key. - **Frame**: the luma plane of one decoded picture, its samples 8-bit, row by row, with the instant it is presented at. A picture of more than 8 bits is read at its 8 most significant bits. - **Run**: a maximal sequence of consecutive frames, or of consecutive audio blocks, that each meet a check's condition. - **Profile**: the thresholds an operator states for its inputs, per check. ## 3. The checks | code | name | reads | thresholds | fault when | |---|---|---|---|---| | 1 | `black` | frames | `luma_max`, `ratio_ppm`, `min_ms` | a run of black frames lasts `min_ms` or longer | | 2 | `freeze` | frames | `mad_milli`, `min_ms` | a run of frozen frames lasts `min_ms` or longer | | 3 | `silence` | audio | `peak_max`, `min_ms` | a run of silent blocks lasts `min_ms` or longer | | 4 | `captions` | caption instants | `max_gap_ms` | `max_gap_ms` is not 0 and the longest gap without a caption exceeds it | | 5 | `cues` | cue instants | `max_gap_ms` | `max_gap_ms` is not 0 and the longest gap without a cue exceeds it | | 6 | `transport` | transport stream | `table_max_gap_ms`, `pid_max_gap_ms` | any indicator of §5.6 counted an event | A record carries the checks its input has: an input with no audio carries no `silence`, and one whose captions are not monitored carries no `captions`. `max_gap_ms` 0 states when captions or cues were last seen and checks nothing: a channel's cues may be hours apart, and only an operator whose encoder sends a cue at a known interval (a splice null as a heartbeat, say) has a gap to check. A **caption** is a caption cue of the input's caption track ([timed-text](timed-text.md)) or a line-21 or caption-service packet in its video; a **cue** is a splice information section of the input (SCTE 35) or a cue event of its room ([live-events](live-events.md)). Which ones the monitor counts is its own; the record binds the instants it counted (§5.4). ## 4. The record ### 4.1 Layout Integers are unsigned and big-endian. | size | field | meaning | |---|---|---| | 4 | `magic` | `WSH1` (ASCII) | | 1 | `version` | 1 in this revision | | 32 | `monitor` | the monitor's Ed25519 key | | 1 | `name_len` | 1 to 64 | | `name_len` | `input` | the input's name: ASCII letters, digits, `.`, `_`, `-` and `:` | | 8 | `from_ms` | u64: the window's first instant | | 8 | `to_ms` | u64: the instant the window ends at | | 1 | `m` | 1 to 6: the checks' count | | … | `checks` | `m` checks, §4.2, in strictly increasing order of `code` | | 64 | `sig` | §4.3 | ### 4.2 A check Every check is `code` (u8), `media` (32 bytes, the digest of the media it read, §5), then its thresholds and its values, then `verdict` (u8: 0 `ok`, 1 `fault`): | code | thresholds | values | |---|---|---| | 1 `black` | `luma_max` u8 · `ratio_ppm` u32 · `min_ms` u32 | `frames` u32 · `matched` u32 · `longest_ms` u64 · `longest_at_ms` u64 | | 2 `freeze` | `mad_milli` u32 · `min_ms` u32 | `frames` u32 · `matched` u32 · `longest_ms` u64 · `longest_at_ms` u64 | | 3 `silence` | `peak_max` u16 · `min_ms` u32 | `blocks` u32 · `matched` u32 · `longest_ms` u64 · `longest_at_ms` u64 | | 4 `captions` | `max_gap_ms` u32 | `seen` u32 · `last_ms` u64 · `longest_gap_ms` u64 | | 5 `cues` | `max_gap_ms` u32 | `seen` u32 · `last_ms` u64 · `longest_gap_ms` u64 | | 6 `transport` | `table_max_gap_ms` u32 · `pid_max_gap_ms` u32 | `packets` u64 · `sync_loss` u32 · `sync_byte` u32 · `pat` u32 · `cc` u32 · `pmt` u32 · `pid` u32 | `matched` counts the frames or blocks that met the check's condition, `longest_ms` is the longest run's duration and `longest_at_ms` its first instant (§5); `seen` counts the instants read, `last_ms` is the last of them and `longest_gap_ms` the longest gap (§5.4); the six counts of `transport` are the indicators of §5.6. ### 4.3 Digest and signature The **digest** of a record is SHA-256 over its bytes from `magic` through its last check. The **signature** is Ed25519 by `monitor` over `wai:signal-health` (ASCII) and 0x01, followed by the bytes the digest covers. ## 5. What each check computes Every quantity here is an integer and every comparison exact. A check's `media` is SHA-256 over a domain string, the byte 0x01 and the media, as below; a verifier that holds the media holds it to that digest before it recomputes anything (§6.4). ### 5.1 Frames: `black` and `freeze` The **frames** are a sequence, each its instant (u64), its width and height (u16 each, at least 1) and its `width × height` luma samples. Their instants are strictly increasing and within the window. Their digest is over `wai:signal-health:video`, 0x01, and for each frame in order its instant (u64), width (u16), height (u16) and samples. A frame's **span** ends at the next frame's instant, and the last frame's at `to_ms`. - **Black.** A frame is black when 1 000 000 × (its samples at or below `luma_max`) ≥ `ratio_ppm` × (its samples). A run of black frames lasts from its first frame's instant to the end of its last frame's span. - **Freeze.** A frame other than the first is frozen when it has the previous frame's width and height and 1000 × Σ |its sample − the previous frame's sample at the same position| ≤ `mad_milli` × (its samples): its mean absolute difference from the previous picture is at most `mad_milli` thousandths of a code value. The first frame is never frozen. A run of frozen frames lasts from the instant of the frame before its first frame, the picture that held, to the end of its last frame's span. `frames` is the frames' count and `matched` the black, or frozen, frames'. `longest_ms` is the longest run's duration and `longest_at_ms` the instant its duration starts at, the earliest run where two are equally long; both are 0 when no frame matched. ### 5.2 Audio: `silence` The **audio** is its first sample's instant `at_ms` (u64), its rate (u32, a multiple of 100 from 100 to 384 000), its channels (u16, 1 to 32) and its samples, 16-bit two's complement, little-endian, interleaved, a whole number of sample frames. Its digest is over `wai:signal-health:audio`, 0x01, `at_ms` (u64), the rate (u32), the channels (u16) and the samples as given. It is cut into **blocks** of rate ÷ 100 sample frames, 10 ms each: block *b* starts at `at_ms` + 10*b*, and a last part shorter than a block is not read. `at_ms` is at or after `from_ms`, and the end of the last whole block at or before `to_ms`. A block is silent when the greatest absolute value of its samples, over every channel, is at most `peak_max` (|−32768| is 32768). A run of silent blocks lasts 10 ms a block. `blocks` is the blocks' count, `matched` the silent blocks', and `longest_ms` and `longest_at_ms` are as in §5.1, `longest_at_ms` the run's first block's start. ### 5.3 Why peaks and means of absolute differences A peak and a sum of absolute differences are exact in integers, need no square root, and do not depend on the order a sum is taken in, so two implementations agree to the sample. Loudness measures something else: a programme can be at its target loudness and carry a silent channel, and a quiet channel is not a silent one. ### 5.4 Instants: `captions` and `cues` The **instants** are a sequence of u64, non-decreasing and within the window. Their digest is over `wai:signal-health:captions` (for `cues`, `wai:signal-health:cues`), 0x01, their count (u32) and each instant (u64). `seen` is their count and `last_ms` the last of them, 0 when there is none. The **gaps** are the instants' differences, in order, from `from_ms` to the first, from each to the next, and from the last to `to_ms`; with no instant, the one gap is the window. `longest_gap_ms` is the greatest. ### 5.5 The transport stream The **transport stream** is its bytes, a whole number of 188-byte packets and at least one, the instant in microseconds its first packet arrived (`first_us`, u64) and the rate it arrived at (`rate_bps`, u64, at least 1). Packet *k*, from 0, arrived at *a*(*k*) = `first_us` + ⌊*k* × 1 504 000 000 ÷ `rate_bps`⌋ µs: a stream of constant rate, as a broadcast multiplex is. `first_us` is at or after `from_ms` × 1000, and the last packet's arrival before `to_ms` × 1000. Its digest is over `wai:signal-health:transport`, 0x01, `first_us` (u64), `rate_bps` (u64) and the bytes. `packets` is the packets' count. Every duration below is compared in microseconds, a threshold in milliseconds taken × 1000, and *E* is `to_ms` × 1000. ### 5.6 The indicators The six indicators of TR 101 290 clause 5.2.1 (Table 5.0a), using its later forms 1.3.a and 1.5.a, as it recommends. Each counts **events**; the check's verdict is `fault` when any count is above 0. **Sync (1.1 `sync_loss`, 1.2 `sync_byte`).** The packets are read in order, 188 bytes apart from the first byte; a packet's sync byte is good when its first byte is 0x47. TR 101 290 proposes five consecutive good sync bytes to acquire sync and two or more consecutive bad ones to lose it. So a reader starts **hunting**; a good sync byte adds one to its run of good ones and a bad one sets the run to 0, and at a run of 5 it is **locked**, and analyses (below) the five packets of the run, in order. Locked, it analyses each packet with a good sync byte; a bad one counts one `sync_byte` event and is not analysed, and the second of two consecutive bad ones counts one `sync_loss` event and returns the reader to hunting, with a run of 0. A stream whose reader is never locked counts one `sync_loss` event. A bad sync byte met while hunting counts nothing. **Analysis.** For an analysed packet: its PID is the 13 bits after its first byte's successor's top three; `scrambling` its fourth byte's top two bits, `afc` the next two, `cc` the low four; `pusi` bit 0x40 of its second byte. With `afc` 2 or 3 its adaptation field's length *L* is its fifth byte, its **discontinuity indicator** is bit 0x80 of its sixth byte where *L* > 0, and its payload starts at byte 5 + *L*; with `afc` 1 at byte 4. It **has a payload** when `afc` is 1 or 3 and its payload starts before byte 188. A packet of PID 0x1FFF (a null packet) is not analysed further. For every other packet, in this order: 1. **Continuity (1.4 `cc`).** A packet with a payload is held to the last `cc` of its PID's packets with a payload, if there is one. With its discontinuity indicator set, it is taken as it is. Otherwise, a `cc` equal to the last is a duplicate: TR 101 290 allows a packet to occur twice, so the PID's second consecutive duplicate, and each one after, counts one event; and a `cc` that is neither the last nor the last plus 1 modulo 16 counts one event (a lost or reordered packet). Anything but a duplicate sets the PID's count of consecutive duplicates to 0. The PID's last `cc` becomes this packet's. A packet without a payload is not held to it and leaves it as it was. 2. **The referred PIDs (1.6 `pid`).** A PID that a program map names as an elementary stream (step 4) is **referred**, and has an anchor, the instant it was last seen. A packet of a referred PID arriving more than `pid_max_gap_ms` after the anchor counts one event; the anchor becomes its arrival. 3. **PID 0 (1.3.a `pat`).** On PID 0, a packet whose `scrambling` is not 0 counts one event. A packet with `pusi` and a payload has a pointer field, its payload's first byte, and a section starts after it, at the payload's start + 1 + the pointer field, if that is before byte 188. A section of `table_id` 0x00 there is a program association section: arriving more than `table_max_gap_ms` after the PAT anchor, which starts at `from_ms` × 1000, it counts one event, and the anchor becomes its arrival. A section of another `table_id` there, but 0xFF (stuffing), counts one event. A program association section whose `section_length` is at least 9 and which ends within its packet (its 3 header bytes and `section_length` bytes) replaces the **program map PIDs** with those of its program loop (every 4-byte entry from its ninth byte to the 4 bytes of its CRC whose `program_number` is not 0), at its arrival. A program map PID that becomes one has its anchor at that arrival; one that stops being one is held to its anchor once more at that arrival (step 4's rule), and is forgotten. 4. **A program map PID (1.5.a `pmt`).** On a program map PID, a packet whose `scrambling` is not 0 counts one event. A section of `table_id` 0x02 that starts in it, as on PID 0, is a program map section: arriving more than `table_max_gap_ms` after the PID's anchor, it counts one event, and the anchor becomes its arrival. One whose `section_length` is at least 13 and which ends within its packet replaces the PID's **elementary PIDs** with those of its stream loop: after its 12 bytes of header and its `program_info_length` bytes of descriptors, each entry of 5 bytes and its `ES_info_length` bytes, while a whole entry lies before the CRC. The referred PIDs are then every program map PID's elementary PIDs together: one that becomes referred has its anchor at this arrival; one that stops being referred is held to its anchor once more at this arrival (step 2's rule), and is forgotten. A PID forgotten as a program map PID takes its elementary PIDs out of the referred ones in the same way. At *E*, the PAT anchor, each program map PID's anchor and each referred PID's anchor are held once more: each more than its gap before *E* counts one event. A gap is counted once, however long it lasts. TR 101 290 makes every other indicator invalid while 1.1 is active; a record counts them for the packets it analysed, and a verifier reads them with `sync_loss`. Its sections' CRCs are second-priority (2.2) and are not checked here. ## 6. What a verifier derives ### 6.1 Reading A reader reads exactly the bytes it is given as one record, in the order of §4.1, and refuses it `signal_health_malformed` when the bytes are more than 1024, end before a field the layout requires or go on after `sig`, or when: 1. its magic is not `WSH1` or its version not 1; 2. `name_len` is 0 or above 64, or `input` holds a byte other than those §4.1 allows; 3. `from_ms` is not below `to_ms`, or `to_ms` is above 2^53 − 1; 4. `m` is 0 or above 6, a `code` is not 1 to 6, or the codes are not strictly increasing; 5. a threshold is out of its range: `ratio_ppm` 1 to 1 000 000, `peak_max` at most 32 768, every `min_ms`, `table_max_gap_ms` and `pid_max_gap_ms` at least 1; 6. a value cannot be one §5 computes: `matched` above `frames` or `blocks`; `longest_ms` above `to_ms` − `from_ms`; `matched` 0 and `longest_ms` or `longest_at_ms` not 0, or `matched` not 0 and `longest_ms` 0 or `longest_at_ms` outside the window; `seen` 0 and `last_ms` not 0 or `longest_gap_ms` not the window's length, or `seen` not 0 and `last_ms` outside the window; `longest_gap_ms` above the window's length; 7. a `verdict` is not 0 or 1. A record that reads is then refused `signal_health_signature_invalid` when its signature does not verify under `monitor` as SPEC §7 *Signatures* verifies every signature: a key that is not the canonical encoding of a point of large order fails, as does a signature whose scalar is not below the group order. ### 6.2 Verdicts A check's verdict follows from its thresholds and values: `fault` exactly when the condition of §3 holds of them (for `black`, `freeze` and `silence`, `longest_ms` ≥ `min_ms`; for `captions` and `cues`, `max_gap_ms` not 0 and `longest_gap_ms` > `max_gap_ms`; for `transport`, a count above 0). A record one of whose checks states another verdict is refused `signal_health_verdict_inconsistent`. A verifier checks this with no media. ### 6.3 The profile A verifier that expects a profile for the input holds the record to it: the record has a check for each check the profile names and no other, each with the profile's thresholds, or it is refused `signal_health_profile_mismatch`. A monitor that loosened a threshold, or left out the check that would have failed, is refused here, without the media; its record is still its word about what it measured. ### 6.4 Recomputing A verifier that holds the media a check read recomputes the check, in the record's order: 1. the media is held to §5's form for it (instants within the window, a rate that is a multiple of 100, whole packets, and so on), or the check is refused `signal_health_media_malformed`; 2. the media's digest is the check's `media`, or `signal_health_media_mismatch`; 3. the values of §5 under the check's thresholds are the check's values, or `signal_health_not_recomputed`. A check whose media the verifier does not hold is **not held**: its values are the monitor's word, bound by its digest to the media it read. `black` and `freeze` read the same frames and carry the same digest. ## 7. The input switch A switch chooses, among the inputs of one channel in its order of preference, the one to carry. It is configured with that order, the monitor keys it trusts, the profile, and `max_age_ms`, how old a record may be and still speak for its input. At an instant *t*, each record it holds is, in this order: - refused (§6.1 to §6.3), with the reason; - `untrusted`: its monitor is not one the switch trusts; - `not_yet`: its `to_ms` is after *t*; - `superseded`: another record of its input that stands this far has a later `to_ms`; - `contradicted`: another record of its input with the same `to_ms` has another digest; - otherwise it `stands`. Each input is then `none` (no record of it stands or is contradicted), `contradicted`, `stale` (*t* − its record's `to_ms` > `max_age_ms`), `fault`, with the names of its record's faulted checks in code order, or `healthy`. The switch carries the first healthy input in its order; with none healthy it keeps the input it carries. It never moves to an input it cannot vouch for: an input with no fresh record is not healthy, so a monitor that stops sending records stops vouching for its input. A switch **SHOULD** recompute (§6.4) a record whose media it holds before it acts on it. ## 8. Reasons The codes this extension adds, each with where it is raised: | reason | raised at | |---|---| | `signal_health_malformed` | §6.1 | | `signal_health_signature_invalid` | §6.1 | | `signal_health_verdict_inconsistent` | §6.2 | | `signal_health_profile_mismatch` | §6.3 | | `signal_health_media_malformed` | §6.4 | | `signal_health_media_mismatch` | §6.4 | | `signal_health_not_recomputed` | §6.4 | ## 9. Conformance [`live-conformance/signal-health/`](../live-conformance/signal-health/) holds the corpus, written from fixed seeds by `wai_signal_health_vectors gen` as `vectors.json`. It has: - **Records**: one with all six checks healthy, and one faulted record for each check, with its digest and its faulted checks; and one negative control for each rule of §6.1, for a signature that does not verify (a non-canonical key among them) and for a verdict that does not follow from its values, each way. - **Checks**: records against a profile and against their media: each check recomputed; a black run, a freeze held by its frame before, a silent run in 10 ms blocks, caption and cue gaps at the window's edges; every transport indicator counting events (sync lost and found again, a bad sync byte, a PAT late, scrambled and of another table, a duplicate allowed and a third occurrence counted, a discontinuity taken, a lost packet, a PMT late, an elementary stream gone quiet); a threshold changed from the profile, a check left out, media that does not match its digest, media out of the window, and a value the media does not give. - **Switches**: a primary and a backup, healthy, faulted, stale, superseded, contradicted, by an untrusted monitor and refused, with the input each switch carries. A conforming verifier gives every case the outcome, the reason and everything else the corpus states. `wai_signal_health_vectors verify` and the test `tests/signal_health.rs` check that the file regenerates byte for byte, that `wai-rs` gives every case its outcome, that every reason of §8 appears, and that §8 lists exactly the reference's codes; the generator declares each media case's values and requires the reference to measure them. `live-conformance/signal_health_verify.py` is the second implementation, written from this text: it shares no code with `wai-rs`; it reads records, recomputes every check from the media and runs the switch itself, 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. ## 10. Security and privacy considerations - **A record is its monitor's word**, bound to the media it read. A verifier with the media recomputes it; one without holds the verdicts to the values (§6.2) and the thresholds to the profile (§6.3), so a monitor cannot report `ok` over values that fail, loosen a threshold, or drop a check, without being refused. It can misstate the values of media no one else holds; a second monitor on the same input, or a recording, is the check on that. - **A switch moves on trusted records only.** Anyone can sign a record that states a fault; a switch that acted on it would let anyone take a channel to its backup. §7 reads only the keys it is configured with, and refuses a record whose verdicts do not follow from its values. - **Freshness**: a switch treats an input with no fresh record as not healthy (§7). A monitor **SHOULD** sign a record a window at a time, with windows shorter than `max_age_ms`. - **What a record discloses**: an input's name, the window and the figures, nothing of its pictures or sound beyond the digests. A digest of media that few hold says little; one of a public broadcast can confirm that a party held a given recording. - **Bounded work**: a record is at most 1024 bytes, read once, in order; recomputing reads each frame, block and packet once and keeps state per PID, at most 8192 PIDs.