Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
node --experimental-strip-types scripts/check-dme.ts
node --experimental-strip-types scripts/check-fader.ts
node --experimental-strip-types scripts/check-preview.ts
node --experimental-strip-types scripts/check-tbar.ts

engine:
name: Engine unit tests
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ in `docs/platform-integration-plan.md` §8.

## Unreleased

### Changed: the T-bar is a real lever (ping-pong), T-bar widget

- GUI: the T-bar stays at the end a transition took it to; the next transition is made by moving
it back (bottom to top, then down again). Before, it jumped back to the top at the bottom, and a
mouse at the end of travel could start the next transition and flicker. Now the drag follows the
pointer only (from where the handle was grabbed), sends one position at a time and in order, is
latched after reaching the end until released, starts nothing within 2 % of the resting end, and
the browser cannot cancel it with a text drag. An Auto runs the handle to the other end; a Cut of
a held transition counts as done.
- API (compatible): `POST /mixer/position` takes `lever` (0 top … 1 bottom, where the lever is)
instead of `position`; the panel's `tbar_end` (`top`/`bottom`, saved) says where the lever rests
and flips with each transition the T-bar or an Auto completes; `GET /console` → `tbar` per ME
(`end`, `position`, `lever`). `position` keeps its meaning. `position: 1` straight from rest now
completes the transition (it answered 500). With `from` (the end the drag began at) a drag from
an end that another console's Auto or Cut changed is ignored; a lever without it (a hardware
panel; several told apart by `source`) is picked up at the resting end first: at start, after an
engine restart or a loaded state and after each change of the end. Ignored values answer
`status: "ignored"` with a `reason`. A held lever snaps within 2 % of either end;
with a stinger or BKGD off it fires one Auto per drag; an Auto that fails leaves the lever.
- Widget `tbar` (`/widget/tbar[?me=<n>][&buttons=false]`): the ME's T-bar lever with Cut and Auto.

### Added: Program streams, multiview widget

- Engine: each ME's Program as a WebRTC stream of its own (`<prefix>/me<n>-pgm`, NVENC H.264,
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ The GUI is a **separate React service** (Vite + TypeScript). Every control on th
- **File** takes the mixer on-air or off-air and opens the **clip and still library** and the stinger library. While a file goes up its bar shows the bytes sent (within each 8 MiB chunk too), then each conversion under way has a bar with the item's name and Cancel; both stay at the top of the library while it scrolls, and a converting item's card has its own bar.
- **Settings → Console layout…** (`PUT /workspace`): video format (off-air), source tiles 16:9 or 9:16 (display only, also on air), the number of sources, MEs, stinger slots, stinger players and keyers. What the production sets through the environment is greyed out.
- The gear on a **source** (`PATCH /inputs/{id}`): name, kind (`mxl_live`, `clip_player`, `black`), flows of a live input, the clip or still of a clip player with loop and play on cut, and the auto-stinger. A live input's **Audio** section (`PATCH /audio/inputs/{id}`): its delay (±1 ms, ±1 frame) and channel map (per PGM channel the input channel or silent; 1:1, Mono), for every ME.
- **Transition section** (per ME): the type (MIX, WIPE, STING) and the rate in frames (`PATCH /panels/{id}/transition`); the next transition's elements **BKGD**, **KEY 1** … (lit when tied; a keyer on air has a red underline; the last one lit stays) and **GRAB** (ME Program to the library); for WIPE the pattern picker (icons, the SMPTE code as a tooltip), reverse, flip-flop (the next wipe goes back), soft edge and border (0–100 % of the pattern's travel) and the border colour; for STING the slot. **CUT**, **AUTO** (`/mixer/auto`: the ME's transition) and **FTB** (lit while the output is black) map to `/mixer/cut`, `/mixer/auto`, `/mixer/fade-to-black`; the **T-bar** lever holds the transition at its position (`/mixer/position`) and follows an Auto.
- **Transition section** (per ME): the type (MIX, WIPE, STING) and the rate in frames (`PATCH /panels/{id}/transition`); the next transition's elements **BKGD**, **KEY 1** … (lit when tied; a keyer on air has a red underline; the last one lit stays) and **GRAB** (ME Program to the library); for WIPE the pattern picker (icons, the SMPTE code as a tooltip), reverse, flip-flop (the next wipe goes back), soft edge and border (0–100 % of the pattern's travel) and the border colour; for STING the slot. **CUT**, **AUTO** (`/mixer/auto`: the ME's transition) and **FTB** (lit while the output is black) map to `/mixer/cut`, `/mixer/auto`, `/mixer/fade-to-black`; the **T-bar** is a real lever (ping-pong): drag the handle from the end it rests at to the other end to make the transition; it stays there, and the next transition goes back the other way; back to its start ends it without the swap. An Auto runs the handle to the other end at the ME's rate; a Cut of a held transition counts as done. The mixer keeps the lever's end, so every console, the T-bar widget and a hardware panel agree. While dragging, the handle follows only the pointer (from where it was grabbed; a click on the track does nothing), and after reaching the end it is latched until released (no flicker at the end of travel).
- **AUDIO** (in the keyer row) opens and closes the active ME's audio strip under the deck (remembered in the browser; P5, `docs/engine-p5.md`): one column per strip (live inputs, clip players, the Programs of higher MEs, the stinger) with its meter (up to 2 bars: with more channels the loudest odd and even channels; a 1 s peak hold and a red over mark), a fader (+10 … −∞; double click: 0 dB; the values go to the mixer while it moves), **MUTE**, **AFV** (audio follows video; off: always in the mix), a red dot while the strip is heard in PGM and a delayed input's delay; then the master: the PGM meter (the true peak as a line, OVER with a count), the loudness of channels 1–2 (M, S, I in LUFS), **RESET** (integrated from now on) and the master fader. Meters poll at 10 Hz only while it is open. No audio preview.
- **SNAP** (in the keyer row) opens the snapshots ([docs/snapshots.md](docs/snapshots.md)): the active ME's, then those of all MEs, each with its number and name (double click renames; START marks the start-up snapshot), **PGM** and **AUD** (its recall's defaults: Program/on-air and audio), **RECALL** and **⋯** (recall once with other options, rename, store the current look over it, recall when the mixer starts, delete). **STORE** stores the active ME, or all MEs, into the lowest free number.
- **Frame grab:** right click on a source tile or on the Preview or Program monitor: *Grab still*, or *Grab to <clip player>* (the frame memory); a toast names the new still.
Expand All @@ -109,11 +109,12 @@ Operator screens (the platform's production designer) frame single controls of F
| `keys` | `me` (required) | 240×150 | that ME's upstream keyers and DSKs with On/Off and Auto |
| `dme` | `me`, `keyer` (required: upstream keyer 1..8) | 320×120 | that keyer's box: On/Off, Auto (a fly with FLY), Run back and forth, Stop and the presets; editing stays in the console |
| `audio` | `me` (required), `faders` (default true) | 480×240 | that ME's audio strips (meter, MUTE, AFV and, unless `faders=false`, the fader) and its master meter with the loudness (M, S, I) and RESET; meters at 10 Hz |
| `tbar` | `me` (optional, default 1), `buttons` (default true) | 120×240 | that ME's T-bar lever, the same as the console's (it stays at the end a transition took it to) and, unless `buttons=false`, Cut and Auto |
| `multiview` | `me` (optional, default 1: whose tally), `pvw` (default false) | 480×270 | a control-room wall over WebRTC: each ME's Program (its own stream), with `pvw=true` each ME's Preview, then the live inputs and clip players (mosaic tiles), each with its label; red border while it is on air on ME `me` through any path (the console's `on_air` when the mixer has it, else Program), green while it is on the Preview of any ME (every ME's Preview picture is green; red wins). The grid follows the mixer (sources and MEs added or removed) and fills the frame with the largest 16:9 pictures that fit |
| `snapshots` | `me` (optional), `all` (default true) | 240×120 | a recall button per snapshot of that ME and, unless `all=false`, of all MEs (without `me`: those only), with its PGM / AUD defaults; the last recall is lit |

- `GET /widgets` lists them with a JSON schema of their parameters (from the mixer's `GET /api/v1/widgets`); it answers an `Origin` that `WIDGET_FRAME_ANCESTORS` names (or `*`) with `Access-Control-Allow-Origin`.
- `GET /widget/bus?me=<n>[&transition=false][&theme=dark|light|transparent]` (and `/widget/keys?me=<n>`, `/widget/dme?me=<n>&keyer=<k>`, `/widget/audio?me=<n>[&faders=false]`, `/widget/snapshots[?me=<n>][&all=false]`, `/widget/multiview[?me=<n>][&pvw=true]`) is the page without the console around it, on the GUI's own origin (its `/api`). A bad parameter answers 400, an unknown widget 404. Only these routes carry `Content-Security-Policy: frame-ancestors <WIDGET_FRAME_ANCESTORS>`; none carries `X-Frame-Options`. The page posts `{type: "widget-ready"}` and `{type: "widget-size", w, h}` to its parent.
- `GET /widget/bus?me=<n>[&transition=false][&theme=dark|light|transparent]` (and `/widget/keys?me=<n>`, `/widget/dme?me=<n>&keyer=<k>`, `/widget/audio?me=<n>[&faders=false]`, `/widget/snapshots[?me=<n>][&all=false]`, `/widget/multiview[?me=<n>][&pvw=true]`, `/widget/tbar[?me=<n>][&buttons=false]`) is the page without the console around it, on the GUI's own origin (its `/api`). A bad parameter answers 400, an unknown widget 404. Only these routes carry `Content-Security-Policy: frame-ancestors <WIDGET_FRAME_ANCESTORS>`; none carries `X-Frame-Options`. The page posts `{type: "widget-ready"}` and `{type: "widget-size", w, h}` to its parent.
- `WIDGET_FRAME_ANCESTORS` is set on the **gui** container: a CSP source list, default `'self'` (an invalid value stops nginx with exit code 78).

## API
Expand Down Expand Up @@ -147,7 +148,7 @@ If `FLOWXER_API_TOKEN` is set, calls to **9610** need `Authorization: Bearer …
| Audio of an ME | `PATCH /audio/panels/{id}` (`master_db`); `PATCH /audio/panels/{id}/strips/{strip}` (`fader_db` −90 … +10, `mute`, `afv`; `strip`: an input or clip player id, a higher panel id (re-entry) or `stinger`, which has no `afv`); `POST /audio/panels/{id}/loudness/reset` (integrated loudness from now on) |
| Audio meters (10 Hz) | `GET /audio/meters`: the last 100 ms block; `sources` (live inputs and clip players by id, `stinger-<k>`: `peak_dbfs`, `rms_dbfs` per channel), `panels` (`peak_dbfs`, `true_peak_dbtp`, `rms_dbfs` per channel, `loudness` per pair: `momentary`, `short_term`, `integrated` LUFS; `overs`; `strips`: `gain_db`, `heard`). Values below −120 dB are −120 |
| SNAP (snapshots) | `GET /snapshots` (also `GET /console` `snapshots`); `POST /snapshots` (`panel_id`: `me-<n>` or `all`, `number` 1–99, default the lowest free; `name`; `options`: `program`, `audio`): store the current state; `GET`, `PATCH` (`name`, `options`), `DELETE /snapshots/{number}`; `POST /snapshots/{number}/recall` (`program`, `audio` override its defaults; answers what it `skipped`); `PUT /snapshots/startup` (`number` or null). See [docs/snapshots.md](docs/snapshots.md) |
| T-bar lever | `POST /mixer/position` (`panel_id`, `position` 0–1): off 0 starts the ME's transition and holds it, 1 completes it, 0 ends it without the swap; AUTO runs the rest; a stinger runs as an Auto |
| T-bar lever | `POST /mixer/position` (`panel_id` and either `position` 0–1: off 0 starts the ME's transition and holds it, 1 completes it, 0 ends it without the swap; or `lever` 0 top … 1 bottom: where a physical lever is, counted from the end it rests at, see below); AUTO runs the rest; a stinger runs as an Auto. The lever rests at `tbar_end` of the panel (`top` or `bottom`, saved); each transition the T-bar or an Auto completes (or a Cut of a held one) moves it to the other end, so the next transition goes back the other way (ping-pong). With `lever`, nothing starts within 2 % of the resting end, and a held transition snaps within 2 % of either end (back without the swap, or complete). When another console's Auto or Cut moves the resting end: a lever request with `from` (`top`/`bottom`, the end its drag began at; the GUI sends it) from an end that is no longer the resting one is ignored, so a drag cannot take the shot back; a lever without `from` (a hardware panel) counts only once it has come within 2 % of the resting end (pickup): at start, after the engine restarts or a state is loaded, and after each change of the end; until then it is ignored, also while a console holds a transition. One hardware lever per ME without `from`, or give each its own `source` (each picks up on its own). An ignored value answers 200 with `status: "ignored"` and `reason` (`stale_from` or `pickup`). With a stinger or BKGD off the lever fires one Auto, then waits for the next drag (or its pickup at the other end). `GET /console` → `tbar` per ME: `end`, `position` (of a held transition), `lever` |
| CUT / AUTO / FTB | `POST /mixer/cut`, `/mixer/auto` (`panel_id`, `frames`), `/mixer/fade-to-black`. Kept for API clients: `/mixer/fade` (a mix), `/mixer/wipe` (arms Wipe), `/mixer/take` (`transition`: `cut`, `mix`, `wipe`, `stinger`; `frames` or `duration_ms`) |
| Stinger chip / ⚙ | `POST /stinger/play`, `PATCH /stinger-slots/{id}` |
| GRAB | `POST /library/grab` (`source`: an input or clip player id, a panel id for its Program, `<panel id>:pvw` for its Preview; `player`: a clip player to load the still into; `name`, default `Grab <source> <YYYY-MM-DD HH:MM:SS>`): the new still (kind `still`, source `grab`, tag `grab`), ready at once |
Expand Down
14 changes: 14 additions & 0 deletions gui/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ server {
try_files /widget.html =404;
}

location = /widget/tbar {
if ($arg_me !~ "^(|[1-4])$") {
return 400 "me must be an ME 1..4\n";
}
if ($arg_buttons !~ "^(|true|false)$") {
return 400 "buttons must be true or false\n";
}
if ($arg_theme !~ "^(|dark|light|transparent)$") {
return 400 "theme must be dark, light or transparent\n";
}
include /etc/nginx/conf.d/flowxer-widgets-csp.inc;
try_files /widget.html =404;
}

location = /widget/multiview {
if ($arg_me !~ "^(|[1-4])$") {
return 400 "me must be an ME 1..4\n";
Expand Down
Loading
Loading