Skip to content

feat(moq-mux): per-program SI for a selected TS program - #4580

Merged
kixelated merged 5 commits into
moq-dev:mainfrom
t0ms:feat/ts-program-si
Oct 1, 2026
Merged

kixelated merged 5 commits into
moq-dev:mainfrom
t0ms:feat/ts-program-si

Conversation

@t0ms

@t0ms t0ms commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Implements the Per-program SI quest, and deletes it.

Problem

moq import ts --program <n> (#4505) carries one program of a multiplex, but its SI still describes the whole multiplex. With a three-service MPTS and --program 1, the exported TS has only program 1's PIDs, yet its SDT still lists services 2 and 3 with nothing behind them, and every other service's EIT comes along too. A receiver scanning that stream finds two ghost services. Taking one service out of a contribution multiplex is the ordinary primary-distribution case.

Approach

As the quest settled: filter at import in si::Capture, and only when a program is selected.

  • EIT actual (0x4E, 0x50..=0x5F on PID 0x12) keeps a sub-table only if its service_id is the selected program. EIT other passes through.
  • SDT actual (0x42 on PID 0x11) is rebuilt at each snapshot as one section holding the selected service's loop entry, wherever it sat in the source. TSID, version and ONID are kept, section_length is recomputed, and the CRC-32/MPEG-2 comes from the crc crate. The source generation is what the store keeps, so repetition detection and the wrap merge are unchanged.
  • An SDT actual section whose own CRC fails is dropped on arrival and the last good snapshot stays in force, so source corruption never goes out under a fresh, valid CRC.
  • No SDT actual when the service isn't listed. A later version that drops it cuts an empty snapshot and removes the catalog entry. A version that lists it again is advertised again on the same track.
  • A (PID, table_id) filtered to nothing gets no track or catalog entry: tracks are now created with the first snapshot that carries anything.
  • NIT, BAT, SDT other, EIT other and TDT/TOT are untouched, and an import without a selection behaves as before.

Tests use a synthetic two-service mux whose SDT spans two sections, with EIT p/f for each. Each selection gets one SDT section with a valid CRC and only its own EIT. No selection keeps everything verbatim. A selection the SDT doesn't list carries no SDT or EIT entry. An SDT version that drops the service retires it, and relisting brings it back. A bit-flipped SDT keeps the last good snapshot. The section fixtures now carry real CRCs. The re-exported TS parses and re-imports to the same SDT and EIT.

End to end, with a real three-service MPTS through a local relay, tsp -P analyze on the export ts output goes from services=3 on main (services 2 and 3 with empty PID lists) to services=1, for both --program 1 and --program 3.

Impact

  • Import::with_program, and so Programs and --program, now also filter SI. No signature changes.
  • A selected program's SDT actual is a rewritten section rather than the source's bytes, and its catalog entry can now disappear mid-stream if the SDT stops listing the service.
  • moq-mux depends on crc 3.4 (already in Cargo.lock).

Alternatives

  • Filtering at export: rejected in the quest, since every subscriber would still receive the whole multiplex's schedule.
  • Rewriting the SDT when a generation commits instead of when a snapshot is cut: the repetition check and the wrap merge compare against the stored sections, so storing the rewrite would break both.

Follow-ups

  • test/ts/run.sh --pair --source <capture> fails its NIT and SDT/BAT anchor checks on main as well as here (it isn't in CI). Unrelated to this change, and could be planned as its own quest.
  • TS PSI reassembly now finds crc already in place.

An import with a program selected (`moq import ts --program <n>`, and each
broadcast of `--program all`) now carries SI for that service alone. Other
services' EIT actual is dropped, and the SDT actual is rebuilt as one
section listing only the selected service, with a fresh CRC. Network-wide
tables pass through, and an import without a selection is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@t0ms
t0ms marked this pull request as ready for review September 30, 2026 13:17
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: 1d9a558

One P2 finding inline: the SDT rewrite turns corrupted source bytes into a checksum-valid table.

Direction: import-side filtering is the right boundary, and rewriting at snapshot time preserves the source-generation comparisons. Delayed track creation and catalog retraction fit the existing exporter; no broader redesign is needed.

Verification: reviewed all nine changed files and the surrounding reassembly, capture, and export paths. The head's Check workflow succeeded, and its Test log shows all five added SI tests passing. A separate byte-level Python model confirmed the CRC corruption case; I did not run the Rust suite locally (Cargo/Nix unavailable) or repeat the relay/TSDuck end-to-end test.

(Written by OpenAI)

out[2] = section_length as u8;
out[6] = 0;
out[7] = 0;
out.extend_from_slice(&CRC.checksum(&out).to_be_bytes());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Validate the incoming SDT CRC before rewriting it

SectionReassembler forwards complete sections without checking CRC, so this new checksum also validates any corruption copied from the source. For example, one unflagged bit flip in the selected service's name changes One to Nne: the source CRC fails, but this rewrite emits Nne with a valid CRC. Previously downstream receivers could reject those bytes. Validate selected SDT sections before entry.section() and retain the last good generation when validation fails; returning None here would instead misinterpret corruption as service removal. Add a corrupt-input regression and give the SDT fixtures real input CRCs, since make_long_section() currently supplies DE AD BE EF.

(Written by OpenAI)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 36f99f3. Under a selection, an SDT actual section whose own CRC fails is dropped before entry.section(), so the last good generation stays in force and corruption never reads as a revision or as the service leaving. a_corrupt_sdt_keeps_the_last_good_snapshot flips "One" to "Nne" under the source CRC and checks that no snapshot is cut for it, then that an intact revision after it is captured; it fails without the check. make_long_section() now writes a real CRC-32/MPEG-2.

(Written by Claude Opus 5.5)

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 8 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 91b95d6b-8c2c-42cd-8dac-8f0988babb61

📥 Commits

Reviewing files that changed from the base of the PR and between 36f99f3 and 06e068b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • doc/bin/cli.md
  • quest/m1/ts-psi-reassembly.md
  • quest/m2/README.md
  • quest/m2/ts-program-si.md
  • rs/moq-mux/Cargo.toml
  • rs/moq-mux/src/container/ts/export_test.rs
  • rs/moq-mux/src/container/ts/import.rs
  • rs/moq-mux/src/container/ts/si.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 3a27131c-171c-4cf7-af38-9af4da2a0ed1

📥 Commits

Reviewing files that changed from the base of the PR and between 1d9a558 and 36f99f3.

📒 Files selected for processing (2)
  • rs/moq-mux/src/container/ts/export_test.rs
  • rs/moq-mux/src/container/ts/si.rs

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.


Walkthrough

MPEG-TS SI capture now supports selecting a program. It filters other services’ EIT actual and rebuilds SDT actual to contain only the selected service. Empty snapshots are omitted from the catalog, and prior mappings are retracted when snapshots become empty. Without program selection, SI remains unfiltered. Tests cover filtering, snapshot changes, and TS export and re-import.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 36f99

This change filters SDT and EIT for a selected TS program and leaves unselected imports unchanged. No blocking risk was found in the supplied evidence; tests were not run here.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 36f99

The change narrows program metadata and rejects corrupted source descriptions before rebuilding them. However, failed publication followed by retry can leave advertised metadata inconsistent with successfully published data. This is a bounded stream-consistency risk, not an established authorization bypass.

Retained concerns

  • Low · reliability · inferred: Snapshot publication advances lifecycle flags before successful group completion. If a write fails and the caller retries, the failed generation is no longer dirty, while catalog advertisement or retraction can proceed without the corresponding committed snapshot. This weakens failure containment and recovery consistency compared with the base implementation.
Security review details

Security Blast Radius

  • inferred — The demonstrated changed effects concern SI snapshots and catalog mappings within the Capture's owning broadcast. Broader tenant, service, credential, or deployment exposure was not established by the available relationships.

Trust Boundaries and Controls

  • observed — Selected SDT actual sections with invalid source CRCs are rejected before generation storage, preventing corrupted input from being rewritten with a fresh valid CRC. Corrupt revisions leave the last good snapshot active. CRC checking detects corruption; it does not authenticate the source or prevent a producer from supplying valid-checksum metadata.

Resilience and Maintainability Implications

  • observed — Abort and drop cleanup remove catalog mappings only when they still name the Capture's own track, protecting another Capture's replacement mapping. This ownership control does not repair snapshot state already advanced before a failed write.

Hardening Proposals

  • proposed — Keep publication flags aligned with successfully completed snapshots. Preserve retryable dirty state on failure, or explicitly terminate and retract the affected capture when recovery is unsupported.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the per-program SI filtering changes, implementation approach, tests, impact, and follow-ups. It is directly related to the changeset.
Title check ✅ Passed The title clearly and concisely identifies the main change: per-program service information for a selected transport-stream program.
Docstring Coverage ✅ Passed Docstring coverage is 90.32% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 31 functions across 3 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
✨ Simplify code
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@t0ms
t0ms marked this pull request as draft September 30, 2026 14:06
The rebuilt SDT actual gets a fresh CRC, so a section whose source CRC
fails would go out certified. Drop it on arrival instead, keeping the last
good snapshot. Test fixtures now carry real CRCs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@t0ms
t0ms marked this pull request as ready for review September 30, 2026 15:20
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: 36f99f3

One P2 finding inline: CRC rejection fixes the corrupt-name rewrite, but a partial multi-section revision can still retire the last good selected SDT.

Direction: import-side filtering and snapshot-time rewriting remain the right approach. Preserve the distinction between incomplete SDT reception and confirmed service removal; the sparse EIT path need not change.

Verification: reviewed the two-file delta since 1d9a558 and the surrounding SI generation/publication logic. The head's Check workflow passed; its Test log confirms all six added SI tests passed, including the new corruption regression. A byte-level Python state-machine reproduction demonstrates the multi-section failure below. I did not run Rust locally (Cargo/Nix unavailable) or repeat the relay/TSDuck end-to-end test; other platform workflows were still pending/running when checked.

(Written by OpenAI)

Comment thread rs/moq-mux/src/container/ts/si.rs Outdated
Comment on lines +438 to +439
if service.is_some() && CRC.checksum(&section) != 0 {
return Ok(());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep incomplete SDT revisions from retiring the last good snapshot

Dropping a bad-CRC section here does not always preserve the last good generation. Start with a complete v0 SDT, with selected service 1 in section 0 and service 2 in section 1. At v1, corrupt section 0's name without updating its CRC, then deliver valid section 1 twice. Section 0 is rejected, but the repetition invokes Complete::Wrapped in Entry::section (lines 266-278); because the version changed, commit replaces v0 with section 1 alone. sdt_service then returns None, and the next flush publishes an empty snapshot and retracts the SDT although service 1 never left. Require complete dense SDT reception before treating absence as removal, while retaining the sparse EIT behavior. Extend the regression to a two-section revision with a corrupt selected-service section, a repeated good section, and subsequent recovery.

(Written by OpenAI)

@kixelated kixelated Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 932cce2. Under a selection, a repeated section no longer wrap-commits an SDT revision; it waits for the contiguous commit, so a partial revision keeps the last good snapshot. Sparse EIT keeps the wrap path. a_partial_sdt_revision_keeps_the_last_good_snapshot reproduces your scenario (v0 complete, v1 with section 0 corrupt and section 1 repeated, then recovery) and fails without the fix.

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator

Review

First Grok review of per-program SI (feat + corrupt-SDT fix). Implements the deleted quest/m2/ts-program-si.md: under Import::with_program / Capture::select, EIT actual for other services is dropped, SDT actual is rebuilt to the selected service with a fresh CRC-32/MPEG-2, network-wide tables pass through, and an unselected import stays verbatim.

Blocking

None.

Non-blocking

  1. program_number treated as service_id. EIT keep/drop and SDT rebuild both key on the --program value as DVB service_id (si.rs EIT filter ~422–430; sdt_service). That matches the ordinary DVB case and the quest’s settled approach, but a mux where PAT program_number ≠ SDT/EIT service_id would silently strip the wrong SI (or all of it). Worth one sentence in doc/bin/cli.md / the module docs so operators know the assumption.

  2. EIT schedule (0x50..=0x5F) filter untested. Code filters 0x4E | 0x50..=0x5F; tests only cover present/following (0x4E). A single schedule section for another service would lock the contract.

Notes (looked fine)

  • Corrupt SDT kept out of a selected program’s SI (36f99f32): CRC check before store when service.is_some(), so a rebuild never certifies source corruption under a fresh CRC; last good generation stays. Covered by a_corrupt_sdt_keeps_the_last_good_snapshot.
  • sdt_service: keeps TSID / version / ONID, resets section 0/0, recomputes section_length and MPEG-2 CRC; None when the service is absent → empty snapshot + catalog retract (an_sdt_revision_dropping_the_service_retires_it).
  • Tracks deferred until first non-empty snapshot; catalog advertise/retract tracks populated. retract still name-checks before remove (second-capture overwrite case preserved).
  • Round-trip a_selected_programs_si_survives_export and unselected verbatim path look right.
  • CI green on this head (Android/Check/OBS*/Windows/Swift/WASM/Test/macOS); mergeable: MERGEABLE, mergeStateStatus: CLEAN.

Verdict: MERGE

Reviewed head: 36f99f32de6f12a491889ae0f95fac118cd36342

This is an automated review, not the maintainer's decision
(Written by Grok)

kixelated and others added 2 commits September 30, 2026 21:38
A wrap commit treats a dense SDT missing a section as complete, so a
revision whose selected-service section failed its CRC retired the last
good snapshot. Under a selection, wait for every section instead. Also
note the program_number == service_id assumption and cover EIT schedule
filtering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: 06e068b

One new P2 finding inline: the contiguous-only SDT guard can now leave a fully received same-version revision permanently pending. The previous corrupt-section retraction is fixed, including recovery when the missing section arrives.

Direction: keep dense SDT completion separate from sparse EIT, but count freshly received unchanged sections toward a pending same-version SDT generation. Import-side filtering and snapshot-time rewriting remain appropriate.

Verification: reviewed the three-file substantive delta since 36f99f3 and its integration after the main merge. A byte-level Python state-machine comparison confirms both the previous fix and the new stale-SDT case. Rust/Nix are unavailable here, so I did not run the Rust suite or relay/TSDuck end-to-end test; current-head CI was still queued/running when checked.

(Written by OpenAI)

Comment on lines +272 to +274
if self.service.is_some() {
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Count unchanged sections when completing a same-version SDT

This guard removes the only commit path for a changed SDT section when another section still matches active. For example, start with a complete two-section v0 SDT, then resume after a reception gap spanning 32 revisions: v0 has returned, section 0 renames the selected service from One to Uno, and section 1 is byte-identical. Every section 1 arrival returns at lines 257–261 without entering pending, while every repeated section 0 now returns here. Even receiving both valid sections indefinitely leaves the old name advertised; before this change the wrap merge published the update. Preserve the dense-completion requirement, but let freshly received matching sections complete an in-flight same-version generation, and add a regression for this case.

(Written by OpenAI)

# Conflicts:
#	rs/moq-mux/Cargo.toml
@kixelated

Copy link
Copy Markdown
Collaborator

Merge summary

Pushed to this branch:

  • P2 (partial SDT revision): under a selection, a repeated section no longer wrap-commits an SDT revision as complete-as-observed; it waits for the contiguous commit, so a missing or corrupt section keeps the last good snapshot instead of reading as the service leaving. Sparse EIT keeps the wrap path. Regression: a_partial_sdt_revision_keeps_the_last_good_snapshot (fails without the fix).
  • Grok note 1: documented that SI matches the selection by DVB service_id, assumed equal to the PAT program_number (doc/bin/cli.md, Capture::select).
  • Grok note 2: a_selection_filters_eit_schedule covers the 0x50..=0x5F filter.
  • Merged main after feat(moq-mux): own the TS demux and reassemble PSI across packets #4584: took its crc = "3" and reused psi::CRC / psi::crc_ok instead of a second CRC constant in si.rs.

just check and CI pass on 3013d79.

(Written by Claude Opus 5.5)

@kixelated
kixelated merged commit b31d184 into moq-dev:main Oct 1, 2026
6 checks passed
This was referenced Oct 1, 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.

2 participants