From 47778d6eed108db2bac8f8712c8c6d486fa3efdd Mon Sep 17 00:00:00 2001 From: ianshade Date: Thu, 17 Jul 2025 13:36:54 +0200 Subject: [PATCH 1/3] feat(EAV-603): allow part to be queued from onTake --- .../src/context/onTakeContext.ts | 4 ++- .../src/blueprints/context/OnTakeContext.ts | 5 ++++ packages/job-worker/src/playout/take.ts | 30 +++++++++++++++---- 3 files changed, 32 insertions(+), 7 deletions(-) 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..144f6d3543 100644 --- a/packages/job-worker/src/blueprints/context/OnTakeContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTakeContext.ts @@ -28,6 +28,7 @@ import { BlueprintQuickLookInfo } from '@sofie-automation/blueprints-integration export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContext, IEventContext { public isTakeAborted: boolean + public partToQueue: { rawPart: IBlueprintPart; rawPieces: IBlueprintPiece[] } | undefined public get quickLoopInfo(): BlueprintQuickLookInfo | null { return this.partAndPieceInstanceService.quickLoopInfo @@ -153,6 +154,10 @@ export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContex return executePeripheralDeviceAction(this._context, deviceId, null, actionId, payload) } + queuePartAfterTake(rawPart: IBlueprintPart, rawPieces: IBlueprintPiece[]): void { + this.partToQueue = { rawPart, rawPieces } + } + getCurrentTime(): number { return getCurrentTime() } diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index 67b8f51742..a4fdd34096 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -198,7 +198,13 @@ 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, queuePart } = await executeOnTakeCallback( + context, + playoutModel, + showStyle, + blueprint, + currentRundown + ) if (isTakeAborted) { await updateTimeline(context, playoutModel) @@ -264,8 +270,12 @@ export async function performTakeToNextedPart( resetPreviousSegmentIfLooping(context, playoutModel) } - // Once everything is synced, we can choose the next part - await setNextPart(context, playoutModel, nextPart, false) + if (queuePart) { + await queuePart() + } else { + // Once everything is synced, we can choose the next part + await setNextPart(context, playoutModel, nextPart, false) + } // If the Hold is PENDING, make it active if (playoutModel.playlist.holdState === RundownHoldState.PENDING) { @@ -289,10 +299,11 @@ async function executeOnTakeCallback( showStyle: ReadonlyObjectDeep, blueprint: ReadonlyObjectDeep, currentRundown: PlayoutRundownModel -): Promise<{ isTakeAborted: boolean }> { +): Promise<{ isTakeAborted: boolean; queuePart: (() => Promise) | undefined }> { const NOTIFICATION_CATEGORY = 'onTake' let isTakeAborted = false + let queuePart: (() => Promise) | undefined = undefined if (blueprint.blueprint.onTake) { const rundownId = currentRundown.rundown._id const partInstanceId = playoutModel.playlist.nextPartInfo?.partInstanceId @@ -300,6 +311,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 +325,7 @@ async function executeOnTakeCallback( playoutModel, showStyle, watchedPackagesHelper, - new PartAndPieceInstanceActionService(context, playoutModel, showStyle, currentRundown) + actionService ) try { const blueprintPersistentState = new PersistentPlayoutStateStore( @@ -323,6 +335,12 @@ async function executeOnTakeCallback( await blueprint.blueprint.onTake(onSetAsNextContext, blueprintPersistentState) await applyOnTakeSideEffects(context, playoutModel, onSetAsNextContext) isTakeAborted = onSetAsNextContext.isTakeAborted + if (onSetAsNextContext.partToQueue) { + const partToQueue = onSetAsNextContext.partToQueue + queuePart = async () => { + await actionService.queuePart(partToQueue.rawPart, partToQueue.rawPieces) + } + } if (blueprintPersistentState.hasChanges) { playoutModel.setBlueprintPersistentState(blueprintPersistentState.getAll()) @@ -354,7 +372,7 @@ async function executeOnTakeCallback( }) } } - return { isTakeAborted } + return { isTakeAborted, queuePart } } async function applyOnTakeSideEffects(context: JobContext, playoutModel: PlayoutModel, onTakeContext: OnTakeContext) { From 0e907d91a537ad5d4968356f8988eb3c91809f16 Mon Sep 17 00:00:00 2001 From: ianshade Date: Mon, 17 Nov 2025 20:37:38 +0100 Subject: [PATCH 2/3] feat(EAV-771): make it possible to queue after take from adLib actions --- .../src/context/adlibActionContext.ts | 3 + .../src/blueprints/context/OnTakeContext.ts | 19 +++++- .../src/blueprints/context/adlibActions.ts | 21 ++++++- .../PartAndPieceInstanceActionService.ts | 63 ++++++++++++------- .../job-worker/src/playout/adlibAction.ts | 2 +- .../job-worker/src/playout/adlibTesting.ts | 2 +- packages/job-worker/src/playout/take.ts | 37 ++++++----- 7 files changed, 106 insertions(+), 41 deletions(-) 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/job-worker/src/blueprints/context/OnTakeContext.ts b/packages/job-worker/src/blueprints/context/OnTakeContext.ts index 144f6d3543..714a5369c4 100644 --- a/packages/job-worker/src/blueprints/context/OnTakeContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTakeContext.ts @@ -23,12 +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 partToQueue: { rawPart: IBlueprintPart; rawPieces: IBlueprintPiece[] } | undefined + public partToQueueAfterTake: QueueablePartAndPieces | undefined public get quickLoopInfo(): BlueprintQuickLookInfo | null { return this.partAndPieceInstanceService.quickLoopInfo @@ -155,7 +159,16 @@ export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContex } queuePartAfterTake(rawPart: IBlueprintPart, rawPieces: IBlueprintPiece[]): void { - this.partToQueue = { rawPart, rawPieces } + 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 { 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 a4fdd34096..36291efde9 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,7 @@ export async function performTakeToNextedPart( const showStyle = await pShowStyle const blueprint = await context.getShowStyleBlueprint(showStyle._id) - const { isTakeAborted, queuePart } = await executeOnTakeCallback( + const { isTakeAborted, partToQueueAfterTake: partToQueueFromOnTake } = await executeOnTakeCallback( context, playoutModel, showStyle, @@ -206,6 +208,8 @@ export async function performTakeToNextedPart( currentRundown ) + partToQueueAfterTake = partToQueueAfterTake ?? partToQueueFromOnTake + if (isTakeAborted) { await updateTimeline(context, playoutModel) return @@ -270,8 +274,16 @@ export async function performTakeToNextedPart( resetPreviousSegmentIfLooping(context, playoutModel) } - if (queuePart) { - await queuePart() + if (partToQueueAfterTake) { + await insertQueuedPartWithPieces( + context, + playoutModel, + takeRundown, + takePartInstance, + partToQueueAfterTake.part, + partToQueueAfterTake.pieces, + undefined + ) } else { // Once everything is synced, we can choose the next part await setNextPart(context, playoutModel, nextPart, false) @@ -299,11 +311,11 @@ async function executeOnTakeCallback( showStyle: ReadonlyObjectDeep, blueprint: ReadonlyObjectDeep, currentRundown: PlayoutRundownModel -): Promise<{ isTakeAborted: boolean; queuePart: (() => Promise) | undefined }> { +): Promise<{ isTakeAborted: boolean; partToQueueAfterTake: QueueablePartAndPieces | undefined }> { const NOTIFICATION_CATEGORY = 'onTake' let isTakeAborted = false - let queuePart: (() => Promise) | undefined = undefined + let partToQueueAfterTake: QueueablePartAndPieces | undefined = undefined if (blueprint.blueprint.onTake) { const rundownId = currentRundown.rundown._id const partInstanceId = playoutModel.playlist.nextPartInfo?.partInstanceId @@ -335,11 +347,8 @@ async function executeOnTakeCallback( await blueprint.blueprint.onTake(onSetAsNextContext, blueprintPersistentState) await applyOnTakeSideEffects(context, playoutModel, onSetAsNextContext) isTakeAborted = onSetAsNextContext.isTakeAborted - if (onSetAsNextContext.partToQueue) { - const partToQueue = onSetAsNextContext.partToQueue - queuePart = async () => { - await actionService.queuePart(partToQueue.rawPart, partToQueue.rawPieces) - } + if (onSetAsNextContext.partToQueueAfterTake) { + partToQueueAfterTake = onSetAsNextContext.partToQueueAfterTake } if (blueprintPersistentState.hasChanges) { @@ -372,7 +381,7 @@ async function executeOnTakeCallback( }) } } - return { isTakeAborted, queuePart } + return { isTakeAborted, partToQueueAfterTake } } async function applyOnTakeSideEffects(context: JobContext, playoutModel: PlayoutModel, onTakeContext: OnTakeContext) { From 6bf546398f9f86f4be8534b14d6407ee8b9612aa Mon Sep 17 00:00:00 2001 From: ianshade Date: Tue, 18 Nov 2025 11:18:24 +0100 Subject: [PATCH 3/3] refactor(EAV-771): select next part only when needed --- packages/job-worker/src/playout/take.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index 36291efde9..87e607d138 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -258,16 +258,6 @@ 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) { @@ -286,6 +276,15 @@ export async function performTakeToNextedPart( ) } 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) }