Skip to content

feat(moq-hls)!: expose a credential-aware serve surface for embedders - #2438

Merged
kixelated merged 1 commit into
mainfrom
claude/moq-hls-pub-surface
Jul 22, 2026
Merged

feat(moq-hls)!: expose a credential-aware serve surface for embedders#2438
kixelated merged 1 commit into
mainfrom
claude/moq-hls-pub-surface

Conversation

@kixelated

@kixelated kixelated commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Lets a consumer embed the fetch-on-demand HLS origin as a library and put its own authorization in front, without a policy hook inside this crate.

Motivation: moq.pro's relay serves live HLS off its in-process origin (moq-dev/moq.pro#695), authorizing each request against its existing token verifier and subscribe scoping.

The new public surface (4 items)

Server::broadcaster()        // the shared, fanned-out pool
Rendition::media_playlist()  // render this rendition's playlist (None until playable)
Rendition::segment()         // fetch one segment by sequence
Rendition::playable()        // await readiness

renditions and segments were already public and already expose this data — but as cursors (Consumer::next()), which is the right shape for a recorder walking a stream once and the wrong shape for a server answering arbitrary sequence numbers for many viewers sitting at different positions. Random access by sequence is the one capability those cursors cannot express; that's what this adds.

Everything an embedder can derive stayed private, so this does not re-widen what #2423 just narrowed:

  • Snapshot, Segment, render_media remain crate-internal — media_playlist returns the rendered String instead of leaking the intermediate, and returns None when nothing is playable yet, which also subsumes is_playable.
  • Broadcaster::is_empty — a ready() timeout already means the catalog is empty.
  • Rendition::initsegments::Consumer::init is already public and literally delegates to it.

Server::broadcaster is the one genuinely new capability. The alternative — a consumer keeping its own broadcaster map — would need Broadcaster::{is_closed, closed} promoted instead (more surface) plus duplicated evict-on-close logic.

No Authorizer/callback trait: the dependency arrow stays one-way and this crate keeps knowing nothing about tokens.

Credential propagation

The renderers emit relative child URLs (<kind>/<name>/media.m3u8, init.mp4, seg/<n>.m4s). A stock player — Safari especially — does not replay request headers onto those follow-ups, so a token supplied only via Authorization/?jwt= on the master request is gone by the time the player fetches a segment. An optional query (e.g. jwt=<token>, no leading ?) is appended to every child URL. It's a plain value, not a policy: the caller decides the parameter name.

It's an argument rather than a Config field on purpose. Config is pool-wide (Server::new stores one, every broadcaster clones it) while one broadcaster fans out to viewers holding different tokens — so a credential in Config would embed one viewer's JWT in another viewer's playlist. Deployment-scoped policy (a future signing key) would belong in Config; this cannot.

Server::router() is unchanged in behaviour and still works as the no-auth convenience path; it now propagates whatever query reached each playlist, so it also works behind token-in-URL auth middleware.

Breaking

Broadcaster::master_playlist takes a query argument. render_media/render_master gained the same argument but are crate-private after #2423, so they aren't public API. Marked feat! + BREAKING CHANGE: so the release tooling classifies the bump.

Testing

cargo test -p moq-hls — 49 pass + doctests, including new coverage that a credential rides the EXT-X-MAP URI, every segment URI, and both audio and video child playlist URIs. clippy + fmt clean, with and without the server feature.

Not yet exercised against live media end-to-end; that check lives downstream in moq.pro and is still outstanding.

Note for reviewers

server/routes.rs matches the broadcast with a single {broadcast} path segment, so a multi-segment broadcast path (<pid>/<name>, or anything the RTMP gateway prefixes a project name onto) never routes. This PR doesn't change that — the downstream consumer works around it with its own catch-all — but it looks like a bug in the built-in router.

(written by Opus 4.8)

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@kixelated
kixelated force-pushed the claude/moq-hls-pub-surface branch from bc1a432 to 58fdd99 Compare July 21, 2026 20:22
@kixelated
kixelated changed the base branch from claude/moq-hls-recorder-hooks to dev July 21, 2026 20:22
@kixelated
kixelated force-pushed the claude/moq-hls-pub-surface branch 2 times, most recently from fc9e2af to a388dac Compare July 22, 2026 16:06
Lets a consumer embed the fetch-on-demand HLS origin as a library and put its own
authorization in front, without a policy hook inside this crate. moq.pro's relay
serves live HLS off its in-process origin and authorizes each request against its
existing token verifier and subscribe scoping.

Two things were missing.

**A serve surface.** The broadcaster pool and the per-rendition accessors were
crate-private, so the only way to serve HLS was `Server::router()` plus wrapping
middleware -- which runs before routing and sees only the raw percent-encoded URI,
an awkward seam for authorizing against a decoded broadcast path. Now public:

  Server::broadcaster()        the shared, fanned-out pool
  Rendition::media_playlist()  render this rendition's playlist
  Rendition::segment()         fetch one segment by sequence
  Rendition::playable()        await readiness

`renditions` and `segments` were already public and already expose this data, but
as cursors: the right shape for a recorder walking a stream once, the wrong shape
for a server answering arbitrary sequence numbers for many viewers sitting at
different positions. Random access by sequence is the one capability those cursors
cannot express.

Kept private everything an embedder can derive: `Snapshot`, `Segment` and
`render_media` stay crate-internal (`media_playlist` returns the rendered string,
and `None` when nothing is playable yet, which also subsumes `is_playable`);
`Broadcaster::is_empty` (a `ready()` timeout means the catalog is empty); and
`Rendition::init` (`segments::Consumer::init` is already public and delegates to
it). No `Authorizer`/callback trait: the dependency arrow stays one-way and this
crate keeps knowing nothing about tokens.

**Credential propagation.** The renderers emit relative child URLs
(`<kind>/<name>/media.m3u8`, `init.mp4`, `seg/<n>.m4s`). A stock player -- Safari
especially -- does not replay request headers onto those follow-ups, so a token
supplied only via `Authorization` or `?jwt=` on the master request is gone by the
time the player fetches a segment. An optional `query` (e.g. `jwt=<token>`, no
leading `?`) is appended to every child URL. It is a plain value, not a policy: the
caller decides the parameter name.

It is an argument rather than a `Config` field on purpose. `Config` is pool-wide
(`Server::new` stores one and every broadcaster clones it) while a single
broadcaster fans out to viewers holding *different* tokens, so a credential in
`Config` would embed one viewer's JWT in another viewer's playlist. Deployment
-scoped policy (a future signing key) would belong in `Config`; this cannot.

`Server::router()` is unchanged in behaviour and still works as the no-auth
convenience path; it now propagates whatever query reached each playlist, so it
also works behind token-in-URL auth middleware.

BREAKING CHANGE: `Broadcaster::master_playlist` takes a `query` argument.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kixelated
kixelated force-pushed the claude/moq-hls-pub-surface branch from a388dac to e724853 Compare July 22, 2026 16:31
@kixelated
kixelated changed the base branch from dev to main July 22, 2026 16:31
@kixelated kixelated changed the title feat(moq-hls): expose the broadcaster pool + propagate credentials into playlist URLs feat(moq-hls)!: expose a credential-aware serve surface for embedders Jul 22, 2026
@kixelated
kixelated merged commit fa0cee3 into main Jul 22, 2026
2 checks passed
@kixelated
kixelated deleted the claude/moq-hls-pub-surface branch July 22, 2026 16:52
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