Skip to content

fix(mux): TS programs follow the PAT as it changes - #6301

Closed
t0ms wants to merge 7 commits into
moq-dev:mainfrom
t0ms:quest/m1/ts-reliability/pat-lifecycle
Closed

t0ms wants to merge 7 commits into
moq-dev:mainfrom
t0ms:quest/m1/ts-reliability/pat-lifecycle

Conversation

@t0ms

@t0ms t0ms commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A live TS source's PAT can change mid-stream. Today:

  • A PAT naming another PMT PID for the program adds a second reassembler, never drops the first, and leaves the catalog's program.pmtPid on the old PID. The export writes the PMT where it first did.
  • Under --program all, a program a later PAT adds is ignored, without a warning.
  • A later PAT that removes one program reaches every program's Import. The removed program's handle_pat errors and the whole multiplex stops, including the programs still listed.

Consumer: moq import ts|srt --program all on a contribution MPTS whose services come and go (a pop-up channel, a part-time feed), and any source whose re-mux moves a PMT.

Approach

The last of the three PRs for the PMT/PAT lifecycle quest, quest/m1/ts-reliability/pat-lifecycle.md (deleted here), as decided there:

  • A moved PMT is followed. The import keeps one reassembler, on the PID the latest PAT names. A PAT that moves it moves the reassembler (a section begun on the old PID is dropped) and updates program.pmtPid in place.
  • The export moves it behind a PAT. When program.pmtPid changes, the export emits a PAT at the next version_number naming the new PID. Every queued PAT and PMT copy that has not started out takes the move, as fix(mux): the TS export follows a PMT revision at the next version #6289's revisions do, and a copy whose PAT has started out keeps its PMT on the PID that PAT names. A PAT version counts once laid, as the PMT's does there, and a switch to another instance follows the last PAT version laid. A track the catalog records on the old PID as it moves takes another, since the PMT still holds it then. If the new PID is in use (a track, the PCR or SI), it keeps the PMT where it is and warns. Under --sync the tables stay as laid.
  • Programs reads every PAT itself. It cuts the input at a PAT that changes the program set, so a removed program never sees the PAT that removed it:
    • an added program publishes as the first PAT's do, from the packet after that PAT;
    • a removed one retires: its tracks finish and its broadcast closes, while the others carry on;
    • a returning program publishes a new epoch, never the retired instance's.
      A single-program import whose program leaves the PAT still ends.
  • Open question, answered here: a program whose PMT never arrives waits, unannounced. It publishes nothing until its PMT declares a track. The quest left this open, from quest(m1): TS semantic fidelity profile and implementation quests #6122. While it waits, nothing reports it: its PMT_error_2 is graded on the program clock, which a program without a PMT doesn't have yet. The alternatives are below.
  • The retired programs' stats are kept, so stats() still covers the whole input.
  • Draft: pmtPid changes in place on a moved PMT. A consumer rebuilding a stream emits a PAT at the next version_number naming the new PID, and never emits the PMT on the new PID before that PAT or on the old one after it. There is a changelog line too.

Tests

Seven tests. Five fail on the base (#6289's head), one pins the decided wait, and one the switch after a move:

  • import::a_moved_pmt_is_followed: the catalog's pmtPid follows the PAT, and a stream the PMT adds on its new PID is published.
  • programs::programs_follow_the_pat: the PAT adds program 2, drops it, then restores it. Program 1 runs throughout and its stats count every frame. Program 2 publishes when added, its broadcast closes when dropped, and its return gets a new epoch.
  • programs::a_dropped_program_does_not_end_the_multiplex: the same, fed a packet at a time.
  • programs::a_program_without_a_pmt_waits_unannounced: the listed program with a PMT is announced, the other is not, and the import doesn't fail. It passes on the base, which already waited; this test makes the decision explicit.
  • revision_test::a_moved_pmt_moves_behind_a_new_pat, on a live export: PATs at versions 0 then 1, nothing on the new PID before the moving PAT and nothing on the old one after it, and the PMT layout on the new PID round-trips.
  • revision_test::a_pmt_moved_before_its_tables_go_out_moves_every_copy, through fixture::Mux, where every copy is still queued at the move: one PAT, at version 0, naming the new PID, and nothing on the old. Without the rewrite, two version-0 PATs name different PIDs.
  • export_test::a_switch_follows_the_last_pat_out: a move with nothing left to carry it, then a switch to another instance, with PATs at versions 0 then 1. It passes on the base, which never moves the PMT; without the seeding, they go out at 0 then 2.

#6289's a_track_recorded_on_the_pmt_pid_after_the_catalog_moves_it_takes_another, whose PMT stays put, becomes a_track_recorded_on_the_pid_the_pmt_moves_from_takes_another: the PMT moves behind PAT version 1, and the track takes neither PID.

Impact

  • moq import ts|srt --program all: follows the PAT instead of ignoring additions or stopping on a removal.
  • moq export ts: follows a moved PMT.
  • Catalog: program.pmtPid can now change within a broadcast's life.
  • No wire or public API change.

Alternatives

  • For a program whose PMT never arrives: refuse it after N PATs without its PMT, or grade its PMT_error_2 against a sibling program's clock. Either one makes the wait observable. The first can end a feed whose PMT is merely late; the second makes one program's health depend on another's clock. Waiting matches what the base does and costs nothing until the PMT arrives.
  • Keep the reassembler on the old PID as well: a stale PMT there could override the new one.
  • Restart every program on any PAT change: the programs still listed would lose their groups in progress.

Stacking and overlaps

Built on #6289, rebased with it onto main now that #5949 (--sync) has merged, so its diff includes #6289's commits until that merges. It stays a draft until then and rebases onto main after. #6276 (exporters follow a moved rendition) changes the same TS export paths.

Gate

just fix, then just ci's steps one by one on macOS: lint is clean, and 3,629, 475 and 222 tests pass. The rest fail on main or the host. bun install's postinstall for @moq/libav (#6243) fails to build ffmpeg under Nix here, so bun install ran with --ignore-scripts; the drafts and markdown checks then pass. In the JS suite, which test/ts/ widens to every package, only tests that need that build fail (@moq/hang's AAC fallback and 10 @moq/watch tests), as on #6289. bench/relay.test.py fails because its fake process asserts --duration is absent. The play build's __eh_frame linker warning is denied by build.warnings; the next step builds the same moq-cli binaries with play and passes.

Decisions needed

(Written by Claude Opus 5.5)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A frame held across a generation boundary follows its track to a newly recorded PID, and a PID the PMT leaves reads as gone, not quiet, until a stream returns on it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@t0ms
t0ms force-pushed the quest/m1/ts-reliability/pat-lifecycle branch from 95fa533 to 651405a Compare October 22, 2026 14:12
…ts program descriptors

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@t0ms
t0ms force-pushed the quest/m1/ts-reliability/pat-lifecycle branch from 651405a to 44436bb Compare October 22, 2026 15:31
…ed track keeps only its own descriptors, and the PMT keeps its PID

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@t0ms
t0ms force-pushed the quest/m1/ts-reliability/pat-lifecycle branch from 44436bb to bf581d6 Compare October 22, 2026 16:40
…criptor sets give way past a section, stale tracks retire once their PID is taken, and a switch follows the last PMT laid

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@t0ms
t0ms force-pushed the quest/m1/ts-reliability/pat-lifecycle branch from bf581d6 to a87e0eb Compare October 22, 2026 17:30
t0ms and others added 2 commits October 22, 2026 19:53
…treams too many for a section end, withdrawn idle tracks go, a listed track keeps its descriptors, and a takeover counts its silence from its listing

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator

Closing this for now; the branch stays on your fork. Two things moved under it:

  • shared-parser (moq-dev/moq#6371) lands first and replaces the per-program Programs accounting this works around. The PAT changes quest gets re-planned on top of it.
  • The export half builds on moq-dev/moq#6289's table handling, which is being reworked. The PAT and PMT get laid from the current table state when their slot is laid (the way --sync already does), instead of riding queued units. A moved PMT PID then becomes one more state change rather than a rewrite of queued copies.

The quest file on main keeps the plan, so nothing here is lost. The tests are worth carrying over when it restarts.

(Written by Claude Opus 5.5)

@kixelated kixelated closed this Oct 22, 2026
@t0ms
t0ms deleted the quest/m1/ts-reliability/pat-lifecycle branch October 24, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants