Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c1331c3
chore(quest): claim quest/m1/api-audit/media-trim.md
kixelated Oct 23, 2026
d17fd99
refactor(video)!: trim the capture, preset report, and DMA-BUF surface
kixelated Oct 23, 2026
9b060c7
refactor(audio): trim the encode surface and await playback observation
kixelated Oct 23, 2026
cf2d744
refactor(media): trim json, mux, hang, and loc surfaces
kixelated Oct 23, 2026
754f27a
refactor(hls)!: drop history and range playlists, expose routes, trim…
kixelated Oct 23, 2026
6b7977f
refactor(mux): drop the unreachable TimelineCheckpoint error
kixelated Oct 23, 2026
d7fe30e
fix: format hls export and drop stale ffi preset fields
kixelated Oct 23, 2026
42cc76b
fix(video): allow Control::cut outside the openh264 capture tests
kixelated Oct 23, 2026
c0f5a2d
Merge remote-tracking branch 'origin/main' into quest/m1/api-audit/me…
kixelated Oct 23, 2026
d71bede
quest: drop media-trim and the history-floor quest it obsoletes
kixelated Oct 23, 2026
772937b
fix: update merged tests for encode::Config and the removed audio preset
kixelated Oct 23, 2026
544cd9c
Merge remote-tracking branch 'origin/main' into quest/m1/api-audit/me…
kixelated Oct 23, 2026
8a2ab8f
fix(video): give the test-only dead_code allows a reason
kixelated Oct 23, 2026
3302537
Merge remote-tracking branch 'origin/main' into quest/m1/api-audit/me…
kixelated Oct 23, 2026
dc31bcf
fix(audio): release a cancelled finish's waker through kio
kixelated Oct 23, 2026
1d1ebb1
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 24, 2026
c328074
fix(audio): resolve an observer's finish once no stream is left
kixelated Oct 24, 2026
3ee9c52
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 24, 2026
84552f4
Merge origin/main into quest/m1/api-audit/media-trim
kixelated Oct 24, 2026
c03ca5a
fix(json): keep window Producer::cut public for the ffi Window
kixelated Oct 24, 2026
ef98635
Merge origin/main into quest/m1/api-audit/media-trim
kixelated Oct 24, 2026
0cd51ef
Merge origin/main into quest/m1/api-audit/media-trim
kixelated Oct 24, 2026
011b2b4
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 24, 2026
e06cc98
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Oct 24, 2026
e78e046
Merge remote-tracking branch 'origin/main' into rq-6517
kixelated Oct 24, 2026
cf9673a
Merge remote-tracking branch 'origin/main' into rq-6517
kixelated Oct 24, 2026
bee0e34
Merge remote-tracking branch 'origin/main' into rq-6517
kixelated Oct 24, 2026
b46d100
fix(video): import DmaBufLayout from frame in the vaapi test fixture
kixelated Oct 24, 2026
84d9f25
Merge remote-tracking branch 'origin/main' into rq-6517
kixelated Oct 24, 2026
7f9fea9
Merge remote-tracking branch 'origin/main' into rqb-6517
kixelated Oct 24, 2026
ba0a8b8
Merge remote-tracking branch 'origin/main' into rqb-6517
kixelated Oct 24, 2026
703122b
Merge remote-tracking branch 'origin/main' into rqb-6517
kixelated Oct 24, 2026
6e84972
Merge remote-tracking branch 'origin/main' into rqb-6517
kixelated Oct 24, 2026
d6f7bd2
Merge remote-tracking branch 'origin/main' into rqb-6517
kixelated Oct 24, 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: 0 additions & 2 deletions Cargo.lock

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

19 changes: 10 additions & 9 deletions cpp/obs/src/moq-audio-encoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ namespace {
// Opus codes natively at 48 kHz, so OBS resamples its mix to it.
constexpr uint32_t SAMPLE_RATE = 48000;

std::optional<moq::AudioPreset> ParsePreset(const char *name)
// The preset picks the Opus packet duration in microseconds: low latency halves
// the packetization delay at twice the packet rate. Quality matches balanced,
// since libopus already runs at full complexity and longer packets only add delay.
std::optional<uint32_t> ParsePreset(const char *name)
{
if (strcmp(name, "low_latency") == 0)
return moq::AudioPreset::kLowLatency;
if (strcmp(name, "balanced") == 0)
return moq::AudioPreset::kBalanced;
if (strcmp(name, "quality") == 0)
return moq::AudioPreset::kQuality;
return 10000;
if (strcmp(name, "balanced") == 0 || strcmp(name, "quality") == 0)
return 20000;
return std::nullopt;
}

Expand All @@ -51,8 +52,8 @@ struct MoQAudioEncoder {
void *Create(obs_data_t *settings, obs_encoder_t *encoder)
{
const char *preset_name = obs_data_get_string(settings, "preset");
const auto preset = ParsePreset(preset_name);
if (!preset) {
const auto frame_duration_us = ParsePreset(preset_name);
if (!frame_duration_us) {
LOG_ERROR("Unknown MoQ audio preset: %s", preset_name);
return nullptr;
}
Expand All @@ -66,7 +67,7 @@ void *Create(obs_data_t *settings, obs_encoder_t *encoder)
output.sample_rate = SAMPLE_RATE;
output.channels = channels;
output.bitrate = Bitrate(settings);
output.preset = *preset;
output.frame_duration_us = *frame_duration_us;

auto codec = moq::AudioEncoder::init(output);
if (!codec) {
Expand Down
5 changes: 1 addition & 4 deletions dart/moq/lib/audio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ typedef Consumer = MoqAudioConsumer;
/// The PCM layout written to a [Producer].
typedef EncoderInput = MoqAudioEncoderInput;

/// The codec side: codec, rate, channels, bitrate, preset, and frame duration (0 takes the codec's own frame).
/// The codec side: codec, rate, channels, bitrate, and frame duration (0 takes the codec's own frame).
typedef EncoderOutput = MoqAudioEncoderOutput;

/// The PCM layout a [Consumer] delivers, how stale a group may get, and its playout delay.
Expand All @@ -43,9 +43,6 @@ typedef Codec = MoqAudioCodec;
/// A raw PCM sample layout.
typedef SampleFormat = MoqAudioSampleFormat;

/// How the encoder trades latency for compression; null in [EncoderOutput] means `balanced`.
typedef Preset = MoqAudioPreset;

/// Duration view over a PCM frame's timestamp.
extension AudioFrameDuration on MoqAudioFrame {
/// Presentation timestamp of the first sample.
Expand Down
3 changes: 1 addition & 2 deletions dart/moq/test/moq_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -751,8 +751,7 @@ void main() {
),
output: moq_audio.EncoderOutput(
codec: moq_audio.Codec.opus(),

preset: moq_audio.Preset.lowLatency,
frameDurationUs: 10000,
),
track: 'mic',
),
Expand Down
101 changes: 0 additions & 101 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import "dart:ffi";
import "dart:io" show Platform, File, Directory;
import "dart:isolate";
import "dart:typed_data";

import "package:ffi/ffi.dart";

import "uniffi_runtime.dart";
export "uniffi_runtime.dart";

Expand Down Expand Up @@ -265,14 +263,12 @@ class MoqAudioEncoderOutput {
final int? channels;
final int? bitrate;
final int frameDurationUs;
final MoqAudioPreset? preset;
MoqAudioEncoderOutput({
required this.codec,
this.sampleRate = null,
this.channels = null,
this.bitrate = null,
this.frameDurationUs = 0,
this.preset = null,
});
}

Expand Down Expand Up @@ -308,19 +304,13 @@ class FfiConverterMoqAudioEncoderOutput {
);
final frameDurationUs = frameDurationUs_lifted.value;
new_offset += frameDurationUs_lifted.bytesRead;
final preset_lifted = FfiConverterOptionalMoqAudioPreset.read(
Uint8List.view(buf.buffer, new_offset),
);
final preset = preset_lifted.value;
new_offset += preset_lifted.bytesRead;
return LiftRetVal(
MoqAudioEncoderOutput(
codec: codec,
sampleRate: sampleRate,
channels: channels,
bitrate: bitrate,
frameDurationUs: frameDurationUs,
preset: preset,
),
new_offset - buf.offsetInBytes,
);
Expand All @@ -333,7 +323,6 @@ class FfiConverterMoqAudioEncoderOutput {
FfiConverterOptionalUInt32.allocationSize(value.channels) +
FfiConverterOptionalUInt32.allocationSize(value.bitrate) +
FfiConverterUInt32.allocationSize(value.frameDurationUs) +
FfiConverterOptionalMoqAudioPreset.allocationSize(value.preset) +
0;
final buf = Uint8List(total_length);
write(value, buf);
Expand Down Expand Up @@ -362,10 +351,6 @@ class FfiConverterMoqAudioEncoderOutput {
value.frameDurationUs,
Uint8List.view(buf.buffer, new_offset),
);
new_offset += FfiConverterOptionalMoqAudioPreset.write(
value.preset,
Uint8List.view(buf.buffer, new_offset),
);
return new_offset - buf.offsetInBytes;
}

Expand All @@ -375,7 +360,6 @@ class FfiConverterMoqAudioEncoderOutput {
FfiConverterOptionalUInt32.allocationSize(value.channels) +
FfiConverterOptionalUInt32.allocationSize(value.bitrate) +
FfiConverterUInt32.allocationSize(value.frameDurationUs) +
FfiConverterOptionalMoqAudioPreset.allocationSize(value.preset) +
0;
}
}
Expand Down Expand Up @@ -4780,44 +4764,6 @@ class FfiConverterMoqVideoProducerOptions {
}
}

enum MoqAudioPreset { lowLatency, balanced, quality }

class FfiConverterMoqAudioPreset {
static LiftRetVal<MoqAudioPreset> read(Uint8List buf) {
final index = buf.buffer.asByteData(buf.offsetInBytes).getInt32(0);
switch (index) {
case 1:
return LiftRetVal(MoqAudioPreset.lowLatency, 4);
case 2:
return LiftRetVal(MoqAudioPreset.balanced, 4);
case 3:
return LiftRetVal(MoqAudioPreset.quality, 4);
default:
throw UniffiInternalError(
UniffiInternalError.unexpectedEnumCase,
"Unable to determine enum variant",
);
}
}

static MoqAudioPreset lift(RustBuffer buffer) {
return FfiConverterMoqAudioPreset.read(buffer.asUint8List()).value;
}

static RustBuffer lower(MoqAudioPreset input) {
return toRustBuffer(createUint8ListFromInt(input.index + 1));
}

static int allocationSize(MoqAudioPreset _value) {
return 4;
}

static int write(MoqAudioPreset value, Uint8List buf) {
buf.buffer.asByteData(buf.offsetInBytes).setInt32(0, value.index + 1);
return 4;
}
}

enum MoqAudioSampleFormat {
u8,
s16,
Expand Down Expand Up @@ -14675,53 +14621,6 @@ class FfiConverterOptionalMoqAudioFrame {
}
}

class FfiConverterOptionalMoqAudioPreset {
static MoqAudioPreset? lift(RustBuffer buf) {
return FfiConverterOptionalMoqAudioPreset.read(buf.asUint8List()).value;
}

static LiftRetVal<MoqAudioPreset?> read(Uint8List buf) {
if (ByteData.view(buf.buffer, buf.offsetInBytes).getInt8(0) == 0) {
return LiftRetVal(null, 1);
}
final result = FfiConverterMoqAudioPreset.read(
Uint8List.view(buf.buffer, buf.offsetInBytes + 1),
);
return LiftRetVal<MoqAudioPreset?>(result.value, result.bytesRead + 1);
}

static int allocationSize([MoqAudioPreset? value]) {
if (value == null) {
return 1;
}
return FfiConverterMoqAudioPreset.allocationSize(value) + 1;
}

static RustBuffer lower(MoqAudioPreset? value) {
if (value == null) {
return toRustBuffer(Uint8List.fromList([0]));
}
final buf = Uint8List(
FfiConverterOptionalMoqAudioPreset.allocationSize(value),
);
FfiConverterOptionalMoqAudioPreset.write(value, buf);
return toRustBuffer(buf);
}

static int write(MoqAudioPreset? value, Uint8List buf) {
if (value == null) {
buf[0] = 0;
return 1;
}
buf[0] = 1;
return FfiConverterMoqAudioPreset.write(
value,
Uint8List.view(buf.buffer, buf.offsetInBytes + 1),
) +
1;
}
}

class FfiConverterOptionalMoqBandwidth {
static MoqBandwidth? lift(RustBuffer buf) {
return FfiConverterOptionalMoqBandwidth.read(buf.asUint8List()).value;
Expand Down
18 changes: 2 additions & 16 deletions doc/bin/hls.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ transcoded, so the player must support the publisher's codecs.
What that means for you:

- **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 default exporter reads the timeline checkpoint and lists at most 256 segments. `export::Config::history` reads a durable recording's retained timeline groups from the beginning instead of joining only the checkpoint tail.
- **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.
Expand All @@ -52,21 +52,7 @@ What that means for you:

A [`moq-archive`](https://docs.rs/moq-archive) recording replayed through its
`Reader` is served the same way, with no second stored copy, and lists the
same window as a live broadcast by default. `export::Config::history` includes
earlier timeline groups even after replay-cache eviction: the exporter FETCHes
missing groups from the Reader's stored timeline objects before following live
edits. Missing or unreadable stored history fails that export with an error,
and so does a recording whose start was pruned by retention.

With `export::Config::history` enabled, a durable recording's media playlists
also accept `start` and `end` query parameters in decimal media-time seconds.
Both are required and name a half-open range, for example
`media.m3u8?start=12.5&end=30`. The playlist includes every overlapping
segment, widening its start to the preceding video sync point, and adds
`EXT-X-ENDLIST` once media covers the requested end. It remains an event
playlist while that end is still ahead of the live edge. A malformed range
returns HTTP 400. A live-only timeline or range past a finished recording
returns HTTP 416 instead of a partial clip, and a failed timeline returns 500.
same window as a live broadcast.

`--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/lib/rs/moq-audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ capture or playback is enabled.

The microphone opens only while someone listens, and can be swapped without
changing the track subscribers know. Opus packs 20 ms per packet by default,
or 10 ms with the low-latency preset. Groups carry at least 20 ms by default,
so the low-latency preset pairs packets. `encode::Options::group_duration`
or 10 ms with a shorter `frame_duration`. Groups carry at least 20 ms by default,
so 10 ms packets pair up. `encode::Options::group_duration`
changes that minimum; zero puts every packet in its own group. Longer groups
mean fewer streams per relay but coarser loss. There is no AAC encoder, so an
AAC encode request is refused; AAC decode is for broadcasts from the ingest gateways.
Expand Down
18 changes: 2 additions & 16 deletions go/wrapper/audio/audio.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,21 @@ type Packet = ffi.MoqAudioPacket
// EncoderFinish holds the packets Encoder.Finish drains and the padded samples to discard.
type EncoderFinish = ffi.MoqAudioEncoderFinish

// Preset is how the encoder trades latency for compression; a nil EncoderOutput.Preset is PresetBalanced.
type Preset = ffi.MoqAudioPreset

const (
// PresetLowLatency packs 10 ms Opus packets, at twice the packet rate.
PresetLowLatency = ffi.MoqAudioPresetLowLatency
// PresetBalanced packs 20 ms Opus packets, the default.
PresetBalanced = ffi.MoqAudioPresetBalanced
// PresetQuality favors compression; 20 ms Opus packets today, the same as PresetBalanced.
PresetQuality = ffi.MoqAudioPresetQuality
)

// EncoderOutput configures the codec side: codec, rate, channels, bitrate, frame duration (0 takes the preset's or codec's own frame), and preset.
// Preset is refused alongside a nonzero FrameDuration.
// EncoderOutput configures the codec side: codec, rate, channels, bitrate, and frame duration (0 takes the codec's own frame; 10 ms Opus halves the packetization delay).
type EncoderOutput struct {
Codec Codec
SampleRate *uint32
Channels *uint32
Bitrate *uint32
FrameDuration time.Duration
Preset *Preset
}

func (o EncoderOutput) ffi() (ffi.MoqAudioEncoderOutput, error) {
frame, err := bridge.Duration(o.FrameDuration, "audio frame duration")
if err == nil && frame > math.MaxUint32 {
err = fmt.Errorf("audio frame duration %v exceeds %d microseconds", o.FrameDuration, uint32(math.MaxUint32))
}
return ffi.MoqAudioEncoderOutput{Codec: o.Codec, SampleRate: o.SampleRate, Channels: o.Channels, Bitrate: o.Bitrate, FrameDurationUs: uint32(frame), Preset: o.Preset}, err
return ffi.MoqAudioEncoderOutput{Codec: o.Codec, SampleRate: o.SampleRate, Channels: o.Channels, Bitrate: o.Bitrate, FrameDurationUs: uint32(frame)}, err
}

// DecoderOutput configures the PCM a Consumer delivers, how stale a group may get before it is skipped, and the playout delay.
Expand Down
2 changes: 1 addition & 1 deletion go/wrapper/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type (
AudioDecoderOutput = ffi.MoqAudioDecoderOutput
// AudioEncoderInput declares the PCM sample format, sample rate, and channel count of frames written to an audio producer.
AudioEncoderInput = ffi.MoqAudioEncoderInput
// AudioEncoderOutput configures the encoder: codec, optional sample rate, channels, bitrate, frame duration, and preset.
// AudioEncoderOutput configures the encoder: codec, optional sample rate, channels, bitrate, and frame duration.
AudioEncoderOutput = ffi.MoqAudioEncoderOutput
// AudioSampleFormat is a raw PCM sample layout, mirroring WebCodecs AudioData.format.
AudioSampleFormat = ffi.MoqAudioSampleFormat
Expand Down
14 changes: 8 additions & 6 deletions js/hang/src/catalog/root.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as z from "@zod/mini";
import { ARCHIVE_VERSION, ArchiveSchema } from "./archive.ts";
import { u53 } from "./integers.ts";
import type { ReferenceEpoch, RelativeBroadcast } from "./path.ts";
import { checkRenditions, DuplicateRendition, minVersion, RootSchema, UnsupportedVersion, VERSION } from "./root.ts";
import { checkRenditions, DuplicateRendition, encode, RootSchema, UnsupportedVersion, VERSION } from "./root.ts";
import { track } from "./track.ts";

// The base catalog carries the media sections (`video`/`audio`) and the data track sections
Expand Down Expand Up @@ -324,12 +324,14 @@ test("the catalog version is needed only for misread fields", () => {
const root = (hd: object) =>
RootSchema.parse({ video: { renditions: { hd } }, audio: { renditions: { en: audio } } });

expect(minVersion(root(avc))).toBeUndefined();
expect(minVersion(root({ ...avc, track: "hd" }))).toBeUndefined();
expect(minVersion(root({ ...avc, track: "video" }))).toBe(4);
expect(minVersion(root({ ...avc, warmup: 500 }))).toBe(4);
expect(encode(root(avc)).version).toBeUndefined();
expect(encode(root({ ...avc, track: "hd" })).version).toBeUndefined();
expect(encode(root({ ...avc, track: "video" })).version).toBe(4);
expect(encode(root({ ...avc, warmup: 500 })).version).toBe(4);
const data = RootSchema.parse({ json: { tracks: { chat: { mode: "stream", track: "chat.json" } } } });
expect(minVersion(data)).toBe(4);
expect(encode(data).version).toBe(4);
// A carried version is recomputed, never trusted.
expect(encode({ ...root(avc), version: 4 }).version).toBeUndefined();
});

test("shared stats and echo discovery round trips without changing old catalogs", async () => {
Expand Down
Loading
Loading