@@ -6,19 +6,16 @@ import { getBestPieceInstanceId, LookaheadTimelineObject } from './findObjects'
66import { PartAndPieces , PieceInstanceWithObjectMap } from './util'
77import { TimelineEnable } from 'superfly-timeline'
88import { TimelineObjectCoreExt } from '@sofie-automation/blueprints-integration'
9-
10- export type StartInfo = { start : number } | { while : number } | undefined
9+ import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune'
1110
1211/**
1312 * 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.
13+ * including the correct `lookaheadOffset` based on explicit numeric `start` or `while` expressions.
1614 *
1715 * This function:
1816 * - Ignores objects whose `enable` is an array (unsupported for lookahead)
1917 * - 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
18+ * - Supports lookahead semantics where `enable.while >= 1` acts like an implicit start value
2219 * - Returns `undefined` when lookahead cannot be computed safely
2320 *
2421 * @param obj - The timeline object associated with the piece and layer. If `undefined`,
@@ -50,17 +47,16 @@ export function computeLookaheadObject(
5047
5148 if ( Array . isArray ( enable ) ) return undefined
5249
53- const startInfo = getStartInfoFromEnable ( enable )
54- const pieceStartInfo = getStartInfoFromEnable ( rawPiece . piece . enable )
50+ const objStart = getStartValueFromEnable ( enable )
51+ const pieceStart = getStartValueFromEnable ( rawPiece . piece . enable )
5552
5653 // 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
54+ if ( pieceStart === undefined ) return undefined
5855
5956 let lookaheadOffset : number | undefined
6057 // Only calculate lookaheadOffset if needed
6158 if ( nextTimeOffset ) {
62- const pieceStart = 'start' in pieceStartInfo ? pieceStartInfo . start : pieceStartInfo . while
63- lookaheadOffset = computeLookaheadOffset ( nextTimeOffset , pieceStart , startInfo )
59+ lookaheadOffset = computeLookaheadOffset ( nextTimeOffset , pieceStart , objStart )
6460 }
6561
6662 return literal < LookaheadTimelineObject > ( {
@@ -70,108 +66,144 @@ export function computeLookaheadObject(
7066 pieceInstanceId : getBestPieceInstanceId ( rawPiece ) ,
7167 infinitePieceInstanceId : rawPiece . infinite ?. infiniteInstanceId ,
7268 partInstanceId : partInstanceId ?? protectString ( unprotectString ( partInfo . part . _id ) ) ,
73- lookaheadOffset,
69+ ... ( lookaheadOffset !== undefined ? { lookaheadOffset } : { } ) ,
7470 } )
7571}
7672
7773/**
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.
74+ * Computes a lookahead offset for an object based on the piece's start time
75+ * and the object's start time, relative to the next part's start time.
10476 *
10577 * @param nextTimeOffset - The upcoming part's start time (or similar time anchor).
10678 * If undefined, no lookahead offset is produced.
10779 * @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.
80+ * @param objStart - The explicit start time of the object (relative to the piece's start time).
11081 *
11182 * @returns A positive lookahead offset, or `undefined` if lookahead cannot be
11283 * determined or would be non-positive.
11384 */
11485function computeLookaheadOffset (
11586 nextTimeOffset : number | undefined ,
11687 pieceStart : number ,
117- info : StartInfo
88+ objStart ?: number
11889) : 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- }
90+ if ( nextTimeOffset === undefined || objStart === undefined ) return undefined
13491
135- return undefined
92+ const offset = nextTimeOffset - pieceStart - objStart
93+ return offset > 0 ? offset : undefined
13694}
13795
13896/**
139- * Extracts a numeric start reference from a {@link TimelineEnable} object,
140- * returning a {@link StartInfo} describing how lookahead should be calculated.
97+ * Extracts a numeric start reference from a {@link TimelineEnable} object
14198 *
142- * The function handles two mutually exclusive modes :
99+ * The function handles two mutually exclusive cases :
143100 *
144101 * **1. `start` mode (`{ start: number }`)**
145102 * - If `enable.start` is a numeric value, it is returned as `start`.
146103 * - If `enable.start` is the string `"now"`, it is treated as `0`.
147104 *
148105 * **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.
106+ * - If `enable.while` is numeric and greater than 1, it's value is returned as is.
107+ * - If `enable.while` is numeric and equal to 1 it's treated as `0`.
152108 *
153109 * If no usable numeric `start` or `while` expression exists, the function returns `undefined`.
154110 *
155111 * @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.
112+ * @returns the relative start value of the object or undefined if there is no explicit value.
158113 */
159- function getStartInfoFromEnable ( enable : TimelineEnable ) : StartInfo {
114+ function getStartValueFromEnable ( enable : TimelineEnable ) : number | undefined {
160115 // Case: start is a number
161116 if ( typeof enable . start === 'number' ) {
162- return { start : enable . start }
117+ return enable . start
163118 }
164119
165120 // Case: start is "now"
166121 if ( enable . start === 'now' ) {
167- return { start : 0 }
122+ return 0
168123 }
169124
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 }
125+ // Case: while is numeric
126+ if ( typeof enable . while === 'number' ) {
127+ // while > 1 we treat it as a start value
128+ if ( enable . while > 1 ) {
129+ return enable . while
130+ }
131+ // while === 1 we treat it as a `0` start value
132+ else if ( enable . while === 1 ) {
133+ return 0
134+ }
173135 }
174136
175137 // No usable numeric expressions
176138 return undefined
177139}
140+
141+ /**
142+ * Filters piece instances for the "next" part when a `nextTimeOffset` is defined.
143+ *
144+ * This function ensures that we only take into account each layer's relevant piece before
145+ * or at the `nextTimeOffset`, while also preserving all pieces starting after the offset.
146+ *
147+ * This is needed to ignore pieces that start before the offset, but then are replaced by another piece at the offset.
148+ * Without ignoring them the lookahead logic would treat the next part as if it was queued from it's start.
149+ *
150+ * **Filtering rules:**
151+ * - If `nextTimeOffset` is not set (0, null, undefined), the original list is returned.
152+ * - Pieces are grouped based on their `outputLayerId`.
153+ * - For each layer:
154+ * - We only keep pieces with the **latest start time** where `start/while <= nextTimeOffset`
155+ * - All pieces *after* `nextTimeOffset` are kept for future lookaheads.
156+ *
157+ * The result is a flattened list of the selected pieces across all layers.
158+ *
159+ * @param {PieceInstanceWithTimings[] } pieces
160+ * The list of piece instances to filter.
161+ *
162+ * @param {number | null | undefined } nextTimeOffset
163+ * The time offset (in part time) that defines relevance.
164+ * Pieces are compared based on their enable.start value.
165+ *
166+ * @returns {PieceInstanceWithTimings[] }
167+ * A filtered list of pieces containing only the relevant pieces per layer.
168+ */
169+ export function filterPieceInstancesForNextPartWithOffset (
170+ pieces : PieceInstanceWithTimings [ ] ,
171+ nextTimeOffset : number | null | undefined
172+ ) : PieceInstanceWithTimings [ ] {
173+ if ( ! nextTimeOffset ) return pieces
174+
175+ // Group pieces by layer
176+ const layers = new Map < string , PieceInstanceWithTimings [ ] > ( )
177+ for ( const p of pieces ) {
178+ const layer = p . piece . outputLayerId || '__noLayer__'
179+ if ( ! layers . has ( layer ) ) layers . set ( layer , [ ] )
180+ layers . get ( layer ) ! . push ( p )
181+ }
182+
183+ const result : PieceInstanceWithTimings [ ] = [ ]
184+
185+ for ( const layerPieces of layers . values ( ) ) {
186+ const beforeOrAt : PieceInstanceWithTimings [ ] = [ ]
187+ const after : PieceInstanceWithTimings [ ] = [ ]
188+
189+ for ( const piece of layerPieces ) {
190+ const pieceStart = getStartValueFromEnable ( piece . piece . enable )
191+
192+ if ( pieceStart !== undefined ) {
193+ if ( pieceStart <= nextTimeOffset ) beforeOrAt . push ( piece )
194+ else after . push ( piece )
195+ }
196+ }
197+
198+ // Pick the relevant piece before/at nextTimeOffset
199+ if ( beforeOrAt . length > 0 ) {
200+ const best = beforeOrAt . reduce ( ( a , b ) => ( a . piece . enable . start > b . piece . enable . start ? a : b ) )
201+ result . push ( best )
202+ }
203+
204+ // Keep all pieces after nextTimeOffset for future lookaheads.
205+ result . push ( ...after )
206+ }
207+
208+ return result
209+ }
0 commit comments