Repository navigation
feat(mux): report TS health and the export's release clock #5849
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c95a662
2c66df3
9974c23
58ffb52
65cf0cf
90c4fbf
eafd7df
c97bc23
3bbadc0
39fa8f5
8a0af79
7bc5705
f38b261
0f0d474
d3c4b72
9c56ab2
bf2d693
4b1952a
4430821
070338f
1bc982c
602fb36
bda2cf4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,64 @@ it reads and records its newest arrival. See the | |
| [hang draft](/draft/moq-hang#media-stats) for track framing, field units, and | ||
| the feedback convention. | ||
|
|
||
| ### MPEG-TS sections | ||
|
|
||
| A TS import or export adds sections of its own, which | ||
| `moq_mux::container::ts::stats::Report` reads. As above, counters are | ||
| cumulative and omitted at zero, and a gauge is omitted until it is measured. | ||
|
|
||
| `mpegts` holds `streams`, one row per elementary stream keyed by its PID in | ||
| decimal, and the stream-wide counters. A row has `track` (the track suffix), | ||
| `class` (`audio`, `video`, or `data`; an unknown class reads as `data`), | ||
| `units` (access units delivered), and `quiet`, a gauge: milliseconds of | ||
| program clock since its last access unit. Its losses are `resyncs`, bytes | ||
| `discarded` scanning for audio sync, `unconfirmed` frames, `damaged` units, | ||
| `unplaced` units the program clock could not place, the times the stream was | ||
| `refused`, and its shares of `continuityCountError`, `transportError`, and | ||
| `ptsError`. | ||
|
|
||
| The stream-wide counters count ETSI TR 101 290 checks at that standard's | ||
| fixed limits: | ||
|
|
||
| | Field | Check | | ||
| | --- | --- | | ||
| | `tsSyncLoss` | 1.1 `TS_sync_loss` | | ||
| | `syncByteError` | 1.2 `Sync_byte_error` | | ||
| | `patError` | 1.3 `PAT_error_2` | | ||
| | `continuityCountError` | 1.4 `Continuity_count_error` | | ||
| | `pmtError` | 1.5 `PMT_error_2` | | ||
| | `transportError` | 2.1 `Transport_error` | | ||
| | `crcError` | 2.2 `CRC_error`, on the PAT and PMT only | | ||
| | `pcrRepetitionError` | 2.3a `PCR_repetition_error` | | ||
| | `pcrDiscontinuityIndicatorError` | 2.3b `PCR_discontinuity_indicator_error` | | ||
| | `ptsError` | 2.5 `PTS_error` | | ||
|
|
||
| An import grades the stream as it arrived. An export builds every header | ||
| itself and runs no checks, so its counters stay zero and only its rows' | ||
| `units` and `quiet` move. Neither grades the wire a receiver sees downstream. | ||
| The PCR checks are graded on PCR values, not arrival: they speak for the | ||
| encoder's insertion, since the network re-times arrival, and a forward step | ||
| over 100 ms counts as both. 1.6 `PID_error` is covered, more strictly, by each | ||
| row's `units` and `quiet`: a dead video path behind a live mux still sends | ||
| PCR-only packets on its PID, which a packet count reads as live. | ||
| `PCR_accuracy_error` is not measured. No verdict is computed: a sparse stream | ||
| such as SCTE-35 is quiet for seconds by design, so an alarm reads counter | ||
| rates and `quiet` against its own thresholds. | ||
|
|
||
| An export also reports `release`, how it keeps up with the source: what it | ||
| drops, trims and never receives, and how its output clock tracks the | ||
| source's. `export ts --passthrough` reports it without `mpegts`. | ||
|
|
||
| | Field | Meaning | | ||
| | --- | --- | | ||
| | `dropped` | Frames lost from the output after their stream began: missed their release deadline, or a video track dropped them waiting for its next keyframe. | | ||
| | `trimmed` | Frames dropped before their stream's first went out, so the output starts on a whole group led by a keyframe. Nothing is missing for these. | | ||
| | `gaps` | Times the source skipped media it will never deliver: a group lost or expired upstream, or a resubscription. A declared break is not one. | | ||
| | `margin` | Gauge, milliseconds rounded away from zero: the least time any frame arrived before its deadline over the last second. Negative when one missed, however narrowly. | | ||
| | `offset` | Gauge, milliseconds rounded away from zero: how far the output clock sits from the source's plus the delay. Positive holds frames longer than the delay. The clock pulls it shut over ten minutes. | | ||
| | `drift` | Gauge, parts per million: the source clock's rate against ours, positive running fast. | | ||
|
Comment on lines
+95
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With AGENTS.md reference: doc/AGENTS.md:L9-L9 Useful? React with 👍 / 👎. |
||
| | `outOfTolerance` | Measurements that found the source clock past the 30 ppm ISO/IEC 13818-1 allows, which the output cannot follow. | | ||
|
|
||
| ## Broadcasts | ||
|
|
||
| Each node publishes under a prefix, `.stats` by default, which | ||
|
|
||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -606,6 +606,12 @@ async fn decode( | |
| // they have to surface as they accumulate rather than at exit. | ||
| let mut log = ts::stats::Log::default(); | ||
| let mut sampled = tokio::time::Instant::now(); | ||
| let sections = report.as_ref().map(crate::report::Stats::sections); | ||
| let report_mpegts = |snapshot: &ts::stats::Snapshot| { | ||
| if let Some(sections) = §ions { | ||
| sections.set(|report| report.mpegts = Some(snapshot.clone())); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a TS feed rotates its PMT through enough elementary PIDs, Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in bda2cf4 by sizing the cap to what this producer can publish rather than evicting rows. An import keeps one row per PID, and PIDs are 13 bits, so a report holds at most 8,192 rows. (Written by Claude Opus 5.5) |
||
| } | ||
| }; | ||
|
|
||
| // Run the read/decode loop so an error surfaces here rather than | ||
| // dropping the decoder (and its tracks) with a bare Error::Dropped. | ||
|
|
@@ -626,6 +632,7 @@ async fn decode( | |
| if sampled.elapsed() >= ts::stats::Log::INTERVAL { | ||
| sampled = tokio::time::Instant::now(); | ||
| if let Some(stats) = decoder.stats() { | ||
| report_mpegts(&stats); | ||
| log.sample(stats); | ||
| } | ||
| } | ||
|
|
@@ -656,6 +663,7 @@ async fn decode( | |
| // The drain at end of input can publish a frame nothing vouched for, so the | ||
| // final snapshot is only complete after `finish`. | ||
| if let Some(stats) = decoder.stats() { | ||
| report_mpegts(&stats); | ||
| log.finish(&stats); | ||
| } | ||
| match outcome { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new section enumerates the
mpegtsandreleasewire shape, including PID-key encoding and individual fields, even though scopeddoc/AGENTS.mdrequires feature docs to leave field lists and wire encodings to API docs or drafts. Keeping a second schema copy here will drift from the serde types, so retain the user-facing monitoring behavior and link to a colocated reference instead.AGENTS.md reference: doc/AGENTS.md:L4-L6
Useful? React with 👍 / 👎.