Skip to content

fix(publish): mint a new track name when a rendition's decoder identity changes - #5552

Merged
kixelated merged 15 commits into
mainfrom
quest/m1/catalog-identity-js
Oct 15, 2026
Merged

kixelated merged 15 commits into
mainfrom
quest/m1/catalog-identity-js

Conversation

@kixelated

@kixelated kixelated commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Catalog edits could redefine served audio/video tracks in place. A codec knob, an audio capture changing its rate or channels, or the Opus decoder description arriving with the first subscriber all rewrote a track's decoder config under the same name. The hang draft's Track Identity section says a name always means the same content.

Approach

Broadcast records each track name's decoder identity from the catalog's first served snapshot. Identity means only the fields a consumer cannot change without reinitializing its decoder or demuxer: codec, description, container, sampleRate, numberOfChannels, displayAspectWidth, and displayAspectHeight. When a later fold changes the identity, the rendition keeps its catalog ID and its track points at a fresh name. The old track ends cleanly on its old identity. The broadcast, its epoch, and every sibling rendition carry on, and viewers follow the catalog to the new name (js/watch already rebuilds its pipeline when track changes). Every other field, including bitrate, frame rate, and coded size, updates the served track in place, in either direction.

Fresh names are a per-broadcast counter, <id>/1, <id>/2, .... Closed-track replacement now uses the same counter. A minted name skips every name the broadcast has registered, minted, or served, and every key and track name in the catalog, including an application's own sections. Names under a rendition's ID are reserved for its replacements: registering one throws, and doc/lib/js/publish.md documents the reservation for application tracks on net.

Identities are cloned, so a producer mutating a shared container cannot rewrite one. They persist across a rendition's removal, so a name that returns with different media gets a fresh name too. Nothing is recorded before the first serve, so pre-announce churn (a camera settling its resolution) mints nothing.

The Opus description is probed with a throwaway encoder that uses the exact live config before advertising, so the first subscriber no longer changes the identity.

Decisions

  • ✅ Decided 2026-10-15 (maintainer): mint a new rendition track name, not a new epoch. This reverses the feat(hang): fix a track's catalog identity for its name #5459 decision.
    • Restart the broadcast under a new epoch (the first revision of this PR).
    • Refuse the change.
  • ✅ Decided 2026-10-15 (maintainer): only fields that force a decoder reinitialization mint a new track; everything else updates in place. This drops the falling bitrate/framerate refusal and the per-name maxima from the earlier revision.
  • ✅ Decided 2026-10-15 (maintainer): counter names <id>/<n>, collision-free since this publisher owns the namespace. This replaces <id>/<uuid>.
  • Which fields are identity (agent, derived from WebCodecs):
    • ✅ codec, description: decoder config fields; changing either reconfigures the decoder.
    • ✅ container: the consumer picks its demuxer once per track.
    • ✅ sampleRate, numberOfChannels: required AudioDecoderConfig fields.
    • ✅ displayAspectWidth/Height: part of watch's decoder config, and the draft already says changing them requires a decoder reinit.
    • Not identity: codedWidth/Height. These are optional hints, and decoders follow in-band resolution changes at keyframes. A size that needs a new level also changes the codec string, and a description pins the size where one exists.
    • Not identity: bitrate, framerate, optimizeForLatency, jitter, delay. These are live state or consumer hints.
  • When identities lock (agent):
    • ✅ From the first served catalog snapshot.
    • From the first fold. This would mint names nobody has seen.
  • Rendition configs that set broadcast or track (agent):
    • ✅ Refuse them. A rendition's media is always this broadcast's own track. A remote reference sent viewers to media the publisher never writes, and a rename would overwrite the target. No producer sets either field.
    • Support remote references. This needs a rendition without a local track and is out of scope.
  • Application tracks on net named like video/1 (agent):
    • ✅ Reserve <id>/ for replacements and document it.
    • Add a uniqueName API to @moq/net. This would be new public surface.

quest/m1/catalog-identity-js.md is completed and removed. quest/m1/catalog-identity-rs.md records the decisions for the Rust side.

Impact

  • Public API: no new exported surface.
    • Rendition.config documents which fields mint a new track. A config that sets broadcast or track is refused.
    • Registering a rendition under a reserved replacement name throws.
    • Media changes no longer recreate Broadcast.net; the first revision of this PR did.
    • CatalogProducer.mutate checks jitter/delay per section, ID, broadcast, and track name. This keeps aliases of one track apart and lets a renamed rendition's new track start over.
  • Wire: no format change. The catalog may now carry track for an identity change, as it already did for replacements.
  • Docs and spec:
    • The hang draft's Track Identity section and doc/concept/hang.md now define identity as the decoder-reinit fields and say every other field is live state.
    • The "only rises" notes on bitrate/framerate are removed from the JS and Rust catalog types.

Validation

just check passes (253 publish tests). New regressions, each confirmed to fail without its fix:

  • Bitrate, frame rate, and coded size update in place in both directions.
  • A codec change and a display aspect change each move the rendition to the next counter name. The old track ends cleanly and the epoch is unchanged.
  • A minted name skips names in the catalog, a removed rendition's track, and a replacement that was never served.
  • Registering a current or retired replacement name throws.
  • A name that returns with a new channel count gets a fresh name.
  • Pre-announce changes mint nothing.
  • A mutated shared container still renames.
  • A rendition config that points elsewhere is refused.
  • A falling estimate on one alias of a track is refused.
  • A real Video.Encoder growing its ceiling updates in place.
  • The Opus probe test asserts that the exact live config is probed before the first catalog.

Review

Codex adversarial review rounds found and fixed:

  • the shared mutable container
  • names held by removed renditions
  • a rendition registering under a minted name
  • replacement names minted without a config
  • aliased tracks collapsing in the estimate check
  • remote rendition references being overwritten

The final round on the head approved with no material findings.

Follow-ups

  • Rust enforcement: quest/m1/catalog-identity-rs.md.
  • The encoders still advertise their max-so-far bitrate, frame rate, and coded-size ceiling. They could advertise current values now that these update in place.
  • CatalogProducer's jitter/delay never-fall check compares adjacent snapshots only, so a rendition removed and restored loses its floor. This is unchanged from main.
  • A real-browser check of a live codec switch.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 14, 2026 16:06
Co-Authored-By: GPT-6 <noreply@openai.com>
Co-Authored-By: GPT-6 <noreply@openai.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Quest outcome: quest/m1/catalog-identity-js.md implemented and removed with its incoming quest references updated. Draft retained as requested.

Served identity/ceiling changes now end the old catalogs and replace the broadcast epoch; exact-config Opus probing happens before advertisement, and encoder maxima reset per network producer. No new public API or wire fields; catalog decreases are refused and custom-track handles follow Broadcast.net replacement.

Validation: complete just check, full JS build/type/lint checks, 3,342 JS tests (249 publisher tests), 483 quest documents, Chromium 153 native WebCodecs exercise, and the nine-case nightly catalog fanout benchmark pass. Before-fix served identity and closed pending timer regressions failed as expected. Logs retained locally under /tmp/media-catalog-*.log.

Follow-up: Rust track identity enforcement remains its own quest. Preserve the exact Opus probe lifecycle when combining the independent audio-warmup change.

(Written by GPT-6)

kixelated and others added 2 commits October 15, 2026 09:00
A served rendition whose codec, description, container, audio format, or
coded-size ceiling changes keeps its catalog ID and moves to a fresh
`track` name in the same epoch, ending the old track on its old identity.
This replaces restarting the whole broadcast under a new epoch, per the
2026-10-15 maintainer decision. Names are fixed once the catalog is first
served, so pre-announce churn mints nothing.

Drops the epoch handoff (per-epoch encoder limits, catalog termination,
announce gating) that only the restart needed. Keeps the Opus description
probe and the refusal of falling bitrate/framerate, now keyed by track name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title fix(publish): restart epochs when served media identity changes fix(publish): mint a new track name when served media identity changes Oct 15, 2026
kixelated and others added 10 commits October 15, 2026 09:09
A producer mutating a container it shares rewrote the stored identity, so an init change kept the same track name. Found by Codex review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same-named tracks referenced in different broadcasts are different tracks, so removing one compared the other against it. Found by Codex review.

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

A rendition re-registered on the same track name at a lower bitrate lowered the advertised maximum, though the track had carried more. Broadcast now advertises the most each served name has carried. Found by Codex review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before the first serve, a codec change with a lower bitrate was refused by the catalog, leaving the old codec advertised. Unserved folds now carry the previous fold's maxima. Found by Codex review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per the 2026-10-15 maintainer decision, only fields that force a decoder reinitialization (codec, description, container, display aspect, sample rate, channel count) mint a new track. Coded size, bitrate, and framerate update in place in either direction, so the falling-maxima refusal and per-name maxima are gone, and the hang draft, concept doc, and Rust quest drop the ceiling and maxima classes. Minted names count up per catalog ID (<id>/1, <id>/2), skipping every name the broadcast already uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Application tracks on net share the namespace, so names under a rendition's ID are reserved for its replacements rather than tracked through the network broadcast's private state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rendition removed before it had a config keeps its network track, so the counter handed its name to a replacement and createTrack threw. Found by Codex review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Registering a minted name as a new rendition pointed two renditions at one track, and the network loop stopped at the duplicate. Found by Codex review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A replacement minted before its rendition had a config was never served, so a
later mint could reuse it and a rendition could register under it. Record every
minted name for the network broadcast's life.

The estimate check keyed entries by track alone, so two catalog IDs aliasing
one track collapsed and a falling estimate on one slipped through. Key each
entry by section, ID, broadcast, and track.

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

Copy link
Copy Markdown
Collaborator Author

Follow-up planned in #5584: quest/m1/catalog-identity-js.md now asks for a real-browser run of both renames (a codec change and Video.Encoder growing past its ceiling), confirming js/watch follows the rendition to its new track without a stall, before this lands.

(Written by Claude Opus 5.5)

A rendition's media is the broadcast's own track, so a config naming another
broadcast or track sent viewers to media this publisher never writes, and an
identity change overwrote the target with a local replacement name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title fix(publish): mint a new track name when served media identity changes fix(publish): mint a new track name when a rendition's decoder identity changes Oct 15, 2026
@kixelated
kixelated marked this pull request as ready for review October 15, 2026 18:17
@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary

  • Only decoder-reinit fields (codec, description, container, sampleRate, numberOfChannels, display aspect) mint a new track. The rendition keeps its catalog ID, track moves to the next free <id>/<n>, and the broadcast epoch is unchanged. Everything else, including bitrate, frame rate, and coded size, updates in place.
  • Naming is collision-free. Minted names are never reused within a network broadcast, and <id>/ is reserved for a rendition's replacements.
  • Identities lock at the first served snapshot. The Opus description is probed before the first catalog, so the first subscriber does not trigger a rename.
  • A rendition config that sets broadcast or track is refused.
  • The hang draft, doc/concept/hang.md, doc/lib/js/publish.md, and quest/m1/catalog-identity-rs.md match the code.
  • The ✅ decisions are recorded in the description.
  • Validation: just check passed, CI is green, and the Codex adversarial review of the head approved with no material findings.

Follow-ups are in the description: Rust enforcement, encoders advertising current values, the jitter/delay floor across a removal, and a real-browser codec switch.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 15, 2026
@coderabbitai

coderabbitai Bot commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

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 14 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: a24ca8dc-df53-405b-b0b0-d4c839a8f898
📥 Commits

Reviewing files that changed from the base of the PR and between 9917c10 and f562ae6.

📒 Files selected for processing: 21
  • doc/concept/hang.md
  • doc/lib/js/publish.md
  • drafts/draft-lcurley-moq-hang.md
  • js/hang/src/catalog/audio.ts
  • js/hang/src/catalog/video.ts
  • js/publish/src/audio/capture.test.ts
  • js/publish/src/audio/encoder.test.ts
  • js/publish/src/audio/encoder.ts
  • js/publish/src/broadcast.test.ts
  • js/publish/src/broadcast.ts
  • js/publish/src/catalog.test.ts
  • js/publish/src/catalog.ts
  • js/publish/src/rendition.ts
  • js/publish/src/video/encoder.test.ts
  • js/publish/src/video/encoder.ts
  • quest/m0/broadcast-epoch/catalog-references.md
  • quest/m1/README.md
  • quest/m1/catalog-identity-js.md
  • quest/m1/catalog-identity-rs.md
  • rs/hang/src/catalog/audio/mod.rs
  • rs/hang/src/catalog/video/mod.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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 Oct 15, 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-10-15T18:21:37.754399Z f562ae6 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: f562ae6677

ℹ️ 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 +263 to +264
// The network loop ends each renamed rendition's old track and serves the new name.
if (Object.keys(renamed).length > 0) this.#names.update((names) => ({ ...names, ...renamed }));

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 Swap tracks before accepting the new media identity

When a demanded rendition changes codec, description, or another identity field while frames continue arriving, catalog.mutate publishes the new config before this update schedules the network effect that closes the old track and hands the encoder its replacement. The encoder can already be configured for the new identity during that microtask gap, so an output frame may be written under the old track name, breaking existing subscribers and the track-name invariant. Gate encoding or swap the producer before the new identity becomes active.

AGENTS.md reference: AGENTS.md:L64-L64

Useful? React with 👍 / 👎.

Merged via the queue into main with commit 3791aec Oct 15, 2026
14 checks passed
@kixelated
kixelated deleted the quest/m1/catalog-identity-js branch October 15, 2026 18:35
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