Skip to content

feat: relative broadcast paths for cross-broadcast catalog renditions (incl. WHEP egress)#1371

Merged
kixelated merged 11 commits into
devfrom
catalog-broadcast-override
Jul 8, 2026
Merged

feat: relative broadcast paths for cross-broadcast catalog renditions (incl. WHEP egress)#1371
kixelated merged 11 commits into
devfrom
catalog-broadcast-override

Conversation

@kixelated

@kixelated kixelated commented May 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Rewritten from scratch against dev (the original main-based implementation predated the moq-mux catalog machinery and the js/watch rework) and now hooked up end to end on the consumer side, including WHIP/WHEP and RTMP egress.

A rendition in a hang catalog can set an optional broadcast field: a path relative to the broadcast that served the catalog (e.g. "../source"), pointing at the broadcast that actually publishes the track. A transcoder can then publish a sidecar catalog that adds new renditions while pointing unchanged ones at the original broadcast, instead of re-publishing those bytes.

Schema / path math

  • moq-net: new PathRelative type (trims slashes, drops . segments, preserves ..; serde only, never on the announce/subscribe wire) and Path::resolve with excess-.. clamping. Mirrored in @moq/net as Path.normalizeRelative / Path.resolve so both sides agree byte-for-byte.
  • hang / @moq/hang: optional broadcast field on VideoConfig and AudioConfig (skipped when unset, so existing catalogs are byte-identical).

Consumer wiring

  • moq-net: origin::Consumer::request_broadcast now weakly caches dynamically-served (OriginDynamic) broadcasts by path, so the catalog and every same-broadcast rendition share one upstream subscription instead of re-invoking the handler N+1 times (which previously duplicated subscriptions, or failed with Unroutable against a one-shot handler). Announced broadcasts were already deduplicated.
  • moq-mux: new Source type built via Source::new(origin, path) (an OriginConsumer plus the catalog broadcast's path). Origin is required: a source without one could never follow a cross-broadcast reference, so there's no bare-BroadcastConsumer path and no MissingOrigin error. Both the catalog broadcast and any referenced sibling resolve through OriginConsumer::request_broadcast, which deduplicates announced paths (a self-reference or excess-.. falls back to the catalog broadcast). All exporters (fmp4, mkv, ts, flv, h264, h265) now take a Source; Source::broadcast() is async.
  • moq-cli subscribe, moq-srt egress, moq-hls, the moq-rtc WHEP server and WHIP client egress, and the moq-rtmp server play path and client push all pass their origin + path through, so every place the exporters run honors cross-broadcast renditions. moq_rtc::Client::publish and moq_rtmp::Client::publish now take (origin, path); moq_hls::Broadcaster::new is async + fallible.
  • @moq/watch: Broadcast.relativeBroadcast(effect, rel) resolves the reference against the broadcast name and consumes the resolved path on the same connection, scoped to the caller's effect (no manual cache: conn.consume mints an independent consumer and each rendition subscribes a distinct track, so the previous per-path cache added no real dedup). Wired into the audio + video decoder and MSE pipelines.

Public API changes

  • New: moq_net::{PathRelative, PathRelativeOwned}, moq_net::Path::resolve (plus an internal request_broadcast dedup, no signature change), moq_mux::Source (new(origin, path) / async broadcast() / subscribe_track()), hang::catalog::{VideoConfig,AudioConfig}::broadcast, @moq/net Path.resolve/Path.normalizeRelative, @moq/hang RelativeBroadcastSchema, @moq/watch Broadcast.relativeBroadcast.
  • Breaking (hence dev): moq-mux exporter constructors, moq_hls::Broadcaster::new (now async + Result), moq-cli's Subscribe::new, moq_rtc::EgressSource::new, moq_rtc::Client::publish, and moq_rtmp::Client::publish all take a Source / (origin, path) instead of a BroadcastConsumer. Catalog format gains an optional field.

Not covered (follow-ups)

  • moq-ffi (and the py/swift/kt/go wrappers) and moq-gst consume tracks from a bare BroadcastConsumer and don't build a Source, so they don't resolve references yet; they need origin context plumbed the same way.
  • The Rust client origin only resolves referenced broadcasts that are announced (or served by a dynamic handler); there's no client-side "subscribe by arbitrary path" fallback like conn.consume(path) in JS.
  • @moq/net conn.consume(path) does not yet deduplicate the way Rust request_broadcast now does (JS close() closes shared state, so it needs a consume-ownership rework). Tracked in js/net: dedupe conn.consume() so repeat subscribes to a path share one broadcast #2121; the current js/watch path works for announced broadcasts (all the browser hits) but churns on effect rerun.

Test plan

  • PathRelative normalization + Path::resolve unit tests (Rust and JS mirrors)
  • Catalog serde round-trip: field deserializes, None stays off the wire, empty string normalizes (Rust hang + @moq/hang zod)
  • moq_mux::Source unit tests: no-override, self-reference reuse, excess-.. fallback, referenced-broadcast resolution through a live origin
  • Rust: cargo test + clippy (default features) across moq-mux, moq-hls, moq-rtc, moq-srt, moq-rtmp, moq-cli; JS: tsc + biome on the watch changes
  • Manual end-to-end: publish room/source, publish a sidecar catalog with broadcast: "../source", verify @moq/watch and moq subscribe pull the track from the source broadcast

(Written by Claude Opus 4.8)

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This change adds an optional per-rendition broadcast reference to audio and video configs and implements path utilities to normalize and resolve relative broadcast strings. Runtime code (Broadcast.trackBroadcast and updated decoder/MSE codepaths) now resolves and caches override broadcasts and falls back to the catalog broadcast when absent or self-referential. Importers populate the new field as None by default. Rust and JS tests exercise normalization, resolution, serialization, and subscription behavior.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Title check ✅ Passed The title accurately summarizes the main change: relative broadcast paths for cross-broadcast renditions.
Description check ✅ Passed The description is clearly related to the changeset and explains the catalog, path, and consumer wiring updates.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch catalog-broadcast-override

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.

@kixelated kixelated marked this pull request as draft May 5, 2026 16:07
@kixelated kixelated marked this pull request as ready for review May 23, 2026 17:58
@kixelated kixelated marked this pull request as draft June 16, 2026 19:53
A hang catalog rendition can now set an optional `broadcast` field, a
path relative to the broadcast that served the catalog (e.g. "../source"),
pointing at the broadcast that actually publishes the track. A transcoder
can then publish a sidecar catalog that adds new renditions while pointing
unchanged ones at the original broadcast instead of re-publishing the bytes.

- moq-net: new PathRelative type (normalized, ".."-capable, serde-only) and
  Path::resolve. Mirrored in @moq/net as Path.normalizeRelative/Path.resolve.
- hang / @moq/hang: optional `broadcast` field on VideoConfig and AudioConfig.
- moq-mux: new Source type bundling the catalog BroadcastConsumer with
  optional (OriginConsumer, path) context. Exporters take `impl Into<Source>`
  (bare BroadcastConsumer still works via From) and the shared ExportSource
  resolves per-rendition references via OriginConsumer::request_broadcast.
- moq-cli subscribe, moq-srt egress, and moq-hls pass their origin through.
- @moq/watch: Broadcast.trackBroadcast() resolves the reference against the
  broadcast name and consumes the resolved path on the same connection,
  caching one subscription per referenced broadcast; wired into the audio and
  video decoder and MSE pipelines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kixelated kixelated force-pushed the catalog-broadcast-override branch from be48c4c to 87b2f9b Compare July 2, 2026 04:07
@kixelated kixelated changed the title hang: cross-broadcast track references in renditions feat: relative broadcast paths for cross-broadcast catalog renditions Jul 2, 2026
@kixelated kixelated changed the base branch from main to dev July 2, 2026 04:08
@kixelated kixelated closed this Jul 2, 2026
@kixelated kixelated reopened this Jul 2, 2026
…-917897

# Conflicts:
#	js/watch/src/broadcast.ts
#	rs/moq-cli/src/main.rs
#	rs/moq-cli/src/subscribe.rs
#	rs/moq-hls/src/export/mod.rs
#	rs/moq-hls/src/export/rendition.rs
#	rs/moq-mux/src/codec/h264/export.rs
#	rs/moq-mux/src/container/fmp4/export.rs
#	rs/moq-mux/src/container/mkv/export.rs
#	rs/moq-mux/src/lib.rs
#	rs/moq-srt/src/ts.rs
kixelated and others added 2 commits July 6, 2026 20:59
# Conflicts:
#	js/watch/src/video/decoder.ts
#	rs/moq-cli/src/subscribe.rs
#	rs/moq-hls/src/export/mod.rs
#	rs/moq-hls/src/export/rendition.rs
#	rs/moq-hls/src/server/mod.rs
#	rs/moq-mux/src/codec/h264/export.rs
#	rs/moq-mux/src/codec/h265/export.rs
#	rs/moq-mux/src/container/flv/export.rs
#	rs/moq-mux/src/container/fmp4/export.rs
#	rs/moq-mux/src/container/mkv/export.rs
#	rs/moq-mux/src/container/source.rs
#	rs/moq-mux/src/container/ts/export.rs
The WHEP egress subscribed each rendition track directly on the catalog
broadcast, ignoring a rendition's catalog `broadcast` field. A sidecar
catalog that points a rendition at a sibling broadcast (e.g. `../source`)
therefore failed to egress that track over WebRTC.

Add `moq_mux::Source::subscribe_track`, the async counterpart to the
poll-driven container exporters: it resolves an optional cross-broadcast
reference against the origin and subscribes, returning a raw
`track::Subscriber` for consumers that wrap it themselves.

`EgressSource::new` now takes `impl Into<moq_mux::Source>` and `pick_track`
routes each rendition through `subscribe_track`, so the WHEP server (which
already holds the subscribe origin) resolves references the same way as
moq-cli/moq-srt/moq-hls. A bare `broadcast::Consumer` still works via
`From` (the WHIP client path), degrading to `Error::MissingOrigin` only if
such a catalog actually references another broadcast.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kixelated kixelated changed the title feat: relative broadcast paths for cross-broadcast catalog renditions feat: relative broadcast paths for cross-broadcast catalog renditions (incl. WHEP egress) Jul 7, 2026
kixelated and others added 2 commits July 7, 2026 04:59
cargo deny check advisories flags crossbeam-epoch 0.9.18 (invalid pointer
dereference in the fmt::Pointer impl for Atomic/Shared). Transitive dep;
lockfile-only bump to the patched 0.9.20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	rs/moq-mux/src/container/flv/export.rs
Comment thread js/watch/src/broadcast.ts Outdated
Comment thread js/watch/src/broadcast.ts Outdated
Comment thread rs/moq-mux/src/source.rs Outdated
@kixelated kixelated marked this pull request as ready for review July 7, 2026 21:54

@sourcery-ai sourcery-ai 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.

Sorry @kixelated, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

kixelated and others added 4 commits July 7, 2026 16:50
Reshape moq_mux::Source per review: require an origin and the catalog
broadcast's path instead of a pre-resolved BroadcastConsumer + optional
origin context. A Source without an origin could never follow a
cross-broadcast rendition reference, so making origin mandatory removes
that footgun and the Error::MissingOrigin it needed.

Source::new(origin, path) now resolves both the catalog broadcast and any
sibling broadcast a rendition's catalog `broadcast` field references
through origin.request_broadcast, which deduplicates announced paths. This
drops From<BroadcastConsumer>, with_origin, and the Into<Source> sugar on
every exporter, so fmp4/mkv/ts/flv/h264/h265 exporters, moq_hls::Broadcaster,
and moq-cli's Subscribe now take a Source directly. broadcast() is async
(it resolves through the origin), and Broadcaster::new is async + fallible.

Callers thread origin + path: moq-cli subscribe, moq-srt, moq-hls, the WHEP
server, and now also the WHIP and RTMP client egress paths and the RTMP
server play path, so cross-broadcast renditions are honored everywhere the
exporters run. moq_rtc::Client::publish and moq_rtmp Client::publish take
(origin, path) to match.

JS (js/watch): rename Broadcast.trackBroadcast to relativeBroadcast and drop
the per-path override cache. conn.consume mints an independent consumer and
each rendition subscribes a distinct track, so the cache added no real
dedup; the reference now resolves inline, scoped to the caller's effect like
the catalog broadcast.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The concept doc still showed the removed Source::new(broadcast).with_origin(origin, path)
API and the bare-BroadcastConsumer error path. Describe the current shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
request_broadcast only deduplicated announced broadcasts and concurrently-queued
requests: a dynamically-served (OriginDynamic) broadcast is never announced, and
the handler removes the queue entry when it serves, so each later request for the
same path re-invoked the handler and opened a duplicate upstream subscription (or,
with a one-shot handler, failed with Unroutable).

Cache served broadcasts weakly in OriginDynamicState, keyed by path, mirroring the
existing weak-dedup that broadcast::Consumer::track already does for tracks. A repeat
request for a still-live served path now resolves to a shared clone; a closed entry
is evicted lazily and re-served. Weak so the broadcast still closes once its real
consumers drop.

This makes moq_mux::Source's "resolve everything through request_broadcast" correct
for the dynamic path (WHEP egress, JIT transcode workers): the catalog and every
same-broadcast rendition share one upstream subscription instead of spinning up N+1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
relativeBroadcast calls #isPathAnnounced in a per-rendition reactive path, so the
Cloudflare 'no discovery' warning fired on every re-evaluation. Gate it behind a
per-connection WeakSet so it logs at most once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kixelated kixelated enabled auto-merge (squash) July 8, 2026 02:30
@kixelated kixelated merged commit 26d9efa into dev Jul 8, 2026
2 checks passed
@kixelated kixelated deleted the catalog-broadcast-override branch July 8, 2026 02:49
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