Skip to main content

WAI Extension: Signal Health

Mirrored from the canonical text at commit 0d3f0d7c ().

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/, 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

3. The checks

codenamereadsthresholdsfault when
1blackframesluma_max, ratio_ppm, min_msa run of black frames lasts min_ms or longer
2freezeframesmad_milli, min_msa run of frozen frames lasts min_ms or longer
3silenceaudiopeak_max, min_msa run of silent blocks lasts min_ms or longer
4captionscaption instantsmax_gap_msmax_gap_ms is not 0 and the longest gap without a caption exceeds it
5cuescue instantsmax_gap_msmax_gap_ms is not 0 and the longest gap without a cue exceeds it
6transporttransport streamtable_max_gap_ms, pid_max_gap_msany 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) 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). 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.

sizefieldmeaning
4magicWSH1 (ASCII)
1version1 in this revision
32monitorthe monitor’s Ed25519 key
1name_len1 to 64
name_leninputthe input’s name: ASCII letters, digits, ., _, - and :
8from_msu64: the window’s first instant
8to_msu64: the instant the window ends at
1m1 to 6: the checks’ count
…checksm checks, §4.2, in strictly increasing order of code
64sig§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):

codethresholdsvalues
1 blackluma_max u8 · ratio_ppm u32 · min_ms u32frames u32 · matched u32 · longest_ms u64 · longest_at_ms u64
2 freezemad_milli u32 · min_ms u32frames u32 · matched u32 · longest_ms u64 · longest_at_ms u64
3 silencepeak_max u16 · min_ms u32blocks u32 · matched u32 · longest_ms u64 · longest_at_ms u64
4 captionsmax_gap_ms u32seen u32 · last_ms u64 · longest_gap_ms u64
5 cuesmax_gap_ms u32seen u32 · last_ms u64 · longest_gap_ms u64
6 transporttable_max_gap_ms u32 · pid_max_gap_ms u32packets 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.

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 + 10b, 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:

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:

reasonraised 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/ holds the corpus, written from fixed seeds by wai_signal_health_vectors gen as vectors.json. It has:

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