Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 49 additions & 0 deletions Cargo.lock

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

52 changes: 43 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ cargo run --release -p moq-studio --features capture,openh264 -- \
the first relay only. The stream's tracks claim their bitrate on the first
relay's link alone, so only that link is probed toward the stream's rate;
no encoder adapts to either estimate.
- `capture` adds camera, display, window, microphone, and system-audio sources.
On Linux it links ALSA; add `pipewire` for PipeWire cameras. `openh264`
- `capture` adds camera, display, window, microphone, and system-audio
sources, and listening to a mix. On Linux it links ALSA; add `pipewire` for PipeWire cameras. `openh264`
is the software H.264 fallback for a machine without a hardware encoder.
- `cef` adds browser sources, on Linux only for now. The build takes CEF from
`CEF_PATH`, which the dev shell sets, or else downloads it, and the binary
Expand Down Expand Up @@ -98,7 +98,7 @@ Every panel is a tab you can drag, split, or stack.
| Devices | List this machine's cameras, displays, windows, and audio inputs, and add one as a source. |
| Guests | Invite a guest, show their join link as a QR code, see who joined, and take a guest out of every scene. |
| Filters | Crop the selected source or item, add, order, and set its filters, set an item's blend mode, and copy or paste filters. |
| Audio | Set each audio source's gain and mute, and watch its meter and the program's. |
| Audio | Add, rename (double-click), and remove mixes, pick the one on program and the one to listen to and on which output, and set each audio source's gain, mute, delay, and the mixes it feeds, with a meter on every mix and source. |
| Output | Start and stop recording, set the canvas size and frame rate, and see the broadcast path, whether the program is encoding, and the encoder's work. |
| Health | See how the stream reaches viewers, relay by relay and track by track, and the cause of each problem. |
| Replay | Replay the last moments of the program or a source in a replay source, slowed down if you like, or save them as a clip. |
Expand Down Expand Up @@ -151,12 +151,34 @@ broadcast through the first relay while that relay has a route to it, and
through the failover otherwise, so a publisher restarted behind only the
failover replaces the first relay's copy once that copy ends. It plays the
best video and audio its catalog lists, switches when the catalog changes, and
comes back on its own when the publisher restarts. Audio from every source is mixed into the program whether
or not its scene is live; mute a source to leave it out.
comes back on its own when the publisher restarts.

### Audio mixes

Audio from every source is mixed whether or not its scene is live. A
collection has one or more **mixes**, each the sum of the sources it is fed:
for example, a stream mix that leaves out music you may not broadcast, and a
monitor mix for your headphones. A source feeds every mix, a new one included,
until you untick that mix in its row of the Audio panel; mute a source to take
it out of every mix. **On program** picks the mix the broadcast carries, and
switching it, or editing a mix, never drops or repeats audio. A recording
holds the mix on program. The mix on program cannot be removed.

**Listen** plays a mix to an audio output, from the `capture` feature's
build: the system default, or the one picked under **Listen on**, which the
studio remembers for you rather than in the scene file. Use headphones: a
microphone that hears the speakers feeds the mix back into itself.

A source's **delay** holds it back in every mix, up to 2 s, so a microphone
lines up with a camera that lags it.

Each mix is advanced on the program clock, and each output reads its mix on
its own, so an output that falls behind skips ahead in its own audio rather
than holding up the mixes or another output.

A browser source renders a web page, such as an overlay, alert box, or chat
widget, at the canvas frame rate (CEF caps it at 60) over a transparent
background, and its sound joins the program audio. Every page shares one
background, and its sound joins the mixes. Every page shares one
browser profile, kept in `$XDG_DATA_HOME/moq-studio/browser`
(`~/.local/share/moq-studio/browser` by default) rather than beside the scene
file, so cookies and logins, such as an alert service's, survive a reload and
Expand Down Expand Up @@ -334,8 +356,14 @@ html = '''
<div class="third">Ada Lovelace</div>
'''

[mixes]
names = ["Stream", "Headphones"]
program = "Stream" # the mix the broadcast carries

[audio.guest]
gain = 0.5
exclude = ["Headphones"] # feeds every other mix
delay = "80ms"

[sources.camera-faded]
kind = "clone"
Expand Down Expand Up @@ -383,6 +411,11 @@ file, which no item places, but which grades what a `lut` filter names it in),
and `replay`, where the Replay panel plays replays. The Devices panel fills in
ids. `[[top]]` items make up the top layer.

`[mixes]` names every mix, in the order the Audio panel lists them, and the
one on `program`; without it, the collection has one mix, `Program`.
`[audio.<source>]` sets a source's linear `gain`, `muted`, the mixes it is
left out of (`exclude`), and its `delay`.

`[video.<source>]` sets a source's `crop` and `filters`, wherever it is shown,
and an item's `crop`, `filters`, and `blend` its own. A `crop` trims a fraction
(0 to 1) of the picture off its `left`, `top`, `right`, and `bottom`, opposite
Expand Down Expand Up @@ -442,7 +475,7 @@ before sharing.
## Output

The program publishes as one H.264 rendition at the canvas size and frame rate,
with Opus stereo audio, under the `--broadcast` path. Timestamps come from the
with the mix on program as Opus stereo audio, under the `--broadcast` path. Timestamps come from the
studio's own clock, so a source that stalls repeats its last picture instead of
holding up the stream.

Expand Down Expand Up @@ -586,8 +619,9 @@ The Health panel grades the stream every second on the thresholds moq's
`hang::health` shares with the rest of the stack, and lists each problem
with its cause in plain words: a relay is unreachable, the uplink is
congested or losing packets, the relay is far away, the encoder is behind or
stuck, a source stalled (a camera or broadcast the program draws, or an
unmuted microphone or system audio), or viewers are late, stalling, or cannot
stuck, a source stalled (a camera or broadcast the program draws, or a
microphone or system audio the mix on program plays: unmuted and not left
out of it), or viewers are late, stalling, or cannot
decode a track. A relay lost while the other serves is
degraded, since viewers have nothing left to fall back to; with none serving
it is unhealthy. Below that it shows:
Expand Down
3 changes: 2 additions & 1 deletion quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ repository.
- [Multiple canvases](/quest/m0/multi-canvas.md) - landscape and portrait canvases composite at once from shared sources
- [The join page is live](/quest/m0/join-page-live.md) - `join.studio.moq.dev` serves the join page `--join-page` defaults to
- [Guest return feed](/quest/m0/guest-return.md) - guests see the program and hear a mix-minus
- [Audio mixes](/quest/m0/mixes.md) - per-output mixes, meters, monitoring, and sync offsets
- [A recording picks its mix](/quest/m0/record-mix.md) - a recording keeps what the stream's mix leaves out
- [Clap-to-measure sync delay](/quest/m0/sync-clap.md) - a clap sets a microphone's delay against its camera
- [Audio filters](/quest/m0/audio-filters.md) - per-source noise suppression, gate, compressor, limiter, and plugins, configured from the studio
- [Browser sources on macOS and Windows](/quest/m0/browser-source.md) - the out-of-process CEF pages Linux has, with shared GPU textures on the other desktops
- [Published independently](/quest/m0/publish-independently.md) - a source opts out of the program and publishes its own track for services that place it themselves
Expand Down
3 changes: 2 additions & 1 deletion quest/m0/broadcast-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ can leave audio and video apart.
Decided 2026-10-26 (maintainer):

- **Scope:** broadcast sources only. Local devices keep the manual
per-source offsets in [Audio mixes](/quest/m0/mixes.md).
per-source `delay`, which [Clap-to-measure](/quest/m0/sync-clap.md) sets
from a clap.
- **Delay:** reuse `moq play`'s timing, the `--delay auto` target from
moq-audio's jitter estimate (`rs/moq-audio/src/jitter.rs`,
`doc/concept/audio-jitter.md`), rather than a second algorithm or a manual
Expand Down
10 changes: 5 additions & 5 deletions quest/m0/guest-return.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The join page plays it.
## Plan

Decided 2026-11-02 (maintainer): guests get a mix-minus, not the full program,
so this waits on audio mixes; video and audio land together. Publish into the
and video and audio land together. Publish into the
room the studio already owns, under names the room's token scope allows, and
keep it free of the studio's own credentials. A guest's token subscribes only
to its own renewals (`.renew/<identity>/<generation>`), so it must also grant the feed's
Expand All @@ -19,7 +19,7 @@ with the guest count, so bound it by design (the guest roster is already
bounded to guests the studio signed for). Raised on
[#10](https://github.com/moq-dev/studio/pull/10).

## Required

- [Audio mixes](/quest/m0/mixes.md) - the per-output mixes a mix-minus is
built from
Audio mixes landed in [#32](https://github.com/moq-dev/studio/pull/32): build
Comment thread
kixelated marked this conversation as resolved.
Resolved
each guest's feed with moq-audio's `bus::Mix::minus` from the mix on program
(`Sources::mix`), taking the guest's own strip out, which costs one
subtraction per block rather than a mix per guest.
26 changes: 0 additions & 26 deletions quest/m0/mixes.md

This file was deleted.

31 changes: 31 additions & 0 deletions quest/m0/record-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# [S] A recording picks its mix

## Goal

A recording in `moq-studio` picks any of the collection's mixes, so it can
keep what the stream leaves out, such as the creator's music, while the
broadcast carries the mix on program. A recording on the same mix as the
program records as it does today.

## Plan

Split from [Audio mixes](https://github.com/moq-dev/studio/pull/32), which
landed the mixes, routing, meters, delay, listening, and the program's pick
(`[mixes] program`).

A recording reads the studio's broadcast the way a viewer does
(`rs/moq-studio/src/record.rs`), so a mix other than the program's needs an
Opus track of its own. Keep that track off the relay: a mix the creator left
off the stream must not be fetchable from it, as source tracks stay local
after [Live encode control](/quest/m0/encode-control.md). Encode it only while
a recording reads it, and record the program's video beside it in one file.
The pick lives in the scene file beside `[mixes] program`, and the Output
panel shows it. Update `README.md`.

## Required

- [Live encode control](/quest/m0/encode-control.md) - the local broadcast a mix track stays off the relay in

## Related

- [Destinations](/quest/m0/destinations.md) - each destination picks a mix the same way
28 changes: 28 additions & 0 deletions quest/m0/sync-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# [S] Clap-to-measure sync delay

## Goal

A clap in front of a camera, seen and heard, measures how far a microphone
leads that camera, and sets the microphone's `delay` to it, instead of the
creator guessing the number.

## Plan

Split from [Audio mixes](https://github.com/moq-dev/studio/pull/32), which
landed the per-source `delay` the measurement sets.

Measure both onsets on the program clock: the clap's sound in the
microphone's audio before it is delayed, and the frame where the hands meet in
the camera's pictures. The difference, bounded by moq-audio's
`bus::MAX_DELAY`, becomes the delay, as one undoable edit. A clap that is not
found, or found in only one of the two, fails with the reason rather than
setting a guess.

The studio sees a source's audio only through its strip's level meter, read
once a frame, which is too coarse to time a clap. Timing it needs the
source's samples, or an onset, with their position on the bus clock, which is
moq-audio work: land it there first and pin it.

## Related

- [Broadcast sources play in sync](/quest/m0/broadcast-sync.md) - broadcast sources align on the publisher's timestamps instead
Loading