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