Skip to content

Commit ad450c3

Browse files
committedMar 13, 2025··
feat: add object to timeline to trigger a regeneration at point in time

File tree

17 files changed

+196
-28
lines changed

17 files changed

+196
-28
lines changed
 

‎meteor/server/api/__tests__/cleanup.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ async function setDefaultDatatoDB(env: DefaultEnvironment, now: number) {
410410
generationVersions: {} as any,
411411
timelineBlob: '' as any,
412412
timelineHash: '' as any,
413+
regenerateTimelineToken: undefined,
413414
})
414415
await TimelineDatastore.mutableCollection.insertAsync({
415416
_id: getRandomId(),

‎meteor/server/lib/__tests__/lib.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ describe('server/lib', () => {
4242
generated: 1234,
4343
timelineBlob: serializeTimelineBlob(mystudioObjs),
4444
generationVersions: {} as any,
45+
regenerateTimelineToken: undefined,
4546
})
4647

4748
const mystudio2Objs: Array<TimelineObjGeneric> = [
@@ -62,6 +63,7 @@ describe('server/lib', () => {
6263
generated: 1234,
6364
timelineBlob: serializeTimelineBlob(mystudio2Objs),
6465
generationVersions: {} as any,
66+
regenerateTimelineToken: undefined,
6567
})
6668

6769
const options: SaveIntoDbHooks<any> = {

‎packages/corelib/src/dataModel/Timeline.ts

+17
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
PartPlaybackCallbackData,
1919
PiecePlaybackCallbackData,
2020
PlayoutChangedType,
21+
TriggerRegenerationCallbackData,
2122
} from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI'
2223
export { PartPlaybackCallbackData, PiecePlaybackCallbackData }
2324

@@ -74,6 +75,16 @@ export interface TimelineObjPieceAbstract extends Omit<TimelineObjRundown, 'enab
7475
}
7576
}
7677

78+
export interface TimelineObjRegenerateTrigger extends TimelineObjRundown {
79+
// used for sending callbacks
80+
content: {
81+
deviceType: TSR.DeviceType.ABSTRACT
82+
type: 'callback'
83+
callBack: PlayoutChangedType.TRIGGER_REGENERATION
84+
callBackData: TriggerRegenerationCallbackData
85+
}
86+
}
87+
7788
export function updateLookaheadLayer(obj: TimelineObjRundown): void {
7889
// Set lookaheadForLayer to reference the original layer:
7990
obj.lookaheadForLayer = obj.layer
@@ -102,4 +113,10 @@ export interface TimelineComplete {
102113
timelineBlob: TimelineBlob
103114
/** Version numbers of sofie at the time the timeline was generated */
104115
generationVersions: TimelineCompleteGenerationVersions
116+
117+
/**
118+
* A special regenerate object can be on the timeline to trigger a regeneration at a certain point
119+
* It uses this token to verify that the regeneration request is valid
120+
*/
121+
regenerateTimelineToken: string | undefined
105122
}

‎packages/job-worker/src/playout/__tests__/playout.test.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartIns
4343
import { ReadonlyDeep } from 'type-fest'
4444
import { adjustFakeTime, getCurrentTime, useFakeCurrentTime } from '../../__mocks__/time'
4545
import { PieceLifespan } from '@sofie-automation/blueprints-integration'
46-
import { PlayoutChangedType } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI'
46+
import {
47+
PlayoutChangedResult,
48+
PlayoutChangedType,
49+
} from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI'
4750
import { ProcessedShowStyleCompound } from '../../jobs'
4851
import { handleOnPlayoutPlaybackChanged } from '../timings'
4952
import { sleep } from '@sofie-automation/shared-lib/dist/lib/lib'
@@ -592,7 +595,7 @@ describe('Playout API', () => {
592595
time: now,
593596
},
594597
},
595-
...pieceInstances.map((pieceInstance) => {
598+
...pieceInstances.map((pieceInstance): PlayoutChangedResult => {
596599
return {
597600
type: PlayoutChangedType.PIECE_PLAYBACK_STARTED,
598601
objId: 'objectId',
@@ -685,7 +688,7 @@ describe('Playout API', () => {
685688
time: now,
686689
},
687690
},
688-
...pieceInstances.map((pieceInstance) => {
691+
...pieceInstances.map((pieceInstance): PlayoutChangedResult => {
689692
return {
690693
type: PlayoutChangedType.PIECE_PLAYBACK_STOPPED,
691694
objId: 'objectId',

‎packages/job-worker/src/playout/__tests__/resolvedPieces.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ describe('Resolved Pieces', () => {
412412
partStarted,
413413
// Approximate `calculatedTimings`, for the partInstances which already have it cached
414414
calculatedTimings: getPartTimingsOrDefaults(partInstance, pieceInstances),
415+
regenerateTimelineAt: undefined,
415416
}
416417
}
417418

‎packages/job-worker/src/playout/__tests__/timeline.test.ts

+13-12
Original file line numberDiff line numberDiff line change
@@ -355,18 +355,19 @@ async function doOnPlayoutPlaybackChanged(
355355
}
356356
: undefined,
357357
// The piece controlObjects start offset into the part, so need a manual offset
358-
...Object.entries<number | null>(timings.pieceOffsets).map(([pieceInstanceId, offset]) =>
359-
offset !== null
360-
? {
361-
type: PlayoutChangedType.PIECE_PLAYBACK_STARTED,
362-
data: {
363-
partInstanceId: timings.partId,
364-
pieceInstanceId: protectString(pieceInstanceId),
365-
time: timings.baseTime + offset,
366-
},
367-
objId: getPieceControlObjectId(protectString(pieceInstanceId)),
368-
}
369-
: undefined
358+
...Object.entries<number | null>(timings.pieceOffsets).map(
359+
([pieceInstanceId, offset]): PlayoutChangedResult | undefined =>
360+
offset !== null
361+
? {
362+
type: PlayoutChangedType.PIECE_PLAYBACK_STARTED,
363+
data: {
364+
partInstanceId: timings.partId,
365+
pieceInstanceId: protectString(pieceInstanceId),
366+
time: timings.baseTime + offset,
367+
},
368+
objId: getPieceControlObjectId(protectString(pieceInstanceId)),
369+
}
370+
: undefined
370371
),
371372
]),
372373
})

‎packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ describe('Lookahead', () => {
276276
partStarted: getCurrentTime() + 546,
277277
pieceInstances: ['1', '2'] as any,
278278
calculatedTimings: { inTransitionStart: null } as any,
279+
regenerateTimelineAt: undefined,
279280
}
280281

281282
const expectedPrevious = {
@@ -299,6 +300,7 @@ describe('Lookahead', () => {
299300
partStarted: getCurrentTime() + 865,
300301
pieceInstances: ['3', '4'] as any,
301302
calculatedTimings: { inTransitionStart: null } as any,
303+
regenerateTimelineAt: undefined,
302304
}
303305
const expectedCurrent = {
304306
part: partInstancesInfo.current.partInstance,
@@ -319,6 +321,7 @@ describe('Lookahead', () => {
319321
partStarted: getCurrentTime() + 142,
320322
pieceInstances: ['5'] as any,
321323
calculatedTimings: { inTransitionStart: null } as any,
324+
regenerateTimelineAt: undefined,
322325
}
323326
const expectedNext = {
324327
part: partInstancesInfo.next.partInstance,

‎packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -812,14 +812,16 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
812812

813813
setTimeline(
814814
timelineObjs: TimelineObjGeneric[],
815-
generationVersions: TimelineCompleteGenerationVersions
815+
generationVersions: TimelineCompleteGenerationVersions,
816+
regenerateTimelineToken: string | undefined
816817
): ReadonlyDeep<TimelineComplete> {
817818
this.timelineImpl = {
818819
_id: this.context.studioId,
819820
timelineHash: getRandomId(), // randomized on every timeline change
820821
generated: getCurrentTime(),
821822
timelineBlob: serializeTimelineBlob(timelineObjs),
822823
generationVersions: generationVersions,
824+
regenerateTimelineToken: regenerateTimelineToken,
823825
}
824826
this.#timelineHasChanged = true
825827

‎packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ describe('buildTimelineObjsForRundown', () => {
201201
partInstance: createMockPartInstance('part0'),
202202
pieceInstances: [],
203203
calculatedTimings: DEFAULT_PART_TIMINGS,
204+
regenerateTimelineAt: undefined,
204205
},
205206
}
206207

@@ -221,6 +222,7 @@ describe('buildTimelineObjsForRundown', () => {
221222
partInstance: createMockPartInstance('part0'),
222223
pieceInstances: [createMockPieceInstance('piece0')],
223224
calculatedTimings: DEFAULT_PART_TIMINGS,
225+
regenerateTimelineAt: undefined,
224226
},
225227
}
226228

@@ -254,6 +256,7 @@ describe('buildTimelineObjsForRundown', () => {
254256
),
255257
pieceInstances: [createMockPieceInstance('piece0')],
256258
calculatedTimings: DEFAULT_PART_TIMINGS,
259+
regenerateTimelineAt: undefined,
257260
},
258261
}
259262

@@ -279,13 +282,15 @@ describe('buildTimelineObjsForRundown', () => {
279282
partInstance: createMockPartInstance('part0'),
280283
pieceInstances: [createMockPieceInstance('piece0')],
281284
calculatedTimings: DEFAULT_PART_TIMINGS,
285+
regenerateTimelineAt: undefined,
282286
},
283287
next: {
284288
nowInPart: 0,
285289
partStarted: undefined,
286290
partInstance: createMockPartInstance('part1'),
287291
pieceInstances: [createMockPieceInstance('piece1')],
288292
calculatedTimings: DEFAULT_PART_TIMINGS,
293+
regenerateTimelineAt: undefined,
289294
},
290295
}
291296

@@ -312,13 +317,15 @@ describe('buildTimelineObjsForRundown', () => {
312317
partInstance: createMockPartInstance('part0', { autoNext: true, expectedDuration: 5000 }),
313318
pieceInstances: [createMockPieceInstance('piece0')],
314319
calculatedTimings: DEFAULT_PART_TIMINGS,
320+
regenerateTimelineAt: undefined,
315321
},
316322
next: {
317323
nowInPart: 0,
318324
partStarted: undefined,
319325
partInstance: createMockPartInstance('part1'),
320326
pieceInstances: [createMockPieceInstance('piece1')],
321327
calculatedTimings: DEFAULT_PART_TIMINGS,
328+
regenerateTimelineAt: undefined,
322329
},
323330
}
324331

@@ -353,13 +360,15 @@ describe('buildTimelineObjsForRundown', () => {
353360
),
354361
pieceInstances: [createMockPieceInstance('piece9')],
355362
calculatedTimings: DEFAULT_PART_TIMINGS,
363+
regenerateTimelineAt: undefined,
356364
},
357365
current: {
358366
nowInPart: 1234,
359367
partStarted: 5678,
360368
partInstance: createMockPartInstance('part0'),
361369
pieceInstances: [createMockPieceInstance('piece0')],
362370
calculatedTimings: DEFAULT_PART_TIMINGS,
371+
regenerateTimelineAt: undefined,
363372
},
364373
}
365374

@@ -395,6 +404,7 @@ describe('buildTimelineObjsForRundown', () => {
395404
),
396405
pieceInstances: [createMockPieceInstance('piece9'), createMockPieceInstance('piece8')],
397406
calculatedTimings: DEFAULT_PART_TIMINGS,
407+
regenerateTimelineAt: undefined,
398408
},
399409
current: {
400410
nowInPart: 1234,
@@ -409,6 +419,7 @@ describe('buildTimelineObjsForRundown', () => {
409419
fromPartPostroll: 400,
410420
fromPartKeepalive: 100,
411421
},
422+
regenerateTimelineAt: undefined,
412423
},
413424
}
414425

@@ -448,6 +459,7 @@ describe('buildTimelineObjsForRundown', () => {
448459
}),
449460
],
450461
calculatedTimings: DEFAULT_PART_TIMINGS,
462+
regenerateTimelineAt: undefined,
451463
},
452464
current: {
453465
nowInPart: 1234,
@@ -462,6 +474,7 @@ describe('buildTimelineObjsForRundown', () => {
462474
fromPartPostroll: 400,
463475
fromPartKeepalive: 100,
464476
},
477+
regenerateTimelineAt: undefined,
465478
},
466479
}
467480

@@ -488,6 +501,7 @@ describe('buildTimelineObjsForRundown', () => {
488501
partInstance: createMockPartInstance('part0', { autoNext: true, expectedDuration: 5000 }),
489502
pieceInstances: [createMockPieceInstance('piece0')],
490503
calculatedTimings: DEFAULT_PART_TIMINGS,
504+
regenerateTimelineAt: undefined,
491505
},
492506
next: {
493507
nowInPart: 0,
@@ -502,6 +516,7 @@ describe('buildTimelineObjsForRundown', () => {
502516
fromPartPostroll: 400,
503517
fromPartKeepalive: 100,
504518
},
519+
regenerateTimelineAt: undefined,
505520
},
506521
}
507522

@@ -543,6 +558,7 @@ describe('buildTimelineObjsForRundown', () => {
543558
}),
544559
],
545560
calculatedTimings: DEFAULT_PART_TIMINGS,
561+
regenerateTimelineAt: undefined,
546562
},
547563
next: {
548564
nowInPart: 0,
@@ -565,6 +581,7 @@ describe('buildTimelineObjsForRundown', () => {
565581
fromPartPostroll: 400,
566582
fromPartKeepalive: 100,
567583
},
584+
regenerateTimelineAt: undefined,
568585
},
569586
}
570587

@@ -597,6 +614,7 @@ describe('buildTimelineObjsForRundown', () => {
597614
),
598615
pieceInstances: [createMockPieceInstance('piece9')],
599616
calculatedTimings: DEFAULT_PART_TIMINGS,
617+
regenerateTimelineAt: undefined,
600618
}
601619

602620
it('infinite starting in current', () => {
@@ -613,6 +631,7 @@ describe('buildTimelineObjsForRundown', () => {
613631
createMockInfinitePieceInstance('piece1', {}, { plannedStartedPlayback: undefined }),
614632
],
615633
calculatedTimings: DEFAULT_PART_TIMINGS,
634+
regenerateTimelineAt: undefined,
616635
},
617636
}
618637

@@ -641,6 +660,7 @@ describe('buildTimelineObjsForRundown', () => {
641660
partInstance: createMockPartInstance('part0'),
642661
pieceInstances: [createMockPieceInstance('piece0')],
643662
calculatedTimings: DEFAULT_PART_TIMINGS,
663+
regenerateTimelineAt: undefined,
644664
},
645665
}
646666

@@ -669,6 +689,7 @@ describe('buildTimelineObjsForRundown', () => {
669689
partInstance: createMockPartInstance('part0'),
670690
pieceInstances: [createMockPieceInstance('piece0')],
671691
calculatedTimings: DEFAULT_PART_TIMINGS,
692+
regenerateTimelineAt: undefined,
672693
},
673694
}
674695

@@ -696,6 +717,7 @@ describe('buildTimelineObjsForRundown', () => {
696717
partInstance: createMockPartInstance('part0'),
697718
pieceInstances: [createMockPieceInstance('piece0'), continueInfinitePiece(infinitePiece)],
698719
calculatedTimings: DEFAULT_PART_TIMINGS,
720+
regenerateTimelineAt: undefined,
699721
},
700722
}
701723

@@ -727,6 +749,7 @@ describe('buildTimelineObjsForRundown', () => {
727749
),
728750
pieceInstances: [createMockPieceInstance('piece0'), infinitePiece],
729751
calculatedTimings: DEFAULT_PART_TIMINGS,
752+
regenerateTimelineAt: undefined,
730753
},
731754
next: {
732755
nowInPart: 0,
@@ -742,6 +765,7 @@ describe('buildTimelineObjsForRundown', () => {
742765
),
743766
pieceInstances: [createMockPieceInstance('piece1'), continueInfinitePiece(infinitePiece)],
744767
calculatedTimings: DEFAULT_PART_TIMINGS,
768+
regenerateTimelineAt: undefined,
745769
},
746770
}
747771

@@ -771,6 +795,7 @@ describe('buildTimelineObjsForRundown', () => {
771795
),
772796
pieceInstances: [createMockPieceInstance('piece0'), createMockInfinitePieceInstance('piece6')],
773797
calculatedTimings: DEFAULT_PART_TIMINGS,
798+
regenerateTimelineAt: undefined,
774799
},
775800
next: {
776801
nowInPart: 0,
@@ -789,6 +814,7 @@ describe('buildTimelineObjsForRundown', () => {
789814
...DEFAULT_PART_TIMINGS,
790815
fromPartKeepalive: 100,
791816
},
817+
regenerateTimelineAt: undefined,
792818
},
793819
}
794820

@@ -821,6 +847,7 @@ describe('buildTimelineObjsForRundown', () => {
821847
createMockInfinitePieceInstance('piece6', { excludeDuringPartKeepalive: true }),
822848
],
823849
calculatedTimings: DEFAULT_PART_TIMINGS,
850+
regenerateTimelineAt: undefined,
824851
},
825852
next: {
826853
nowInPart: 0,
@@ -839,6 +866,7 @@ describe('buildTimelineObjsForRundown', () => {
839866
...DEFAULT_PART_TIMINGS,
840867
fromPartKeepalive: 100,
841868
},
869+
regenerateTimelineAt: undefined,
842870
},
843871
}
844872

‎packages/job-worker/src/playout/timeline/generate.ts

+62-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BlueprintId, TimelineHash } from '@sofie-automation/corelib/dist/dataModel/Ids'
1+
import { BlueprintId, RundownPlaylistId, TimelineHash } from '@sofie-automation/corelib/dist/dataModel/Ids'
22
import { JobContext, JobStudio } from '../../jobs'
33
import { ReadonlyDeep } from 'type-fest'
44
import {
@@ -16,10 +16,11 @@ import {
1616
TimelineObjGeneric,
1717
TimelineObjRundown,
1818
TimelineObjType,
19+
TimelineObjRegenerateTrigger,
1920
} from '@sofie-automation/corelib/dist/dataModel/Timeline'
2021
import { RundownBaselineObj } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineObj'
2122
import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance'
22-
import { applyToArray, clone, literal, normalizeArray, omit } from '@sofie-automation/corelib/dist/lib'
23+
import { applyToArray, clone, getHash, literal, normalizeArray, omit } from '@sofie-automation/corelib/dist/lib'
2324
import { PlayoutModel } from '../model/PlayoutModel'
2425
import { logger } from '../../logging'
2526
import { getCurrentTime, getSystemVersion } from '../../lib'
@@ -46,6 +47,7 @@ import { getPartTimingsOrDefaults, PartCalculatedTimings } from '@sofie-automati
4647
import { applyAbPlaybackForTimeline } from '../abPlayback'
4748
import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError'
4849
import { PlayoutPartInstanceModel } from '../model/PlayoutPartInstanceModel'
50+
import { PlayoutChangedType } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI'
4951

5052
function isModelForStudio(model: StudioPlayoutModelBase): model is StudioPlayoutModel {
5153
const tmp = model as StudioPlayoutModel
@@ -126,7 +128,7 @@ export async function updateStudioTimeline(
126128
logAnyRemainingNowTimes(context, baselineObjects)
127129
}
128130

129-
const timelineHash = saveTimeline(context, playoutModel, baselineObjects, versions)
131+
const timelineHash = saveTimeline(context, playoutModel, baselineObjects, versions, undefined)
130132

131133
if (studioBaseline) {
132134
updateBaselineExpectedPackagesOnStudio(context, playoutModel, studioBaseline)
@@ -144,7 +146,12 @@ export async function updateTimeline(context: JobContext, playoutModel: PlayoutM
144146
throw new Error(`RundownPlaylist ("${playoutModel.playlist._id}") is not active")`)
145147
}
146148

147-
const { versions, objs: timelineObjs, timingContext: timingInfo } = await getTimelineRundown(context, playoutModel)
149+
const {
150+
versions,
151+
objs: timelineObjs,
152+
timingContext: timingInfo,
153+
regenerateTimelineToken,
154+
} = await getTimelineRundown(context, playoutModel)
148155

149156
flattenAndProcessTimelineObjects(context, timelineObjs)
150157

@@ -156,7 +163,7 @@ export async function updateTimeline(context: JobContext, playoutModel: PlayoutM
156163
logAnyRemainingNowTimes(context, timelineObjs)
157164
}
158165

159-
const timelineHash = saveTimeline(context, playoutModel, timelineObjs, versions)
166+
const timelineHash = saveTimeline(context, playoutModel, timelineObjs, versions, regenerateTimelineToken)
160167
logger.verbose(`updateTimeline done, hash: "${timelineHash}"`)
161168

162169
if (span) span.end()
@@ -227,9 +234,10 @@ export function saveTimeline(
227234
context: JobContext,
228235
studioPlayoutModel: StudioPlayoutModelBase,
229236
timelineObjs: TimelineObjGeneric[],
230-
generationVersions: TimelineCompleteGenerationVersions
237+
generationVersions: TimelineCompleteGenerationVersions,
238+
regenerateTimelineToken: string | undefined
231239
): TimelineHash {
232-
const newTimeline = studioPlayoutModel.setTimeline(timelineObjs, generationVersions)
240+
const newTimeline = studioPlayoutModel.setTimeline(timelineObjs, generationVersions, regenerateTimelineToken)
233241

234242
// Also do a fast-track for the timeline to be published faster:
235243
context.hackPublishTimelineToFastTrack(newTimeline)
@@ -248,6 +256,7 @@ export interface SelectedPartInstanceTimelineInfo {
248256
partInstance: ReadonlyDeep<DBPartInstance>
249257
pieceInstances: PieceInstanceWithTimings[]
250258
calculatedTimings: PartCalculatedTimings
259+
regenerateTimelineAt: number | undefined
251260
}
252261

253262
function getPartInstanceTimelineInfo(
@@ -273,6 +282,7 @@ function getPartInstanceTimelineInfo(
273282
partStarted,
274283
// Approximate `calculatedTimings`, for the partInstances which already have it cached
275284
calculatedTimings: getPartTimingsOrDefaults(partInstanceWithOverrides, pieceInstances),
285+
regenerateTimelineAt: undefined, // Future use
276286
}
277287
}
278288

@@ -286,6 +296,7 @@ async function getTimelineRundown(
286296
objs: Array<TimelineObjRundown>
287297
versions: TimelineCompleteGenerationVersions
288298
timingContext: RundownTimelineTimingContext | undefined
299+
regenerateTimelineToken: string | undefined
289300
}> {
290301
const span = context.startSpan('getTimelineRundown')
291302
try {
@@ -341,6 +352,9 @@ async function getTimelineRundown(
341352
timelineObjs = timelineObjs.concat(rundownTimelineResult.timeline)
342353
timelineObjs = timelineObjs.concat(await pLookaheadObjs)
343354

355+
const regenerateTimelineObj = createRegenerateTimelineObj(playoutModel.playlistId, partInstancesInfo)
356+
if (regenerateTimelineObj) timelineObjs.push(regenerateTimelineObj.obj)
357+
344358
const blueprint = await context.getShowStyleBlueprint(showStyle._id)
345359
timelineVersions = generateTimelineVersions(
346360
context.studio,
@@ -438,6 +452,7 @@ async function getTimelineRundown(
438452
}),
439453
versions: timelineVersions ?? generateTimelineVersions(context.studio, undefined, '-'),
440454
timingContext: rundownTimelineResult.timingContext,
455+
regenerateTimelineToken: regenerateTimelineObj?.token,
441456
}
442457
} else {
443458
if (span) span.end()
@@ -446,6 +461,7 @@ async function getTimelineRundown(
446461
objs: [],
447462
versions: generateTimelineVersions(context.studio, undefined, '-'),
448463
timingContext: undefined,
464+
regenerateTimelineToken: undefined,
449465
}
450466
}
451467
} catch (e) {
@@ -455,10 +471,49 @@ async function getTimelineRundown(
455471
objs: [],
456472
versions: generateTimelineVersions(context.studio, undefined, '-'),
457473
timingContext: undefined,
474+
regenerateTimelineToken: undefined,
458475
}
459476
}
460477
}
461478

479+
function createRegenerateTimelineObj(
480+
playlistId: RundownPlaylistId,
481+
partInstancesInfo: SelectedPartInstancesTimelineInfo
482+
) {
483+
const regenerateTimelineAt = Math.min(
484+
partInstancesInfo.current?.regenerateTimelineAt ?? Number.POSITIVE_INFINITY,
485+
partInstancesInfo.next?.regenerateTimelineAt ?? Number.POSITIVE_INFINITY
486+
)
487+
if (regenerateTimelineAt < Number.POSITIVE_INFINITY) {
488+
// The timeline has requested a regeneration at a specific time
489+
const token = getHash(`regenerate-${playlistId}-${getCurrentTime()}`)
490+
const obj = literal<TimelineObjRegenerateTrigger & OnGenerateTimelineObjExt>({
491+
id: `regenerate_${token}`,
492+
enable: {
493+
start: regenerateTimelineAt,
494+
},
495+
layer: '__timeline_regeneration_trigger__', // Some unique name, as callbacks need to be on a layer
496+
priority: 1,
497+
content: {
498+
deviceType: TSR.DeviceType.ABSTRACT,
499+
type: 'callback',
500+
callBack: PlayoutChangedType.TRIGGER_REGENERATION,
501+
callBackData: {
502+
rundownPlaylistId: playlistId,
503+
regenerationToken: token,
504+
},
505+
},
506+
objectType: TimelineObjType.RUNDOWN,
507+
metaData: undefined,
508+
partInstanceId: null,
509+
})
510+
511+
return { token, obj }
512+
} else {
513+
return null
514+
}
515+
}
516+
462517
/**
463518
* Process the timeline objects, to provide some basic validation. Also flattens the nested objects into a single array
464519
* Note: Input array is mutated in place

‎packages/job-worker/src/playout/timings/index.ts

+19
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyE
77
import { PlayoutChangedType } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI'
88
import { onPiecePlaybackStarted, onPiecePlaybackStopped } from './piecePlayback'
99
import { onPartPlaybackStarted, onPartPlaybackStopped } from './partPlayback'
10+
import { updateTimeline } from '../timeline/generate'
1011

1112
export { handleTimelineTriggerTime } from './timelineTriggerTime'
1213

@@ -18,6 +19,8 @@ export async function handleOnPlayoutPlaybackChanged(
1819
data: OnPlayoutPlaybackChangedProps
1920
): Promise<void> {
2021
return runJobWithPlayoutModel(context, data, null, async (playoutModel) => {
22+
let triggerRegeneration = false
23+
2124
for (const change of data.changes) {
2225
try {
2326
if (change.type === PlayoutChangedType.PART_PLAYBACK_STARTED) {
@@ -42,9 +45,25 @@ export async function handleOnPlayoutPlaybackChanged(
4245
pieceInstanceId: change.data.pieceInstanceId,
4346
stoppedPlayback: change.data.time,
4447
})
48+
} else if (change.type === PlayoutChangedType.TRIGGER_REGENERATION) {
49+
if (
50+
playoutModel.timeline?.regenerateTimelineToken &&
51+
change.data.regenerationToken === playoutModel.timeline.regenerateTimelineToken
52+
) {
53+
triggerRegeneration = true
54+
} else {
55+
logger.info(
56+
`Playout gateway requested a regeneration of the timeline, with an incorrect regenerationToken. Got ${change.data.regenerationToken}, expected ${playoutModel.timeline?.regenerateTimelineToken}`
57+
)
58+
}
4559
} else {
4660
assertNever(change)
4761
}
62+
63+
if (triggerRegeneration) {
64+
logger.info('Playout gateway requested a regeneration of the timeline')
65+
await updateTimeline(context, playoutModel)
66+
}
4867
} catch (err) {
4968
logger.error(stringifyError(err))
5069
}

‎packages/job-worker/src/playout/timings/timelineTriggerTime.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,14 @@ function timelineTriggerTimeInner(
181181
}
182182
}
183183
if (tlChanged) {
184-
const timelineHash = saveTimeline(context, studioPlayoutModel, timelineObjs, timeline.generationVersions)
184+
const timelineHash = saveTimeline(
185+
context,
186+
studioPlayoutModel,
187+
timelineObjs,
188+
// Preserve some current values:
189+
timeline.generationVersions,
190+
timeline.regenerateTimelineToken
191+
)
185192

186193
logger.verbose(`timelineTriggerTime: Updated Timeline, hash: "${timelineHash}"`)
187194
}

‎packages/job-worker/src/studio/model/StudioPlayoutModel.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ export interface StudioPlayoutModelBase extends StudioPlayoutModelBaseReadonly {
4747
*/
4848
setTimeline(
4949
timelineObjs: TimelineObjGeneric[],
50-
generationVersions: TimelineCompleteGenerationVersions
50+
generationVersions: TimelineCompleteGenerationVersions,
51+
regenerateTimelineToken: string | undefined
5152
): ReadonlyDeep<TimelineComplete>
5253
}
5354

‎packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,16 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel {
8787

8888
setTimeline(
8989
timelineObjs: TimelineObjGeneric[],
90-
generationVersions: TimelineCompleteGenerationVersions
90+
generationVersions: TimelineCompleteGenerationVersions,
91+
regenerateTimelineToken: string | undefined
9192
): ReadonlyDeep<TimelineComplete> {
9293
this.#timeline = {
9394
_id: this.context.studioId,
9495
timelineHash: getRandomId(),
9596
generated: getCurrentTime(),
9697
timelineBlob: serializeTimelineBlob(timelineObjs),
9798
generationVersions: generationVersions,
99+
regenerateTimelineToken: regenerateTimelineToken,
98100
}
99101
this.#timelineHasChanged = true
100102

‎packages/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"postinstall": "cd .. && \"$PROJECT_CWD/node_modules/.bin/husky\" install",
1919
"build": "lerna run build --ignore @sofie-automation/openapi",
2020
"build:all": "lerna run build",
21-
"build:try": "lerna run --no-bail build --ignore @sofie-automation/openapi",
21+
"build:try": "lerna run --no-bail build --ignore @sofie-automation/openapi || true",
2222
"watch": "lerna run --parallel build:main --ignore @sofie-automation/openapi -- --watch --preserveWatchOutput",
2323
"stage-versions": "git add -u \"*/package.json\" \"*/CHANGELOG.md\" lerna.json yarn.lock",
2424
"set-version": "lerna version --exact --no-changelog --no-git-tag-version --no-push --yes",

‎packages/playout-gateway/src/tsrHandler.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -844,14 +844,18 @@ export class TSRHandler {
844844
time: number,
845845
objId: string,
846846
callbackName0: string,
847-
data: PeripheralDeviceAPI.PartPlaybackCallbackData | PeripheralDeviceAPI.PiecePlaybackCallbackData
847+
data:
848+
| PeripheralDeviceAPI.PartPlaybackCallbackData
849+
| PeripheralDeviceAPI.PiecePlaybackCallbackData
850+
| PeripheralDeviceAPI.TriggerRegenerationCallbackData
848851
): void {
849852
if (
850853
![
851854
PeripheralDeviceAPI.PlayoutChangedType.PART_PLAYBACK_STARTED,
852855
PeripheralDeviceAPI.PlayoutChangedType.PART_PLAYBACK_STOPPED,
853856
PeripheralDeviceAPI.PlayoutChangedType.PIECE_PLAYBACK_STARTED,
854857
PeripheralDeviceAPI.PlayoutChangedType.PIECE_PLAYBACK_STOPPED,
858+
PeripheralDeviceAPI.PlayoutChangedType.TRIGGER_REGENERATION,
855859
].includes(callbackName0 as PeripheralDeviceAPI.PlayoutChangedType)
856860
) {
857861
// @ts-expect-error Untyped bunch of methods
@@ -914,6 +918,16 @@ export class TSRHandler {
914918
},
915919
})
916920
break
921+
case PeripheralDeviceAPI.PlayoutChangedType.TRIGGER_REGENERATION:
922+
this.changedResults.changes.push({
923+
type: callbackName,
924+
objId,
925+
data: {
926+
regenerationToken: (data as PeripheralDeviceAPI.TriggerRegenerationCallbackData)
927+
.regenerationToken,
928+
},
929+
})
930+
break
917931
default:
918932
assertNever(callbackName)
919933
}

‎packages/shared-lib/src/peripheralDevice/peripheralDeviceAPI.ts

+12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ export interface PiecePlaybackStartedResult extends PiecePlaybackCallbackData {
2424
}
2525
export type PiecePlaybackStoppedResult = PiecePlaybackStartedResult
2626

27+
export interface TriggerRegenerationCallbackData {
28+
rundownPlaylistId: RundownPlaylistId
29+
// partInstanceId: PartInstanceId
30+
regenerationToken: string
31+
}
32+
2733
export type PlayoutChangedResults = {
2834
rundownPlaylistId: RundownPlaylistId
2935
changes: PlayoutChangedResult[]
@@ -33,6 +39,7 @@ export enum PlayoutChangedType {
3339
PART_PLAYBACK_STOPPED = 'partPlaybackStopped',
3440
PIECE_PLAYBACK_STARTED = 'piecePlaybackStarted',
3541
PIECE_PLAYBACK_STOPPED = 'piecePlaybackStopped',
42+
TRIGGER_REGENERATION = 'triggerRegeneration',
3643
}
3744
export type PlayoutChangedResult = {
3845
objId: string
@@ -41,6 +48,7 @@ export type PlayoutChangedResult = {
4148
| PlayoutChangedType.PART_PLAYBACK_STOPPED
4249
| PlayoutChangedType.PIECE_PLAYBACK_STARTED
4350
| PlayoutChangedType.PIECE_PLAYBACK_STOPPED
51+
| PlayoutChangedType.TRIGGER_REGENERATION
4452
} & (
4553
| {
4654
type: PlayoutChangedType.PART_PLAYBACK_STARTED
@@ -58,6 +66,10 @@ export type PlayoutChangedResult = {
5866
type: PlayoutChangedType.PIECE_PLAYBACK_STOPPED
5967
data: Omit<PiecePlaybackStoppedResult, 'rundownPlaylistId'>
6068
}
69+
| {
70+
type: PlayoutChangedType.TRIGGER_REGENERATION
71+
data: Omit<TriggerRegenerationCallbackData, 'rundownPlaylistId'>
72+
}
6173
)
6274

6375
// Note The actual type of a device is determined by the Category, Type and SubType

0 commit comments

Comments
 (0)
Please sign in to comment.