Skip to content

fix(ws): take a preview PiP to program from a macro - #180

Draft
wagenet wants to merge 3 commits into
Eyevinn:mainfrom
wagenet:wagenet/ws-macro-pip-take
Draft

wagenet wants to merge 3 commits into
Eyevinn:mainfrom
wagenet:wagenet/ws-macro-pip-take

Conversation

@wagenet

@wagenet wagenet commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Top of a three-PR stack: #182 (harness) → #179 (displace a PGM PiP) → this. The diff contains all three commits until those merge — only the third, fix(ws): take a preview PiP to program from a macro, belongs to this PR.

Problem

A macro TAKE never promotes a PiP sitting in preview, and the failure is silent rather than merely stale.

SELECT_PVW_PIP sets newTally = { pgm: tally.pgm, pvw: null }, so tally.pvw is null whenever a PiP is in preview. The macro TAKE then does:

const newTally = { pgm: tally.pvw, pvw: tally.pgm };   // → { pgm: null, pvw: <old pgm> }
broadcast(productionId, { type: 'TALLY', ...newTally });
await stromTransition(currentDoc, fromPad, tally.pvw, 'cut');
//                                         ^^^^^^^^^ null → early return on !toMixerInput

So every subscriber is told the program bus went empty, Strom is never called and keeps airing the previous source, and pgmPipByProduction is never set — the server does not know the PiP took either. The operator sees a macro that appears to do nothing while the tally says the show went black.

Fix

Restructures the macro TAKE to mirror the interactive handler's three branches — PiP moving PVW→PGM, PiP moving PGM→PVW, and no PiP — instead of the single displacement path that only covered the second.

The pip maps are now swapped as a pair (newPgmPip = curPvwPip, newPvwPip = curPgmPip) rather than cleared, and PIP_STATE is emitted unconditionally, both matching the interactive TAKE.

The forward branch reuses pvwBeforePipByProduction as to_input and records it as the new pgmBg, so a later CUT or TRANSITION while the PiP is on program has a background to report and Strom keeps pgm_input ≠ pvw_input.

Impact

The selectPreview({ source: { pip: N } }) and transition pair in the forward branch is what reaches the mixer — previously no Strom call was made at all on this path. trigger_transition reads the authoritative PGM/PVW source from overlay state (gst/pipeline/effects/take.rs), which selectPreview is what sets.

As in #179, the from_input/to_input values are carried for consistency with the interactive handler rather than as a fix; Strom reads them only when overlay state is missing.

With no PiP on either bus, the new else branch is the previous behaviour unchanged.

Conflicts with #156

Same five hunks as #179 — three in src/ws/controller.ts, two in src/__tests__/ws-macro-pip.test.ts — since this PR carries #179's commit until that merges. Mechanical, and I will rebase whichever lands second.

Testing

Ships with the harness case it makes pass. That case asserts Strom is called at all on this path, which is precisely what the previous behaviour got wrong — it fails against #179 and passes here.

  • npx vitest run src/__tests__/ws-macro-pip.test.ts — 4 passed.
  • npx tsc --noEmit clean.
  • npx vitest run — the suite's 27 pre-existing failures in activation.test.ts and macros.test.ts are unchanged and present on main too.
  • Not exercised against a live mixer. Strom builds and runs locally, but neither of its sample flows carries a vision mixer block and its own suite has no PiP coverage, so Strom's response to selectPreview({pip:N}) rests on reading flows.rs and mixer_ops.rs rather than on execution.

wagenet and others added 3 commits August 27, 2026 17:35
The WS controller has no test coverage — `activation.test.ts` and
`macros.test.ts` both mock `../ws/controller.js` out entirely — so there is
nowhere to assert what a macro action sends to Strom or broadcasts to
subscribers.

Adds that harness: `handleMessage` driven directly with CouchDB and
StromClient mocked, asserting the Strom call sequence and the broadcast
payloads. State is established through real inbound messages rather than by
reaching into the module-level maps, so each case exercises the same state
machine the server runs in production.

One case here, a macro CUT with no PiP anywhere, which pins the behaviour the
PiP work must leave untouched. Cases that require a behaviour change to pass
belong with the commits that make them pass.

`handleMessage` is exported for this and is the only production change; the
plugin exposes just a websocket route, so there was no other seam.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uhnqt88WrXHGV7VF4D65x3
The CUT and TRANSITION handlers each do four things when a real source
replaces a PiP that is on program: resolve `from_input` from
`pgmBgByProduction`, move the PiP from PGM to PVW, broadcast `PIP_STATE`,
and call `selectPreview` so the PiP lands back in Strom's preview.

The same three actions executed from a macro do none of it. `MACRO_EXEC`
updates `tally` and calls `stromTransition` directly, so with a PiP on
program the PiP is never returned to preview, `pgmPipByProduction` and
`pgmBgByProduction` keep their old values, and no `PIP_STATE` is emitted —
leaving both the server and every subscriber believing a PiP is on program
long after it left.

The `selectPreview` omission is the one that reaches the mixer. Strom's
`trigger_transition` reads the authoritative PGM/PVW source from the block's
overlay state, which `selectPreview` is what sets. Without the restore,
preview holds the cut target that `stromTransition` selected rather than the
displaced PiP, so the operator's next take airs the wrong source.

The frame airing at the moment the macro runs is unaffected: the cut target
comes from `stromTransition`'s own `selectPreview`, which is untouched here.

Passing `fromPad` instead of `tally.pgm` is included for consistency with the
interactive handlers, not as a fix. Strom ignores the request's
`from_input`/`to_input` whenever overlay state exists, falling back to them
only when it is missing entirely.

Mirrors the interactive logic into the three macro actions. The displacement
block is inlined rather than extracted so each macro path can be diffed
against the handler it copies; the interactive handlers are left untouched.

Adds the two harness cases this makes pass — a macro CUT and a macro
TRANSITION over a PiP on program. Both fail without the change.

A PiP sitting in *preview* is still not promoted to program by a macro TAKE
— that needs the full TAKE path, which tracks `pvwBeforePipByProduction` and
runs its own Strom sequence. This commit only covers displacing a PiP that is
already on program.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uhnqt88WrXHGV7VF4D65x3
A macro TAKE never promoted a PiP sitting in preview, and the failure was
silent. `SELECT_PVW_PIP` leaves `tally.pvw` null, so the swap produced
`{ pgm: null, pvw: <old pgm> }`, which was broadcast as-is and then passed to
`stromTransition` as `toMixerInput` — where it early-returns on the null.

The result: every subscriber is told the program bus is empty, Strom is never
called at all and keeps airing the previous source, and `pgmPipByProduction`
is never set, so the server does not know the PiP took either.

Restructures the macro TAKE to mirror the interactive handler's three
branches — PiP moving PVW to PGM, PiP moving PGM to PVW, and no PiP — instead
of the single displacement path that only covered the second. The pip maps
are swapped as a pair (`newPgmPip = curPvwPip`, `newPvwPip = curPgmPip`)
rather than cleared, and `PIP_STATE` is emitted unconditionally, matching the
interactive TAKE.

The forward branch reuses `pvwBeforePipByProduction` as `to_input` and records
it as the new `pgmBg`, so a later CUT or TRANSITION while the PiP is on
program has a background to report and Strom keeps `pgm_input ≠ pvw_input`.

Adds the harness case this makes pass. It asserts that Strom is called at all
on this path, which is what the previous behaviour got wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uhnqt88WrXHGV7VF4D65x3
@birme

birme commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

This PR is part of a multi-PR dependency stack (the WS macro / PiP fixes), in dependency order:

Holding off on automated review/merge here — both open PRs are drafts and each still carries the other's commits until the one below it merges, so they need a human (or the relevant dev agent) to assess the stack as a whole, not a per-PR pass. Nothing further is needed from the author; this comment records the dependency on the PR itself rather than only in a run log.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants