/
lookahead.ts
387 lines (328 loc) · 13.2 KB
/
lookahead.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
import { Meteor } from 'meteor/meteor'
import * as _ from 'underscore'
import { LookaheadMode, Timeline as TimelineTypes, OnGenerateTimelineObj } from 'tv-automation-sofie-blueprints-integration'
import { RundownData, Rundown } from '../../../lib/collections/Rundowns'
import { Studio, MappingExt } from '../../../lib/collections/Studios'
import { TimelineObjGeneric, TimelineObjRundown, fixTimelineId, TimelineObjType } from '../../../lib/collections/Timeline'
import { Part } from '../../../lib/collections/Parts'
import { Piece } from '../../../lib/collections/Pieces'
import { getOrderedPiece } from './pieces'
import { clone, literal } from '../../../lib/lib'
const LOOKAHEAD_OBJ_PRIORITY = 0.1
export function getLookeaheadObjects (rundownData: RundownData, studio: Studio): Array<TimelineObjGeneric> {
const activeRundown = rundownData.rundown
const currentPart = activeRundown.currentPartId ? rundownData.partsMap[activeRundown.currentPartId] : undefined
const timelineObjs: Array<TimelineObjGeneric> = []
const mutateAndPushObject = (rawObj: TimelineObjRundown, i: string, enable: TimelineObjRundown['enable'], mapping: MappingExt, priority: number) => {
const obj: TimelineObjGeneric = clone(rawObj)
obj.id = `lookahead_${i}_${obj.id}`
obj.priority = priority
obj.enable = enable
obj.isLookahead = true
delete obj.keyframes
delete obj.inGroup // force it to be cleared
if (mapping.lookahead === LookaheadMode.PRELOAD) {
obj.lookaheadForLayer = obj.layer
obj.layer += '_lookahead'
}
timelineObjs.push(obj)
}
_.each(studio.mappings || {}, (mapping: MappingExt, layerId: string) => {
const lookaheadDepth = mapping.lookahead === LookaheadMode.PRELOAD ? mapping.lookaheadDepth || 1 : 1 // TODO - test other modes
const lookaheadObjs = findLookaheadForlayer(rundownData, layerId, mapping.lookahead, lookaheadDepth)
// Add the objects that have some timing info
_.each(lookaheadObjs.timed, (entry, i) => {
const enable: TimelineTypes.TimelineEnable = {
start: 1 // Absolute 0 without a group doesnt work
}
if (i !== 0) {
const prevObj = lookaheadObjs.timed[i - 1].obj
const prevHasDelayFlag = (prevObj.classes || []).indexOf('_lookahead_start_delay') !== -1
// Start with previous piece
const startOffset = prevHasDelayFlag ? 2000 : 0
enable.start = `#${prevObj.id}.start + ${startOffset}`
}
if (!entry.obj.id) throw new Meteor.Error(500, 'lookahead: timeline obj id not set')
const finiteDuration = entry.partId === activeRundown.currentPartId || (currentPart && currentPart.autoNext && entry.partId === activeRundown.nextPartId)
enable.end = finiteDuration ? `#${entry.obj.id}.start` : undefined
mutateAndPushObject(entry.obj, `timed${i}`, enable, mapping, LOOKAHEAD_OBJ_PRIORITY)
})
// Add each of the future objects, that have no end point
const futureObjCount = lookaheadObjs.future.length
const futurePriorityScale = LOOKAHEAD_OBJ_PRIORITY / (futureObjCount + 1)
_.each(lookaheadObjs.future, (entry, i) => {
if (!entry.obj.id) throw new Meteor.Error(500, 'lookahead: timeline obj id not set')
// Prioritise so that the earlier ones are higher, decreasing within the range 'reserved' for lookahead
const priority = futurePriorityScale * (futureObjCount - i)
mutateAndPushObject(entry.obj, `future${i}`, { while: '1' }, mapping, priority)
// We use while: 1 for the enabler, as any time before it should be active will be filled by either a playing object, or a timed lookahead.
// And this allows multiple futures to be timed in a way that allows them to co-exist
})
})
return timelineObjs
}
interface PartInfo {
partId: string
segmentId: string
part: Part
}
interface PartInfoWithPieces extends PartInfo {
pieces: Piece[]
}
export interface LookaheadObjectEntry {
obj: TimelineObjRundown
partId: string
}
export interface LookaheadResult {
timed: Array<LookaheadObjectEntry>
future: Array<LookaheadObjectEntry>
}
export function findLookaheadForlayer (
rundownData: RundownData,
layer: string,
mode: LookaheadMode,
lookaheadDepth: number
): LookaheadResult {
let activeRundown: Rundown = rundownData.rundown
if (mode === undefined || mode === LookaheadMode.NONE) {
return { timed: [], future: [] }
}
// find all pieces that touch the layer
const piecesUsingLayer = _.filter(rundownData.pieces, (piece: Piece) => {
return !!(
piece.content &&
piece.content.timelineObjects &&
_.find(piece.content.timelineObjects, (o) => (o && o.layer === layer))
)
})
if (piecesUsingLayer.length === 0) {
return { timed: [], future: [] }
}
// If mode is retained, and only one instance exists in the rundown, then we can take a shortcut
if (mode === LookaheadMode.RETAIN && piecesUsingLayer.length === 1) {
const piece = piecesUsingLayer[0]
if (piece.content && piece.content.timelineObjects) {
const obj = piece.content.timelineObjects.find(o => o !== null && o.layer === layer)
if (obj) {
fixTimelineId(obj)
return {
timed: [], // TODO - is this correct?
future: [{ obj: obj as TimelineObjRundown, partId: piece.partId }]
}
}
}
return { timed: [], future: [] }
}
// have pieces grouped by part, so we can look based on rank to choose the correct one
const piecesUsingLayerByPart: {[partId: string]: Piece[]} = {}
piecesUsingLayer.forEach(i => {
if (!piecesUsingLayerByPart[i.partId]) {
piecesUsingLayerByPart[i.partId] = []
}
piecesUsingLayerByPart[i.partId].push(i)
})
const { timeOrderedParts, currentPartIndex, currentSegmentId } = getPartsOrderedByTime(rundownData)
if (timeOrderedParts.length === 0) {
return { timed: [], future: [] }
}
const timeOrderedPartsWithPieces: PartInfoWithPieces[] = timeOrderedParts.map(part => ({
...part,
pieces: piecesUsingLayerByPart[part.partId] || []
}))
// Start by taking the value from the current (if any), or search forwards
let startingPartOnLayer: PartInfoWithPieces | undefined
let startingPartOnLayerIndex: number = -1
for (let i = currentPartIndex; i < timeOrderedPartsWithPieces.length; i++) {
const v = timeOrderedPartsWithPieces[i]
if (v.pieces.length > 0) {
startingPartOnLayer = v
startingPartOnLayerIndex = i
break
}
}
// If set to retain, then look backwards.
// This sets the previous usage of the layer as the current part. This lets the algorithm include the object even though it has already played and finished
if (mode === LookaheadMode.RETAIN) {
for (let i = currentPartIndex - 1; i >= 0; i--) {
const part = timeOrderedPartsWithPieces[i]
// abort if the piece potential match is from another segment
if (startingPartOnLayer && part.segmentId !== currentSegmentId) {
break
}
if (part.pieces.length > 0) {
startingPartOnLayer = part
startingPartOnLayerIndex = i
break
}
}
}
// No possible part was found using the layer, so nothing to lookahead
if (!startingPartOnLayer) {
return { timed: [], future: [] }
}
const res: LookaheadResult = {
timed: [],
future: []
}
const partId = startingPartOnLayer.partId
const startingPartIsFuture = startingPartOnLayer.partId !== activeRundown.currentPartId
findObjectsForPart(rundownData, layer, timeOrderedPartsWithPieces, startingPartOnLayerIndex, startingPartOnLayer)
.forEach(o => (startingPartIsFuture ? res.future : res.timed).push({ obj: o, partId: partId }))
// Loop over future parts until we have enough objects, or run out of parts
let nextPartOnLayerIndex = startingPartOnLayerIndex
while (nextPartOnLayerIndex !== -1 && res.future.length < lookaheadDepth) {
nextPartOnLayerIndex = _.findIndex(timeOrderedPartsWithPieces, (v, i) => i > nextPartOnLayerIndex && v.pieces.length > 0)
if (nextPartOnLayerIndex !== -1) {
const nextPartOnLayer = timeOrderedPartsWithPieces[nextPartOnLayerIndex]
const partId = nextPartOnLayer.partId
findObjectsForPart(rundownData, layer, timeOrderedPartsWithPieces, nextPartOnLayerIndex, nextPartOnLayer)
.forEach(o => res.future.push({ obj: o, partId: partId }))
}
}
return res
}
function getPartsOrderedByTime (rundownData: RundownData) {
// This could be cached across all lookahead layers, as it doesnt care about layer
const activeRundown = rundownData.rundown
// calculate ordered list of parts, which can be cached for other layers
const parts = rundownData.parts.map(part => ({
partId: part._id,
rank: part._rank,
segmentId: part.segmentId,
part: part
}))
parts.sort((a, b) => {
if (a.rank < b.rank) {
return -1
}
if (a.rank > b.rank) {
return 1
}
return 0
})
let currentPartIndex = 0
let currentSegmentId: string | undefined
const currentIndex = parts.findIndex(l => l.partId === activeRundown.currentPartId)
let partInfos: PartInfo[] = []
if (currentIndex >= 0) {
// Find the current part, and the parts before
partInfos = partInfos.concat(parts.slice(0, currentIndex + 1))
currentSegmentId = partInfos[partInfos.length - 1].segmentId
currentPartIndex = currentIndex
}
// Find the next part
const nextPartIndex = activeRundown.nextPartId
? parts.findIndex(l => l.partId === activeRundown.nextPartId)
: (currentIndex >= 0 ? currentIndex + 1 : -1)
if (nextPartIndex >= 0) {
// Add the future parts to the array
partInfos = partInfos.concat(...parts.slice(nextPartIndex))
}
const timeOrderedParts = partInfos.map(partInfo => ({
partId: partInfo.partId,
segmentId: partInfo.segmentId,
part: partInfo.part
}))
return {
timeOrderedParts,
currentPartIndex,
currentSegmentId
}
}
function findObjectsForPart (rundownData: RundownData, layer: string, timeOrderedPartsWithPieces: PartInfoWithPieces[], startingPartOnLayerIndex: number, startingPartOnLayer: PartInfoWithPieces): (TimelineObjRundown & OnGenerateTimelineObj)[] {
const activeRundown = rundownData.rundown
// Sanity check, if no part to search, then abort
if (!startingPartOnLayer || startingPartOnLayer.pieces.length === 0) {
return []
}
let allObjs: TimelineObjRundown[] = []
startingPartOnLayer.pieces.forEach(i => {
if (i.content && i.content.timelineObjects) {
_.each(i.content.timelineObjects, (obj) => {
if (obj) {
fixTimelineId(obj)
allObjs.push(literal<TimelineObjRundown & OnGenerateTimelineObj>({
...obj,
_id: '', // set later
studioId: '', // set later
objectType: TimelineObjType.RUNDOWN,
rundownId: rundownData.rundown._id,
pieceId: i._id,
infinitePieceId: i.infiniteId
}))
}
})
}
})
// let allObjs: TimelineObjRundown[] = _.compact(rawObjs)
if (allObjs.length === 0) {
// Should never happen. suggests something got 'corrupt' during this process
return []
}
if (allObjs.length > 1) {
if (startingPartOnLayer.part) {
const orderedItems = getOrderedPiece(startingPartOnLayer.part)
let allowTransition = false
let classesFromPreviousPart: string[] = []
if (startingPartOnLayerIndex >= 1 && activeRundown.currentPartId) {
const prevPieceGroup = timeOrderedPartsWithPieces[startingPartOnLayerIndex - 1]
allowTransition = !prevPieceGroup.part.disableOutTransition
classesFromPreviousPart = prevPieceGroup.part.classesForNext || []
}
const transObj = orderedItems.find(i => !!i.isTransition)
const transObj2 = transObj ? startingPartOnLayer.pieces.find(l => l._id === transObj._id) : undefined
const hasTransition = (
allowTransition &&
transObj2 &&
transObj2.content &&
transObj2.content.timelineObjects &&
transObj2.content.timelineObjects.find(o => o != null && o.layer === layer)
)
const res: TimelineObjRundown[] = []
orderedItems.forEach(i => {
if (!startingPartOnLayer || (!allowTransition && i.isTransition)) {
return
}
const piece = startingPartOnLayer.pieces.find(l => l._id === i._id)
if (!piece || !piece.content || !piece.content.timelineObjects) {
return
}
// If there is a transition and this piece is abs0, it is assumed to be the primary piece and so does not need lookahead
if (
hasTransition &&
!i.isTransition &&
piece.enable.start === 0 // <-- need to discuss this!
) {
return
}
// Note: This is assuming that there is only one use of a layer in each piece.
const obj = piece.content.timelineObjects.find(o => o !== null && o.layer === layer)
if (obj) {
// Try and find a keyframe that is used when in a transition
let transitionKF: TimelineTypes.TimelineKeyframe | undefined = undefined
if (allowTransition) {
transitionKF = _.find(obj.keyframes || [], kf => kf.enable.while === '.is_transition')
// TODO - this keyframe matching is a hack, and is very fragile
if (!transitionKF && classesFromPreviousPart && classesFromPreviousPart.length > 0) {
// Check if the keyframe also uses a class to match. This handles a specific edge case
transitionKF = _.find(obj.keyframes || [], kf => _.any(classesFromPreviousPart, cl => kf.enable.while === `.is_transition & .${cl}`))
}
}
const newContent = Object.assign({}, obj.content, transitionKF ? transitionKF.content : {})
res.push(literal<TimelineObjRundown & OnGenerateTimelineObj>({
...obj,
_id: '', // set later
studioId: '', // set later
objectType: TimelineObjType.RUNDOWN,
rundownId: rundownData.rundown._id,
pieceId: piece._id,
infinitePieceId: piece.infiniteId,
content: newContent
}))
}
})
return res
}
}
return allObjs
}