Skip to content

fix: shared control undefined state#419

Merged
mint-dewit merged 12 commits into
Sofie-Automation:release53from
bbc:fix/shared-control-undefined-state
Jan 27, 2026
Merged

fix: shared control undefined state#419
mint-dewit merged 12 commits into
Sofie-Automation:release53from
bbc:fix/shared-control-undefined-state

Conversation

@mint-dewit

@mint-dewit mint-dewit commented Jan 21, 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: Bug Fix aimed at the Shared Control feature used with the Atem integration

Current Behavior

With no active timeline objects the Atem Integration will overwrite any changes made to Mix Effects by external actors. This is because the "old state" will have the external changes added to it but the "new state" will not get these changes (as there is no address to apply them to)

New Behavior

The Integration correctly responds to external actors even without active timeline objects (i.e. does nothing until the Expected State is changed or Control Value changes). Additionally, when a timeline object is removed from the timeline, the Expected State for the device will be removed as well. I believe this should address all cases when dealing with "undefined" states on the timeline.

The additional Device Setting lets a user override the startup behaviour when a Device reports some State before the TSR has seen Timeline Objects for this Address:

t0 t1 t2 t3
Event Timeline is Generated with some object starting at t0 TSR is (re)started Device connects and reports its state TSR receives timeline from t0
Without "Sync on Start" All Address States are cleared The Address is blocked from control until a new Timeline object is added As t2 > t0 the Address will not be updated
With "Sync on Start" All Address States are cleared The Address is updated but remains available for TSR to control Address will be updated as per the timeline created at t0

Additional changes

  • Under shared control, the Atem keyers are diffed using the state library for consistency
  • QuickTSR no longer complains about unused variables in typescript (makes it quick to comment and uncomment timeline objects)
  • Improved logging in TSR
  • Atem Commands now include the command id when logging

Testing Instructions

With shared control enabled, an empty timeline should not control the Atem. When going from an empty timeline back to a timeline object the integration should correctly reassert control.

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.

Summary

This PR fixes a bug in the Shared Control feature when used with the Atem integration, where the Atem device could incorrectly overwrite external changes when no active timeline objects exist. The fix ensures the integration properly ignores external actors until timeline objects are present, and correctly removes expected state for addresses no longer on the timeline.

Changes

Core Bug Fix - Shared Control State Management:

  • Updated the addressStateReassertsControl method signature in the Device API to accept undefined for the newState parameter, allowing the integration to distinguish between "no timeline object" and "timeline object with specific state"
  • Implemented a new unsetExpectedState mechanism in StateTracker to remove expected state entries when addresses are not present on the timeline
  • Modified state diff logic to unset addresses not present in the new state after processing timeline addresses
  • Updated StateTracker initialization with a syncOnStartup parameter (defaulting to true) to control whether the device syncs on initial connection before timeline objects are present

Atem Integration Improvements:

  • Enhanced diffAddressStates to use the state library for consistent comparison of UpStreamKey (Atem keyer) states via resolveUpstreamKeyerState
  • Added state refresh from current Atem state upon connection establishment with shared control enabled
  • Modified sendCommand to include command constructor names in logged command payloads for improved debugging

Logging and Configuration Enhancements:

  • Added optional logCommandReports boolean setting to TSRSettings to control command-related event logging
  • Restructured event listeners on connectionManager with improved event routing: separated 'error', 'warning', 'info', and 'debug' events with appropriate log levels
  • Disabled TypeScript's noUnusedLocals compiler option in QuickTSR to eliminate spurious unused-variable warnings

Schema and Testing:

  • Added syncOnStartup property to device common options schema, allowing users to disable initial device sync when shared control is enabled
  • Expanded state handler tests to cover reassertion control scenarios, device-ahead behavior with and without timeline objects, and address unset behavior

Testing

  • Verified that with shared control enabled, an empty timeline does not control the Atem device
  • Tested that control is properly reasserted when timeline objects return
  • Added/updated relevant unit tests for the new state management and tracking behavior

@mint-dewit mint-dewit requested a review from a team as a code owner January 21, 2026 15:43
@mint-dewit mint-dewit added bug Something isn't working contribution from BBC Contributions sponsored by BBC (bbc.co.uk) labels Jan 21, 2026
@coderabbitai

coderabbitai Bot commented Jan 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

This PR introduces optional startup synchronization control for devices, refactors address state reassertion logic to permit undefined states, enhances command report logging in quick-tsr, improves ATEM upstream-keyer state comparison using resolver utilities, and updates state management with debouncing and address cleanup mechanisms.

Changes

Cohort / File(s) Summary
Command Reporting & Event Logging
packages/quick-tsr/src/index.ts, packages/quick-tsr/src/tsrHandler.ts
Added optional logCommandReports boolean field to TSRSettings interface. Refactored event listener surface on connectionManager: replaced specific 'connectionEvent:error' with generalized 'error' and 'info'/'debug' events; added conditional guards for command-related events ('slowSentCommand', 'slowFulfilledCommand', 'commandReport') gated by logCommandReports flag; reorganized logging to use console.log/error/warning appropriately.
Build Configuration
packages/quick-tsr/tsconfig.json
Added noUnusedLocals: false compiler option to reduce unused variable checks.
Device API
packages/timeline-state-resolver-api/src/device.ts
Updated addressStateReassertsControl method signature in BaseDeviceAPI and Device interfaces to allow newState parameter as AddressState | undefined (previously required); refined accompanying documentation.
Device Schema
packages/timeline-state-resolver/src/$schemas/common-options.json
Added new boolean property syncOnStartup to Device Common Options schema with default true; controlled UI hints indicate availability when Shared Hardware Control is supported.
ATEM Integration
packages/timeline-state-resolver/src/integrations/atem/index.ts
Updated addressStateReassertsControl to handle undefined newState with early return; ignored displayClock.currentTime-only changes; added immediate address-state refresh on connection; wrapped command items with constructor names in emitted payloads for enhanced logging.
ATEM State Comparison
packages/timeline-state-resolver/src/integrations/atem/state.ts
Extended diffAddressStates to handle UpStreamKey comparisons using imported resolveUpstreamKeyerState resolver with configured flags; returns true if resolver output indicates differences, otherwise falls back to deep equality.
Device Instance & Synchronization
packages/timeline-state-resolver/src/service/DeviceInstance.ts, packages/timeline-state-resolver/src/service/stateTracker.ts
Introduced syncOnStartup parameter (defaults to true) passed from config to StateTracker constructor; added debounce logic via doRecalc flag and setImmediate wrapper for recalculation on deviceUpdated; new public method unsetExpectedState clears expected state for addresses.
State Handler Logic
packages/timeline-state-resolver/src/service/stateHandler.ts
Refactored address-state conditional logic; introduced unsetAddresses tracking to identify and unset expected state for addresses not in timeline; redefined reassertsControl computation to prefer addressStateReassertsControl (when available) OR diffAddressStates; added cleanup pass to unset obsolete addresses.
State Handler Tests
packages/timeline-state-resolver/src/service/__tests__/stateHandler.spec.ts
Expanded StateTracker mock with public-style helpers (addresses, deviceAhead, expState, reset); updated deviceTrackerMethodsImpl with reassertsControl flag and address state mutation logic; added test cases for reassert control, device-ahead scenarios, timeline control, and undefined state transitions; modified getNewStateHandler to compute diffStates with added/removed/changed command handling.
State Tracker Tests
packages/timeline-state-resolver/src/service/__tests__/stateTracker.spec.ts
Updated StateTracker constructor calls to accept two-argument signature (diffFn, didSetDevice boolean); adjusted test expectations for device-ahead behavior; added new test suite section for unsetExpectedState covering removal and verification of expected state.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hopping through state and device control,
With syncOnStartup as our goal,
Unsetting unused addresses with care,
Debouncing logic here and there,
The timeline and device now share the dream!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.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 'fix: shared control undefined state' directly relates to the main bug fix: handling undefined state in shared control scenarios, particularly for the Atem integration. It concisely summarizes the primary change.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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.

@PeterC89

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jan 21, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/timeline-state-resolver-api/src/device.ts (1)

109-112: Device.addressStateReassertsControl signature must allow undefined newState.

The Device interface (line 111) declares newState: AddressState, but BaseDeviceAPI (line 154) allows newState: AddressState | undefined. Real usage in stateHandler.ts line 242 passes undefined via optional chaining (addrState from nextState.addressStates?.[addr]), and the ATEM implementation correctly guards against it. The Device interface signature must be updated to match BaseDeviceAPI and reflect actual usage:

-	addressStateReassertsControl?(oldState: AddressState | undefined, newState: AddressState): boolean
+	addressStateReassertsControl?(oldState: AddressState | undefined, newState: AddressState | undefined): boolean
🤖 Fix all issues with AI agents
In `@packages/quick-tsr/src/tsrHandler.ts`:
- Around line 112-114: The handler registered on this.tsr.connectionManager for
the 'connectionEvent:info' event is using console.error incorrectly; change the
logging call in the listener for 'connectionEvent:info' (the callback registered
on this.tsr.connectionManager.on) to use console.info or console.log instead of
console.error so informational connection events use an appropriate log level.
- Around line 99-101: The 'info' event handler is logging with the wrong prefix;
locate the this.tsr.connectionManager.on('info', (msg: string) => { ... })
handler and change the console.log message from "Warning: connectionManager" to
a correct "Info: connectionManager" (or use console.info with
"connectionManager" + msg) so the log prefix matches the event.

In `@packages/timeline-state-resolver/src/`$schemas/common-options.json:
- Around line 31-36: The ui hint for the JSON property "syncOnStartup" contains
an extra leading space before the quoted phrase " Shared Hardware Control";
update the "ui:hint" value in the syncOnStartup schema to remove the leading
space so the quoted phrase reads "Shared Hardware Control" (reference: property
name syncOnStartup and key "ui:hint").

In `@packages/timeline-state-resolver/src/service/__tests__/stateHandler.spec.ts`:
- Line 427: Fix the typo in the test comment that currently reads "device is
ah`ead" by removing the stray backtick so it reads "device is ahead"; update the
comment in the test around the stateHandler.spec.ts assertion where that comment
appears to ensure it is correct and clear.

In `@packages/timeline-state-resolver/src/service/DeviceInstance.ts`:
- Around line 132-143: The deviceUpdated event handler is currently reading the
first argument (address) into the `ahead` variable so `ahead` is a string and
the boolean check always passes; update the handler signature used with
this._stateTracker.on('deviceUpdated', ...) to accept the address first and the
boolean second (e.g., `(address, ahead)` or `(_, ahead)`), then use that second
parameter in the setImmediate callback to conditionally call
this._stateHandler.recalcDiff(); keep the existing doRecalc debounce logic
intact.
🧹 Nitpick comments (3)
packages/timeline-state-resolver/src/integrations/atem/state.ts (1)

255-270: Use correct ME and keyer indices in upstream keyer diff comparison.

Line 256 hardcodes ME index 0 and creates single-element arrays, discarding the ME and keyer indices from state1.index. The address state structure uses [meIndex, keyerId] (as seen in the address state construction), but this is ignored. For multi-ME or multi-keyer systems, the diff will compare against the wrong indices. Pass arrays indexed by keyer ID:

♻️ Suggested fix
-		const output = resolveUpstreamKeyerState(0, [state1.state], [state2.state], {
+		const meIndex = state1.index[0] as number
+		const keyerIndex = state1.index[1] as number
+		const oldKeyers: UpstreamKeyer[] = []
+		const newKeyers: UpstreamKeyer[] = []
+		oldKeyers[keyerIndex] = state1.state
+		newKeyers[keyerIndex] = state2.state
+		const output = resolveUpstreamKeyerState(meIndex, oldKeyers, newKeyers, {
 			sources: true,
 			onAir: true,
 			type: true,
 			mask: true,
 			flyKeyframes: 'all',
 			flyProperties: true,
 			dveSettings: true,
 			chromaSettings: false,
 			advancedChromaSettings: false,
 			lumaSettings: true,
 			patternSettings: true,
 		})
packages/quick-tsr/src/tsrHandler.ts (2)

109-111: Inconsistent log level: warnings logged to console.error.

Warnings are being logged via console.error. While this may be intentional for visibility, it's inconsistent with typical logging conventions where warnings use console.warn.

♻️ Suggested change
 this.tsr.connectionManager.on('connectionEvent:warning', (deviceId: string, warning: string) => {
-  console.error(`Device ${deviceId} connection warning: ${warning}`)
+  console.warn(`Device ${deviceId} connection warning: ${warning}`)
 })

124-140: No-op handlers for slow command events.

The slowSentCommand and slowFulfilledCommand handlers are empty (with commented-out logging). If these events are not needed, consider removing the handlers entirely to avoid confusion. If they're placeholders for future implementation, add a TODO comment.

♻️ Suggested change: Remove or document

Option 1 - Remove the no-op handlers:

 if (tsrSettings.logCommandReports) {
-  this.tsr.connectionManager.on(
-    'connectionEvent:slowSentCommand',
-    (_deviceId: string, _info: SlowSentCommandInfo) => {
-      // console.log(`Device ${device.deviceId} slow sent command: ${_info}`)
-    }
-  )
-  this.tsr.connectionManager.on(
-    'connectionEvent:slowFulfilledCommand',
-    (_deviceId: string, _info: SlowFulfilledCommandInfo) => {
-      // console.log(`Device ${device.deviceId} slow fulfilled command: ${_info}`)
-    }
-  )
   this.tsr.connectionManager.on('connectionEvent:commandReport', (deviceId: string, command: any) => {
     console.log(`Device ${deviceId} command: ${JSON.stringify(command)}`)
   })
 }

Option 2 - Add TODO if these are placeholders:

 this.tsr.connectionManager.on(
   'connectionEvent:slowSentCommand',
   (_deviceId: string, _info: SlowSentCommandInfo) => {
-    // console.log(`Device ${device.deviceId} slow sent command: ${_info}`)
+    // TODO: Implement slow command logging if needed
   }
 )

Comment thread packages/quick-tsr/src/tsrHandler.ts
Comment thread packages/quick-tsr/src/tsrHandler.ts
Comment thread packages/timeline-state-resolver/src/$schemas/common-options.json
Comment thread packages/timeline-state-resolver/src/service/__tests__/stateHandler.spec.ts Outdated
Comment thread packages/timeline-state-resolver/src/service/DeviceInstance.ts
@jstarpl jstarpl changed the title Fix/shared control undefined state fix: shared control undefined state Jan 24, 2026
@mint-dewit

Copy link
Copy Markdown
Member Author

Updated the PR to resolve issues highlighted in the generated review.

return false
} else if (state1.type === AddressType.UpStreamKey && state2.type === AddressType.UpStreamKey) {
const output = resolveUpstreamKeyerState(0, [state1.state], [state2.state], {
sources: true,

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.

Is this diff object the same as the one being used at the point where the command diffing happens?
Perhaps it should be pulled out as a constant that both places can use to keep it in sync?

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.

Yes, I was thinking about that as well. I mostly just dropped this in because the device state and timeline state aren't strictly/deeply equal even though we should treat them as such. I don't think they have to be using the same options but equally I can't think of a reason why they shouldn't. I'll just pull it out so at least the next person is aware of the usage.

@mint-dewit mint-dewit merged commit a03f88d into Sofie-Automation:release53 Jan 27, 2026
19 checks passed
@mint-dewit mint-dewit deleted the fix/shared-control-undefined-state branch January 27, 2026 11:56
@coderabbitai coderabbitai Bot mentioned this pull request Feb 3, 2026
4 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working contribution from BBC Contributions sponsored by BBC (bbc.co.uk)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants