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,okorfault. 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 thesignal_healthmodule ofwai-rs(featuresignal_health); the corpus islive-conformance/signal-health/, and its second implementation islive-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 writes one.
- Window: the instants from
from_msup 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) 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.
| 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
- and its
width × heightluma samples. Their instants are strictly increasing and within the window. Their digest is overwai: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 atto_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 mostmad_millithousandths 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 + 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:
- Continuity (1.4
cc). A packet with a payload is held to the lastccof its PID’s packets with a payload, if there is one. With its discontinuity indicator set, it is taken as it is. Otherwise, accequal 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 accthat 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 lastccbecomes this packet’s. A packet without a payload is not held to it and leaves it as it was. - 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 thanpid_max_gap_msafter the anchor counts one event; the anchor becomes its arrival. - PID 0 (1.3.a
pat). On PID 0, a packet whosescramblingis not 0 counts one event. A packet withpusiand 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 oftable_id0x00 there is a program association section: arriving more thantable_max_gap_msafter the PAT anchor, which starts atfrom_ms× 1000, it counts one event, and the anchor becomes its arrival. A section of anothertable_idthere, but 0xFF (stuffing), counts one event. A program association section whosesection_lengthis at least 9 and which ends within its packet (its 3 header bytes andsection_lengthbytes) 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 whoseprogram_numberis 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. - A program map PID (1.5.a
pmt). On a program map PID, a packet whosescramblingis not 0 counts one event. A section oftable_id0x02 that starts in it, as on PID 0, is a program map section: arriving more thantable_max_gap_msafter the PID’s anchor, it counts one event, and the anchor becomes its arrival. One whosesection_lengthis 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 itsprogram_info_lengthbytes of descriptors, each entry of 5 bytes and itsES_info_lengthbytes, 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:
- its magic is not
WSH1or its version not 1; name_lenis 0 or above 64, orinputholds a byte other than those §4.1 allows;from_msis not belowto_ms, orto_msis above 2^53 − 1;mis 0 or above 6, acodeis not 1 to 6, or the codes are not strictly increasing;- a threshold is out of its range:
ratio_ppm1 to 1 000 000,peak_maxat most 32 768, everymin_ms,table_max_gap_msandpid_max_gap_msat least 1; - a value cannot be one §5 computes:
matchedaboveframesorblocks;longest_msaboveto_ms−from_ms;matched0 andlongest_msorlongest_at_msnot 0, ormatchednot 0 andlongest_ms0 orlongest_at_msoutside the window;seen0 andlast_msnot 0 orlongest_gap_msnot the window’s length, orseennot 0 andlast_msoutside the window;longest_gap_msabove the window’s length; - a
verdictis 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:
- 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; - the media’s digest is the check’s
media, orsignal_health_media_mismatch; - 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: itsto_msis after t;superseded: another record of its input that stands this far has a laterto_ms;contradicted: another record of its input with the sameto_mshas 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/ 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
okover 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.