Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/timeline-state-resolver-types/src/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ export interface TSRTimelineObjProps {
isLookahead?: boolean
/** Only valid when isLookahead is true. Set so that a lookahead object knows what layer it belongs to */
lookaheadForLayer?: string | number
/** Only valid when isLookahead is true. Offsets the lookahead when a part or piece is queued with an offset */
lookaheadOffset?: number
Comment on lines +29 to +30
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

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function makeDeviceTimelineStateObject<TContent extends TSRTimelineConten

isLookahead: false,
lookaheadForLayer: undefined,
lookaheadOffset: undefined,

instance: {
id: `@${object.id}:0`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,15 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
const holdOnFirstFrame = !isForeground || layerProps.isLookahead
const loopingPlayTime = content.loop && !content.seek && !content.inPoint && !content.length

const seekOffsetByLookahead =
content.seek && layerProps.lookaheadOffset
? content.seek + layerProps.lookaheadOffset
: content.seek
? content.seek
: layerProps.lookaheadOffset
? layerProps.lookaheadOffset
: undefined
Comment on lines +316 to +323
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.


stateLayer = literal<MediaLayer>({
id: layer.id,
layerNo: mapping.layer,
Expand All @@ -324,7 +333,7 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
playing: !layerProps.isLookahead && (content.playing !== undefined ? content.playing : isForeground),

looping: content.loop,
seek: content.seek,
seek: !layerProps.isLookahead ? content.seek : seekOffsetByLookahead,
inPoint: content.inPoint,
length: content.length,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ function setPortStateFromLayer(
} else {
const startTime = layer.instance.originalStart || layer.instance.start

const inPointSeekOffsetByLookahead =
content.inPoint && layer.lookaheadOffset
? content.inPoint + layer.lookaheadOffset
: content.inPoint
? content.inPoint
: layer.lookaheadOffset
? layer.lookaheadOffset
: undefined
Comment on lines +127 to +134
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.


port.timelineObjId = layer.id
port.notOnAir = content.notOnAir || isLookahead
port.outTransition = content.outTransition
Expand All @@ -137,7 +146,7 @@ function setPortStateFromLayer(
pauseTime: content.pauseTime,
playing: isLookahead ? false : content.playing ?? true,

inPoint: content.inPoint,
inPoint: !isLookahead ? content.inPoint : inPointSeekOffsetByLookahead,
length: content.length,

playTime: (content.noStarttime || isLookahead ? null : startTime) || null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export class StateHandler<
lastModified: obj.lastModified,
isLookahead: objExt.isLookahead,
lookaheadForLayer: objExt.lookaheadForLayer,
lookaheadOffset: objExt.lookaheadOffset,
} satisfies Complete<DeviceTimelineStateObject>
}),
}
Expand Down
Loading