moq-studio is a native streaming studio for Linux, macOS, and Windows. It
composes cameras, screens, windows, images, colors, web pages, HTML and CSS
overlays, and broadcasts from a relay into scenes on the GPU, mixes their audio, and publishes the result to a
MoQ relay. It is early and not packaged yet,
so build it from source. The moq crates come from the moq submodule:
git clone --recurse-submodules https://github.com/moq-dev/studio
cd studio
cargo run --release -p moq-studio --features capture,openh264 -- \
show.toml --connect https://cdn.moq.dev/anon --broadcast my-showshow.tomlis the scene collection. If it does not exist yet, guided setup proposes one and creates it. Changes are saved to it a second after you stop editing, with no save step.--recordings <dir>is where recordings are saved, by default besideshow.toml.- Without
--connect, the studio composes, previews, and records but publishes nothing, and broadcast sources stay offline. --failover <url>publishes the same broadcast to a second relay under the same broadcast instance, so a viewer linked to both resumes mid-group from it when the first fails. Both relays must speak the one--connect-version, which must carry instances (moq-lite-07-wiptoday, which a relay accepts only when configured to), or the studio refuses to start; it also refuses a fixed--connect-bindport, since each relay's connection binds its own. Its credential rides its URL's?jwt=;--connect-tokengoes to 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, and the video follows that link's estimate alone.- Quitting while live ends every track after its last pictures and sound, then closes each relay session once the relay has them, waiting up to about a second, so viewers and recordings on the relay see the broadcast end rather than cut off.
captureadds camera, display, window, microphone, and system-audio sources, and listening to an audio scene. On Linux it links ALSA; addpipewirefor PipeWire cameras and the virtual camera's PipeWire node.openh264is the software H.264 fallback for a machine without a hardware encoder.cefadds browser sources, on Linux only for now. The build takes CEF fromCEF_PATH, which the dev shell sets, or else downloads it, and the binary loadslibcef.sofrom beside itself or from thatCEF_PATH.- On Linux, display and window sources capture X11 and XWayland only; a Wayland screen or window cannot be picked yet.
- On macOS, the studio is not an app bundle yet, so the camera, microphone, and Screen & System Audio Recording permissions are asked of, and granted to, the app that launched it, such as your terminal. Display, window, and system-audio sources need the last, granted in System Settings > Privacy & Security.
- The
--connect-*,--quic-*, and--log-levelflags are moq-cli's; seemoq-studio --help.
Opening a scene file that does not exist yet starts guided setup instead of the studio. It runs three checks, and a check that fails shows its reason:
- Devices: the cameras, displays, and audio inputs this machine has.
- Encoders: for each canvas setup can propose (1080p and 720p at 60 or 30 fps, and 360p at 30 fps), the encoder the program would pick, whether it reads the GPU, the bitrate it encodes at, and how long a frame takes over a short burst of moving frames. An encoder keeps up when a frame takes at most three quarters of the frame period, leaving the rest for sources and the compositor.
- Uplink: with
--connect, setup publishes a probe at the--broadcastpath for about six seconds and subscribes to it through the relay, so the relay pulls it. The congestion controller's send-rate estimate is the measurement. With--failover, only the--connectrelay is measured, since only its link is probed toward the stream's rate. A connection that reports no estimate, such as WebSocket, fails the check.
Setup then proposes a scene named Main and a canvas, explaining each choice. The scene has the first display filling the canvas, the first camera in the bottom-right corner over it (or filling the canvas alone), and the first audio input. Swap any of them for another device or leave it out. The canvas is the largest one whose encoder keeps up and whose video and audio fit in two thirds of the measured uplink, the most the Health panel grades healthy. Without a measurement, it is the default 1080p at 30 fps. If no encoder keeps up, it is the canvas closest to keeping up.
Nothing is written until you start the studio, so quitting during setup runs it again on the next launch. Start with this scene and Start with an empty scene both create the scene file at once, the second with the chosen canvas and no sources. The proposed scene is the first edit in the history, so undoing it leaves that empty scene.
Setup writes an ordinary scene file. Change its sources and scenes in their panels, and its canvas in the Output panel. The Health panel shows how the uplink holds up once you are live.
Every panel is a tab you can drag, split, or stack.
| Panel | Use it to |
|---|---|
| Preview, Program | See the scene you are editing, at its own size, and the one that is live, and drag items in the preview to place them. A preview no tab shows is not drawn. Cut puts the preview on program at once; Transition takes it there by the transition picked beside it, over the time set there, or for a stinger by the media source and cut point set there (see Transitions). Interact passes the mouse over the preview to the browser source under it instead, and the keyboard to the one last clicked. |
| Scenes | Pick the scene in preview, and add, rename (double-click), reorder, remove, or tick Publish on one and pick the audio scene it carries (see Published scenes). A scene on program, or still leaving it in a transition, cannot be removed, even by an undo, until program cuts away from it. Add, rename, reorder, and remove audio scenes too (see Audio scenes). |
| History | See every edit, and undo or redo back to any of them. |
| Items | Place, show, hide, and order what the preview scene or the top layer draws, back to front. The top layer is edited over a scene of the program's size. Select an item to filter or copy it. |
| Sources | Add a source by hand, see whether each and its own track are live and where it is shown, clone one, and remove one. Select a source to filter it. A camera or broadcast without a new picture, or a microphone or system audio without new sound, for a second reads as stalled. A browser source shows whether its page is copied on the GPU or, as a fallback, uploaded from memory (CPU, saying why on hover), in place of a track, and Reload restarts its page. A media file reads as ended once it plays to its end without looping, Play plays it from its start, Stop stops it at once, blank and silent, until played again, and Reload reads the file again. |
| 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 | Remove the selected source's background, crop the selected source or item, add, order, and set its filters, set an item's blend mode, and copy or paste filters. |
| Keyframes | Animate the selected item: add, time, ease, and remove its keyframes, and set how far each moves, resizes, crops, and filters it (see Keyframes). Play plays the preview scene's keyframes again. |
| Audio | Pick the audio scene on program and the one to listen to and on which output, set each sound's gain, mute, and delay, and send each sound and audio scene into audio scenes, with a meter on every one. |
| Output | Start and stop recording, pick the picture and the audio scene it records, set the canvas size and frame rate and each scene's size, see the broadcast path, whether the program is encoding, the encoder sessions open, and the encoder work of the program, the recording, and each published scene, and turn the virtual camera of the program or of each published scene on. |
| Health | See how the stream reaches viewers, relay by relay and, for the program and each published scene, track by track, and the cause of each problem, and which recordings a crash left unfinished were repaired at launch. |
| 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. |
| Hotkeys | Bind keys to cut, transition, preview a scene, replay, save a clip, and start or stop recording, and clear them (see Hotkeys). |
A new source, from the Sources or Devices panel, is placed over whatever the Items panel shows. The top layer is drawn over program, and over the preview of a scene of the program's size, and stays on through a cut or transition: a downstream keyer for a logo or a lower third.
Every scene has a size, the program's unless the Output panel's Scene sizes gives it its own, such as 1080x1920 for a portrait scene beside a 1920x1080 program. Every scene is drawn at the program's frame rate. Only a scene of the program's size goes on program: Cut and Transition refuse another, and an edit resizing the scene on air is refused. The preview shows a scene at its own size.
Tick Publish on a scene in the Scenes panel to put it live as a broadcast
of its own beneath the program's, at <--broadcast>/<scene name>, at its own
size, with renditions and encoders of its own (see Output). A
published scene is always live and never switched: it shows what its items
show, whatever is on program. For a portrait stream that follows the program,
publish a portrait scene drawing the program through a program source (see
Scene sources), with any overlay of its own placed beside
it; the top layer goes over the program alone. It carries the audio scene
on program, or the one picked beside its Publish box, encoded as audio of
its own, so picking another never changes the track its viewers play.
Unticking it, or removing the scene, ends
its broadcast. A published scene's name is one segment of its broadcast path,
so it holds no /, and a \ only before another \. A scene named viewers, where viewers report back,
does not publish while live.
A source captures or decodes once however many scenes show it. A published
scene is drawn only while one of its encoders takes the frame, so with
--on-demand, or without a relay, one nobody watches costs no drawing. An
item's keyframes play in a published scene from when it was published, picked
for recording, or the studio started, whichever came first: a recording and a
broadcast of one scene share one picture.
A scene source draws another scene as one picture, which an item places, scales, crops, filters, and blends like any other: a lower third built once and shown in every scene, or a whole layout shrunk into a corner. Add one in the Sources panel as kind Scene, picking the scene it draws, or Program, which shows whatever is on air, a transition under way included, on the same frame, such as for a portrait scene that follows the program, cropped.
A nested scene is drawn at its own size, transparent where it draws nothing, once per frame however many items show it, and only while something drawn shows it. Its items' keyframes play from when the scene showing it goes on, as that scene's own items' do, so the top layer, which never goes on, cannot show a scene with keyframes. A media file or overlay in a nested scene set to restart starts over when the scene showing it goes on program.
A scene cannot draw itself, even through other scenes, so an edit that would is refused. A scene showing the program cannot go on program, nor can the top layer show it, since either would draw the program into itself: Cut and Transition refuse such a scene, and an edit making the scene on air show the program is refused. Renaming a scene renames what its scene sources draw, and removing one removes them, with the items showing them.
| Transition | What it does |
|---|---|
| Fade | The incoming scene fades in over the outgoing one, at an even pace. |
| Slide | The incoming scene pushes the outgoing one off the canvas, both moving left, right, up, or down. |
| Wipe | An edge sweeps across the canvas left, right, up, or down, uncovering the incoming scene behind it. |
| Move | Each item showing a source both scenes place glides from its place in the outgoing scene to its place in the incoming one, resizing, recropping, and mixing its filters' settings on the way. If its blend mode or kinds of filter differ, it changes look halfway. The rest of the outgoing scene fades out and the rest of the incoming scene fades in. Items stack as the outgoing scene has them until halfway, then as the incoming one does. A source placed more than once pairs up in order, back to front. |
| Stinger | A media source's clip plays over the program from its start, and the program switches scenes under it at the cut point, the time into the clip when it covers the canvas. It lasts as long as the clip. |
A slide, wipe, or move eases in and out. A transition is drawn on the program clock, so the stream and a recording show the same frames of it, and a second Transition while one runs starts from the scene it was heading to. A transition set to 0 ms is a cut.
A stinger's clip fills the canvas, cropped evenly to the canvas's shape rather than stretched, through its source's crop and filters, so a clip without an alpha channel can be keyed. The cut point counts from the clip's first picture or sound, however long the file takes to start decoding. Its sound joins the mixes as its source's does. Its source waits blank and silent for the first stinger rather than playing once when it opens, and rests ended between stingers, which the Health panel does not count unless a scene on air places it. A clip played to its end holds its last picture, as any media file does, and a looping one stops, blank and silent, after one pass. A clip that cannot play, or that is played again from the Sources panel or reloaded while a stinger plays it, cuts at once; one stopped there ends the stinger at once. A Cut, or another kind of transition, while a stinger runs stops its clip, sound included; another stinger plays the clip again from its start.
An item's keyframes animate it: an item can fly in, fade in, or blur into focus, and a camera can push in. Each keyframe is a time after the item's scene goes on and how far the item is from where it rests then: moved and resized by canvas pixels, cropped further, and drawn through filters after its own. Between two keyframes the item eases from one to the next, linearly or easing in, out, or (by default) both. Filters of the same kinds, in order, mix their settings, and others change halfway. A keyframe's filters cannot be a LUT or a mask.
Keyframes play once, from when the item's scene goes on program, and hold the last keyframe; before the first, the item holds the first. A cut or transition starts them again, even to the scene already on program, while a scene leaving in a transition carries on with its own. The preview plays them from when the scene went into preview; pick the scene again, or press Play, to watch them again. They are drawn on the program clock, so the stream, a recording, and a replay of the program show the same frames. Top-layer items have none, since the top layer never goes on.
Dragging an item in the preview grabs it where its keyframes have it and moves where it rests, so its whole animation moves with it.
In the preview, click an item of whatever the Items panel shows to select it, drag it to move it, and drag its edges or corners to resize it. A drag snaps edges and centers to the canvas's edges and center, to the other items shown, and to the grid while Grid is on, drawing a guide through what it lines up with. A moved item also snaps to the spacing of its neighbors, marking the equal spaces. Hold Ctrl (Cmd on macOS) to drag without snapping, or turn Snap off. Resizing a camera, display, window, image, media file, broadcast, or replay keeps its shape, and resizing anything else does not; hold Shift to flip that. Click the preview, then the arrow keys nudge the selected item a pixel, or ten with Shift held, and a held key undoes as one step. The snap and grid settings are kept per user, with the hotkeys, not in the scene file.
Items showing the same source are linked: one capture, with the source's filters, wherever it is shown. A clone is a new source showing another's capture with filters of its own, so a camera can be shown faded in one scene and plain in another while being opened once. An item's own filters apply after its source's.
Edit > Undo (Ctrl+Z) and Redo (Ctrl+Shift+Z or Ctrl+Y) step through the history, and a drag undoes as one step; on macOS each shortcut uses Cmd in place of Ctrl. Copy (Ctrl+C) takes the selected item with the source it shows; Paste (Ctrl+V) adds it over whatever the Items panel shows, in this studio or another. Pasted where the same source exists under the same name, the item is linked to it; otherwise the source comes along, renamed if its name is taken. Copied filters paste onto the selected source or item, after its own. The clipboard holds text in the scene file's format.
A broadcast source subscribes through the same relay connections. It plays its 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.
A broadcast source, a guest's included, plays on its publisher's timestamps,
so its sound and picture stay in sync through network jitter and stalls. Its
audio plays as far behind what arrives as the network's jitter needs, measured
as it goes, the same delay moq play --delay auto holds, up to 2 s. Each
picture waits for the sound stamped with it, and the canvas shows the last one
meanwhile. A sound stall longer than that delay pauses the picture too and
starts both again together; a picture stall leaves the sound playing. A burst
after a stall skips ahead to the delay. Without audio, the
picture plays 100 ms behind. Sources shows how far behind each broadcast
plays.
Audio from every source is mixed whether or not its scene is live, and cuts
never change it. A collection has one or more audio scenes, each the sum
of the sounds it holds and of the audio scenes it holds: for example a "Mics"
audio scene holding every microphone, which both a "Stream" audio scene (Mics,
without music you may not broadcast) and a "Headphones" one (Mics and the
music) hold. A sound an audio scene reaches more than one way plays in it
once, and an audio scene never holds itself, even through others. Add,
rename, reorder, and remove audio scenes in the Scenes panel, where they are
tagged audio and listed after the scenes.
The Audio panel is a mixer. Each sound has a strip, with its gain, mute, delay, and meter, which apply in every audio scene holding it, and a send to each audio scene; each audio scene has a strip with its meter and a send to each other audio scene. A source added from the Sources or Devices panel, or pasted, is sent to the audio scene on program, so it is heard at once. On program picks the audio scene the broadcast carries, and switching it, or editing an audio scene, never drops or repeats audio. A recording holds the audio scene on program unless the Output panel picks another, such as one keeping music the stream leaves out, and each published scene carries the one on program unless the Scenes panel picks another beside its Publish box; switching either while it runs never drops or repeats audio either. The audio scene on program cannot be removed; removing one a recording or published scene picks returns it to the one on program.
Listen plays an audio scene 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 its audio scene back
into itself.
A source's delay holds it back in every audio scene, up to 2 s, so a microphone lines up with a camera that lags it. A broadcast's picture already follows its sound, so its delay holds the sound back from its own picture: use it only for a publisher whose sound leads their picture.
Each audio scene is mixed on the program clock, and each output reads its audio scene on its own, so an output that falls behind skips ahead in its own audio rather than holding up the mixing 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 audio scenes that hold it. 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
a restart of the studio. They are not encrypted with the desktop's keyring,
so a locked keyring never holds a page back, and anything that can read your
home directory can read them. One studio at a time can use the profile: a
second one's browser sources fail saying so. Each page renders in a process of its own, so
a page that crashes fails only its own source, with the reason, while the
stream carries on. A crash of the browser itself fails every browser source.
Either shows at once, since the browser's processes dump no core. On Linux,
pages run without Chromium's sandbox, as in OBS, so a page that exploits a
browser bug can do anything you can: add only pages you trust.
Reload in Sources restarts a page, failed or not. A page's dropdown
lists, such as a <select>'s, draw over it and take clicks under
Interact, but it opens no other windows (window.open and new-tab links
do nothing). Copy and Cut there put the page's selected text on the clipboard,
and nothing from a password field. Except for a page copied on the GPU through
an X display, the text goes as plain text that the page's own copy handlers
cannot change.
An overlay is a title, lower third, or other graphic written as HTML and CSS, drawn by the studio itself (through Blitz) rather than a browser, on every platform. Its CSS animations and transitions run on the program clock, so they land on the same frame in the program and in a recording or replay of it, whatever the studio's load. It runs no scripts and never touches the network. Its text is set in the bundled Noto Sans, regular, bold, or italic, unless it loads a face of its own, and never in the machine's fonts, so it looks the same on every machine.
An overlay's own images and fonts, such as a logo or a brand face, come from
data: URLs or from files in the scene file's directory, named relative to
it (url(brand/logo.png)), never from the web or anywhere else on disk. The
studio reads them all as the overlay opens, so its first frame has them, and
a file changed on disk changes nothing on air until the overlay opens again,
as when it is edited; a restart on program reuses what it read. Images are PNG or JPEG, at most 4096 pixels a side;
fonts are TrueType, OpenType, WOFF, or WOFF2, loaded with @font-face:
@font-face { font-family: Brand; src: url(brand/face.woff2); font-weight: bold }
.title { font-family: Brand; background: url(brand/logo.png) no-repeat }An overlay outside what overlays draw fails to start, naming the first element, attribute, rule, property, or asset it refused:
- Elements: text and the boxes around it (
div,span,p, headings, lists, tables, and the like),img, plusstyle,title, andmeta. Notscript,link,iframe,svg,canvas, or form controls. - Attributes:
id,class,style,lang,dir,title,hidden, table and list numbering ones,data-*andaria-*, and animg'ssrcandalt. - CSS rules: style rules, nested ones included,
@media,@keyframes, and@font-facewithfont-family,src,font-weight,font-style, andfont-display. Not@import, norlocal()orimage-set(). - CSS properties: box layout, flexbox, grid, positioning, margins and
padding, colors, opacity, gradients, borders and their radii,
box-shadow, outlines, transforms,clip-path, fonts and text layout, text decoration,content, and animations and transitions, by their usual shorthands too. Not yetfilter,backdrop-filter,mix-blend-mode,text-shadow,mask,text-overflow, or multi-column layout; the list grows as Blitz draws more.
An overlay's animation starts when the overlay does: when the studio opens
it, and again whenever it is edited. It also starts over each time a scene
showing it goes on program, as an item's keyframes do, so a lower third plays
its entrance at every cut, and when a scene showing it goes into preview while
program does not show it, so the preview shows the entrance too. Preview and
program share its timeline, so a preview never restarts one program shows.
Set restart = false (Restart on program, off, in the add form) for one
that runs on across cuts, such as a clock or a ticker. One that no preview or
program shows costs nothing until it is shown again, where its animation has
carried on unless it restarts.
Overlays publish no track of their own, so viewers see them only in the
program.
A media source plays a video or audio file, such as an intro clip or a music
bed, on the program clock, and its sound joins the audio scenes like any other
source's. It reads fragmented MP4, MKV or WebM, MPEG-TS, and FLV, as moq's
importers do, and fails to start on a codec this machine cannot decode. A
regular MP4 fails to start with a remux to fragmented MP4 that makes it readable:
ffmpeg -i in.mp4 -c copy -movflags frag_keyframe+empty_moov out.mp4. The
whole file is read into memory, compressed, when the source starts, so it
plays from its start at once and loops without reading the disk again; keep it
to clips and music, not hours of video.
[sources.intro]
kind = "media"
path = "intro.mkv" # relative to the scene file, like an image's
restart = true # play from the start each time it goes on program
[sources.music]
kind = "media"
path = "music.webm"
loop = true # start over at the endWithout loop, it plays once, then holds its last picture, silent, and reads
as ended in Sources and, while the program or a published scene shows or
plays it, in Health.
Play in Sources plays it from its start again, as does going on program
with restart, in a scene on air or the top layer, when it was not on program
before. Either clears its picture at once, so the end of the last play never
shows on program. Stop in Sources stops it at once: blank, silent, sound
already queued included, and reading as ended until played again. A file that
cannot be read or decoded fails, with the reason.
The source a stinger plays waits, blank and silent, for the
first stinger instead of playing as it opens. Its pictures and its sound each have a track of their own (see Source
tracks), its pictures on one timeline across plays. Each is
used apart, as a camera's and a microphone's are: the pictures while a scene on
air shows it, the sound while the audio scene on program plays it, and either while
read. So a music bed that no scene shows encodes its sound alone. A file with
no sound has no strip in the Audio panel.
Guests and phones join a room the studio owns on its relay, from a web page,
and their camera and screen appear as broadcast sources. The page plays the
show back to them, without their own voice. The room follows
moq-room: a guest
named alice publishes alice/camera.hang and alice/screen.hang under it.
Guests need a relay that verifies tokens, so run your own with
moq auth serve behind it, and a key that can
sign only for the room. The relay must be built from moq's main and accept
moq-lite-07 (--listen-version moq-lite-07-wip, beside the versions your
other clients use), the version that renews a guest's token without
reconnecting. Its wire is still a work in progress, so the join page offers
nothing else, and a guest cannot join through a relay without it.
moq auth generate --algorithm ES256 --out room.jwk --public-dir keys \
--root show/guests --publish '**' --subscribe '**'
moq-studio show.toml --connect 'https://relay.example.com/show?jwt=...' \
--room guests --room-key room.jwk --join-page https://join.example.com/--roomis a path under the--connectURL's, hereshow/guestson the relay. The studio's own token must let it subscribe there. A guest's browser dials the same relay, so--connectmust behttps://, orhttp://only to this machine or the local network.--room-keysigns each guest's token. The studio refuses a key whose scope reaches outside the room, so a stolen key cannot sign for the rest of the relay. Put its public half in the directorymoq auth serve --key-dirreads.--join-pageis where the join page (js/join,bun run --cwd js/join build) is hosted. Browsers open the camera only on anhttps://page orlocalhost, so the studio refuses any other. It defaults tohttps://join.studio.moq.dev/, which this repository deploys frommain.--guest-expiry(default4h, at least1m) is how long a link works, and each token the studio renews it with. While a guest is in the room, the studio signs them a fresh token every half of it, over a session of its own in the room that the room key signs for, and the join page presents it without reconnecting. A guest's token may read only their own renewals and return feed, at.renew/<name>/<generation>and.return/<name>/<generation>in the room, and the program at.program, but anything else the relay lets subscribe there can read every guest's, so grant that only to the studio. A guest who leaves needs a link, or a renewed token, that has not expired to come back. The Guests panel says when renewal is failing.- Upgrading from a studio without renewal: generate a new room key, drop
the old one's public half from
--key-dir, then restart the relay (or wait out the old links'--guest-expiry) before starting the upgraded studio. Links from before let their holder subscribe to the whole room, renewals included, so they must die with the old key, and a relay keeps a session it already admitted until its token expires. - With
--failover, a link still names only the--connectrelay: the join page dials one relay, and a guest's token is rooted at the room there, so guests drop while that relay is down. The studio follows the room through both relays, like any broadcast source, so a guest the failover routes at the same path, as a relay in the same cluster does, keeps reaching it. Anyone the failover lets publish in the room joins as a guest, so guard the room there as on the--connectrelay.
Invite in the Guests panel makes a link for one guest name and shows it as
a QR code; Copy link puts it on the clipboard instead. The guest's token is
in the link's fragment, which the browser never sends to a server, and lets
them publish only under their own name, so they cannot appear as someone else,
and read only their own renewals, their return feed, and the program.
Treat the link like a password: anyone holding it can join as that guest
before it expires, and is then renewed for as long as they stay, until you
remove them. A guest's broadcasts become sources named after them (alice,
alice screen) when they first appear, in no scene or audio scene: place
them from the Items panel and send them to the audio scene on program in Audio
to put them on air. That includes a
guest invited before a restart whose link still works. A guest's source is
saved to the scene file but is not your edit: it stays out of History, so undo
never removes it and a guest joining never loses what you could redo. A guest
who reconnects after you removed their source comes back under a new name
(alice 2), since undoing the removal brings back the old one.
Remove from scenes is your edit, so it undoes: it takes a guest, any clone
of their sources, and any mask reading them out of every scene and the top
layer and mutes them. A signed token cannot be taken back, so it also stops
renewing theirs and ends their return feed, and they are disconnected within
--guest-expiry. That
holds across a restart, whichever scene file you open: removed guests are
kept beside the room key, as room.guests.toml for room.jwk, so its
directory must be writable. Undo puts them back in the scenes but not on
renewal; New link lets them back in and renews them again, under a new
generation, so a link from before the removal cannot read their renewals.
While a guest is in the room, the join page plays their return feed: the
program's video, and the audio scene on program without that guest's microphone (a
mix-minus), so they follow the show without hearing themselves back. Ask
guests to wear headphones, so the show does not reach their microphone. The
studio publishes it over its session in the room, never with its own
credentials: the program once at .program, and each guest's feed at
.return/<name>/<generation>. The video is the program's own two sizes,
and a guest's player picks one for their link; this session uploads the size
a guest watches a second time, and a guest watching the half size makes it
encode as any viewer does. Each guest in the room costs one 64 kbps Opus
encode of their mix-minus. A guest's
screen share stays in their feed, since only their camera's microphone is
taken out.
The collection is TOML, so it diffs and reviews like code. It names devices, files, and broadcast paths, never a relay URL or token. Check its image paths and device IDs before sharing it, since they can name your home directory or hardware. A file from another format version fails to load rather than being guessed at.
version = 1
[canvas]
width = 1920
height = 1080
framerate = 30
[transition]
kind = "slide" # fade, slide, wipe, move, or stinger
duration = "300ms" # all but a stinger
direction = "left" # slide and wipe: left, right, up, or down
# A stinger names a media source and its cut point instead:
# kind = "stinger"
# source = "sting"
# cut = "500ms"
[sources.camera]
kind = "camera"
[sources.logo]
kind = "image"
path = "logo.png" # relative to the scene file
[sources.guest]
kind = "broadcast"
path = "room/guest"
[sources.alerts]
kind = "browser"
url = "https://example.com/alerts"
width = 800 # the page's own size, whatever its items stretch it to
height = 600
[sources.name]
kind = "overlay"
width = 1920 # the overlay's own size, like a page's
height = 1080
restart = true # start over each time it goes on program, the default
html = '''
<style>
.third { position: absolute; left: 80px; bottom: 120px; padding: 16px 32px;
background: linear-gradient(90deg, #1e3a8a, #2563eb); border-radius: 12px;
font: bold 48px sans-serif; color: white; animation: in 400ms ease-out both; }
@keyframes in { from { transform: translateX(-40px); opacity: 0 } }
</style>
<div class="third">Ada Lovelace</div>
'''
[sources.music]
kind = "media"
path = "music.webm"
loop = true
[audio.guest]
gain = 0.5
delay = "80ms"
[sources.camera-faded]
kind = "clone"
source = "camera"
[sources.main-view]
kind = "scene"
scene = "Main" # draws the Main scene as one picture
[sources.on-air]
kind = "program" # whatever the program shows
[video.camera-faded]
filters = [{ kind = "opacity", amount = 0.5 }]
[sources.grade]
kind = "lut"
path = "teal-orange.cube"
[video.camera]
background = "remove" # cut the person out, leaving the rest transparent
crop = { left = 0.1, right = 0.1 } # trim 10% of the picture off each side
filters = [
{ kind = "chroma_key", color = "#00ff00", similarity = 0.4, smoothness = 0.08, spill = 0.1 },
{ kind = "lut", source = "grade", amount = 0.8 },
]
[[scenes]]
name = "Main"
items = [
{ source = "camera", rect = { x = 0, y = 0, width = 1920, height = 1080 } },
{ source = "guest", rect = { x = 1280, y = 720, width = 640, height = 360 }, filters = [{ kind = "drop_shadow", x = 8, y = 8, blur = 12, color = "#00000080" }] },
{ source = "logo", rect = { x = 0, y = 0, width = 1920, height = 1080 }, blend = "multiply", filters = [{ kind = "opacity", amount = 0.3 }] },
]
[[scenes]]
name = "Break"
items = [
{ source = "camera-faded", rect = { x = 0, y = 0, width = 1920, height = 1080 } },
# Flies in from the right, blurred, over half a second, landing where it rests.
{ source = "guest", rect = { x = 1280, y = 720, width = 640, height = 360 }, keyframes = [
{ at = "0s", x = 700, filters = [{ kind = "blur", radius = 12 }] },
{ at = "500ms", ease = "out", filters = [{ kind = "blur", radius = 0 }] },
] },
{ source = "main-view", rect = { x = 80, y = 80, width = 640, height = 360 } },
]
[[scenes]]
name = "Portrait"
size = { width = 1080, height = 1920 } # its own size; the program's without one
published = true # live at <--broadcast>/Portrait
recorded = true # recordings hold this scene rather than the program
items = [
{ source = "on-air", rect = { x = 0, y = 0, width = 1080, height = 1920 }, crop = { left = 0.34, right = 0.34 } },
{ source = "logo", rect = { x = 780, y = 40, width = 280, height = 120 } },
]
[[audio_scenes]]
name = "Voices"
sounds = ["guest"]
[[audio_scenes]]
name = "Stream"
program = true # the audio scene the broadcast carries
scenes = ["Voices"]
[[audio_scenes]]
name = "Headphones"
recorded = true # recordings hold it rather than the one on program
scenes = ["Voices"]
sounds = ["music"]
[[top]]
source = "logo"
rect = { x = 1600, y = 40, width = 280, height = 120 }Source kinds are camera, display, and microphone (each with an optional
id, the default device without one), window (with an id), system_audio
(macOS), image (PNG or JPEG), media (a video or audio file; see
Media files), color (#rrggbb or #rrggbbaa),
broadcast, browser (a url, and a width and height up to 4096),
overlay (its html, a width and height up to 4096, and whether it restarts, true by default), clone (with the source it shows), lut (a 3D Adobe or Resolve .cube
file, which no item places, but which grades what a lut filter names it in),
replay, where the Replay panel plays replays, scene (with the scene it
draws), and program (see Scene sources). The Devices panel fills in
ids. [[top]] items make up the top layer.
A scene's size gives it a width and height of its own; without one it is
the program's. published = true puts it live as its own broadcast (see
Published scenes), so its name is one path segment, and
recorded = true has recordings hold it rather than the program, on one scene
at most.
A scene's audio names the audio scene it carries while published, the
one on program without it.
[[audio_scenes]] lists the audio scenes, in the order the Scenes panel lists
them, each with its name, the sources whose sounds it holds, and the other
audio scenes it holds. Exactly one is on program = true, and
recorded = true has recordings hold one rather than the one on program.
Without [[audio_scenes]], the collection has one audio scene, Program, on
program and holding nothing. [audio.<source>] sets a source's linear gain,
muted, and delay, in every audio scene holding it.
[video.<source>] sets a source's background, crop, and filters,
wherever it is shown, and an item's crop, filters, and blend its own.
background = "remove" cuts the person out of a camera, display, window,
image, media, broadcast, browser, or replay source's picture before its crop
and filters, leaving the rest transparent. A segmentation model finds them in
each picture on the CPU, so each picture shows only once its cut-out is
ready, a frame or more late, and a model that falls behind skips pictures
rather than queue them. It is MediaPipe's selfie segmentation model, made for
one or two people near the camera. A clone shows its source's cut-out, so it
cannot remove a background of its own. A crop trims a fraction
(0 to 1) of the picture off its left, top, right, and bottom, opposite
sides together trimming less than the whole, and stretches the rest over the
item; an item's crop trims what its source's
leaves. Filters apply in order, the source's then the item's, with lengths in
canvas pixels:
| Filter | Settings |
|---|---|
opacity, grayscale, sepia, invert |
amount, 0 to 1, as in CSS |
brightness, contrast, saturate |
amount, 0 or more, 1 leaving the picture as it is, as in CSS |
hue_rotate |
degrees, as in CSS |
blur |
radius, the standard deviation; the edges fade out, as in CSS |
drop_shadow |
x, y, blur (the standard deviation), and color, as in CSS |
chroma_key |
color keyed out, and similarity, smoothness, and spill from 0 to 1 |
luma_key |
min and max luma kept, min at most max, and smoothness, all from 0 to 1 |
lut |
the lut source to grade through, and amount from 0 to 1 |
mask |
a source whose picture, stretched over the item, keeps what it covers: by its alpha, or mode = "luminance" |
An item's blend mixes it with what is under it by a CSS mix-blend-mode:
multiply, screen, overlay, darken, lighten, color_dodge,
color_burn, hard_light, soft_light, difference, exclusion, hue,
saturation, color, or luminosity. Removing a source removes the filters
that name it.
An item's keyframes, in order of time, each have an at, how long after the
scene goes on, and optionally an ease into it from the one before (linear,
in, out, or in_out, the default), x, y, width, and height added
to the item's rect, a crop of what the item's crop leaves, and filters
applied after the item's own.
The studio writes the file a second after the last change, so a burst such as a slider drag is one write, and once more on quit. Edits that never pause for a second are written five seconds after the first, so a crash loses at most the last five seconds of edits. Each write replaces the whole file at once, never leaving half of it, and a collection that has not changed is not rewritten, so a hand-written file keeps its comments and layout until the next edit in the studio. A failed write shows in the status bar and is retried every second; until one succeeds, a crash loses every edit since the last write.
The file can be edited by hand, or by another studio, while the studio runs. The studio looks at it every second and before each write. With no edits of its own unsaved, it reloads the change, as File > Reload does, which replaces even unsaved edits. If both sides changed, or the change cannot be loaded, the studio saves nothing until you pick Keep the studio's, which writes its version over the file, or Take the file's; quitting before you pick saves nothing. A file deleted while the studio runs is written again. One overwrite remains possible: a change saved in the instant between the studio's last look and its write replacing the file.
The last 100 edits are written the same way beside the scene file, as
show.history.toml for show.toml, after the scene file, and opening the
scene file restores them, so undo reaches back past a restart. An undo or redo
is saved like any other edit. An edit that no longer matches the scene file,
such as after editing it by hand, is dropped with every older one, and the
status bar says how many. A history file that cannot be read stops the
studio, naming the file; delete it to start a new history. The history holds
whatever the scene file held, including removed sources, so check it too
before sharing.
The program publishes as two H.264 renditions, at the canvas size and at half
of it, at the canvas frame rate, with the audio scene on program as Opus stereo audio,
under the --broadcast path. A canvas under 360 lines has no half size. Each
published scene publishes the same way at <--broadcast>/<scene name>, at
its own size and half of it, its tracks sharing the connection's bandwidth
with the program's, and its own encode of the audio scene it carries.
Timestamps come from the studio's own clock, so a source that stalls repeats
its last picture instead of holding up the stream.
Each rendition follows its share of the bandwidth the first relay's connection estimates: a closing uplink lowers its bitrate at once, and an opening one raises it back over a few seconds, up to 0.07 bits per pixel (about 4.4 Mb/s for 1080p at 30 fps). The half size is funded first. When the share left for the canvas size cannot fund it over the half size, the catalog lists it as disabled and viewers move to the half size until the uplink recovers. A viewer joining a track whose newest group has grown heavy asks for a keyframe, and the encoder cuts one, so later viewers start near it.
A new canvas size or frame rate, from the Output panel, an undo, or a reload,
takes effect live. A track never changes what it carries, so the old
renditions' tracks end and new ones are listed in their place in a catalog
update, on the same timeline, and viewers switch to them; the video pauses
while the new encoders open. Items keep their place in canvas pixels. A canvas
the encoder cannot take stops the video, saying why, until the canvas changes
again. At startup, it stops the studio when live; without --connect, the
studio starts without video.
The canvas size encodes whether or not anyone watches, so a viewer who arrives
late can fetch the recent past. The half size encodes only while someone
watches it. --on-demand copies and encodes the canvas size only while someone
watches it too, which saves that work but leaves nothing from before the first
viewer; audio encodes either way. A recording has an encoder of its own (see
Recording). A hardware encoder that reads
the GPU (NVENC on Linux, VideoToolbox on macOS, Media Foundation on Windows)
takes each frame there when it can read the GPU the studio draws on. When it
cannot, such as NVENC while the studio draws on a laptop's integrated GPU, the
studio passes over it for an encoder that reads each frame back to the CPU
(the opt-in vaapi or openh264 features), which the Output panel counts.
Frames the encoder cannot keep up with are dropped rather than delaying the
stream.
Each camera, display, window, image, media file, color, microphone, and system
audio also has a track of its own, so the studio can replay or record it on its
own. These tracks stay in the studio: the program is all it publishes to a relay. They are listed in a local
catalog under the dev.moq.studio section's sources, by source name, each
as a hang catalog of its own on the program's clock.
A captured source encodes its track as H.264 only while it is used: while it is shown on program or in a published or recorded scene being drawn, so a replay of it has its recent past, or while a recording reads its track. Otherwise it holds no encoder. A display or window that goes into use while its screen stands still, and so delivers nothing new, restarts its capture, so its track starts with the picture as it is. A capture that changes size or color space moves to a new track, which its catalog entry follows. Images and colors keep their picture once, as the image file and as a one-pixel PNG. Removing or editing a source ends its track. A track carries the plain capture, before any filter, and a clone has none, since it shows its source's capture. A removed background stays in the track, since H.264 carries no transparency, so a replay of the source shows it unless the replay source removes it too. A browser source or an overlay has no track of its own, and a broadcast source is already its publisher's.
A microphone, system audio, or media file's sound encodes its track as Opus, from the same sound the audio scenes hear, only while it is used: while the audio scene on program plays it (unmuted and held by it), so a replay of it has its recent past, or while a recording reads its track. The track carries the sound as captured, before its gain, mute, and delay, which stay the audio scenes'. A buffer the capture lost, or a stretch the source was out of use, is a gap on its track. Opus encodes on the CPU, about 1% of one Apple M4 core for a stereo source and half that for mono, and holds no encode session.
Every encoder holds an encode session, and consumer GPUs allow only a few hardware sessions at once: 8 on a GeForce card, while a base Apple M4 has one encode engine for them all. The Output panel counts the sessions open:
| Encoder | Sessions |
|---|---|
| The program's canvas size, live | 1, or none without --connect |
| Its half size | 1 while someone watches it |
| Each published scene's own size and half size | as the program's |
| The recording's own size | 1 while recording |
| Each captured source or media file | 1 while used, as above |
So a live scene showing a camera, a display, and a window holds 4 sessions, 5
with the half size watched, and 6 while recording too. A published portrait
scene drawing the program beside it adds 1 more, 2 with its half size watched,
plus 1 for each captured source it shows that program does not. When the program
encodes on hardware, so do the sources, and a
session the GPU refuses fails that source's track with the encoder's reason in
the Sources panel rather than falling back to software. The source keeps
drawing in the studio's own scenes. The program opens its session before any
source, even with --on-demand, so the sources past the cap fail rather than
the program; so does each scene published in the scene file at startup. A
scene published later, past the cap, publishes without video and says why in
the Output panel. When the program encodes in software, a source takes hardware
where it can and software otherwise.
Start recording in the Output panel saves the program at the canvas size,
or the scene picked beside it at its own size,
to <scene file name> <UTC start time>.mp4 in the --recordings directory,
live or not. The recording has an encoder of its own, opened only while it
records, at full quality whatever the uplink grants the live stream, as OBS's
advanced output mode records apart from streaming. It costs one more encode
session and one more copy of each program frame while recording. With
each source too, every camera, display, window, microphone, and system
audio records its own track into a file of its own beside it,
<scene file name> <UTC start time> <source>.mp4, holding the capture before
any filter, or the sound before its gain and mute; a source being recorded
encodes whether or not it is on program.
Each broadcast source, a guest's included, records its broadcast as it arrives
from the relay, its largest video and its audio copied without a re-encode, on
its publisher's timestamps, into a file named after it while it is live. A
guest who leaves finishes their file, and one who joins again goes on in a new
one, numbered after the first, since each join is a new broadcast with a
timeline of its own. A source added while recording
starts its file then, and one removed finishes its file then. A new canvas
size or frame rate, a recorded scene of another size, or a capture that
changes size or color space, moves the video to a new track, so the recording
or that source goes on in a new file,
numbered after the first, since players show only one video track. A file
starts with up to 2 s from before it opened, so a recording starts a little
early, and a track that goes on into the next file, such as the program
audio, repeats up to 2 s of the last. The
program and local sources are read from the studio's own broadcasts, never what
the relay serves, so a recording starts and stops without touching the live
stream. The audio scene it holds is
encoded a second time for it while recording: the one on program, so the file
holds what viewers hear, or the one the Output panel picks, which viewers never
can. Without --connect, only the
recording's encoder encodes the program.
Each file plays while it grows, so a crash leaves a playable fragmented MP4, and becomes a regular MP4 when the recording stops or the studio quits. Quitting first lets the recording's encoders publish what they still hold, and the program's file keeps it. A file nothing reached before the recording stopped is removed.
While a file is written, a marker <name>.mp4.unfinished sits beside it. A
crash, a power loss, or a write that failed leaves the marker, and the next
launch repairs that file without asking: it writes <name>.repaired.mp4
beside it, numbered if a file already has that name, a regular MP4 of
everything the file holds up to its last complete fragment, leaves the
original untouched, and removes the marker, so the file is repaired
only once. The repair runs in the background and appears at the top of the
Health panel and in the log. A file another studio is still recording into the
same directory is left alone.
Sources make the opposite trip. A frame already on the GPU is drawn where it
lies: PipeWire camera and VA-API decode DMA-BUFs on Linux, given a Vulkan
driver that imports them, and NV12 capture and VideoToolbox decode surfaces on
macOS. A browser source's page is copied on the GPU from the shared texture CEF
paints, given the same Vulkan DMA-BUF import and a display to paint through:
the Wayland one when $WAYLAND_DISPLAY is set, needing no XWayland, or else
the X one in $DISPLAY, when the X server runs on the studio's GPU, since
CEF paints on the X server's there. Every other source, every source on Windows or on a
GPU that cannot import, and every still image delivers its frames from the
CPU, which the compositor uploads. A browser source takes that path with no
display at all too, and there renders its page in software, WebGL included, so
a WebGL page runs slower.
The Hotkeys panel binds a key to Cut, Transition, Replay, Save clip, starting or stopping a recording, and putting each scene in preview. Press Bind, then the key with any modifiers; Escape, or leaving the window or the panel, cancels. Every action starts unbound, so no key a game uses is taken unasked, and a key bound to one action is refused for another until it is cleared there. Replay and Save clip use the Replay panel's picks, and a recording the Output panel's.
The key pressed is the key that fires, wherever it sits, on any keyboard layout and after a layout change: the panel names it as the layout printed it when it was bound. Keypad keys bind apart from the keys above the letters, so keypad 1 and 1 can do different things; the panel calls them Keypad 1 and 1.
Bound keys fire even while another app, such as a game in full screen, has focus, and the app does not see them, so bind keys you do not type or play with, such as a function key, a keypad key, or one with modifiers:
- macOS and Windows register them with the system, which needs no permission. A key another app already registered is refused, saying so, as is a key the system has no name for. On macOS 15 and later, the system refuses a key whose only modifiers are Option, or Option and Shift. Windows hears a keypad key only with NumLock on, cannot tell keypad Enter from Enter so refuses it, and fires a binding on Enter for both.
- Linux on X11 grabs them from the X server by where they sit, so any key binds, such as ö on a German layout or a key with no name, and fires whatever NumLock and CapsLock are.
- Linux on Wayland, where no app hears keys meant for another, asks the desktop through its GlobalShortcuts portal (KDE Plasma, and GNOME 48 and later). The desktop shows its own dialog, may bind other keys than those asked, and lists them in its shortcut settings; the panel shows what it bound. Without the portal, or if the dialog is declined, bound keys work only while the studio has focus, and the panel says so.
The bindings, the snap and grid settings, and the output Listen plays on
are kept per user in the app's settings, ~/Library/Application Support/moq-studio on macOS, $XDG_DATA_HOME/moq-studio (or
~/.local/share/moq-studio) on Linux, and %APPDATA%\moq-studio\data on
Windows, apart from the scene file, so a scene file moves between machines
without one operator's keys. A key bound to a scene follows it when renamed in
the studio.
Add a replay source and place it like any other, such as full screen in a
scene of its own. In the Replay panel, pick the program or a source, a length
up to 30 seconds, and a speed of 100%, 50%, or 25%, then Replay plays that
many seconds up to the click in the replay source, which is transparent before
and after. Cut to its scene to put it on program. Sound replays only at
100%, through the replay source's strip in the Audio panel, so a replay of a
microphone or system audio, which has only sound, plays only at 100%. A new
replay cuts
off the one playing, and a stretch the source was offline or paused replays
as the gap it was.
Save clip writes the same seconds, in real time, beside the scene file as a
regular MP4: show-clip-1.mp4 for show.toml, then show-clip-2.mp4, and so
on. Nothing is re-encoded, so a clip starts at the keyframe before those
seconds, up to one keyframe interval earlier. It shows nothing after them, so
video with B-frames can end early: the pictures shown before the end that refer
to one shown after it are left out, along with everything decoded after that
one. That is usually a fraction of a second, but it can reach back as far as
the keyframe for a slow or deeply reordered source. The panel says where once
it is written, shortly after the click.
Nothing records in the background for this. The program's tracks keep their
last 30 seconds for late viewers anyway, and each source's track while it is on
program (a picture shown in a scene on air, or a sound played by the audio
scene on program) or recorded, and a replay copies from those, so a replay
goes back at most that far. A replay of the program needs --connect; a local
source keeps its track without a relay. With --on-demand, the program has
no past from before someone watched it, and a source has none from before it
was last on program or recorded; a replay of nothing is refused, saying why. A
broadcast source replays from what the studio received of it, and only over a
relay connection whose protocol version can address a track by time; an older
one refuses with the reason in the panel. An image, a color, or a web page has
no past to replay: a page publishes no track of its own, so replay the program
to show it. A replay source on program encodes each replay as its own track
while it plays, like a camera.
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 or a published
scene draws, or a microphone or system audio that the audio scene on program
or one a published scene carries plays: unmuted and held by it), a source
one of them shows or plays failed or, as a media file that does not loop,
played to its end, a browser source one of them shows is uploaded from memory
rather than copied on the GPU, 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. A problem with a published scene's broadcast, or a source
only it shows or plays, names the scene.
Below that it shows:
- Relays: each relay's state, and the protocol its session negotiated.
- Uplink: the round trip to the first connected relay, the bandwidth the connection estimates it can carry, the media every broadcast's tracks write, and the share lost and resent. Loss and headroom are graded only while someone watches, since the relay pulls nothing otherwise. Source tracks never reach the relay, so they are left out of the media.
Then, for the program's broadcast and each published scene's, each with its own viewers:
- Watching: the sessions subscribed to it, from the relays'
.statsbroadcasts. A relay shares those only with a connection allowed to read them, so most show "unknown". In a cluster, a relay forwarding the stream to another counts as one session. Two relays whose stats use the same node name publish them at the same path, so only one of them counts: the first relay's while it has them. - Tracks: each track's share of the connection's bandwidth (Granted, or "off" for a rendition the share cannot fund), its bitrate and frame rate, and, from viewers that report, the lowest bitrate any received, its worst grade, and the latency from the program clock to the slowest viewer. A published scene lists the program's audio, graded where the program writes it. Latency compares the two hosts' wall clocks, so keep both synced.
- Viewers: each viewer that reports, with its round trip, latency, and playout delay. The studio reads 64 at most per broadcast, a sample the grades rest on: it counts the rest, and a reader one frees goes to the next viewer to arrive.
Last, Sources lists each source's own tracks, grouped by source; a still image or color carries no media to grade.
Each catalog asks viewers to report beneath its broadcast, under
<broadcast>/viewers/ or <broadcast>/<scene>/viewers/: a viewer publishes
<broadcast>/viewers/<name>.echo, as moq export --echo <name> does. The relay decides who may publish there, as for any broadcast.
The Virtual camera toggle in the Output panel shows the program as a camera that video-call and conferencing apps can pick, at the canvas size and frame rate. Each published scene has a toggle of its own beneath it in the panel, showing that scene at its own size as a camera of its own, so an app can pick the program or any published scene. A camera stays on until you turn it off or quit; it is not saved in the scene file. A scene's camera turns off when you stop publishing or rename the scene. A picture is copied for a camera only while an app reads it, and a scene's camera has the scene drawn only then. Changing the size or frame rate of what a camera shows reopens it at the new one, so an app reading it opens it again. The Devices panel leaves the studio's cameras out while they are on, so one cannot be added as a source of the picture it shows.
- Linux: a PipeWire camera named "MoQ Studio" for the program, and
"MoQ Studio: " for a scene (needs the
pipewirefeature), which browsers and other PipeWire-aware apps list beside your other cameras. Without PipeWire, each camera writes to the first v4l2loopback device that takes its size and that no other camera of the studio holds, for apps that open/dev/video*directly. The studio never loads that module, since it needs root. Load a device for each camera you turn on, such as two:sudo modprobe v4l2loopback devices=2 exclusive_caps=1,1 card_label="MoQ Studio","MoQ Studio 2". - macOS and Windows: not available yet.
Use the Nix dev shell (direnv allow, or nix develop --command ...) so
tooling matches CI, then:
just check # Lint everything, including the quest tree
just test # Run the tests
just ci # Both, as CI runs them
just fix # Auto-fix formatting
just join # Serve the join page locallyrs/moq-scene is the UI-free compositor, rs/moq-studio the app, and
js/join the join page, built on @moq/room, @moq/publish, and
@moq/watch. Like the
Rust crates, those come from the moq submodule rather than npm, so
bun install needs it checked out, and Nix to build @moq/libav. See
AGENTS.md and CONTRIBUTING.md, and the
quest tree for planned work.
Licensed under either MIT or Apache-2.0, at your option.