Skip to content

feat: Replace ANNOUNCE -> (PUBLISH_NAMESPACE + PUBLISH) and SUBSCRIBE_NAMESPACE#134

Open
itzmanish wants to merge 25 commits intocloudflare:mainfrom
itzmanish:feat/publish
Open

feat: Replace ANNOUNCE -> (PUBLISH_NAMESPACE + PUBLISH) and SUBSCRIBE_NAMESPACE#134
itzmanish wants to merge 25 commits intocloudflare:mainfrom
itzmanish:feat/publish

Conversation

@itzmanish
Copy link
Copy Markdown
Contributor

@itzmanish itzmanish commented Jan 27, 2026

Summary
Replaces the legacy ANNOUNCE-based namespace advertisement and subscription routing with the draft-16 aligned PUBLISH_NAMESPACE, PUBLISH, and SUBSCRIBE_NAMESPACE message flows. This is a major protocol evolution that decouples namespace advertisement from subscription routing, enabling both push-based (PUBLISH) and pull-based (SUBSCRIBE) media delivery through the relay.

Motivation
The ANNOUNCE mechanism in earlier drafts bundled namespace advertisement with subscription routing — when a publisher announced a namespace, incoming SUBSCRIBEs were routed through the Announce object directly. Draft-16 separates these concerns:

  • PUBLISH_NAMESPACE — advertises that a publisher has tracks available in a namespace (replaces ANNOUNCE)
  • SUBSCRIBE — pull-based: subscriber requests data, relay forwards upstream to publisher
  • PUBLISH — push-based: publisher proactively sends track data to the relay
  • SUBSCRIBE_NAMESPACE — subscriber discovers available namespaces from a publisher
    This separation enables more flexible relay architectures where publishers and subscribers can use different delivery strategies for the same content.

Changes

Protocol Layer (moq-transport/src/session/)

  • Removed: announce.rs (Announce/AnnounceRecv) — replaced by decoupled message handlers
  • Added: publish_namespace.rs — outbound PUBLISH_NAMESPACE (publisher → relay)
  • Added: publish_namespace_received.rs — inbound PUBLISH_NAMESPACE handler (renamed from announced.rs)
  • Added: published.rs — outbound PUBLISH with full subgroup/datagram serving (publisher → relay)
  • Added: publish_received.rs — inbound PUBLISH handler (relay side)
  • Added: subscribe_namespace.rs — outbound SUBSCRIBE_NAMESPACE
  • Added: subscribe_namespace_received.rs — inbound SUBSCRIBE_NAMESPACE handler
  • Modified: publisher.rs — removed announce(), added publish_namespace(), publish(), subscribe_namespace_received() APIs; all SUBSCRIBEs now route through unknown_subscribed queue
  • Modified: subscriber.rs — added PUBLISH message handling, publish alias tracking
  • Modified: subscribed.rs — added is_closed() cancellation checks in serve paths
  • Modified: mlog events for PUBLISH/PUBLISH_OK/PUBLISH_ERROR/PUBLISH_DONE

Relay (moq-relay-ietf/)

  • local.rs: Complete rewrite — introduced TrackInfo state machine (Pending → Subscribing → Subscribed → Publishing → Closed) and Locals registry with LocalsEntry containing TracksReader, TracksWriter, and per-track HashMap<String, Arc<TrackInfo>> for concurrent push/pull management
  • producer.rs: serve_subscribe() now uses TrackInfo system — calls get_or_create_track_info(), checks should_subscribe_upstream(), forwards via subscribe_upstream(). Added serve_subscribe_namespace() handler
  • consumer.rs: serve() renamed to serve_publish_namespace() — registers both reader AND writer with Locals. Added serve_publish() for handling inbound PUBLISH messages (push-based delivery)

Serve Layer (moq-transport/src/serve/)

  • tracks.rs: Added forward_upstream() on TracksReader, insert() and remove() on TracksWriter for relay track management
  • track.rs: Added is_closed(), largest_location(), comprehensive test suite for stale track cache eviction
  • subgroup.rs/stream.rs/datagram.rs: Added is_closed() checks

Clients

  • moq-pub: Switched from publisher.announce(reader) to publisher.publish_namespace(namespace) + explicit serve_subscriptions() loop
  • moq-clock-ietf: Updated to use publish_namespace() API
  • moq-test-client: Updated scenarios for PUBLISH message flow

Bug Fixes Included

  • fix: prevent moof/mdat interleaving in moq-sub — The new relay architecture (shared TrackInfo with OnceLock + inline forward_upstream) delivers track data with tighter timing than the old announce-based path, which increases the probability of concurrent audio/video stdout writes interleaving between moof and mdat boxes. This caused H.264 decode errors (Invalid NAL unit size where 0x74726166 = "traf" box type was parsed as NAL data). Fixed by buffering moof and writing moof+mdat as a single atomic operation in moq-sub's recv_group().
  • fix: detect and evict stale cached TrackReadersTracksReader::subscribe() now checks is_closed() before returning cached tracks, evicting stale entries and requesting fresh ones from the publisher. Prevents subscribers from being stuck on dead tracks after publisher reconnection.
  • fix: is_closed() method on Track — Properly checks all states (TrackWriter dropped, SubgroupsWriter dropped, closed with error) to determine if a track is truly closed.
  • fix: race between publish and subscribeTrackInfo state machine prevents conflicts when both PUBLISH and SUBSCRIBE arrive for the same track.

Testing

  • Unit tests for stale track cache eviction (test_stale_track_cache_bug)
  • Unit tests for track deduplication (test_track_deduplication_while_alive)
  • Unit tests for subgroup writer lifecycle (test_track_not_stale_after_subgroups_transition, test_track_stale_after_subgroups_writer_dropped)
  • Updated moq-test-client scenarios

* handles publish_namespace correctly and register to the coordinator
* adds subscribe_namespace request handling
* updates moq-pub which correct publish_namespace and handles incoming
subscribe request at application level
@itzmanish itzmanish changed the title feat: remove announce and add publish and subscribe namespace feat: remove announce and add PUBLISH and SUBSCRIBE_NAMESPACE Jan 30, 2026
itzmanish and others added 10 commits February 2, 2026 13:20
When a TrackReader in the cache has been closed (due to publisher
disconnect, error, etc.), new subscribers would receive the stale
cached reader instead of triggering a fresh upstream subscription.

This change:
- Adds TrackReader::is_closed() to detect closed/dropped tracks
- Updates TracksReader::subscribe() to check liveness before returning
  cached tracks, evicting stale entries when found

Includes unit tests for both the bug scenario and normal deduplication.
Replace is_some() + unwrap() patterns with if-let to satisfy
clippy::unnecessary_unwrap lint in Rust 1.93+.

Affected files:
- moq-pub/src/media.rs
- moq-relay-ietf/src/relay.rs
A standardized test client for MoQT interoperability testing. Implements
test scenarios based on the moq-interop-runner framework.

Test cases:
- setup-only: Connect, complete SETUP exchange, close gracefully
- announce-only: Announce namespace, receive OK
- subscribe-error: Subscribe to non-existent track, expect error
- announce-subscribe: Publisher announces, subscriber subscribes
- subscribe-before-announce: Out-of-order setup handling
- publish-namespace-done: Announce then send PUBLISH_NAMESPACE_DONE

Also enables TLS_DISABLE_VERIFY environment variable support in
moq-native-ietf for containerized testing workflows.

Usage:
  moq-test-client --relay https://localhost:4443
  moq-test-client --relay https://localhost:4443 --test setup-only
  moq-test-client --list
fix: clippy warnings

fix: clippy warning

fix: cargo fmt
…rrors

Bug: When multiple tracks (audio + video) are played concurrently in
moq-sub, each track's recv_group() writes objects individually to a
shared stdout via Arc<Mutex<O>>. Since fMP4 objects alternate between
moof (metadata) and mdat (media data) boxes, a concurrent task can
acquire the lock between a moof and its corresponding mdat, causing
interleaving:

  [video moof] → [audio moof] → [audio mdat] → [video mdat]

ffmpeg then reads video moof + audio mdat as a single segment, parsing
MP4 traf box bytes (0x74726166) as H.264 NAL unit sizes, producing:

  Invalid NAL unit size (1953653094 > 413)
  Error splitting the input into NAL units

This was not observed on the main branch because the relay's announce-
based subscribe path (TracksReader::subscribe) creates per-subscriber
Track pairs forwarded through the consumer.rs event loop, delivering
objects at the natural publish rate with low interleave probability.

On feat/publish, the relay's TrackInfo/subscribe_upstream architecture
uses shared tracks (OnceLock) with inline forward_upstream(), creating
a tighter setup window that increases concurrent delivery probability
between audio and video tracks.

Fix: Buffer moof objects in recv_group() and write moof+mdat as a
single atomic operation under one lock acquisition, preventing any
interleaving regardless of relay delivery timing.
@itzmanish itzmanish changed the title feat: remove announce and add PUBLISH and SUBSCRIBE_NAMESPACE feat: Replace ANNOUNCE with PUBLISH_NAMESPACE + PUBLISH + SUBSCRIBE_NAMESPACE Feb 12, 2026
@itzmanish itzmanish changed the title feat: Replace ANNOUNCE with PUBLISH_NAMESPACE + PUBLISH + SUBSCRIBE_NAMESPACE feat: Replace ANNOUNCE with PUBLISH_NAMESPACE + PUBLISH, SUBSCRIBE_NAMESPACE Feb 12, 2026
@itzmanish itzmanish changed the title feat: Replace ANNOUNCE with PUBLISH_NAMESPACE + PUBLISH, SUBSCRIBE_NAMESPACE feat: Replace ANNOUNCE -> (PUBLISH_NAMESPACE + PUBLISH) and SUBSCRIBE_NAMESPACE Feb 12, 2026
…lookup

The subscriber maintained two separate alias maps and Notify instances to
resolve track_alias for subscribe (pull) vs publish (push) flows. Since
the publisher assigns track_alias in both cases, they share a single
namespace — the dual-map design required a complex tokio::select! race
with fallback in both recv_stream_inner and recv_datagram.

Replace with a single TrackOrigin enum, one map, and one Notify.
englishm-cloudflare pushed a commit to englishm-cloudflare/moq-rs-1 that referenced this pull request Mar 12, 2026
…gering subscriber support

Add new types and trait methods to prepare the Coordinator API surface for
upcoming protocol features. All new methods have default implementations
that return no-op/empty results, so existing implementors compile without
changes.

New types:
- ScopeConfig: Per-scope configuration (origin_fallback, lingering_subscribe)
- NamespaceSubscription: RAII handle for SUBSCRIBE_NAMESPACE with existing matches
- NamespaceInfo: Identity of a registered namespace
- RelayInfo: Relay endpoint for forwarding notifications
- TrackRegistration: RAII handle for track-level PUBLISH
- TrackEntry: Identity of a registered track
- TrackSubscription: RAII handle for lingering subscriber interest

New Coordinator methods:
- get_scope_config(scope): Get per-scope configuration
- subscribe_namespace(scope, prefix): Register interest in namespace prefix
- unsubscribe_namespace(scope, prefix): Remove namespace prefix interest
- lookup_namespace_subscribers(scope, namespace): Find interested relays
- register_track(scope, namespace, track): Register track availability
- unregister_track(scope, namespace, track): Remove track registration
- list_tracks(scope, namespace): List tracks under a namespace
- subscribe_track(scope, namespace, track): Pre-register track interest
- unsubscribe_track(scope, namespace, track): Remove track interest
- lookup_track_subscribers(scope, namespace, track): Find waiting subscribers

These stubs align with moq-worker RPCs (GetScopeConfig, RegisterSubscribeNamespace,
ListNamespaces, LookupSubscribers, RegisterTrack, ListTracks, etc.) and enable
the relay layer to be wired up once PR cloudflare#134 (PUBLISH_NAMESPACE/SUBSCRIBE_NAMESPACE
protocol support) lands.
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