Skip to content

fix(mux): synced TS legs revise their PMT identically - #6542

Closed
kixelated wants to merge 2 commits into
mainfrom
quest/m1/ts-reliability/pmt-export
Closed

kixelated wants to merge 2 commits into
mainfrom
quest/m1/ts-reliability/pmt-export

Conversation

@kixelated

@kixelated kixelated commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Quest: quest/m1/ts-reliability/pmt-export.md

Summary

Under moq export ts --sync, an added track no longer fails the export, and every leg revises its PMT and lanes at the same slot. This builds on #6289, which did the non-sync revision.

Reproduced first: two synced legs with an AAC track added mid-stream failed with TS track layout changed after PAT/PMT was emitted under --sync: '2.aac' added.

How a revision lands (the quest's open question):

  • A track's first unit lists it, and its end retires it. The revision lands at the first table repetition (500 ms grid) at least two slots past that unit's jitter-buffer key. Every leg reads that key the same way, and reads it before its clock reaches the key, so that slot and everything placed after it are still to go out on every leg that holds the unit on time.
  • The added track goes out from its first group that opens after the revision slot. The revised PMT goes out in that slot, so it always comes before the new stream's packets. The slot's PCR still rides the old clock PID. The new clock takes over from the next slot.
  • A track listed on a PID that another track still holds (a catalog that drops one stream and lists another on its PID) cuts the old one at the new one's revision. The old one is read up to the cut, so every leg carries the same tail.
  • version_number is a function of what the PMT lists: each stream adds a term from 1 to 31, and the program descriptors add one more. A leg that joins after a revision therefore announces the same version, and a revision that adds or removes a single stream always changes it.

Mechanism:

  • lane::Lanes is a schedule of layouts by slot, one per revision. Each layout carries its tables, its clock PID and its PMT. Lanes are looked up by PID, and each stream's Run bounds its own lane.
  • Units are still planned when they are read. Each track now logs its planned units, so a revision re-plans the ones it reaches, matching a leg that read them after the revision.
  • A program descriptor change or a muxRate change, with no stream change, is logged and ignored, as the rate change already was. See Decisions needed.

Tests (two synced legs that joined at different times, a third that joins after the revision, run with each track lagging in turn):

  • synced_legs_revise_their_pmt_alike_for_an_added_track
  • synced_legs_revise_their_pmt_alike_for_a_removed_track
  • synced_legs_hand_a_pid_over_alike

Each checks the packets match from the revision on (PMTs and continuity counters included, with no position differing), the revision lands in the same slot on both legs, and the joiner's PMT and version match. New lane unit tests cover a unit that straddles a revision, a group opening in the revision slot, a unit that runs past the end of its run, and the revision slot's tables, counter flag and clock. I also checked that the legs still agree when one reads only every 300 ms. a_track_added_under_sync_fails_the_export is deleted because that behavior is gone.

Public API and wire impact

  • No public API change: Export::with_sync keeps its signature, and its rustdoc now describes revisions.
  • No wire, catalog or draft change.
  • Output change under --sync only:
    • The PMT version_number is now derived from the PMT's content instead of starting at 0.
    • An added track is carried instead of failing the export.
    • A track that ends now leaves the PMT.
    • The clock PID moves when a track takes it.
  • Non-sync output is unchanged.

Decisions needed

  1. Where a revision lands. Implemented: at the first table repetition at least two slots past the jitter-buffer key of the stream's first or last unit. Recommend keeping it. Alternatives are anchoring at the unit's decode time, which is not safe against units already placed, or at the next video keyframe, which ties audio-only programs to nothing. Cost: an added stream starts at its first group after that slot, up to about delay + 500 ms + one group late.
  2. version_number derived from content (an additive hash, so a single add or remove always changes it). The other option is for the importer to record the source PMT's version in the catalog, which is a wire change and doesn't help non-TS sources. Recommend content-derived. A collision with the previous version is possible only when two streams change at once, or when only the descriptors change (about 1 in 31).
  3. Program descriptor or muxRate change alone under --sync. No media marks where it lands, so a leg that opens after it differs from one that saw it. Options: (a) the importer records an effective media time in the mpegts section (wire change and draft update; a rate change also needs lane positions that survive a rate change); (b) apply it with the next media-anchored revision; (c) keep ignoring it with a log line. Recommend (c) in this PR, and (a) as the follow-up. The quest now tracks it.
  4. Unrecorded PIDs and adds. A track without a recorded PID takes the next free PID in catalog name order. A track added under a name that sorts before the others therefore gets a different PID on a later joiner. TS sources record their PIDs, and unique_name names sort in order of creation. Recommend leaving it for the quest's follow-up.

Checks

  • cargo nextest run -p moq-mux: 1691 passed.
  • cargo clippy --all-targets -p moq-mux -D warnings: clean.
  • just check: passed (3704 + 518 + 226 tests, lint, docs).
  • Not run: just test interop --all, since this changes no wire format, moq-ffi or gateway.

Follow-ups

  • The rewritten quest covers program descriptor and muxRate changes under --sync and PID assignment for unrecorded names.
  • Consider a --sync leg in test/ts that tsduck grades across a revision.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

https://claude.ai/code/session_01EeyiEy5FWz3z44DYZVo7vU

kixelated and others added 2 commits October 23, 2026 04:01
Under `export ts --sync`, a track the catalog adds, one that ends, and a
PID another stream takes over now revise the PMT and the lanes at the
first table repetition two slots past the jitter-buffer key of that
stream's first or last unit, which every leg reads alike and before its
clock reaches it. The lanes become a schedule of layouts by slot; units
planned before a revision is read are logged and re-planned on it. The
revised PMT's version_number follows from what it lists, so a leg that
joins after it announces it alike. An added track no longer fails the
export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeyiEy5FWz3z44DYZVo7vU
@kixelated kixelated changed the title Synced TS legs revise their PMT identically fix(mux): synced TS legs revise their PMT identically Oct 23, 2026
@kixelated

Copy link
Copy Markdown
Collaborator Author

Status: the implementation is complete for the cases where the media marks the revision (a track is added, a track ends, a PID is taken over), and just check passes. This PR stays a draft until the four items under Decisions needed are settled. Items 1 and 2 are implemented as recommended. Item 3 (a program descriptor or muxRate change alone) and item 4 (PID assignment for unrecorded names) are left in the rewritten quest.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Closing: export ts --sync is being removed in #6554, which also deletes this PR's quest (quest/m1/ts-reliability/pmt-export.md). The plain export's PMT revision already landed in #6289.

(Written by Claude Opus 5.5)

@kixelated kixelated closed this Oct 23, 2026
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.

1 participant