Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 7 additions & 1 deletion packages/blueprints-integration/src/api/showStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,14 @@ export interface ShowStyleBlueprintManifest<TRawConfig = IBlueprintConfig, TProc

// Events

onRundownActivate?: (context: IRundownActivationContext, wasActive: boolean) => Promise<void>
/**
* Called when a RundownPlaylist has been activated
* Note: Prior to this being called, onRundownReset might have been called
*/
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
}
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 @@ -149,7 +149,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'
import { logger } from '../logging'
import { getActiveRundownPlaylistsInStudioFromDb } from '../studio/lib'
import { cleanTimelineDatastore } from './datastore'
import { resetRundownPlaylist } from './lib'
import { getActivationContextState, resetRundownPlaylist } from './lib'
import { PlayoutModel } from './model/PlayoutModel'
import { selectNextPart } from './selectNextPart'
import { setNextPart } from './setNext'
Expand All @@ -17,12 +17,12 @@ import { updateStudioTimeline, updateTimeline } from './timeline/generate'
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
16 changes: 2 additions & 14 deletions packages/job-worker/src/playout/activePlaylistJobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import {
} from '@sofie-automation/corelib/dist/worker/studio'
import { JobContext } from '../jobs'
import { runJobWithPlayoutModel } from './lock'
import { resetRundownPlaylist } from './lib'
import { updateTimeline } from './timeline/generate'
import { getActiveRundownPlaylistsInStudioFromDb } from '../studio/lib'
import {
activateRundownPlaylist,
Expand Down Expand Up @@ -53,9 +51,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)
}
)
}
Expand Down Expand Up @@ -110,15 +106,7 @@ export async function handleResetRundownPlaylist(context: JobContext, data: Rese
}
},
async (playoutModel) => {
await resetRundownPlaylist(context, playoutModel)

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, data.activate !== 'active', true) // Activate rundown
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks to be a change in behaviour; if activate is undefined then activateRundownPlaylist will now be called, when it wasnt previously.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as I explained under "New behavior" this is my intent.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/user-attachments/assets/988ee625-7d60-4ecc-a0e6-274c346e5831
That causes the playlist to be activated to rehearsal during a 'normal' reset of the rundown

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After having talked to @Julusian the behavior is now closer to how it was originally, with only resetRundownPlaylist being called if the playlist is inactive.

}
)
}
Expand Down
9 changes: 8 additions & 1 deletion 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'
import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance'
import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids'
Expand Down Expand Up @@ -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,
}
}