Skip to content

feat: allow hotcuts while using transition property#46

Merged
mint-dewit merged 6 commits into
mainfrom
feat/hotcuts
Mar 7, 2026
Merged

feat: allow hotcuts while using transition property#46
mint-dewit merged 6 commits into
mainfrom
feat/hotcuts

Conversation

@mint-dewit

@mint-dewit mint-dewit commented Feb 24, 2026

Copy link
Copy Markdown
Member

About the Contributor

This pull request is posted on behalf of the BBC.

Type of Contribution

This is a:
Improvement (someone may argue a fix?)

Current Behavior

It's not possible to control the preview bus while performing cuts using the "input"/"transition" properties

New Behavior

It's possible to control the preview bus while performing cuts but only when the transition is set to Cut and there is no currently selected upstream keyers

Testing Instructions

Unit tests seem to imply that behaviour remains as it was before

Other Information

This mechanism is similar to the one used in Sofie's VMix integration

I believe this is fully backwards compatible with existing behaviour with one exception where the user wishes to set the Program Bus while adjusting the selected upstream keyers without taking those to air. The alternative is to not allow for Cuts to take these Keyers on air at all which may be more in line with what the state of the USK's is saying? @Julusian I'd love to hear your thoughts on this one.

Status

  • PR is ready to be reviewed.
  • The functionality has been tested by the author.
  • Relevant unit tests has been added / updated.
  • Relevant documentation (code comments, system documentation) has been added / updated.

@PeterC89 PeterC89 added the contribution from BBC Contributions sponsored by BBC (bbc.co.uk) label Feb 24, 2026
@PeterC89 PeterC89 requested review from a team, Julusian and Copilot February 24, 2026 14:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for “hot cuts” when controlling ME program/preview via the custom transition property, aiming to allow preview-bus control during cuts (with safety constraints around selected keyers).

Changes:

  • Refactors MixEffect typing to always extend atem-connection’s VideoState.MixEffect, while keeping input as a deprecated alias and transition optional.
  • Updates resolveMixEffectsState to choose between hot-cut (ProgramInputCommand) vs preview+cut sequencing based on transition and nextSelection.
  • Updates and expands unit tests around program/preview and deprecated input behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
src/state.ts Makes MixEffect always extend the upstream VideoState.MixEffect, adding optional transition and deprecated input.
src/resolvers/mixEffect.ts Implements hot-cut vs preview+cut behavior and changes preview/program command emission logic.
src/resolvers/tests/mixEffect.spec.ts Updates tests for the new command ordering/behavior and adds coverage for deprecated input.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/resolvers/mixEffect.ts Outdated
Comment thread src/resolvers/mixEffect.ts Outdated
Comment on lines +56 to +57
const programInput = newMixEffect?.input ?? newMixEffect?.programInput
const oldProgramInput = oldMixEffect?.input ?? oldMixEffect?.programInput

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deprecated input field is currently preferred over programInput (newMixEffect?.input ?? newMixEffect?.programInput). If both are present, this will ignore the non-deprecated field and could produce surprising results. Prefer programInput first and only fall back to input when programInput is undefined.

Suggested change
const programInput = newMixEffect?.input ?? newMixEffect?.programInput
const oldProgramInput = oldMixEffect?.input ?? oldMixEffect?.programInput
const programInput = newMixEffect?.programInput ?? newMixEffect?.input
const oldProgramInput = oldMixEffect?.programInput ?? oldMixEffect?.input

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If both are present, this will ignore the non-deprecated field and could produce surprising results

Surprising behaviour that happens to be consistent with the previous behaviour and therefore backwards compatible? I guess trying to not introduce breaking changes would be novel for an LLM.

Comment thread src/resolvers/mixEffect.ts Outdated
Comment on lines +107 to +112
if (canHotCut && oldMixEffect?.previewInput !== newMixEffect?.previewInput) {
// set preview when there is no auto transition command
commands.push(
new AtemCommands.PreviewInputCommand(mixEffectId, newMixEffect?.previewInput ?? Defaults.Video.defaultInput)
)
}

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preview bus updates are now gated by canHotCut (if (canHotCut && oldMixEffect?.previewInput !== newMixEffect?.previewInput)), which means a plain preview change will emit no command whenever a USK is selected for the next transition. Previously preview changes were always applied when programPreview diffing was enabled. Consider handling previewInput diffs independently of canHotCut (and, when doing a Preview+Cut sequence, apply the requested preview after the cut if both program+preview are changing).

Copilot uses AI. Check for mistakes.
ME2.previewInput = 2
ME2.programInput = 1
ME2.transition = Enums.TransitionStyle.CUT
const commands = ME.resolveMixEffectsState([ME1], [ME2], fullDiffObject) as Array<Commands.PreviewInputCommand>

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type assertion in this test casts the result to Array<Commands.PreviewInputCommand>, but the expectation checks for a ProgramInputCommand at index 0. Using a union type here (or avoiding the cast) will keep the test type-safe and prevent future refactors from being masked by an overly narrow cast.

Suggested change
const commands = ME.resolveMixEffectsState([ME1], [ME2], fullDiffObject) as Array<Commands.PreviewInputCommand>
const commands = ME.resolveMixEffectsState(
[ME1],
[ME2],
fullDiffObject
) as Array<Commands.ProgramInputCommand | Commands.PreviewInputCommand>

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +98
const commands = ME.resolveMixEffectsState([ME1], [ME2], fullDiffObject) as Array<Commands.PreviewInputCommand>
expect(commands).toHaveLength(2)

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: this assertion narrows to Array<Commands.PreviewInputCommand> while the test asserts a ProgramInputCommand is present. Widen the asserted type (eg a union) so TypeScript can help catch incorrect assumptions about command ordering/types.

Copilot uses AI. Check for mistakes.
Comment thread src/resolvers/mixEffect.ts Outdated
Comment on lines +59 to +79
const canHotCut =
transition === Enums.TransitionStyle.CUT &&
!newMixEffect?.transitionProperties.nextSelection.find(
(layer) => layer !== ConnectionEnums.TransitionSelection.Background
)

if (newMixEffect.transition === Enums.TransitionStyle.CUT) {
commands.push(new AtemCommands.CutCommand(mixEffectId))
} else if (newMixEffect.transition !== Enums.TransitionStyle.DUMMY) {
if (newMixEffect.transition !== oldMeTransition) {
if (programInput !== oldProgramInput) {
switch (transition) {
// cut to new source
case Enums.TransitionStyle.CUT:
if (canHotCut) {
commands.push(
new AtemCommands.ProgramInputCommand(mixEffectId, programInput ?? Defaults.Video.defaultInput)
)
} else {
commands.push(
new AtemCommands.PreviewInputCommand(mixEffectId, programInput ?? Defaults.Video.defaultInput),
new AtemCommands.CutCommand(mixEffectId)
)
}
break

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new canHotCut / Preview+Cut branching introduces important behavior (eg when USKs are selected in nextSelection). There are unit tests for the hot-cut path, but no coverage for the non-hotcut path (where a CutCommand is used) or for how preview updates behave when keyers are selected. Adding tests for those cases would prevent regressions and clarify the intended semantics.

Copilot uses AI. Check for mistakes.
Comment thread src/resolvers/mixEffect.ts Outdated
@codecov

codecov Bot commented Feb 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.24561% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/resolvers/downstreamKeyer.ts 92.85% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Consolidates transition decision flow across downstream/upstream keyers and mix effects, changes several resolver return types to include a doTransition flag, narrows MixEffect typing, and updates tests to use centralized getState initialization and new command/field semantics. (43 words)

Changes

Cohort / File(s) Summary
Type Definitions
src/state.ts
Narrowed exported MixEffect to ExtendedMixEffect; ExtendedMixEffect now extends VideoState.MixEffect with optional deprecated input and optional transition; State now exposes video: AtemVideoState.
Resolver Orchestration
src/resolvers/index.ts
Processes downstream keyers earlier, collects doTransition from downstream/upstream resolvers, and passes doTransition into resolveMixEffectsState.
Mix Effects Resolver
src/resolvers/mixEffect.ts
Added optional dskTransition/doTransition parameter; unified program/preview/transition logic using programInput, oldProgramInput, transition, and transitionIsBg; emits ProgramInput/PreviewInput and transition-related commands (Cut, AutoTransition, TransitionPosition, TransitionProperties, etc.) under new flow.
Downstream Keyer Resolver
src/resolvers/downstreamKeyer.ts
Return shape changed to { commands, doTransition }; tracks doTransition when tied OnAir changes correspond to mix-effect transitions; diffOptions typing adjusted to SectionsToDiff['video'].
Upstream Keyer Resolver
src/resolvers/upstreamKeyers/...
Added newStateTransSelection parameter; return changed to { commands, doTransition }; sets doTransition when an upstream OnAir change is handled via a transition rather than immediate OnAir command.
Tests — MixEffect & Keyers
src/resolvers/__tests__/*, src/resolvers/upstreamKeyers/__tests__/*, src/resolvers/downstreamKeyer.spec.ts
Tests updated to use getState initialization and base MixEffect types; adjusted expectations for new command types and ordering; resolver calls now destructure { commands } and supply extra params for new signatures.
Package manifest / Misc tests
package.json, src/resolvers/__tests__/mixEffect.spec.ts
Minor test/import adjustments and formatting changes to accommodate updated resolver signatures and returned shapes.

Sequence Diagram(s)

sequenceDiagram
  participant Orch as Orchestrator (resolvers/index)
  participant DSK as DownstreamKeyer Resolver
  participant USK as UpstreamKeyer Resolver
  participant ME as MixEffect Resolver
  participant CmdQ as CommandQueue
  participant ATEM as ATEM Device

  Orch->>DSK: resolveDownstreamKeyerState(oldDsks, newDsks, diffOptions)
  DSK-->>Orch: { commands, doTransition }
  Orch->>USK: resolveUpstreamKeyerState(mixEffectId, oldUSK, newUSK, newTransSelection, diffOptions)
  USK-->>Orch: { commands, doTransition }
  Orch->>ME: resolveMixEffectsState(oldME, newME, diffOptions, doTransition)
  ME-->>Orch: { commands }
  Orch->>CmdQ: enqueue DSK/USK/ME commands
  CmdQ->>ATEM: execute queued commands
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature: enabling hotcuts (preview bus control via cuts) while using the transition property, which aligns directly with the PR's core objective.
Description check ✅ Passed The description comprehensively explains the current behavior, new behavior, testing status, backwards compatibility considerations, and technical context, all directly related to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/hotcuts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/resolvers/mixEffect.ts`:
- Around line 55-112: The code currently only pushes a PreviewInputCommand when
canHotCut is true, so if transition is MIX/WIPE/etc and only previewInput
changed no preview command is emitted; fix by tracking whether an
auto-transition was queued (e.g., set a boolean like autoTransitionQueued when
you push AutoTransitionCommand in the default branch) and then replace the
canHotCut check at the later preview-update block with a check that
previewUpdateNeeded = oldMixEffect?.previewInput !== newMixEffect?.previewInput
&& !autoTransitionQueued; if previewUpdateNeeded push a PreviewInputCommand for
newMixEffect.previewInput; keep existing canHotCut behavior for CUT branch but
ensure the flag is set only when AutoTransitionCommand is actually enqueued
(symbols: programPreview, canHotCut, transition, AutoTransitionCommand,
PreviewInputCommand, TransitionPropertiesCommand, TransitionPositionCommand).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec671b7 and f42514a.

📒 Files selected for processing (3)
  • src/resolvers/__tests__/mixEffect.spec.ts
  • src/resolvers/mixEffect.ts
  • src/state.ts

Comment thread src/resolvers/mixEffect.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/resolvers/mixEffect.ts`:
- Around line 93-99: The check uses oldMixEffect?.transitionProperties?.style
(readonly current style) but the command updates nextStyle, so change the
comparison to compare transition against
oldMixEffect?.transitionProperties?.nextStyle; update the condition that
currently reads if (transition !== (oldMixEffect?.transition ??
oldMixEffect?.transitionProperties?.style)) to compare with nextStyle instead,
ensuring the AtemCommands.TransitionPropertiesCommand(mixEffectId) updateProps({
nextStyle: ... }) is emitted when the pending nextStyle actually differs.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f42514a and 7676b61.

📒 Files selected for processing (2)
  • src/resolvers/__tests__/mixEffect.spec.ts
  • src/resolvers/mixEffect.ts

Comment thread src/resolvers/mixEffect.ts
Comment thread src/resolvers/mixEffect.ts Outdated
case Enums.TransitionStyle.CUT:
if (canHotCut) {
commands.push(
new AtemCommands.ProgramInputCommand(mixEffectId, programInput ?? Defaults.Video.defaultInput)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should each of these be doing ?? Defaults.Video.defaultInput or should that be done when defining programInput?

Im wondering what the correct behaviour is when programInput = 0 and oldProgramInput = undefined. That will be seen as a change, even though they both resolve to 0

Comment thread src/resolvers/mixEffect.ts Outdated
const transition = newMixEffect?.transition ?? Enums.TransitionStyle.CUT
const canHotCut =
transition === Enums.TransitionStyle.CUT &&
!newMixEffect?.transitionProperties?.nextSelection?.find(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think checking the dsk should be included here too, as that can be tied to the cut too

Comment thread src/resolvers/mixEffect.ts Outdated
}

if (
(oldProgramInput === programInput || canHotCut) &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this canHotCut could be loosened to transition === Enums.TransitionStyle.CUT.
Even if we are doing a pvw+cut, I think it will be safe to add another pvw immediately after that too.

Maybe if it did a pvw+cut it should always be sending this pvw command too, to ensure it ends up in the desired state in case it was mangled when changing pgm? That would get us closer to a state where pvw is predictable (I think just leaving cases when a non-cut transition is used)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an interesting thought, I'll run it up against an atem to verify

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/resolvers/upstreamKeyers/index.ts (1)

104-111: Logic correct but bitmask calculation could be clearer.

The calculation Math.pow(2, upstreamKeyerId + 1) computes the TransitionSelection enum value for each keyer (Key1=2, Key2=4, etc.). The logic correctly:

  • Pushes direct OnAirCommand when the keyer is NOT in the transition selection
  • Signals doTransition when the keyer IS in the transition selection (to be toggled via cut/auto)

Consider using the enum directly for clarity:

const keyerSelectionValue = Math.pow(2, upstreamKeyerId + 1) as ConnectionEnums.TransitionSelection
const isInTransition = newStateTransSelection?.includes(keyerSelectionValue)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/resolvers/upstreamKeyers/index.ts` around lines 104 - 111, The bitmask
calculation for detecting if a keyer is in TransitionSelection is unclear;
replace the inline Math.pow(2, upstreamKeyerId + 1) usage with an explicit
enum-derived value (e.g., compute keyerSelectionValue as the appropriate
ConnectionEnums.TransitionSelection for upstreamKeyerId) and use that to set a
clearer flag (isInTransition) instead of the current transitions variable, then
use isInTransition in the existing conditional that decides between creating an
AtemCommands.MixEffectKeyOnAirCommand (respecting runOrderGroup logic) or
setting doTransition when thisDiffOptions.onAir and oldKeyer.onAir !==
newKeyer.onAir.
src/resolvers/__tests__/downstreamKeyer.spec.ts (1)

29-32: Tests adapted correctly, but missing coverage for doTransition behavior.

The tests properly destructure { commands } from the return value. However, there's no test verifying that doTransition is returned as true when a DSK's onAir changes while properties.tie is set. Consider adding a test case:

test('Unit: Downstream keyer: onAir with tie returns doTransition', function () {
  DSK2[0].properties.tie = true
  DSK2[0].onAir = true
  const { commands, doTransition } = resolveDownstreamKeyerState(DSK1, DSK2, fullDiff)
  expect(doTransition).toBe(true)
  expect(commands.filter(c => c.constructor.name === 'DownstreamKeyOnAirCommand')).toHaveLength(0)
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/resolvers/__tests__/downstreamKeyer.spec.ts` around lines 29 - 32, Add a
unit test for resolveDownstreamKeyerState that verifies doTransition is returned
true when a downstream keyer's onAir changes and its properties.tie is true:
mutate the test fixture (set DSK2[0].properties.tie = true and DSK2[0].onAir =
true), call resolveDownstreamKeyerState(DSK1, DSK2, fullDiff), and assert that
the returned doTransition is true and that no DownstreamKeyOnAirCommand
instances are present in the commands array (e.g., filter commands by
constructor.name === 'DownstreamKeyOnAirCommand' and expect length 0).
src/resolvers/mixEffect.ts (1)

107-113: Preview update condition may drop preview-only changes during auto transitions.

The condition allows preview updates when:

  • Program input didn't change (oldProgramInput === programInput), OR
  • No transition is happening (!doTransition), OR
  • Transition is CUT

However, if doTransition is true due to USK/DSK changes (not program change), and the transition is MIX/WIPE, preview-only changes will still be dropped. This is an improvement over the previous code but may still have edge cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/resolvers/mixEffect.ts` around lines 107 - 113, The current
preview-update check in mixEffect.ts (using oldProgramInput, programInput,
doTransition, transition, Enums.TransitionStyle.CUT, previewInput,
oldPreviewInput and AtemCommands.PreviewInputCommand with mixEffectId) can still
drop preview-only changes when doTransition is true due to USK/DSK but program
didn't change; update the condition so preview updates are only suppressed when
the program input actually changed AND a non-CUT transition is in progress.
Concretely, replace the if-condition so it allows sending PreviewInputCommand
whenever previewInput !== oldPreviewInput unless (oldProgramInput !==
programInput && doTransition && transition !== Enums.TransitionStyle.CUT); keep
using the same variables and push the PreviewInputCommand as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/resolvers/__tests__/mixEffect.spec.ts`:
- Around line 91-93: The type annotation for the commands variable in the test
for resolveMixEffectsState incorrectly repeats Commands.PreviewInputCommand;
remove the duplicate and replace it with the intended union (or a single type) —
e.g. use Array<Commands.PreviewInputCommand | Commands.ProgramInputCommand> if
both are expected, or simply Array<Commands.PreviewInputCommand> if only that
type is returned; update the declaration where ME.resolveMixEffectsState is
cast.

In `@src/resolvers/index.ts`:
- Around line 24-44: The DSK onAir changes can be lost if
resolveDownstreamKeyerState sets doTransition=true but resolveMixEffectsState is
not called (diffOptions.video?.mixEffects falsy), so update the flow to provide
a fallback: after calling resolveDownstreamKeyerState (function
resolveDownstreamKeyerState) check if doTransition is true and
diffOptions.video?.mixEffects is falsy (or resolveMixEffectsState will not run),
and in that case push explicit DownstreamKeyOnAirCommand(s) for the tied DSK(s)
whose onAir changed; ensure you reuse the same keyer identifiers and state from
the dskDiff (or downstream keyer diff result) so activation is identical to what
resolveMixEffectsState would do.

In `@src/resolvers/mixEffect.ts`:
- Around line 54-56: The bug is that doTransition is being overwritten by
uskDiff.doTransition instead of combined with the existing DSK transition state;
update the assignment in the mixEffect resolver so that after merging
uskDiff.commands you combine the transition flags (e.g., set doTransition to the
logical OR of the existing doTransition/dskTransition and uskDiff.doTransition)
rather than replacing it, keeping the downstream keyer's request if it was true;
locate the code that currently does doTransition = uskDiff.doTransition and
change it to combine both flags (preserving existing doTransition) so any true
value from dskTransition or uskDiff.doTransition is honored.
- Around line 63-68: The empty-array branch in computing transitionIsBg is
unreachable because nextSelection is always set from
newMixEffect?.transitionProperties?.nextSelection with a fallback to
Defaults.Video.TransitionProperties.nextSelection (which is not empty); either
remove the length check or explicitly document intent. Fix by updating the
transitionIsBg expression in mixEffect.ts (where nextSelection is assigned) to
either: 1) remove the "nextSelection.length === 0 ||" clause and rely solely on
checking for Background via nextSelection.find(layer => layer ===
ConnectionEnums.TransitionSelection.Background), or 2) if an empty array should
semantically mean “background only” add a concise comment above the assignment
explaining that empty nextSelection is treated as Background and keep the check;
reference nextSelection, Defaults.Video.TransitionProperties.nextSelection,
transitionIsBg, and ConnectionEnums.TransitionSelection.Background when making
the change.

---

Nitpick comments:
In `@src/resolvers/__tests__/downstreamKeyer.spec.ts`:
- Around line 29-32: Add a unit test for resolveDownstreamKeyerState that
verifies doTransition is returned true when a downstream keyer's onAir changes
and its properties.tie is true: mutate the test fixture (set
DSK2[0].properties.tie = true and DSK2[0].onAir = true), call
resolveDownstreamKeyerState(DSK1, DSK2, fullDiff), and assert that the returned
doTransition is true and that no DownstreamKeyOnAirCommand instances are present
in the commands array (e.g., filter commands by constructor.name ===
'DownstreamKeyOnAirCommand' and expect length 0).

In `@src/resolvers/mixEffect.ts`:
- Around line 107-113: The current preview-update check in mixEffect.ts (using
oldProgramInput, programInput, doTransition, transition,
Enums.TransitionStyle.CUT, previewInput, oldPreviewInput and
AtemCommands.PreviewInputCommand with mixEffectId) can still drop preview-only
changes when doTransition is true due to USK/DSK but program didn't change;
update the condition so preview updates are only suppressed when the program
input actually changed AND a non-CUT transition is in progress. Concretely,
replace the if-condition so it allows sending PreviewInputCommand whenever
previewInput !== oldPreviewInput unless (oldProgramInput !== programInput &&
doTransition && transition !== Enums.TransitionStyle.CUT); keep using the same
variables and push the PreviewInputCommand as before.

In `@src/resolvers/upstreamKeyers/index.ts`:
- Around line 104-111: The bitmask calculation for detecting if a keyer is in
TransitionSelection is unclear; replace the inline Math.pow(2, upstreamKeyerId +
1) usage with an explicit enum-derived value (e.g., compute keyerSelectionValue
as the appropriate ConnectionEnums.TransitionSelection for upstreamKeyerId) and
use that to set a clearer flag (isInTransition) instead of the current
transitions variable, then use isInTransition in the existing conditional that
decides between creating an AtemCommands.MixEffectKeyOnAirCommand (respecting
runOrderGroup logic) or setting doTransition when thisDiffOptions.onAir and
oldKeyer.onAir !== newKeyer.onAir.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0acc54e-ff1e-4b7e-910d-808544e60728

📥 Commits

Reviewing files that changed from the base of the PR and between 7676b61 and 454d19e.

📒 Files selected for processing (7)
  • src/resolvers/__tests__/downstreamKeyer.spec.ts
  • src/resolvers/__tests__/mixEffect.spec.ts
  • src/resolvers/downstreamKeyer.ts
  • src/resolvers/index.ts
  • src/resolvers/mixEffect.ts
  • src/resolvers/upstreamKeyers/__tests__/upstreamKeyer.spec.ts
  • src/resolvers/upstreamKeyers/index.ts

Comment thread src/resolvers/__tests__/mixEffect.spec.ts
Comment thread src/resolvers/index.ts
Comment thread src/resolvers/mixEffect.ts
Comment thread src/resolvers/mixEffect.ts
@mint-dewit mint-dewit merged commit 978ec38 into main Mar 7, 2026
17 of 20 checks passed
@mint-dewit mint-dewit deleted the feat/hotcuts branch March 9, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution from BBC Contributions sponsored by BBC (bbc.co.uk)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants