feat(moq-hls)!: expose a credential-aware serve surface for embedders - #2438
Merged
Conversation
kixelated
force-pushed
the
claude/moq-hls-pub-surface
branch
from
July 21, 2026 20:22
bc1a432 to
58fdd99
Compare
kixelated
force-pushed
the
claude/moq-hls-pub-surface
branch
2 times, most recently
from
July 22, 2026 16:06
fc9e2af to
a388dac
Compare
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
force-pushed
the
claude/moq-hls-pub-surface
branch
from
July 22, 2026 16:31
a388dac to
e724853
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
renditionsandsegmentswere 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_mediaremain crate-internal —media_playlistreturns the renderedStringinstead of leaking the intermediate, and returnsNonewhen nothing is playable yet, which also subsumesis_playable.Broadcaster::is_empty— aready()timeout already means the catalog is empty.Rendition::init—segments::Consumer::initis already public and literally delegates to it.Server::broadcasteris the one genuinely new capability. The alternative — a consumer keeping its own broadcaster map — would needBroadcaster::{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 viaAuthorization/?jwt=on the master request is gone by the time the player fetches a segment. An optionalquery(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
Configfield on purpose.Configis pool-wide (Server::newstores one, every broadcaster clones it) while one broadcaster fans out to viewers holding different tokens — so a credential inConfigwould embed one viewer's JWT in another viewer's playlist. Deployment-scoped policy (a future signing key) would belong inConfig; 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_playlisttakes aqueryargument.render_media/render_mastergained the same argument but are crate-private after #2423, so they aren't public API. Markedfeat!+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 theEXT-X-MAPURI, every segment URI, and both audio and video child playlist URIs. clippy + fmt clean, with and without theserverfeature.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.rsmatches 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)