Skip to content

Conversation

@justandras
Copy link
Member

@justandras justandras commented Dec 30, 2025

About the Contributor

This pull request is posted on behalf of the BBC.

Type of Contribution

This is a: Feature

Bug fix / Feature / Code improvement / Documentation improvement / Other (please specify)

Current Behavior

There is no way to know about time offsets for the next part on lookaheads.

New Behavior

Added the lookaheadOffset property which is calculated by core. Implemented support for CasparCG and Quantel media.

Testing Instructions

Use the Set next from here feature on a CasparCG clip (pr with the feature: Sofie-Automation/sofie-core#1573)

The lookahead should seek to the desired time to provide accurate previews and prevent flicker on take.

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.

Overview

This pull request adds support for a new lookaheadOffset property that enables precise time offset handling during lookahead operations. This feature allows CasparCG and Quantel media handlers to seek to the correct position when previewing upcoming parts, improving preview accuracy and preventing visual flicker during take transitions.

Changes Made

Type System

  • Added optional lookaheadOffset: number property to TSRTimelineObjProps interface to allow core to convey time offsets for lookahead objects

Type Definition Updates

  • Updated the mock object in __mocks__/objects.ts to include the new lookaheadOffset property in DeviceTimelineStateObject
  • Modified state handler to propagate lookaheadOffset from resolved timeline objects to the device timeline state

CasparCG Integration

  • Implemented seekOffsetByLookahead calculation for media layers during lookahead mode
  • When a layer is in lookahead mode (isLookahead is true), the seek position is calculated by combining content.seek and layerProps.lookaheadOffset
  • Falls back gracefully to existing seek behavior when lookahead offset is not present

Quantel Integration

  • Implemented inPointSeekOffsetByLookahead calculation for clip positioning during lookahead
  • When handling lookahead clips, the inPoint is adjusted by adding the lookaheadOffset to content.inPoint
  • Maintains existing behavior for non-lookahead cases

Testing

The feature has been tested using the "Set next from here" functionality on CasparCG clips, which ensures that lookahead properly seeks to the desired time for accurate previews without visual artifacts during part transitions.

Notes

  • This is a contribution from the BBC
  • References sofie-core PR #1573 for related core functionality
  • All changes maintain backward compatibility by making the new property optional

@justandras justandras requested a review from a team as a code owner December 30, 2025 12:26
@justandras justandras added enhancement New feature or request Contribution from BBC Contributions sponsored by BBC (bbc.co.uk) labels Dec 30, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

Walkthrough

Added optional lookaheadOffset property to timeline object types and integrated it throughout the system. Updated CasparCG and Quantel integrations to compute adjusted seek and inPoint values using this offset during lookahead mode. Modified mock objects and state handler to propagate the property through object construction.

Changes

Cohort / File(s) Summary
Type Definitions
packages/timeline-state-resolver-types/src/mapping.ts
Added optional lookaheadOffset?: number property to TSRTimelineObjProps interface to support lookahead offset configuration.
Mock Objects
packages/timeline-state-resolver/src/__mocks__/objects.ts
Included lookaheadOffset property (set to undefined) in mock DeviceTimelineStateObject to align with updated type shape.
Integration Logic
packages/timeline-state-resolver/src/integrations/casparCG/index.ts, packages/timeline-state-resolver/src/integrations/quantel/state.ts
Implemented offset calculations: CasparCG computes seekOffsetByLookahead by combining layerProps.lookaheadOffset and content.seek; Quantel computes inPointSeekOffsetByLookahead by combining content.inPoint and layer.lookaheadOffset. Both use computed offsets during lookahead mode.
State Management
packages/timeline-state-resolver/src/service/stateHandler.ts
Added lookaheadOffset field propagation from objExt.lookaheadOffset into constructed DeviceTimelineStateObject.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 Lookahead bounds expand with cheer,
Offset calculations crystal clear,
Through states it hops, from seek to inPoint,
Timeline resolved at the perfect juncture! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: lookahead offset' clearly and concisely summarizes the main change: adding lookahead offset functionality across the codebase.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@jstarpl
Copy link
Contributor

jstarpl commented Jan 9, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 9, 2026

✅ 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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @packages/timeline-state-resolver/src/integrations/casparCG/index.ts:
- Around line 316-323: The computation for seekOffsetByLookahead incorrectly
uses truthiness checks and treats 0 as falsy; update the logic in the
seekOffsetByLookahead assignment to explicitly check for undefined on
content.seek and layerProps.lookaheadOffset (e.g., use typeof or !== undefined),
add the two values when both are present (treat undefined as 0 for addition),
return the single numeric value when one is defined, and return undefined only
when both are undefined; locate the code by the seekOffsetByLookahead variable
and references to content.seek and layerProps.lookaheadOffset to apply this fix.

In @packages/timeline-state-resolver/src/integrations/quantel/state.ts:
- Around line 127-134: The ternary logic computing inPointSeekOffsetByLookahead
treats 0 as falsy and returns wrong results; change it to explicitly check for
undefined: treat content.inPoint and layer.lookaheadOffset as numbers (use e.g.
const inPoint = content.inPoint ?? undefined and const offset =
layer.lookaheadOffset ?? undefined), compute the sum when either is defined
(using 0 for the missing operand) and return undefined only when both are
undefined; update the inPointSeekOffsetByLookahead expression to use these
explicit undefined checks instead of truthiness.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dddc23e and 02642db.

📒 Files selected for processing (5)
  • packages/timeline-state-resolver-types/src/mapping.ts
  • packages/timeline-state-resolver/src/__mocks__/objects.ts
  • packages/timeline-state-resolver/src/integrations/casparCG/index.ts
  • packages/timeline-state-resolver/src/integrations/quantel/state.ts
  • packages/timeline-state-resolver/src/service/stateHandler.ts
🔇 Additional comments (5)
packages/timeline-state-resolver/src/__mocks__/objects.ts (1)

28-28: LGTM!

The addition of lookaheadOffset: undefined correctly extends the mock object to include the new optional property, maintaining consistency with the updated type definitions.

packages/timeline-state-resolver-types/src/mapping.ts (1)

29-30: LGTM!

The addition of the optional lookaheadOffset property is well-documented and correctly typed. The comment clearly indicates it's only valid when isLookahead is true, which aligns with the usage patterns in the integrations.

packages/timeline-state-resolver/src/integrations/quantel/state.ts (1)

149-149: Verify the conditional logic for inPoint assignment.

The assignment uses !isLookahead ? content.inPoint : inPointSeekOffsetByLookahead, which means:

  • Non-lookahead clips use content.inPoint directly
  • Lookahead clips use the calculated offset

This logic appears correct, but ensure this aligns with the intended behavior where lookahead objects should respect the offset while regular playback should not.

packages/timeline-state-resolver/src/service/stateHandler.ts (1)

118-118: LGTM!

The addition of lookaheadOffset: objExt.lookaheadOffset correctly propagates the property from the timeline object to the device timeline state object. The satisfies Complete<DeviceTimelineStateObject> constraint ensures type safety.

packages/timeline-state-resolver/src/integrations/casparCG/index.ts (1)

336-336: Verify the conditional logic for seek assignment.

The assignment uses !layerProps.isLookahead ? content.seek : seekOffsetByLookahead, which means:

  • Non-lookahead media uses content.seek directly
  • Lookahead media uses the calculated offset

This logic appears correct and aligns with the intended behavior for the "Set next from here" feature, where lookahead previews need to seek to the offset position.

Comment on lines +316 to +323
const seekOffsetByLookahead =
content.seek && layerProps.lookaheadOffset
? content.seek + layerProps.lookaheadOffset
: content.seek
? content.seek
: layerProps.lookaheadOffset
? layerProps.lookaheadOffset
: undefined
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix truthiness check to handle 0 values correctly.

The current logic uses truthiness checks (content.seek && layerProps.lookaheadOffset) which incorrectly treats 0 as falsy. Since 0 is a valid seek value (seeking to the beginning of a clip), this will cause incorrect behavior when content.seek is 0.

For example:

  • seek=0, lookaheadOffset=undefined → currently returns undefined instead of 0
  • seek=0, lookaheadOffset=10 → currently returns 10 instead of 10 (works by accident)

This is the same issue as in the Quantel integration.

🔧 Proposed fix using explicit undefined checks
-		const seekOffsetByLookahead =
-			content.seek && layerProps.lookaheadOffset
-				? content.seek + layerProps.lookaheadOffset
-				: content.seek
-				? content.seek
-				: layerProps.lookaheadOffset
-				? layerProps.lookaheadOffset
-				: undefined
+		const seekOffsetByLookahead =
+			content.seek !== undefined || layerProps.lookaheadOffset !== undefined
+				? (content.seek ?? 0) + (layerProps.lookaheadOffset ?? 0)
+				: undefined

This approach:

  • Correctly handles 0 as a valid seek value
  • Adds both values when present (treating undefined as 0 for addition)
  • Returns undefined only when both are undefined
  • Is significantly more readable
🤖 Prompt for AI Agents
In @packages/timeline-state-resolver/src/integrations/casparCG/index.ts around
lines 316 - 323, The computation for seekOffsetByLookahead incorrectly uses
truthiness checks and treats 0 as falsy; update the logic in the
seekOffsetByLookahead assignment to explicitly check for undefined on
content.seek and layerProps.lookaheadOffset (e.g., use typeof or !== undefined),
add the two values when both are present (treat undefined as 0 for addition),
return the single numeric value when one is defined, and return undefined only
when both are undefined; locate the code by the seekOffsetByLookahead variable
and references to content.seek and layerProps.lookaheadOffset to apply this fix.

Comment on lines +127 to +134
const inPointSeekOffsetByLookahead =
content.inPoint && layer.lookaheadOffset
? content.inPoint + layer.lookaheadOffset
: content.inPoint
? content.inPoint
: layer.lookaheadOffset
? layer.lookaheadOffset
: undefined
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix truthiness check to handle 0 values correctly.

The current logic uses truthiness checks (content.inPoint && layer.lookaheadOffset) which incorrectly treats 0 as falsy. Since 0 is a valid seek/inPoint value (seeking to the beginning), this will cause incorrect behavior when content.inPoint is 0.

For example:

  • inPoint=0, lookaheadOffset=undefined → currently returns undefined instead of 0
  • inPoint=0, lookaheadOffset=5 → currently returns 5 instead of 5 (works by accident)
🔧 Proposed fix using explicit undefined checks
-		const inPointSeekOffsetByLookahead =
-			content.inPoint && layer.lookaheadOffset
-				? content.inPoint + layer.lookaheadOffset
-				: content.inPoint
-				? content.inPoint
-				: layer.lookaheadOffset
-				? layer.lookaheadOffset
-				: undefined
+		const inPointSeekOffsetByLookahead =
+			content.inPoint !== undefined || layer.lookaheadOffset !== undefined
+				? (content.inPoint ?? 0) + (layer.lookaheadOffset ?? 0)
+				: undefined

This approach:

  • Correctly handles 0 as a valid value
  • Adds both values when present (treating undefined as 0 for addition)
  • Returns undefined only when both are undefined
  • Is more readable than nested ternaries
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const inPointSeekOffsetByLookahead =
content.inPoint && layer.lookaheadOffset
? content.inPoint + layer.lookaheadOffset
: content.inPoint
? content.inPoint
: layer.lookaheadOffset
? layer.lookaheadOffset
: undefined
const inPointSeekOffsetByLookahead =
content.inPoint !== undefined || layer.lookaheadOffset !== undefined
? (content.inPoint ?? 0) + (layer.lookaheadOffset ?? 0)
: undefined
🤖 Prompt for AI Agents
In @packages/timeline-state-resolver/src/integrations/quantel/state.ts around
lines 127 - 134, The ternary logic computing inPointSeekOffsetByLookahead treats
0 as falsy and returns wrong results; change it to explicitly check for
undefined: treat content.inPoint and layer.lookaheadOffset as numbers (use e.g.
const inPoint = content.inPoint ?? undefined and const offset =
layer.lookaheadOffset ?? undefined), compute the sum when either is defined
(using 0 for the missing operand) and return undefined only when both are
undefined; update the inPointSeekOffsetByLookahead expression to use these
explicit undefined checks instead of truthiness.

Comment on lines +29 to +30
/** Only valid when isLookahead is true. Offsets the lookahead when a part or piece is queued with an offset */
lookaheadOffset?: number
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the meaning of what "offsetting the look-ahead" means here needs to be clarified.

Suggested change
/** Only valid when isLookahead is true. Offsets the lookahead when a part or piece is queued with an offset */
lookaheadOffset?: number
/** Only valid when isLookahead is true. If the nature of the content represented by the timeline object has a mutable timing dimension, present the content in a state it should be in after `lookaheadOffset` of it's contents has been played. */
lookaheadOffset?: number

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) enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants