Skip to content
8 changes: 1 addition & 7 deletions doc/bin/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,6 @@ 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 @@ -366,7 +360,7 @@ over one connection:

```bash
moq --connect https://relay.example.com/anon \
import --broadcast event.hang --hls rtsp --connect rtsp://camera.local/stream1 \
import --broadcast event.hang srt --listen 0.0.0.0:9000 \
-- export --broadcast event.hang hls --listen 0.0.0.0:8080 \
-- export --broadcast event.hang archive file:///recordings
```
Expand Down
6 changes: 3 additions & 3 deletions doc/bin/hls.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +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
# Publish with HLS records, then serve. Players open
# Publish, 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
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 fmp4
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 @@ -49,7 +49,7 @@ 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.
- **The publisher writes the records.** Only a rendition whose catalog entry carries `hls` gets a playlist. Rust publishers built on `moq-mux` (the CLI, the RTMP, SRT, and WebRTC gateways, and the native bindings) write them by default, and `moq_mux::catalog::Config::with_hls(None)` turns them off; `@moq/publish` does not write them yet. 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 record checkpoint and lists at most 256 segments.
Expand Down
2 changes: 1 addition & 1 deletion doc/concept/hang.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ 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.
- **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.
- **HLS records.** A video or audio rendition may carry 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. Rust publishers (`moq-mux` and everything built on it) publish them by default; `@moq/publish` does not yet. 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
8 changes: 4 additions & 4 deletions doc/setup/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ These land with the next breaking release.
released verifiers reject, so upgrade every auth server and verifier before
issuing capped tokens.
- **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`).
section is gone. A video or audio rendition carries its own `hls` field
naming a `<track>.hls` record track, which Rust publishers write by default.
Neither side reads the other's shape, so upgrade HLS exporters and their
publishers together.
- **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
6 changes: 3 additions & 3 deletions drafts/draft-lcurley-moq-hang.md
Original file line number Diff line number Diff line change
Expand Up @@ -962,11 +962,11 @@ HLS records exist only so an edge can serve a broadcast as HLS or DASH without s
MoQ groups carry only an opaque sequence number; the timestamps live inside the media frames.
A record track republishes where each segment of one rendition starts, a few bytes per segment, so an edge builds playlists from the records alone and fetches a segment's groups only when a player requests it.

Records are optional and opt-in: a video or audio rendition carries them only when its publisher enables them, and a rendition without them is not HLS-renderable.
Records are optional: a video or audio rendition carries them only when its publisher publishes them, and a rendition without them is not HLS-renderable.
Other tracks, including the catalog and data tracks ({{data}}), carry none.

## Catalog Field {#hls-catalog}
A video or audio rendition opts in with its `hls` field:
A video or audio rendition advertises its records with its `hls` field:

~~~
type HlsSchema = {
Expand Down Expand Up @@ -1144,7 +1144,7 @@ This document has no IANA actions.
## moq-hang-04
{:numbered="false"}

- Replaced the root `archive` section and its per-track timelines with an opt-in, per-rendition `hls` field on video and audio ({{hls}}). Each record is a segment's first `group` and `pts` in milliseconds, with an optional `duration` where zero marks a stop; frame positions, `end`, `keyframe`, `timescale`, `replay`, `replayEpoch`, `store`, and `version` are gone. Each rendition numbers its own segments, and an edge maps a rendition's record `n` to its segment `n`. The record track is named `<track>.hls` and is not compressed.
- Replaced the root `archive` section and its per-track timelines with an optional, per-rendition `hls` field on video and audio ({{hls}}). Each record is a segment's first `group` and `pts` in milliseconds, with an optional `duration` where zero marks a stop; frame positions, `end`, `keyframe`, `timescale`, `replay`, `replayEpoch`, `store`, and `version` are gone. Each rendition numbers its own segments, and an edge maps a rendition's record `n` to its segment `n`. The record track is named `<track>.hls` and is not compressed.
- A reader refuses a JSON frame in which an object repeats a member name.
- Added the root `version` ({{catalog-version}}): the oldest draft that reads the catalog, which a consumer refuses when above its own. A `track` other than the entry's ID or a video `warmup` needs 4. Consumers MUST ignore unknown fields.
- A data track marks a discontinuity with a group of one object with no bytes, since its empty payload is data. An object with no bytes on an audio or video track, or sharing its group, is malformed.
Expand Down
2 changes: 1 addition & 1 deletion js/hang/src/catalog/hls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { u53Schema } from "./integers";
/**
* A rendition's HLS segment records, which exist only for HLS compatibility.
*
* Opt-in, and only on video and audio renditions. Each rendition numbers its own segments. A
* Optional, and only on video and audio renditions. Each rendition numbers its own segments. A
* rendition without it is not HLS-renderable. See the `Hls` module for the records.
*/
export const HlsSchema = z.object({
Expand Down
2 changes: 1 addition & 1 deletion js/hang/src/hls.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* HLS segment records: where each of a rendition's HLS segments starts.
*
* A rendition opts in with its catalog `hls` field, which names a track carrying one
* A rendition advertises them with its catalog `hls` field, which names a track carrying one
* {@link Record} per segment. A segment starts at a group boundary, so a record is the segment's
* first group and pts. It runs to the next record: it holds every group up to the next record's
* `group` and lasts until the next record's `pts`, unless it carries an explicit `duration`. A
Expand Down
1 change: 0 additions & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ surface and zero-copy texture import, land before the cut.
- [Main is no slower than release](/quest/m1/release-perf/README.md) - every benchmark where main is slower than release is fixed or accepted with a reason, checked by a final paired run before the cut
- [Interop coverage](/quest/m1/interop-coverage/README.md) - the riskiest moq-transport features moq-net exposes get cross-implementation tests over drafts 18 and 22
- [Calibrate the paired nightly benchmarks](/quest/m1/bench-ci.md) - A/A and deliberately slowed Nightly runs set the paired comparison's threshold, so its regression alerts can be trusted
- [HLS opt-in for gateways](/quest/m1/hls-opt-in-gateways.md) - SRT, RTMP, WebRTC ingest and the bindings can publish HLS records
- [HLS and DASH segment conformance](/quest/m1/hls-conformance.md) - nightly ffprobe checks that advertised timing, sync flags, and bytes match the media served
- [App-limited BBR3 estimate ages](/quest/m1/bbr-app-limited.md) - an app-limited BBR3 estimate falls within seconds after the path drops
- [Aborted epoch reads keep their cause](/quest/m1/epoch-abort-cause.md) - an aborted read through an epoch route ends with its own error, not NotFound
Expand Down
24 changes: 0 additions & 24 deletions quest/m1/hls-opt-in-gateways.md

This file was deleted.

2 changes: 1 addition & 1 deletion quest/m1/segmenter-cuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Found while landing [moq-dev/moq#6783](https://github.com/moq-dev/moq/pull/6783)
`Segmenter.cuts` (`rs/moq-mux/src/hls.rs`) is a `VecDeque<Timestamp>` that
`cut()` appends to without bound, and only a new group start drains it. Cuts
come from a declared fMP4 segment, including a `styp` box in network input
(for example `moq-cli` HLS ingest of a remote origin with `--hls`). An origin
(for example `moq-cli` HLS ingest of a remote origin). An origin
that sends `styp` segments with rising pts and no sync sample never starts a
group, so the queue grows by one entry per segment.

Expand Down
14 changes: 11 additions & 3 deletions quest/m1/time-addressing/consumers.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

Every first-party consumer that numbered groups works by start timestamp,
in Rust and JS, and hang stops writing duration and discontinuity markers
that group ends and gaps now carry.
that group ends and gaps now carry. The HLS/DASH export lists segments
straight from group starts and ends, so the HLS records go away.

## Plan

Expand All @@ -19,6 +20,11 @@ Decided 2026-10-25 (maintainer), from the spike:
- Legacy egress synthesizes neither: old players lose end durations and
pause markers, which they already tolerate being absent.

Decided 2026-10-27 (maintainer): moq-mux publishes HLS records by default
until this lands. Then the `<track>.hls` record tracks, the catalog `hls`
field (hang draft and both catalog libraries), the `catalog::Config::with_hls`
opt-out, and any A/V segment alignment go away.

Convert each consumer that numbers groups: hang and moq-mux timelines,
HLS/DASH export, `seek` for GOP-aligned failover in moq-mux, HLS import's
discontinuity packing, transcode's 1:1 numbering, archive keys and resume,
Expand All @@ -27,8 +33,10 @@ JS container consumers and `js/watch`. Update the hang draft and the docs
each one makes stale; rename CLI flags per the repo's CLI rule.

Public API: breaking where a consumer exposes group numbers (CLI
`--group`, archive keys). Wire: hang drops empty duration frames and
discontinuity marker groups.
`--group`, archive keys), and `moq_mux::hls`, `hang::hls`, and
`catalog::Config::with_hls` are deleted. Wire: hang drops empty duration
frames, discontinuity marker groups, the `hls` catalog field, and the record
tracks.

## Required

Expand Down
22 changes: 11 additions & 11 deletions quest/m2/js-hls.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@

## Goal

`@moq/publish` publishes the opt-in per-rendition `hls` field and tracks Rust
does, with the same records and declared duration, when the application
enables it, so a browser-published broadcast exports as HLS like a native one.
Nothing publishes it by default.
`@moq/publish` publishes the per-rendition `hls` field and record tracks by
default, with the same records and declared duration Rust does, so a
browser-published broadcast exports as HLS like a native one.

## Plan

Retargeted 2026-10-17: the field exists only for HLS compatibility and is
opt-in (Catalog HLS field, landed). The archive
does not use it, so the old idle `flush()` and catalog and data track timelines
are dropped from this port.
Decided 2026-10-27 (maintainer): records are on by default in Rust
(`moq_mux::catalog::Config` publishes them unless `with_hls(None)`), and JS
matches, until [time addressing](/quest/m1/time-addressing/consumers.md) lets
the export list segments from group starts and deletes the records. The
archive does not use the field, so the old idle `flush()` and catalog and data
track timelines stay out of this port.

Each rendition cuts its own segments on a pts grid and numbers its own
records, sharing no state with the others, as `rs/moq-mux/src/hls.rs` does.

The read side follows Catalog HLS field. Port the Rust producer
(`rs/moq-mux`) to `js/hang`, wire the opt-in through `@moq/publish`, and check
its records against Rust output in the interop suite.

(`rs/moq-mux`) to `js/hang`, publish it from `@moq/publish` by default, and
check its records against Rust output in the interop suite.
2 changes: 1 addition & 1 deletion rs/hang/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Delete matches on `TooManyRenditions`. An application that wants a limit counts
### HLS records replace the root archive

`catalog::Archive`, `catalog::Timeline`, `Catalog::archive`, and the `timeline` module are gone.
A video or audio rendition opts in to HLS with its own `hls: Option<catalog::Hls>` field, naming its record track and nominal segment duration.
A video or audio rendition carries its own `hls: Option<catalog::Hls>` field, naming its record track and nominal segment duration.
Records are `hls::Record { sequence, group, pts, duration }` in milliseconds on a `<track>.hls` track (`hls::default_name`, `hls::SUFFIX`).
Build one with `Record::new(sequence, group, pts)`, adding `with_duration(0)` where content stops.
A catalog with an old root `archive` still parses, with the entry ignored.
Expand Down
Loading
Loading