Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fd20f75
chore(quest): claim quest/m1/archive/catalog-hls.md
kixelated Oct 25, 2026
4460458
wip(hang)!: per-rendition hls records replace the root archive
kixelated Oct 25, 2026
25c8a34
wip(hls)!: export renditions from their own hls records
kixelated Oct 25, 2026
cce13db
feat(hang)!: opt-in per-rendition hls records replace the root archive
kixelated Oct 25, 2026
9e6eab0
Merge origin/main into quest/m1/archive/catalog-hls
kixelated Oct 25, 2026
49d1213
fix(hls): keep follower boundaries, abort records, skip stops in DASH…
kixelated Oct 25, 2026
3405d79
Merge remote-tracking branch 'origin/main' into worktree-agent-a228af…
kixelated Oct 25, 2026
c176d50
fix(mux): bound follower alignment state and release undeclared enrol…
kixelated Oct 25, 2026
4ae7485
fix(mux): record HLS segments for video and audio only
kixelated Oct 25, 2026
b1992ab
Merge origin/main into quest/m1/archive/catalog-hls
kixelated Oct 26, 2026
f09216f
fix(hls): settle the edge and record failure findings
kixelated Oct 26, 2026
64d1b23
fix(mux): advertise HLS records only on the enrolled rendition
kixelated Oct 26, 2026
c32feb9
fix(mux): hand over HLS pacing when the pacer's report is rejected
kixelated Oct 26, 2026
d40856c
fix(hls): prune dropped renditions as new ones register
kixelated Oct 26, 2026
35313bc
fix(hls): wake a cursor waiting on the lead when the lead hands over
kixelated Oct 26, 2026
fea0dcd
Merge remote-tracking branch 'origin/main' into worktree-agent-a228af…
kixelated Oct 26, 2026
02815ec
Merge remote-tracking branch 'origin/main' into merge-6783
kixelated Oct 26, 2026
a57f1c4
fix(moq-studio): reserve the program video with with_hls_hint
kixelated Oct 26, 2026
7ccfa57
fix(hls): advertise a rendition once its first segment is aligned
kixelated Oct 26, 2026
9ea78b2
Merge remote-tracking branch 'origin/main' into merge-6783
kixelated Oct 26, 2026
d9075ef
Merge remote-tracking branch 'origin/main' into catalog-hls-iterate
kixelated Oct 26, 2026
7432cdd
feat(hls)!: cut each rendition's segments on its own pts grid
kixelated Oct 26, 2026
f72042a
fix(hls): restate the wall clock after a discontinuity, and record an…
kixelated Oct 26, 2026
a1af6f0
fix(hls): let a codec-stated duration win over the segment end
kixelated Oct 26, 2026
9b423a5
Merge origin/main into quest/m1/archive/catalog-hls
kixelated Oct 27, 2026
1dbdb2c
Merge origin/main into quest/m1/archive/catalog-hls
kixelated Oct 27, 2026
ba11638
fix(hls): quantize the end gap at the output scale, keep recorder dis…
kixelated Oct 27, 2026
991dc51
test(hls): port the takeover test to stop gaps
kixelated Oct 27, 2026
20e554c
style(mux): format the end-gap test
kixelated Oct 27, 2026
e1da2dc
Merge origin/main into quest/m1/archive/catalog-hls
kixelated Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion doc/bin/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ moq --connect https://relay.example.com/anon --broadcast camera.hang \
import rtsp --connect rtsp://admin:secret@192.168.1.64/stream1
```

`--hls` publishes [HLS segment records](/bin/hls#how-export-works) beside each
video and audio rendition, which `export hls` needs to serve the broadcast.
Renditions' segments line up wherever the source's keyframes share timestamps.
The stdin formats, `hls`, `rtsp`, and `capture` take it; the other gateways build their
own catalog and refuse it.

## Export

The `fmp4`, `mkv`, `flv`, `h264`, and `h265` exports pick renditions with
Expand Down Expand Up @@ -360,7 +366,7 @@ over one connection:

```bash
moq --connect https://relay.example.com/anon \
import --broadcast event.hang srt --listen 0.0.0.0:9000 \
import --broadcast event.hang --hls rtsp --connect rtsp://camera.local/stream1 \
-- export --broadcast event.hang hls --listen 0.0.0.0:8080 \
-- export --broadcast event.hang archive file:///recordings
```
Expand Down
33 changes: 18 additions & 15 deletions doc/bin/hls.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ that can't speak MoQ, records it as HLS files, or both. `moq import hls` pulls
a remote HLS master or media playlist into a broadcast.

```bash
# Serve. Players open http://localhost:8089/my-stream.hang/master.m3u8 (or manifest.mpd for DASH)
# Publish with HLS records, then serve. Players open
# http://localhost:8089/my-stream.hang/master.m3u8 (or manifest.mpd for DASH)
ffmpeg -re -i input.mp4 -c copy -f mp4 -movflags cmaf+separate_moof+delay_moov+skip_trailer -frag_duration 1000 - | moq --connect https://relay.example.com/anon --broadcast my-stream.hang import --hls fmp4
Comment thread
coderabbitai[bot] marked this conversation as resolved.
moq --connect https://relay.example.com/anon --broadcast my-stream.hang export hls --listen '[::]:8089'

# Record into ./recording, ending once the broadcast finishes
Expand Down Expand Up @@ -40,20 +42,21 @@ requested bytes fails the segment.
## How export works

Export never subscribes to media. It reads each rendition's
[timeline track](/concept/hang#catalog), a small index of that rendition's
groups, to build playlists. A segment request fetches exactly the frames it
covers from the relay's cache and transmuxes them to CMAF. Media is never
transcoded, so the player must support the publisher's codecs.
[HLS records](/concept/hang#catalog), a small track listing where each of
that rendition's segments starts, to build playlists. A segment request fetches
exactly the groups it covers from the relay's cache and transmuxes them to CMAF.
Media is never transcoded, so the player must support the publisher's codecs.

What that means for you:

- **Opt in at the publisher.** Only a rendition whose catalog entry carries `hls` gets a playlist; nothing publishes it by default. Pass `--hls` to `moq import` (the stdin formats, `hls`, `rtsp`, and `capture`), or enable it with `moq_mux::catalog::Config::with_hls` in Rust. Without a duration hint, a rendition declares its records after its first segment.
- **Each rendition cuts its own segments.** A group starts a segment when its pts falls in a later multiple of the segment duration than the previous group's, and each rendition numbers its own segments. Players match renditions by timestamp, so nothing coordinates the ladder: segments line up wherever keyframes share timestamps, a rendition that starts late lines up from its next multiple, and audio lines up with video whose keyframes sit on those multiples. The DASH manifest leaves `segmentAlignment` out. A break in one rendition's content starts a discontinuity on that rendition alone; `EXT-X-PROGRAM-DATE-TIME`, written after every discontinuity when the catalog has a clock, lets players resync.
- **Idle renditions cost nothing**, and a segment is servable for as long as the relay's [cache](/bin/relay/config#cache) retains it. Keep `--window` (the playlist length, default 16 s) within that retention.
- **Live joins cost the window.** The exporter reads the timeline checkpoint and lists at most 256 segments.
- **Publish timelines.** A broadcast whose catalog advertises no timelines is skipped, and a rendition is listed only once its own timeline is advertised. Without a duration hint, a publisher declares that timeline after the rendition's first segment.
- **Align GOPs across video renditions.** Segment boundaries come from one reference rendition. Other video renditions snap to their nearest keyframe, and a segment with none nearby becomes an `EXT-X-GAP`.
- **Disabled renditions are hidden.** A rendition the catalog marks `enabled: false` leaves the multivariant playlist and DASH manifest until it re-enables. Its media playlist keeps serving, so a player already on it is not ended with `EXT-X-ENDLIST`. If it supplied segment boundaries, an enabled sibling takes over at its first record numbered past the last advertised one, and DASH starts a new period. The media sequence is the record number, so every edge agrees: a sibling numbered ahead restarts the playlist with a discontinuity, and one numbered behind (such as a re-enabled rung) holds every playlist until its numbering catches up. `EXT-X-DISCONTINUITY-SEQUENCE` and DASH periods are counted per edge, so edges agree on them only when no discontinuity or handoff happened while each was running.
- **Live joins cost the window.** The exporter reads the record checkpoint and lists at most 256 segments.
- **Segments are timed by their records.** A segment is listed once the record after it arrives, and its duration, its DASH `t` and `d`, and its last sample's duration all come from its own record and the next, so a segment's bytes never change with a catalog estimate. A stop record (a break or the end of the content) is listed as a zero-length gap, and what follows it starts a discontinuity.
- **Disabled renditions are hidden.** A rendition the catalog marks `enabled: false` leaves the multivariant playlist and DASH manifest until it re-enables. Its media playlist keeps serving, so a player already on it is not ended with `EXT-X-ENDLIST`.
- **URLs name the publisher instance.** Every URL the multivariant playlist and DASH manifest list sits under `@<instance>`, the publisher's epoch, so every edge and every reload derive the same URLs and a CDN in front of export caches them. A restarted publisher gets URLs of its own, whose numbering starts over. The multivariant playlist and manifest switch to it at once, while a player already on the old instance keeps playing until that publisher leaves; the player then has to reload the multivariant playlist. An instance without an epoch (one that reached export over moq-lite-06 or older) gets an id each export mints, so only that export serves its URLs.
- **Only a clean finish ends a playlist.** `EXT-X-ENDLIST` and a `static` DASH manifest mean the timeline finished. If the segment-boundary rendition's timeline fails instead, such as a publisher dropping its session, every media playlist and the DASH manifest answer HTTP 500 with `Cache-Control: no-store`, so a truncated recording never looks complete. A rendition whose own timeline fails answers the error alone and leaves the multivariant playlist and DASH manifest, so players switch to a sibling.
- **Only a clean finish ends a playlist.** `EXT-X-ENDLIST` and a `static` DASH manifest mean the records finished. A rendition whose records fail instead, such as a publisher dropping its session, answers HTTP 500 with `Cache-Control: no-store`, so a truncated recording never looks complete, and leaves the multivariant playlist and DASH manifest so players switch to a sibling. With none left, the DASH manifest answers the error too.

A [`moq-archive`](https://docs.rs/moq-archive) recording is served the same
way, with no second stored copy: record a broadcast, republish it, and export
Expand All @@ -65,8 +68,8 @@ moq --connect https://relay.example.com/anon --broadcast event-replay.hang impor
moq --connect https://relay.example.com/anon --broadcast event-replay.hang export hls --listen '[::]:8089'
```

The archived catalog and timelines replay as recorded, so the export lists the
same window as a live broadcast.
The archived catalog and HLS records replay as recorded, so the export lists
the same window as a live broadcast.

## Record

Expand All @@ -78,11 +81,11 @@ after the recording completes.

The recording starts at the oldest segment in the `--window` and gets
`EXT-X-ENDLIST` only once the broadcast finishes cleanly. If the publisher drops
out, the timeline restarts, or a segment can't be fetched, the export exits with
out, the HLS records restart, or a segment can't be fetched, the export exits with
an error and leaves the playlists unended. A rendition whose decoder is
reconfigured keeps one playlist, switching to a new init at an
`EXT-X-DISCONTINUITY` that every playlist marks at the same segment, so the whole
recording stays playable from its master playlist.
`EXT-X-DISCONTINUITY` in that playlist, so the whole recording stays playable
from its master playlist.

`--listen-tls-cert`/`--listen-tls-key` or `--listen-tls-generate` serve HTTPS,
and `--cors-origin` opens it to browsers. HLS TLS reads `MOQ_HLS_TLS_*`;
Expand Down
4 changes: 2 additions & 2 deletions doc/concept/hang.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ A few things the catalog can express beyond decoder config:
- **Spatial media.** A video rendition may carry `projection` (flat `rectilinear`, or `equirectangular` with the degrees of longitude it spans, 180 for VR180) and `stereo` (`mono`, `left-right`, or `top-bottom` frame packing), and an audio rendition may carry `layout: "ambisonic"` for AmbiX channels. Readers treat a value they don't recognize as absent. Nothing in `@moq/watch` renders them yet.
- **Disabled renditions.** An audio or video rendition with `enabled: false` has no frames coming, and a player must not select it. Pausing a rendition in `@moq/publish` keeps it in the catalog this way rather than removing it, so a mute is a one-field update; a disabled video rendition encodes one black keyframe first, so a player that predates the field shows black instead of a frozen picture. The field is written only when false. The older video `stalled` field is no longer written, and readers ignore it.
- **Preference.** A video rendition may carry a signed `preference`, default `0`. A player keeps only the highest preference it can decode, then picks by size and bitrate within it, as do the single-rendition egresses (RTMP play, WHEP, FLV, the transcoder's source). A publisher that adds its own H.264 copy of an H.265 source marks the copy `-1`, so only a viewer that can't decode H.265 pulls it. Picking a rendition by name still overrides it. The field is written only when nonzero.
- **Archive.** A broadcast may advertise an `archive` entry mapping each track, the catalog included, to its own timeline track (a small index of the track's spans: timing plus group and frame positions) and fixed nominal record duration, plus the replay MoQ path, object-store URL, and format version when recorded. Media records end at the first group boundary at or beyond a 4s target; sparse timelines such as the catalog declare zero. Tracks commit and expire independently. An untimed track has no timing to index, so its timeline declares no duration and holds one record per group: it can be recorded and replayed, but not turned into HLS or kept in a DVR. The timelines are what let the [HLS gateway](/bin/hls) build playlists without subscribing to media. A reader that cannot decode the `archive` entry, such as one from a different version, ignores it and loses the timelines and replay, but live media still plays.
- **Clock.** The optional root `clock` maps PTS zero to wall time so every media track and the archive index share one fixed epoch after timescale conversion. It is independent of `archive`, so a live-only publisher can expose wall-clock timing without creating a timeline. Publish independent sources as sibling broadcasts under one prefix, such as `robot/front.hang` and `robot/rear.hang`; each broadcast's audio and video entries are renditions of that source. In one Rust publisher, pass the same `moq_mux::Clock` to each `catalog::Config::with_clock`; JS publishers on one page already share the page clock. Consumers discover the siblings by prefix and align their timestamps through the advertised wall mapping; `@moq/watch` plays one broadcast at a time.
- **HLS records.** A video or audio rendition may opt in with an `hls` field naming a small track of HLS segment records, each segment's first group and pts, plus the nominal segment duration. Each rendition numbers its own segments, and an edge serves a rendition's segment `n` from its record `n`; segments line up across renditions wherever their keyframes do. The records are what let the [HLS gateway](/bin/hls) build playlists without subscribing to media; a rendition without them gets no playlist. Nothing publishes them unless the application enables them, and a reader that cannot decode the field ignores it.
- **Clock.** The optional root `clock` maps PTS zero to wall time so every media track and every HLS record share one fixed epoch after timescale conversion. Publish independent sources as sibling broadcasts under one prefix, such as `robot/front.hang` and `robot/rear.hang`; each broadcast's audio and video entries are renditions of that source. In one Rust publisher, pass the same `moq_mux::Clock` to each `catalog::Config::with_clock`; JS publishers on one page already share the page clock. Consumers discover the siblings by prefix and align their timestamps through the advertised wall mapping; `@moq/watch` plays one broadcast at a time.
- **Version.** The root `version` is the oldest moq-hang draft that reads the catalog correctly, written only when a field needs one newer than 03: a `track` that differs from its ID, or a video `warmup`. Readers ignore fields they don't know, but refuse a `version` above their own with a typed "unsupported catalog version" error, so a player can say to upgrade instead of misreading the catalog. An additive field never raises it.
- **Extensions.** The root is a loose object. Applications add their own sections (`scte35`, for example) next to the ones hang defines, optionally naming a track that carries the data. Every library exposes a way to write your section without clobbering the built-in ones, and readers ignore what they don't know.

Expand Down
10 changes: 5 additions & 5 deletions doc/setup/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ These land with the next breaking release.
or `subscribe` carries a `rate` cap encodes that role as an object, which
released verifiers reject, so upgrade every auth server and verifier before
issuing capped tokens.
- **Archive timelines declare a duration** (#5715). Each catalog
`archive.timelines` entry is an object instead of a track name. Neither side
reads the other's shape, and a catalog it can't read keeps its live media but
drops the archive, so seeking, recording, and HLS history need publishers and
their readers upgraded together.
- **HLS records replace archive timelines.** The hang catalog's root `archive`
section is gone. A video or audio rendition opts in to HLS with its own `hls`
field naming a `<track>.hls` record track, and nothing publishes one by
default. Neither side reads the other's shape, so upgrade HLS exporters and
their publishers together, and opt the publishers in (`moq import --hls`).
- **Announce restarts.** moq-lite 07 adds `ANNOUNCE_RESTART` (0x3): another
publisher instance now serves the prefix. lite-06's `ANNOUNCE_RESTART` is
named `ANNOUNCE_UPDATE`, as it always meant. moq-transport with the epoch
Expand Down
Loading
Loading