Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/blueprints-integration/src/api/showStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,13 @@ export interface ShowStyleBlueprintManifest<TRawConfig = IBlueprintConfig, TProc

// Events

onRundownActivate?: (context: IRundownActivationContext, wasActive: boolean) => Promise<void>
/**
* Called when a RundownPlaylist has been activated
*/
onRundownActivate?: (context: IRundownActivationContext) => Promise<void>
/** Called upon the first take in a RundownPlaylist */
onRundownFirstTake?: (context: IPartEventContext) => Promise<void>
/** Called when a RundownPlaylist has been deactivated */
onRundownDeActivate?: (context: IRundownActivationContext) => Promise<void>

/** Called before a Take action */
Expand Down
16 changes: 15 additions & 1 deletion packages/blueprints-integration/src/context/rundownContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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
}
2 changes: 2 additions & 0 deletions packages/corelib/src/worker/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<ProcessedShowStyleCompound>,
rundown: ReadonlyDeep<DBRundown>
options: {
playoutModel: PlayoutModel
showStyle: ReadonlyDeep<ProcessedShowStyleCompound>
rundown: ReadonlyDeep<DBRundown>
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<IBlueprintPlayoutDevice[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
]
`;
Expand Down Expand Up @@ -57,7 +57,7 @@ exports[`Playout API Basic rundown control 3`] = `
"studio": "asdf",
},
"timelineBlob": "[]",
"timelineHash": "randomId9007",
"timelineHash": "randomId9011",
},
]
`;
Expand Down
5 changes: 4 additions & 1 deletion packages/job-worker/src/playout/__tests__/playout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
39 changes: 31 additions & 8 deletions packages/job-worker/src/playout/activePlaylistActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<void> {
logger.info('Activating rundown ' + playoutModel.playlist._id + (rehearsal ? ' (Rehearsal)' : ''))

rehearsal = !!rehearsal
const wasActive = !!playoutModel.playlist.activationId

const anyOtherActiveRundowns = await getActiveRundownPlaylistsInStudioFromDb(
context,
Expand All @@ -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)
}
Expand Down Expand Up @@ -93,37 +95,58 @@ 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)
const blueprint = await context.getShowStyleBlueprint(showStyle._id)

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)}`)
}
})
}
export async function deactivateRundownPlaylist(context: JobContext, playoutModel: PlayoutModel): Promise<void> {
// 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)
const blueprint = await context.getShowStyleBlueprint(showStyle._id)

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) {
Expand Down
21 changes: 10 additions & 11 deletions packages/job-worker/src/playout/activePlaylistJobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
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,
Expand Down Expand Up @@ -53,9 +52,7 @@
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)
}
)
}
Expand Down Expand Up @@ -110,14 +107,16 @@
}
},
async (playoutModel) => {
await resetRundownPlaylist(context, playoutModel)
if (playoutModel.playlist.activationId || data.activate !== undefined) {
const goToRehearsal =
data.activate === undefined
? playoutModel.playlist.rehearsal ?? false

Check failure on line 113 in packages/job-worker/src/playout/activePlaylistJobs.ts

View workflow job for this annotation

GitHub Actions / Lint Package (job-worker)

Replace `playoutModel.playlist.rehearsal·??·false` with `(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)
}
}
)
Expand Down
15 changes: 11 additions & 4 deletions packages/job-worker/src/playout/lib.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -104,7 +104,7 @@
reset: true,
},
}
)
)

Check failure on line 107 in packages/job-worker/src/playout/lib.ts

View workflow job for this annotation

GitHub Actions / Lint Package (job-worker)

Replace `··` with `↹`
: undefined,
allToReset.length
? context.directCollections.PieceInstances.update(
Expand All @@ -118,14 +118,14 @@
reset: true,
},
}
)
)

Check failure on line 121 in packages/job-worker/src/playout/lib.ts

View workflow job for this annotation

GitHub Actions / Lint Package (job-worker)

Replace `··` with `↹`
: undefined,
allToReset.length > 0
? context.directCollections.Notifications.remove({
'relatedTo.studioId': context.studioId,
'relatedTo.rundownId': { $in: rundownIds },
'relatedTo.partInstanceId': { $in: allToReset },
})
})

Check failure on line 128 in packages/job-worker/src/playout/lib.ts

View workflow job for this annotation

GitHub Actions / Lint Package (job-worker)

Replace `··` with `↹`
: undefined,
])
})
Expand Down Expand Up @@ -203,3 +203,10 @@
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,
}
}
Loading