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..daa351a2626 --- /dev/null +++ b/doc/bin/ts.md @@ -0,0 +1,277 @@ +--- +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 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 | +| 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 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 | +| --- | --- | +| 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 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 +`--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 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. + 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 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 | +| 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 da300e96d79..4262bfb309a 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -49,7 +49,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 - [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 - [moq-mux reads only the base layer's H.265 SPS](/quest/m1/h265-base-layer-sps.md) - a layered MV-HEVC or SHVC stream imports as its base layer instead of misreading an enhancement-layer SPS, which still reaches the track 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.