Skip to content

feat(moq-hls): expose the export observers a recorder needs#2381

Closed
kixelated wants to merge 1 commit into
devfrom
claude/moq-hls-recorder-hooks
Closed

feat(moq-hls): expose the export observers a recorder needs#2381
kixelated wants to merge 1 commit into
devfrom
claude/moq-hls-recorder-hooks

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

What

The export::Broadcaster is built to serve HLS on demand: it exposes rendition() (look up one) and master_playlist(), but keeps rendition enumeration and the change signals internal. A consumer that records a broadcast (mirrors every rendition to durable storage) needs to observe the same state the server does, so this exposes it read-only:

  • Broadcaster::renditions() -> Vec<Arc<Rendition>> — snapshot every current rendition, not just serve one by name.
  • Broadcaster::updated() -> watch::Receiver<usize> — catalog-sync signal (fires on add/remove/reconfigure), so a recorder re-enumerates on catalog changes. The broadcast-level analogue of Rendition::updated.
  • Broadcaster::is_closed() / closed() — were pub(crate); a recorder awaits closed() to finalize.
  • Rendition::updated() — was pub(crate); the per-rendition timeline signal a recorder awaits before fetching newly-closed segments via segment().
  • Rendition::playable() — was pub(crate); lets a recorder gate on real content.

Why

All read-only accessors over existing state; no behavior change to the serve path. This unblocks moq.pro's VOD recorder, which archives a broadcast by driving the export as a fetch-on-demand mirror (enumerate renditions, await updated(), fetch each completed segment()/init(), persist to object storage).

Risk

Purely additive visibility + two new accessor methods; nothing on the existing HTTP serve path changes.

The export Broadcaster is built to SERVE HLS on demand: it exposes `rendition()`
(look up one) and `master_playlist()`, but keeps rendition enumeration and the
change signals internal. A consumer that RECORDS a broadcast (mirrors every
rendition to durable storage) needs to observe the same state the server does, so
expose it read-only:

- `Broadcaster::renditions() -> Vec<Arc<Rendition>>`: snapshot every current
  rendition, not just serve one by name.
- `Broadcaster::updated() -> watch::Receiver<usize>`: catalog-sync signal (fires on
  add/remove/reconfigure), so a recorder re-enumerates on catalog changes. The
  broadcast-level analogue of `Rendition::updated`.
- `Broadcaster::is_closed()` / `closed()`: was pub(crate); a recorder awaits
  `closed()` to finalize.
- `Rendition::updated()`: was pub(crate); the per-rendition timeline signal a
  recorder awaits before fetching newly-closed segments via `segment()`.
- `Rendition::playable()`: was pub(crate); lets a recorder gate on real content.

All read-only accessors over existing state; no behavior change to the serve path.

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

@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 added a commit that referenced this pull request Jul 18, 2026
The export module was built to *serve* HLS on demand. A recorder that mirrors
every rendition and every segment to storage had to reach into raw signals
(a `watch::Receiver<usize>` of the rendition count, a per-rendition
`watch::Receiver<u64>` timeline epoch) and re-derive the whole reconcile + fetch
loop itself. Replace those with two cursors in the moq-net idiom, so the engine
drives the loop once and the consumer just pulls the next element:

- `renditions::{Producer, Consumer}` + `Event`: the Consumer yields one
  `Added`/`Removed` at a time, replaying the current set as `Added` and ending
  (`None`) when the source closes. A reconfigure is a `Removed` then `Added`.
  The Producer owns the current set (reconciled by `sync`, moved off Broadcaster)
  and the serve path reads it synchronously.
- `segments::{Producer, Consumer}` + `Segment`: rehomes `timeline.rs`. The
  Producer is the timeline window (kio-backed, replacing the tokio watch); the
  Consumer's `next()` awaits the next *finalized* segment, FETCHes and transmuxes
  it, and yields the CMAF bytes with duration + PDT. Runs dry (`None`) on clean
  end or reset.

The HTTP serve path keeps its pull API (`rendition`, `master_playlist`,
`playlist`, `segment`), now `pub(crate)` since only in-crate callers use it; the
long-poll waits via `Rendition::wait_playable`. No leaky `watch` types remain in
the public surface. Supersedes the raw-accessor approach in #2381.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kixelated kixelated closed this Jul 18, 2026
kixelated added a commit that referenced this pull request Jul 18, 2026
The export module was built to *serve* HLS on demand. A recorder that mirrors
every rendition and every segment to storage had to reach into raw signals
(a `watch::Receiver<usize>` of the rendition count, a per-rendition
`watch::Receiver<u64>` timeline epoch) and re-derive the whole reconcile + fetch
loop itself. Replace those with two cursors in the moq-net idiom, so the engine
drives the loop once and the consumer just pulls the next element:

- `renditions::{Producer, Consumer}` + `Event`: the Consumer yields one
  `Added`/`Removed` at a time, replaying the current set as `Added` and ending
  (`None`) when the source closes. A reconfigure is a `Removed` then `Added`.
  The Producer owns the current set (reconciled by `sync`, moved off Broadcaster)
  and the serve path reads it synchronously.
- `segments::{Producer, Consumer}` + `Segment`: rehomes `timeline.rs`. The
  Producer is the timeline window (kio-backed, replacing the tokio watch); the
  Consumer's `next()` awaits the next *finalized* segment, FETCHes and transmuxes
  it, and yields the CMAF bytes with duration + PDT. Runs dry (`None`) on clean
  end or reset.

The HTTP serve path keeps its pull API (`rendition`, `master_playlist`,
`playlist`, `segment`), now `pub(crate)` since only in-crate callers use it; the
long-poll waits via `Rendition::wait_playable`. No leaky `watch` types remain in
the public surface. Supersedes the raw-accessor approach in #2381.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Jul 18, 2026
The export module was built to *serve* HLS on demand. A recorder that mirrors
every rendition and every segment to storage had to reach into raw signals
(a `watch::Receiver<usize>` of the rendition count, a per-rendition
`watch::Receiver<u64>` timeline epoch) and re-derive the whole reconcile + fetch
loop itself. Replace those with two cursors in the moq-net idiom, so the engine
drives the loop once and the consumer just pulls the next element:

- `renditions::{Producer, Consumer}` + `Event`: the Consumer yields one
  `Added`/`Removed` at a time, replaying the current set as `Added` and ending
  (`None`) when the source closes. A reconfigure is a `Removed` then `Added`.
  The Producer owns the current set (reconciled by `sync`, moved off Broadcaster)
  and the serve path reads it synchronously.
- `segments::{Producer, Consumer}` + `Segment`: rehomes `timeline.rs`. The
  Producer is the timeline window (kio-backed, replacing the tokio watch); the
  Consumer's `next()` awaits the next *finalized* segment, FETCHes and transmuxes
  it, and yields the CMAF bytes with duration + PDT. Runs dry (`None`) on clean
  end or reset.

The HTTP serve path keeps its pull API (`rendition`, `master_playlist`,
`playlist`, `segment`), now `pub(crate)` since only in-crate callers use it; the
long-poll waits via `Rendition::wait_playable`. No leaky `watch` types remain in
the public surface. Supersedes the raw-accessor approach in #2381.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Jul 18, 2026
The export module was built to *serve* HLS on demand. A recorder that mirrors
every rendition and every segment to storage had to reach into raw signals
(a `watch::Receiver<usize>` of the rendition count, a per-rendition
`watch::Receiver<u64>` timeline epoch) and re-derive the whole reconcile + fetch
loop itself. Replace those with two cursors in the moq-net idiom, so the engine
drives the loop once and the consumer just pulls the next element:

- `renditions::{Producer, Consumer}` + `Event`: the Consumer yields one
  `Added`/`Removed` at a time, replaying the current set as `Added` and ending
  (`None`) when the source closes. A reconfigure is a `Removed` then `Added`.
  The Producer owns the current set (reconciled by `sync`, moved off Broadcaster)
  and the serve path reads it synchronously.
- `segments::{Producer, Consumer}` + `Segment`: rehomes `timeline.rs`. The
  Producer is the timeline window (kio-backed, replacing the tokio watch); the
  Consumer's `next()` awaits the next *finalized* segment, FETCHes and transmuxes
  it, and yields the CMAF bytes with duration + PDT. Runs dry (`None`) on clean
  end or reset.

The HTTP serve path keeps its pull API (`rendition`, `master_playlist`,
`playlist`, `segment`), now `pub(crate)` since only in-crate callers use it; the
long-poll waits via `Rendition::wait_playable`. No leaky `watch` types remain in
the public surface. Supersedes the raw-accessor approach in #2381.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kixelated added a commit that referenced this pull request Jul 18, 2026
The export module was built to *serve* HLS on demand. A recorder that mirrors
every rendition and every segment to storage had to reach into raw signals
(a `watch::Receiver<usize>` of the rendition count, a per-rendition
`watch::Receiver<u64>` timeline epoch) and re-derive the whole reconcile + fetch
loop itself. Replace those with two cursors in the moq-net idiom, so the engine
drives the loop once and the consumer just pulls the next element:

- `renditions::{Producer, Consumer}` + `Event`: the Consumer yields one
  `Added`/`Removed` at a time, replaying the current set as `Added` and ending
  (`None`) when the source closes. A reconfigure is a `Removed` then `Added`.
  The Producer owns the current set (reconciled by `sync`, moved off Broadcaster)
  and the serve path reads it synchronously.
- `segments::{Producer, Consumer}` + `Segment`: rehomes `timeline.rs`. The
  Producer is the timeline window (kio-backed, replacing the tokio watch); the
  Consumer's `next()` awaits the next *finalized* segment, FETCHes and transmuxes
  it, and yields the CMAF bytes with duration + PDT. Runs dry (`None`) on clean
  end or reset.

The HTTP serve path keeps its pull API (`rendition`, `master_playlist`,
`playlist`, `segment`), now `pub(crate)` since only in-crate callers use it; the
long-poll waits via `Rendition::wait_playable`. No leaky `watch` types remain in
the public surface. Supersedes the raw-accessor approach in #2381.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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