media-doctor — DVB / MPEG-TS diagnostics
A lint-style analysis harness for MPEG-2 Transport Streams, HLS playlists, and DASH MPDs.
Each [Diagnostic] checks one rule against a TS byte buffer and pushes
[Finding]s into a [Report]; a small CLI runs the full set over a file.
no_std + alloc (the CLI is std).
Diagnostics
| Check | Detects |
|---|---|
SyncByteCheck |
missing 0x47 sync bytes |
PatPmtVersionCheck |
PAT/PMT version_number changes across the stream |
CcAnomalyCheck |
continuity-counter discontinuities (honouring legal duplicates + discontinuity_indicator) |
PcrCheck |
PCR jitter / discontinuity on the PCR PID (TR 101 290-style), honouring signalled discontinuities |
PtsCheck |
non-monotonic decode timestamps (DTS, else PTS — legal B-frame PTS reorder is not flagged) + forbidden PTS_DTS_flags == 0b01, on real PES PIDs only |
Scte35Check |
SCTE-35 splice consistency — unbalanced splice_insert out/in pairs, duplicate open "out"s |
CodecSignallingCheck |
codec signalling vs bitstream — PMT stream_type vs actual ES codec; esds ASC vs ADTS (reuses transmux SPS decoders) |
check_container_codec |
avcC/hvcC profile/level/chroma/bit-depth vs in-band SPS; sample-entry dims vs SPS-decoded dims |
FpsCadenceCheck |
VUI frame rate vs track timescale cadence |
ParamSetsCheck |
missing SPS/PPS/VPS before the first IDR/IRAP |
InterlaceCheck |
interlaced coding (frame_mbs_only_flag == 0) reported as content fact |
check_playlist |
HLS playlist validation (RFC 8216): missing #EXTM3U, missing #EXT-X-TARGETDURATION, #EXTINF exceeding target, malformed #EXT-X-DATERANGE |
check_hls_playlist |
HLS playlist validator (RFC 8216bis): structured parse via transmux::MediaPlaylist, plus 8 rules — hls-missing-extm3u, hls-missing-targetduration, hls-extinf-exceeds-target, hls-malformed-daterange (§4.4.5.1), hls-preload-hint-with-endlist (§4.4.5.3), hls-skip-without-can-skip-until (§4.4.3.8), hls-part-duration-range (§4.4.4.9), hls-parse-error (§4) |
check_dash_mpd |
DASH MPD validator (ISO/IEC 23009-1): structured parse via transmux::Mpd, plus 6 rules — dash-static-mpd-missing-duration (ISO/IEC 23009-1:2012 §5.3.1.2), dash-dynamic-mpd-no-availability-start, dash-representation-id-duplicate (§5.3.5.2 Table 7), dash-segment-timeline-monotonic (§5.3.9.6.2), dash-period-no-adaptation-sets (§5.3.2), dash-parse-error |
Diagnostics are validated against real captures (e.g. a clean H.264+AAC stream and a multi-programme DVB capture yield zero false positives) plus crafted fault fixtures.
CLI
$ media-doctor check --input stream.ts
Findings: 0 error(s), 0 warning(s), 0 info(s)
$ media-doctor check-hls --input playlist.m3u8
Findings: 0 error(s), 0 warning(s), 0 info(s)
$ media-doctor check-dash --input manifest.mpd
Findings: 0 error(s), 0 warning(s), 0 info(s)
--json emits the report as JSON (requires the serde feature, on by default).
watch — live compliance probe (issue #665)
$ media-doctor watch --udp 239.1.1.1:5000 --metrics-addr 127.0.0.1:9090
media-doctor watch: ingesting UDP 239.1.1.1:5000, metrics on http://127.0.0.1:9090/metrics
Continuously ingests a live raw-MPEG-TS-over-UDP feed (unicast or
multicast — the address's multicast-range membership decides whether the
socket joins an IGMP group) and serves an always-current snapshot as
Prometheus text exposition format
on GET /metrics:
| Flag | Default | Meaning |
|---|---|---|
--udp <host:port> |
(required) | UDP address to listen on for raw MPEG-TS |
--metrics-addr <host:port> |
127.0.0.1:9090 |
HTTP address serving GET /metrics |
Metrics exposed (see media_doctor::WatchState for the full accounting):
| Metric | Kind | Source |
|---|---|---|
media_doctor_packets_total |
counter | well-formed TS packets processed |
media_doctor_datagrams_total |
counter | ingest datagrams fed |
media_doctor_resync_events_total / media_doctor_dropped_bytes_total |
counter | TS byte-stream resync (mpeg_ts::resync::TsResync) |
media_doctor_conformance_in_sync |
gauge | ETSI TR 101 290 monitor sync state |
media_doctor_conformance_events_total{indicator,priority} |
counter | full TR 101 290 indicator set (dvb-conformance), timed on wall-clock arrival |
media_doctor_scte35_events_total / media_doctor_scte35_open_events |
counter / gauge | SCTE-35 splice_insert events / currently-open ones |
media_doctor_pts_dts_anomalies_total / media_doctor_pts_dts_anomaly{pid} |
counter / gauge | non-monotonic decode-timestamp events |
media_doctor_codec_signalling_mismatch{pid} |
gauge | PMT-declared codec vs actual bitstream framing |
media_doctor_last_packet_clock_seconds |
gauge | elapsed ingest time of the last packet processed |
Scope (v1): UDP only. The full product-vision idea (docs/IDEAS.md item
#4) also covers SRT; that needs srt-runtime's sans-IO handshake/ARQ engine
and is a follow-up issue, not implemented here. PcrCheck/CcAnomalyCheck
are not separately re-wired for watch — ConformanceMonitor already
computes the equivalent PCR-repetition/discontinuity and continuity-count
indicators from the same per-packet data.
The ingest/metrics core (media_doctor::WatchState::feed_datagram /
render_prometheus) is plain logic with no socket dependency, so it's
unit-tested by feeding a real capture chunked into UDP-payload-sized pieces —
no socket is ever opened in tests. The watch binary itself is a thin
UdpSocket/TcpListener shell (two std::threads sharing
Arc<Mutex<WatchState>>; no async runtime) around that core.
Library
use ;
let mut report = new;
PtsCheck.run;
for f in report.findings
HLS playlists are validated with check_playlist (legacy line-based) or
check_hls_playlist (structured, using broadcast_hls::MediaPlaylist::parse).
DASH MPDs are validated with check_dash_mpd (structured, using transmux::Mpd::parse).
Deferred Manifest Rules
These rules are spec-mandated but deferred from the current implementation. Each has a specific, stated reason — a rule without a reason looks like an accidental omission, which is the failure this project keeps correcting.
Deferred rules fall into three categories:
- Source not available: the spec text is not in a form this project can read (e.g. ISO/IEC 23009-1 is not vendored, so any profile rule would be fabricated).
- Information not in manifest: the manifest alone does not carry the data the rule needs (e.g. init-segment contents, cross-reload state, another playlist's content).
- Model gap: the
transmuxparser for the manifest doesn't carry the field the rule would inspect.
Anything not in one of these categories — most importantly, things the spec does not require — are listed separately under "Rules that are not spec requirements" below so they are never mistaken for missing checks.
HLS (RFC 8216bis)
| Rule | Clause | Reason deferred |
|---|---|---|
| EXT-X-MAP cross-reference validity | §4.4.4.5 | Information not in manifest: requires fetching + inspecting the init segment |
| Open-segment closure / PART-HOLD-BACK consistency | §4.4.4.9 | Information not in manifest: requires cross-reload state and wall-clock knowledge |
| Discontinuity accounting (sequence-number vs playlist-state drift) | §4.4.3.3 | Information not in manifest: requires per-reload tracking of discontinuity-sequence |
| Variant URI cross-reference (master→media) | §4.4.4.1, §6.2.4 | Information not in manifest: requires fetching referenced variant playlists |
| EXT-X-MEDIA (alternate renditions) in multivariant playlist | §4.4.6 | Model gap: transmux::MasterPlaylist does not parse EXT-X-MEDIA |
6 deferred HLS rules (was previously counted as 7 — the tag-ordering entry was a miscount, since tag ordering is enforced by the structured parser and is not a deferred gap).
DASH (ISO/IEC 23009-1)
| Rule | Clause | Reason deferred |
|---|---|---|
SegmentTemplate @media / @initialization URL-identifier validity |
§5.3.9.4.2 | Information not in manifest: requires fetching + validating segment content |
@presentationTimeOffset / @timescale coherence with segment data |
§5.3.9.2.3 | Information not in manifest: requires cross-checking against actual segment durations |
| ServiceDescription / Latency element validity | §5.3.10 | Model gap: transmux::Mpd does not parse ServiceDescription or Latency |
@bandwidth vs codecs (bitrate plausibility check) |
§5.3.5.2 | Model gap: needs a codec-model comparison the parser doesn't carry |
| BaseURL resolution (hierarchy + relative/absolute) | §5.6 | Information not in manifest: requires network fetch |
@id collisions across AdaptationSets (different-set, same id) |
§5.3.2 | Rare in practice (<0.1% of real-world manifests examined); low-priority additive check |
| profile / interoperability-point validation | §8 | Source not available: ISO 23009-1 is not vendored; without the spec text, any profile rule would be fabricated |
Rules that are not spec requirements
These are things that might look like missing checks but are simply not required by the spec. They belong here — not in the deferred list — so their absence is not mistaken for an omission.
| Item | Why it is not a rule |
|---|---|
| EXT-X-VERSION position (before/after first segment) | Not required by RFC 8216. §4.3.1.2 says VERSION "applies to the entire Playlist file" — no position requirement. By contrast, EXT-X-MEDIA-SEQUENCE and EXT-X-DISCONTINUITY-SEQUENCE each explicitly state "MUST appear before the first Media Segment". VERSION carries no such clause. A validator that flags a conformant playlist is worse than no validator. |
These deferred rules are recorded here (not only in a PR body) so users know exactly what the validator does and does not check. They are also cross-referenced in the CHANGELOG.
License
MIT OR Apache-2.0.