Skip to content

feat(cli): publish stdin imports on the broadcast clock - #4122

Merged
kixelated merged 2 commits into
mainfrom
quest/m1/cli-import-clock
Sep 25, 2026
Merged

kixelated merged 2 commits into
mainfrom
quest/m1/cli-import-clock

Conversation

@kixelated

@kixelated kixelated commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

moq import ts|fmp4|flv published the input's own timestamps against a catalog clock sampled at startup. A TS feed whose PTS starts hours in, or whose first frame arrives late, advertised the wrong wall time. A source that restarted its timestamps rewound (FLV, TS clamped it to the live edge) or was refused (fMP4 NonMonotonicDecodeTime).

Approach

  • The fMP4, TS, and FLV importers gain live(). It translates onto the catalog's broadcast clock through one mapping per input:
    • the first frame is live on arrival;
    • every track keeps its source offset, so A/V sync and B-frame order are exact;
    • a restart continues forward after the real idle gap, and at least past the last frames' end.
  • A muxed input can't use one SourceMap, because interleaved audio and video step back further than MAX_REORDER. So a crate-private Anchor holds the shared offset, and each track has a Lane that detects its own restarts. The first lane to restart moves the anchor, and the other lanes adopt the new mapping instead of re-applying it. SourceMap is now one anchor plus one lane, so both run the same code.
  • fMP4 passthrough rewrites each fragment's tfdt. TS translates right after the 33-bit unwrap, and a PCR discontinuity_indicator counts as an explicit restart. FLV translates each tag's PTS. seek marks a restart on fMP4 and FLV.
  • An unmappable source is refused with UnmappableTimestamp.
  • Clock::new puts PTS zero 10s before construction, on both the monotonic and the wall clock. A source anchored at arrival may carry frames muxed with earlier PTS (a leading B-frame, audio ahead of video). Without the lead they would map before the broadcast began. The wall mapping is unchanged.
  • moq import enables live() for all three formats.
  • Fixtures drive each importer, not just the helper:
    • late first frame with a large starting PTS, compared frame by frame against a verbatim import (one offset across every track);
    • restart to zero, both immediate and after 300ms idle;
    • retained archive playback: the fMP4 timeline record maps to the wall time the segment went live;
    • a CLI test through Publish::new(Ts).
  • Docs: doc/bin/cli.md and doc/lib/rs/moq-mux.md. Deletes the quest and its references, which unblocks 2278-watch-absolute-wall-clock....

live() stays opt-in, so SRT, RTMP, HLS import, and existing importer tests keep publishing verbatim. The 10s lead stays in Clock::new, so every default publisher has the headroom.

Impact

  • Public API (moq-mux, additive): container::{fmp4,ts,flv}::Import::live(self) -> Self.
  • Behavior (moq-mux):
    • Clock::new() now starts now() at about 10s, and wall is 10s earlier. wall + pts is unchanged.
    • SourceMap::translate now returns the source's timescale, not micros.
    • SourceMap re-anchors after the furthest start and estimated end, not after the last frame's start.
  • CLI: stdin ts/fmp4/flv imports publish on the broadcast clock. Restarted fMP4 decode times are accepted, not refused.
  • Wire: none. No catalog field changes.

Alternatives

  • Make live() the default and have callers who pin Config::with_clock opt out. More correct by default, but it touches every importer test and changes the SRT/RTMP/HLS gateways. Left opt-in.
  • Names with_clock() or translate() instead of live(). Kept live().
  • Put the Clock::new lead only in the CLI's catalog config. That keeps Clock::new literal, but every other live() user would need to know about it. Kept in Clock::new.
  • One SourceMap per track: each track anchors on its own first frame, so A/V drift apart by their first-frame PTS difference. Rejected.
  • One SourceMap for the whole input: interleaving reads as a reset. Rejected.
  • Sampling the wall at the first frame: the catalog publishes before any frame, and the mapping must stay fixed. Rejected.

Follow-ups

  • The SRT and RTMP gateways (moq-srt, moq-rtmp) and HLS import could call live() too.
  • A forward PTS jump with no idle and no signalled discontinuity still passes straight through, as in SourceMap.

(Written by Grok 4.7)

kixelated and others added 2 commits September 25, 2026 14:34
`moq import ts|fmp4|flv` published source timestamps verbatim against a wall
sampled at startup, so a late first frame or a large starting PTS advertised
the wrong wall time, and a restarted source rewound or was refused.

The fMP4, TS, and FLV importers gain `live()`, which translates onto the
catalog's broadcast clock through one shared mapping per source: the first
frame is live on arrival, every track keeps its source offset, and a restart
continues forward after the real idle gap. fMP4 passthrough rewrites `tfdt`.
`SourceMap` now runs on the same per-source anchor, and `Clock::new` puts PTS
zero ten seconds back so earlier-muxed frames still map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated force-pushed the quest/m1/cli-import-clock branch from 21d9105 to 6ddfab7 Compare September 25, 2026 21:34
@kixelated
kixelated marked this pull request as ready for review September 25, 2026 21:34
@coderabbitai

coderabbitai Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 3 minutes.

Check out review usage here.

View limit details

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

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

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 294d0faa-cd84-47c2-bd16-70042f1ecf42

📥 Commits

Reviewing files that changed from the base of the PR and between 42c3c4c and 6ddfab7.

📒 Files selected for processing (15)
  • doc/bin/cli.md
  • doc/lib/rs/moq-mux.md
  • quest/m1/2278-watch-absolute-wall-clock-latency-target-for-synchronized.md
  • quest/m1/README.md
  • quest/m1/cli-import-clock.md
  • quest/m2/teleop/correlation.md
  • rs/moq-cli/src/publish.rs
  • rs/moq-mux/src/clock.rs
  • rs/moq-mux/src/container/flv/import.rs
  • rs/moq-mux/src/container/flv/import_test.rs
  • rs/moq-mux/src/container/fmp4/import.rs
  • rs/moq-mux/src/container/fmp4/import_test.rs
  • rs/moq-mux/src/container/test_util.rs
  • rs/moq-mux/src/container/ts/import.rs
  • rs/moq-mux/src/container/ts/import_test.rs

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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T21:41:39.110458Z 6ddfab7 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ddfab7eed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2765 to +2767
let extended = Timestamp::from_scale(unwrap.unwrap(raw), 90_000)?;
Ok(Some(match anchor {
Some(anchor) => anchor.translate(&mut unwrap.lane, extended)?,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Detect long-running TS resets before unwrapping PTS

When an encoder resets without a PCR discontinuity after its raw PTS has passed half of the 33-bit range (about 13.25 hours), PtsUnwrap::unwrap interprets the jump to zero as a normal wrap and adds 2^33 before Anchor sees it. The translated source time therefore moves forward by hours, so the lane never detects a restart and moq import ts publishes frames far in the future instead of resuming after the idle gap. Restart detection needs to consider the raw PTS or arrival progression before wrap extension.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

A signalled PCR discontinuity_indicator already calls PtsUnwrap::discontinuity, which drops the wrap baseline and marks the lane to restart, so that reset re-anchors after the idle gap even past the halfway point of the 33-bit field.

An unsignalled drop of more than half the field is the case PtsUnwrap exists for: the ~26.5h wrap. Treating it as a restart before the unwrap would also re-anchor a real wrap. Unsignalled forward jumps stay as they are; that is the follow-up already noted in the PR.

(Written by Grok 4.7)

Comment thread rs/moq-mux/src/clock.rs
Comment on lines +278 to +283
let stepped_back = lane
.last_source
.is_some_and(|last| last > src + SourceMap::MAX_REORDER.as_micros());
// A restart another lane already applied is adopted, not applied twice.
if (lane.restart || stepped_back) && lane.offset.is_some() && lane.generation == self.generation {
self.reanchor(src, now);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve TS reordering beyond 500 milliseconds

For live TS input, this classifies any per-stream PTS step backward by more than 500 ms as a restart, although the TS importer explicitly accepts reorder delays up to two seconds in reorder_delay. A valid low-frame-rate or deeply reordered GOP can therefore move the shared anchor instead of retaining its PTS ordering, shifting that video lane away from the audio until the latter independently detects a restart. Use DTS or the TS discontinuity signal for restart detection, or make this bound cover the reorder range already accepted by the importer. rs/moq-mux/CLAUDE.mdL6-L6

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

reorder_delay's 2s cap is the PTS-DTS gap that jitter ignores, not the backwards step between consecutive frames on one lane. SourceMap::MAX_REORDER (500ms) is the shared automatic bound for fMP4, TS, and FLV: a larger step back re-anchors that lane. A broadcast GOP's present-before-decode step is a frame or two (the clock test uses 20ms), which is what the importer's own comment means by reorder being well under 2s.

Widening the bound, or detecting restarts from DTS, would change reset behavior for every importer. Leaving the shared 500ms bound.

(Written by Grok 4.7)

@kixelated
kixelated enabled auto-merge (squash) September 25, 2026 21:45
@kixelated
kixelated merged commit c87159d into main Sep 25, 2026
4 checks passed
@kixelated
kixelated deleted the quest/m1/cli-import-clock branch September 25, 2026 21:52
@moq-bot moq-bot Bot mentioned this pull request Sep 25, 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