Skip to content

Commit 80fdd49

Browse files
authored
Merge pull request Sofie-Automation#1627 from tv2norge-collab/contribute/EAV-737
fix: prevent undesired timelines reaching TSR
2 parents e24c11a + d1e0ba1 commit 80fdd49

7 files changed

Lines changed: 168 additions & 81 deletions

File tree

packages/blueprints-integration/src/api/showStyle.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,16 @@ export interface ShowStyleBlueprintManifest<
201201
// Events
202202

203203
/**
204-
* Called when a RundownPlaylist has been activated
204+
* Called at the final stage of RundownPlaylist activation, before the updated timeline is submitted to the Playout Gateway,
205+
* This is a good place to prepare any external systems for the rundown going live.
205206
*/
206207
onRundownActivate?: (context: IRundownActivationContext) => Promise<void>
207208
/** Called upon the first take in a RundownPlaylist */
208209
onRundownFirstTake?: (context: IPartEventContext) => Promise<void>
209-
/** Called when a RundownPlaylist has been deactivated */
210+
/**
211+
* Called at the final stage of RundownPlaylist deactivation, before the updated timeline is submitted to the Playout Gateway,
212+
* This is a good place to prepare any external systems for the rundown going offline.
213+
*/
210214
onRundownDeActivate?: (context: IRundownActivationContext) => Promise<void>
211215

212216
/** Called before a Take action */

packages/job-worker/src/playout/model/PlayoutModel.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,12 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa
387387
*/
388388
getNowInPlayout(): Time
389389

390+
/**
391+
* Mark the playlist as needing a timeline update.
392+
* The timeline will be generated and published when model is ready to be saved.
393+
*/
394+
markTimelineNeedsUpdate(): void
395+
390396
/** Lifecycle */
391397

392398
/**

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout
7171
import { NotificationsModelHelper } from '../../../notifications/NotificationsModelHelper.js'
7272
import { getExpectedLatency } from '@sofie-automation/corelib/dist/studio/playout'
7373
import { ExpectedPackage } from '@sofie-automation/blueprints-integration'
74+
import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError'
75+
import {
76+
getTimelineRundown,
77+
flattenAndProcessTimelineObjects,
78+
preserveOrReplaceNowTimesInObjects,
79+
logAnyRemainingNowTimes,
80+
getStudioTimeline,
81+
} from '../../timeline/generate.js'
82+
import { deNowifyMultiGatewayTimeline } from '../../timeline/multi-gateway.js'
7483

7584
export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly {
7685
public readonly playlistId: RundownPlaylistId
@@ -315,6 +324,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
315324

316325
#playlistHasChanged = false
317326
#timelineHasChanged = false
327+
#timelineNeedsRegeneration = false
318328

319329
#pendingPartInstanceTimingEvents = new Set<PartInstanceId>()
320330

@@ -694,8 +704,17 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
694704
}
695705
this.#deferredBeforeSaveFunctions.length = 0 // clear the array
696706

707+
// Generate timeline if needed
708+
if (this.#timelineNeedsRegeneration) {
709+
await this.#regenerateTimeline()
710+
this.#timelineNeedsRegeneration = false
711+
}
712+
697713
// Prioritise the timeline for publication reasons
698714
if (this.#timelineHasChanged && this.timelineImpl) {
715+
// Do a fast-track for the timeline to be published faster:
716+
this.context.hackPublishTimelineToFastTrack(this.timelineImpl)
717+
699718
await this.context.directCollections.Timelines.replace(this.timelineImpl)
700719
if (!process.env.JEST_WORKER_ID) {
701720
// Wait a little bit before saving the rest.
@@ -886,6 +905,10 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
886905
return result
887906
}
888907

908+
markTimelineNeedsUpdate(): void {
909+
this.#timelineNeedsRegeneration = true
910+
}
911+
889912
/** Notifications */
890913

891914
async getAllNotifications(
@@ -922,6 +945,40 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
922945

923946
/** BaseModel */
924947

948+
async #regenerateTimeline(): Promise<void> {
949+
const span = this.context.startSpan('PlayoutModelImpl.regenerateTimeline')
950+
logger.debug('Regenerating timeline...')
951+
952+
try {
953+
const {
954+
versions,
955+
objs: timelineObjs,
956+
timingContext: timingInfo,
957+
regenerateTimelineToken,
958+
} = this.playlist.activationId
959+
? await getTimelineRundown(this.context, this)
960+
: await getStudioTimeline(this.context, this)
961+
962+
flattenAndProcessTimelineObjects(this.context, timelineObjs)
963+
964+
preserveOrReplaceNowTimesInObjects(this, timelineObjs)
965+
966+
if (this.isMultiGatewayMode) {
967+
deNowifyMultiGatewayTimeline(this, timelineObjs, timingInfo)
968+
969+
logAnyRemainingNowTimes(this.context, timelineObjs)
970+
}
971+
972+
const timelineHash = this.setTimeline(timelineObjs, versions, regenerateTimelineToken).timelineHash
973+
logger.verbose(`Timeline regeneration done, hash: "${timelineHash}"`)
974+
} catch (err) {
975+
logger.error(`Error regenerating timeline: ${stringifyError(err)}`)
976+
throw err
977+
} finally {
978+
if (span) span.end()
979+
}
980+
}
981+
925982
/**
926983
* Assert that no changes should have been made to the model, will throw an Error otherwise. This can be used in
927984
* place of `saveAllToDatabase()`, when the code controlling the model expects no changes to have been made and any

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

Lines changed: 45 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BlueprintId, RundownPlaylistId, TimelineHash } from '@sofie-automation/corelib/dist/dataModel/Ids'
1+
import { BlueprintId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids'
22
import { JobContext, JobStudio } from '../../jobs/index.js'
33
import { ReadonlyDeep } from 'type-fest'
44
import { BlueprintResultBaseline, OnGenerateTimelineObj, Time, TSR } from '@sofie-automation/blueprints-integration'
@@ -37,7 +37,6 @@ import { deserializePieceTimelineObjectsBlob } from '@sofie-automation/corelib/d
3737
import { convertResolvedPieceInstanceToBlueprints } from '../../blueprints/context/lib.js'
3838
import { buildTimelineObjsForRundown, RundownTimelineTimingContext } from './rundown.js'
3939
import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase'
40-
import { deNowifyMultiGatewayTimeline } from './multi-gateway.js'
4140
import { validateTimeline } from 'superfly-timeline'
4241
import { getPartTimingsOrDefaults, PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings'
4342
import { applyAbPlaybackForTimeline } from '../abPlayback/index.js'
@@ -67,25 +66,19 @@ function generateTimelineVersions(
6766
}
6867
}
6968

70-
export async function updateStudioTimeline(
69+
/**
70+
* Generate timeline objects for a studio (when no playlist is active)
71+
*/
72+
export async function getStudioTimeline(
7173
context: JobContext,
7274
playoutModel: StudioPlayoutModel | PlayoutModel
73-
): Promise<void> {
74-
const span = context.startSpan('updateStudioTimeline')
75-
logger.debug('updateStudioTimeline running...')
75+
): Promise<{
76+
objs: Array<TimelineObjRundown>
77+
versions: TimelineCompleteGenerationVersions
78+
timingContext: undefined
79+
regenerateTimelineToken: undefined
80+
}> {
7681
const studio = context.studio
77-
// Ensure there isn't a playlist active, as that should be using a different function call
78-
if (isModelForStudio(playoutModel)) {
79-
const activePlaylists = playoutModel.getActiveRundownPlaylists()
80-
if (activePlaylists.length > 0) {
81-
throw new Error(`Studio has an active playlist`)
82-
}
83-
} else {
84-
if (playoutModel.playlist.activationId) {
85-
throw new Error(`Studio has an active playlist`)
86-
}
87-
}
88-
8982
let baselineObjects: TimelineObjRundown[] = []
9083
let studioBaseline: BlueprintResultBaseline | undefined
9184

@@ -119,60 +112,58 @@ export async function updateStudioTimeline(
119112
studioBlueprint?.blueprint?.blueprintVersion ?? '-'
120113
)
121114

122-
flattenAndProcessTimelineObjects(context, baselineObjects)
123-
124-
// Future: We should handle any 'now' objects that are at the root of this timeline
125-
preserveOrReplaceNowTimesInObjects(playoutModel, baselineObjects)
126-
127-
if (playoutModel.isMultiGatewayMode) {
128-
logAnyRemainingNowTimes(context, baselineObjects)
115+
if (studioBaseline) {
116+
updateBaselineExpectedPackagesOnStudio(context, playoutModel, studioBaseline)
129117
}
130118

131-
const timelineHash = saveTimeline(context, playoutModel, baselineObjects, versions, undefined)
119+
return {
120+
objs: baselineObjects,
121+
versions,
122+
timingContext: undefined,
123+
regenerateTimelineToken: undefined,
124+
}
125+
}
132126

133-
if (studioBaseline) {
134-
updateBaselineExpectedPackagesOnStudio(context, playoutModel, studioBaseline)
127+
export async function updateStudioTimeline(
128+
context: JobContext,
129+
playoutModel: StudioPlayoutModel | PlayoutModel
130+
): Promise<void> {
131+
const span = context.startSpan('updateStudioTimeline')
132+
logger.debug('updateStudioTimeline: marking studio as needing timeline update')
133+
// Ensure there isn't a playlist active, as that should be using a different function call
134+
if (isModelForStudio(playoutModel)) {
135+
const activePlaylists = playoutModel.getActiveRundownPlaylists()
136+
if (activePlaylists.length > 0) {
137+
throw new Error(`Studio has an active playlist`)
138+
}
139+
} else {
140+
if (playoutModel.playlist.activationId) {
141+
throw new Error(`Studio has an active playlist`)
142+
}
135143
}
136144

137-
logger.verbose(`updateStudioTimeline done, hash: "${timelineHash}"`)
145+
playoutModel.markTimelineNeedsUpdate()
146+
138147
if (span) span.end()
139148
}
140149

141150
export async function updateTimeline(context: JobContext, playoutModel: PlayoutModel): Promise<void> {
142151
const span = context.startSpan('updateTimeline')
143-
logger.debug('updateTimeline running...')
152+
logger.debug('updateTimeline: marking playlist as needing timeline update')
144153

145154
if (!playoutModel.playlist.activationId) {
146155
throw new Error(`RundownPlaylist ("${playoutModel.playlist._id}") is not active")`)
147156
}
148157

149-
const {
150-
versions,
151-
objs: timelineObjs,
152-
timingContext: timingInfo,
153-
regenerateTimelineToken,
154-
} = await getTimelineRundown(context, playoutModel)
155-
156-
flattenAndProcessTimelineObjects(context, timelineObjs)
157-
158-
preserveOrReplaceNowTimesInObjects(playoutModel, timelineObjs)
159-
160-
if (playoutModel.isMultiGatewayMode) {
161-
deNowifyMultiGatewayTimeline(playoutModel, timelineObjs, timingInfo)
162-
163-
logAnyRemainingNowTimes(context, timelineObjs)
164-
}
165-
166-
const timelineHash = saveTimeline(context, playoutModel, timelineObjs, versions, regenerateTimelineToken)
167-
logger.verbose(`updateTimeline done, hash: "${timelineHash}"`)
158+
playoutModel.markTimelineNeedsUpdate()
168159

169160
if (span) span.end()
170161
}
171162

172-
function preserveOrReplaceNowTimesInObjects(
163+
export function preserveOrReplaceNowTimesInObjects(
173164
studioPlayoutModel: StudioPlayoutModelBase,
174165
timelineObjs: Array<TimelineObjGeneric>
175-
) {
166+
): void {
176167
const timeline = studioPlayoutModel.timeline
177168
const oldTimelineObjsMap = normalizeArray(
178169
(timeline?.timelineBlob !== undefined && deserializeTimelineBlob(timeline.timelineBlob)) || [],
@@ -202,7 +193,7 @@ function preserveOrReplaceNowTimesInObjects(
202193
})
203194
}
204195

205-
function logAnyRemainingNowTimes(_context: JobContext, timelineObjs: Array<TimelineObjGeneric>): void {
196+
export function logAnyRemainingNowTimes(_context: JobContext, timelineObjs: Array<TimelineObjGeneric>): void {
206197
const badTimelineObjs: any[] = []
207198

208199
for (const obj of timelineObjs) {
@@ -229,22 +220,6 @@ function hasNow(obj: TimelineEnableExt | TimelineEnableExt[]) {
229220
return res
230221
}
231222

232-
/** Store the timelineobjects into the model, and perform any post-save actions */
233-
export function saveTimeline(
234-
context: JobContext,
235-
studioPlayoutModel: StudioPlayoutModelBase,
236-
timelineObjs: TimelineObjGeneric[],
237-
generationVersions: TimelineCompleteGenerationVersions,
238-
regenerateTimelineToken: string | undefined
239-
): TimelineHash {
240-
const newTimeline = studioPlayoutModel.setTimeline(timelineObjs, generationVersions, regenerateTimelineToken)
241-
242-
// Also do a fast-track for the timeline to be published faster:
243-
context.hackPublishTimelineToFastTrack(newTimeline)
244-
245-
return newTimeline.timelineHash
246-
}
247-
248223
export interface SelectedPartInstancesTimelineInfo {
249224
previous?: SelectedPartInstanceTimelineInfo
250225
current?: SelectedPartInstanceTimelineInfo
@@ -303,7 +278,7 @@ function getPartInstanceTimelineInfo(
303278
/**
304279
* Returns timeline objects related to rundowns in a studio
305280
*/
306-
async function getTimelineRundown(
281+
export async function getTimelineRundown(
307282
context: JobContext,
308283
playoutModel: PlayoutModel
309284
): Promise<{
@@ -562,7 +537,7 @@ function createRegenerateTimelineObj(
562537
* @param context
563538
* @param timelineObjs Array of timeline objects
564539
*/
565-
function flattenAndProcessTimelineObjects(context: JobContext, timelineObjs: Array<TimelineObjGeneric>): void {
540+
export function flattenAndProcessTimelineObjects(context: JobContext, timelineObjs: Array<TimelineObjGeneric>): void {
566541
const span = context.startSpan('processTimelineObjects')
567542

568543
// first, split out any grouped objects, to make the timeline shallow:

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { OnTimelineTriggerTimeProps } from '@sofie-automation/corelib/dist/worke
44
import { logger } from '../../logging.js'
55
import { JobContext } from '../../jobs/index.js'
66
import { runJobWithPlaylistLock } from '../lock.js'
7-
import { saveTimeline } from '../timeline/generate.js'
87
import { applyToArray, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib'
98
import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance'
109
import { runJobWithStudioPlayoutModel } from '../../studio/lock.js'
@@ -69,7 +68,6 @@ export async function handleTimelineTriggerTime(context: JobContext, data: OnTim
6968

7069
// Take ownership of the playlist in the db, so that we can mutate the timeline and piece instances
7170
const changes = timelineTriggerTimeInner(
72-
context,
7371
studioCache,
7472
data.results,
7573
partInstanceMap,
@@ -81,7 +79,7 @@ export async function handleTimelineTriggerTime(context: JobContext, data: OnTim
8179
})
8280
} else {
8381
// No playlist is active. no extra lock needed
84-
timelineTriggerTimeInner(context, studioCache, data.results, undefined, undefined, undefined)
82+
timelineTriggerTimeInner(studioCache, data.results, undefined, undefined, undefined)
8583
}
8684
})
8785
}
@@ -123,7 +121,6 @@ interface PieceInstancesChanges {
123121
}
124122

125123
function timelineTriggerTimeInner(
126-
context: JobContext,
127124
studioPlayoutModel: StudioPlayoutModel,
128125
results: OnTimelineTriggerTimeProps['results'],
129126
partInstances: Map<PartInstanceId, Pick<DBPartInstance, '_id' | 'partPlayoutTimings'>> | undefined,
@@ -205,14 +202,11 @@ function timelineTriggerTimeInner(
205202
}
206203
}
207204
if (tlChanged) {
208-
const timelineHash = saveTimeline(
209-
context,
210-
studioPlayoutModel,
205+
const timelineHash = studioPlayoutModel.setTimeline(
211206
timelineObjs,
212-
// Preserve some current values:
213207
timeline.generationVersions,
214208
timeline.regenerateTimelineToken
215-
)
209+
).timelineHash
216210

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

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,10 @@ export interface StudioPlayoutModel extends StudioPlayoutModelBase, BaseModel {
7979
* @returns Whether the change may affect timeline generation
8080
*/
8181
switchRouteSet(routeSetId: string, isActive: boolean | 'toggle'): boolean
82+
83+
/**
84+
* Mark the studio as needing a timeline update.
85+
* The timeline will be generated and published when model is ready to be saved.
86+
*/
87+
markTimelineNeedsUpdate(): void
8288
}

0 commit comments

Comments
 (0)