diff --git a/packages/blueprints-integration/src/context/adlibActionContext.ts b/packages/blueprints-integration/src/context/adlibActionContext.ts index 4435d76b41..6f9931eeea 100644 --- a/packages/blueprints-integration/src/context/adlibActionContext.ts +++ b/packages/blueprints-integration/src/context/adlibActionContext.ts @@ -40,6 +40,9 @@ export interface IActionExecutionContext /** Insert a queued part to follow the current part */ queuePart(part: IBlueprintPart, pieces: IBlueprintPiece[]): Promise + /** Insert a queued part to follow the taken part */ + queuePartAfterTake(part: IBlueprintPart, pieces: IBlueprintPiece[]): void + /** Misc actions */ // updateAction(newManifest: Pick): void // only updates itself. to allow for the next one to do something different // executePeripheralDeviceAction(deviceId: string, functionName: string, args: any[]): Promise diff --git a/packages/blueprints-integration/src/context/onTakeContext.ts b/packages/blueprints-integration/src/context/onTakeContext.ts index 3918bdd7ee..461f64bfa1 100644 --- a/packages/blueprints-integration/src/context/onTakeContext.ts +++ b/packages/blueprints-integration/src/context/onTakeContext.ts @@ -1,4 +1,4 @@ -import { IEventContext, IShowStyleUserContext, Time } from '../index.js' +import { IBlueprintPart, IBlueprintPiece, IEventContext, IShowStyleUserContext, Time } from '../index.js' import { IPartAndPieceActionContext } from './partsAndPieceActionContext.js' import { IExecuteTSRActionsContext } from './executeTsrActionContext.js' @@ -18,4 +18,6 @@ export interface IOnTakeContext * but the next part will not be taken. */ abortTake(): void + /** Insert a queued part to follow the taken part */ + queuePartAfterTake(part: IBlueprintPart, pieces: IBlueprintPiece[]): void } diff --git a/packages/job-worker/src/blueprints/context/OnTakeContext.ts b/packages/job-worker/src/blueprints/context/OnTakeContext.ts index 578f038c33..714a5369c4 100644 --- a/packages/job-worker/src/blueprints/context/OnTakeContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTakeContext.ts @@ -23,11 +23,16 @@ import { WatchedPackagesHelper } from './watchedPackages.js' import { getCurrentTime } from '../../lib/index.js' import { JobContext, ProcessedShowStyleCompound } from '../../jobs/index.js' import { executePeripheralDeviceAction, listPlayoutDevices } from '../../peripheralDevice.js' -import { ActionPartChange, PartAndPieceInstanceActionService } from './services/PartAndPieceInstanceActionService.js' +import { + ActionPartChange, + PartAndPieceInstanceActionService, + QueueablePartAndPieces, +} from './services/PartAndPieceInstanceActionService.js' import { BlueprintQuickLookInfo } from '@sofie-automation/blueprints-integration/dist/context/quickLoopInfo' export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContext, IEventContext { public isTakeAborted: boolean + public partToQueueAfterTake: QueueablePartAndPieces | undefined public get quickLoopInfo(): BlueprintQuickLookInfo | null { return this.partAndPieceInstanceService.quickLoopInfo @@ -153,6 +158,19 @@ export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContex return executePeripheralDeviceAction(this._context, deviceId, null, actionId, payload) } + queuePartAfterTake(rawPart: IBlueprintPart, rawPieces: IBlueprintPiece[]): void { + const currentPartInstance = this._playoutModel.currentPartInstance + if (!currentPartInstance) { + throw new Error('Cannot queue part when no current partInstance') + } + this.partToQueueAfterTake = this.partAndPieceInstanceService.processPartAndPiecesToQueueOrFail( + rawPart, + rawPieces, + this._playoutModel.currentPartInstance.partInstance.rundownId, + this._playoutModel.currentPartInstance.partInstance.segmentId + ) + } + getCurrentTime(): number { return getCurrentTime() } diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 60a8aa328e..da104e4e8b 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -29,7 +29,11 @@ import { ProcessedShowStyleConfig } from '../config.js' import { DatastorePersistenceMode } from '@sofie-automation/shared-lib/dist/core/model/TimelineDatastore' import { removeTimelineDatastoreValue, setTimelineDatastoreValue } from '../../playout/datastore.js' import { executePeripheralDeviceAction, listPlayoutDevices } from '../../peripheralDevice.js' -import { ActionPartChange, PartAndPieceInstanceActionService } from './services/PartAndPieceInstanceActionService.js' +import { + ActionPartChange, + PartAndPieceInstanceActionService, + QueueablePartAndPieces, +} from './services/PartAndPieceInstanceActionService.js' import { BlueprintQuickLookInfo } from '@sofie-automation/blueprints-integration/dist/context/quickLoopInfo' import { setNextPartFromPart } from '../../playout/setNext.js' @@ -74,6 +78,8 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct */ public forceRegenerateTimeline = false + public partToQueueAfterTake: QueueablePartAndPieces | undefined + public get quickLoopInfo(): BlueprintQuickLookInfo | null { return this.partAndPieceInstanceService.quickLoopInfo } @@ -162,6 +168,19 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct return this.partAndPieceInstanceService.queuePart(rawPart, rawPieces) } + queuePartAfterTake(rawPart: IBlueprintPart, rawPieces: IBlueprintPiece[]): void { + const currentPartInstance = this._playoutModel.currentPartInstance + if (!currentPartInstance) { + throw new Error('Cannot queue part when no current partInstance') + } + this.partToQueueAfterTake = this.partAndPieceInstanceService.processPartAndPiecesToQueueOrFail( + rawPart, + rawPieces, + this._playoutModel.currentPartInstance.partInstance.rundownId, + this._playoutModel.currentPartInstance.partInstance.segmentId + ) + } + async moveNextPart(partDelta: number, segmentDelta: number, ignoreQuickloop?: boolean): Promise { const selectedPart = selectNewPartWithOffsets( this._context, diff --git a/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts b/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts index 2e183391ae..7f788b8089 100644 --- a/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts +++ b/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts @@ -44,7 +44,7 @@ import { PieceTimelineObjectsBlob, serializePieceTimelineObjectsBlob, } from '@sofie-automation/corelib/dist/dataModel/Piece' -import { PartInstanceId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartInstanceId, PieceInstanceId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { protectString, unprotectString, @@ -70,6 +70,11 @@ export interface IPartAndPieceInstanceActionContext { readonly nextPartState: ActionPartChange } +export interface QueueablePartAndPieces { + part: Omit + pieces: Piece[] +} + export class PartAndPieceInstanceActionService { private readonly _context: JobContext private readonly _playoutModel: PlayoutModel @@ -387,11 +392,41 @@ export class PartAndPieceInstanceActionService { throw new Error('Too close to an autonext to queue a part') } + const { part, pieces } = this.processPartAndPiecesToQueueOrFail( + rawPart, + rawPieces, + currentPartInstance.partInstance.rundownId, + currentPartInstance.partInstance.segmentId + ) + + // Do the work + const newPartInstance = await insertQueuedPartWithPieces( + this._context, + this._playoutModel, + this._rundown, + currentPartInstance, + part, + pieces, + undefined + ) + + this.nextPartState = ActionPartChange.SAFE_CHANGE + this.queuedPartInstanceId = newPartInstance.partInstance._id + + return convertPartInstanceToBlueprints(newPartInstance.partInstance) + } + + public processPartAndPiecesToQueueOrFail( + rawPart: IBlueprintPart, + rawPieces: IBlueprintPiece[], + rundownId: RundownId, + segmentId: SegmentId + ): QueueablePartAndPieces { if (rawPieces.length === 0) { throw new Error('New part must contain at least one piece') } - const newPart: Omit = { + const part: Omit = { ...rawPart, _id: getRandomId(), notes: [], @@ -407,31 +442,17 @@ export class PartAndPieceInstanceActionService { this._context, rawPieces, this.showStyleCompound.blueprintId, - currentPartInstance.partInstance.rundownId, - currentPartInstance.partInstance.segmentId, - newPart._id, + rundownId, + segmentId, + part._id, false ) - if (!isPartPlayable(newPart)) { + if (!isPartPlayable(part)) { throw new Error('Cannot queue a part which is not playable') } - // Do the work - const newPartInstance = await insertQueuedPartWithPieces( - this._context, - this._playoutModel, - this._rundown, - currentPartInstance, - newPart, - pieces, - undefined - ) - - this.nextPartState = ActionPartChange.SAFE_CHANGE - this.queuedPartInstanceId = newPartInstance.partInstance._id - - return convertPartInstanceToBlueprints(newPartInstance.partInstance) + return { part, pieces } } async stopPiecesOnLayers(sourceLayerIds: string[], timeOffset: number | undefined): Promise { diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 36eaa59a10..48edfd3bd8 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -278,7 +278,7 @@ async function applyAnyExecutionSideEffects( await applyActionSideEffects(context, playoutModel, actionContext) if (actionContext.takeAfterExecute) { - await performTakeToNextedPart(context, playoutModel, now) + await performTakeToNextedPart(context, playoutModel, now, actionContext.partToQueueAfterTake) } else if ( actionContext.forceRegenerateTimeline || actionContext.currentPartState !== ActionPartChange.NONE || diff --git a/packages/job-worker/src/playout/adlibTesting.ts b/packages/job-worker/src/playout/adlibTesting.ts index a20b62525a..7594f8dbee 100644 --- a/packages/job-worker/src/playout/adlibTesting.ts +++ b/packages/job-worker/src/playout/adlibTesting.ts @@ -47,7 +47,7 @@ export async function handleActivateAdlibTesting(context: JobContext, data: Acti playoutModel.setPartInstanceAsNext(newPartInstance, true, false) // Take into the newly created Part - await performTakeToNextedPart(context, playoutModel, getCurrentTime()) + await performTakeToNextedPart(context, playoutModel, getCurrentTime(), undefined) } ) } diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index 67b8f51742..87e607d138 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -19,7 +19,7 @@ import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyE import { updateTimeline } from './timeline/generate.js' import { OnTakeContext, PartEventContext, RundownContext } from '../blueprints/context/index.js' import { WrappedShowStyleBlueprint } from '../blueprints/cache.js' -import { innerStopPieces } from './adlibUtils.js' +import { innerStopPieces, insertQueuedPartWithPieces } from './adlibUtils.js' import { reportPartInstanceHasStarted, reportPartInstanceHasStopped } from './timings/partPlayback.js' import { convertPartInstanceToBlueprints, convertResolvedPieceInstanceToBlueprints } from '../blueprints/context/lib.js' import { processAndPrunePieceInstanceTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' @@ -30,6 +30,7 @@ import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' import { WatchedPackagesHelper } from '../blueprints/context/watchedPackages.js' import { PartAndPieceInstanceActionService, + QueueablePartAndPieces, applyActionSideEffects, } from '../blueprints/context/services/PartAndPieceInstanceActionService.js' import { PlayoutRundownModel } from './model/PlayoutRundownModel.js' @@ -83,7 +84,7 @@ export async function handleTakeNextPart(context: JobContext, data: TakeNextPart }) } - return performTakeToNextedPart(context, playoutModel, now) + return performTakeToNextedPart(context, playoutModel, now, undefined) } ) } @@ -97,7 +98,8 @@ export async function handleTakeNextPart(context: JobContext, data: TakeNextPart export async function performTakeToNextedPart( context: JobContext, playoutModel: PlayoutModel, - now: number + now: number, + partToQueueAfterTake: QueueablePartAndPieces | undefined ): Promise { const span = context.startSpan('takeNextPartInner') @@ -198,7 +200,15 @@ export async function performTakeToNextedPart( const showStyle = await pShowStyle const blueprint = await context.getShowStyleBlueprint(showStyle._id) - const { isTakeAborted } = await executeOnTakeCallback(context, playoutModel, showStyle, blueprint, currentRundown) + const { isTakeAborted, partToQueueAfterTake: partToQueueFromOnTake } = await executeOnTakeCallback( + context, + playoutModel, + showStyle, + blueprint, + currentRundown + ) + + partToQueueAfterTake = partToQueueAfterTake ?? partToQueueFromOnTake if (isTakeAborted) { await updateTimeline(context, playoutModel) @@ -248,24 +258,35 @@ export async function performTakeToNextedPart( const wasLooping = playoutModel.playlist.quickLoop?.running playoutModel.updateQuickLoopState() - const nextPart = selectNextPart( - context, - playoutModel.playlist, - takePartInstance.partInstance, - null, - playoutModel.getAllOrderedSegments(), - playoutModel.getAllOrderedParts(), - { ignoreUnplayable: true, ignoreQuickLoop: false } - ) - takePartInstance.setTaken(now, timeOffset) if (wasLooping) { resetPreviousSegmentIfLooping(context, playoutModel) } - // Once everything is synced, we can choose the next part - await setNextPart(context, playoutModel, nextPart, false) + if (partToQueueAfterTake) { + await insertQueuedPartWithPieces( + context, + playoutModel, + takeRundown, + takePartInstance, + partToQueueAfterTake.part, + partToQueueAfterTake.pieces, + undefined + ) + } else { + // Once everything is synced, we can choose the next part + const nextPart = selectNextPart( + context, + playoutModel.playlist, + takePartInstance.partInstance, + null, + playoutModel.getAllOrderedSegments(), + playoutModel.getAllOrderedParts(), + { ignoreUnplayable: true, ignoreQuickLoop: false } + ) + await setNextPart(context, playoutModel, nextPart, false) + } // If the Hold is PENDING, make it active if (playoutModel.playlist.holdState === RundownHoldState.PENDING) { @@ -289,10 +310,11 @@ async function executeOnTakeCallback( showStyle: ReadonlyObjectDeep, blueprint: ReadonlyObjectDeep, currentRundown: PlayoutRundownModel -): Promise<{ isTakeAborted: boolean }> { +): Promise<{ isTakeAborted: boolean; partToQueueAfterTake: QueueablePartAndPieces | undefined }> { const NOTIFICATION_CATEGORY = 'onTake' let isTakeAborted = false + let partToQueueAfterTake: QueueablePartAndPieces | undefined = undefined if (blueprint.blueprint.onTake) { const rundownId = currentRundown.rundown._id const partInstanceId = playoutModel.playlist.nextPartInfo?.partInstanceId @@ -300,6 +322,7 @@ async function executeOnTakeCallback( // Clear any existing notifications for this partInstance. This will clear any from the previous take playoutModel.clearAllNotifications(NOTIFICATION_CATEGORY) + const actionService = new PartAndPieceInstanceActionService(context, playoutModel, showStyle, currentRundown) const watchedPackagesHelper = WatchedPackagesHelper.empty(context) const onSetAsNextContext = new OnTakeContext( @@ -313,7 +336,7 @@ async function executeOnTakeCallback( playoutModel, showStyle, watchedPackagesHelper, - new PartAndPieceInstanceActionService(context, playoutModel, showStyle, currentRundown) + actionService ) try { const blueprintPersistentState = new PersistentPlayoutStateStore( @@ -323,6 +346,9 @@ async function executeOnTakeCallback( await blueprint.blueprint.onTake(onSetAsNextContext, blueprintPersistentState) await applyOnTakeSideEffects(context, playoutModel, onSetAsNextContext) isTakeAborted = onSetAsNextContext.isTakeAborted + if (onSetAsNextContext.partToQueueAfterTake) { + partToQueueAfterTake = onSetAsNextContext.partToQueueAfterTake + } if (blueprintPersistentState.hasChanges) { playoutModel.setBlueprintPersistentState(blueprintPersistentState.getAll()) @@ -354,7 +380,7 @@ async function executeOnTakeCallback( }) } } - return { isTakeAborted } + return { isTakeAborted, partToQueueAfterTake } } async function applyOnTakeSideEffects(context: JobContext, playoutModel: PlayoutModel, onTakeContext: OnTakeContext) {