diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 61ad4d8a87..51bee4fe10 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -195,8 +195,13 @@ export interface ShowStyleBlueprintManifest Promise + /** + * Called when a RundownPlaylist has been activated + */ + onRundownActivate?: (context: IRundownActivationContext) => Promise + /** Called upon the first take in a RundownPlaylist */ onRundownFirstTake?: (context: IPartEventContext) => Promise + /** Called when a RundownPlaylist has been deactivated */ onRundownDeActivate?: (context: IRundownActivationContext) => Promise /** Called before a Take action */ diff --git a/packages/blueprints-integration/src/context/rundownContext.ts b/packages/blueprints-integration/src/context/rundownContext.ts index 9faf667fa3..402da1fa39 100644 --- a/packages/blueprints-integration/src/context/rundownContext.ts +++ b/packages/blueprints-integration/src/context/rundownContext.ts @@ -13,7 +13,11 @@ export interface IRundownContext extends IShowStyleContext { export interface IRundownUserContext extends IUserNotesContext, IRundownContext {} -export interface IRundownActivationContext extends IRundownContext, IExecuteTSRActionsContext, IDataStoreMethods {} +export interface IRundownActivationContext extends IRundownContext, IExecuteTSRActionsContext, IDataStoreMethods { + /** Info about the RundownPlaylist state before the Activation / Deactivation event */ + readonly previousState: IRundownActivationContextState + readonly currentState: IRundownActivationContextState +} export interface ISegmentUserContext extends IUserNotesContext, IRundownContext, IPackageInfoContext { /** Display a notification to the user of an error */ @@ -23,3 +27,13 @@ export interface ISegmentUserContext extends IUserNotesContext, IRundownContext, /** Display a notification to the user of a note */ notifyUserInfo: (message: string, params?: { [key: string]: any }, partExternalId?: string) => void } + +/** Info about the RundownPlaylist state at a point in time */ +export interface IRundownActivationContextState { + /** If the playlist was active */ + active: boolean + /** If the playlist was in rehearsal mode */ + rehearsal: boolean + /** Timestamp when the playlist was last reset. Used to silence a few errors upon reset.*/ + resetTime?: number +} diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 9c26cce808..e5df4d1311 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -238,7 +238,9 @@ export type ActivateHoldProps = RundownPlayoutPropsBase export type DeactivateHoldProps = RundownPlayoutPropsBase export type PrepareRundownForBroadcastProps = RundownPlayoutPropsBase export interface ResetRundownPlaylistProps extends RundownPlayoutPropsBase { + /** If set, also activate the RundownPlaylist */ activate?: 'active' | 'rehearsal' + /** If true and `activate` is set, deactivates any other active Playlists and activates this one. */ forceActivate?: boolean } export interface ActivateRundownPlaylistProps extends RundownPlayoutPropsBase { diff --git a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts index 7ffe281b1b..a1c6849245 100644 --- a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts @@ -2,6 +2,7 @@ import { DatastorePersistenceMode, IBlueprintPlayoutDevice, IRundownActivationContext, + IRundownActivationContextState, TSR, } from '@sofie-automation/blueprints-integration' import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids' @@ -17,22 +18,38 @@ export class RundownActivationContext extends RundownEventContext implements IRu private readonly _playoutModel: PlayoutModel private readonly _context: JobContext + private readonly _previousState: IRundownActivationContextState + private readonly _currentState: IRundownActivationContextState + constructor( context: JobContext, - playoutModel: PlayoutModel, - showStyleCompound: ReadonlyDeep, - rundown: ReadonlyDeep + options: { + playoutModel: PlayoutModel + showStyle: ReadonlyDeep + rundown: ReadonlyDeep + previousState: IRundownActivationContextState + currentState: IRundownActivationContextState + } ) { super( context.studio, context.getStudioBlueprintConfig(), - showStyleCompound, - context.getShowStyleBlueprintConfig(showStyleCompound), - rundown + options.showStyle, + context.getShowStyleBlueprintConfig(options.showStyle), + options.rundown ) this._context = context - this._playoutModel = playoutModel + this._playoutModel = options.playoutModel + this._previousState = options.previousState + this._currentState = options.currentState + } + + get previousState(): IRundownActivationContextState { + return this._previousState + } + get currentState(): IRundownActivationContextState { + return this._currentState } async listPlayoutDevices(): Promise { diff --git a/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap b/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap index 570be55e51..a76933d825 100644 --- a/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap +++ b/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap @@ -11,8 +11,8 @@ exports[`Playout API Basic rundown control 1`] = ` "core": "0.0.0-test", "studio": "asdf", }, - "timelineBlob": "[{"id":"playlist_randomId9000_status","objectType":"rundown","enable":{"while":1},"layer":"rundown_status","content":{"deviceType":"ABSTRACT"},"classes":["rundown_active"],"priority":0},{"id":"part_group_randomId9000_part0_0_randomId9003","objectType":"rundown","enable":{"start":"now"},"priority":5,"layer":"","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"isGroup":true,"metaData":{"isPieceTimeline":true}},{"id":"part_group_firstobject_randomId9000_part0_0_randomId9003","objectType":"rundown","enable":{"start":0},"layer":"group_first_object","content":{"deviceType":"ABSTRACT","type":"callback","callBack":"partPlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9003"},"callBackStopped":"partPlaybackStopped"},"inGroup":"part_group_randomId9000_part0_0_randomId9003","classes":[],"priority":0},{"id":"piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001","objectType":"rundown","enable":{"start":0},"layer":"vt0","priority":5,"content":{"deviceType":"ABSTRACT","type":"callback","callBack":"piecePlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9003","pieceInstanceId":"randomId9000_part0_0_randomId9003_randomId9000_piece001","dynamicallyInserted":false},"callBackStopped":"piecePlaybackStopped"},"classes":["current_part"],"inGroup":"part_group_randomId9000_part0_0_randomId9003","metaData":{"isPieceTimeline":true,"triggerPieceInstanceId":"randomId9000_part0_0_randomId9003_randomId9000_piece001"}},{"id":"piece_group_randomId9000_part0_0_randomId9003_randomId9000_piece001","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"inGroup":"part_group_randomId9000_part0_0_randomId9003","isGroup":true,"objectType":"rundown","enable":{"start":"#piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001.start - 0","end":"#piece_group_control_randomId9000_part0_0_randomId9003_randomId9000_piece001.end + 0"},"layer":"","metaData":{"isPieceTimeline":true,"pieceInstanceGroupId":"randomId9000_part0_0_randomId9003_randomId9000_piece001"},"priority":0}]", - "timelineHash": "randomId9006", + "timelineBlob": "[{"id":"playlist_randomId9000_status","objectType":"rundown","enable":{"while":1},"layer":"rundown_status","content":{"deviceType":"ABSTRACT"},"classes":["rundown_active"],"priority":0},{"id":"part_group_randomId9000_part0_0_randomId9007","objectType":"rundown","enable":{"start":"now"},"priority":5,"layer":"","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"isGroup":true,"metaData":{"isPieceTimeline":true}},{"id":"part_group_firstobject_randomId9000_part0_0_randomId9007","objectType":"rundown","enable":{"start":0},"layer":"group_first_object","content":{"deviceType":"ABSTRACT","type":"callback","callBack":"partPlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9007"},"callBackStopped":"partPlaybackStopped"},"inGroup":"part_group_randomId9000_part0_0_randomId9007","classes":[],"priority":0},{"id":"piece_group_control_randomId9000_part0_0_randomId9007_randomId9000_piece001","objectType":"rundown","enable":{"start":0},"layer":"vt0","priority":5,"content":{"deviceType":"ABSTRACT","type":"callback","callBack":"piecePlaybackStarted","callBackData":{"rundownPlaylistId":"playlist_randomId9000","partInstanceId":"randomId9000_part0_0_randomId9007","pieceInstanceId":"randomId9000_part0_0_randomId9007_randomId9000_piece001","dynamicallyInserted":false},"callBackStopped":"piecePlaybackStopped"},"classes":["current_part"],"inGroup":"part_group_randomId9000_part0_0_randomId9007","metaData":{"isPieceTimeline":true,"triggerPieceInstanceId":"randomId9000_part0_0_randomId9007_randomId9000_piece001"}},{"id":"piece_group_randomId9000_part0_0_randomId9007_randomId9000_piece001","content":{"deviceType":"ABSTRACT","type":"group"},"children":[],"inGroup":"part_group_randomId9000_part0_0_randomId9007","isGroup":true,"objectType":"rundown","enable":{"start":"#piece_group_control_randomId9000_part0_0_randomId9007_randomId9000_piece001.start - 0","end":"#piece_group_control_randomId9000_part0_0_randomId9007_randomId9000_piece001.end + 0"},"layer":"","metaData":{"isPieceTimeline":true,"pieceInstanceGroupId":"randomId9000_part0_0_randomId9007_randomId9000_piece001"},"priority":0}]", + "timelineHash": "randomId9010", }, ] `; @@ -57,7 +57,7 @@ exports[`Playout API Basic rundown control 3`] = ` "studio": "asdf", }, "timelineBlob": "[]", - "timelineHash": "randomId9007", + "timelineHash": "randomId9011", }, ] `; diff --git a/packages/job-worker/src/playout/__tests__/playout.test.ts b/packages/job-worker/src/playout/__tests__/playout.test.ts index 369f445397..563c23ba74 100644 --- a/packages/job-worker/src/playout/__tests__/playout.test.ts +++ b/packages/job-worker/src/playout/__tests__/playout.test.ts @@ -152,7 +152,10 @@ describe('Playout API', () => { await handleResetRundownPlaylist(context, { playlistId: playlistId0 }) - expect(Timeline.operations).toMatchObject([{ args: ['mockStudio0', undefined], type: 'findOne' }]) + expect(Timeline.operations).toMatchObject([ + { args: ['mockStudio0', undefined], type: 'findOne' }, + { args: ['mockStudio0'], type: 'replace' }, + ]) Timeline.clearOpLog() const orgRundownData = await getAllRundownData(await getRundown0()) diff --git a/packages/job-worker/src/playout/activePlaylistActions.ts b/packages/job-worker/src/playout/activePlaylistActions.ts index c4c2dff622..89f049a150 100644 --- a/packages/job-worker/src/playout/activePlaylistActions.ts +++ b/packages/job-worker/src/playout/activePlaylistActions.ts @@ -8,7 +8,7 @@ import { getCurrentTime } from '../lib/index.js' import { logger } from '../logging.js' import { getActiveRundownPlaylistsInStudioFromDb } from '../studio/lib.js' import { cleanTimelineDatastore } from './datastore.js' -import { resetRundownPlaylist } from './lib.js' +import { getActivationContextState, resetRundownPlaylist } from './lib.js' import { PlayoutModel } from './model/PlayoutModel.js' import { selectNextPart } from './selectNextPart.js' import { setNextPart } from './setNext.js' @@ -17,12 +17,12 @@ import { updateStudioTimeline, updateTimeline } from './timeline/generate.js' export async function activateRundownPlaylist( context: JobContext, playoutModel: PlayoutModel, - rehearsal: boolean + rehearsal: boolean, + forceReset?: boolean ): Promise { logger.info('Activating rundown ' + playoutModel.playlist._id + (rehearsal ? ' (Rehearsal)' : '')) rehearsal = !!rehearsal - const wasActive = !!playoutModel.playlist.activationId const anyOtherActiveRundowns = await getActiveRundownPlaylistsInStudioFromDb( context, @@ -37,8 +37,10 @@ export async function activateRundownPlaylist( JSON.stringify(otherActiveIds) ) } + // Get the ActivationContext state, for later use. (This must be done before any actions are done on the PlayoutModel) + const previousState = getActivationContextState(playoutModel) - if (!playoutModel.playlist.activationId) { + if (!playoutModel.playlist.activationId || forceReset) { // Reset the playlist if it wasnt already active await resetRundownPlaylist(context, playoutModel) } @@ -93,6 +95,9 @@ export async function activateRundownPlaylist( await updateTimeline(context, playoutModel) + // Get the ActivationContext state, for later use. (This must be done after all actions are done on the PlayoutModel) + const currentState = getActivationContextState(playoutModel) + playoutModel.deferBeforeSave(async () => { if (!rundown) return // if the proper rundown hasn't been found, there's little point doing anything else const showStyle = await context.getShowStyleCompound(rundown.showStyleVariantId, rundown.showStyleBaseId) @@ -100,9 +105,15 @@ export async function activateRundownPlaylist( try { if (blueprint.blueprint.onRundownActivate) { - const blueprintContext = new RundownActivationContext(context, playoutModel, showStyle, rundown) - - await blueprint.blueprint.onRundownActivate(blueprintContext, wasActive) + const blueprintContext = new RundownActivationContext(context, { + playoutModel, + showStyle, + rundown, + previousState, + currentState, + }) + + await blueprint.blueprint.onRundownActivate(blueprintContext) } } catch (err) { logger.error(`Error in showStyleBlueprint.onRundownActivate: ${stringifyError(err)}`) @@ -110,12 +121,18 @@ export async function activateRundownPlaylist( }) } export async function deactivateRundownPlaylist(context: JobContext, playoutModel: PlayoutModel): Promise { + // Get the ActivationContext state, for later use. (This must be done before any actions are done on the PlayoutModel) + const previousState = getActivationContextState(playoutModel) + const rundown = await deactivateRundownPlaylistInner(context, playoutModel) await updateStudioTimeline(context, playoutModel) await cleanTimelineDatastore(context, playoutModel) + // Get the ActivationContext state, for later use. (This must be done after all actions are done on the PlayoutModel) + const currentState = getActivationContextState(playoutModel) + playoutModel.deferBeforeSave(async () => { if (rundown) { const showStyle = await context.getShowStyleCompound(rundown.showStyleVariantId, rundown.showStyleBaseId) @@ -123,7 +140,13 @@ export async function deactivateRundownPlaylist(context: JobContext, playoutMode try { if (blueprint.blueprint.onRundownDeActivate) { - const blueprintContext = new RundownActivationContext(context, playoutModel, showStyle, rundown) + const blueprintContext = new RundownActivationContext(context, { + playoutModel, + showStyle, + rundown, + previousState, + currentState, + }) await blueprint.blueprint.onRundownDeActivate(blueprintContext) } } catch (err) { diff --git a/packages/job-worker/src/playout/activePlaylistJobs.ts b/packages/job-worker/src/playout/activePlaylistJobs.ts index a6b202d980..05e200bb77 100644 --- a/packages/job-worker/src/playout/activePlaylistJobs.ts +++ b/packages/job-worker/src/playout/activePlaylistJobs.ts @@ -9,7 +9,6 @@ import { import { JobContext } from '../jobs/index.js' import { runJobWithPlayoutModel } from './lock.js' import { resetRundownPlaylist } from './lib.js' -import { updateTimeline } from './timeline/generate.js' import { getActiveRundownPlaylistsInStudioFromDb } from '../studio/lib.js' import { activateRundownPlaylist, @@ -53,9 +52,7 @@ export async function handlePrepareRundownPlaylistForBroadcast( await checkNoOtherPlaylistsActive(context, playlist) }, async (playoutModel) => { - await resetRundownPlaylist(context, playoutModel) - - await activateRundownPlaylist(context, playoutModel, true) // Activate rundownPlaylist (rehearsal) + await activateRundownPlaylist(context, playoutModel, true, true) // Activate rundownPlaylist (rehearsal) } ) } @@ -110,14 +107,16 @@ export async function handleResetRundownPlaylist(context: JobContext, data: Rese } }, async (playoutModel) => { - await resetRundownPlaylist(context, playoutModel) + if (playoutModel.playlist.activationId || data.activate !== undefined) { + const goToRehearsal = + data.activate === undefined + ? playoutModel.playlist.rehearsal ?? false + : data.activate === 'rehearsal' - if (data.activate) { - // Do the activation - await activateRundownPlaylist(context, playoutModel, data.activate !== 'active') // Activate rundown - } else if (playoutModel.playlist.activationId) { - // Only update the timeline if this is the active playlist - await updateTimeline(context, playoutModel) + await activateRundownPlaylist(context, playoutModel, goToRehearsal, true) // Activate rundown + } else { + // If the Playlist is inactive, and we are not activating it, just reset it: + await resetRundownPlaylist(context, playoutModel) } } ) diff --git a/packages/job-worker/src/playout/lib.ts b/packages/job-worker/src/playout/lib.ts index 5158357678..5dd9dcf107 100644 --- a/packages/job-worker/src/playout/lib.ts +++ b/packages/job-worker/src/playout/lib.ts @@ -1,6 +1,6 @@ import { TimelineObjGeneric } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { applyToArray, clone } from '@sofie-automation/corelib/dist/lib' -import { TSR } from '@sofie-automation/blueprints-integration' +import { TSR, IRundownActivationContextState } from '@sofie-automation/blueprints-integration' import { JobContext } from '../jobs/index.js' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -104,7 +104,7 @@ export function resetPartInstancesWithPieceInstances( reset: true, }, } - ) + ) : undefined, allToReset.length ? context.directCollections.PieceInstances.update( @@ -118,14 +118,14 @@ export function resetPartInstancesWithPieceInstances( reset: true, }, } - ) + ) : undefined, allToReset.length > 0 ? context.directCollections.Notifications.remove({ 'relatedTo.studioId': context.studioId, 'relatedTo.rundownId': { $in: rundownIds }, 'relatedTo.partInstanceId': { $in: allToReset }, - }) + }) : undefined, ]) }) @@ -203,3 +203,10 @@ export async function updateTimelineFromStudioPlayoutModel( await updateStudioTimeline(context, studioPlayoutModel) } } +export function getActivationContextState(playoutModel: PlayoutModel): IRundownActivationContextState { + return { + active: playoutModel.playlist.activationId ? true : false, + rehearsal: playoutModel.playlist.rehearsal ? true : false, + resetTime: playoutModel.playlist.resetTime, + } +}