diff --git a/packages/blueprints-integration/src/documents/part.ts b/packages/blueprints-integration/src/documents/part.ts index 42283ac360..3554e7576a 100644 --- a/packages/blueprints-integration/src/documents/part.ts +++ b/packages/blueprints-integration/src/documents/part.ts @@ -56,6 +56,13 @@ export interface IBlueprintMutatablePart): IBlueprintP disableNextInTransition: part.disableNextInTransition, outTransition: clone(part.outTransition), expectedDuration: part.expectedDuration, + availablePostrollDuration: part.availablePostrollDuration, holdMode: part.holdMode, shouldNotifyCurrentPlayingPart: part.shouldNotifyCurrentPlayingPart, ingestNotifyPartExternalId: part.ingestNotifyPartExternalId, diff --git a/packages/job-worker/src/playout/__tests__/resolvedPieces.test.ts b/packages/job-worker/src/playout/__tests__/resolvedPieces.test.ts index bb83480a06..46ae113fe2 100644 --- a/packages/job-worker/src/playout/__tests__/resolvedPieces.test.ts +++ b/packages/job-worker/src/playout/__tests__/resolvedPieces.test.ts @@ -347,7 +347,9 @@ describe('Resolved Pieces', () => { describe('getResolvedPiecesForPartInstancesOnTimeline', () => { function createPartInstance( - partProps?: Partial> + partProps?: Partial< + Pick + > ): DBPartInstance { return { _id: getRandomId(), @@ -965,5 +967,394 @@ describe('Resolved Pieces', () => { }, ] satisfies StrippedResult) }) + + test('nextPart keepalive does not shorten current part resolved duration', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const now = 990000 + const currentPartTimes = createPartCurrentTimes(now, now - 2000) + const currentPartLength = 13000 + + const currentPartInfo = createPartInstanceInfo( + currentPartTimes, + createPartInstance({ + autoNext: true, + expectedDuration: currentPartLength, + }), + [piece001] + ) + + const nextPartInfo = createPartInstanceInfo( + createPartCurrentTimes(now, currentPartTimes.partStartTime! + currentPartLength), + createPartInstance(), + [piece010] + ) + nextPartInfo.calculatedTimings = { + ...nextPartInfo.calculatedTimings, + fromPartRemaining: 2000, + fromPartKeepalive: 2000, + fromPartPostroll: 0, + } + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + next: nextPartInfo, + }, + now + ) + + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: currentPartTimes.partStartTime!, + resolvedDuration: currentPartLength, + }, + { + _id: piece010._id, + resolvedStart: currentPartTimes.partStartTime! + currentPartLength - 2000, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('nextPart outTransition does not shorten current part resolved duration', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const now = 990000 + const currentPartTimes = createPartCurrentTimes(now, now - 2000) + const currentPartLength = 13000 + + const currentPartInfo = createPartInstanceInfo( + currentPartTimes, + createPartInstance({ + autoNext: true, + expectedDuration: currentPartLength, + outTransition: { duration: 1200 }, + }), + [piece001] + ) + + const nextPartInfo = createPartInstanceInfo( + createPartCurrentTimes(now, currentPartTimes.partStartTime! + currentPartLength), + createPartInstance(), + [piece010] + ) + nextPartInfo.calculatedTimings = { + ...nextPartInfo.calculatedTimings, + fromPartRemaining: 1200, + fromPartKeepalive: 0, + fromPartPostroll: 0, + } + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + next: nextPartInfo, + }, + now + ) + + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: currentPartTimes.partStartTime!, + resolvedDuration: currentPartLength, + }, + { + _id: piece010._id, + resolvedStart: currentPartTimes.partStartTime! + currentPartLength - 1200, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('nextPart preroll-only overlap does not extend current part resolved duration', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const now = 990000 + const currentPartTimes = createPartCurrentTimes(now, now - 2000) + const currentPartLength = 13000 + + const currentPartInfo = createPartInstanceInfo( + currentPartTimes, + createPartInstance({ + autoNext: true, + expectedDuration: currentPartLength, + }), + [piece001] + ) + + const nextPartInfo = createPartInstanceInfo( + createPartCurrentTimes(now, currentPartTimes.partStartTime! + currentPartLength), + createPartInstance(), + [piece010] + ) + nextPartInfo.calculatedTimings = { + ...nextPartInfo.calculatedTimings, + fromPartRemaining: 1000, + fromPartKeepalive: 0, + fromPartPostroll: 0, + } + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + next: nextPartInfo, + }, + now + ) + + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: currentPartTimes.partStartTime!, + resolvedDuration: currentPartLength, + }, + { + _id: piece010._id, + resolvedStart: currentPartTimes.partStartTime! + currentPartLength - 1000, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('nextPart keepalive is capped by availablePostrollDuration = 0', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const now = 990000 + const currentPartTimes = createPartCurrentTimes(now, now - 2000) + const currentPartLength = 13000 + + const currentPartInfo = createPartInstanceInfo( + currentPartTimes, + createPartInstance({ + autoNext: true, + expectedDuration: currentPartLength, + availablePostrollDuration: 0, + }), + [piece001] + ) + + const nextPartInfo = createPartInstanceInfo( + createPartCurrentTimes(now, currentPartTimes.partStartTime! + currentPartLength), + createPartInstance(), + [piece010] + ) + nextPartInfo.calculatedTimings = { + ...nextPartInfo.calculatedTimings, + fromPartRemaining: 2000, + fromPartKeepalive: 2000, + fromPartPostroll: 0, + } + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + next: nextPartInfo, + }, + now + ) + + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: currentPartTimes.partStartTime!, + resolvedDuration: currentPartLength, + }, + { + _id: piece010._id, + resolvedStart: currentPartTimes.partStartTime! + currentPartLength - 2000, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('nextPart keepalive is capped when availablePostrollDuration is undefined', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const now = 990000 + const currentPartTimes = createPartCurrentTimes(now, now - 2000) + const currentPartLength = 13000 + + const currentPartInfo = createPartInstanceInfo( + currentPartTimes, + createPartInstance({ + autoNext: true, + expectedDuration: currentPartLength, + }), + [piece001] + ) + + const nextPartInfo = createPartInstanceInfo( + createPartCurrentTimes(now, currentPartTimes.partStartTime! + currentPartLength), + createPartInstance(), + [piece010] + ) + nextPartInfo.calculatedTimings = { + ...nextPartInfo.calculatedTimings, + fromPartRemaining: 2000, + fromPartKeepalive: 2000, + fromPartPostroll: 0, + } + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + next: nextPartInfo, + }, + now + ) + + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: currentPartTimes.partStartTime!, + resolvedDuration: currentPartLength, + }, + { + _id: piece010._id, + resolvedStart: currentPartTimes.partStartTime! + currentPartLength - 2000, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('nextPart keepalive is partially capped by availablePostrollDuration', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const now = 990000 + const currentPartTimes = createPartCurrentTimes(now, now - 2000) + const currentPartLength = 13000 + + const currentPartInfo = createPartInstanceInfo( + currentPartTimes, + createPartInstance({ + autoNext: true, + expectedDuration: currentPartLength, + availablePostrollDuration: 700, + }), + [piece001] + ) + + const nextPartInfo = createPartInstanceInfo( + createPartCurrentTimes(now, currentPartTimes.partStartTime! + currentPartLength), + createPartInstance(), + [piece010] + ) + nextPartInfo.calculatedTimings = { + ...nextPartInfo.calculatedTimings, + fromPartRemaining: 2000, + fromPartKeepalive: 2000, + fromPartPostroll: 0, + } + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + next: nextPartInfo, + }, + now + ) + + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: currentPartTimes.partStartTime!, + resolvedDuration: currentPartLength + 700, + }, + { + _id: piece010._id, + resolvedStart: currentPartTimes.partStartTime! + currentPartLength - 1300, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) + + test('nextPart keepalive is not capped when availablePostrollDuration is large enough', async () => { + const sourceLayerId = Object.keys(sourceLayers)[0] + expect(sourceLayerId).toBeTruthy() + + const piece001 = createPieceInstance(sourceLayerId, { start: 0 }) + const piece010 = createPieceInstance(sourceLayerId, { start: 0 }) + + const now = 990000 + const currentPartTimes = createPartCurrentTimes(now, now - 2000) + const currentPartLength = 13000 + + const currentPartInfo = createPartInstanceInfo( + currentPartTimes, + createPartInstance({ + autoNext: true, + expectedDuration: currentPartLength, + availablePostrollDuration: 5000, + }), + [piece001] + ) + + const nextPartInfo = createPartInstanceInfo( + createPartCurrentTimes(now, currentPartTimes.partStartTime! + currentPartLength), + createPartInstance(), + [piece010] + ) + nextPartInfo.calculatedTimings = { + ...nextPartInfo.calculatedTimings, + fromPartRemaining: 2000, + fromPartKeepalive: 2000, + fromPartPostroll: 0, + } + + const simpleResolvedPieces = getResolvedPiecesForPartInstancesOnTimeline( + context, + { + current: currentPartInfo, + next: nextPartInfo, + }, + now + ) + + expect(stripResult(simpleResolvedPieces)).toEqual([ + { + _id: piece001._id, + resolvedStart: currentPartTimes.partStartTime!, + resolvedDuration: currentPartLength + 2000, + }, + { + _id: piece010._id, + resolvedStart: currentPartTimes.partStartTime! + currentPartLength, + resolvedDuration: undefined, + }, + ] satisfies StrippedResult) + }) }) }) diff --git a/packages/job-worker/src/playout/resolvedPieces.ts b/packages/job-worker/src/playout/resolvedPieces.ts index 4bbc96a8c6..5305377651 100644 --- a/packages/job-worker/src/playout/resolvedPieces.ts +++ b/packages/job-worker/src/playout/resolvedPieces.ts @@ -11,6 +11,21 @@ import { import { SelectedPartInstancesTimelineInfo } from './timeline/generate.js' import { PlayoutPartInstanceModel } from './model/PlayoutPartInstanceModel.js' +function getAutoNextExpectedDurationExtension(partInstancesInfo: SelectedPartInstancesTimelineInfo): number { + if (!partInstancesInfo.current || !partInstancesInfo.next) return 0 + + // Keepalive and outTransition extend the effective expectedDuration, but preroll must stay unchanged. + const requiredExtension = Math.max( + 0, + partInstancesInfo.next.calculatedTimings.fromPartKeepalive, + partInstancesInfo.current.partInstance.part.outTransition?.duration ?? 0 + ) + + const availablePostrollDuration = partInstancesInfo.current.partInstance.part.availablePostrollDuration ?? 0 + + return Math.max(0, Math.min(requiredExtension, availablePostrollDuration)) +} + /** * Resolve the PieceInstances for a PartInstance * Uses the getCurrentTime() as approximation for 'now' @@ -46,12 +61,26 @@ export function getResolvedPiecesForPartInstancesOnTimeline( if (!partInstancesInfo.current) return [] const currentPartStarted = partInstancesInfo.current.partTimes.partStartTime ?? now + const currentPartDuration = + partInstancesInfo.current.partInstance.part.expectedDuration !== undefined + ? partInstancesInfo.current.partInstance.part.expectedDuration + + partInstancesInfo.current.calculatedTimings.toPartDelay + + partInstancesInfo.current.calculatedTimings.toPartPostroll + + getAutoNextExpectedDurationExtension(partInstancesInfo) + : null const nextPartStarted = partInstancesInfo.current.partInstance.part.autoNext && partInstancesInfo.current.partInstance.part.expectedDuration !== 0 && - partInstancesInfo.current.partInstance.part.expectedDuration !== undefined - ? currentPartStarted + partInstancesInfo.current.partInstance.part.expectedDuration + currentPartDuration !== null + ? currentPartStarted + + currentPartDuration - + (partInstancesInfo.next?.calculatedTimings.fromPartRemaining ?? 0) + : null + + const currentPartEnd = + partInstancesInfo.current.partInstance.part.autoNext && currentPartDuration !== null + ? currentPartStarted + currentPartDuration : null // Calculate the next part if needed @@ -73,7 +102,7 @@ export function getResolvedPiecesForPartInstancesOnTimeline( ) // Translate start to absolute times - offsetResolvedStartAndCapDuration(currentResolvedPieces, currentPartStarted, nextPartStarted) + offsetResolvedStartAndCapDuration(currentResolvedPieces, currentPartStarted, currentPartEnd) // Calculate the previous part let previousResolvedPieces: ResolvedPieceInstance[] = [] diff --git a/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts b/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts index d8ceb5dd71..e57987c5c3 100644 --- a/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts +++ b/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts @@ -385,6 +385,240 @@ describe('buildTimelineObjsForRundown', () => { }) describe('overlap and keepalive', () => { + it('autonext with keepalive extends current part duration', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + partTimes: createPartCurrentTimes(currentTime, 5678), + partInstance: createMockPartInstance('part0', { + autoNext: true, + expectedDuration: 5000, + }), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + regenerateTimelineAt: undefined, + }, + next: { + partTimes: createPartCurrentTimes(currentTime, undefined), + partInstance: createMockPartInstance('part1'), + pieceInstances: [createMockPieceInstance('piece1')], + calculatedTimings: { + inTransitionStart: 200, + toPartDelay: 500, + toPartPostroll: 0, + fromPartRemaining: 500 + 400, + fromPartPostroll: 400, + fromPartKeepalive: 100, + }, + regenerateTimelineAt: undefined, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) + + expect(objs.timingContext?.currentPartGroup.enable).toEqual({ + start: 'now', + duration: 5000, + }) + }) + + it('autonext with outTransition extends current part duration', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + partTimes: createPartCurrentTimes(currentTime, 5678), + partInstance: createMockPartInstance('part0', { + autoNext: true, + expectedDuration: 5000, + outTransition: { duration: 1200 }, + }), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + regenerateTimelineAt: undefined, + }, + next: { + partTimes: createPartCurrentTimes(currentTime, undefined), + partInstance: createMockPartInstance('part1'), + pieceInstances: [createMockPieceInstance('piece1')], + calculatedTimings: { + inTransitionStart: null, + toPartDelay: 1200, + toPartPostroll: 0, + fromPartRemaining: 1200, + fromPartPostroll: 0, + fromPartKeepalive: 0, + }, + regenerateTimelineAt: undefined, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) + + expect(objs.timingContext?.currentPartGroup.enable).toEqual({ + start: 'now', + duration: 5000, + }) + }) + + it('autonext does not extend current part duration for preroll-only overlap', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + partTimes: createPartCurrentTimes(currentTime, 5678), + partInstance: createMockPartInstance('part0', { autoNext: true, expectedDuration: 5000 }), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + regenerateTimelineAt: undefined, + }, + next: { + partTimes: createPartCurrentTimes(currentTime, undefined), + partInstance: createMockPartInstance('part1'), + pieceInstances: [createMockPieceInstance('piece1')], + calculatedTimings: { + inTransitionStart: null, + toPartDelay: 1000, + toPartPostroll: 0, + fromPartRemaining: 1000, + fromPartPostroll: 0, + fromPartKeepalive: 0, + }, + regenerateTimelineAt: undefined, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) + + expect(objs.timingContext?.currentPartGroup.enable).toEqual({ + start: 'now', + duration: 5000, + }) + }) + + it('autonext keepalive is capped by availablePostrollDuration = 0', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + partTimes: createPartCurrentTimes(currentTime, 5678), + partInstance: createMockPartInstance('part0', { + autoNext: true, + expectedDuration: 5000, + availablePostrollDuration: 0, + }), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + regenerateTimelineAt: undefined, + }, + next: { + partTimes: createPartCurrentTimes(currentTime, undefined), + partInstance: createMockPartInstance('part1'), + pieceInstances: [createMockPieceInstance('piece1')], + calculatedTimings: { + inTransitionStart: 200, + toPartDelay: 500, + toPartPostroll: 0, + fromPartRemaining: 500 + 400, + fromPartPostroll: 400, + fromPartKeepalive: 100, + }, + regenerateTimelineAt: undefined, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) + + expect(objs.timingContext?.currentPartGroup.enable).toEqual({ + start: 'now', + duration: 5000, + }) + }) + + it('autonext keepalive is capped when availablePostrollDuration is undefined', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + partTimes: createPartCurrentTimes(currentTime, 5678), + partInstance: createMockPartInstance('part0', { + autoNext: true, + expectedDuration: 5000, + }), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + regenerateTimelineAt: undefined, + }, + next: { + partTimes: createPartCurrentTimes(currentTime, undefined), + partInstance: createMockPartInstance('part1'), + pieceInstances: [createMockPieceInstance('piece1')], + calculatedTimings: { + inTransitionStart: 200, + toPartDelay: 500, + toPartPostroll: 0, + fromPartRemaining: 500 + 400, + fromPartPostroll: 400, + fromPartKeepalive: 100, + }, + regenerateTimelineAt: undefined, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) + + expect(objs.timingContext?.currentPartGroup.enable).toEqual({ + start: 'now', + duration: 5000, + }) + }) + + it('autonext keepalive is partially capped by availablePostrollDuration', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + partTimes: createPartCurrentTimes(currentTime, 5678), + partInstance: createMockPartInstance('part0', { + autoNext: true, + expectedDuration: 5000, + availablePostrollDuration: 50, + }), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + regenerateTimelineAt: undefined, + }, + next: { + partTimes: createPartCurrentTimes(currentTime, undefined), + partInstance: createMockPartInstance('part1'), + pieceInstances: [createMockPieceInstance('piece1')], + calculatedTimings: { + inTransitionStart: 200, + toPartDelay: 500, + toPartPostroll: 0, + fromPartRemaining: 500 + 400, + fromPartPostroll: 400, + fromPartKeepalive: 100, + }, + regenerateTimelineAt: undefined, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) + + expect(objs.timingContext?.currentPartGroup.enable).toEqual({ + start: 'now', + duration: 5050, + }) + }) + it('current and previous parts', () => { const context = setupDefaultJobEnvironment() diff --git a/packages/job-worker/src/playout/timeline/rundown.ts b/packages/job-worker/src/playout/timeline/rundown.ts index 5c4bac7562..4409710581 100644 --- a/packages/job-worker/src/playout/timeline/rundown.ts +++ b/packages/job-worker/src/playout/timeline/rundown.ts @@ -51,6 +51,24 @@ export interface RundownTimelineResult { timingContext: RundownTimelineTimingContext | undefined } +function getAutoNextExpectedDurationExtension( + currentPartInfo: SelectedPartInstanceTimelineInfo, + nextPartTimings: PartCalculatedTimings | undefined +): number { + if (!nextPartTimings) return 0 + + // Keepalive and outTransition extend the effective expectedDuration, but preroll must stay unchanged. + const requiredExtension = Math.max( + 0, + nextPartTimings.fromPartKeepalive, + currentPartInfo.partInstance.part.outTransition?.duration ?? 0 + ) + + const availablePostrollDuration = currentPartInfo.partInstance.part.availablePostrollDuration ?? 0 + + return Math.max(0, Math.min(requiredExtension, availablePostrollDuration)) +} + export function buildTimelineObjsForRundown( context: JobContext, activePlaylist: ReadonlyDeep, @@ -139,7 +157,11 @@ export function buildTimelineObjsForRundown( : new Map() // The startTime of this start is used as the reference point for the calculated timings, so we can use 'now' and everything will lie after this point - const currentPartEnable = createCurrentPartGroupEnable(partInstancesInfo.current, !!partInstancesInfo.next) + const currentPartEnable = createCurrentPartGroupEnable( + partInstancesInfo.current, + !!partInstancesInfo.next, + partInstancesInfo.next?.calculatedTimings + ) const currentPartGroup = createPartGroup(partInstancesInfo.current.partInstance, currentPartEnable) const timingContext: RundownTimelineTimingContext = { @@ -225,7 +247,8 @@ export function buildTimelineObjsForRundown( function createCurrentPartGroupEnable( currentPartInfo: SelectedPartInstanceTimelineInfo, - hasNextPart: boolean + hasNextPart: boolean, + nextPartTimings: PartCalculatedTimings | undefined ): PartEnable { // The startTime of this start is used as the reference point for the calculated timings, so we can use 'now' and everything will lie after this point const currentPartEnable: PartEnable = { start: 'now' } @@ -239,9 +262,12 @@ function createCurrentPartGroupEnable( currentPartInfo.partInstance.part.autoNext && currentPartInfo.partInstance.part.expectedDuration !== undefined ) { + const expectedDurationExtension = getAutoNextExpectedDurationExtension(currentPartInfo, nextPartTimings) + // If there is a valid autonext out of the current part, then calculate the duration currentPartEnable.duration = currentPartInfo.partInstance.part.expectedDuration + + expectedDurationExtension + currentPartInfo.calculatedTimings.toPartDelay + currentPartInfo.calculatedTimings.toPartPostroll // autonext should have the postroll added to it to not confuse the timeline