From 2c2a640a0ad1d2f2ca23cf8ac00201875224e6b2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 17:20:02 -0800 Subject: [PATCH 1/4] chore(quest): claim quest/m1/ts-reliability/README.md Quest-Claim: 384e9fa6e8c86072 From 3d72ce2c7b36fe3ff16c604aaf855a233610582c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 17:27:44 -0800 Subject: [PATCH 2/4] docs(ts): an MPEG-TS guide with the evidence behind each claim Adds doc/bin/ts.md: which mode to choose, what the semantic import carries and refuses, passthrough's admitted-interval identity, acquisition, loss, and a coverage table linking every claim to its automated test and CI cadence, with unmeasured cells listed. The TS paragraphs move out of the CLI page rather than being repeated. Finishes quest/m1/ts-reliability. Co-Authored-By: Claude Opus 5.5 --- doc/.vitepress/config.ts | 5 +- doc/bin/cli.md | 88 +-------- doc/bin/srt.md | 8 +- doc/bin/ts.md | 275 +++++++++++++++++++++++++++ doc/concept/hang.md | 2 +- doc/lib/rs/moq-mux.md | 4 +- quest/m1/README.md | 1 - quest/m1/ts-reliability/README.md | 126 ------------ quest/m3/release/feature-review.md | 2 +- quest/m3/ts-multi-program-restart.md | 6 +- test/ts/README.md | 4 +- 11 files changed, 295 insertions(+), 226 deletions(-) create mode 100644 doc/bin/ts.md delete mode 100644 quest/m1/ts-reliability/README.md diff --git a/doc/.vitepress/config.ts b/doc/.vitepress/config.ts index d5134cdd50c..7214cd3627f 100644 --- a/doc/.vitepress/config.ts +++ b/doc/.vitepress/config.ts @@ -167,7 +167,10 @@ export default defineConfig({ { text: "moq-cli", link: "/bin/cli", - items: [{ text: "Inspect a relay", link: "/bin/inspect" }], + items: [ + { text: "MPEG-TS", link: "/bin/ts" }, + { text: "Inspect a relay", link: "/bin/inspect" }, + ], }, { text: "moq-studio", link: "/bin/studio" }, { diff --git a/doc/bin/cli.md b/doc/bin/cli.md index 9e5a5de42d2..5a46af26322 100644 --- a/doc/bin/cli.md +++ b/doc/bin/cli.md @@ -93,68 +93,9 @@ moq --connect https://relay.example.com/rooms/1 --connect-token "$TOKEN" --broad The `ts`, `fmp4`, and `flv` imports publish the input's own timestamps, mapped to the wall time the first frame arrived. A timeline that rewinds, such as a restarted encoder or a looping file wrapping to the top, ends the import with -an error; run it again to publish anew. `import ts` and `import srt` republish -instead when an MPEG-TS clock steps onto another timeline: the PCR and every -stream stepping back, flagged or not, or more than 10 s forward without a -`discontinuity_indicator`, and when a PAT names another `transport_stream_id` -or program number. The broadcast finishes there, and the rest of the -input publishes as a new stream under a new -[instance](/concept/moq-lite#publisher-instances), on the same SRT connection, which -`export ts --stitch --follow` carries on into. The new stream carries nothing -over from the old one's PAT and PMT: it waits for the source to repeat them and -for its next keyframe, so a switch whose tables went out just before its clock -stepped announces up to one GOP later. The new stream keeps the mux -rate the old one measured, so its catalog goes out with its first media rather -than after a fresh two-second measurement. An unflagged step usually means -redundant encoders behind a changeover switch: time-lock their clocks, or have -the switch set `discontinuity_indicator`. `--program all` and `--passthrough` -still end at a rewind, and `--program all` at a changed identity. A flagged MPEG-TS discontinuity that -jumps forward continues the broadcast and is declared on the exported clock. -MPEG-TS timestamps follow their source history across the 33-bit wrap -(every 26.5 hours), including files imported faster than real time and flagged -jumps, so a jump of more than half a wrap (13.25 hours) can read as a rewind. - -For a source already in MPEG-TS, `ffmpeg -c copy -f mpegts` re-muxes and drops PIDs -(NIT, TDT/TOT, extra audio, teletext, SCTE-35), so pipe its bytes instead, for example -`tsp -I ip 239.1.2.3:1234 -O file - | moq --connect https://relay.example.com/anon --broadcast my-stream.hang import ts`, -or use [`import srt`](/bin/srt). - -MPEG-TS import carries H.264/H.265 and AAC/MP2/AC-3/E-AC-3/Opus, and passes -SCTE-35, DSM-CC sections, and every other stream it does not decode (MPEG-2 -video, LATM AAC, KLV, subtitles, DVB AC-3) through as tracks. MPEG-2 video still -paces the program by its timestamps, and a subscriber joins it at a sequence -header. H.264/H.265 AUs can span PES, and a PES can contain several. Supply -a PTS for each AU or declare a proportional POC clock: AVC needs -`fixed_frame_rate_flag` and `temporal_poc_flag` in the TS timing descriptor plus -fixed-rate SPS VUI; HEVC needs VUI's `vui_poc_proportional_to_timing_flag`. Missing timing -ends the import instead of repeating a timestamp. Private and undecoded PES go through whole, at their place on the program clock -(PCR); one before the first PCR is dropped. A stream it cannot carry unaltered -is refused with a warning: any in a program with no PCR, and one whose PES carry -an ESCR, pack header or TREF, are scrambled, leave a non-video PES unbounded, -or have a malformed header, as is `0x86` without a SCTE-35 registration, which -may be DTS audio. A PID no table claims, such as conditional access or ATSC -PSIP, is dropped with one warning and counted in the -[stats](/concept/stats#mpeg-ts-sections); `--passthrough` keeps them. -SCTE-35 cues and private PES share the media clock, or anchor on the first PCR -when the PMT declares only data. Audio-only and data-only programs round-trip -without video; a data-only program still requires PCR. A program declaring -media waits for its timestamp and is refused if input ends or declares a clock -break before one arrives. -PES and untimed section staging share a bounded budget, so a source that never -supplies a PES boundary or clock fails rather than retaining bytes indefinitely. -It takes one program: a multi-program -stream is refused until `--program` picks one, or `--program all` publishes -each as its own broadcast (`event.hang` becomes `event/1.hang`, -`event/2.hang`), following the PAT: a program it adds publishes, one it drops -ends, and one that returns publishes as a new instance. A listed program whose -PMT does not arrive within 0.5 s of program clock ends the import. A damaged packet is -dropped on its own PID, so video freezes until its next keyframe while the -other tracks carry on. The importer also -logs feed health (stalled streams, TR 101 290 errors) without changing what it -publishes. `--passthrough` publishes the multiplex whole instead, every packet -in order on one [`m2ts`](/concept/hang#transport-streams) track with every -program aboard, for what demultiplexing cannot carry, such as a scrambled -service; `export ts --passthrough` writes it back out. FLV accepts the same +an error; run it again to publish anew. `import ts` instead republishes when an +MPEG-TS clock steps onto another timeline. The [MPEG-TS guide](/bin/ts) covers +what it carries, `--program`, `--passthrough`, acquisition, and loss. FLV accepts the same legacy and enhanced-RTMP codecs as the [RTMP gateway](/bin/rtmp). fMP4 import splits each fragment into one frame per sample, so players need no @@ -224,18 +165,7 @@ moq ... export mp4 --output recording.mp4 moq repair mp4 recording.mp4 ``` -MPEG-TS export pads to the source's constant mux rate when the catalog -recorded one, or to `--mux-rate`, on a constant-rate schedule an IRD or groomer -can lock to. Each frame goes out as early as the receiver's buffers admit, up -to `--delay` ahead of its decode time and no further than its decoder buffer -holds. That send-ahead comes out of the delay, so the output trails a TS source -by `--delay`. A frame that cannot arrive in time at the rate fails the export, -as does video whose HRD declares a longer buffer delay than `--delay`; the -error names the `--delay` it needs. A clock jump flags its first PCR without -changing the PMT version. Padding keeps PCR values on a continuous grid; after -an outage exhausts the buffered media, overdue padding can arrive in a catch-up -burst when media resumes. Raw -`--passthrough` keeps surviving packets unchanged, including across loss. +MPEG-TS export is covered in the [MPEG-TS guide](/bin/ts#export). ## Play @@ -554,14 +484,8 @@ A timeline that steps further than the delay absorbs with no restart marker, as a changeover between two encoders whose clocks disagree, re-acquires the clock as at startup rather than read the step as drift. -`export ts --passthrough` reads the track an `import ts --passthrough` published -and writes its packets back out unchanged, from its first group on, each run -between two PCRs `--delay` after the first one's time on the source clock. The -output is exactly as conformant as the source, and two exporters emit the same -bytes. A run that arrives after its deadline is dropped and counted, leaving a -continuity error, and the rest carry on. It takes neither `--follow`, -`--stitch` nor `--mux-rate`. Only the hang catalog names the track, so an -`.msf` broadcast needs `--catalog-format hang`. +`export ts --passthrough` writes the source's packets back out unchanged; see +[passthrough](/bin/ts#passthrough-mode). A stdout export ends when its catalog ends and its media has drained, so finishing only the media tracks leaves it waiting for catalog updates. diff --git a/doc/bin/srt.md b/doc/bin/srt.md index 42884ba65fd..eca541716c4 100644 --- a/doc/bin/srt.md +++ b/doc/bin/srt.md @@ -8,7 +8,7 @@ description: SRT contribution and playback `moq import srt` accepts SRT pushes (`--listen`) or pulls from a remote SRT source (`--connect`); `moq export srt` serves SRT to players or pushes to a remote. The payload is MPEG-TS, so the same codecs and behavior as -[`import ts`](/bin/cli) apply: H.264/H.265 video and AAC, MP2, AC-3, E-AC-3, or +[`import ts`](/bin/ts) apply: H.264/H.265 video and AAC, MP2, AC-3, E-AC-3, or Opus audio. A damaged unit is dropped and the session stays up; video resumes at its next keyframe, freezing for up to one GOP. @@ -33,10 +33,8 @@ return or a replacement on the same connection, as they do for [`export ts`](/bin/cli#retention-and-latency). A multi-program feed is refused unless `--program` -picks one: `--program 2` imports program 2 alone, and `--program all` -publishes each program as its own broadcast (`event.hang` becomes -`event/1.hang`, `event/2.hang`, and so on), following the PAT as programs -join and leave. +picks one, as for [`import ts`](/bin/ts#programs-and-changes). SRT has no +passthrough mode. ```bash moq --connect https://relay.example.com/anon --broadcast event.hang import srt --listen '[::]:9000' --program all diff --git a/doc/bin/ts.md b/doc/bin/ts.md new file mode 100644 index 00000000000..954a84ee4d2 --- /dev/null +++ b/doc/bin/ts.md @@ -0,0 +1,275 @@ +--- +title: MPEG-TS +description: Carry a transport stream through MoQ, demultiplexed or whole, and what each mode guarantees +--- + +# MPEG-TS + +`moq import ts` reads a transport stream on stdin and `moq export ts` writes one +to stdout. [SRT](/bin/srt) carries the same import and export over the network. +There are two modes, and they make different promises. + +```bash +# Semantic (default): demultiplex into tracks, rebuild a TS on the way out +moq --connect https://relay.example.com/anon --broadcast event.hang import ts < feed.ts +moq --connect https://relay.example.com/anon --broadcast event.hang export ts > out.ts + +# Passthrough: carry the multiplex whole, write the same packets back out +moq ... --broadcast event.hang import ts --passthrough < feed.ts +moq ... --broadcast event.hang export ts --passthrough > out.ts +``` + +## Choose a mode + +| | Semantic | Passthrough (`--passthrough`) | +| --- | --- | --- | +| On the wire | One track per elementary stream, plus the PSI and SI the import keeps, in the [`mpegts`](/draft/moq-mpegts) catalog section | One [`m2ts`](/concept/hang#transport-streams) track of whole 188-byte packets | +| Output | A rebuilt TS: same media, programs, descriptors and kept SI; new packetization, PCR and table cadence | The source's packets, byte for byte, from the first group on | +| Browsers and `moq play` | Watch it | Have nothing to decode | +| A relay | Drops and prioritizes per track | Caches and drops whole runs of packets | +| Scrambled services, CA, unlisted PIDs | Refused and counted | Carried | +| Multi-program | One program per broadcast (`--program`) | The whole MPTS in one broadcast | +| Loss | Video resumes at its next keyframe; the clock and tables stay valid | Surviving packets go out unchanged; the gap stays a gap | +| Buffer conformance | Rebuilt to the [T-STD](#evidence) for the supported codecs | Exactly as conformant as the source | + +Use semantic unless you need the exact packets: a scrambled service, a stream +the importer refuses, or downstream gear that must see the source's own +multiplex. Byte identity is not the semantic mode's contract. + +For a source that is already MPEG-TS, pipe its bytes rather than re-muxing: +`ffmpeg -c copy -f mpegts` drops PIDs (NIT, TDT/TOT, extra audio, teletext, +SCTE-35). For example, +`tsp -I ip 239.1.2.3:1234 -O file - | moq ... import ts`. + +## Semantic mode + +### What it carries + +The import decodes H.264/H.265 video and AAC (ADTS), MP2, AC-3, E-AC-3, and +Opus audio into renditions any player can watch. Everything else it keeps goes +through without being decoded: + +| Source | Carried as | +| --- | --- | +| H.264, H.265 | Video renditions, with each PID and its descriptors | +| AAC (ADTS), MP2, AC-3, E-AC-3, Opus | Audio renditions. The export labels Opus mono, stereo, or 3 to 8 channels in Vorbis order, and refuses other mapping families | +| MPEG-2 video, LATM AAC, KLV and other metadata, DVB AC-3, subtitles, any other PES | Whole PES at their place on the program clock (PCR), PTS and DTS rebased. MPEG-2 video still paces the program, and a subscriber joins it at a sequence header | +| SCTE-35, DSM-CC, private sections | Whole sections; SCTE-35 `pts_adjustment` rebased | +| DVB SI on PIDs 0x0010, 0x0011, 0x0012, 0x0014 | A snapshot track per PID and `table_id`; pending (`current_next_indicator` 0) sections are dropped | +| Program identity, program and ES descriptors | Recorded in the catalog and followed through each PMT revision | + +It refuses, with a warning and a `refused` count in the +[stats](/concept/stats#mpeg-ts-sections): a scrambled stream; PES carrying an +ESCR, pack header or TREF; a non-video PES with no length or a malformed +header; `0x86` without a SCTE-35 registration, which may be DTS audio; and any +stream in a program with no PCR. A PID no table claims, such as the CAT, conditional access or ATSC +PSIP, is dropped with one warning and counted the same way. + +H.264/H.265 access units can span PES, and one PES can hold several. Each +access unit needs a PTS, or a declared proportional POC clock: AVC needs +`fixed_frame_rate_flag` and `temporal_poc_flag` in the TS timing descriptor +plus fixed-rate SPS VUI, and HEVC needs VUI's +`vui_poc_proportional_to_timing_flag`. Missing timing ends the import rather +than repeat a timestamp. Sections and whole PES take their place from the +media clock, or from the first PCR when the PMT declares only data; one before +the first PCR is dropped. Audio-only and data-only programs work too; a +data-only program still needs a PCR. A program declaring media waits for its +first timestamp and is refused if the input ends or breaks its clock first. +PES and untimed sections share a bounded staging budget, so a source that never +supplies a PES boundary or a clock fails rather than grow without bound. + +### Programs and changes + +A broadcast holds one program. A multi-program stream is refused until +`--program 2` picks one, or `--program all` publishes each as its own +broadcast (`event.hang` becomes `event/1.hang`, `event/2.hang`), following the +PAT: a program it adds publishes, one it drops ends, and one that returns +publishes as a new [instance](/concept/moq-lite#publisher-instances). A listed +program whose PMT does not arrive within 0.5 s of program clock ends the import. + +A PMT that adds, removes, or retypes a stream, or reuses a PID, ends the old +track and continues under a fresh name; the export announces each layout at its +next PMT `version_number`. A PAT that moves the PMT is followed, and the export +moves its own PMT behind a new PAT. + +The import republishes as a new instance when the source moves onto another +timeline: the PCR and every stream stepping back, flagged or not, a step more +than 10 s forward without `discontinuity_indicator`, or a PAT naming another +`transport_stream_id` or program number. The new stream carries nothing over +from the old PAT and PMT, so it announces once the source repeats them and its +next keyframe arrives, up to a GOP later; it keeps the measured mux rate. +`export ts --stitch --follow` carries on into it (see +[retention](/bin/cli#retention-and-latency)). An unflagged step usually means +redundant encoders behind a changeover switch: time-lock their clocks, or have +the switch set `discontinuity_indicator`. A flagged jump forward continues the +broadcast and is flagged on the exported PCR. `--program all` still ends at a +rewind or a changed identity. Timestamps follow their history across the 33-bit +wrap (every 26.5 hours), so a jump of more than half a wrap reads as a rewind. + +### Export + +The export rebuilds PES headers, packets, the PCR (on the first video PID, else +audio, else a PCR-only PID), and PAT/PMT, every 500 ms and at each keyframe, +with SI at the DVB maxima. It pads to the source's constant mux rate when the +catalog recorded one, or to `--mux-rate`, on a schedule an IRD or groomer can +lock to, with each PCR within 500 ns of its byte position. A variable-rate +source exports unpadded, with no mux-rate or system-buffer claim. + +Each frame goes out as early as the receiver's buffers admit, up to `--delay` +(default 1 s) ahead of its decode time, so the output trails the source by +`--delay`. A frame that cannot arrive in time at the rate fails the export, as +does video whose HRD declares a longer buffer delay than `--delay`; the error +names the delay it needs. + +## Passthrough mode + +`import ts --passthrough` publishes every packet, in input order, on one +track. It reads the PAT and PMT to find the clock and the video stream and +nothing else past the adaptation field, so a scrambled multiplex groups and +paces like its clear twin. It takes the whole stream, SPTS or MPTS: it never +extracts a program, so it refuses `--program`. To carry programs separately, +import each as its own SPTS into its own broadcast. 192- and 204-byte packets +are refused rather than reframed. `--pcr-pid` paces on another PID's PCR. + +Each frame is the run of packets from one PCR on the clock PID to the next, +stamped with the PCR time of its first byte. A group starts at a +`random_access_indicator` on the video PID (or the clock PID with no video), at +a flagged PCR jump forward, or after a second without either. Any step back +ends the import. + +`export ts --passthrough` writes each run `--delay` after its time on the +source clock, so two exporters emit the same bytes. It takes neither +`--follow`, `--stitch` nor `--mux-rate`. Only the hang catalog names the +track, so an `.msf` broadcast needs `--catalog-format hang`. The encoding is in +[draft-lcurley-moq-mpegts](/draft/moq-mpegts). + +## Acquisition + +An importer joining a live feed waits for what it needs before publishing. + +- **Semantic:** the import waits for the PAT, PMT, and a PCR, and holds the + catalog until two seconds of PCR have measured a constant mux rate or shown + the input to be variable-rate. A subscriber joins at a keyframe, and the + export puts a PAT and PMT ahead of it. From there a late exporter writes the + same packets as a running one, but for continuity counters. +- **Passthrough:** nothing is published until the PAT, the clock program's + PMT, a PCR, and a group start. Bytes before that, and bytes off the 188-byte + grid, are dropped: the output equals the input over the *admitted interval*, + from the first group on, not the whole input. An exporter that joins later + starts at a later group. + +A group boundary is where MoQ can start delivery; it is not a promise that a +decoder can start there. The catalog's `randomAccess` is true only while the +PAT lists one program with at most one video stream and every group so far +began at a `random_access_indicator`. In an MPTS the programs' random access +points rarely line up, so a group usually starts mid-GOP for some of them, and +passthrough never inserts tables to help. Even a true `randomAccess` says +nothing about whether the PAT, PMT, codec configuration, or clock reference has +arrived yet; a receiver acquires them from the packets that follow. + +## Loss + +Loss here means MoQ content that never arrives: a group skipped or expired at a +relay, a group cut short, or a frame later than `--delay`. QUIC retransmits +what the network drops, so random packet loss alone loses no content. + +- **Semantic:** a missing group leaves a hole in the media, not in the + multiplex: padding keeps the PCR on its grid, the first PCR after the hole + flags the jump, and PAT/PMT versions do not change. A group cut short keeps + what arrived. Video resumes at its next keyframe, dropping orphaned leading + pictures; other tracks carry on. On a path slower than the media, whole groups + are shed and the latency holds. After an outage that exhausts the buffered + media, overdue padding can arrive in a burst when media resumes. +- **Passthrough:** surviving packets go out unchanged, on their source schedule. + A missing interval stays missing: the export never repairs continuity counters, + inserts tables or stuffing, or closes the gap. Each skip counts in the + `release` section's `gaps` and each late run in `dropped` + ([stats](/concept/stats#mpeg-ts-sections)). Exact surviving bytes do not mean + the output decodes, or meets the T-STD, across the gap. + +Damage in the source is the importer's concern, not MoQ's. In semantic mode a +damaged packet is dropped on its own PID, so video freezes until its next +keyframe while the other tracks carry on, and feed health (stalled streams, +TR 101 290 counters) is logged and reported without changing what publishes. +Passthrough carries damaged packets as they came. + +## Evidence + +Every claim above has an automated test. **PR** runs in the Rust gate on every +pull request (`just rs test -p moq-mux`, on a paused clock). **Nightly** runs in +the [TS workflow](https://github.com/moq-dev/moq/blob/main/.github/workflows/ts.yml) +every night and on release PRs: a relay round trip through the CLI, graded by +TSDuck, FFmpeg, and the harness's own models. **Nightly only** skips release +PRs. The [harness README](https://github.com/moq-dev/moq/blob/main/test/ts/README.md) +details each grader and its negative controls. + +### Semantic profile + +| Claim | Test | Runs | +| --- | --- | --- | +| H.264 and AAC decode to the source's frames and samples | `just test ts` ([decode.py](https://github.com/moq-dev/moq/blob/main/test/ts/decode.py)), `just test ts-controls` | Nightly | +| H.264/H.265 survive repartitioned PES and POC-derived timing | [`import.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/import.rs) `video_poc_repartition_preserves_frames_timestamps_and_export` | PR | +| MP2, AC-3, E-AC-3 round-trip byte-exact | [`export_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/export_test.rs) `mp2_kyrion_roundtrip_byte_exact`, `ac3_roundtrip_byte_exact`, `eac3_roundtrip_byte_exact` | PR | +| Opus layouts export, and an unlabelled head refuses | [`export_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/export_test.rs) `opus_export_import_roundtrip`, `opus_export_refuses_a_head_it_cannot_label` | PR | +| Whole PES (KLV, DVB AC-3) come back byte-equal outside PTS/DTS, at their placement | `just test ts --klv --strict`; [`pes_packet_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/pes_packet_test.rs) | Nightly, PR | +| MPEG-2 video decodes to the source's pictures | `just test ts --mpeg2`; [`routes_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/routes_test.rs) `mpeg2_video_rides_whole_and_survives_a_round_trip` | Nightly, PR | +| SCTE-35 and DSM-CC sections round-trip | [`export_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/export_test.rs) `scte35_fixtures_survive_roundtrip`; [`routes_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/routes_test.rs) `dsmcc_types_ride_as_sections` | PR | +| Refusals: PES clock fields, scrambling, ambiguous `0x86`, unclaimed PIDs | [`pes_packet_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/pes_packet_test.rs) `a_pes_with_a_field_no_rebase_reaches_refuses_its_stream`; [`routes_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/routes_test.rs) `ambiguous_streams_are_refused_and_latm_rides_whole`, `unclaimed_pids_count_once_the_pmt_is_in` | PR | +| DVB SI survives, on its own interval, pending versions dropped | [`export_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/export_test.rs) `service_layer_survives_roundtrip`, `si_pids_are_re_emitted_on_their_own_interval`; `just test ts-eit` | PR, Nightly | +| PMT revisions: streams joining and leaving, retypes, PID reuse | `just test ts --revision --strict`; [`revision_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/revision_test.rs) | Nightly, PR | +| PAT changes: moved PMT, programs added and dropped | [`revision_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/revision_test.rs) `a_moved_pmt_moves_behind_a_new_pat`; [`programs.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/programs.rs) `programs_follow_the_pat` | PR | +| Programs without video | `just test ts --clockless audio-scte35`, `sections`, `pes` | Nightly | +| Timeline steps republish; a changed identity restarts at its PAT | [`step_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/step_test.rs) | PR | +| Transport structure: PAT/PMT, CRC, continuity, PCR presence and order | `just test ts`, `--fragment-pes` (TSDuck) | Nightly | +| T-STD and system buffers, at 2 and 10 Mb/s, a 9 Mbit CPB, a full leak, a full and an over-Rx multiplex, open GOP | `just test ts`, `--bitrate 2000000`, `--hrd`, `--leak`, `--full`, `--headroom`, `--open-gop --strict`; model controls `just test ts-tstd` | Nightly | +| CBR padding, PCR within 500 ns, byte schedule | `just test ts` (`pcrverify`, `pcr-schedule`); [`export_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/export_test.rs) `export_pcr_is_its_byte_position_at_the_rate` | Nightly, PR | +| Release timing and PCR rate against the arrival clock | `just test ts --live --duration 120` | Nightly only | + +### Passthrough + +| Claim | Test | Runs | +| --- | --- | --- | +| The capture is the source in place over the admitted interval, with the source's compliance verdicts | `just test ts --passthrough`; `just test ts-passthrough-capture` (a hardware encoder's capture with a dirty start) | Nightly | +| A whole MPTS, two programs on their own clocks | `just test ts --passthrough --mpts` | Nightly | +| Byte identity from the first group; nothing before PMT and PCR; off-grid junk dropped | [`passthrough_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/passthrough_test.rs) `a_broadcast_capture_is_byte_identical_from_the_first_group`, `nothing_is_published_before_the_pmt_and_a_pcr`, `leading_junk_does_not_hide_the_grid` | PR | +| Scrambled input groups and paces like its clear twin | [`passthrough_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/passthrough_test.rs) `a_scrambled_twin_groups_and_paces_the_same` | PR | +| `randomAccess` false for several programs or video streams | [`passthrough_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/passthrough_test.rs) `two_programs_on_one_clock_publish_no_random_access`, `two_video_streams_in_one_program_publish_no_random_access` | PR | +| Clock: wrap, flagged jump, step back refused, 192/204-byte packets refused | [`passthrough_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/passthrough_test.rs) | PR | +| Export releases each run byte-identical on its own PCR, whatever the delivery skew | [`release_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/release_test.rs) `a_capture_is_released_byte_identical_on_its_own_pcr`, `releases_fed_with_different_skew_write_the_same_bytes` | PR | + +### Acquisition and loss + +| Claim | Test | Runs | +| --- | --- | --- | +| Semantic: a join starts on a whole group, each acquisition point carries PAT and PMT and decodes like the continuous source | [`recovery_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/recovery_test.rs) `a_join_starts_on_a_whole_group`, `a_late_catalog_starts_on_the_group_in_flight` | PR | +| Semantic: a late exporter matches a running one | [`export_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/export_test.rs) `late_join_matches_a_running_exporter`; `just test ts --pair` | PR, Nightly only | +| Semantic: a skipped group flags only the first PCR after it, with no table change | [`export_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/export_test.rs) `a_skipped_group_flags_the_clock`; [`recovery_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/recovery_test.rs) `disjoint_outages_across_both_tracks_keep_the_clock` | PR | +| Semantic: skipped, cut, stalled, and late groups, decoded against the source; open GOP and HEVC RASL trimmed, RADL kept | [`recovery_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/recovery_test.rs) (graders with failure controls) | PR | +| Semantic: two minutes of faults hold their latency | `just rs ts-recovery` | Nightly only | +| Semantic: a congested and an overflowing path shed whole groups, stay T-STD compliant, keep their latency | `just test ts --headroom --rate 7000000`, with `--queue 250ms` | Nightly only | +| Passthrough: a missing group leaves its time out and counts one gap; a cut group loses only its tail; a late run is dropped alone | [`release_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/release_test.rs) `a_group_never_delivered_leaves_its_time_out`, `a_group_cut_short_loses_only_its_tail`, `an_object_past_its_deadline_is_a_gap_in_that_release_only` | PR | +| Passthrough: an overflowing path keeps surviving packets in place through the source's last | `just test ts --passthrough --rate 7000000 --queue 250ms` | Nightly only | +| Source damage drops only the damaged PID's unit | [`import.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/import.rs) `damaged_h264_nal_recovers`, `video_drops_a_partial_access_unit_across_a_continuity_break` | PR | + +### Not claimed + +These are outside what the tests above establish: + +- **Hardware receivers and commercial analyzers.** No IRD or commercial T-STD + analyzer runs in CI; passing establishes the software profiles above, not + broadcast compliance in general. +- **Buffer models for other codecs.** The T-STD model has no parameters for + MPEG-1 video, MPEG-4 part 2, or HEVC beyond Main and Main 10, and refuses + them. Opus borrows ADTS's buffer size. Sections and private data are not + buffer-graded. +- **Decoded content beyond H.264, H.265, AAC and MPEG-2 video.** MP2, AC-3, + E-AC-3 and Opus are compared byte for byte, not decoded. +- **Variable-rate sources.** No PCR accuracy, byte-schedule, or system-buffer + claim for unpadded output. +- **`--program all` through a relay.** Covered by the Rust tests only; no + harness round trip. +- **Programs carried separately in passthrough.** Each is an ordinary SPTS + import; no test combines them. +- **Random datagram loss.** `just test ts --loss` exists but no CI step runs + it; the congested paths above grade realistic loss instead. +- **Physical-layer TR 101 290 items**, which a file cannot show. diff --git a/doc/concept/hang.md b/doc/concept/hang.md index c822ac3cda7..1e30df81c5a 100644 --- a/doc/concept/hang.md +++ b/doc/concept/hang.md @@ -175,7 +175,7 @@ steps back is a new broadcast. `randomAccess` says whether every published group began where a receiver can start decoding, and `muxRate` is the measured constant multiplex rate in bits per second, absent for a variable-rate source. A player has nothing to decode here. `moq import ts --passthrough` writes this -section (see [moq-cli](/bin/cli)); the encoding is in +section (see [MPEG-TS](/bin/ts#passthrough-mode)); the encoding is in [draft-lcurley-moq-mpegts](/draft/moq-mpegts). ## Container diff --git a/doc/lib/rs/moq-mux.md b/doc/lib/rs/moq-mux.md index 84762aedeb5..88a2f0df13d 100644 --- a/doc/lib/rs/moq-mux.md +++ b/doc/lib/rs/moq-mux.md @@ -39,8 +39,8 @@ timeline that rewinds ends the import. The MPEG-TS import ends at a clock step onto another timeline with `ts::Restart`, which carries the input from the step for the caller to publish as a new broadcast. fMP4 export fixes its track set at the init segment, so a new rendition or a changed configuration ends the -export. The [CLI page](/bin/cli#import) covers these and the MPEG-TS specifics: -one program per broadcast, private PES, damaged packets, and feed checks. +export. The [MPEG-TS guide](/bin/ts) covers the MPEG-TS specifics: one program +per broadcast, private PES, passthrough, acquisition, and loss. Exporters take a catalog stream alongside the source. Choose its encoding and rendition selection before exporting; the same stream shape drives every container. diff --git a/quest/m1/README.md b/quest/m1/README.md index eeb53d8119d..cf3ff5133da 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -50,7 +50,6 @@ moves what remains of them once their claims merge. - [One assets() for every hosted file](/quest/m1/hosted-assets.md) - `@moq/hang` owns `assets()`, which also points the libav polyfill at the hosted wasm - [Watch sources default to the decoder's probe](/quest/m1/watch-supported-default.md) - a source built without `in.supported` plays instead of selecting nothing - [Push-to-talk's press-to-audible latency fits a budget on the defaults](/quest/m1/push-to-talk-latency.md) - press to audible on the browser defaults, excluding device output, stays under a kixtop-measured budget that `just test push-to-talk` enforces -- [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - both modes have explicit profiles, automated validation, loss tests, and a dedicated guide - [MoQ on rind](/quest/m1/rind/README.md) - every MoQ crate takes its crypto from rind, a pure-Rust graviola fork, so no build compiles C crypto - [VA-API encode is correct on radeonsi](/quest/m1/vaapi-radeonsi.md) - AMD's VA-API H.264 encode decodes, once moq takes moq-vaapi 0.2.1 - [Tests run on a mocked clock](/quest/m1/mock-clock-tests/README.md) - about 300 tests stop proving nothing happened by waiting, sleeping to settle, or asserting on elapsed time diff --git a/quest/m1/ts-reliability/README.md b/quest/m1/ts-reliability/README.md deleted file mode 100644 index 9fb9e8c8369..00000000000 --- a/quest/m1/ts-reliability/README.md +++ /dev/null @@ -1,126 +0,0 @@ -# [M] TS fidelity and recovery - -## Goal - -The two TS modes have explicit, software-validated contracts under clean -delivery, late join, and lost MoQ content. A dedicated guide explains which -mode to choose, supported profiles, acquisition, loss, and the automated -evidence behind each claim. - -## Plan - -Decided in the 2026-10-11 TS audit: - -- TS to Hang to TS is the semantic mode. Remove packetization and stuffing; - retain the supported source semantics through media tracks and catalog - extensions. Hang's core stays container-neutral. Byte identity is not its - contract. -- TS to MoQ to TS is the packet-preserving fallback. Keep an SPTS or an entire - MPTS unchanged. Per-program tracks mean separately supplied SPTS inputs - in separate raw broadcasts under the existing catalog contract, - not extraction from an MPTS. Reject per-PID splitting and reversible packet - scatter for this lane: both add reconstruction complexity to the fallback. -- Loss in the fallback emits unchanged surviving packets on their source - schedule and reports gaps. Never silently repair continuity counters, - insert tables or stuffing, or compress the missing interval. Exact bytes - do not imply successful decoding or T-STD conformance after loss. -- Start with explicit software-validated profiles and expand with fixtures. - Hardware receivers and commercial qualification are useful additional - evidence, not prerequisites for repeatable CI. Do not claim universal - broadcast compliance from the current subset of the buffer model. -- Every TS validation, including TSDuck and FFmpeg, is an automated test - with assertions and a CI invocation. One-off analysis is diagnostic - evidence, not completion. Long real-time tests may run nightly; short - deterministic checks belong on relevant PRs. - -The existing raw importer admits an aligned 188-byte suffix after acquisition; -it does not currently retain every byte supplied before that point. The guide -must distinguish full-input identity from identity over the admitted interval, -and distinguish a MoQ group boundary from a decoder acquisition point. RAI, -periodic cuts, and an intact object alone do not ensure available PAT/PMT, -codec configuration, clock context, or references for every MPTS program. - -The README owns the guide and integration proof for the modes present in -m1: semantic remux and raw import/carriage. Raw export and its packet-level -proof landed in #5856; the guide covers it too. Reuse the children's -fixtures and CI recipes, including their known-bad controls. Publish -a coverage table linking each supported profile and loss guarantee to its -test; unsupported or unmeasured cells stay explicit. Keep CLI instructions -and the MPEG-TS draft consistent in each feature change. - -Private PES (decided 2026-10-16, #5718) landed in #5832 and #5921: a -private PES rides whole as a timed `pesPacket` track, stamped with its -program-clock placement, and the draft -00 `pes` framing is refused. The -guide documents that contract and the clock fields that refuse a stream. - -The semantic fidelity audit (2026-10-20) maps each source mechanism to its -contract in the semantic lane. The guide's profile table starts from it; -each open row has a child quest, and an audit finding is not authorization -to guess a public API. Preserved means carried through the catalog or a -track and re-emitted; normalized means rebuilt with the stated meaning; -refused means an error or a counted drop. - -| Mechanism | Contract | Owner | -|:--|:--|:--| -| H.264 (0x1B), H.265 (0x24) | Preserved: video rendition, PID and ES descriptors in `mpegts.tracks`; AUs cross PES boundaries with source or explicitly proportional POC-derived PTS; DTS normalized from decode order | landed | -| AAC ADTS (0x0F), MP2, AC-3, E-AC-3, Opus (0x06 + `Opus`) | Preserved: audio rendition; Opus export labels mono and stereo, and 3 to 8 channels with an OpusHead in the Vorbis layout. Other mapping families (such as 255) refuse | landed | -| Private and unknown PES types, LATM (0x11), metadata (0x15) | Preserved: `pesPacket`, PTS and DTS rebased | landed | -| PES clock fields (ESCR, pack header, TREF), PES scrambling | Refused, counted in `refused` | landed | -| Private sections (0x05), SCTE-35 (0x86 + `CUEI`) | Preserved: `verbatim` section, `pts_adjustment` rebased | landed | -| MPEG-1/2 video (0x01/0x02) | Preserved: `pesPacket` grouped from one sequence header to the next, keeping its clock role | landed | -| DSM-CC types (0x0A to 0x0D), 0x86 without `CUEI` | DSM-CC preserved as a `verbatim` section; 0x86 without `CUEI` refused, counted in `refused` | landed | -| CAT, EMM/ECM, ATSC PSIP, other SI PIDs, unlisted PIDs, TS scrambling | Outside the profile, raw lane; counted in `refused`, unclaimed PIDs when the import reads every program | landed | -| DVB SI on 0x0010, 0x0011, 0x0012, 0x0014 | Preserved: snapshot track per PID and `table_id` | landed | -| Program identity, program and ES descriptors | Preserved: descriptors follow each PMT revision; a PAT naming another identity restarts the import at that PAT under a new instance, and `--program all` refuses it | landed | -| PMT add, remove, retype; PID reuse | Normalized without a new epoch: a changed or reused PID ends its track and continues under a fresh name, and the export rebuilds the PMT at the next `version_number` | landed | -| PAT changes: moved PMT PID, programs added or removed | Normalized: followed; a removed program retires, and returns as a new epoch, and a running export moves the PMT behind a PAT at the next `version_number` | landed | -| Programs without video | Preserved when the PMT names a PCR PID; refused without one | landed | -| PCR | Normalized: rebased, carried on the first video PID, else audio, else a dedicated PCR-only PID | landed | -| Mux rate, null stuffing | Normalized: re-padded to `muxRate` while the source is CBR | landed | -| PSI and SI repetition | Normalized: PAT/PMT every 500 ms and at keyframes, SI at DVB maxima | landed | -| Media PES headers, TS packet layer | Normalized: rebuilt by export; the raw lane keeps them | landed | -| PAT network PID entry | Preserved: listed on 0x0010 when a NIT is carried | landed | - -Decided in the 2026-10-22 root-cause audit: the rows above kept getting -patched one at a time because two foundations were missing. Decode time is -reconstructed from history instead of decode order, and import state has no -owner that retires it. Those quests rank ahead of the remaining rows. Rejected: carrying -decode time in every frame container, one shared ingest byte budget, -and replacing the demux with GStreamer or another parser (it fixes neither -cause). Out of scope for now: decoupling catalog publication from mux-rate -measurement, except for a supervised restart, which reuses its predecessor's -measurement (decided 2026-10-28). Re-planned the same day: decode order covers MKV and FLV too and -absorbs the release audit's export-reorder quest, and the shared parser lands -before the open stream-routes and PAT drafts, which rebase onto it. - -Decided 2026-11-03 (maintainer, #7221): the TS export counts an unmarked step -back as a restart only where a reorder bound proves the frame falls outside -valid reordering: audio, which never reorders, and video that steps back past -its catalog `reorder` depth, in both cases by more than the export's delay. -Video that declares no `reorder` is left untouched: no detection, no rewrite, -no error. - -Decided 2026-11-04 (maintainer, design review of -[moq-dev/moq#7780](https://github.com/moq-dev/moq/pull/7780)): - -- ✅ A TS restart carries nothing across. The new import starts with no - program map and waits for the source's own PAT/PMT and next keyframe, - accepting up to one GOP of extra delay before the new instance announces - after an unannounced switch: the event is rare, and a guess across the - restart is fragile. Not chosen: a private provisional map on `Restart` - read through `Import::after(&restart)`; a source-level `Source` owning - PAT/PMT and steps; holding PSI packets until the next clock reading. -- ✅ A PAT naming another `transport_stream_id` or program restarts the - import at that PAT (`Step::Identity`), even before the new source's first - PCR. Not chosen: refusing it, which ended the whole publish. -- ✅ Decoded media parses incrementally, so a config publishes before its PES - ends, and the initial reservation releases at the first PTS. This is - independent of the handoff. - -PMT fragmentation (#5220), initial mux-rate measurement (#5287), and -decoder-time admission (#5306) have landed, as have PSI runs (#5606) and -catalog linger (#5305). - -## Related - -- [MSFTS mapping](/quest/m5/msfts-convergence.md) - document the exact equivalence and differences between the carriage modes diff --git a/quest/m3/release/feature-review.md b/quest/m3/release/feature-review.md index 6253a06ad57..3340a5cec2a 100644 --- a/quest/m3/release/feature-review.md +++ b/quest/m3/release/feature-review.md @@ -29,7 +29,7 @@ ship as is. The first run must cover the MPEG-TS work (decided 2026-10-19 instead of a separate TS audit): everything since the 2026-10-11 TS audit, judged against -the contracts in [TS fidelity and recovery](/quest/m1/ts-reliability/README.md). +the contracts in the [MPEG-TS guide](/doc/bin/ts.md). Question each TS capability's premise the way the move to distinct importer epochs dropped active-active publishers. diff --git a/quest/m3/ts-multi-program-restart.md b/quest/m3/ts-multi-program-restart.md index 95555b8f1d1..8409b74735f 100644 --- a/quest/m3/ts-multi-program-restart.md +++ b/quest/m3/ts-multi-program-restart.md @@ -25,13 +25,9 @@ import. - Reuse #7780's step judgement and its "carry nothing" rule rather than a second mechanism; decide whether the switch restarts every program or only the ones whose identity or clock changed, and say why in the PR. -- Keep the existing [TS reliability](/quest/m1/ts-reliability/README.md) +- Keep the [MPEG-TS guide](/doc/bin/ts.md#programs-and-changes)'s rules for PAT changes that aren't a source switch (a moved PMT PID, a program added or removed) as they are. Public API: possibly additive on `Programs` (a restart result like `Import`'s). Wire: none. - -## Related - -- [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - the single-program restart and PAT-change rules this extends diff --git a/test/ts/README.md b/test/ts/README.md index 55259e4555c..f526cd97c7d 100644 --- a/test/ts/README.md +++ b/test/ts/README.md @@ -201,8 +201,8 @@ sequence header opens each access unit. Opus is graded against ADTS's buffers for the same channel count: the Opus-in-TS draft gives Rx (2 Mb/s for 1-2 channels, matching ADTS) but leaves the buffer size unset, so that size is borrowed rather than specified. A stream with no parameters -at all is refused by name rather than skipped, which fails the check: MPEG-1/2 -video, DVB E-AC-3, and HEVC beyond Main/Main 10. Sections and private data +at all is refused by name rather than skipped, which fails the check: MPEG-1 +video, MPEG-4 part 2, and HEVC beyond Main/Main 10. Sections and private data (SCTE-35, teletext) are listed as not graded; passing elementary-stream buffers does not establish conformance for them. From 85c34d98d350788f95fd4192aae3daa386033f3b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 17:39:32 -0800 Subject: [PATCH 3/4] docs(ts): codec playback depends on the player; padded holes stay unflagged Co-Authored-By: Claude Opus 5.5 --- doc/bin/ts.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/bin/ts.md b/doc/bin/ts.md index 954a84ee4d2..c1f328730bd 100644 --- a/doc/bin/ts.md +++ b/doc/bin/ts.md @@ -25,7 +25,7 @@ moq ... --broadcast event.hang export ts --passthrough > out.ts | --- | --- | --- | | On the wire | One track per elementary stream, plus the PSI and SI the import keeps, in the [`mpegts`](/draft/moq-mpegts) catalog section | One [`m2ts`](/concept/hang#transport-streams) track of whole 188-byte packets | | Output | A rebuilt TS: same media, programs, descriptors and kept SI; new packetization, PCR and table cadence | The source's packets, byte for byte, from the first group on | -| Browsers and `moq play` | Watch it | Have nothing to decode | +| Browsers and `moq play` | Watch the video and AAC or Opus audio; MP2, AC-3 and E-AC-3 play only where the player has a decoder for them, which `moq play` does not | Have nothing to decode | | A relay | Drops and prioritizes per track | Caches and drops whole runs of packets | | Scrambled services, CA, unlisted PIDs | Refused and counted | Carried | | Multi-program | One program per broadcast (`--program`) | The whole MPTS in one broadcast | @@ -45,9 +45,9 @@ SCTE-35). For example, ### What it carries -The import decodes H.264/H.265 video and AAC (ADTS), MP2, AC-3, E-AC-3, and -Opus audio into renditions any player can watch. Everything else it keeps goes -through without being decoded: +The import parses H.264/H.265 video and AAC (ADTS), MP2, AC-3, E-AC-3, and +Opus audio into renditions, without transcoding: a player still needs a +decoder for each codec. Everything else it keeps rides through whole: | Source | Carried as | | --- | --- | @@ -174,10 +174,11 @@ relay, a group cut short, or a frame later than `--delay`. QUIC retransmits what the network drops, so random packet loss alone loses no content. - **Semantic:** a missing group leaves a hole in the media, not in the - multiplex: padding keeps the PCR on its grid, the first PCR after the hole - flags the jump, and PAT/PMT versions do not change. A group cut short keeps - what arrived. Video resumes at its next keyframe, dropping orphaned leading - pictures; other tracks carry on. On a path slower than the media, whole groups + multiplex: padding keeps the PCR on its continuous grid, unflagged. Only a + hole longer than the export can pad over skips PCR slots, and then the first + PCR after them flags the clock jump. PAT/PMT versions do not change either + way. A group cut short keeps what arrived. Video resumes at its next + keyframe, dropping orphaned leading pictures; other tracks carry on. On a path slower than the media, whole groups are shed and the latency holds. After an outage that exhausts the buffered media, overdue padding can arrive in a burst when media resumes. - **Passthrough:** surviving packets go out unchanged, on their source schedule. @@ -243,7 +244,7 @@ details each grader and its negative controls. | --- | --- | --- | | Semantic: a join starts on a whole group, each acquisition point carries PAT and PMT and decodes like the continuous source | [`recovery_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/recovery_test.rs) `a_join_starts_on_a_whole_group`, `a_late_catalog_starts_on_the_group_in_flight` | PR | | Semantic: a late exporter matches a running one | [`export_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/export_test.rs) `late_join_matches_a_running_exporter`; `just test ts --pair` | PR, Nightly only | -| Semantic: a skipped group flags only the first PCR after it, with no table change | [`export_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/export_test.rs) `a_skipped_group_flags_the_clock`; [`recovery_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/recovery_test.rs) `disjoint_outages_across_both_tracks_keep_the_clock` | PR | +| Semantic: a padded hole keeps the PCR unflagged; a longer one flags only the first PCR after its skipped slots; tables keep their versions | [`export_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/export_test.rs) `a_skipped_group_flags_the_clock`; [`recovery_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/recovery_test.rs) `disjoint_outages_across_both_tracks_keep_the_clock` | PR | | Semantic: skipped, cut, stalled, and late groups, decoded against the source; open GOP and HEVC RASL trimmed, RADL kept | [`recovery_test.rs`](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/src/container/ts/recovery_test.rs) (graders with failure controls) | PR | | Semantic: two minutes of faults hold their latency | `just rs ts-recovery` | Nightly only | | Semantic: a congested and an overflowing path shed whole groups, stay T-STD compliant, keep their latency | `just test ts --headroom --rate 7000000`, with `--queue 250ms` | Nightly only | From ffad731bfa34abef947e2f1fea16183d34f747f2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 21:04:13 -0800 Subject: [PATCH 4/4] docs(ts): passthrough frames also end at a group start; groups cut at the cache bound Co-Authored-By: Claude Opus 5.5 --- doc/bin/ts.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/bin/ts.md b/doc/bin/ts.md index c1f328730bd..daa351a2626 100644 --- a/doc/bin/ts.md +++ b/doc/bin/ts.md @@ -131,11 +131,12 @@ extracts a program, so it refuses `--program`. To carry programs separately, import each as its own SPTS into its own broadcast. 192- and 204-byte packets are refused rather than reframed. `--pcr-pid` paces on another PID's PCR. -Each frame is the run of packets from one PCR on the clock PID to the next, -stamped with the PCR time of its first byte. A group starts at a -`random_access_indicator` on the video PID (or the clock PID with no video), at -a flagged PCR jump forward, or after a second without either. Any step back -ends the import. +Each frame is a run of packets that ends at the next PCR on the clock PID or at +the next group start, stamped with the PCR time of its first byte. A group +starts at a `random_access_indicator` on the video PID (or the clock PID with +no video), at a flagged PCR jump forward, after a second without either, or +where the next frame would take the group past the relay's cache bound. Any +step back ends the import. `export ts --passthrough` writes each run `--delay` after its time on the source clock, so two exporters emit the same bytes. It takes neither