Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ee4e81b
chore(quest): claim quest/m0/guests.md
kixelated Nov 2, 2026
35ef056
feat(studio): invite guests into a moq-room and add them as sources
kixelated Nov 2, 2026
4ede99c
feat(join): the page a guest opens to join the studio's room
kixelated Nov 2, 2026
067a074
docs: guests in the README; finish quest/m0/guests.md
kixelated Nov 2, 2026
4b8a03b
fix(guests): show capture failures on the join page; refuse a room a …
kixelated Nov 2, 2026
243e97c
Merge remote-tracking branch 'origin/main' into quest/m0/guests
kixelated Nov 3, 2026
05cbae9
feat(join): deploy the join page to GitHub Pages and default --join-p…
kixelated Nov 3, 2026
197511d
fix(guests): track every guest, muted until placed; refuse hidden names
kixelated Nov 3, 2026
bbb0395
Merge remote-tracking branch 'origin/main' into quest/m0/guests
kixelated Nov 3, 2026
954f655
fix(guests): admit only a one-segment guest's canonical camera and sc…
kixelated Nov 3, 2026
9ab74e4
Merge remote-tracking branch 'origin/main' into quest/m0/guests
kixelated Nov 3, 2026
90ac6d2
docs(guests): note a joining guest autosaves; join-page-live protects…
kixelated Nov 3, 2026
5e79f02
Merge remote-tracking branch 'origin/main' into quest/m0/guests
kixelated Nov 3, 2026
b67ec3b
fix(join): survive denied storage, clear old errors, say when no scre…
kixelated Nov 3, 2026
433f78d
Merge remote-tracking branch 'origin/main' into guests-merge
kixelated Nov 3, 2026
1fa94a2
feat(scene): a guest joining stays out of the history
kixelated Nov 3, 2026
f6a74f4
Merge remote-tracking branch 'origin/main' into guests-merge
kixelated Nov 3, 2026
99c28ce
Merge remote-tracking branch 'origin/main' into quest/m0/guests
kixelated Nov 3, 2026
dd4ee30
Merge remote-tracking branch 'origin/main' into guests-merge
kixelated Nov 3, 2026
19f745d
Merge remote-tracking branch 'origin/main' into HEAD
kixelated Nov 3, 2026
d19c74f
fix(studio): follow guests only once the engine has started
kixelated Nov 3, 2026
7bb2a14
Merge remote-tracking branch 'origin/main' into quest/m0/guests
kixelated Nov 3, 2026
b5a5bf9
Merge origin/main (#19, failover relay) into quest/m0/guests
kixelated Nov 4, 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
64 changes: 64 additions & 0 deletions .github/workflows/join.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Join page

# Deploys the join page (js/join) to GitHub Pages from main, at the domain
# `--join-page` defaults to. Check builds it on every ready PR.

permissions:
contents: read

on:
push:
branches: [main]
paths:
- "js/join/**"
- "package.json"
- "bun.lock"
- "flake.lock"
- ".github/workflows/join.yml"
workflow_dispatch:

# One deploy at a time, and never cancel one halfway.
concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
name: Build
if: github.repository_owner == 'moq-dev'
runs-on: ubuntu-24.04-arm
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: ./.github/actions/nix

- name: Build
run: |
nix develop --command bun install --frozen-lockfile
nix develop --command bun run --cwd js/join build

- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: js/join/dist

deploy:
name: Deploy
needs: build
runs-on: ubuntu-24.04-arm
timeout-minutes: 10
# The only job with write access. `github-pages` deploys only from main.
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
permissions:
pages: write
id-token: write

steps:
- id: deploy
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ result

# direnv
.direnv

# The join page's dependencies and build.
node_modules
dist
20 changes: 20 additions & 0 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ allow_attributes_without_reason = "warn"
[workspace.dependencies]
hang = { path = "moq/rs/hang" }
kio = { path = "moq/rs/kio" }
moq-auth = { path = "moq/rs/moq-auth", default-features = false }
moq-audio = { path = "moq/rs/moq-audio", default-features = false }
moq-auth = { path = "moq/rs/moq-auth" }
moq-json = { path = "moq/rs/moq-json" }
moq-mux = { path = "moq/rs/moq-mux" }
moq-net = { path = "moq/rs/moq-net" }
moq-relay = { path = "moq/rs/moq-relay", default-features = false }
moq-room = { path = "moq/rs/moq-room" }
moq-scene = { path = "rs/moq-scene" }
moq-stats = { path = "moq/rs/moq-stats" }
moq-tokio = { path = "moq/rs/moq-tokio", default-features = false }
Expand Down
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Every panel is a tab you can drag, split, or stack.
| Items | Place, show, hide, and order what the preview scene or the top layer draws, back to front. 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. |
| 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. |
| Output | See the broadcast path, whether the program is encoding, the canvas, and the encoder's work. |
Expand Down Expand Up @@ -134,6 +135,63 @@ 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.

## Guests

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 room follows
[moq-room](https://github.com/moq-dev/moq/tree/main/rs/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`](https://doc.moq.dev/bin/cli) behind it, and a key that can
sign only for the room:

```bash
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/
```

- `--room` is a path under the `--connect` URL's, here `show/guests` on the
relay. The studio's own token must let it subscribe there. A guest's browser
dials the same relay, so `--connect` must be `https://`, or `http://` only to
this machine or the local network.
- `--room-key` signs 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 directory `moq auth serve --key-dir` reads.
- `--join-page` is where the join page (`js/join`, `bun run --cwd js/join
build`) is hosted. Browsers open the camera only on an `https://` page or
`localhost`, so the studio refuses any other. It defaults to `https://join.studio.moq.dev/`, which this
repository deploys from `main`, but that domain is not live yet, so for now
host the page yourself and pass its URL.
- `--guest-expiry` (default `4h`) is how long a link works. The relay
disconnects a guest when their link expires, so send a new one before then.
- With `--failover`, a link still names only the `--connect` relay: 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 `--connect` relay.

**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.
Treat the link like a password: anyone holding it can join as that guest until
it expires. A guest's broadcasts become sources named after them (`alice`,
`alice screen`) when they first appear, muted and in no scene: place them from
the Items panel and unmute them 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 they stay
connected until it expires.

## Scene file

The collection is TOML, so it diffs and reviews like code. It names devices,
Expand Down Expand Up @@ -374,9 +432,11 @@ 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 locally
```

`rs/moq-scene` is the UI-free compositor and `rs/moq-studio` the app. See
`rs/moq-scene` is the UI-free compositor, `rs/moq-studio` the app, and
`js/join` the join page, built on `@moq/room` and `@moq/publish`. See
[AGENTS.md](AGENTS.md) and [CONTRIBUTING.md](CONTRIBUTING.md), and the
[quest tree](quest/README.md) for planned work.

Expand Down
51 changes: 51 additions & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// moq's settings, so the join page reads like moq's own packages.
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
// Only the JavaScript. `!!moq` keeps the submodule out of the scan, whose
// own root config biome would otherwise refuse.
"includes": ["js/**", "package.json", "biome.jsonc", "!!moq"]
},
"formatter": {
"lineWidth": 120,
"indentStyle": "tab",
"indentWidth": 4,
"lineEnding": "lf"
},
"linter": {
"rules": {
"suspicious": {
"noImportCycles": {
"level": "error",
"options": {
"ignoreTypes": true
}
}
},
"nursery": {
"noFloatingPromises": "error",
"noMisusedPromises": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"yaml": {
"formatter": {
"enabled": false
}
},
"markdown": {
"formatter": {
"enabled": false
}
}
}
Loading