Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a3c3079
quest(m1): start T-STD compliant TS export questline
kixelated Sep 30, 2026
02126be
quest(m1/tstd): claim fixed-delay release
kixelated Sep 30, 2026
8edda55
feat(moq-mux)!: release TS export frames on a fixed delay
kixelated Oct 1, 2026
bcb0741
fix(moq-mux): close TS export spans on decode time
kixelated Oct 1, 2026
1cda12b
refactor(moq-mux): rename the release stage to jitter::Buffer
kixelated Oct 1, 2026
a11aea3
fix(moq-mux): space reordered DTS a frame period apart
kixelated Oct 1, 2026
c38c133
feat(moq-mux): lay TS export packets on a constant-rate schedule
kixelated Oct 1, 2026
bad8405
feat(moq-cli): gate the TS byte schedule and pace export with the pac…
kixelated Oct 1, 2026
17d8a2f
fix(moq-mux): re-buffer instead of dropping a late sync frame
kixelated Oct 1, 2026
dbb4e1f
quest(m1): name the jitter buffer in the FLV and MKV delay entries
kixelated Oct 1, 2026
402500b
fix(moq-mux): drop every late TS frame, as SRT does
kixelated Oct 1, 2026
70e7383
test(ts): grade the full T-STD buffer model (#4643)
kixelated Oct 1, 2026
06dbcdc
Merge remote-tracking branch 'origin/quest/m1/tstd/README' into quest…
kixelated Oct 1, 2026
8df1e43
Merge remote-tracking branch 'origin/dev' into quest/m1/tstd/README
kixelated Oct 1, 2026
da42622
Merge remote-tracking branch 'origin/quest/m1/tstd/README' into quest…
kixelated Oct 1, 2026
02a332f
fix(moq-mux): finish each TS unit a millisecond before its DTS
kixelated Oct 1, 2026
4b7158d
fix(moq-mux): finish each TS unit one packet's drain before its DTS
kixelated Oct 1, 2026
4822c07
fix(moq-mux): keep every TS track on one jitter clock
kixelated Oct 1, 2026
a1f3f5f
fix(moq-mux): author TS video DTS in time, not picture counts
kixelated Oct 1, 2026
9e08202
fix(moq-mux): admit TS packets per PID against the receiver's buffers
kixelated Oct 1, 2026
aa82b10
fix(moq-mux): send passed-through AC-3 a sync frame a PES
kixelated Oct 1, 2026
11d68e7
fix(moq-mux): follow the source clock in the TS jitter buffer
kixelated Oct 1, 2026
5e7650a
fix(moq-mux): lay a padded TS slot alike in every exporter
kixelated Oct 1, 2026
2ef0d8a
test(ts): grade a broadcast-sized CPB and PCR accuracy
kixelated Oct 1, 2026
a8b1517
quest(m1): record the T-STD measurement after t0ms's grading
kixelated Oct 1, 2026
49efbc9
style(moq-mux): format
kixelated Oct 1, 2026
acc5fdd
fix(moq-mux): only a declared restart breaks the TS program clock
kixelated Oct 1, 2026
0160e26
fix(moq-mux): join the TS export at the live edge
kixelated Oct 1, 2026
2dc542b
fix(moq-mux): hold the TS export's clock to 13818-1 drift limits
kixelated Oct 1, 2026
e855672
test(moq-mux): mocked-time export tests for join skips, source drift,…
t0ms Oct 1, 2026
4f36c8b
fix(moq-mux): acquire the TS export's clock and recover it from floors
kixelated Oct 1, 2026
016789a
fix(moq-mux): drop cut AC-3 frames, end without a missed deadline, mo…
kixelated Oct 1, 2026
6f6123a
style(moq-mux): format, and keep the export drift cases under nextest…
kixelated Oct 1, 2026
559a352
refactor(moq-mux): name the export's stats ts::export::Stats
kixelated Oct 1, 2026
7a75ff3
test(moq-mux): a track sent later than the delay loses nothing
t0ms Oct 2, 2026
01fedae
fix(moq-mux): anchor and steer on the track sent latest, after hearin…
t0ms Oct 2, 2026
1cbfddb
Merge origin/main into quest/m1/tstd/delay
kixelated Oct 6, 2026
fe7cec1
fix(moq-mux): release equal decode times by track, not by nanosecond …
kixelated Oct 6, 2026
e4beb00
Merge origin/main into quest/m1/tstd/delay
kixelated Oct 7, 2026
e6d9c22
fix(moq-mux): a track reaching a new generation without a marker join…
kixelated Oct 7, 2026
d7323b0
docs(upgrade): the TS export's release counters join ts::stats::Export
kixelated Oct 7, 2026
ed6dd46
fix(moq-mux): a new generation keeps each track's lead from the last
kixelated Oct 7, 2026
78b7afd
fix(moq-mux): keep the previous generation ahead of the new one's fir…
kixelated Oct 7, 2026
55f5dbe
Merge remote-tracking branch 'origin/main' into quest/m1/tstd/delay
kixelated Oct 7, 2026
6dfd45f
fix(moq-mux): saturate the HRD rate and name the clocks --delay spans
kixelated Oct 7, 2026
899ef70
Merge remote-tracking branch 'origin/main' into merge-4645-b
kixelated Oct 8, 2026
50935e6
fix(moq-mux): drop an old-timeline frame without steering on it
kixelated Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/interop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,24 @@ jobs:
env:
MOQ_TEST_RUNS: ${{ runner.temp }}/moq-test-runs

# The same round-trip at 2 Mb/s, where every keyframe outgrows a 25 ms PCR slot,
# so the export has to spread it over the slots before its DTS and still keep
# the constant-rate schedule.
- name: TS keyframe bursts
run: nix develop --command just test ts --bitrate 2000000
shell: bash -leo pipefail {0}
env:
MOQ_TEST_RUNS: ${{ runner.temp }}/moq-test-runs

# 1080p video filling a broadcast-sized 9 Mbit CPB, at the default delay: the
# source sends pictures most of a second ahead of their decode time, and the
# export has to as well without overflowing the receiver's buffers.
- name: TS broadcast-sized CPB
run: nix develop --command just test ts --hrd
shell: bash -leo pipefail {0}
env:
MOQ_TEST_RUNS: ${{ runner.temp }}/moq-test-runs

# The same round-trip over an open-GOP clip: every leading picture after a
# recovery point must come back, in decode order.
- name: TS open GOP
Expand Down
61 changes: 46 additions & 15 deletions doc/bin/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,13 @@ SI matches the selection by DVB `service_id`, which is assumed to equal the PAT
moq --connect https://relay.example.com/anon --broadcast event.hang import ts --program all < mux.ts
```

MPEG-TS export restarts its clock and table cadence after a declared marker,
discarding the old mux buffer. The first new clock packet signals the break and
stdout pacing re-anchors. Every rendition joins the new program generation;
no track is fenced across the marker.
MPEG-TS export starts a new clock after a declared marker, and the first new
clock packet signals the break. Each track goes out on the old clock until it
reaches the marker, then joins the new one, unless an old-clock frame would go
out after the new clock's first, which is dropped. Once the export has measured
each track's lead over the others (two seconds of media), the new clock keeps
it, so a track the source sends later than the rest crosses without losing
frames. No track is fenced across the marker.

MPEG-TS export frames AAC as ADTS, which labels only the AAC Main, LC, SSR,
and LTP profiles. HE-AAC and HE-AACv2 go out as their AAC-LC core, and decoders
Expand All @@ -152,11 +155,20 @@ the program without that AAC track, and adds the track if an element arrives.

A constant-rate MPEG-TS source records its multiplex rate in the catalog
(`mpegts.muxRate`, measured off the PCR clock, null stuffing included), and
`export ts` pads its output with null packets back to that rate so an IRD or
groomer receives a constant-rate stream. `--mux-rate 5000000` pads to an explicit
rate instead, including for a broadcast that recorded none. Media is never delayed
or dropped to fit: a source that sustains more than the rate overruns it, and a
VBR source records nothing, so export without either stays unpadded.
`export ts` pads its output with null packets back to that rate on a constant-rate
schedule, so an IRD or groomer recovering its clock from packet arrival can lock:
every PCR is the time of its own byte position at the rate. `--mux-rate 5000000`
pads to an explicit rate instead, including for a broadcast that recorded none.

Each frame goes out as early as a receiver's buffers for its PID admit (the
ISO 13818-1 T-STD: no more of a PID's packets per interval than its transport
buffer passes on, and no more bytes than its decoder buffer holds, sized from the
SPS's HRD or level for video and per codec for audio), up to `--delay` ahead of its
decode time on the output's PCR clock, earliest decode time first. So a heavy passage rides the intervals
before it, and the output trails the source by twice the delay. A frame that cannot
arrive by its decode time at the rate fails the export with an error naming both
knobs; a broadcast-sized decoder buffer (a CPB of a second or more) needs a delay to
match. A VBR source records no rate, so export without one stays unpadded.

The `fmp4`, `mkv`, `flv`, `h264`, and `h265` exports select renditions with
flags before the sink: `--video-name` and `--audio-name` pick a rendition,
Expand Down Expand Up @@ -502,13 +514,32 @@ See [Authentication](/bin/relay/auth).
`import --max-age` (default 30 s) tells relays how long to keep old
groups fetchable, which the [HLS gateway](/bin/hls) depends on. `export --max-delay` (default 500 ms) is how far a stalled group may fall
behind the live edge before *this* consumer skips it. Raising the first never delays playback.
`export ts` still spells its budget `--max-age`.

For `export ts`, `--max-age` also bounds how long the muxer holds a leading
track for a lagging one. Frames go out in media-time order across all tracks,
not arrival order, so two exporters of one broadcast emit them in one order. A
track quiet for longer is muxed around until it catches up; a sparse track
(SCTE-35) costs that wait once per cue. `--max-age 0` keeps arrival order.
`export ts` takes `--delay` (default 500 ms) instead, and works like an SRT
receiver's latency. Every frame is released to the multiplexer that long after
its decode time, on a clock that keeps the source's pace and muxes all tracks in decode order
whatever their arrival skew: two exporters of one broadcast emit them in one
order. The export joins at the newest group and holds its output until it
has heard from every audio, video, DVB AC-3 and teletext track (two delays at most) and a
track starts its next group, or one of the frames it holds falls due. It then
starts the clock on the track the source sends latest against its decode time,
from that track's freshest frame: a broadcast TS sends video most of a second
ahead and audio just in time, and the audio must keep the delay too. What of
the joined group is older than the delay is dropped, so a joiner runs at the
delay from its first output. A frame that arrives later than its deadline is
dropped, and video then resumes at its next keyframe. The clock follows the
source's, measured from the frames that arrive least delayed on the track sent
latest, so a source whose clock runs a
little fast or slow neither goes late nor piles up over a long run, and a
spell of network queueing does not move it. The output's PCR is that clock,
so it keeps to what ISO/IEC 13818-1 allows a system clock: within 30 ppm of
the exporter's, its rate changing by at most 0.075 Hz/s. Catching up with a
source near that limit takes hours; one further off is counted in the
export's stats, and fails the export once it has used half the delay. Each
25 ms slice of the output is written when the clock reaches it. A stalled group
is skipped once it falls half the delay behind, leaving the group after it the
other half. `--delay 0` holds nothing and drops
nothing, writing frames in arrival order as they come.

A stdout export ends with the broadcast. `export ts --linger 10s` waits that
long for the broadcast to come back instead: a publisher that restarts within
Expand Down
5 changes: 3 additions & 2 deletions doc/bin/srt.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ publishes each program as its own broadcast (`event.hang` becomes
moq --connect https://relay.example.com/anon --broadcast event.hang import srt --listen '[::]:9000' --program all
```

`--latency` sets the SRT receive buffer and doubles as the skip threshold on
export. Export paces each SRT payload on the media clock, and re-anchors that
`--latency` sets the SRT receive buffer and doubles as the export's jitter
buffer delay, as `export ts --delay` in the [CLI](/bin/cli): each frame is muxed that
long after its decode time, and one arriving later is dropped. Export paces each SRT payload on the media clock, and re-anchors that
pacing on a declared marker, so a restarted timeline plays out from the
live edge instead of stalling until it catches up. A `--connect` URL needs a `streamid` query or a path; a listener
bridges one `--broadcast` and ignores the stream id it is offered. The
Expand Down
12 changes: 9 additions & 3 deletions doc/setup/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ These land with the next breaking release, not the 2026-09-23 train.
the rendition's own delay added on top. `Sync.out.jitter` now always
equals `Sync.out.delay`, and `"auto"` with no decoder registered resolves to
0 rather than 100 ms.
- **`moq export ts --delay` replaces `--max-age`** (#4645). Each frame is
written that long after its decode time, on a clock that follows the
source's; `--max-age` and `--latency-max` are refused with the new flag
named. In Rust, moq-mux's `ts::Export::with_max_age` is `with_delay`, and
`ts::stats::Export` gains the dropped-frame count (late frames, and the
video frames then dropped waiting for a keyframe), measured drift and
out-of-tolerance count beside its `streams` rows. It is no longer `Eq`.
- **moq-mux has no clock translators.** `clock::Anchor`, `clock::Lane`, and
`SourceMap` (#4667) are gone, along with the importers' `live()`. Publish the
source's own timestamps and let the catalog clock map them to wall time;
Expand Down Expand Up @@ -141,7 +148,7 @@ These land with the next breaking release, not the 2026-09-23 train.
`moq_video_decoder_output`, `moq_consume_video`, and `moq_consume_audio`.
`moq export fmp4`, `mkv`, `flv`, `h264`, `h265`, and `rtmp` take
`--max-delay`, and refuse `--max-age`. `track::Info::max_age`,
`MoqTrackInfo.max_age_us`, `moq import --max-age`, and `moq export ts --max-age` are unchanged, as is the wire.
`MoqTrackInfo.max_age_us`, and `moq import --max-age` are unchanged, as is the wire.
- **moq-relay auth takes the client-CA answer.** `auth::Config::validate` and
`init` take `client_ca: bool`, whether any listener verifies client
certificates, and `validate_client_ca` is gone. `moq --listen` with an
Expand All @@ -168,8 +175,7 @@ These land with the next breaking release, not the 2026-09-23 train.
- **moq-mux TS stats live in `ts::stats`.** `ts::Stats` is
`ts::stats::Snapshot` and `ts::StreamStats` is `ts::stats::Stream`, whose
`track` is an owned `String`. `ts::Export::stats` returns
`ts::stats::Export`, which carries only `streams`; feed it to
`stats::Log` with `.into()`. `ts::MultipleProgramsError` is
`ts::stats::Export`; feed it to `stats::Log` with `.into()`. `ts::MultipleProgramsError` is
`#[non_exhaustive]`: recover it by downcast and read `programs`.
- **@moq/publish drops `OpusConfig.usedtx`.** Chromium's DTX output shifts the
audio timeline, so Opus DTX is always off (the WebCodecs default). Remove the
Expand Down
4 changes: 2 additions & 2 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ blocks. The quests that gated m0 lines moved under them.
- [Audio warmup](/quest/m1/audio-warmup.md) - a viewer joining an Opus rendition mid-stream never hears the unconverged first 80 ms
- [#3021](/quest/m1/3021-moq-gst-anchor-generated-media-timelines-to-wall-clock.md) - moq-gst picks the broadcast wall epoch; a restarted source is a new epoch, not a forward re-anchor
- [TS passthrough](/quest/m1/ts-passthrough.md) - `--passthrough` carries the multiplex as whole packets, listed in an `m2ts` catalog section, and writes it back byte-identical (less late drops) on a fixed delay
- [FLV export delay](/quest/m1/flv-export-delay.md) - FLV interleaves through the shared fixed-delay release stage
- [MKV export delay](/quest/m1/mkv-export-delay.md) - MKV interleaves through the shared fixed-delay release stage
- [FLV export delay](/quest/m1/flv-export-delay.md) - FLV interleaves through the shared jitter buffer
- [MKV export delay](/quest/m1/mkv-export-delay.md) - MKV interleaves through the shared jitter buffer
- [MKV lacing](/quest/m1/mkv-lacing.md) - laced MKV blocks import as one timed frame each, refused without DefaultDuration
- [Release profile](/quest/m1/release-profile.md) - every release build gets fat LTO, one codegen unit, and stripping from the workspace profile instead of three script exports
- [Size report](/quest/m1/size-report.md) - a nightly job reports every shipped artifact's size, native and JS, and alerts when one grows
Expand Down
12 changes: 4 additions & 8 deletions quest/m1/flv-export-delay.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
## Goal

`moq export flv --delay <dur>` interleaves tracks through the shared
fixed-delay release stage, so its tag order is a function of the media, not
jitter buffer, so its tag order is a function of the media, not
of arrival, as the TS export's is.

## Plan

Today `pick_next_track` (`rs/moq-mux/src/container/flv/export.rs`) takes the
smallest *pending* timestamp, so which track goes first depends on which frame
has arrived. Replace it with the release stage from
[fixed-delay release](/quest/m1/tstd/delay.md), with the same `--delay` flag
and the same late-frame drop. Update `doc/bin/cli.md`.
has arrived. Replace it with the jitter buffer the TS export uses
(`rs/moq-mux/src/jitter.rs`), with the same `--delay` flag and the same
late-frame drop. Update `doc/bin/cli.md`.

Decided in the 2026-10-06 audit: `--delay` replaces the staleness flag, as it
replaced `--max-age` on TS, so one knob sets both the release delay and the
Expand All @@ -22,7 +22,3 @@ renames that flag to `--max-delay` first; this quest then replaces

Public API: breaking, the exporter's staleness setting becomes the delay
setting. Wire: none.

## Required

- [Fixed-delay release](/quest/m1/tstd/delay.md) - builds the shared release stage
12 changes: 4 additions & 8 deletions quest/m1/mkv-export-delay.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
## Goal

`moq export mkv --delay <dur>` interleaves tracks through the shared
fixed-delay release stage, so its tag order is a function of the media, not
jitter buffer, so its tag order is a function of the media, not
of arrival, as the TS export's is.

## Plan

Today `pick_next_track` (`rs/moq-mux/src/container/mkv/export.rs`) takes the
smallest *pending* timestamp, so which track goes first depends on which frame
has arrived. Replace it with the release stage from
[fixed-delay release](/quest/m1/tstd/delay.md), with the same `--delay` flag
and the same late-frame drop. Update `doc/bin/cli.md`.
has arrived. Replace it with the jitter buffer the TS export uses
(`rs/moq-mux/src/jitter.rs`), with the same `--delay` flag and the same
late-frame drop. Update `doc/bin/cli.md`.

Decided in the 2026-10-06 audit: `--delay` replaces the staleness flag, as it
replaced `--max-age` on TS, so one knob sets both the release delay and the
Expand All @@ -22,7 +22,3 @@ renames that flag to `--max-delay` first; this quest then replaces

Public API: breaking, the exporter's staleness setting becomes the delay
setting. Wire: none.

## Required

- [Fixed-delay release](/quest/m1/tstd/delay.md) - builds the shared release stage
4 changes: 0 additions & 4 deletions quest/m1/open-gop-leading-pictures.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,6 @@ decode, and the JS test should drive the software decoder.
group's leading pictures trimmed too, so an implementation that only trims
the initial group fails. Both cases in both languages.

## Required

- [Fixed-delay release](/quest/m1/tstd/delay.md) - the TS export this trims lands with #4645

## Related

- [Consumer warmup](/quest/m2/intra-refresh/consumer-warmup.md) - the `recovery_frame_cnt > 0` case this rule does not cover
Expand Down
6 changes: 2 additions & 4 deletions quest/m1/ts-passthrough.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ Decided (2026-10-01), from a discussion with t0ms:
- A multiplex is paced on one PCR PID: the `PCR_PID` in the PMT of the PAT's
first program, or `--pcr-pid`. Every byte stays in order behind it. Programs
on independent clocks are out of scope.
- The export reuses the [fixed-delay release](/quest/m1/tstd/delay.md)
stage, keyed on each object's PCR time instead of a DTS, which also
- The export reuses the TS export's jitter buffer (`rs/moq-mux/src/jitter.rs`,
#4645), keyed on each object's PCR time instead of a DTS, which also
spreads each object's bytes at the PCR-implied rate. It must pace on the
source's PCR, not on arrival: a pacer that re-clocks on arrival moves the
PCR-to-PTS offset over a long capture and fails the decoder buffers, even
Expand Down Expand Up @@ -119,10 +119,8 @@ hang catalog gains an `m2ts` root section; additive.
## Required

- [TS restart](/quest/m0/broadcast-epoch/ts-restart.md) - the restart-under-a-new-epoch path a backward PCR discontinuity reuses
- [Fixed-delay release](/quest/m1/tstd/delay.md) - the release stage this reuses, with its clock recovery (#4645)

## Related

- [TS hitless](/quest/m2/ts-hitless.md) - the demultiplexed lane's 2022-7 legs and the `--sync` anchor
- [MSFTS convergence](/quest/m2/msfts-convergence.md) - the ES-level side of the same mapping
- [TS byte schedule](/quest/m1/tstd/byte-schedule.md) - the remux's equivalent of pacing on the source PCR
22 changes: 14 additions & 8 deletions quest/m1/tstd/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# T-STD compliant TS export
# [M] T-STD compliant TS export

## Goal

Expand All @@ -16,20 +16,26 @@ Padding, pacing, and muxing all assume a fixed delay, so the export gets one
first. t0ms is testing whether T-STD compliance is feasible at all; record
the result here. If it isn't, re-plan this line.

The full-model check (`tstd` in `test/ts/compliance.py`) fails today's export
for two reasons, both delivery timing: 7-10 % of video access units finish
arriving after their DTS, and audio runs ~230 ms ahead in bursts that overflow
its TB and B. The media itself fits its buffers, so nothing yet says the bar
is out of reach.
Measured (2026-10-01, #4645): with the fixed-delay jitter buffer, per-PID
admission against each PID's T-STD buffers, and PCRs at their byte position, a
clean-path round trip passes the strict T-STD check and TSDuck's ±500 ns
pcrverify at the default 500 ms delay, for the generated clip at 10 Mb/s and
2 Mb/s and for a 1080p encode filling a 9 Mbit CPB (`just test ts --hrd`, after
t0ms's recipe). A unit that cannot arrive by its DTS fails the export rather
than arrive late. t0ms graded it on a CNN broadcast capture (PAFF H.264, MP2,
DVB AC-3, teletext, SCTE-35; 2026-10-02): at 1 s on loopback, across hosts and
under the #4613 rig at 0 % and 1 % loss, every track passed T-STD and
`compliance.py` with no late drops, once the clock anchored on the track sent
latest. At 500 ms a 540 s run still misses a video deadline about 157 s in,
which [send-ahead](/quest/m1/tstd/send-ahead.md)'s 1 s default addresses, and
10 % loss is still to pass.

This README owns the end-to-end proof: the #4613 netem rig (10% loss, a real
~10 Mb/s broadcast TS) passes the strict T-STD check, and the recipe runs
nightly.

## Required

- [Fixed-delay release](/quest/m1/tstd/delay.md) - frames go out at media time plus a fixed `--delay`, in one order under loss
- [TS byte schedule](/quest/m1/tstd/byte-schedule.md) - PCRs sit on the byte grid the mux rate implies, paced against the fixed delay
- [Send-ahead within the delay](/quest/m1/tstd/send-ahead.md) - total lag is `--delay`, send-ahead included, with a 1 s default
- [Mux-rate hold](/quest/m1/tstd/mux-rate-hold.md) - import publishes its catalog once the mux rate is measured, so export is constant-rate from the start

Expand Down
Loading
Loading