Skip to content

feat(moq-mux)!: importers publish stream timestamps with monotonic group starts; the catalog clock maps them to wall - #4543

Merged
kixelated merged 10 commits into
devfrom
quest/m1/remove-live
Sep 30, 2026
Merged

kixelated merged 10 commits into
devfrom
quest/m1/remove-live

Conversation

@kixelated

@kixelated kixelated commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

live() on the fMP4, MPEG-TS, and FLV importers rewrote every timestamp onto an arrival-time anchor and re-anchored an encoder restart forward. That hid the source's own timeline from everything downstream and broke same-hop importers, which must derive every timestamp from the input alone (#4388). Without live(), the importers published verbatim timestamps but advertised a construction-time catalog clock. That put the wall mapping off for any source not starting near zero, and the SRT, RTMP, and HLS gateways all took that path. MPEG-TS also shifted any rewind (loop wraps, restarts, joins) forward onto the live edge, and MKV silently dropped rewound blocks.

Approach

  • The fMP4, MPEG-TS, FLV, and MKV importers always publish the stream's own timestamps. MPEG-TS unwraps its 33-bit PTS first, and fMP4 passthrough keeps tfdt.
  • The catalog clock carries the mapping. The quest left one question open, and this PR settles it: the importer anchors the catalog clock from its first frame, rather than the caller building the catalog once the first PTS is known. The caller API is unchanged and can't be misused. The alternative would force every caller to demux ahead of constructing the importer.
    • catalog::Producer keeps the clock in its shared state. A crate-private anchor(pts) sets an unset clock once so pts reads now. Every clone, track, and HLS rendition then shares that mapping, and later calls are no-ops.
    • Config::with_clock pins the clock, and an importer never re-anchors it. This covers recordings whose zero names their real start.
    • Clock now stores one (instant, reading) pair, so it can read an arbitrary PTS now without subtracting from Instant, which fails before boot on Windows. Clock::new() is that with a 10 s reading, so the boot-time fallback is gone.
  • Group starts never go backwards, and audio never goes backwards (maintainer rule). A group starting before the previous group's start ends the import. An equal start is allowed, because strictly increasing starts aren't worth enforcing. That is a restart, which must be a new broadcast; the broadcast epoch line turns the republish into a fresh epoch.
    • container::Producer::write replaces its live-edge floor with this rule. A keyframe below the previous group's start, or any frame below the start of the group before its own, returns TimestampRewind. Frames may still dip below the previous group's content: B-frames, open-GOP pictures, and a keyframe overlapping the last frame (moq import ts aborts on continuous content join (TimestampRewind) #3798). live_edge() stays as where untimed payloads (TS sections, PES without PTS) land, but it is no longer a floor.
    • container::Consumer enforces the same rule instead of rejecting any frame below the previous group's content.
    • drafts/draft-lcurley-moq-hang.md states the rule. quest/m1/legacy-end-overshoot.md now carries it, so that quest aligns the JS consumer, which still aborts below the live edge.
    • MPEG-TS: deleted the Reanchor shift, along with the codec floor/live_edge helpers that only fed it. Loop wraps and restarts, flagged or not, now surface as TimestampRewind. A flagged discontinuity that jumps forward still publishes with its break markers.
    • fMP4 keeps its stricter CMAF rule: tfdt must advance per track (NonMonotonicDecodeTime). FLV goes through the producer rule.
    • MKV: replay across drain passes is now deduplicated by absolute byte offset instead of by timestamp. The timestamp dedup silently dropped rewound blocks (and B-frames). A rewind now surfaces as TimestampRewind, and MKV anchors the clock on its first block like the others.
  • Deleted live() (fMP4, TS, FLV), clock::Anchor/Lane, SourceMap, and Clock::source.
  • Deleted the ts-import-shared-shift quest. Rewinds are a new broadcast per the maintainer, so there is no shift left to share, and fix(moq-mux): one TS re-anchor shift per program #4513 is obsolete.

Impact

  • moq_mux::container::{fmp4,ts,flv}::Import::live removed (breaking, targets dev).
  • moq_mux::SourceMap and moq_mux::Clock::source removed (breaking).
  • moq_mux::container::fmp4::Import::discontinuity added: an fMP4 seek now keeps each track's last decode time, so an HLS sequence skip cannot rewind tfdt, and only a signalled EXT-X-DISCONTINUITY resets it.
  • moq_mux::catalog::Producer::clock() may change once, when an importer anchors it on its first frame, unless Config::with_clock set it.
  • moq_mux::Error::UnmappableTimestamp now means a first timestamp too large to map (PTS zero before 2020).
  • Behavior: moq import {ts,fmp4,flv}, import srt, import rtmp, import hls, and MKV import publish source timestamps verbatim with a catalog clock mapping the first frame to its arrival.
  • Behavior: every importer ends with an error when a group starts before the previous group's start. For MPEG-TS this includes looping playout (moq import ts: one damaged audio frame header aborts the whole broadcast instead of resyncing #2729). An IDR overlapping the previous group's last frame (moq import ts aborts on continuous content join (TimestampRewind) #3798) now publishes unshifted. An audio PES below the last published frame (moq export ts: a continuous timeline whose content restarts permanently stalls video and primary audio since #3375 #3533's shape) is still a rewind, because each audio frame is its own group.
  • container::Producer::write and container::Consumer enforce non-decreasing group starts instead of a live-edge floor. TimestampRewind's message changes to match.
  • Format: the hang draft's malformed-group rule becomes "a group's first timestamp MUST NOT be lower than any earlier group's". This is more permissive than the live-edge rule.
  • Wire: no encoding change. The catalog root clock shape is unchanged; only its value differs.

Alternatives

  • Caller builds the catalog once the first PTS is known: rejected, because it needs a demux pass before the importer exists and every caller would repeat it.
  • Withhold the catalog until the anchor: FLV already does this, since its reservation drops on the first media frame. fMP4 and MKV publish after their headers, so their first snapshot can carry the provisional default clock, which the anchored one replaces before any frame is written. Holding fMP4's reservation to the first fragment would also hold back HLS catalogs behind every rendition's first segment. Accepted by the maintainer.

Follow-ups

  • A first timestamp older than 2020 relative to now (for example an fMP4 tfdt counted from 1970) cannot be anchored and errors out. A signed or rebased wall would lift that.
  • The JS consumer still aborts on a group below the live edge; quest/m1/legacy-end-overshoot.md now owns aligning it.
  • Looping TS playout now ends the import. Whether callers such as moq import srt should republish automatically belongs to the broadcast epoch line.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits September 29, 2026 13:24
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k maps them to wall

Delete live() from the fMP4, MPEG-TS, and FLV importers. They publish the source's own timestamps and anchor the catalog clock on their first frame, unless Config::with_clock pinned it. fMP4 and FLV rewinds now end the import with an error. Removes SourceMap, Clock::source, and the crate-private Anchor/Lane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 2 commits September 29, 2026 15:14
A backward timestamp is an encoder restart and a new broadcast. MPEG-TS drops the Reanchor shift, so loop wraps, restarts, and joins below the edge end the import with TimestampRewind. MKV anchors the catalog clock on its first block and dedups drain replays by byte offset, so a rewind errors instead of being dropped. Deletes the ts-import-shared-shift quest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title feat(moq-mux)!: importers publish stream timestamps; the catalog clock maps them to wall feat(moq-mux)!: importers publish stream timestamps, refuse rewinds; the catalog clock maps them to wall Sep 29, 2026
@kixelated
kixelated marked this pull request as ready for review September 29, 2026 23:12
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 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-29T23:53:42.251012Z d4424da New commits
ℹ️ 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

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 🔄 Running since 2026-09-29T23:12:40.312536Z 798ce05 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: 798ce057fb

ℹ️ 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 thread rs/moq-mux/src/container/fmp4/import.rs Outdated
Comment on lines 1024 to 1025
track.pending_sequence = Some(sequence);
track.last_decode_time = None;

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 Preserve decode-time history across seeks

When an HLS import skips a sequence or crosses an EXT-X-DISCONTINUITY, push_segment calls seek before decoding the next segment. Clearing last_decode_time here allows that segment's tfdt to rewind without triggering NonMonotonicDecodeTime, and the fMP4 passthrough then publishes timestamps below earlier groups under the unchanged catalog clock instead of ending the import as intended. Keep the prior decode time across this sequence-only seek, or otherwise compare the next tfdt against it.

AGENTS.md reference: rs/moq-mux/AGENTS.md:L9-L9

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.

Leaving this for the maintainer: it turns on the rewind rule, which is still being decided (the PR now uses monotonic group starts). Whether an HLS sequence skip or EXT-X-DISCONTINUITY keeps the prior decode time depends on whether a discontinuity may legitimately rewind tfdt. Recommendation: keep the prior decode time across a sequence-only skip (a skip is not a timebase break), and reset it only on a signalled discontinuity, which the plan treats as a new epoch.

(Written by Claude Opus 5.5)

container::Producer and Consumer refuse a group starting before the previous group's start, or a frame below the start of the group before its own, instead of any frame below the previous group's content. A keyframe overlapping the previous group's last frame now publishes. The hang draft states the rule, and quest/m1/legacy-end-overshoot.md carries it to the JS consumer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title feat(moq-mux)!: importers publish stream timestamps, refuse rewinds; the catalog clock maps them to wall feat(moq-mux)!: importers publish stream timestamps with monotonic group starts; the catalog clock maps them to wall Sep 29, 2026

@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: d4424da51f

ℹ️ 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 +411 to +415
let floor = match frame.keyframe {
true => self.start,
false => self.floor,
};
if floor.is_some_and(|floor| timestamp_lt(frame.timestamp, floor)) {

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 Align the browser rewind check with group-start ordering

When a new keyframe overlaps the previous group's tail but remains above its start, this Rust producer now emits it, while js/hang/src/container/consumer.ts lines 388-408 still compares later groups against the maximum delivered timestamp and aborts the track as being below the live edge; js/hang/src/container/legacy.ts lines 195-198 likewise refuses producing the same valid sequence. Browser consumers will therefore reject Rust broadcasts using the newly allowed overlap semantics, so update the JS producer, consumer, and tests with this container change.

AGENTS.md reference: rs/moq-mux/AGENTS.md:L12-L14

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.

Leaving this for the maintainer: it follows from the group-start rewind rule, which is still being decided. If monotonic group starts stand, the JS consumer (js/hang/src/container/consumer.ts) and legacy producer (legacy.ts) need the same group-start comparison so browsers accept overlapping keyframes, and that JS change should land in this PR. If the rule reverts to a live-edge floor, nothing changes in JS.

(Written by Claude Opus 5.5)

@kixelated kixelated left a comment

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.

Automated review by review (OpenAI)

Reviewed head: d4424da

Direction: worth doing. Source-owned timestamps plus one catalog mapping remove substantial translation/restart machinery and fit redundant ingest. Prefer targeted completion over a new abstraction: cover every timestamp-emitting route, preserve sequence-only seek history, and ship the JS ordering semantics alongside Rust.

New P2 below: the clock-only MPEG-TS route can publish timestamped SCTE-35 sections without anchoring the catalog.

I also source-confirmed the existing browser overlap finding (r4139488387) and fMP4 seek/decode-history finding (r4139281525), so I am not duplicating those inline. The JS counterpart is explicitly deferred, but remains an integration/release blocker: groups [100,133,166] then [150,183] now pass Rust while the browser still rejects the second group. The seek reset itself predates this PR's non-live mode; it is an unclosed hole in the new no-rewind guarantee after translation is removed. HLS calls that path on sequence gaps/discontinuities.

Verification: static review of all production diffs, changed test boundaries, affected importers, producer/consumer, HLS caller, and JS checks. No build, runtime, or packet-level tests executed; the Rust/Bun toolchains are unavailable here.

(Written by OpenAI)

Comment on lines +692 to +695
// The first PES is live on arrival: its PTS needs no unwrap yet.
if let Some(pts) = pending.pts {
self.catalog.anchor(Timestamp::from_scale(pts, 90_000)?)?;
}

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.

P2: Anchor the catalog for section-only MPEG-TS output

The catalog is now anchored only when flushing a PES, but MPEG-1/2 video uses Stream::Clock and returns from handle_pes_start before entering the pending/flush path. An input with MPEG-2 video plus SCTE-35 sections, without an audio or other published PES stream, therefore never anchors its catalog: SectionStream::emit publishes the video's source PTS while the catalog retains its construction-time clock. A first video PTS of 3600 seconds produces cues whose advertised wall time is approximately 3590 seconds in the future. The previous live path anchored during handle_pes_start, so this regresses CLI TS imports. Anchor before publishing timestamped sections, including the Stream::Clock path, and add a section-only clock-mapping regression test.

Automated finding by review (OpenAI). (Written by OpenAI)

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.

Agreed, and reproduced: with MPEG-2 video as the only PES, the catalog clock stayed at its construction-time reading. 6118ee0 anchors the catalog on a clock-only stream's PTS in handle_pes_start, and adds a_clock_only_stream_anchors_the_catalog, which fails without the fix.

(Written by Claude Opus 5.5)

…ions

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

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

A group starting at the previous group's start is a rewind too, in the producer, consumer, and hang draft. Untimed payloads and cues sharing the previous cue's instant land 1 us past the latest group start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title feat(moq-mux)!: importers publish stream timestamps with monotonic group starts; the catalog clock maps them to wall feat(moq-mux)!: importers publish stream timestamps with strictly increasing group starts; the catalog clock maps them to wall Sep 30, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Reverts the strict group-start rule: only a start below the previous group's is a rewind. Drops Producer::next_start, so cues and untimed payloads land on the live edge again, and the draft says MUST NOT be lower than.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title feat(moq-mux)!: importers publish stream timestamps with strictly increasing group starts; the catalog clock maps them to wall feat(moq-mux)!: importers publish stream timestamps with monotonic group starts; the catalog clock maps them to wall Sep 30, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

…y resets it

An HLS media-sequence skip no longer lets the next segment's tfdt rewind unnoticed. fmp4::Import::discontinuity starts a new timeline, and the HLS importer calls it when the discontinuity sequence changes.

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

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@kixelated
kixelated merged commit 7d57479 into dev Sep 30, 2026
8 checks passed
@kixelated
kixelated deleted the quest/m1/remove-live branch September 30, 2026 03:19
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