diff --git a/quest/README.md b/quest/README.md index ccf43bd7..b2cfd2b9 100644 --- a/quest/README.md +++ b/quest/README.md @@ -19,6 +19,6 @@ there first. A quest here that needs it waits on a pin quest, which ## Required - [m0: the studio](/quest/m0/README.md) - the native studio for creators who rely on OBS today -- [m1: after the studio ships](/quest/m1/README.md) - studio features held past m0; empty now +- [m1: after the studio ships](/quest/m1/README.md) - what the redesigned studio gains, and the way in from OBS - [m2: later](/quest/m2/README.md) - work with no consumer yet - [m3: someday](/quest/m3/README.md) - work no customer asks for, kept so its plans are not lost diff --git a/quest/m0/README.md b/quest/m0/README.md index c628bfc1..553eee4e 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -86,6 +86,13 @@ replanned 2026-11-08 around independently published sources layered over the program). Overlays and animation render into the program here; that epic's layout track carries only the layers of independently published sources. +Changed 2026-11-09 with the maintainer (/quest-plan): the studio gets one +designed layout, a live picture with pages around it, in place of sixteen +dockable panels; see [Broadcaster UX](/quest/m0/ux/README.md). Docking no +longer argues for a toolkit, so that epic's spike decides it again between +egui and Slint, the two that draw the compositor's textures with no copy on +the studio's own device. + Dropped from the first scoping round: relay fan-out ("encode once, the relay restreams") and its upload savings. Direct push with shared encoders matches OBS multistream, and the hosted version is product work outside this @@ -104,6 +111,8 @@ repository. - [Browser sources on macOS and Windows](/quest/m0/browser-source/README.md) - the out-of-process CEF pages Linux has, with shared GPU textures on the other desktops - [Virtual camera](/quest/m0/vcam/README.md) - the program or a published scene exposed as a system camera on Linux, Windows, and macOS - [The Apple signing identity is available](/quest/m0/signing.md) - the Apple credentials the macOS app and its camera need; Windows ships unsigned +- [Broadcaster UX](/quest/m0/ux/README.md) - one live picture with pages around it, in place of the dock of panels and the launch flags +- [moq-video's capture listing is pinned](/quest/m0/pin-capture-list.md) - icons and live pictures of windows and devices, for the Add page - [Packaging](/quest/m0/packaging/README.md) - a signed macOS app, a Windows installer, and a Flatpak for Linux, each its own quest - [The persistable portal restore token is pinned](/quest/m0/pin-portal-token.md) - moq-video's restore token, for Wayland sources - [Platform AAC encoders are pinned](/quest/m0/pin-aac-encode.md) - moq-audio's AAC encoders, for AAC destinations diff --git a/quest/m0/destinations.md b/quest/m0/destinations.md index 8fefbb2b..02b63aff 100644 --- a/quest/m0/destinations.md +++ b/quest/m0/destinations.md @@ -5,9 +5,9 @@ A creator adds any number of RTMP, RTMPS, and WHIP destinations alongside MoQ, each with its own codec and bitrate, and goes live to all of them at once. Destinations that take the same picture scene and audio scene with identical -encode settings share one encoder. Each destination's status and errors show -in the UI. Going live and stopping are actions the Hotkeys panel can bind, -as recording is. Audio is Opus here; AAC is +encode settings share one encoder. Each destination is a row in the go-live +sheet and in the health dot's list, with its status and errors. Audio is Opus +here; AAC is [AAC for destinations](/quest/m0/destinations-aac.md). The fan-out lands here as a UI-free egress library, with the studio as its @@ -72,8 +72,15 @@ Decided 2026-11-09 (maintainer, /quest-audit): here, with no moq quest or pin). This quest uses them through [the pin](/quest/m0/pin-egress-dial.md). +Changed 2026-11-09 (maintainer, /quest-plan): going live and stopping, as +actions and as hotkeys, are +[Going live from inside the app](/quest/m0/ux/go-live.md), which lands them +for MoQ relays. This quest adds its destinations to that sheet rather than +building a second way to start a stream. + ## Required +- [Going live from inside the app](/quest/m0/ux/go-live.md) - the go-live sheet, the live control, and the health list its destinations join - [moq's egress dial changes are pinned](/quest/m0/pin-egress-dial.md) - RTMPS that keeps `tcUrl`, and a dial policy the caller supplies ## Related diff --git a/quest/m0/pin-capture-list.md b/quest/m0/pin-capture-list.md new file mode 100644 index 00000000..60ca4005 --- /dev/null +++ b/quest/m0/pin-capture-list.md @@ -0,0 +1,23 @@ +# [XS] moq-video's capture listing is pinned + +## Goal + +The `moq` submodule includes moq-video's richer capture listing: each window +and app with its icon and a stable identity, a live thumbnail of a window, +display, or camera without adding it as a source, and a notice when the list +changes. + +## Plan + +Planned 2026-11-09 (maintainer, /quest-plan) for the +[Add page](/quest/m0/ux/add.md), which shows pictures where the Devices panel +shows names. Advance it with `just upgrade-moq` in its own PR, fixing whatever +the new API breaks in `rs/moq-studio/src/devices.rs`. + +## Waiting + +- moq-dev/moq: [the capture listing quest](https://github.com/moq-dev/moq/blob/main/quest/m3/capture-list.md) lands on moq's `main`. Check that quest's PR has merged. + +## Required + +- [The studio is caught up to moq's main](/quest/m0/pin-main.md) - moq's breaking changes since moq-dev/moq#8272, so this pin takes only its own diff --git a/quest/m0/ux/README.md b/quest/m0/ux/README.md new file mode 100644 index 00000000..f68a949d --- /dev/null +++ b/quest/m0/ux/README.md @@ -0,0 +1,147 @@ +# Broadcaster UX + +## Goal + +A broadcaster opens the studio to one live picture, with the scenes, the sound +levels, the state of the stream, and a panic key always on screen. Everything +else is on a page, in the selected layer's inspector, or in Settings, and an +edit changes what is on air. An optional Studio mode adds cue and take for +broadcast operators. + +It replaces sixteen dockable text panels, a Preview and Program pair, and +command-line flags for what a broadcaster must set before going live. + +Non-goals: media or protocol work, which stays in moq; a plugin API; and +integrations built on any one platform's API. Chat, activity, and dashboards +reach the studio as [private panels](/quest/m1/panels.md), any web page shown +beside the show and never in it. + +This README's own work, once every child has merged: `README.md`'s Panels +section is rewritten around the pages, with a picture of each from the +[screenshots](/quest/m0/ux/shots.md), and the maintainer runs one real show +start to finish on the finished UI. + +## Plan + +Decided 2026-11-09 with the maintainer (/quest-plan). The mock-ups in +[`mock/`](/quest/m0/ux/mock/shell.html) are the reference for every child: HTML +pages drawn at 1440x900, each frame rendered beside it as +`-.webp` by `mock/render.sh`. They fix layout, hierarchy, and +tone. The app is native, so a child matches them in what is where and how it +reads, not pixel for pixel, and a child that has to depart from one changes +the mock-up in the same PR. + +![LIVE](/quest/m0/ux/mock/shell-live.webp) + +### Decisions + +- **Shape:** ✅ one live picture with pages around it (rejected: a hardware + console under the picture, which spends a third of the window on controls a + solo streamer drives by hotkey; workspaces that lock editing on the live + page; a restyle of today's panels). +- **Pages:** ✅ LIVE, BUILD, MIX, and REVIEW (rejected: a left rail of + drawers). They sit in the title bar when the show is landscape and in a left + sidebar when it is portrait, where the spare room is (rejected: always along + the bottom, which costs a landscape picture its height). +- **Live editing:** ✅ no Preview monitor. A scene click takes that scene, and + an edit applies on air. Any scene can be opened for editing off air: the + frame turns amber, the live picture shrinks to a corner, and one button + takes it live or goes back. +- **Studio mode:** ✅ cue and take, off by default (rejected: a Preview and + Program split, which halves the live picture; no mode at all). A scene click + cues, and TAKE or AUTO puts it on air. +- **One control per state:** the live control reads LIVE with the clock and + becomes END STREAM under the pointer. One health dot stands for every + destination, the viewers, and the encoder, which its hover lists. Undo, + record, and settings are icons with tooltips. +- **Not on the home page:** the format, canvas size, and frame rate, which a + broadcaster sets once. They live in Settings. +- **Panels:** ✅ chat and other private panels are their own windows, hidden + from capture, dockable into spare room but never docked by default + (rejected: a chat column in the main window; leaving chat to the browser). +- **Formats:** ✅ one format per show in m0, landscape or portrait, and the + layout follows it. [Both at once](/quest/m1/dual-format.md) is m1 (rejected: + designing every m0 page for two pictures). +- **Layout follows the picture:** the picture gets the largest rectangle the + window allows, and the scenes and the sound take whichever side is left + over. See the portrait and half-screen frames. +- **Toolkit:** ✅ decided by [a spike](/quest/m0/ux/spike.md) of the LIVE page + in egui and in Slint before any page is built (rejected: staying on egui + unasked; egui against Rune, which + [moq-dev/studio#98](https://github.com/moq-dev/studio/pull/98) already + spiked). Only those two draw the compositor's own textures with no copy on + the studio's own device today. +- **Milestones:** ✅ redesigning what exists is m0, here. Features that do not + exist yet are [m1](/quest/m1/README.md), and three more are + [m2](/quest/m2/README.md) (rejected: all in m0; all in m1). +- **Dropped:** a rundown that fills overlays, one-click looks and a brand kit, + active-speaker layouts, control surfaces, and a rehearsal mode. Not planned. + +### What is where + +The first column holds on every page, not only LIVE. The bottom strip of +scene pads, panic key, and sound is on all four. On MIX, which is the sound, +its sound slot holds a small live picture instead, and on REVIEW the +program's card is the live picture. + +| Always on screen | On a page | In Settings | +| --- | --- | --- | +| The live picture and its tally frame | BUILD: layers, adding a source, the inspector, keyframes | Format, canvas size, frame rate, scene sizes | +| Scene pads with live thumbnails | MIX: every strip, sends, filters, delay | Where the stream goes, keys, failover | +| A meter and a mute for each sound on air | REVIEW: replay, clips, recordings | Recording folder and contents | +| The live control, health dot, and record | Health details, from the health dot | Hotkeys, virtual camera, guests' room | +| The panic key | Guests, from Add | Effects, published scenes, the scene graph, edit history | + +### The frames + +| Frame | Shows | +| --- | --- | +| [live](/quest/m0/ux/mock/shell-live.webp), [hover](/quest/m0/ux/mock/shell-hover.webp) | The LIVE page, and what the health dot, the live control, and an icon show under the pointer | +| [build](/quest/m0/ux/mock/shell-build.webp), [offair](/quest/m0/ux/mock/shell-offair.webp) | The BUILD page on air, and editing a scene that is not | +| [studio](/quest/m0/ux/mock/shell-studio.webp) | Studio mode: a cued scene, NEXT, TAKE, and AUTO | +| [portrait](/quest/m0/ux/mock/shell-portrait.webp), [compact](/quest/m0/ux/mock/shell-compact.webp) | The layout following a portrait show, and a window snapped beside a game | + +`add.html`, `firstrun.html`, `mix.html`, `settings.html`, `review.html`, and +`golive.html` hold the other m0 pages, and `later.html` the m1 features; each +quest links its own. + +### Borrowed + +- Editing the live picture, with layers beside it: Meld Studio and StreamYard, + which stay safe by constraining what a click can do. +- Cue and take, red for program and amber for preview: hardware switchers. + vMix and OBS Studio Mode show the cost of the split this plan rejects. +- Selection handles, smart guides, a toolbar on the selected thing, and an + inspector that follows the selection: Figma and Keynote. +- Pages for kinds of work: DaVinci Resolve. +- Sends as a matrix of sounds against mixes: Wave Link. +- The first-run format choice before any scene exists: mobile-first studios. +- OBS 32.2's add-source dialog with thumbnails, which this goes past with app + icons and live pictures. + +### What a person must check + +An agent can prove a page matches its mock-up's layout, that nothing clips at +any window size, and what a frame costs. It cannot judge these, so each child's +PR hands them to the maintainer as `## Waiting` with what to look at: + +- **Taste:** whether the page reads as bold and calm at once. The PR shows its + screenshots beside the mock-up frames. +- **Feel:** drag latency, how a scene change and a tally change move, the hold + to end a stream. The PR attaches a short recording, and the maintainer tries + it on their own machine. +- **Real devices:** cameras, capture cards, microphones, a second monitor, and + mixed display scales. The agent hosts have no camera. +- **System dialogs:** permission prompts and the operating system's own window + pickers, which no headless run can answer. +- **A real stream:** going live to a real destination with the maintainer's + credentials, and ending it. +- **Other apps in front:** hotkeys, and later the mini controller, while a + full-screen game has focus. +- **Readability:** contrast, and red against amber for a color-blind reader. + +## Related + +- [m1](/quest/m1/README.md) - the features that follow the redesign +- [Destinations](/quest/m0/destinations.md) - RTMP and WHIP rows in the go-live sheet this epic builds +- [Wayland display and window sources](/quest/m0/wayland-sources.md) - the system picker the Add page opens diff --git a/quest/m0/ux/add.md b/quest/m0/ux/add.md new file mode 100644 index 00000000..257cd137 --- /dev/null +++ b/quest/m0/ux/add.md @@ -0,0 +1,63 @@ +# [L] Adding a source by picking a picture + +## Goal + +BUILD's Add tab shows what can be added as pictures, in the frames of +[`mock/add.html`](/quest/m0/ux/mock/add.html): + +- **Apps:** every window as a live thumbnail with its app's real icon, its + app's name, and its title, grouped by app. +- **Screens and cameras:** a live picture of each, with an icon for the kind + of device (webcam, capture card, phone, display) and its name. +- **Sound:** each microphone and the system's sound with a moving meter, so + the right one is found by talking. +- **Everything else:** files, a web page, text, guests, and other scenes, + each a card. + +A search field filters all of it, and a Recent row holds what was added +lately. A card is added by dragging it onto the picture, where it lands under +the pointer, or by selecting it and pressing one Add button. The list +refreshes itself when a device is plugged in or a window opens. No id is ever +typed. + +It replaces the Devices panel and the add-source form, where a window is +chosen by typing its id. + +## Plan + +- The pictures and icons come from moq-video's capture listing, which today + gives only ids, names, and sizes (`Window { id, title, app, width, height }` + in `moq/rs/moq-video/src/capture/mod.rs`), with no icon, no thumbnail, no + change notice, and apps listed on macOS alone. That work is planned in moq + and reaches here through [its pin](/quest/m0/pin-capture-list.md). +- Where the operating system owns the picker, as on Wayland, the Apps and + Screens sections are one button that opens the system's picker, and the + choice is remembered: see the `system` frame and + [Wayland sources](/quest/m0/wayland-sources.md). +- Recommended, not decided: a click selects and a button adds, rather than a + click adding at once. A new layer is on air the moment it is added, and the + selected card's picture is the last check against airing the wrong window. +- The inspector folds away while Add is open, since nothing is selected, so + the picture keeps its width. +- The `apps` frame draws a "With its sound" switch. An app's own sound is not + captured today, so the switch is left out until moq can. +- A card for something that cannot be added says why in its place (a kind + this build lacks, a permission not granted) with the step that fixes it, + rather than being absent. +- A dropped card lands where it is dropped, sized to its own shape, and is + sent to the audio scene on program if it has sound, as an added source is + today. +- The first-run screen reuses these cards. + +Human check: real cameras, capture cards, and microphones, the operating +system's permission prompts, and whether a window is found faster than by +its title. + +## Required + +- [The BUILD page](/quest/m0/ux/build.md) - the column the Add tab lives in +- [moq-video's capture listing is pinned](/quest/m0/pin-capture-list.md) - icons, live pictures, and change notices for windows and devices + +## Related + +- [Wayland display and window sources](/quest/m0/wayland-sources.md) - the remembered system pick the Apps and Screens sections open diff --git a/quest/m0/ux/build.md b/quest/m0/ux/build.md new file mode 100644 index 00000000..9bce613b --- /dev/null +++ b/quest/m0/ux/build.md @@ -0,0 +1,54 @@ +# [L] The BUILD page + +## Goal + +The BUILD page of the [mock-up](/quest/m0/ux/mock/shell-build.webp) replaces +the Items, Sources, Filters, and Keyframes panels: + +- **Layers** on the left list what the scene draws, front to back, with the + always-on-top layers in a group of their own. Each row has an icon for its + kind, its name, what it shows in words, and eye and lock toggles. Rows drag + to reorder. +- **The picture** is edited directly. A selected layer has handles and a + small toolbar on it (crop, fit, fill, shape, cut out, more), and a right + click anywhere offers what applies there. +- **The inspector** on the right follows the selection: place, shape, look + (background removal, color grade, filters, blend), and an Animate tab that + holds the layer's keyframes. +- **Linked or independent is visible:** a layer that shows a source other + scenes also show says so, with how many, and offers to make it + independent. Duplicating a scene asks which. + +Undo and redo are the title bar's icons, and the history is a list under the +undo icon. The scene graph moves to Settings, Advanced. + +## Plan + +The words change with the place: "item" becomes "layer" and "top layer" +becomes "always on top" in the UI and in `README.md`. The scene file's keys +do not change. + +- A source nothing shows no longer needs a panel of its own: Layers lists + the scene's layers, and unused sources appear under Add, where a source is + also removed for good. +- "Make independent" is today's clone (`kind = "clone"`). OBS users lose + work to the opposite default, where a duplicate silently shares its + sources, so say which one a duplicate will be before it happens. +- Every status a source has today (live, stalled, ended, failed with its + reason, GPU or CPU for a web page, the delay a broadcast plays behind) + shows on its layer row as an icon with the words on hover, and Play, Stop, + and Reload are in its inspector. +- Snapping, the grid, nudging, aspect lock, and copy and paste keep working + as `interact.rs` and `snap.rs` have them. Add the right-click menus and the + on-picture toolbar; there is no context menu anywhere today. +- Errors that were one status-bar line (`Panels::message`) appear where they + happened: on the layer, the field, or the pad. +- **Interact** with a web page becomes a mode of the selected layer, entered + from its toolbar and left with Escape. + +Human check: feel of dragging, resizing, and reordering on a real machine, +and whether the inspector's grouping is where a streamer looks first. + +## Required + +- [One live picture](/quest/m0/ux/live.md) - the frame, the stage, and the page this fills diff --git a/quest/m0/ux/first-run.md b/quest/m0/ux/first-run.md new file mode 100644 index 00000000..f6535e5f --- /dev/null +++ b/quest/m0/ux/first-run.md @@ -0,0 +1,47 @@ +# [M] First run + +## Goal + +A new show is made in four pictures, as in +[`mock/firstrun.html`](/quest/m0/ux/mock/firstrun.html): + +1. **What are you making?** Landscape or portrait. +2. **What goes in it?** A camera, a screen or window, and a microphone, each + picked from live pictures and a meter that says "say something", or none. +3. **Can this machine and connection carry it?** One verdict in large type, + such as "Ready for 1080p at 60", with the three checks as short lines and + the measurements behind Details. A check that failed is one plain sentence + and one button that fixes or skips it. +4. **A starting layout**, from a few pictured templates, then Start. + +It replaces guided setup's screen of findings, which shows a failed check as +a red line of the error's own text and needs `--connect` on the command line +to measure the uplink. + +## Plan + +The probes and the canvas choice are guided setup's +(`rs/moq-studio/src/setup.rs`); this is how they are shown and reached. + +- It runs from New show in the library, not only when a named file is + missing. +- The connection check uses the relay set in the same flow: a field for its + address with the default filled in, skippable, in which case the verdict + says the canvas was chosen without measuring. +- The templates are scene files in the repository with places for the + picked camera, picture, and microphone, and each works with any of them + missing. +- Where the operating system owns the picker, step 2's screen or window is + the Add page's one button that opens it, not a row of thumbnails. +- The `check` frame draws a failed check beside a passing one to show how it + reads; the screen shows one verdict. +- Nothing is written until Start, as today, and the proposed scene is the + first edit in the history. + +Human check: a first run by someone who has not used the studio, on a +machine with a real camera and microphone, watched without helping. + +## Required + +- [Adding a source by picking a picture](/quest/m0/ux/add.md) - the cards with live pictures and meters +- [Going live from inside the app](/quest/m0/ux/go-live.md) - the show library it starts from and the relay it measures diff --git a/quest/m0/ux/go-live.md b/quest/m0/ux/go-live.md new file mode 100644 index 00000000..8f254d13 --- /dev/null +++ b/quest/m0/ux/go-live.md @@ -0,0 +1,77 @@ +# [L] Going live from inside the app + +## Goal + +`moq-studio` starts with no arguments and no terminal, and a broadcaster goes +live and stops from the window, as in +[`mock/golive.html`](/quest/m0/ux/mock/golive.html): + +- **Opened with no show**, it lists recent shows with a picture of each, a + New show card, and Open a show file. +- **Off air**, the frame around the picture is neutral, the tag reads OFF + AIR, and the live control is GO LIVE. +- **GO LIVE** opens a short sheet: where the stream is going, each + destination with a toggle and its state, and a checklist that fills in + (camera, a microphone level that moves, an encoder that keeps up, a + connection that carries the bitrate), then one confirm. A failed line is + one plain sentence and its fix. +- **On air**, the control is the clock, and END STREAM under the pointer, + held to confirm. Ending delivers the broadcast's tail before the session + closes, as quitting does today. +- **The health dot** opens every destination, the viewers, and the encoder, + and its details: the worst problem first, in plain words with its cause, + which is today's Health panel. + +Where the stream goes is set in Settings: the relay's address, its token, a +failover relay, the broadcast's name, and whether to encode only while +someone watches. Go live and End stream are hotkey actions. + +## Plan + +Today the studio is live from launch when `--connect` is given and never +otherwise, and the relay, token, broadcast name, failover, on-demand, and +recordings folder are flags (`rs/moq-studio/src/main.rs`). A packaged app is +opened by a double click, so none of that can stay a flag only. + +- ✅ **Connecting is a runtime action.** The engine opens and closes its + relay sessions on Go live and End stream. Composing, previewing, and + recording never needed a relay and still do not. +- **Flags stay** as overrides for scripts and tests: a flag wins over the + saved setting for that run and is never written back. +- **Where it is saved:** the relay address, broadcast name, and options are + per show, beside the scene file, not in it, since the scene file is meant + to be shared and names no relay. Tokens go in the operating system's + credential store, as [destinations](/quest/m0/destinations.md) decided for + stream keys, and never in a file. +- **The checklist is guided setup's probes** (`rs/moq-studio/src/setup.rs`): + devices, the encoder's pace, and the uplink. The uplink probe runs against + the relay about to be used and is skipped with its reason when a + connection reports no estimate. +- Recommended, not decided: a failed check never blocks going live. Its + sentence and its fix are advice, since a streamer on a bad connection may + still choose to go. Only having nothing to send, no destination switched + on, disables the confirm. +- GO LIVE is not red. Red stays the on-air state alone. +- **A replay of the program** needs the relay's cache, so off air it says so + rather than failing. +- **Recent shows** are kept per user. A show's picture is the last frame of + its program, saved on quit. +- Guests' room flags move with the rest; the room key stays a file the + setting points at. + +[Destinations](/quest/m0/destinations.md) adds RTMP and WHIP rows to the same +sheet and the same health popover; this quest lands them for MoQ relays +alone. + +Human check: a real stream to a real relay with the maintainer's token, +ended and started again; the credential store's own prompt on each desktop; +and opening the app from a file manager with no terminal. + +## Required + +- [One live picture](/quest/m0/ux/live.md) - the live control, the health dot, and the frame this drives + +## Related + +- [Destinations](/quest/m0/destinations.md) - RTMP and WHIP rows in this sheet +- [Packaging](/quest/m0/packaging/README.md) - an app opened by a double click has no flags diff --git a/quest/m0/ux/guests.md b/quest/m0/ux/guests.md new file mode 100644 index 00000000..f17d03c5 --- /dev/null +++ b/quest/m0/ux/guests.md @@ -0,0 +1,24 @@ +# [S] Guests are invited and placed from Add + +## Goal + +The Guests section of Add (the `guests` frame of +[`mock/add.html`](/quest/m0/ux/mock/add.html)) replaces the Guests panel: +Invite makes a link and shows its QR code, each guest who joined is a card +with their live picture, dragged onto the picture like any source, and a +guest's card offers Remove from scenes and New link. A failing token renewal +shows on the card. + +Without a room configured, the section says what to set and opens that +Settings pane. + +## Plan + +The behavior is today's (`rs/moq-studio/src/guests.rs`): only the place +changes. A guest's sound still joins no audio scene until placed, so the card +says "not heard yet" until it is sent to the stream's mix, with the button +that does it. + +## Required + +- [Adding a source by picking a picture](/quest/m0/ux/add.md) - the page and the cards diff --git a/quest/m0/ux/kit.md b/quest/m0/ux/kit.md new file mode 100644 index 00000000..0b578e77 --- /dev/null +++ b/quest/m0/ux/kit.md @@ -0,0 +1,51 @@ +# [L] The studio's look + +## Goal + +The studio has a look of its own: its colors, type, spacing, and icons come +from one set of tokens, and its controls are its own widgets, drawn as the +[mock-ups](/quest/m0/ux/mock/shell-live.webp) draw them. Today's panels wear +it at once, and a gallery view shows every widget in every state, in the +[screenshots](/quest/m0/ux/shots.md). + +Today the app sets no fonts, style, or visuals, has no icon anywhere, and +hard-codes a handful of colors. + +## Plan + +`mock/kit.css` is the reference: its custom properties are the tokens, and +its classes are the widgets. + +- **Tokens:** near-black surfaces in three steps, one line color, text in + three strengths. Red is the program and nothing else. Amber is + everything that is not right and not on air: cued, off air, degraded, or + needing attention. Green is a healthy level. Blue is selection. +- **Type:** three bundled faces under an open license: a condensed face for + state and names (ON AIR, scene names, page names), a plain face for + everything else, and a monospace for clocks and rates, whose digits must + not jitter. The mock-ups use Barlow Condensed, Inter, and JetBrains Mono; + keep them unless one fails to render well in the chosen toolkit. Bundled, + never the machine's, as overlays already do. +- **Icons:** one open-licensed set in a single stroke weight (the mock-ups + draw in Lucide's style), named by role in one place. Every icon-only + control has a tooltip that names it and its key. +- **Widgets:** the scene pad, the pill, the icon button, the toggle, the + slider, the fader, the segmented meter, the number field that drags, the + popover, the tooltip, the sheet, the keycap, and the tally frame. +- **Effects:** today's rune-fx effects (`rs/moq-studio/src/fx.rs`) read the + tokens, and stop registering spots while panels draw, which ties them to + the layout. The Effects setting and reduced motion keep working. + +Measure the UI's frame time before and after, under a real compositor. + +Recommendation: keep each widget's state in the caller, so the pages can +draw from a snapshot of the engine rather than holding its lock while they +draw, as `Studio::ui` does today. + +Human check: taste, on the gallery's screenshots, and readability of red +against amber. + +## Required + +- [Toolkit spike](/quest/m0/ux/spike.md) - the toolkit the widgets are written for +- [UI screenshots](/quest/m0/ux/shots.md) - the gallery's pictures diff --git a/quest/m0/ux/live.md b/quest/m0/ux/live.md new file mode 100644 index 00000000..ce65135e --- /dev/null +++ b/quest/m0/ux/live.md @@ -0,0 +1,80 @@ +# [XL] One live picture + +## Goal + +The studio opens to the LIVE page of the +[mock-up](/quest/m0/ux/mock/shell-live.webp), in its own frame: + +- **The picture** is the program, in a red tally frame, as large as the + window allows. The Preview monitor and the Program monitor are gone. +- **Scene pads** show each scene's live thumbnail, name, and key. A click + takes the scene on air by the show's transition. +- **The sound strip** has a meter and a mute for each sound the stream + carries, and the stream's own meter. +- **The title bar** is the window's: the show's name, undo and redo, the + pages, one health dot, record, and the live control. The + [hover frame](/quest/m0/ux/mock/shell-hover.webp) shows what each opens. +- **The panic key** cuts to a scene the show marks as its safe scene and + mutes every microphone, from a button or a hotkey. +- **The layout follows the picture:** a + [portrait show](/quest/m0/ux/mock/shell-portrait.webp) moves the pages to a + sidebar and the scenes and sound to columns, and a + [narrow window](/quest/m0/ux/mock/shell-compact.webp) stacks them. Nothing + clips at any size the window allows. + +The title bar and the bottom strip are on every page, so the scenes, the +sound, the panic key, and a live picture never leave the window while on air. + +BUILD, MIX, and REVIEW exist as pages holding today's panels in the new look, +so nothing the studio does is lost before its own quest redesigns it. +egui_dock, or whatever the toolkit's dock is, is removed. + +## Plan + +Decided 2026-11-09 (maintainer, /quest-plan); see +[the epic](/quest/m0/ux/README.md) for the options rejected. + +- ✅ **No Preview.** The compositor keeps its preview target + (`Compositor::set_preview` in `rs/moq-scene/src/compositor.rs`) for + [editing off air and Studio mode](/quest/m0/ux/studio-mode.md); on this + page the edited scene is always the scene on air. Everything that targets + `preview_scene` today (the Items panel, arrange, nudge, interact) targets + the program's scene. +- ✅ **A click takes.** The refusals stay and are said in place: a scene of + another size, or one that draws the program, cannot go on air + (`Collection::takes`), so its pad says why instead of failing in a status + line. +- ✅ **One live control.** Until [go live](/quest/m0/ux/go-live.md) lands, + the studio is still live from launch when `--connect` is given, so the + control shows the clock and its END STREAM face is not offered. +- ✅ **One health dot**, the worst grade of `health.rs`, with destinations, + viewers, and the encoder in its popover. +- ✅ **Pages in the title bar for a landscape show, a left sidebar for a + portrait one** (rejected: along the bottom). +- The hotkey actions (`rs/moq-studio/src/controls.rs`) keep working: a + scene's key takes it, as its pad does. Cut and Transition have nothing to + act on until Studio mode, so they are hidden until then. +- The panic key's safe scene and the mute are new state in the scene file; + document both in `README.md`. + +The layout is chosen by one pure function of the window's size and the +picture's shape, tested without a window: for every size from the minimum up, +no region overlaps another and the picture is the largest rectangle. The +mock-up's three arrangements are its cases. + +Draw the pages from a snapshot of the engine, not while holding its lock +(`Studio::ui` holds `engine.state` across the whole dock today, and the +render thread wants the same lock). Split `ui.rs`, 4,900 lines behind one +`Panels` struct, by page as each is touched. + +Measure the UI's frame time against today's under a real compositor, and +that the program's render time is unchanged. + +Human check: taste and feel on the maintainer's machine, a second monitor and +mixed display scales, and the custom frame's drag, resize, snap, and +full-screen behavior on each desktop, which no headless run exercises. + +## Required + +- [The studio's look](/quest/m0/ux/kit.md) - the widgets the page is built from +- [A small live picture of every scene and source](/quest/m0/ux/scene-thumbs.md) - the scene pads' thumbnails diff --git a/quest/m0/ux/mix.md b/quest/m0/ux/mix.md new file mode 100644 index 00000000..24821d1a --- /dev/null +++ b/quest/m0/ux/mix.md @@ -0,0 +1,58 @@ +# [L] The MIX page + +## Goal + +The MIX page of [`mock/mix.html`](/quest/m0/ux/mock/mix.html) replaces the +Audio panel: + +- One strip per sound, with its icon and name, a tall meter, a fader, and a + mute, and the audio scenes as master strips beside them. +- Under each strip, where it is heard: one dot per audio scene. The audio + scene the stream carries is marked as such, and any can be listened to + with a headphone button. +- A strip opens to its pan, its delay, and its filters. +- The same sends as a matrix, for someone who thinks in routing. + +The LIVE page's sound strip is the same meters and mutes for the sounds the +stream carries. On this page the bottom strip keeps the scene pads and the +panic key and shows a small live picture where that sound strip sits +elsewhere, so an operator riding a fader still sees what is on air. + +## Plan + +Nothing about mixing changes: gain, pan, mute, delay, sends, the audio scene +on program, the recorded one, and Listen are as `README.md`'s Audio scenes +section has them. "Audio scene" stays the word, since it was chosen over +"mix" on 2026-11-06 +([moq-dev/studio#77](https://github.com/moq-dev/studio/pull/77)). + +- A strip opens in place and its neighbours narrow, so no meter is ever + covered during a show. Strips and the matrix are two views of one page. +- The output Listen plays on is picked on the listened audio scene's strip, + not in Settings. +- A fader is gain in decibels, with unity marked and a double click + returning to it, as pan returns to center today. +- The meter keeps the peak tick and the red at full scale of + `rs/moq-studio/src/ui/levels.rs`, drawn as segments. +- A sound heard twice by construction cannot happen here, since an audio + scene plays a sound once however it reaches it. Show that: a dot is filled + when the sound reaches the audio scene by any route, and hollow-ringed when + it does so only through another audio scene. +- A microphone feeding the output it is heard on is the one loop the studio + cannot prevent. Warn on Listen when the output is not headphones, where the + system can tell. +- [Audio filters](/quest/m0/audio-filters.md) and + [clap to measure](/quest/m0/sync-clap.md) add their controls to the opened + strip; this quest leaves their places. + +Human check: a real microphone and headphones, and whether the sends read at +a glance to someone who has never seen an audio scene. + +## Required + +- [One live picture](/quest/m0/ux/live.md) - the page and the sound strip + +## Related + +- [Audio filters](/quest/m0/audio-filters.md) - the filter chain an opened strip configures +- [Clap-to-measure sync delay](/quest/m0/sync-clap.md) - the button beside a strip's delay diff --git a/quest/m0/ux/mock/add-apps.webp b/quest/m0/ux/mock/add-apps.webp new file mode 100644 index 00000000..c3b554fe Binary files /dev/null and b/quest/m0/ux/mock/add-apps.webp differ diff --git a/quest/m0/ux/mock/add-cameras.webp b/quest/m0/ux/mock/add-cameras.webp new file mode 100644 index 00000000..714aadee Binary files /dev/null and b/quest/m0/ux/mock/add-cameras.webp differ diff --git a/quest/m0/ux/mock/add-drop.webp b/quest/m0/ux/mock/add-drop.webp new file mode 100644 index 00000000..18dd6b82 Binary files /dev/null and b/quest/m0/ux/mock/add-drop.webp differ diff --git a/quest/m0/ux/mock/add-guests.webp b/quest/m0/ux/mock/add-guests.webp new file mode 100644 index 00000000..6fb1676d Binary files /dev/null and b/quest/m0/ux/mock/add-guests.webp differ diff --git a/quest/m0/ux/mock/add-system.webp b/quest/m0/ux/mock/add-system.webp new file mode 100644 index 00000000..74839f54 Binary files /dev/null and b/quest/m0/ux/mock/add-system.webp differ diff --git a/quest/m0/ux/mock/add.html b/quest/m0/ux/mock/add.html new file mode 100644 index 00000000..00ec1482 --- /dev/null +++ b/quest/m0/ux/mock/add.html @@ -0,0 +1,260 @@ + + + + + +Add a source + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quest/m0/ux/mock/firstrun-check.webp b/quest/m0/ux/mock/firstrun-check.webp new file mode 100644 index 00000000..223579fa Binary files /dev/null and b/quest/m0/ux/mock/firstrun-check.webp differ diff --git a/quest/m0/ux/mock/firstrun-format.webp b/quest/m0/ux/mock/firstrun-format.webp new file mode 100644 index 00000000..8161c993 Binary files /dev/null and b/quest/m0/ux/mock/firstrun-format.webp differ diff --git a/quest/m0/ux/mock/firstrun-layout.webp b/quest/m0/ux/mock/firstrun-layout.webp new file mode 100644 index 00000000..24a63cfc Binary files /dev/null and b/quest/m0/ux/mock/firstrun-layout.webp differ diff --git a/quest/m0/ux/mock/firstrun-sources.webp b/quest/m0/ux/mock/firstrun-sources.webp new file mode 100644 index 00000000..4b1b0915 Binary files /dev/null and b/quest/m0/ux/mock/firstrun-sources.webp differ diff --git a/quest/m0/ux/mock/firstrun.html b/quest/m0/ux/mock/firstrun.html new file mode 100644 index 00000000..8b62201a --- /dev/null +++ b/quest/m0/ux/mock/firstrun.html @@ -0,0 +1,207 @@ + + + + + +First run + + + + + + + + + + + + + + + + + + + + + + diff --git a/quest/m0/ux/mock/golive-health.webp b/quest/m0/ux/mock/golive-health.webp new file mode 100644 index 00000000..2d27732e Binary files /dev/null and b/quest/m0/ux/mock/golive-health.webp differ diff --git a/quest/m0/ux/mock/golive-library.webp b/quest/m0/ux/mock/golive-library.webp new file mode 100644 index 00000000..ec226c4d Binary files /dev/null and b/quest/m0/ux/mock/golive-library.webp differ diff --git a/quest/m0/ux/mock/golive-preflight.webp b/quest/m0/ux/mock/golive-preflight.webp new file mode 100644 index 00000000..a3537c64 Binary files /dev/null and b/quest/m0/ux/mock/golive-preflight.webp differ diff --git a/quest/m0/ux/mock/golive-ready.webp b/quest/m0/ux/mock/golive-ready.webp new file mode 100644 index 00000000..2fc5e9a4 Binary files /dev/null and b/quest/m0/ux/mock/golive-ready.webp differ diff --git a/quest/m0/ux/mock/golive.html b/quest/m0/ux/mock/golive.html new file mode 100644 index 00000000..080c6a01 --- /dev/null +++ b/quest/m0/ux/mock/golive.html @@ -0,0 +1,161 @@ + + + + + +Studio go live + + + + + + + + + + + + + + + + + + diff --git a/quest/m0/ux/mock/kit.css b/quest/m0/ux/mock/kit.css new file mode 100644 index 00000000..675da877 --- /dev/null +++ b/quest/m0/ux/mock/kit.css @@ -0,0 +1,245 @@ +/* Shared look of the studio mock-ups: tokens, chrome, fake pictures. Reference only; the app is not HTML. */ +/* Mock-up only: fixed colors on purpose, this is the app's own dark chrome. */ +.mock{--bg:#09090c;--p1:#111116;--p2:#18181f;--p3:#22222b;--line:#2a2a35;--text:#f3f3f6;--dim:#8d8d9c;--faint:#5b5b69; + --air:#ff2d46;--cue:#ffb020;--ok:#35d07f;--sel:#4f8cff; + --disp:'Barlow Condensed','Arial Narrow',sans-serif;--ui:Inter,system-ui,sans-serif;--mono:'JetBrains Mono',ui-monospace,monospace} +*{box-sizing:border-box} +body{margin:0;font-family:Inter,system-ui,sans-serif} +.switch{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px} +.switch button{font:600 12px/1 Inter,system-ui,sans-serif;padding:8px 10px;border-radius:8px;border:1px solid var(--border,#444);background:transparent;color:inherit;cursor:pointer} +.switch button[aria-pressed=true]{background:var(--foreground,#fff);color:var(--background,#000);border-color:transparent} +.note{font:13px/1.5 Inter,system-ui,sans-serif;margin:10px 2px 0;opacity:.85} +.fit{position:relative;width:100%;overflow:hidden;border-radius:10px} +.frame{position:absolute;left:0;top:0;width:1440px;height:900px;transform-origin:0 0;display:none; + background:var(--bg);color:var(--text);font:13px/1.3 var(--ui)} +.frame.on{display:grid} +body.shot .switch,body.shot .note{display:none} +body.shot .fit{border-radius:0} + +svg.i{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;flex:none} +.sm svg.i,svg.i.sm{width:14px;height:14px} +kbd{font:500 10px/1 var(--mono);padding:3px 5px;border-radius:4px;background:#0006;border:1px solid #fff2;color:#fffb} +.row{display:flex;align-items:center;gap:8px} +.grow{flex:1;min-width:0} +.dim{color:var(--dim)} +.cap{font:600 11px/1 var(--disp);letter-spacing:.14em;text-transform:uppercase;color:var(--dim)} + +/* top bar: the window's own title bar */ +.top{grid-column:1/-1;display:flex;align-items:center;gap:14px;padding:0 14px;background:var(--p1);border-bottom:1px solid var(--line)} +.logo{width:22px;height:22px;border-radius:7px;background:conic-gradient(from 210deg,var(--air),#ff8a3d,var(--air))} +.show{font:600 14px/1 var(--ui)} +.pill{display:flex;align-items:center;gap:7px;height:28px;padding:0 10px;border-radius:14px;background:var(--p2);border:1px solid var(--line);font:500 12px/1 var(--ui);white-space:nowrap} +.pill b{font:500 12px/1 var(--mono)} +.dot{width:8px;height:8px;border-radius:50%;background:var(--ok)} +.onair{height:30px;padding:0 12px;border-radius:6px;background:var(--air);color:#fff;font:700 17px/30px var(--disp);letter-spacing:.1em;display:flex;gap:10px;box-shadow:0 0 22px #ff2d4666} +.onair b{font:500 14px/30px var(--mono);letter-spacing:0} +.btn{height:30px;padding:0 12px;border-radius:8px;border:1px solid var(--line);background:var(--p2);color:var(--text);font:600 12px/28px var(--ui);display:flex;align-items:center;gap:7px;white-space:nowrap} +.btn.end{background:transparent;border-color:var(--air);color:var(--air)} +.btn.rec .dot{background:var(--air)} +.wc{display:flex;gap:7px;margin-left:4px}.wc i{width:12px;height:12px;border-radius:50%;background:var(--p3)} + +/* rail */ +.rail{background:var(--p1);border-right:1px solid var(--line);display:flex;flex-direction:column;align-items:center;padding:10px 0;gap:4px} +.rail a{width:48px;padding:7px 0 5px;border-radius:10px;display:flex;flex-direction:column;align-items:center;gap:4px;color:var(--dim);font:500 9.5px/1 var(--ui)} +.rail a.on{background:var(--p3);color:var(--text)} +.rail a.cue{color:var(--cue)} +.rail .sp{flex:1} + +/* stage */ +.stagewrap{position:relative;min-width:0;min-height:0;display:flex;flex-direction:column;padding:16px 18px 8px;gap:8px; + background:radial-gradient(120% 90% at 50% 0,#15151d,var(--bg))} +.stage{position:relative;flex:1;min-height:0;display:flex;align-items:center;justify-content:center;container-type:size} +.tally{position:relative;width:min(100cqw - 12px,(100cqh - 12px)*16/9);aspect-ratio:16/9;border-radius:10px;outline:3px solid var(--air);outline-offset:3px;box-shadow:0 0 60px #ff2d4633} +.tally.cue{outline-color:var(--cue);box-shadow:0 0 60px #ffb02026} +.tally.cue::after{content:"";position:absolute;inset:-10px;border-radius:16px;z-index:-1; + background:repeating-linear-gradient(-45deg,#ffb02033 0 12px,transparent 12px 24px)} +.tag{position:absolute;left:12px;top:12px;z-index:5;padding:0 9px;border-radius:5px;background:var(--air);color:#fff;font:700 15px/24px var(--disp);letter-spacing:.12em} +.tag.cue{background:var(--cue);color:#1a1200} +.under{display:flex;align-items:center;gap:8px;height:30px;color:var(--dim);font-size:12px} +.seg{display:flex;border:1px solid var(--line);border-radius:8px;overflow:hidden} +.seg span{padding:5px 9px;font:500 11.5px/1 var(--ui);color:var(--dim)} +.seg span.on{background:var(--p3);color:var(--text)} +.chip{display:flex;align-items:center;gap:6px;height:26px;padding:0 9px;border-radius:8px;border:1px solid var(--line);font:500 11.5px/1 var(--ui);color:var(--dim);white-space:nowrap} +.rewind{flex:1;height:6px;border-radius:3px;background:linear-gradient(90deg,var(--p3) 0 78%,var(--air) 78% 100%);position:relative} +.rewind::after{content:"";position:absolute;right:0;top:-4px;width:3px;height:14px;background:#fff;border-radius:2px} + +/* fake pictures: sizes in cqw so the same markup is a thumbnail */ +.pic{position:relative;width:100%;height:100%;overflow:hidden;border-radius:inherit;container-type:size;background:#000} +.pic>*{position:absolute} +.game{background:linear-gradient(#1b0b3a 0,#5a1f6e 38%,#ff7a3d 55%,#12061f 55.5%,#0a0414)} +.game .sun{left:38%;top:22%;width:24%;aspect-ratio:1;border-radius:50%;background:linear-gradient(#ffd166,#ff3d7f)} +.game .grid{left:-20%;right:-20%;top:55.5%;bottom:0;background: + repeating-linear-gradient(90deg,#ff3df055 0 .25cqw,transparent .25cqw 6cqw), + repeating-linear-gradient(0deg,#ff3df055 0 .25cqw,transparent .25cqw 4cqw);transform:perspective(40cqw) rotateX(58deg);transform-origin:top} +.game .hp{left:3%;top:13%;width:18%;height:2.2%;border-radius:1cqw;background:linear-gradient(90deg,#35d07f 70%,#0006 70%)} +.game .map{right:3%;top:5%;width:11%;aspect-ratio:1;border-radius:50%;border:.3cqw solid #fff8;background:#0b1f2a} +.game .ammo{left:44%;bottom:5%;font:700 3.4cqw/1 var(--disp);color:#fff;letter-spacing:.05em} +.cam{right:3%;bottom:6%;width:19%;aspect-ratio:1;border-radius:50%;overflow:hidden;background:linear-gradient(160deg,#2b3350,#141826);box-shadow:0 0 0 .35cqw #fff,0 1cqw 3cqw #000a} +.cam::before{content:"";position:absolute;left:32%;top:20%;width:36%;aspect-ratio:1;border-radius:50%;background:#e9b48f} +.cam::after{content:"";position:absolute;left:14%;top:58%;width:72%;height:60%;border-radius:45% 45% 0 0;background:#3b6cf6} +.l3{left:3%;bottom:7%;padding:.9cqw 1.6cqw;border-radius:.9cqw;background:linear-gradient(90deg,#111a,#1118);border-left:.5cqw solid var(--air);color:#fff;font:700 2.6cqw/1.1 var(--disp);letter-spacing:.04em;backdrop-filter:blur(6px)} +.l3 small{display:block;font:500 1.3cqw/1.4 var(--ui);letter-spacing:0;opacity:.8} +.chat{background:linear-gradient(135deg,#2a1638,#101826)} +.chat .cam{left:6%;top:10%;right:auto;bottom:auto;width:56%;height:80%;aspect-ratio:auto;border-radius:2cqw} +.chat .col{right:5%;top:10%;width:28%;height:80%;border-radius:1.6cqw;background:#0007; + background-image:repeating-linear-gradient(#fff3 0 1.2cqw,transparent 1.2cqw 4cqw);background-size:70% 100%;background-repeat:no-repeat;background-position:1.5cqw 2cqw} +.brb{background:repeating-linear-gradient(-45deg,#16161d 0 6cqw,#1e1e28 6cqw 12cqw);display:grid;place-items:center} +.brb b{position:static;font:700 15cqw/1 var(--disp);letter-spacing:.06em;color:#fff} +.soon{background:radial-gradient(circle at 50% 120%,#ff2d46,#2a0a22 60%,#0b0612);display:grid;place-items:center} +.soon b{position:static;font:700 8cqw/1 var(--disp);letter-spacing:.08em;color:#fff;text-align:center} +.duo{background:#0d1320} +.duo .cam{width:44%;height:70%;aspect-ratio:auto;border-radius:2cqw;top:15%;bottom:auto} +.duo .cam.a{left:4%;right:auto}.duo .cam.b{right:4%}.duo .cam.b::after{background:#e2574c} +.close{background:radial-gradient(circle at 50% 40%,#3b3b47,#0c0c10 70%)} +.close .obj{left:36%;top:22%;width:28%;height:52%;border-radius:2cqw;background:linear-gradient(160deg,#f6d365,#fda085);box-shadow:0 3cqw 5cqw #000a} + +/* selection on the stage */ +.selbox{position:absolute;right:3%;bottom:6%;width:19%;aspect-ratio:1;border:2px solid var(--sel);z-index:6} +.selbox i{position:absolute;width:10px;height:10px;background:#fff;border:2px solid var(--sel);border-radius:2px} +.selbox i:nth-child(1){left:-6px;top:-6px}.selbox i:nth-child(2){right:-6px;top:-6px}.selbox i:nth-child(3){left:-6px;bottom:-6px}.selbox i:nth-child(4){right:-6px;bottom:-6px} +.ctx{position:absolute;right:1%;bottom:calc(6% + 19% * 16 / 9 + 14px);z-index:7;display:flex;gap:2px;padding:4px;border-radius:10px;background:#1c1c25f2;border:1px solid var(--line);box-shadow:0 10px 30px #000a} +.ctx span{display:flex;align-items:center;gap:6px;padding:6px 9px;border-radius:7px;font:500 12px/1 var(--ui);color:var(--text);white-space:nowrap} +.ctx span.on{background:var(--sel)} +.guide{position:absolute;top:0;bottom:0;right:calc(3% + 9.5%);width:1px;background:#ff3df0;z-index:6} + +/* dock */ +.dock{grid-column:2/-1;display:flex;background:var(--p1);border-top:1px solid var(--line);min-width:0} +.scenes{flex:1;min-width:0;display:flex;align-items:center;gap:10px;padding:0 16px} +.pad{position:relative;width:124px;flex:none;border-radius:10px;background:var(--p2);border:2px solid transparent;padding:4px 4px 0} +.pad .th{height:66px;border-radius:7px;overflow:hidden} +.pad .nm{display:flex;align-items:center;justify-content:space-between;padding:7px 4px 8px;font:600 14px/1 var(--disp);letter-spacing:.06em;text-transform:uppercase;white-space:nowrap;overflow:hidden} +.pad kbd{position:absolute;right:8px;top:8px;z-index:2;background:#000a} +.pad.air{border-color:var(--air);box-shadow:0 0 24px #ff2d4640} +.pad.cued{border-color:var(--cue);box-shadow:0 0 24px #ffb02033} +.pad .tg{position:absolute;left:8px;top:8px;z-index:2;font:700 11px/16px var(--disp);letter-spacing:.12em;padding:0 5px;border-radius:3px;background:var(--air);color:#fff} +.pad.cued .tg{background:var(--cue);color:#1a1200} +.pad.add{height:100px;display:grid;place-items:center;border:2px dashed var(--line);background:transparent;color:var(--dim);width:52px} +.tr{display:flex;flex-direction:column;align-items:center;gap:6px;color:var(--dim);font:500 10.5px/1 var(--ui)} +.mix{width:360px;flex:none;border-left:1px solid var(--line);padding:10px 14px;display:flex;gap:12px} +.chs{flex:1;display:flex;flex-direction:column;gap:7px;justify-content:center} +.ch{display:flex;align-items:center;gap:8px;font:500 12px/1 var(--ui)} +.ch .n{width:74px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.led{flex:1;height:9px;border-radius:2px;background: + linear-gradient(90deg,var(--ok) 0 var(--v,60%),transparent var(--v,60%)), + repeating-linear-gradient(90deg,var(--p3) 0 5px,transparent 5px 7px); + -webkit-mask:repeating-linear-gradient(90deg,#000 0 5px,transparent 5px 7px);mask:repeating-linear-gradient(90deg,#000 0 5px,transparent 5px 7px)} +.led.hot{background:linear-gradient(90deg,var(--ok) 0 70%,var(--cue) 70% 88%,var(--air) 88% var(--v),transparent var(--v)),repeating-linear-gradient(90deg,var(--p3) 0 5px,transparent 5px 7px)} +.mute{width:26px;height:22px;border-radius:6px;background:var(--p3);display:grid;place-items:center;color:var(--text)} +.mute.off{background:var(--air)} +.master{width:64px;display:flex;flex-direction:column;align-items:center;gap:6px} +.vled{flex:1;width:22px;border-radius:3px;background:linear-gradient(0deg,var(--ok) 0 58%,var(--cue) 58% 66%,var(--p3) 66%);-webkit-mask:repeating-linear-gradient(0deg,#000 0 4px,transparent 4px 6px);mask:repeating-linear-gradient(0deg,#000 0 4px,transparent 4px 6px)} +.quick{display:flex;gap:6px;position:absolute;right:18px;bottom:8px} +.q{height:30px;padding:0 11px;border-radius:8px;background:var(--p2);border:1px solid var(--line);display:flex;align-items:center;gap:6px;font:600 13px/1 var(--disp);letter-spacing:.08em;text-transform:uppercase} +.q.panic{border-color:var(--cue);color:var(--cue)} + +/* side panels */ +.side{background:var(--p1);border-left:1px solid var(--line);display:flex;flex-direction:column;min-height:0;min-width:0} +.tabs{display:flex;gap:16px;padding:12px 14px 0;border-bottom:1px solid var(--line)} +.tabs span{padding-bottom:10px;font:600 14px/1 var(--disp);letter-spacing:.1em;text-transform:uppercase;color:var(--dim)} +.tabs span.on{color:var(--text);box-shadow:0 2px 0 var(--text)} +.msgs{flex:1;padding:12px 14px;display:flex;flex-direction:column;gap:9px;overflow:hidden;font-size:12.5px;line-height:1.35} +.msgs b{font-weight:600} +.ev{padding:8px 10px;border-radius:8px;background:var(--p2);border-left:3px solid var(--cue)} +.input{margin:0 12px 12px;height:34px;border-radius:8px;background:var(--p2);border:1px solid var(--line);display:flex;align-items:center;padding:0 10px;color:var(--faint);font-size:12px;gap:8px} +.sec{padding:12px 14px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:9px} +.f{display:grid;grid-template-columns:repeat(4,1fr);gap:6px} +.f span{height:28px;border-radius:6px;background:var(--p2);border:1px solid var(--line);display:flex;align-items:center;gap:6px;padding:0 8px;font:500 12px/1 var(--mono)} +.f span em{font:500 10px/1 var(--ui);color:var(--faint);font-style:normal} +.shape{display:grid;grid-template-columns:repeat(4,1fr);gap:6px} +.shape span{height:44px;border-radius:8px;background:var(--p2);border:1px solid var(--line);display:grid;place-items:center} +.shape span.on{border-color:var(--sel);background:#4f8cff22} +.shape i{display:block;width:22px;height:22px;background:var(--dim)} +.sl{display:flex;align-items:center;gap:10px;font-size:12px}.sl .n{width:92px;color:var(--dim)} +.sl .bar{flex:1;height:4px;border-radius:2px;background:linear-gradient(90deg,var(--sel) var(--v,40%),var(--p3) var(--v,40%));position:relative} +.sl .bar::after{content:"";position:absolute;left:var(--v,40%);top:-5px;width:14px;height:14px;margin-left:-7px;border-radius:50%;background:#fff} +.tog{width:32px;height:18px;border-radius:9px;background:var(--p3);position:relative;flex:none} +.tog::after{content:"";position:absolute;left:2px;top:2px;width:14px;height:14px;border-radius:50%;background:#fff} +.tog.on{background:var(--sel)}.tog.on::after{left:16px} + +/* layers drawer */ +.drawer{position:absolute;left:0;top:0;bottom:0;width:272px;z-index:8;background:#131319f7;border-right:1px solid var(--line);box-shadow:20px 0 50px #0008;display:flex;flex-direction:column} +.ly{display:flex;align-items:center;gap:9px;padding:7px 12px;font-size:12.5px} +.ly.on{background:#4f8cff26;box-shadow:inset 3px 0 0 var(--sel)} +.ly small{display:block;color:var(--faint);font-size:10.5px;margin-top:2px} +.ic{width:30px;height:30px;border-radius:8px;display:grid;place-items:center;flex:none;color:#fff} + +/* studio mode */ +.cuecard{position:absolute;right:26px;top:22px;width:300px;z-index:8;border-radius:12px;background:#15151cf5;border:1px solid var(--line);padding:8px;box-shadow:0 16px 40px #000a} +.cuecard .th{height:160px;border-radius:8px;outline:2px solid var(--cue);overflow:hidden;position:relative} +.take{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:8px} +.take span{height:40px;border-radius:8px;display:grid;place-items:center;font:700 18px/1 var(--disp);letter-spacing:.14em} +.take .t{background:var(--air);color:#fff}.take .a{background:var(--p3)} +.pip{position:absolute;right:26px;top:22px;width:260px;z-index:8;border-radius:10px;outline:3px solid var(--air);overflow:hidden;height:146px;box-shadow:0 16px 40px #000a} +.banner{position:absolute;left:50%;top:22px;transform:translateX(-50%);z-index:8;display:flex;align-items:center;gap:10px;padding:6px 6px 6px 14px;border-radius:10px;background:var(--cue);color:#1a1200;font:700 16px/1 var(--disp);letter-spacing:.1em} +.banner span{padding:7px 10px;border-radius:7px;background:#1a1200;color:var(--cue);font:600 12px/1 var(--ui);letter-spacing:0} + +/* layouts */ +.A{grid-template:48px 1fr 150px/60px 1fr 332px} +.A .rail{grid-row:2/4} +.B{grid-template:48px 1fr 292px/1fr 332px} +.B .dock{grid-column:1/-1;background:linear-gradient(#1b1b22,#0f0f14);padding:14px 18px;gap:18px} +.B .bank{border-radius:12px;background:#0c0c10;border:1px solid #000;box-shadow:inset 0 1px 0 #fff1,0 1px 0 #fff1;padding:12px;display:flex;flex-direction:column;gap:10px} +.keys{display:grid;grid-template-columns:repeat(5,112px);gap:8px} +.key{height:92px;border-radius:9px;position:relative;overflow:hidden;border:2px solid #000;box-shadow:inset 0 0 0 1px #fff2,0 3px 0 #000} +.key b{position:absolute;left:0;right:0;bottom:0;padding:10px 6px 5px;font:700 13px/1 var(--disp);letter-spacing:.08em;text-transform:uppercase;background:linear-gradient(transparent,#000d)} +.key.air{border-color:var(--air);box-shadow:0 0 18px var(--air),0 3px 0 #000} +.key.plain{background:linear-gradient(#2a2a34,#1b1b23);display:grid;place-items:center;color:var(--dim)} +.big{width:104px;height:92px;border-radius:9px;display:grid;place-items:center;font:700 24px/1 var(--disp);letter-spacing:.12em;border:2px solid #000;box-shadow:inset 0 1px 0 #fff3,0 3px 0 #000} +.big.cut{background:linear-gradient(#ff5167,#d81932)}.big.auto{background:linear-gradient(#3a3a46,#24242d)} +.tbar{width:54px;height:192px;border-radius:8px;background:#000;border:1px solid #fff1;position:relative} +.tbar::after{content:"";position:absolute;left:-10px;right:-10px;top:26px;height:26px;border-radius:6px;background:linear-gradient(#e9e9ee,#9a9aa6);box-shadow:0 3px 0 #000} +.faders{display:flex;gap:10px;flex:1} +.fd{width:62px;display:flex;flex-direction:column;align-items:center;gap:7px;font:600 11px/1 var(--disp);letter-spacing:.08em;text-transform:uppercase} +.fd .tr2{flex:1;display:flex;gap:6px} +.fd .slot{width:8px;border-radius:4px;background:#000;position:relative} +.fd .slot::after{content:"";position:absolute;left:-9px;top:var(--t,35%);width:26px;height:16px;border-radius:4px;background:linear-gradient(#f2f2f5,#a5a5b1);box-shadow:0 2px 0 #000} +.fd .vled{width:9px;flex:none} +.C{grid-template:48px 1fr 104px 40px/288px 1fr 332px} +.C .dock{grid-column:1/-1} +.C .pad{width:132px}.C .pad .th{height:50px}.C .pad.add{height:84px;width:52px} +.pages{grid-column:1/-1;display:flex;justify-content:center;gap:4px;align-items:center;background:#0c0c10;border-top:1px solid var(--line)} +.pages span{display:flex;align-items:center;gap:8px;padding:8px 22px;border-radius:8px;font:700 15px/1 var(--disp);letter-spacing:.14em;color:var(--dim)} +.pages span.on{background:var(--p3);color:var(--text)} +.lib{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:12px} +.lib div{height:64px;border-radius:9px;background:var(--p2);border:1px solid var(--line);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;font-size:11.5px;color:var(--dim)} + +/* v2: pages instead of a rail, one live control, nothing that is not needed every minute */ +.nav{display:flex;gap:2px;padding:3px;border-radius:10px;background:var(--p2);border:1px solid var(--line)} +.nav span{display:flex;align-items:center;gap:7px;padding:6px 14px;border-radius:7px;font:700 14px/1 var(--disp);letter-spacing:.14em;color:var(--dim)} +.nav span.on{background:var(--p3);color:var(--text)} +.ib{width:30px;height:30px;border-radius:8px;display:grid;place-items:center;color:var(--dim);flex:none;position:relative} +.ib.b{background:var(--p2);border:1px solid var(--line);color:var(--text)} +.ib.cue{color:var(--cue);border-color:var(--cue)} +.onair.end{background:#2a0b12;box-shadow:inset 0 0 0 2px var(--air);color:var(--air)} +.pop{position:absolute;z-index:20;right:118px;top:54px;width:316px;border-radius:12px;background:#17171ef7;border:1px solid var(--line);box-shadow:0 20px 50px #000b;padding:6px 14px 10px;font-size:12.5px} +.pop .r{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--line)} +.pop .r:last-child{border:0}.pop b{font:500 12px/1 var(--mono)} +.tip{position:absolute;z-index:20;padding:6px 8px;border-radius:7px;background:#000;border:1px solid var(--line);font:500 11.5px/1 var(--ui);color:var(--text);white-space:nowrap;display:flex;gap:8px;align-items:center} +.L{grid-template:48px 1fr 150px/1fr} +.L .dock,.Bd .dock{grid-column:1/-1} +.Bd{grid-template:48px 1fr 150px/272px 1fr 332px} +.Bd .side.l{border-left:0;border-right:1px solid var(--line)} +.P .vs{background:var(--p1);border-right:1px solid var(--line);padding:14px;display:grid;grid-template-columns:1fr 1fr;gap:10px;align-content:start} +.P .vs .pad{width:auto}.P .vs .pad .th{height:142px}.P .vs .pad.add{height:176px;width:auto} +.tally.port{width:min(100cqw - 12px,(100cqh - 12px)*9/16);aspect-ratio:9/16} +.P .mixcol{background:var(--p1);border-left:1px solid var(--line);display:flex;flex-direction:column;padding:14px;gap:14px} +.P .mixcol .mix{width:auto;border:0;padding:0;flex-direction:column} +.P .mixcol .master{width:auto;flex-direction:row}.P .mixcol .vled{height:10px;width:auto;flex:1;background:linear-gradient(90deg,var(--ok) 0 58%,var(--cue) 58% 66%,var(--p3) 66%);-webkit-mask:repeating-linear-gradient(90deg,#000 0 5px,transparent 5px 7px);mask:repeating-linear-gradient(90deg,#000 0 5px,transparent 5px 7px)} +.price{right:6%;top:5%;padding:1.5cqw 3cqw;border-radius:2cqw;background:#fff;color:#111;font:700 7cqw/1 var(--disp)} +.port .l3{font-size:6cqw;left:6%;bottom:5%;padding:2cqw 3cqw;border-left-width:1.2cqw}.port .l3 small{font-size:3cqw} +.port .cam{width:34%;right:6%;bottom:16%} +.K{grid-template:1fr/1fr 1fr;background:#05050a} +.K .gamewin{position:relative;margin:14px 7px 14px 14px;border-radius:10px;overflow:hidden;border:1px solid var(--line)} +.K .gamewin::before{content:"Your game";position:absolute;z-index:3;left:0;right:0;top:0;height:30px;background:#000a;font:500 12px/30px var(--ui);padding-left:12px;color:var(--dim)} +.K .win{margin:14px 14px 14px 7px;border-radius:10px;overflow:hidden;border:1px solid var(--line);display:grid;background:var(--bg);min-height:0} +.K .win .dock{grid-column:1}.K .win .scenes{padding:0 10px;gap:8px}.K .win .pad{width:112px}.K .win .pad .th{height:60px} +.K .win .mix{width:auto;border:0;border-top:1px solid var(--line)} +.K .win .top{gap:8px;padding:0 10px} +/* portrait: the pages become a sidebar, where the spare room is */ +.snav{background:var(--p1);border-right:1px solid var(--line);display:flex;flex-direction:column;align-items:center;gap:4px;padding:10px 0} +.snav span{width:52px;padding:9px 0 7px;border-radius:10px;display:flex;flex-direction:column;align-items:center;gap:5px;font:700 11px/1 var(--disp);letter-spacing:.1em;color:var(--dim)} +.snav span.on{background:var(--p3);color:var(--text)} +.P{grid-template:48px 1fr/64px 212px 1fr 292px 320px} +.K .win{grid-template:44px 1fr 118px auto/1fr} +.K .nav span{padding:6px 9px;font-size:12px} + diff --git a/quest/m0/ux/mock/kit.js b/quest/m0/ux/mock/kit.js new file mode 100644 index 00000000..b2fcffdf --- /dev/null +++ b/quest/m0/ux/mock/kit.js @@ -0,0 +1,167 @@ +// Shared pieces of the studio mock-ups: icons, fake pictures, the chrome every page repeats, +// and mount(), which turns a table of frames into a switcher. Reference only. +document.body.insertAdjacentHTML('afterbegin',String.raw` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

`); + +const T=id=>document.getElementById('t-'+id).innerHTML; +const pic=k=>T(k); +const I=(n,c='')=>``; +const NAV=p=>``; +function topbar(page,o={}){return `
+ ${o.compact?'':'
Friday Night Ranked ▾
'} + ${o.compact?'':`
${I('undo')}
${I('undo')}
`} +
${o.nav===false?'':NAV(page)}
+
${o.compact?'':'Excellent'}
+ ${o.compact?'':`
`} + ${o.end?'
END STREAM
':'
LIVE 01:24:07
'} + ${o.compact?'':`
${I('gear')}
`} +
`} +function stage(o={}){ + const k=o.pic||'game', cue=o.cue; + return `
${o.extra||''}
${cue?'OFF AIR':'ON AIR'}${pic(k)}${o.sel?'
'+I('crop','sm')+'CropFitFillShape'+I('wand','sm')+'Cut out⋯
':''}
`; +} +const quick=`
${I('shield')}BRB
${I('clip')}
${I('eyeoff')}
`; +const scenes=(studio)=>T('scenes').replace('
Fade · 300 ms ▾
',`
${I('cue')}
`); +const dock=(studio)=>`
${scenes(studio)}${quick}${T('mix')}
`; +const layersCol=()=>`
LayersAdd
${T('layers').replace('class="drawer"','style="display:contents"').replace(/
.*?<\/div>/s,'')}
`; +const snav=p=>`
${[['LIVE','cam'],['BUILD','layers'],['MIX','sliders'],['REVIEW','film']].map(([n,i])=>`${I(i)}${n}`).join('')}
`; + +function air(f,s){const p=f.querySelector(`.pad[data-s=${s}]`);if(p){p.classList.add('air');p.insertAdjacentHTML('afterbegin','ON AIR')}} +function cue(f,s){const p=f.querySelector(`.pad[data-s=${s}]`);if(p){p.classList.add('cued');p.insertAdjacentHTML('afterbegin','NEXT')}} +// V maps a frame id to {label, cls, note, html(), after(frame)}. `#shot:` shows one frame bare, for a screenshot. +function mount(V){ + const fitEl=document.getElementById('fit'),sw=document.getElementById('sw'),note=document.getElementById('note'); + for(const [k,v] of Object.entries(V)){ + const f=document.createElement('div');f.className='frame '+v.cls;f.dataset.v=k;f.innerHTML=v.html(); + f.querySelectorAll('[data-pic]').forEach(e=>e.innerHTML=pic(e.dataset.pic)); + v.after&&v.after(f);fitEl.appendChild(f); + const b=document.createElement('button');b.textContent=v.label;b.onclick=()=>show(k);b.dataset.v=k;sw.appendChild(b); + } + function show(k){document.querySelectorAll('.frame').forEach(f=>f.classList.toggle('on',f.dataset.v==k)); + sw.querySelectorAll('button').forEach(b=>b.setAttribute('aria-pressed',b.dataset.v==k));note.innerHTML=V[k].note} + function fit(){const s=fitEl.clientWidth/1440;fitEl.style.height=(900*s)+'px';document.querySelectorAll('.frame').forEach(f=>f.style.transform='scale('+s+')')} + const h=location.hash.replace('#','');if(h.startsWith('shot:'))document.body.classList.add('shot'); + const k=h.replace('shot:','');show(V[k]?k:Object.keys(V)[0]);fit();addEventListener('resize',fit); +} diff --git a/quest/m0/ux/mock/later-autolayout.webp b/quest/m0/ux/mock/later-autolayout.webp new file mode 100644 index 00000000..faeee62e Binary files /dev/null and b/quest/m0/ux/mock/later-autolayout.webp differ diff --git a/quest/m0/ux/mock/later-autoplace.webp b/quest/m0/ux/mock/later-autoplace.webp new file mode 100644 index 00000000..e980423b Binary files /dev/null and b/quest/m0/ux/mock/later-autoplace.webp differ diff --git a/quest/m0/ux/mock/later-capturehealth.webp b/quest/m0/ux/mock/later-capturehealth.webp new file mode 100644 index 00000000..0ee6969d Binary files /dev/null and b/quest/m0/ux/mock/later-capturehealth.webp differ diff --git a/quest/m0/ux/mock/later-drop.webp b/quest/m0/ux/mock/later-drop.webp new file mode 100644 index 00000000..a3fde035 Binary files /dev/null and b/quest/m0/ux/mock/later-drop.webp differ diff --git a/quest/m0/ux/mock/later-dual.webp b/quest/m0/ux/mock/later-dual.webp new file mode 100644 index 00000000..80c4f26f Binary files /dev/null and b/quest/m0/ux/mock/later-dual.webp differ diff --git a/quest/m0/ux/mock/later-mini.webp b/quest/m0/ux/mock/later-mini.webp new file mode 100644 index 00000000..faf58661 Binary files /dev/null and b/quest/m0/ux/mock/later-mini.webp differ diff --git a/quest/m0/ux/mock/later-multiview.webp b/quest/m0/ux/mock/later-multiview.webp new file mode 100644 index 00000000..b4e4be90 Binary files /dev/null and b/quest/m0/ux/mock/later-multiview.webp differ diff --git a/quest/m0/ux/mock/later-panels.webp b/quest/m0/ux/mock/later-panels.webp new file mode 100644 index 00000000..6c401d8c Binary files /dev/null and b/quest/m0/ux/mock/later-panels.webp differ diff --git a/quest/m0/ux/mock/later-shield.webp b/quest/m0/ux/mock/later-shield.webp new file mode 100644 index 00000000..72310cd2 Binary files /dev/null and b/quest/m0/ux/mock/later-shield.webp differ diff --git a/quest/m0/ux/mock/later.html b/quest/m0/ux/mock/later.html new file mode 100644 index 00000000..be08e836 --- /dev/null +++ b/quest/m0/ux/mock/later.html @@ -0,0 +1,476 @@ + + + + + +Studio, later features + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quest/m0/ux/mock/mix-mix.webp b/quest/m0/ux/mock/mix-mix.webp new file mode 100644 index 00000000..7312de82 Binary files /dev/null and b/quest/m0/ux/mock/mix-mix.webp differ diff --git a/quest/m0/ux/mock/mix-routes.webp b/quest/m0/ux/mock/mix-routes.webp new file mode 100644 index 00000000..d6841c7f Binary files /dev/null and b/quest/m0/ux/mock/mix-routes.webp differ diff --git a/quest/m0/ux/mock/mix-strip.webp b/quest/m0/ux/mock/mix-strip.webp new file mode 100644 index 00000000..8c75ff7b Binary files /dev/null and b/quest/m0/ux/mock/mix-strip.webp differ diff --git a/quest/m0/ux/mock/mix.html b/quest/m0/ux/mock/mix.html new file mode 100644 index 00000000..d336863c --- /dev/null +++ b/quest/m0/ux/mock/mix.html @@ -0,0 +1,264 @@ + + + + + +Studio MIX + + + + + + + + + + + + + + + + + + + diff --git a/quest/m0/ux/mock/render.sh b/quest/m0/ux/mock/render.sh new file mode 100755 index 00000000..2e398e72 --- /dev/null +++ b/quest/m0/ux/mock/render.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Render frames of a mock-up page to -.webp beside it, at the 1440x900 they are drawn for: +# ./render.sh shell live build portrait +set -euo pipefail +cd "$(dirname "$0")" +page=$1 +shift +for frame in "$@"; do + "${CHROME:-google-chrome}" --headless=new --disable-gpu --hide-scrollbars --window-size=1440,900 \ + --virtual-time-budget=4000 --screenshot="$PWD/$page-$frame.webp" "file://$PWD/$page.html#shot:$frame" >/dev/null 2>&1 +done diff --git a/quest/m0/ux/mock/review-review.webp b/quest/m0/ux/mock/review-review.webp new file mode 100644 index 00000000..83dfd3ec Binary files /dev/null and b/quest/m0/ux/mock/review-review.webp differ diff --git a/quest/m0/ux/mock/review.html b/quest/m0/ux/mock/review.html new file mode 100644 index 00000000..9d780e11 --- /dev/null +++ b/quest/m0/ux/mock/review.html @@ -0,0 +1,131 @@ + + + + + +Studio REVIEW + + + + + + + + + + + + + + + + + diff --git a/quest/m0/ux/mock/settings-advanced.webp b/quest/m0/ux/mock/settings-advanced.webp new file mode 100644 index 00000000..ee1d6293 Binary files /dev/null and b/quest/m0/ux/mock/settings-advanced.webp differ diff --git a/quest/m0/ux/mock/settings-hotkeys.webp b/quest/m0/ux/mock/settings-hotkeys.webp new file mode 100644 index 00000000..156a605a Binary files /dev/null and b/quest/m0/ux/mock/settings-hotkeys.webp differ diff --git a/quest/m0/ux/mock/settings-recording.webp b/quest/m0/ux/mock/settings-recording.webp new file mode 100644 index 00000000..cac9083a Binary files /dev/null and b/quest/m0/ux/mock/settings-recording.webp differ diff --git a/quest/m0/ux/mock/settings-show.webp b/quest/m0/ux/mock/settings-show.webp new file mode 100644 index 00000000..5c376db9 Binary files /dev/null and b/quest/m0/ux/mock/settings-show.webp differ diff --git a/quest/m0/ux/mock/settings-stream.webp b/quest/m0/ux/mock/settings-stream.webp new file mode 100644 index 00000000..bf74a88e Binary files /dev/null and b/quest/m0/ux/mock/settings-stream.webp differ diff --git a/quest/m0/ux/mock/settings.html b/quest/m0/ux/mock/settings.html new file mode 100644 index 00000000..37ef3c68 --- /dev/null +++ b/quest/m0/ux/mock/settings.html @@ -0,0 +1,166 @@ + + + + + +Studio settings + + + + + + + + + + + + + + + + + + + + diff --git a/quest/m0/ux/mock/shell-build.webp b/quest/m0/ux/mock/shell-build.webp new file mode 100644 index 00000000..7d0acb6e Binary files /dev/null and b/quest/m0/ux/mock/shell-build.webp differ diff --git a/quest/m0/ux/mock/shell-compact.webp b/quest/m0/ux/mock/shell-compact.webp new file mode 100644 index 00000000..bedd76be Binary files /dev/null and b/quest/m0/ux/mock/shell-compact.webp differ diff --git a/quest/m0/ux/mock/shell-hover.webp b/quest/m0/ux/mock/shell-hover.webp new file mode 100644 index 00000000..a9be2a42 Binary files /dev/null and b/quest/m0/ux/mock/shell-hover.webp differ diff --git a/quest/m0/ux/mock/shell-live.webp b/quest/m0/ux/mock/shell-live.webp new file mode 100644 index 00000000..3407e2be Binary files /dev/null and b/quest/m0/ux/mock/shell-live.webp differ diff --git a/quest/m0/ux/mock/shell-offair.webp b/quest/m0/ux/mock/shell-offair.webp new file mode 100644 index 00000000..eeb89e4d Binary files /dev/null and b/quest/m0/ux/mock/shell-offair.webp differ diff --git a/quest/m0/ux/mock/shell-portrait.webp b/quest/m0/ux/mock/shell-portrait.webp new file mode 100644 index 00000000..8692a25a Binary files /dev/null and b/quest/m0/ux/mock/shell-portrait.webp differ diff --git a/quest/m0/ux/mock/shell-studio.webp b/quest/m0/ux/mock/shell-studio.webp new file mode 100644 index 00000000..b1cbd4cd Binary files /dev/null and b/quest/m0/ux/mock/shell-studio.webp differ diff --git a/quest/m0/ux/mock/shell.html b/quest/m0/ux/mock/shell.html new file mode 100644 index 00000000..7990ef1d --- /dev/null +++ b/quest/m0/ux/mock/shell.html @@ -0,0 +1,33 @@ + + + + + +Studio shell + + + + + + + + + diff --git a/quest/m0/ux/review.md b/quest/m0/ux/review.md new file mode 100644 index 00000000..bb75a0a5 --- /dev/null +++ b/quest/m0/ux/review.md @@ -0,0 +1,46 @@ +# [M] The REVIEW page + +## Goal + +The REVIEW page of [`mock/review.html`](/quest/m0/ux/mock/review.html) +replaces the Replay panel and the recording half of the Output panel: the +last 30 seconds as a strip to pick a stretch from, what to replay (the +program or a source), the speed, and Replay and Save clip; beside it, this +show's recordings and clips as cards with a picture, a length, and a size, +one that was repaired after a crash marked as such, and a button that opens +their folder. + +The bottom strip stays, and the program's card in "replay what" is the live +picture, so looking back never hides what is on air. + +The title bar's record button starts and stops a recording from any page and +shows its clock while recording. + +## Plan + +Recording and repair behave as `README.md` has them; this is their place and +look. Recommended, not decided: Replay and Save clip take any stretch of the +last 30 seconds, picked with two handles, where today they take the last N +seconds up to the click. The track already holds the 30 seconds. The +alternative pins the right handle to now. The strip's pictures are frames the program's track +already keeps for late viewers, decoded at a low rate while the page is +shown, and nothing records in the background for it. + +- The large picture is the frame under the scrub head, so the choice is + seen before it is replayed. The program's card keeps its ON AIR tag, so the + live picture never leaves the screen. +- A replay that cannot play says why where the button is: off air with no + relay, a source with no past, a speed that drops the sound. +- A recording's card comes from the files in the recordings folder that + carry this show's name. Reading their length and a first picture must not + hold up the page. +- Which picture and audio scene a recording holds are Settings, Recording. + +[Markers and clips](/quest/m2/markers.md) later adds markers to the strip +and chapters to a recording. + +Human check: scrubbing and picking a stretch by feel. + +## Required + +- [One live picture](/quest/m0/ux/live.md) - the page and the record button diff --git a/quest/m0/ux/scene-thumbs.md b/quest/m0/ux/scene-thumbs.md new file mode 100644 index 00000000..617490ae --- /dev/null +++ b/quest/m0/ux/scene-thumbs.md @@ -0,0 +1,36 @@ +# [M] A small live picture of every scene and source + +## Goal + +moq-scene draws a thumbnail of any scene or source on request: a small +texture on the compositor's device, refreshed a few times a second, and only +while something shows it. The studio's Scenes panel shows one beside each +scene. The cost of a strip of them is measured. + +## Plan + +The scene pads, the Add page's cards, and the multiview all show live +pictures of things that are not on air (see +[the LIVE frame](/quest/m0/ux/mock/shell-live.webp)). Today only the program +and one preview are drawn (`Compositor` in +`rs/moq-scene/src/compositor.rs`), and a scene nobody watches costs nothing. +Keep that: a thumbnail nobody shows is not drawn, as the preview is skipped +when no tab shows it. + +- A thumbnail is a scene at its own shape, scaled down, drawn at a low rate + on the program clock. A source's is its latest frame through its own crop + and filters. +- A scene that is on air is not drawn twice: its thumbnail samples the + program. +- A capture that is not otherwise in use is not opened for a thumbnail. A + source with no frame yet shows nothing, and the UI draws its icon instead. + Live pictures of devices that are not sources yet are + [the capture listing's](/quest/m0/pin-capture-list.md). +- Thumbnails never reach the program, a published scene, or a recording. + +Measure, per AGENTS.md: frame time and GPU copies with zero, six, and thirty +thumbnails shown, and that the program's render time does not move. Settle the +size and rate from that. + +It does not wait on the [toolkit spike](/quest/m0/ux/spike.md): the texture is +toolkit-free, and today's Scenes panel is its first consumer. diff --git a/quest/m0/ux/settings.md b/quest/m0/ux/settings.md new file mode 100644 index 00000000..e1694e76 --- /dev/null +++ b/quest/m0/ux/settings.md @@ -0,0 +1,49 @@ +# [M] Settings + +## Goal + +Everything a broadcaster sets once is in one Settings window, opened from the +title bar's gear, as in +[`mock/settings.html`](/quest/m0/ux/mock/settings.html): + +- **Show:** landscape or portrait, the canvas size and frame rate as a few + named choices with the numbers behind Custom, and per-scene sizes behind a + disclosure. +- **Stream:** the relay, failover, and options from + [go live](/quest/m0/ux/go-live.md). +- **Recording:** the folder, what a recording holds, and each source too. +- **Hotkeys:** today's Hotkeys panel, on keycaps. +- **Guests:** the room, its key, the join page, and how long a link lasts. +- **Advanced:** publishing a scene as its own broadcast, publishing a source + independently, the virtual camera, the scene graph, the effects level. + +It replaces the Output and Hotkeys panels, the View menu, and the flags that +have no other home. The File and Edit menus go too: reload, undo, redo, copy, +and paste keep their keys and are reachable from the show's menu in the title +bar. + +## Plan + +- The mock-up lists a Sound pane. Its one setting, the output Listen plays + on, is on the [MIX page](/quest/m0/ux/mix.md) instead, so there is none. + The Guests pane has no frame; it follows the Stream pane's shape. +- Each setting keeps its home on disk: the scene file for what belongs to + the show (canvas, scene sizes, published scenes), per-user settings for + what belongs to the operator (hotkeys, snapping, effects, the listen + output), and the per-show file from go live for where it streams. +- A setting with a cost says it in one line where it is set: an encoder + session per published scene, the bitrate a canvas needs, a source leaving + the program when published independently. +- Encoder sessions, encode times, and the broadcast path, which the Output + panel shows today, are the health dot's details, not settings. +- A change that restarts an encoder or a track, such as a new canvas size, + says so before it applies while on air. +- The scene graph (`rs/moq-studio/src/ui/graph.rs`) opens from Advanced in a + window of its own. + +Human check: whether each setting is where a streamer would look for it, and +the words on each pane. + +## Required + +- [Going live from inside the app](/quest/m0/ux/go-live.md) - the Stream pane's settings and where they are saved diff --git a/quest/m0/ux/shots.md b/quest/m0/ux/shots.md new file mode 100644 index 00000000..0a306499 --- /dev/null +++ b/quest/m0/ux/shots.md @@ -0,0 +1,44 @@ +# [M] UI screenshots on demand + +## Goal + +One command renders named views of the studio to PNG files with no display: +each page, and each state a quest names, on a scene file kept in the +repository. A view is picked by name, never by clicking at coordinates, and +two runs of a still view give the same picture. Every UI pull request shows +its screenshots beside the mock-up frames it implements, and CI keeps the +command working. + +## Plan + +The UI has no screenshot or snapshot test today. /quest-plan built a throwaway +harness on 2026-11-09 that proves the shape and shows what is missing: + +- It ran the studio in a private headless sway and captured with grim, driving + a hand-written virtual pointer over the Wayland wire protocol, because the + layout is fixed in `Studio::new` and a panel can only be reached by clicking + its tab. +- The dev shell has no sway, grim, or input tool. Outside NixOS, Nix-built + sway cannot find the host's GL and Vulkan drivers without four environment + variables. +- Meters, clocks, and playing clips never settle, so a capture catches an + arbitrary moment. +- One launch in ten crashed; see + [The studio starts every time](/quest/m0/ux/surface-crash.md). + +So the app takes the view to show as an argument or an environment variable, +and runs on a paused `kio::clock` with still sources for a screenshot. Which +mechanism draws the picture follows the +[toolkit spike](/quest/m0/ux/spike.md): a headless render inside the toolkit +where it has one, a headless compositor otherwise. Either way the views are +named in one table that quests add to. + +AGENTS.md's rule stands: frame time is measured under a real compositor. +Screenshots are for looking, not timing. + +Human check: none. The maintainer reads the pictures in later PRs. + +## Required + +- [Toolkit spike](/quest/m0/ux/spike.md) - which toolkit draws the views, and what its headless run can do +- [The studio starts every time](/quest/m0/ux/surface-crash.md) - a launch that can crash cannot run in CI diff --git a/quest/m0/ux/spike.md b/quest/m0/ux/spike.md new file mode 100644 index 00000000..f13e0a10 --- /dev/null +++ b/quest/m0/ux/spike.md @@ -0,0 +1,88 @@ +# [L] Toolkit spike for the redesign + +## Goal + +The LIVE page of the [mock-up](/quest/m0/ux/mock/shell-live.webp) runs twice, +once on egui and once on [Slint](https://slint.dev): the title bar with its +pages, health dot, and live control, the live picture in its tally frame, the +scene pads, and the sound strip, on the real compositor. Its PR reports what +each cost and recommends one, and the maintainer picks the toolkit every other +quest in this epic builds on. + +## Plan + +Decided 2026-11-09 (maintainer, /quest-plan): + +- ✅ **Spike before any page is built** (rejected: staying on egui unasked; + moving unasked). +- ✅ **egui and Slint** (rejected: egui and Rune, the first pick; all three; + egui, Slint, and zgui). A survey of twelve Rust toolkits the same day put + one requirement first: drawing the compositor's own wgpu textures with no + copy, on the device the studio opens, which carries an external-semaphore + extension only an app-created device can have + (`moq/rs/moq-video/src/render/device.rs`). On wgpu 30, egui passes + (`WgpuSetup::Existing`, `register_native_texture`) and so does Slint + (`WGPUConfiguration::Manual`, an `Image` from a `wgpu::Texture`). Nothing + else surveyed does. + +The others, on paper only; the report says what changed for each since: + +- **Rune** was spiked on 2026-11-07, + [moq-dev/studio#98](https://github.com/moq-dev/studio/pull/98), which said + stay on egui. The redesign has no dock, one of its costs there. The rest + stand: it cannot take the studio's device without a patch, imports no + texture without a copy on Linux, takes no pull requests, and has no + release. +- **zgui** is built for this, with a device taken from the app and a surface + element, but is on wgpu 29 and is a 0.1.0. Look again when it reaches + wgpu 30. +- **GPUI, Xilem, Floem, Freya, Vizia, Makepad, Iced, and Blitz** fail the + texture requirement or meet it only on an unreleased branch or their own + device. + +[A critique of GPUI](https://blog.zortax.de/posts/a-critique-of-gpui) prompted +the survey. Its case against immediate mode is cost that follows the size of +the UI rather than the change. That weighs little here: this window repaints +every frame for live video and meters, and its lists are tens of rows. What it +does raise is tested below. + +Both halves stay off `main`. The PR deletes this quest and links both +branches. + +The report compares, on the same scene and host: + +- **Fidelity:** each half's screenshot beside the mock-up frame, and what + could not be matched. Count what each toolkit lacks and the studio would + own: the pads, the segmented meters at the display's rate, popovers and + tooltips, a custom title bar with the window's drag region and controls on + each desktop, text in the three faces. Slint has no paint callback in Rust, + so say what a meter and a fader cost in its language. +- **Layout:** the centred pages in the title bar, a popover placed against + its anchor, and a grid of cards, each right on its first frame. Immediate + mode places a thing before it knows its size; say what each case took. +- **State:** how the engine's scene graph reaches the UI. egui reads it each + frame; Slint needs it pushed into properties. Count the glue, and what a + missed update looks like. +- **Frame time**, measured as AGENTS.md requires (a real compositor, not + Xvfb), with accessibility on and off, and that the program's render time + is unchanged. +- **Windows:** a second native window sharing the compositor's textures, + which [multiview](/quest/m1/multiview.md), the + [mini controller](/quest/m1/mini-controller.md), and + [panels](/quest/m1/panels.md) need, and an always-on-top borderless one. +- **Testing:** what a headless run can assert and screenshot, which decides + how [screenshots](/quest/m0/ux/shots.md) work. +- **Risk:** what each asks of upstream and what a year of maintenance looks + like. For Slint: its wgpu feature is marked unstable and outside its + version promise, and its license is GPLv3, royalty-free with attribution, + or commercial, where the studio is MIT or Apache-2.0. Say plainly what each + license would require of the studio and of anyone who redistributes it. + +No fixed pass bar: the maintainer decides from the report. Hand the PR back +with that decision under `## Waiting`, and record the answer in +[the epic](/quest/m0/ux/README.md) and m0's README, whose 2026-10-22 entry +chose egui with egui_dock. + +## Related + +- [The look](/quest/m0/ux/kit.md) - the first quest the choice unblocks diff --git a/quest/m0/ux/studio-mode.md b/quest/m0/ux/studio-mode.md new file mode 100644 index 00000000..c3d93a82 --- /dev/null +++ b/quest/m0/ux/studio-mode.md @@ -0,0 +1,40 @@ +# [M] Editing off air, and Studio mode + +## Goal + +Any scene can be opened for editing without going on air: the frame turns +amber, the tag reads OFF AIR, the live picture shrinks to a corner, and one +button takes the scene live or returns to the live one. See the +[off-air frame](/quest/m0/ux/mock/shell-offair.webp). + +Studio mode, off by default, uses the same staging for a show run by an +operator. A switch at the end of the scene strip turns it on, and then a +scene click cues instead of taking: the pad turns amber, a NEXT monitor docks +on the picture, and TAKE or AUTO puts the cued scene on air, as in the +[Studio mode frame](/quest/m0/ux/mock/shell-studio.webp). + +## Plan + +Decided 2026-11-09 (maintainer, /quest-plan): ✅ cue and take (rejected: a +Preview and Program split, which halves the live picture and is what the +studio had; no mode at all, leaving only off-air editing). + +- Both are the compositor's existing preview scene: off-air editing shows it + large with the program small, and Studio mode shows it small with the + program large. One mechanism, two arrangements. +- With Studio mode on, a scene's hotkey cues it, and the Cut and Transition + actions are TAKE and AUTO. With it off, a scene's hotkey takes it. The + Hotkeys settings say which, and the switch itself can be bound. +- Studio mode is kept per user with the other editor settings, not in the + scene file: it is how one operator works, not part of the show. +- The NEXT monitor never covers the same corner as the item being dragged; + it moves to the opposite one. +- Keyframes and overlays restart in the cued scene as they do today when a + scene goes into preview, so NEXT shows the entrance the audience will see. + +Human check: an operator's judgment that cue, TAKE, and AUTO feel like a +switcher, on a real show with several scenes. + +## Required + +- [One live picture](/quest/m0/ux/live.md) - the stage, the pads, and the tally frame this extends diff --git a/quest/m0/ux/surface-crash.md b/quest/m0/ux/surface-crash.md new file mode 100644 index 00000000..0008fa47 --- /dev/null +++ b/quest/m0/ux/surface-crash.md @@ -0,0 +1,32 @@ +# [S] The studio starts every time + +## Goal + +`moq-studio` no longer dies at startup with `wgpu error: Validation Error ... +In Surface::configure: Failed to wait for GPU to come idle before +reconfiguring the Surface`, followed by a panic. A test or a scripted run +launches it enough times, on a hardware GPU and on lavapipe, to show the crash +is gone. + +## Plan + +Found 2026-11-09 while taking screenshots for /quest-plan: 1 of about 11 +launches under headless sway on radv, and 2 of 3 on lavapipe, each on the +launch that opens a scene file. Reproduce it first. The trigger is a guess to +confirm or discard: the compositor resizes the window right after it maps, +while the studio's own render thread keeps the shared device busy, so +eframe's reconfigure cannot get the device idle. + +Fix the cause, not the symptom: no retry around `configure`. The compositor +and the UI share one device (`engine::Gpu`, handed to eframe as +`WgpuSetup::Existing` in `rs/moq-studio/src/main.rs`), so the fix likely +orders the engine's submissions against the surface's reconfigure. + +No single PR caused it: the shared device is as old as the app. It does not +wait on the [toolkit spike](/quest/m0/ux/spike.md), since it is today's +studio that crashes, but say in the PR whether the fix would survive a move +off eframe. + +## Related + +- [UI screenshots](/quest/m0/ux/shots.md) - cannot run routinely while a launch can crash diff --git a/quest/m0/wayland-sources.md b/quest/m0/wayland-sources.md index 75699ce1..bb51fa69 100644 --- a/quest/m0/wayland-sources.md +++ b/quest/m0/wayland-sources.md @@ -29,3 +29,7 @@ X11-only. ## Required - [The persistable portal restore token is pinned](/quest/m0/pin-portal-token.md) - the token this quest stores + +## Related + +- [Adding a source by picking a picture](/quest/m0/ux/add.md) - the Add page whose Apps and Screens sections open this picker diff --git a/quest/m1/README.md b/quest/m1/README.md index 642d9f48..be6d776e 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -2,8 +2,9 @@ ## Goal -The work held until m0 ships: studio features planned as follow-ups of m0 -work but not needed to ship it. Nothing is planned here now. +The work held until m0 ships: what the redesigned studio gains that no +streaming app has today, and the shortest way in for someone leaving OBS. +Each builds on [Broadcaster UX](/quest/m0/ux/README.md). ## Plan @@ -23,3 +24,27 @@ was m2 then and shipped in Broadened 2026-11-04 (maintainer, /quest-plan): m1 also holds studio features planned as follow-ups of m0 work but not needed to ship it, rather than opening an m2 for them. + +Planned 2026-11-09 (maintainer, /quest-plan): redesigning what the studio +already does is m0, and the features below, which do not exist yet, are here +(rejected: all in m0; all in m1). Three more were held for +[m2](/quest/m2/README.md): a phone as a remote, markers and clips, and a +command palette. Dropped, not planned: a rundown that fills overlays, +one-click looks and a brand kit, active-speaker layouts, control surfaces, +and a rehearsal mode. + +## Required + +- [A camera that keeps out of the way](/quest/m1/auto-place.md) - a layer moves off what matters in the picture, decided on the machine +- [Scenes that lay themselves out](/quest/m1/auto-layout.md) - slots and roles that reflow when a guest joins +- [A capture that went black says so](/quest/m1/capture-health.md) - a badge and the fix on the layer +- [A mini controller for one monitor](/quest/m1/mini-controller.md) - an always-on-top bar and a tray item, never on stream +- [The studio hides itself and private apps from capture](/quest/m1/privacy-shield.md) - its own windows and marked apps left out of display capture +- [A source that follows the game](/quest/m1/follow-game.md) - whichever app is in front, without picking again +- [Drop anything on the picture](/quest/m1/drop.md) - a dragged file or a paste becomes a layer +- [Opening an OBS scene collection](/quest/m1/obs-import.md) - a show made from an OBS collection, with a report +- [A multiview window](/quest/m1/multiview.md) - every scene and source on a second monitor, with tally +- [Private panels](/quest/m1/panels.md) - chat and dashboards as windows that are never on stream +- [Landscape and portrait at once](/quest/m1/dual-format.md) - both formats from one show +- [moq-video's foreground app is pinned](/quest/m1/pin-capture-foreground.md) - which app is in front, for following the game +- [moq-video's capture exclusion is pinned](/quest/m1/pin-capture-exclude.md) - leaving chosen apps out of a display capture diff --git a/quest/m1/auto-layout.md b/quest/m1/auto-layout.md new file mode 100644 index 00000000..68ed501f --- /dev/null +++ b/quest/m1/auto-layout.md @@ -0,0 +1,41 @@ +# [L] Scenes that lay themselves out + +## Goal + +A scene can be built from slots instead of fixed rectangles: a main slot, a +camera slot, and a row or grid of guest slots. A slot holds whatever fills +its role, and the scene reflows with a glide when a guest joins, a guest +leaves, or a source goes away. The `autolayout` frame of +[`mock/later.html`](/quest/m0/ux/mock/later.html) shows one scene before and +after a second guest joins. + +A show names its roles once (which camera is the main camera, which window is +the main picture), so a scene or a template built from roles survives a +change of hardware. + +## Plan + +- A slot layout is a new way to place a scene's items, beside today's `rect`, + in moq-scene and the scene file. A scene mixes both: a logo stays where it + was put while the guests reflow. Settle the file's shape here, keeping it + hand-editable. +- Roles map names to sources per show. An unfilled role draws nothing and + its slot closes up. +- A guest who joins fills the next guest slot of the scene on air only if + the show says guests appear on their own; otherwise they wait in Add, as + today. +- The reflow is a Move transition between the two arrangements, on the + program clock, so the stream and a recording show the same frames. +- BUILD shows slots as outlines with their role names, and dragging a layer + onto a slot assigns it. + +Human check: whether the reflow reads as intended rather than as a glitch, +with real guests joining mid-show. + +## Required + +- [The BUILD page](/quest/m0/ux/build.md) - where slots are drawn and filled + +## Related + +- [First run](/quest/m0/ux/first-run.md) - its starting layouts become role-based templates diff --git a/quest/m1/auto-place.md b/quest/m1/auto-place.md new file mode 100644 index 00000000..714e304e --- /dev/null +++ b/quest/m1/auto-place.md @@ -0,0 +1,55 @@ +# [L] A camera that keeps out of the way + +## Goal + +A layer with **Keep out of the way** on moves itself so it does not cover what +matters in the picture under it: a game's minimap, a slide's text, the window +the streamer is working in. It chooses among a few slots along the edges, +moves only when the content changes, and glides there. The `autoplace` frame +of [`mock/later.html`](/quest/m0/ux/mock/later.html) shows the slots, what it +saw, and the move. + +The decision is made on the machine. Nothing is uploaded. + +## Plan + +Decided 2026-11-09 (maintainer, /quest-plan): ✅ local only (rejected: a +hosted decision model asked on each content change, cheap but it uploads +thumbnails of the streamer's screen and its image input is not production +ready; local first with a hosted model as an opt-in tie-breaker). + +- **What it reads:** the composited picture under the layer, scaled far down + on the GPU a few times a second: where it moves, where it is dense with + edges and text, and what stays put while the rest moves, which is a HUD. + The inspector says which of these it stays clear of, and each can be + switched off. It reads neither the pointer nor the focused window, since + moq reports neither a pointer nor a window's place on its display. +- **What it decides:** the slot whose area matters least, among the corners + and edge middles that fit the layer, inside the canvas's margins. +- **When it moves:** when the scene changes, when the content under it + changes kind (another layout, or another app in front where + [moq reports it](/quest/m1/pin-capture-foreground.md)), or when its slot has been the + worst for a sustained time. Never on a single busy frame, and never while + the streamer is dragging it. A manual move wins and holds until the content + changes. +- **Memory:** the slot chosen for an app is remembered per user, so a known + game is right from its first frame. +- **Where it lives:** the analysis and the choice are UI-free, in moq-scene, + tested on recorded frames with a paused clock. The move is the item's + rect animated as a Move transition eases. + +Measure, per AGENTS.md: the frame time and GPU copies the analysis adds, and +that the encoder's load does not move. Should the signals prove too weak, a +small on-device model is a later quest, not a hosted one. + +Human check: living with it for a few streams. Whether it moves too often is +a judgment no test makes. + +## Required + +- [The BUILD page](/quest/m0/ux/build.md) - the inspector's switch and the layer it moves + +## Related + +- [moq-video's foreground app is pinned](/quest/m1/pin-capture-foreground.md) - when the app in front changes, and the key its slot is remembered under +- [Scenes that lay themselves out](/quest/m1/auto-layout.md) - slots chosen by role rather than by content diff --git a/quest/m1/capture-health.md b/quest/m1/capture-health.md new file mode 100644 index 00000000..a87a2059 --- /dev/null +++ b/quest/m1/capture-health.md @@ -0,0 +1,34 @@ +# [M] A capture that went black says so + +## Goal + +When a camera, window, or display source goes black or stops sending +pictures, its layer carries an amber badge on the picture and in Layers, and +a popover says what happened in plain words and offers the fix: pick the +window again, capture the whole screen instead, or reopen the device. The +health dot counts it. See the `capturehealth` frame of +[`mock/later.html`](/quest/m0/ux/mock/later.html). + +## Plan + +A black capture is the commonest complaint about OBS, where the picture is +simply black and the cause is a forum search away. + +- A stalled camera or broadcast is already detected (no new picture for a + second). A window or display that stands still is not a fault, so those + are judged by content: a picture that is all one dark color for a + sustained time while its source claims to be live. +- The check reads a far smaller copy of the frame at a low rate, on the GPU. + Measure its cost per source. +- Each fix is an action the studio can take itself; where it cannot (a + permission was revoked, a game refuses capture), the popover says what the + streamer must do, and where. +- Recommended, not decided, what viewers see meanwhile: the last good + picture for a few seconds, then the black capture as it is, so a hiccup is + invisible but a stale frame never passes for live. The alternatives are + black at once, as today, or a placeholder the show chooses. +- A source that is meant to be black can dismiss the badge for good. + +## Required + +- [The BUILD page](/quest/m0/ux/build.md) - the layer rows and the on-picture badge diff --git a/quest/m1/drop.md b/quest/m1/drop.md new file mode 100644 index 00000000..ea1d61cf --- /dev/null +++ b/quest/m1/drop.md @@ -0,0 +1,29 @@ +# [M] Drop anything on the picture + +## Goal + +A file dragged from a file manager onto the picture becomes the right kind +of layer where it was dropped: an image, a video or sound file, a color +grade. A pasted image or web address does the same at the pointer. While the +drag is over the window, the picture shows where the layer will land and +what it will become. See the `drop` frame of +[`mock/later.html`](/quest/m0/ux/mock/later.html). + +Nothing accepts a drop today. + +## Plan + +- A dropped or pasted file is copied beside the scene file, since sources + name files relative to it, unless it is already under that directory. +- A file the studio cannot play says why where it was dropped, with the fix + it has today: a regular MP4 names the remux that makes it readable. +- A pasted image has no file, so it is saved as a PNG beside the scene file. +- The windowing library reports dropped files only. Settle here whether a + web address dragged from a browser is worth a hook per platform; paste + covers it meanwhile. +- A drop on a layer that takes a file, such as an image layer, offers to + replace its file instead. + +## Required + +- [The BUILD page](/quest/m0/ux/build.md) - the picture's drop targets and the layers it makes diff --git a/quest/m1/dual-format.md b/quest/m1/dual-format.md new file mode 100644 index 00000000..69905d61 --- /dev/null +++ b/quest/m1/dual-format.md @@ -0,0 +1,40 @@ +# [L] Landscape and portrait at once + +## Goal + +A show can go out in both formats together. Each scene has a landscape and a +portrait arrangement of the same layers, the LIVE page shows the two +pictures side by side, each in its tally frame, and a scene pad takes both. +A layer says where it is shown: both, landscape, or portrait. The go-live +sheet sends each destination one format. See the `dual` frame of +[`mock/later.html`](/quest/m0/ux/mock/later.html). + +## Plan + +Decided 2026-11-09 (maintainer, /quest-plan): ✅ one format per show in m0 +and both at once here (rejected: designing every m0 page for two pictures; +never planning it). + +The engine can already do it the long way: a scene has its own size, can be +published as a broadcast of its own, and can draw the program through a +program source, cropped. This quest makes that one switch. + +- Settle the model here: one scene with an arrangement per format, or two + scenes linked by name. The first keeps a rename, a reorder, and a take from + ever going out of step. +- A portrait arrangement starts from the landscape one: the main picture + cropped around its subject, the camera stacked under it. It is then the + streamer's to move. +- Each format is its own encode, so it costs an encoder session and its + bitrate; the checklist says whether the machine and the uplink carry both. +- Where a layer is shown is said under the pictures and in the inspector, + never on the layer, so nothing covers either picture. +- The pages follow: BUILD edits one arrangement at a time with the other + beside it, and Studio mode cues both. + +Human check: a phone and a desktop viewer watching the same show. + +## Required + +- [The BUILD page](/quest/m0/ux/build.md) - editing an arrangement per format +- [Going live from inside the app](/quest/m0/ux/go-live.md) - a format per destination diff --git a/quest/m1/follow-game.md b/quest/m1/follow-game.md new file mode 100644 index 00000000..60e2e9dc --- /dev/null +++ b/quest/m1/follow-game.md @@ -0,0 +1,30 @@ +# [M] A source that follows the game + +## Goal + +Add offers a source that shows whichever game or app is in front, rather +than one fixed window. Nothing needs picking again when the game restarts, +opens a new window, or the streamer switches to another. The studio itself +and [private apps](/quest/m1/privacy-shield.md) are never followed: the +source holds its last picture, or a card the show chooses, while one of them +is in front. + +## Plan + +OBS users ask for this by name, and get it from a plugin or another app. + +- The foreground app and its window come from moq-video, through + [its pin](/quest/m1/pin-capture-foreground.md). The source is a new kind + in moq-scene beside `window`. +- It can be narrowed from any app to a list of them. +- A switch between apps is a cut inside the source, keeping the layer's + place and fitting the new window by the layer's fit rule. +- Where the desktop does not report the foreground app, the card says so and + cannot be added. + +Human check: real games, a restart mid-stream, and alt-tabbing fast. + +## Required + +- [Adding a source by picking a picture](/quest/m0/ux/add.md) - the card that adds it +- [moq-video's foreground app is pinned](/quest/m1/pin-capture-foreground.md) - which app and window are in front diff --git a/quest/m1/mini-controller.md b/quest/m1/mini-controller.md new file mode 100644 index 00000000..b9ec233e --- /dev/null +++ b/quest/m1/mini-controller.md @@ -0,0 +1,32 @@ +# [M] A mini controller for one monitor + +## Goal + +A streamer with one monitor keeps a tiny bar over their game: the first few +scene pads, the microphone's mute, the live clock, the health dot, and the +panic key, always on top and never on stream. A tray or menu-bar item shows +whether the studio is live and offers the same actions. See the `mini` frame +of [`mock/later.html`](/quest/m0/ux/mock/later.html). + +## Plan + +- A second native window, borderless and always on top, drawing the same + widgets as the LIVE page from the same state. Closing the main window to + the tray leaves the bar and the stream running. +- It is hidden from capture by + [the privacy shield](/quest/m1/privacy-shield.md), so a display capture + never shows it. +- Where the desktop gives an app no way to stay on top, as on most Wayland + compositors, the bar is not offered and Settings says why. Decide support + per desktop at startup; never open a bar that will sink behind the game. +- It takes no keyboard focus from the game. Hotkeys remain the way to act + without the mouse. + +Human check: over a real full-screen game on each desktop, including +exclusive full screen on Windows, and that a third-party recorder does not +see the bar. + +## Required + +- [One live picture](/quest/m0/ux/live.md) - the widgets and state the bar repeats +- [The studio hides itself and private apps from capture](/quest/m1/privacy-shield.md) - the bar must not appear on stream diff --git a/quest/m1/multiview.md b/quest/m1/multiview.md new file mode 100644 index 00000000..75404635 --- /dev/null +++ b/quest/m1/multiview.md @@ -0,0 +1,31 @@ +# [M] A multiview window + +## Goal + +A second window, for another monitor, shows every scene and chosen sources +in a grid of live pictures with their names, the scene on air framed red and +the cued one amber, and a clock. A click cues or takes, as a scene pad does. +See the `multiview` frame of +[`mock/later.html`](/quest/m0/ux/mock/later.html). + +## Plan + +It is the operator's view a broadcast studio expects beside Studio mode. + +- The tiles are [scene thumbnails](/quest/m0/ux/scene-thumbs.md) at a larger + size and the program's own rate for the tiles on air and cued, a lower one + for the rest. Measure the cost of a 4x4 grid. +- The grid is uniform, since the main window already shows the live picture + large. A source the live picture shows carries a red dot, never a frame: + the red frame is the scene on air alone. +- It goes full screen on the monitor it is on, and its place is remembered + per user. +- It is the studio's own window, so + [the privacy shield](/quest/m1/privacy-shield.md) keeps it out of capture. + +Human check: on a real second monitor, at a distance. + +## Required + +- [Editing off air, and Studio mode](/quest/m0/ux/studio-mode.md) - cue, take, and the amber and red it shows +- [A small live picture of every scene and source](/quest/m0/ux/scene-thumbs.md) - the tiles diff --git a/quest/m1/obs-import.md b/quest/m1/obs-import.md new file mode 100644 index 00000000..56ef4d9e --- /dev/null +++ b/quest/m1/obs-import.md @@ -0,0 +1,39 @@ +# [L] Opening an OBS scene collection + +## Goal + +The show library opens an OBS scene collection and makes a show from it: +the same scenes, sources, positions, crops, order, visibility, volumes, +mutes, and sync offsets, with the filters and transitions the studio has +equivalents for. A report lists what was carried over, what was +approximated, and what was left out and why. + +## Plan + +The studio is for creators who rely on OBS today, and nobody rebuilds +twenty scenes to try a new tool. + +- The converter is UI-free, in moq-scene, from OBS's collection JSON to a + `Collection`, tested on collections kept in the repository. +- It maps what has an equivalent: capture, image, media, color, browser, and + scene sources; text to an overlay; chroma key, color correction, LUT, crop, + and mask filters; fade, cut, and stinger transitions. +- A device rarely keeps its id between the two apps, so a device source it + cannot match arrives unpicked, and Add shows it as missing with its old + name until one is chosen. +- Anything else is named in the report rather than dropped silently: plugin + sources and filters, scripts, and audio monitoring that has no audio scene + to map to. +- It reads the file and never writes OBS's. + +## Waiting + +- Maintainer: gather real OBS collections to test against, from your own + setups and from creators willing to share one, and put them where the + quest can read them. An agent can write synthetic ones but not the mess + real ones hold. + +## Required + +- [Going live from inside the app](/quest/m0/ux/go-live.md) - the show library it opens from +- [Adding a source by picking a picture](/quest/m0/ux/add.md) - where a missing device is picked again diff --git a/quest/m1/panels.md b/quest/m1/panels.md new file mode 100644 index 00000000..9862b861 --- /dev/null +++ b/quest/m1/panels.md @@ -0,0 +1,44 @@ +# [L] Private panels + +## Goal + +Any web page can be kept beside the show as a private panel: a chat, an +activity feed, a seller's dashboard. A panel is its own window, which can sit +on another monitor or stay on top, and it can be docked into spare room +beside the picture. It is never on stream: a panel cannot be placed in a +scene, and its window is hidden from capture. See the `panels` frame of +[`mock/later.html`](/quest/m0/ux/mock/later.html). + +## Plan + +Decided 2026-11-09 (maintainer, /quest-plan): ✅ its own window, dockable, +nothing docked by default (rejected: a chat column in the main window, which +puts chat beside the streamer's own face all show; leaving chat to the +browser). This is how the studio meets platforms without naming one: the +page is the platform's own. + +- A panel is rendered by the browser the studio already ships for browser + sources, with the same profile, so a login made once holds for both. It + takes the mouse and keyboard as **Interact** does. +- Panels are the operator's, kept per user with a name and an address, not + in the scene file. +- Docking uses the room the layout has left after the picture, the scenes, + and the sound, and is not offered when there is none. +- The panel's window is hidden from capture by + [the privacy shield](/quest/m1/privacy-shield.md). +- A page can do anything the browser can, as a browser source can: the same + warning to add only pages you trust applies, and is shown when adding one. + +Browser sources exist on Linux only today, so panels arrive on macOS and +Windows with them. + +Human check: a real chat page, logged in, typed into during a stream. + +## Required + +- [One live picture](/quest/m0/ux/live.md) - the layout a panel docks into +- [The studio hides itself and private apps from capture](/quest/m1/privacy-shield.md) - a panel must not appear on stream + +## Related + +- [Browser sources on macOS and Windows](/quest/m0/browser-source/README.md) - the browser panels render with on the other desktops diff --git a/quest/m1/pin-capture-exclude.md b/quest/m1/pin-capture-exclude.md new file mode 100644 index 00000000..1ca63aa3 --- /dev/null +++ b/quest/m1/pin-capture-exclude.md @@ -0,0 +1,21 @@ +# [XS] moq-video's capture exclusion is pinned + +## Goal + +The `moq` submodule includes moq-video's display capture that leaves chosen +apps out. moq plans it for macOS alone, the one desktop whose capture can +filter, and an Unsupported error elsewhere. + +## Plan + +Planned 2026-11-09 (maintainer, /quest-plan) for +[the privacy shield](/quest/m1/privacy-shield.md). Advance it with +`just upgrade-moq` in its own PR. + +## Waiting + +- moq-dev/moq: [the capture exclusion quest](https://github.com/moq-dev/moq/blob/main/quest/m4/capture-exclude.md) lands on moq's `main`. Check that quest's PR has merged. + +## Required + +- [The studio is caught up to moq's main](/quest/m0/pin-main.md) - moq's breaking changes since moq-dev/moq#8272, so this pin takes only its own diff --git a/quest/m1/pin-capture-foreground.md b/quest/m1/pin-capture-foreground.md new file mode 100644 index 00000000..fa179f56 --- /dev/null +++ b/quest/m1/pin-capture-foreground.md @@ -0,0 +1,20 @@ +# [XS] moq-video's foreground app is pinned + +## Goal + +The `moq` submodule includes moq-video's report of which app and window are +in front, with a notice when that changes, on the desktops that tell an app. + +## Plan + +Planned 2026-11-09 (maintainer, /quest-plan) for +[a source that follows the game](/quest/m1/follow-game.md). Advance it with +`just upgrade-moq` in its own PR. + +## Waiting + +- moq-dev/moq: [the foreground app quest](https://github.com/moq-dev/moq/blob/main/quest/m4/capture-foreground.md) lands on moq's `main`. Check that quest's PR has merged. + +## Required + +- [The studio is caught up to moq's main](/quest/m0/pin-main.md) - moq's breaking changes since moq-dev/moq#8272, so this pin takes only its own diff --git a/quest/m1/privacy-shield.md b/quest/m1/privacy-shield.md new file mode 100644 index 00000000..d763a414 --- /dev/null +++ b/quest/m1/privacy-shield.md @@ -0,0 +1,38 @@ +# [M] The studio hides itself and private apps from capture + +## Goal + +The studio's own windows never appear in a display capture, its own or +another app's. Apps the streamer marks private, such as a password manager +or a messenger, are left out of every display capture the studio makes. See +the `shield` frame of [`mock/later.html`](/quest/m0/ux/mock/later.html). + +## Plan + +- **The studio's own windows** ask the operating system to leave them out of + capture, where it offers that: the window sharing type on macOS and the + display affinity on Windows. This needs no moq change. +- **Private apps** are left out by moq-video's capture, which reaches here + through [its pin](/quest/m1/pin-capture-exclude.md). moq plans that for + macOS alone: no other desktop's capture can filter, and painting over a + window instead would show it for a frame whenever it moves. +- The frame draws a lock where the private window was. A filtered capture + shows what is behind the window instead, and a window's place on its + display is not something moq reports, so there is no lock. +- The list of private apps is the operator's, kept per user, not in the scene + file. Add marks an app private from its card. +- Where the desktop offers neither, as on X11 and for a portal-picked screen + on Wayland, the setting says so and is off. It never claims a protection + it cannot give: support is decided per desktop at startup, and a display + source on an unprotected desktop carries a note in its inspector. + +Settle here whether it also hides notification pop-ups, the commonest leak +in a whole-screen capture, where a desktop lets an app silence or exclude +them. + +Human check: a second capture tool on each desktop, to see what it sees. + +## Required + +- [The BUILD page](/quest/m0/ux/build.md) - the inspector's note and the Add card's mark +- [moq-video's capture exclusion is pinned](/quest/m1/pin-capture-exclude.md) - leaving chosen apps out of a display capture diff --git a/quest/m2/README.md b/quest/m2/README.md index 73a53b61..ac02c581 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -22,6 +22,9 @@ and nothing plans a browser WebTransport session for moq-net. ## Required +- [A phone as a remote](/quest/m2/phone-remote.md) - scene pads, mutes, and tally on a phone from a QR code +- [Markers and clips](/quest/m2/markers.md) - markers on the show, clips around them, chapters in a recording +- [A command palette](/quest/m2/command-palette.md) - one search box that runs any action and finds any setting - [Overlays drop their Blitz panic guard](/quest/m2/overlay-blitz-release.md) - the next Blitz release lets overlay checks parse without `catch_unwind` - [Viewer-side composition](/quest/m2/compose/README.md) - players composite independently published sources over the program from a layer layout track - [Browser sources paint on the GPU under Mesa AMD Wayland](/quest/m2/mesa-amd.md) - the Intel two-plane fix, checked on the `desktop` host's AMD GPU diff --git a/quest/m2/command-palette.md b/quest/m2/command-palette.md new file mode 100644 index 00000000..bee68251 --- /dev/null +++ b/quest/m2/command-palette.md @@ -0,0 +1,23 @@ +# [M] A command palette + +## Goal + +One key opens a search box that runs anything the studio can do: take a +scene, mute a sound, add a window, start a recording, open a setting. It +lists each action with its key, and it is how a setting is found by name. + +## Plan + +Planned 2026-11-09 (maintainer, /quest-plan), and held for later: ✅ m2 +(rejected: m1). + +- Every action is registered once, with its name and the words it answers + to, and that one table feeds the palette, the Hotkeys settings, and the + tooltips, so they cannot disagree. Today the bindable actions are a fixed + enum (`rs/moq-studio/src/controls.rs`). +- Scenes, sources, and settings are found by their own names. +- An action that cannot run now is shown with the reason, not hidden. + +## Required + +- [Settings](/quest/m0/ux/settings.md) - the settings it finds, and the Hotkeys pane that shares its table diff --git a/quest/m2/markers.md b/quest/m2/markers.md new file mode 100644 index 00000000..dfb3535b --- /dev/null +++ b/quest/m2/markers.md @@ -0,0 +1,23 @@ +# [M] Markers and clips + +## Goal + +A key or a button drops a marker on the show as it runs. The REVIEW page +shows markers on its strip and in a list, a clip is cut around one in a +click, and a recording carries its markers as chapters. + +## Plan + +Planned 2026-11-09 (maintainer, /quest-plan), and held for later: ✅ m2 +(rejected: m1). + +- A marker is a time on the program clock and an optional name. Markers of + a session are kept beside the recordings, not in the scene file. +- A clip around a marker uses Save clip's copy without re-encoding, so the + same keyframe limits apply. +- Chapters are written when a recording becomes a regular MP4. +- Marker is a hotkey action. + +## Required + +- [The REVIEW page](/quest/m0/ux/review.md) - the strip and the list markers appear on diff --git a/quest/m2/phone-remote.md b/quest/m2/phone-remote.md new file mode 100644 index 00000000..78fdacfb --- /dev/null +++ b/quest/m2/phone-remote.md @@ -0,0 +1,27 @@ +# [L] A phone as a remote + +## Goal + +A QR code in the studio opens a page on a phone that shows the scene pads, +the mutes, the live clock, and a tally, and drives them. No app is +installed, and it works wherever the phone reaches the relay, as a guest's +join page does. + +## Plan + +Planned 2026-11-09 (maintainer, /quest-plan), and held for later: ✅ m2 +(rejected: m1). + +- It is a page beside the join page (`js/join`), over the room the studio + already owns: the studio publishes its state there, and the remote + publishes commands the studio subscribes to. +- A remote's token is signed like a guest's and lets it do only that. Its + commands are untrusted input from the network: a fixed, small vocabulary, + rejected whole when malformed, and bounded in rate by design. +- Removing a remote stops renewing its token, as for a guest. +- The page follows the look of the [LIVE page](/quest/m0/ux/live.md) at a + phone's width. + +## Required + +- [Going live from inside the app](/quest/m0/ux/go-live.md) - the relay and room it is reached through