Skip to content

Commit 62e0617

Browse files
committed
chore: improve type safety for lookaheadOffset calculations, ignore objects with no explicit start
1 parent 2352384 commit 62e0617

File tree

2 files changed

+184
-38
lines changed

2 files changed

+184
-38
lines changed

packages/job-worker/src/playout/lookahead/findObjects.ts

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ import { JobContext } from '../../jobs'
1313
import { PartAndPieces, PieceInstanceWithObjectMap } from './util'
1414
import { deserializePieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece'
1515
import { ReadonlyDeep, SetRequired } from 'type-fest'
16+
import { computeLookaheadObject } from './lookaheadOffset'
1617

17-
function getBestPieceInstanceId(piece: ReadonlyDeep<PieceInstance>): string {
18+
export function getBestPieceInstanceId(piece: ReadonlyDeep<PieceInstance>): string {
1819
if (!piece.isTemporary || piece.partInstanceId) {
1920
return unprotectString(piece._id)
2021
}
@@ -105,43 +106,11 @@ export function findLookaheadObjectsForPart(
105106
if (shouldIgnorePiece(partInfo, rawPiece)) continue
106107

107108
const obj = getObjectMapForPiece(rawPiece).get(layer)
108-
if (obj) {
109-
// TODO: forcing these types feels wrong, do we even need to take the object's enable into account?
110-
const objEnable: typeof rawPiece.piece.enable = Array.isArray(obj.enable)
111-
? (obj.enable[0] as typeof rawPiece.piece.enable)
112-
: (obj.enable as typeof rawPiece.piece.enable)
113-
let lookaheadOffset: number | undefined
114-
115-
if (nextTimeOffset) {
116-
const pieceStart = rawPiece.piece.enable.start === 'now' ? 0 : rawPiece.piece.enable.start
117-
const objStart = objEnable.start === 'now' ? 0 : objEnable.start
118-
119-
const offset = nextTimeOffset - pieceStart - objStart
120-
121-
lookaheadOffset = offset > 0 ? offset : undefined
122-
} else {
123-
lookaheadOffset = undefined
124-
}
125-
// TODO: remove console log before PR
126-
// console.log(
127-
// `--------------------------LOOK HERE---------------------------2\n${JSON.stringify(
128-
// { ...obj, lookaheadOffset },
129-
// null,
130-
// 2
131-
// )}`
132-
// )
133-
console.log('lookaheadOffset: ' + nextTimeOffset)
134-
allObjs.push(
135-
literal<LookaheadTimelineObject>({
136-
metaData: undefined,
137-
...obj,
138-
objectType: TimelineObjType.RUNDOWN,
139-
pieceInstanceId: getBestPieceInstanceId(rawPiece),
140-
infinitePieceInstanceId: rawPiece.infinite?.infiniteInstanceId,
141-
partInstanceId: partInstanceId ?? protectString(unprotectString(partInfo.part._id)),
142-
lookaheadOffset,
143-
})
144-
)
109+
110+
// we only consider lookahead objects for lookahead and calculate the lookaheadOffset for each object.
111+
const computedLookaheadObj = computeLookaheadObject(obj, rawPiece, partInfo, partInstanceId, nextTimeOffset)
112+
if (computedLookaheadObj) {
113+
allObjs.push(computedLookaheadObj)
145114
}
146115
}
147116

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids'
2+
import { TimelineObjType } from '@sofie-automation/corelib/dist/dataModel/Timeline'
3+
import { literal } from '@sofie-automation/corelib/dist/lib'
4+
import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString'
5+
import { getBestPieceInstanceId, LookaheadTimelineObject } from './findObjects'
6+
import { PartAndPieces, PieceInstanceWithObjectMap } from './util'
7+
import { TimelineEnable } from 'superfly-timeline'
8+
import { TimelineObjectCoreExt } from '@sofie-automation/blueprints-integration'
9+
10+
export type StartInfo = { start: number } | { while: number } | undefined
11+
12+
/**
13+
* Computes a full {@link LookaheadTimelineObject} for a given piece/object pair,
14+
* including the correct `lookaheadOffset` based on explicit numeric `start` or
15+
* `while > 1` expressions.
16+
*
17+
* This function:
18+
* - Ignores objects whose `enable` is an array (unsupported for lookahead)
19+
* - Extracts a usable numeric start reference from both the object and its parent piece
20+
* - Supports lookahead semantics where `enable.while > 1` acts like an implicit start value
21+
* but the offset is added *to the while value* instead of replacing start
22+
* - Returns `undefined` when lookahead cannot be computed safely
23+
*
24+
* @param obj - The timeline object associated with the piece and layer. If `undefined`,
25+
* no lookahead object is created.
26+
* @param rawPiece - The piece instance containing the object map and its own enable
27+
* expression, which determines the base start time for lookahead.
28+
* @param partInfo - Metadata about the part the piece belongs to, required for
29+
* associating the lookahead object with the correct `partInstanceId`.
30+
* @param partInstanceId - The currently active or next part instance ID. If `null`,
31+
* the function falls back to the part ID from `partInfo`.
32+
* @param nextTimeOffset - An optional offset of the in point of the next part
33+
* used to calculate the lookahead offset. If omitted, no
34+
* lookahead offset is generated.
35+
*
36+
* @returns A fully constructed {@link LookaheadTimelineObject} ready to be pushed
37+
* into the lookahead timeline, or `undefined` when no valid lookahead
38+
* calculation is possible.
39+
*/
40+
export function computeLookaheadObject(
41+
obj: TimelineObjectCoreExt<any, unknown, unknown> | undefined,
42+
rawPiece: PieceInstanceWithObjectMap,
43+
partInfo: PartAndPieces,
44+
partInstanceId: PartInstanceId | null,
45+
nextTimeOffset?: number
46+
): LookaheadTimelineObject | undefined {
47+
if (!obj) return undefined
48+
49+
const enable = obj.enable
50+
51+
if (Array.isArray(enable)) return undefined
52+
53+
const startInfo = getStartInfoFromEnable(enable)
54+
const pieceStartInfo = getStartInfoFromEnable(rawPiece.piece.enable)
55+
56+
// We make sure to only consider objects for lookahead that have an explicit numeric start/while value. (while = 1 and 0 is considered boolean)
57+
if (!pieceStartInfo) return undefined
58+
59+
let lookaheadOffset: number | undefined
60+
// Only calculate lookaheadOffset if needed
61+
if (nextTimeOffset) {
62+
const pieceStart = 'start' in pieceStartInfo ? pieceStartInfo.start : pieceStartInfo.while
63+
lookaheadOffset = computeLookaheadOffset(nextTimeOffset, pieceStart, startInfo)
64+
}
65+
66+
return literal<LookaheadTimelineObject>({
67+
metaData: undefined,
68+
...obj,
69+
objectType: TimelineObjType.RUNDOWN,
70+
pieceInstanceId: getBestPieceInstanceId(rawPiece),
71+
infinitePieceInstanceId: rawPiece.infinite?.infiniteInstanceId,
72+
partInstanceId: partInstanceId ?? protectString(unprotectString(partInfo.part._id)),
73+
lookaheadOffset,
74+
})
75+
}
76+
77+
/**
78+
* Computes a lookahead offset for an object based on the next part's start time,
79+
* the piece's start time, and the object's enable expression (represented as a {@link StartInfo}).
80+
*
81+
* This function supports two mutually exclusive modes:
82+
*
83+
* **1. `start` mode (`{ start: number | undefined }`)**
84+
* - A numeric `start` value is treated as the object's explicit start time.
85+
* - The offset is calculated as:
86+
* ```
87+
* offset = nextTimeOffset - pieceStart - start
88+
* ```
89+
* - Returns `undefined` if the resulting offset is not positive.
90+
*
91+
* **2. `while` mode (`{ while: number | undefined }`)**
92+
* - `while = 0` is treated as a boolean "false" → no lookahead offset.
93+
* - `while = 1` is treated as a boolean "true" equivalent to `start = 0`,
94+
* meaning the object starts immediately.
95+
* - Any `while > 1` value is treated as a timestamp signifying when the object
96+
* is considered to begin, and the lookahead offset is added *on top of*
97+
* the while value. Example:
98+
* ```
99+
* effectiveStart = (while === 1 ? 0 : while)
100+
* offset = nextTimeOffset - pieceStart - effectiveStart
101+
* returned = while + offset
102+
* ```
103+
* - Returns `undefined` if the computed offset is not positive.
104+
*
105+
* @param nextTimeOffset - The upcoming part's start time (or similar time anchor).
106+
* If undefined, no lookahead offset is produced.
107+
* @param pieceStart - The start time of the piece this object belongs to.
108+
* @param info - A `StartInfo` discriminated union describing whether the object
109+
* uses a numeric `start` or a `while` expression.
110+
*
111+
* @returns A positive lookahead offset, or `undefined` if lookahead cannot be
112+
* determined or would be non-positive.
113+
*/
114+
function computeLookaheadOffset(
115+
nextTimeOffset: number | undefined,
116+
pieceStart: number,
117+
info: StartInfo
118+
): number | undefined {
119+
if (nextTimeOffset === undefined || !info) return undefined
120+
121+
if ('start' in info) {
122+
const offset = nextTimeOffset - pieceStart - info.start
123+
return offset > 0 ? offset : undefined
124+
}
125+
126+
if ('while' in info) {
127+
// while == 0 is treated as false
128+
if (info.while !== 0) {
129+
// while == 1 is treated as true which is equal to start == 0. Any other value means the object starts at that timestamp and doesn't have an end.
130+
const offset = nextTimeOffset - pieceStart - info.while === 1 ? 0 : info.while
131+
return offset > 0 ? info.while + offset : undefined
132+
}
133+
}
134+
135+
return undefined
136+
}
137+
138+
/**
139+
* Extracts a numeric start reference from a {@link TimelineEnable} object,
140+
* returning a {@link StartInfo} describing how lookahead should be calculated.
141+
*
142+
* The function handles two mutually exclusive modes:
143+
*
144+
* **1. `start` mode (`{ start: number }`)**
145+
* - If `enable.start` is a numeric value, it is returned as `start`.
146+
* - If `enable.start` is the string `"now"`, it is treated as `0`.
147+
*
148+
* **2. `while` mode (`{ while: number }`)**
149+
* - If `enable.while` is numeric and greater than 1, it is returned as `while`.
150+
* - This indicates the object should be considered as starting at this
151+
* timestamp, with any lookahead offset added on top.
152+
*
153+
* If no usable numeric `start` or `while` expression exists, the function returns `undefined`.
154+
*
155+
* @param enable - The timeline object's enable expression to extract start info from.
156+
* @returns A {@link StartInfo} object containing either `start` or `while`, or `undefined`
157+
* if no numeric value is available for lookahead calculations.
158+
*/
159+
function getStartInfoFromEnable(enable: TimelineEnable): StartInfo {
160+
// Case: start is a number
161+
if (typeof enable.start === 'number') {
162+
return { start: enable.start }
163+
}
164+
165+
// Case: start is "now"
166+
if (enable.start === 'now') {
167+
return { start: 0 }
168+
}
169+
170+
// Case: while is numeric and > 1 → offset must be added to while
171+
if (typeof enable.while === 'number' && enable.while > 1) {
172+
return { while: enable.while }
173+
}
174+
175+
// No usable numeric expressions
176+
return undefined
177+
}

0 commit comments

Comments
 (0)