Skip to content

Commit 6d7f192

Browse files
committed
feat: add object to timeline to trigger a regeneration at point in time
1 parent c267d55 commit 6d7f192

File tree

13 files changed

+164
-28
lines changed

13 files changed

+164
-28
lines changed

packages/corelib/src/dataModel/Timeline.ts

Lines changed: 17 additions & 0 deletions
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

Lines changed: 6 additions & 3 deletions
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__/timeline.test.ts

Lines changed: 13 additions & 12 deletions
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/model/implementation/PlayoutModelImpl.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,14 +786,16 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
786786

787787
setTimeline(
788788
timelineObjs: TimelineObjGeneric[],
789-
generationVersions: TimelineCompleteGenerationVersions
789+
generationVersions: TimelineCompleteGenerationVersions,
790+
regenerateTimelineToken: string | undefined
790791
): ReadonlyDeep<TimelineComplete> {
791792
this.timelineImpl = {
792793
_id: this.context.studioId,
793794
timelineHash: getRandomId(), // randomized on every timeline change
794795
generated: getCurrentTime(),
795796
timelineBlob: serializeTimelineBlob(timelineObjs),
796797
generationVersions: generationVersions,
798+
regenerateTimelineToken: regenerateTimelineToken,
797799
}
798800
this.#timelineHasChanged = true
799801

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

Lines changed: 62 additions & 7 deletions
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/timeline/rundown.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ export function buildTimelineObjsForRundown(
137137
const timingContext: RundownTimelineTimingContext = {
138138
currentPartGroup,
139139
currentPartDuration: currentPartEnable.duration,
140+
141+
// regenerateTimelineAt: For future use
140142
}
141143

142144
// Start generating objects

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

Lines changed: 19 additions & 0 deletions
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

Lines changed: 8 additions & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 3 additions & 1 deletion
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

0 commit comments

Comments
 (0)