Skip to content

Commit ee65a3a

Browse files
committed
fix: ignore pieces that are replaced before the in-point by another piece.
1 parent 62e0617 commit ee65a3a

File tree

2 files changed

+108
-72
lines changed

2 files changed

+108
-72
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { LookaheadTimelineObject } from './findObjects'
2424
import { hasPieceInstanceDefinitelyEnded } from '../timeline/lib'
2525
import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
2626
import { ReadonlyDeep } from 'type-fest'
27+
import { filterPieceInstancesForNextPartWithOffset } from './lookaheadOffset'
2728

2829
const LOOKAHEAD_OBJ_PRIORITY = 0.1
2930

@@ -118,7 +119,10 @@ export async function getLookeaheadObjects(
118119
part: partInstancesInfo0.next.partInstance,
119120
onTimeline: !!partInstancesInfo0.current?.partInstance?.part?.autoNext, //TODO -QL
120121
nowInPart: partInstancesInfo0.next.nowInPart,
121-
allPieces: partInstancesInfo0.next.pieceInstances,
122+
allPieces: filterPieceInstancesForNextPartWithOffset(
123+
partInstancesInfo0.next.pieceInstances,
124+
playoutModel.playlist.nextTimeOffset
125+
),
122126
calculatedTimings: partInstancesInfo0.next.calculatedTimings,
123127
})
124128
: undefined,

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

Lines changed: 103 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,16 @@ import { getBestPieceInstanceId, LookaheadTimelineObject } from './findObjects'
66
import { PartAndPieces, PieceInstanceWithObjectMap } from './util'
77
import { TimelineEnable } from 'superfly-timeline'
88
import { 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
*/
11485
function 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

Comments
 (0)