From b3b4043b92f37e12823e51cc037672cc50cded4f Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 5 Apr 2024 10:30:11 +0100 Subject: [PATCH 1/9] fix: ab playback resolver not allowing sessions to span between segments --- .../context/OnTimelineGenerateContext.ts | 4 +- .../abPlayback/__tests__/abPlayback.spec.ts | 2 +- .../__tests__/abSessionHelper.spec.ts | 45 +++++++++++++++++-- .../__tests__/applyAssignments.spec.ts | 2 +- .../src/playout/abPlayback/abSessionHelper.ts | 35 +++++++++++++-- .../src/playout/timeline/generate.ts | 1 + 6 files changed, 79 insertions(+), 10 deletions(-) diff --git a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts index 21a87eed19..a7dabed447 100644 --- a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts @@ -19,7 +19,7 @@ import { convertPartInstanceToBlueprints } from './lib' import { RundownContext } from './RundownContext' import { AbSessionHelper } from '../../playout/abPlayback/abSessionHelper' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PieceInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' export class OnTimelineGenerateContext extends RundownContext implements ITimelineEventContext { readonly currentPartInstance: Readonly | undefined @@ -36,6 +36,7 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli showStyleBlueprintConfig: ProcessedShowStyleConfig, playlist: ReadonlyDeep, rundown: ReadonlyDeep, + orderedSegmentIds: ReadonlyDeep>, previousPartInstance: ReadonlyDeep | undefined, currentPartInstance: ReadonlyDeep | undefined, nextPartInstance: ReadonlyDeep | undefined, @@ -64,6 +65,7 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli } this.abSessionsHelper = new AbSessionHelper( + orderedSegmentIds, partInstances, clone(playlist.trackedAbSessions ?? []) ) diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts index 186593aeca..baac6852b9 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts @@ -88,7 +88,7 @@ function resolveAbSessions( describe('resolveMediaPlayers', () => { // TODO - rework this to use an interface instead of mocking the methods - const abSessionHelper = new AbSessionHelper([], []) + const abSessionHelper = new AbSessionHelper([], [], []) const mockGetPieceSessionId: jest.MockedFunction = jest.fn() const mockGetObjectSessionId: jest.MockedFunction = jest.fn() diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts index b3d494fb4a..cddd16fe36 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts @@ -1,4 +1,9 @@ -import { PartInstanceId, PieceInstanceInfiniteId, PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { + PartInstanceId, + PieceInstanceInfiniteId, + PartId, + SegmentId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { ABSessionInfo } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -28,7 +33,13 @@ describe('AbSessionHelper', () => { ) { const partInstances = _.compact([previousPartInstance, currentPartInstance, nextPartInstance]) - const abSessionHelper = new AbSessionHelper(partInstances, clone(trackedAbSessions ?? [])) + const orderedSegmentIds: SegmentId[] = [protectString('segment0'), protectString('segment1')] + + const abSessionHelper = new AbSessionHelper( + orderedSegmentIds, + partInstances, + clone(trackedAbSessions ?? []) + ) let nextId = 0 abSessionHelper.getNewSessionId = () => getSessionId(nextId++) @@ -71,12 +82,21 @@ describe('AbSessionHelper', () => { isLookahead: !!isLookahead, } as any } - function createPartInstance(id: string, partId: string, rank: number): DBPartInstance { + function createPartInstance( + id: string, + partId: string, + rank: number, + segmentId?: SegmentId | string + ): DBPartInstance { + segmentId = segmentId ?? 'segment0' + // This defines only the minimum required values for the method we are calling return { _id: id, + segmentId, part: { _id: partId, + segmentId, _rank: rank, }, } as any @@ -243,6 +263,25 @@ describe('AbSessionHelper', () => { expect(abSessionHelper.getPieceABSessionId(piece2, 'name0')).toEqual(sessionId) expect(abSessionHelper.knownSessions).toHaveLength(1) }) + test('getPieceABSessionId - continue normal session from previous segment', async () => { + const { rundownId } = await setupDefaultRundownPlaylist(jobContext) + const rundown = (await jobContext.mockCollections.Rundowns.findOne(rundownId)) as DBRundown + expect(rundown).toBeTruthy() + + const nextPartInstance = createPartInstance('abcdef', 'aaa', 0, 'segment1') + const currentPartInstance = createPartInstance('12345', 'bbb', 0, 'segment0') + + const abSessionHelper = getSessionHelper([], undefined, currentPartInstance, nextPartInstance) + + const sessionId = getSessionId(0) + const piece0 = createPieceInstance(currentPartInstance._id) + expect(abSessionHelper.getPieceABSessionId(piece0, 'name0')).toEqual(sessionId) + expect(abSessionHelper.knownSessions).toHaveLength(1) + + const piece2 = createPieceInstance(nextPartInstance._id) + expect(abSessionHelper.getPieceABSessionId(piece2, 'name0')).toEqual(sessionId) + expect(abSessionHelper.knownSessions).toHaveLength(1) + }) test('getPieceABSessionId - promote lookahead session from previous part', async () => { const { rundownId } = await setupDefaultRundownPlaylist(jobContext) diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts index 16e2305492..2e70098b83 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts @@ -10,7 +10,7 @@ import { applyAbPlayerObjectAssignments } from '../applyAssignments' const POOL_NAME = 'clip' describe('applyMediaPlayersAssignments', () => { - const abSessionHelper = new AbSessionHelper([], []) + const abSessionHelper = new AbSessionHelper([], [], []) const mockGetPieceSessionId: jest.MockedFunction = jest.fn() const mockGetObjectSessionId: jest.MockedFunction = jest.fn() diff --git a/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts b/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts index 5b97bd619b..d2e80d2795 100644 --- a/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts +++ b/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts @@ -1,5 +1,5 @@ import { AB_MEDIA_PLAYER_AUTO } from '@sofie-automation/blueprints-integration' -import { PartId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartId, PieceInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { ABSessionInfo } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -18,11 +18,17 @@ interface ABSessionInfoExt extends ABSessionInfo { * A helper class for generating unique and persistent AB-playback sessionIds */ export class AbSessionHelper { + readonly #orderedSegmentIds: ReadonlyDeep> readonly #partInstances: ReadonlyDeep> readonly #knownSessions: ABSessionInfoExt[] - constructor(partInstances: ReadonlyDeep>, knownSessions: ABSessionInfo[]) { + constructor( + orderedSegmentIds: ReadonlyDeep>, + partInstances: ReadonlyDeep>, + knownSessions: ABSessionInfo[] + ) { + this.#orderedSegmentIds = orderedSegmentIds this.#partInstances = partInstances this.#knownSessions = knownSessions } @@ -76,8 +82,7 @@ export class AbSessionHelper { } // Check if we can continue sessions from the part before, or if we should create new ones - const canReuseFromPartInstanceBefore = - partInstanceIndex > 0 && this.#partInstances[partInstanceIndex - 1].part._rank < partInstance.part._rank + const canReuseFromPartInstanceBefore = this.#canReuseFromPartInstanceBefore(partInstanceIndex, partInstance) if (canReuseFromPartInstanceBefore) { // Try and find a session from the part before that we can use @@ -117,6 +122,28 @@ export class AbSessionHelper { return sessionId } + #canReuseFromPartInstanceBefore(partInstanceIndex: number, partInstance: ReadonlyDeep) { + if (partInstanceIndex <= 0) return false + + const previousPartInstance = this.#partInstances[partInstanceIndex - 1] + + // Check if the previous instance is in the same segment, and is positioned before this one + if ( + previousPartInstance.segmentId === partInstance.segmentId && + previousPartInstance.part._rank < partInstance.part._rank + ) + return true + + // Check if the previous instance is in an earlier segment + if ( + this.#orderedSegmentIds.indexOf(previousPartInstance.segmentId) < + this.#orderedSegmentIds.indexOf(partInstance.segmentId) + ) + return true + + return false + } + /** * Get the full session id for a timelineobject that belongs to an ab playback session * sessionName should also be used in calls to getPieceABSessionId for the owning piece diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index cb9e55df68..acc4c71a55 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -385,6 +385,7 @@ async function getTimelineRundown( context.getShowStyleBlueprintConfig(showStyle), playoutModel.playlist, activeRundown.rundown, + activeRundown.segments.map((s) => s.segment._id), previousPartInstance?.partInstance, currentPartInstance?.partInstance, nextPartInstance?.partInstance, From 97431bbab9094062ae0a47d80c7b17bf7a1ca74f Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 6 Sep 2024 15:12:08 +0100 Subject: [PATCH 2/9] Revert "fix: ab playback resolver not allowing sessions to span between segments" This reverts commit b3b4043b92f37e12823e51cc037672cc50cded4f. --- .../context/OnTimelineGenerateContext.ts | 4 +- .../abPlayback/__tests__/abPlayback.spec.ts | 2 +- .../__tests__/abSessionHelper.spec.ts | 45 ++----------------- .../__tests__/applyAssignments.spec.ts | 2 +- .../src/playout/abPlayback/abSessionHelper.ts | 35 ++------------- .../src/playout/timeline/generate.ts | 1 - 6 files changed, 10 insertions(+), 79 deletions(-) diff --git a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts index a7dabed447..21a87eed19 100644 --- a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts @@ -19,7 +19,7 @@ import { convertPartInstanceToBlueprints } from './lib' import { RundownContext } from './RundownContext' import { AbSessionHelper } from '../../playout/abPlayback/abSessionHelper' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { PieceInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' export class OnTimelineGenerateContext extends RundownContext implements ITimelineEventContext { readonly currentPartInstance: Readonly | undefined @@ -36,7 +36,6 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli showStyleBlueprintConfig: ProcessedShowStyleConfig, playlist: ReadonlyDeep, rundown: ReadonlyDeep, - orderedSegmentIds: ReadonlyDeep>, previousPartInstance: ReadonlyDeep | undefined, currentPartInstance: ReadonlyDeep | undefined, nextPartInstance: ReadonlyDeep | undefined, @@ -65,7 +64,6 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli } this.abSessionsHelper = new AbSessionHelper( - orderedSegmentIds, partInstances, clone(playlist.trackedAbSessions ?? []) ) diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts index baac6852b9..186593aeca 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts @@ -88,7 +88,7 @@ function resolveAbSessions( describe('resolveMediaPlayers', () => { // TODO - rework this to use an interface instead of mocking the methods - const abSessionHelper = new AbSessionHelper([], [], []) + const abSessionHelper = new AbSessionHelper([], []) const mockGetPieceSessionId: jest.MockedFunction = jest.fn() const mockGetObjectSessionId: jest.MockedFunction = jest.fn() diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts index cddd16fe36..b3d494fb4a 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts @@ -1,9 +1,4 @@ -import { - PartInstanceId, - PieceInstanceInfiniteId, - PartId, - SegmentId, -} from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartInstanceId, PieceInstanceInfiniteId, PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { ABSessionInfo } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -33,13 +28,7 @@ describe('AbSessionHelper', () => { ) { const partInstances = _.compact([previousPartInstance, currentPartInstance, nextPartInstance]) - const orderedSegmentIds: SegmentId[] = [protectString('segment0'), protectString('segment1')] - - const abSessionHelper = new AbSessionHelper( - orderedSegmentIds, - partInstances, - clone(trackedAbSessions ?? []) - ) + const abSessionHelper = new AbSessionHelper(partInstances, clone(trackedAbSessions ?? [])) let nextId = 0 abSessionHelper.getNewSessionId = () => getSessionId(nextId++) @@ -82,21 +71,12 @@ describe('AbSessionHelper', () => { isLookahead: !!isLookahead, } as any } - function createPartInstance( - id: string, - partId: string, - rank: number, - segmentId?: SegmentId | string - ): DBPartInstance { - segmentId = segmentId ?? 'segment0' - + function createPartInstance(id: string, partId: string, rank: number): DBPartInstance { // This defines only the minimum required values for the method we are calling return { _id: id, - segmentId, part: { _id: partId, - segmentId, _rank: rank, }, } as any @@ -263,25 +243,6 @@ describe('AbSessionHelper', () => { expect(abSessionHelper.getPieceABSessionId(piece2, 'name0')).toEqual(sessionId) expect(abSessionHelper.knownSessions).toHaveLength(1) }) - test('getPieceABSessionId - continue normal session from previous segment', async () => { - const { rundownId } = await setupDefaultRundownPlaylist(jobContext) - const rundown = (await jobContext.mockCollections.Rundowns.findOne(rundownId)) as DBRundown - expect(rundown).toBeTruthy() - - const nextPartInstance = createPartInstance('abcdef', 'aaa', 0, 'segment1') - const currentPartInstance = createPartInstance('12345', 'bbb', 0, 'segment0') - - const abSessionHelper = getSessionHelper([], undefined, currentPartInstance, nextPartInstance) - - const sessionId = getSessionId(0) - const piece0 = createPieceInstance(currentPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece0, 'name0')).toEqual(sessionId) - expect(abSessionHelper.knownSessions).toHaveLength(1) - - const piece2 = createPieceInstance(nextPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece2, 'name0')).toEqual(sessionId) - expect(abSessionHelper.knownSessions).toHaveLength(1) - }) test('getPieceABSessionId - promote lookahead session from previous part', async () => { const { rundownId } = await setupDefaultRundownPlaylist(jobContext) diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts index 2e70098b83..16e2305492 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts @@ -10,7 +10,7 @@ import { applyAbPlayerObjectAssignments } from '../applyAssignments' const POOL_NAME = 'clip' describe('applyMediaPlayersAssignments', () => { - const abSessionHelper = new AbSessionHelper([], [], []) + const abSessionHelper = new AbSessionHelper([], []) const mockGetPieceSessionId: jest.MockedFunction = jest.fn() const mockGetObjectSessionId: jest.MockedFunction = jest.fn() diff --git a/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts b/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts index d2e80d2795..5b97bd619b 100644 --- a/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts +++ b/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts @@ -1,5 +1,5 @@ import { AB_MEDIA_PLAYER_AUTO } from '@sofie-automation/blueprints-integration' -import { PartId, PieceInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartId, PieceInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { ABSessionInfo } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -18,17 +18,11 @@ interface ABSessionInfoExt extends ABSessionInfo { * A helper class for generating unique and persistent AB-playback sessionIds */ export class AbSessionHelper { - readonly #orderedSegmentIds: ReadonlyDeep> readonly #partInstances: ReadonlyDeep> readonly #knownSessions: ABSessionInfoExt[] - constructor( - orderedSegmentIds: ReadonlyDeep>, - partInstances: ReadonlyDeep>, - knownSessions: ABSessionInfo[] - ) { - this.#orderedSegmentIds = orderedSegmentIds + constructor(partInstances: ReadonlyDeep>, knownSessions: ABSessionInfo[]) { this.#partInstances = partInstances this.#knownSessions = knownSessions } @@ -82,7 +76,8 @@ export class AbSessionHelper { } // Check if we can continue sessions from the part before, or if we should create new ones - const canReuseFromPartInstanceBefore = this.#canReuseFromPartInstanceBefore(partInstanceIndex, partInstance) + const canReuseFromPartInstanceBefore = + partInstanceIndex > 0 && this.#partInstances[partInstanceIndex - 1].part._rank < partInstance.part._rank if (canReuseFromPartInstanceBefore) { // Try and find a session from the part before that we can use @@ -122,28 +117,6 @@ export class AbSessionHelper { return sessionId } - #canReuseFromPartInstanceBefore(partInstanceIndex: number, partInstance: ReadonlyDeep) { - if (partInstanceIndex <= 0) return false - - const previousPartInstance = this.#partInstances[partInstanceIndex - 1] - - // Check if the previous instance is in the same segment, and is positioned before this one - if ( - previousPartInstance.segmentId === partInstance.segmentId && - previousPartInstance.part._rank < partInstance.part._rank - ) - return true - - // Check if the previous instance is in an earlier segment - if ( - this.#orderedSegmentIds.indexOf(previousPartInstance.segmentId) < - this.#orderedSegmentIds.indexOf(partInstance.segmentId) - ) - return true - - return false - } - /** * Get the full session id for a timelineobject that belongs to an ab playback session * sessionName should also be used in calls to getPieceABSessionId for the owning piece diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 1687b201ba..ae055d97a1 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -385,7 +385,6 @@ async function getTimelineRundown( context.getShowStyleBlueprintConfig(showStyle), playoutModel.playlist, activeRundown.rundown, - activeRundown.segments.map((s) => s.segment._id), previousPartInstance?.partInstance, currentPartInstance?.partInstance, nextPartInstance?.partInstance, From 0c98315caded165e1c06839787a97945b0a3fe9e Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 6 Sep 2024 15:11:41 +0100 Subject: [PATCH 3/9] feat: allow ab sessions to declare themselves as 'globally unique' This bypasses any sessionName collision/restarting logic, and lets blueprints take care of that internally --- .../corelib/src/dataModel/RundownPlaylist.ts | 2 + .../context/OnTimelineGenerateContext.ts | 4 +- .../abPlayback/__tests__/abPlayback.spec.ts | 186 ++++++++++++++---- .../__tests__/abSessionHelper.spec.ts | 186 ++++++++++-------- .../__tests__/applyAssignments.spec.ts | 4 +- .../playout/abPlayback/abPlaybackSessions.ts | 10 +- .../src/playout/abPlayback/abSessionHelper.ts | 101 +++++++--- .../playout/abPlayback/applyAssignments.ts | 5 +- .../shared-lib/src/core/model/Timeline.ts | 8 + 9 files changed, 343 insertions(+), 163 deletions(-) diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index 58511757f2..28b713d314 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -17,6 +17,8 @@ export interface ABSessionInfo { id: string /** The name of the session from the blueprints */ name: string + /** Whether the name is treated as globally unique */ + isUniqueName: boolean /** Set if the session is being by lookahead for a future part */ lookaheadForPartId?: PartId /** Set if the session is being used by an infinite PieceInstance */ diff --git a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts index 21a87eed19..9b7af560d3 100644 --- a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts @@ -85,13 +85,13 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli const partInstanceId = pieceInstance?.partInstanceId if (!partInstanceId) throw new Error('Missing partInstanceId in call to getPieceABSessionId') - return this.abSessionsHelper.getPieceABSessionId(pieceInstance, sessionName) + return this.abSessionsHelper.getPieceABSessionIdFromSessionName(pieceInstance, sessionName) } /** * @deprecated Use core provided AB resolving */ getTimelineObjectAbSessionId(tlObj: OnGenerateTimelineObjExt, sessionName: string): string | undefined { - return this.abSessionsHelper.getTimelineObjectAbSessionId(tlObj, sessionName) + return this.abSessionsHelper.getTimelineObjectAbSessionIdFromSessionName(tlObj, sessionName) } } diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts index 186593aeca..10f0739173 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abPlayback.spec.ts @@ -1,4 +1,9 @@ -import { ABResolverOptions, IBlueprintPieceType, PieceLifespan } from '@sofie-automation/blueprints-integration' +import { + ABResolverOptions, + IBlueprintPieceType, + PieceAbSessionInfo, + PieceLifespan, +} from '@sofie-automation/blueprints-integration' import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { PieceInstancePiece, ResolvedPieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { ABSessionAssignments } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -21,7 +26,8 @@ function createBasicResolvedPieceInstance( start: number, duration: number | undefined, reqId: string | undefined, - optional?: boolean + optional?: boolean, + uniqueSessionName?: boolean ): ResolvedPieceInstance { const piece = literal({ _id: protectString(id), @@ -47,6 +53,7 @@ function createBasicResolvedPieceInstance( sessionName: reqId, poolName: POOL_NAME, optional: optional, + sessionNameIsGloballyUnique: uniqueSessionName, }, ] } @@ -120,7 +127,9 @@ describe('resolveMediaPlayers', () => { createBasicResolvedPieceInstance('2', 800, 4000, 'ghi'), ] - mockGetPieceSessionId.mockImplementation((piece, name) => `${piece._id}_${name}`) + mockGetPieceSessionId.mockImplementation( + (piece, session) => `${piece._id}_${session.poolName}_${session.sessionName}` + ) const assignments = resolveAbSessions( abSessionHelper, @@ -143,9 +152,18 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, { + poolName: 'clip', + sessionName: 'abc', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, { + poolName: 'clip', + sessionName: 'def', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, { + poolName: 'clip', + sessionName: 'ghi', + } satisfies PieceAbSessionInfo) }) test('basic pieces - players with string Ids', () => { @@ -156,7 +174,9 @@ describe('resolveMediaPlayers', () => { createBasicResolvedPieceInstance('2', 800, 4000, 'ghi'), ] - mockGetPieceSessionId.mockImplementation((piece, name) => `${piece._id}_${name}`) + mockGetPieceSessionId.mockImplementation( + (piece, session) => `${piece._id}_${session.poolName}_${session.sessionName}` + ) const assignments = resolveAbSessions( abSessionHelper, @@ -179,9 +199,18 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, { + poolName: 'clip', + sessionName: 'abc', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, { + poolName: 'clip', + sessionName: 'def', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, { + poolName: 'clip', + sessionName: 'ghi', + } satisfies PieceAbSessionInfo) }) test('basic pieces - players with number and string Ids', () => { @@ -192,7 +221,9 @@ describe('resolveMediaPlayers', () => { createBasicResolvedPieceInstance('2', 800, 4000, 'ghi'), ] - mockGetPieceSessionId.mockImplementation((piece, name) => `${piece._id}_${name}`) + mockGetPieceSessionId.mockImplementation( + (piece, session) => `${piece._id}_${session.poolName}_${session.sessionName}` + ) const assignments = resolveAbSessions( abSessionHelper, @@ -215,9 +246,18 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, { + poolName: 'clip', + sessionName: 'abc', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, { + poolName: 'clip', + sessionName: 'def', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, { + poolName: 'clip', + sessionName: 'ghi', + } satisfies PieceAbSessionInfo) }) test('Multiple pieces same id', () => { @@ -229,7 +269,7 @@ describe('resolveMediaPlayers', () => { createBasicResolvedPieceInstance('3', 6400, 1000, 'abc'), // Gap before ] - mockGetPieceSessionId.mockImplementation((_piece, name) => `tmp_${name}`) + mockGetPieceSessionId.mockImplementation((_piece, session) => `tmp_${session.poolName}_${session.sessionName}`) const assignments = resolveAbSessions( abSessionHelper, @@ -250,10 +290,22 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(4) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(4, pieces[3].instance, 'clip_abc') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, { + poolName: 'clip', + sessionName: 'abc', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, { + poolName: 'clip', + sessionName: 'abc', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, { + poolName: 'clip', + sessionName: 'abc', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(4, pieces[3].instance, { + poolName: 'clip', + sessionName: 'abc', + } satisfies PieceAbSessionInfo) }) test('Reuse after gap', () => { @@ -264,7 +316,9 @@ describe('resolveMediaPlayers', () => { createBasicResolvedPieceInstance('3', 6400, 1000, 'ghi'), // Wait, then reuse first ] - mockGetPieceSessionId.mockImplementation((piece, name) => `${piece._id}_${name}`) + mockGetPieceSessionId.mockImplementation( + (piece, session) => `${piece._id}_${session.poolName}_${session.sessionName}` + ) const assignments = resolveAbSessions( abSessionHelper, @@ -287,9 +341,18 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, { + poolName: 'clip', + sessionName: 'abc', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, { + poolName: 'clip', + sessionName: 'def', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, { + poolName: 'clip', + sessionName: 'ghi', + } satisfies PieceAbSessionInfo) }) test('Reuse immediately', () => { @@ -300,7 +363,9 @@ describe('resolveMediaPlayers', () => { createBasicResolvedPieceInstance('3', 5400, 1000, 'ghi'), // Wait, then reuse first ] - mockGetPieceSessionId.mockImplementation((piece, name) => `${piece._id}_${name}`) + mockGetPieceSessionId.mockImplementation( + (piece, session) => `${piece._id}_${session.poolName}_${session.sessionName}` + ) const assignments = resolveAbSessions( abSessionHelper, @@ -323,9 +388,18 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, { + poolName: 'clip', + sessionName: 'abc', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, { + poolName: 'clip', + sessionName: 'def', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, { + poolName: 'clip', + sessionName: 'ghi', + } satisfies PieceAbSessionInfo) }) test('Reuse immediately dense', () => { @@ -336,7 +410,9 @@ describe('resolveMediaPlayers', () => { createBasicResolvedPieceInstance('3', 5400, 1000, 'ghi'), // Wait, then reuse first ] - mockGetPieceSessionId.mockImplementation((piece, name) => `${piece._id}_${name}`) + mockGetPieceSessionId.mockImplementation( + (piece, session) => `${piece._id}_${session.poolName}_${session.sessionName}` + ) const assignments = resolveAbSessions( abSessionHelper, @@ -359,9 +435,18 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, { + poolName: 'clip', + sessionName: 'abc', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, { + poolName: 'clip', + sessionName: 'def', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, { + poolName: 'clip', + sessionName: 'ghi', + } satisfies PieceAbSessionInfo) }) test('basic reassignment', () => { @@ -383,7 +468,9 @@ describe('resolveMediaPlayers', () => { createBasicResolvedPieceInstance('2', 2800, 4000, 'ghi'), ] - mockGetPieceSessionId.mockImplementation((piece, name) => `${piece._id}_${name}`) + mockGetPieceSessionId.mockImplementation( + (piece, session) => `${piece._id}_${session.poolName}_${session.sessionName}` + ) const assignments = resolveAbSessions( abSessionHelper, @@ -406,9 +493,18 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, { + poolName: 'clip', + sessionName: 'abc', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, { + poolName: 'clip', + sessionName: 'def', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, { + poolName: 'clip', + sessionName: 'ghi', + } satisfies PieceAbSessionInfo) }) test('optional gets discarded', () => { @@ -430,7 +526,9 @@ describe('resolveMediaPlayers', () => { createBasicResolvedPieceInstance('2', 2800, 4000, 'ghi'), ] - mockGetPieceSessionId.mockImplementation((piece, name) => `${piece._id}_${name}`) + mockGetPieceSessionId.mockImplementation( + (piece, session) => `${piece._id}_${session.poolName}_${session.sessionName}` + ) const assignments = resolveAbSessions( abSessionHelper, @@ -453,9 +551,19 @@ describe('resolveMediaPlayers', () => { expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3) expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0) - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, 'clip_abc') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, 'clip_def') - expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi') + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0].instance, { + poolName: 'clip', + sessionName: 'abc', + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1].instance, { + poolName: 'clip', + sessionName: 'def', + optional: true, + } satisfies PieceAbSessionInfo) + expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, { + poolName: 'clip', + sessionName: 'ghi', + } satisfies PieceAbSessionInfo) }) // TODO add some tests which check lookahead diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts index b3d494fb4a..8b8d52033c 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts @@ -10,6 +10,7 @@ import _ = require('underscore') import { setupDefaultRundownPlaylist, setupMockShowStyleCompound } from '../../../__mocks__/presetCollections' import { AbSessionHelper } from '../abSessionHelper' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { TimelineObjectAbSessionInfo } from '@sofie-automation/shared-lib/dist/core/model/Timeline' describe('AbSessionHelper', () => { let jobContext: MockJobContext @@ -22,11 +23,9 @@ describe('AbSessionHelper', () => { const getSessionId = (n: number): string => `session#${n}` function getSessionHelper( trackedAbSessions: ABSessionInfo[], - previousPartInstance: DBPartInstance | undefined, - currentPartInstance: DBPartInstance | undefined, - nextPartInstance: DBPartInstance | undefined + ...sortedPartInstances: Array ) { - const partInstances = _.compact([previousPartInstance, currentPartInstance, nextPartInstance]) + const partInstances = _.compact(sortedPartInstances) const abSessionHelper = new AbSessionHelper(partInstances, clone(trackedAbSessions ?? [])) @@ -82,6 +81,19 @@ describe('AbSessionHelper', () => { } as any } + const testSession0: TimelineObjectAbSessionInfo = { + poolName: 'pool0', + sessionName: 'name0', + } + const testSession1: TimelineObjectAbSessionInfo = { + poolName: 'pool0', + sessionName: 'name1', + } + const testSession2: TimelineObjectAbSessionInfo = { + poolName: 'pool0', + sessionName: 'name2', + } + test('getPieceABSessionId - knownSessions basic', async () => { const { rundownId } = await setupDefaultRundownPlaylist(jobContext) const rundown = (await jobContext.mockCollections.Rundowns.findOne(rundownId)) as DBRundown @@ -89,16 +101,16 @@ describe('AbSessionHelper', () => { // No sessions { - const abSessionHelper = getSessionHelper([], undefined, undefined, undefined) + const abSessionHelper = getSessionHelper([]) expect(abSessionHelper.knownSessions).toEqual([]) } // some sessions { - const sessions: ABSessionInfo[] = [{ id: 'abc', name: 'no' }] + const sessions: ABSessionInfo[] = [{ id: 'abc', name: 'no', isUniqueName: false }] // Mod the sessions to be returned by knownSessions const moddedSessions = sessions.map((s) => ({ ...s, keep: true })) - const abSessionHelper = getSessionHelper(moddedSessions, undefined, undefined, undefined) + const abSessionHelper = getSessionHelper(moddedSessions) expect(abSessionHelper.knownSessions).toEqual(sessions) } }) @@ -109,30 +121,30 @@ describe('AbSessionHelper', () => { expect(rundown).toBeTruthy() { - const abSessionHelper = getSessionHelper([], undefined, undefined, undefined) + const abSessionHelper = getSessionHelper([]) const piece1 = createPieceInstance(undefined as any) - expect(() => abSessionHelper.getPieceABSessionId(piece1, 'name0')).toThrow( + expect(() => abSessionHelper.getPieceABSessionId(piece1, testSession0)).toThrow( 'Unknown partInstanceId in call to getPieceABSessionId' ) const piece2 = createPieceInstance('defdef') - expect(() => abSessionHelper.getPieceABSessionId(piece2, 'name0')).toThrow( + expect(() => abSessionHelper.getPieceABSessionId(piece2, testSession0)).toThrow( 'Unknown partInstanceId in call to getPieceABSessionId' ) } { const tmpPartInstance = createPartInstance('abcdef', 'aaa', 1) - const abSessionHelper = getSessionHelper([], undefined, undefined, tmpPartInstance) + const abSessionHelper = getSessionHelper([], tmpPartInstance) const piece0 = createPieceInstance('defdef') - expect(() => abSessionHelper.getPieceABSessionId(piece0, 'name0')).toThrow( + expect(() => abSessionHelper.getPieceABSessionId(piece0, testSession0)).toThrow( 'Unknown partInstanceId in call to getPieceABSessionId' ) const piece1 = createPieceInstance('abcdef') - expect(abSessionHelper.getPieceABSessionId(piece1, 'name0')).toBeTruthy() + expect(abSessionHelper.getPieceABSessionId(piece1, testSession0)).toBeTruthy() } }) @@ -143,7 +155,7 @@ describe('AbSessionHelper', () => { const nextPartInstance = createPartInstance('abcdef', 'aaa', 1) const currentPartInstance = createPartInstance('12345', 'bbb', 0) - const abSessionHelper = getSessionHelper([], undefined, currentPartInstance, nextPartInstance) + const abSessionHelper = getSessionHelper([], currentPartInstance, nextPartInstance) // Get the id const piece0 = createPieceInstance(nextPartInstance._id) @@ -151,31 +163,32 @@ describe('AbSessionHelper', () => { { id: getSessionId(0), infiniteInstanceId: undefined, - name: 'name0', + name: 'pool0_name0', + isUniqueName: false, partInstanceIds: [nextPartInstance._id], }, ] - expect(abSessionHelper.getPieceABSessionId(piece0, 'name0')).toEqual(expectedSessions[0].id) + expect(abSessionHelper.getPieceABSessionId(piece0, testSession0)).toEqual(expectedSessions[0].id) expect(getAllKnownSessions(abSessionHelper)).toEqual(expectedSessions) expect(abSessionHelper.knownSessions).toHaveLength(1) // Should get the same id again - expect(abSessionHelper.getPieceABSessionId(piece0, 'name0')).toEqual(expectedSessions[0].id) + expect(abSessionHelper.getPieceABSessionId(piece0, testSession0)).toEqual(expectedSessions[0].id) expect(getAllKnownSessions(abSessionHelper)).toEqual(expectedSessions) expect(abSessionHelper.knownSessions).toHaveLength(1) const piece1 = createPieceInstance(nextPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece1, 'name0')).toEqual(expectedSessions[0].id) + expect(abSessionHelper.getPieceABSessionId(piece1, testSession0)).toEqual(expectedSessions[0].id) expect(getAllKnownSessions(abSessionHelper)).toEqual(expectedSessions) expect(abSessionHelper.knownSessions).toHaveLength(1) // Try for the other part const piece2 = createPieceInstance(currentPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece2, 'name0')).not.toEqual(expectedSessions[0].id) + expect(abSessionHelper.getPieceABSessionId(piece2, testSession0)).not.toEqual(expectedSessions[0].id) expect(abSessionHelper.knownSessions).toHaveLength(2) // Or another name - expect(abSessionHelper.getPieceABSessionId(piece1, 'name1')).not.toEqual(expectedSessions[0].id) + expect(abSessionHelper.getPieceABSessionId(piece1, testSession1)).not.toEqual(expectedSessions[0].id) expect(abSessionHelper.knownSessions).toHaveLength(3) }) @@ -190,36 +203,39 @@ describe('AbSessionHelper', () => { const expectedSessions: ABSessionInfo[] = [ { id: 'current0', - name: 'name0', + name: 'pool0_name0', + isUniqueName: false, partInstanceIds: [currentPartInstance._id], }, { id: 'current1', - name: 'name1', + name: 'pool0_name1', + isUniqueName: false, partInstanceIds: [currentPartInstance._id], }, { id: 'next0', - name: 'name0', + name: 'pool0_name0', + isUniqueName: false, partInstanceIds: [nextPartInstance._id], }, ] - const abSessionHelper = getSessionHelper(expectedSessions, undefined, currentPartInstance, nextPartInstance) + const abSessionHelper = getSessionHelper(expectedSessions, currentPartInstance, nextPartInstance) // Reuse the ids const piece0 = createPieceInstance(currentPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece0, 'name0')).toEqual(expectedSessions[0].id) + expect(abSessionHelper.getPieceABSessionId(piece0, testSession0)).toEqual(expectedSessions[0].id) expect(getAllKnownSessions(abSessionHelper)).toEqual(expectedSessions) expect(abSessionHelper.knownSessions).toHaveLength(1) const piece1 = createPieceInstance(currentPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece1, 'name1')).toEqual(expectedSessions[1].id) + expect(abSessionHelper.getPieceABSessionId(piece1, testSession1)).toEqual(expectedSessions[1].id) expect(getAllKnownSessions(abSessionHelper)).toEqual(expectedSessions) expect(abSessionHelper.knownSessions).toHaveLength(2) const piece2 = createPieceInstance(nextPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece2, 'name0')).toEqual(expectedSessions[2].id) + expect(abSessionHelper.getPieceABSessionId(piece2, testSession0)).toEqual(expectedSessions[2].id) expect(getAllKnownSessions(abSessionHelper)).toEqual(expectedSessions) expect(abSessionHelper.knownSessions).toHaveLength(3) }) @@ -232,15 +248,15 @@ describe('AbSessionHelper', () => { const nextPartInstance = createPartInstance('abcdef', 'aaa', 1) const currentPartInstance = createPartInstance('12345', 'bbb', 0) - const abSessionHelper = getSessionHelper([], undefined, currentPartInstance, nextPartInstance) + const abSessionHelper = getSessionHelper([], currentPartInstance, nextPartInstance) const sessionId = getSessionId(0) const piece0 = createPieceInstance(currentPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece0, 'name0')).toEqual(sessionId) + expect(abSessionHelper.getPieceABSessionId(piece0, testSession0)).toEqual(sessionId) expect(abSessionHelper.knownSessions).toHaveLength(1) const piece2 = createPieceInstance(nextPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece2, 'name0')).toEqual(sessionId) + expect(abSessionHelper.getPieceABSessionId(piece2, testSession0)).toEqual(sessionId) expect(abSessionHelper.knownSessions).toHaveLength(1) }) @@ -256,45 +272,43 @@ describe('AbSessionHelper', () => { const lookaheadSessions: ABSessionInfo[] = [ { id: 'lookahead0', - name: 'name0', + name: 'pool0_name0', + isUniqueName: false, lookaheadForPartId: currentPartInstance.part._id, partInstanceIds: [currentPartInstance._id], }, { id: 'lookahead1', - name: 'name1', + name: 'pool0_name1', + isUniqueName: false, lookaheadForPartId: currentPartInstance.part._id, partInstanceIds: undefined, }, { id: 'lookahead2', - name: 'name2', + name: 'pool0_name2', + isUniqueName: false, lookaheadForPartId: distantPartId, partInstanceIds: undefined, }, ] - const abSessionHelper = getSessionHelper( - lookaheadSessions, - previousPartInstance, - currentPartInstance, - undefined - ) + const abSessionHelper = getSessionHelper(lookaheadSessions, previousPartInstance, currentPartInstance) // lookahead0 is for us const piece0 = createPieceInstance(currentPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece0, 'name0')).toEqual('lookahead0') + expect(abSessionHelper.getPieceABSessionId(piece0, testSession0)).toEqual('lookahead0') expect(abSessionHelper.knownSessions).toHaveLength(1) // lookahead1 is for us const piece1 = createPieceInstance(currentPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece1, 'name1')).toEqual('lookahead1') + expect(abSessionHelper.getPieceABSessionId(piece1, testSession1)).toEqual('lookahead1') expect(abSessionHelper.knownSessions).toHaveLength(2) // lookahead2 is not for us, so we shouldnt get it const sessionId = getSessionId(0) const piece2 = createPieceInstance(currentPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece2, 'name2')).toEqual(sessionId) + expect(abSessionHelper.getPieceABSessionId(piece2, testSession2)).toEqual(sessionId) expect(abSessionHelper.knownSessions).toHaveLength(3) }) @@ -306,28 +320,28 @@ describe('AbSessionHelper', () => { const nextPartInstance = createPartInstance('abcdef', 'aaa', 1) const currentPartInstance = createPartInstance('12345', 'bbb', 10) - const abSessionHelper = getSessionHelper([], undefined, currentPartInstance, nextPartInstance) + const abSessionHelper = getSessionHelper([], currentPartInstance, nextPartInstance) // Start a new infinite session const sessionId0 = getSessionId(0) const infinite0 = protectString('infinite0') const piece0 = createPieceInstance(currentPartInstance._id, infinite0) - expect(abSessionHelper.getPieceABSessionId(piece0, 'name0')).toEqual(sessionId0) + expect(abSessionHelper.getPieceABSessionId(piece0, testSession0)).toEqual(sessionId0) expect(abSessionHelper.knownSessions).toHaveLength(1) // Double check the reuslt - expect(abSessionHelper.getPieceABSessionId(piece0, 'name0')).toEqual(sessionId0) + expect(abSessionHelper.getPieceABSessionId(piece0, testSession0)).toEqual(sessionId0) expect(abSessionHelper.knownSessions).toHaveLength(1) // Normal piece in the same part gets different id const sessionId1 = getSessionId(1) const piece1 = createPieceInstance(currentPartInstance._id) - expect(abSessionHelper.getPieceABSessionId(piece1, 'name0')).toEqual(sessionId1) + expect(abSessionHelper.getPieceABSessionId(piece1, testSession0)).toEqual(sessionId1) expect(abSessionHelper.knownSessions).toHaveLength(2) // Span session to a part with a lower rank const piece2 = createPieceInstance(nextPartInstance._id, infinite0) - expect(abSessionHelper.getPieceABSessionId(piece2, 'name0')).toEqual(sessionId0) + expect(abSessionHelper.getPieceABSessionId(piece2, testSession0)).toEqual(sessionId0) expect(abSessionHelper.knownSessions).toHaveLength(2) }) @@ -336,14 +350,14 @@ describe('AbSessionHelper', () => { const rundown = (await jobContext.mockCollections.Rundowns.findOne(rundownId)) as DBRundown expect(rundown).toBeTruthy() - const abSessionHelper = getSessionHelper([], undefined, undefined, undefined) + const abSessionHelper = getSessionHelper([]) // no session needed - expect(abSessionHelper.getTimelineObjectAbSessionId({} as any, 'name0')).toBeUndefined() + expect(abSessionHelper.getTimelineObjectAbSessionId({} as any, testSession0)).toBeUndefined() // unknown partInstance const obj1 = createTimelineObject('abcd') - expect(abSessionHelper.getTimelineObjectAbSessionId(obj1, 'name0')).toBeUndefined() + expect(abSessionHelper.getTimelineObjectAbSessionId(obj1, testSession0)).toBeUndefined() }) function generateGetTimelineObjectAbSessionIdSessions( @@ -356,45 +370,53 @@ describe('AbSessionHelper', () => { return [ { id: 'current0', - name: 'name0', + name: 'pool0_name0', + isUniqueName: false, partInstanceIds: [currentPartInstance._id], }, { id: 'current1', - name: 'name1', + name: 'pool0_name1', + isUniqueName: false, partInstanceIds: [currentPartInstance._id], }, { id: 'next0', - name: 'name0', + name: 'pool0_name0', + isUniqueName: false, partInstanceIds: [nextPartInstance._id], }, { id: 'lookahead0', - name: 'name0', + name: 'pool0_name0', + isUniqueName: false, lookaheadForPartId: currentPartInstance.part._id, partInstanceIds: [currentPartInstance._id], }, { id: 'lookahead1', - name: 'name1', + name: 'pool0_name1', + isUniqueName: false, lookaheadForPartId: currentPartInstance.part._id, partInstanceIds: undefined, }, { id: 'lookahead2', - name: 'name2', + name: 'pool0_name2', + isUniqueName: false, lookaheadForPartId: distantPartId, partInstanceIds: undefined, }, { id: 'inf0', - name: 'name0', + name: 'pool0_name0', + isUniqueName: false, infiniteInstanceId: infinite0, }, { id: 'inf1', - name: 'name0', + name: 'pool0_name0', + isUniqueName: false, infiniteInstanceId: infinite1, }, ] @@ -416,22 +438,22 @@ describe('AbSessionHelper', () => { protectString('infinite1') ) - const abSessionHelper = getSessionHelper(existingSessions, undefined, currentPartInstance, nextPartInstance) + const abSessionHelper = getSessionHelper(existingSessions, currentPartInstance, nextPartInstance) // no session recorded for partInstance const obj1 = createTimelineObject(nextPartInstance._id) - expect(abSessionHelper.getTimelineObjectAbSessionId(obj1, 'name0')).toBeUndefined() + expect(abSessionHelper.getTimelineObjectAbSessionId(obj1, testSession0)).toBeUndefined() // partInstance with session const obj2 = createTimelineObject(currentPartInstance._id) - expect(abSessionHelper.getTimelineObjectAbSessionId(obj2, 'name0')).toEqual('current0') - expect(abSessionHelper.getTimelineObjectAbSessionId(obj2, 'name1')).toEqual('current1') + expect(abSessionHelper.getTimelineObjectAbSessionId(obj2, testSession0)).toEqual('current0') + expect(abSessionHelper.getTimelineObjectAbSessionId(obj2, testSession1)).toEqual('current1') // // define a session now // overwriteKnownSessions(context, [{ // { // id: 'current0', - // name: 'name0', + // name: 'pool0_name0', // partInstanceIds: [currentPartInstance._id], // }, // }]) @@ -457,52 +479,49 @@ describe('AbSessionHelper', () => { protectString('infinite1') ) - const abSessionHelper = getSessionHelper( - [...existingSessions], - undefined, - currentPartInstance, - nextPartInstance - ) + const abSessionHelper = getSessionHelper([...existingSessions], currentPartInstance, nextPartInstance) // no session if no partId const obj1 = createTimelineObject(null, undefined, true) - expect(abSessionHelper.getTimelineObjectAbSessionId(obj1, 'name0')).toBeUndefined() + expect(abSessionHelper.getTimelineObjectAbSessionId(obj1, testSession0)).toBeUndefined() expect(abSessionHelper.knownSessions).toHaveLength(0) // existing 'distant' lookahead session const obj2 = createTimelineObject(unprotectString(distantPartId), undefined, true) - expect(abSessionHelper.getTimelineObjectAbSessionId(obj2, 'name2')).toEqual('lookahead2') + expect(abSessionHelper.getTimelineObjectAbSessionId(obj2, testSession2)).toEqual('lookahead2') expect(abSessionHelper.knownSessions).toHaveLength(1) // new 'distant' lookahead session const obj2a = createTimelineObject(unprotectString(distantPartId), undefined, true) - expect(abSessionHelper.getTimelineObjectAbSessionId(obj2a, 'name0')).toEqual(getSessionId(0)) + expect(abSessionHelper.getTimelineObjectAbSessionId(obj2a, testSession0)).toEqual(getSessionId(0)) expect(abSessionHelper.knownSessions).toHaveLength(2) existingSessions.push({ id: getSessionId(0), lookaheadForPartId: distantPartId, - name: 'name0', + name: 'pool0_name0', + isUniqueName: false, }) // current partInstance session const obj3 = createTimelineObject(currentPartInstance._id, undefined, true) - expect(abSessionHelper.getTimelineObjectAbSessionId(obj3, 'name1')).toEqual('current1') + expect(abSessionHelper.getTimelineObjectAbSessionId(obj3, testSession1)).toEqual('current1') expect(abSessionHelper.knownSessions).toHaveLength(3) // next partInstance session const obj4 = createTimelineObject(nextPartInstance._id, undefined, true) - expect(abSessionHelper.getTimelineObjectAbSessionId(obj4, 'name0')).toEqual('next0') + expect(abSessionHelper.getTimelineObjectAbSessionId(obj4, testSession0)).toEqual('next0') expect(abSessionHelper.knownSessions).toHaveLength(4) // next partInstance new session const obj5 = createTimelineObject(nextPartInstance._id, undefined, true) - expect(abSessionHelper.getTimelineObjectAbSessionId(obj5, 'name1')).toEqual(getSessionId(1)) + expect(abSessionHelper.getTimelineObjectAbSessionId(obj5, testSession1)).toEqual(getSessionId(1)) expect(abSessionHelper.knownSessions).toHaveLength(5) existingSessions.push({ id: getSessionId(1), lookaheadForPartId: nextPartInstance.part._id, - name: 'name1', + name: 'pool0_name1', + isUniqueName: false, partInstanceIds: [nextPartInstance._id], }) @@ -529,23 +548,18 @@ describe('AbSessionHelper', () => { infinite1 ) - const abSessionHelper = getSessionHelper( - [...existingSessions], - undefined, - currentPartInstance, - nextPartInstance - ) + const abSessionHelper = getSessionHelper([...existingSessions], currentPartInstance, nextPartInstance) const obj1 = createTimelineObject(currentPartInstance._id, infinite0) - expect(abSessionHelper.getTimelineObjectAbSessionId(obj1, 'name0')).toEqual('inf0') + expect(abSessionHelper.getTimelineObjectAbSessionId(obj1, testSession0)).toEqual('inf0') expect(abSessionHelper.knownSessions).toHaveLength(1) const obj2 = createTimelineObject(null, infinite1) - expect(abSessionHelper.getTimelineObjectAbSessionId(obj2, 'name0')).toEqual('inf1') + expect(abSessionHelper.getTimelineObjectAbSessionId(obj2, testSession0)).toEqual('inf1') expect(abSessionHelper.knownSessions).toHaveLength(2) const obj3 = createTimelineObject(null, protectString('fake')) - expect(abSessionHelper.getTimelineObjectAbSessionId(obj3, 'name0')).toBeUndefined() + expect(abSessionHelper.getTimelineObjectAbSessionId(obj3, testSession0)).toBeUndefined() expect(abSessionHelper.knownSessions).toHaveLength(2) // Ensure the sessions havent changed diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts index 16e2305492..af6a9a4e0b 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/applyAssignments.spec.ts @@ -75,7 +75,9 @@ describe('applyMediaPlayersAssignments', () => { const pieceInstanceId = 'piece0' const partInstanceId = protectString('part0') - mockGetObjectSessionId.mockImplementation((obj, name) => `${obj.pieceInstanceId}_${name}`) + mockGetObjectSessionId.mockImplementation( + (obj, session) => `${obj.pieceInstanceId}_${session.poolName}_${session.sessionName}` + ) const objects = [ literal({ diff --git a/packages/job-worker/src/playout/abPlayback/abPlaybackSessions.ts b/packages/job-worker/src/playout/abPlayback/abPlaybackSessions.ts index 757db512ad..94fb3b7213 100644 --- a/packages/job-worker/src/playout/abPlayback/abPlaybackSessions.ts +++ b/packages/job-worker/src/playout/abPlayback/abPlaybackSessions.ts @@ -35,10 +35,7 @@ export function calculateSessionTimeRanges( for (const session of abSessions) { if (session.poolName !== poolName) continue - const sessionId = abSessionHelper.getPieceABSessionId( - p.instance, - abSessionHelper.validateSessionName(p.instance._id, session) - ) + const sessionId = abSessionHelper.getPieceABSessionId(p.instance, session) // Note: multiple generated sessionIds for a single piece will not work as there will not be enough info to assign objects to different players. TODO is this still true? const val = sessionRequests[sessionId] @@ -81,10 +78,7 @@ export function calculateSessionTimeRanges( ) { for (const session of obj.abSessions) { if (session.poolName === poolName) { - const sessionId = abSessionHelper.getTimelineObjectAbSessionId( - obj, - abSessionHelper.validateSessionName(obj.pieceInstanceId, session) - ) + const sessionId = abSessionHelper.getTimelineObjectAbSessionId(obj, session) if (sessionId) { const existing = groupedLookaheadMap.get(sessionId) groupedLookaheadMap.set(sessionId, existing ? [...existing, obj] : [obj]) diff --git a/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts b/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts index 5b97bd619b..18df420b07 100644 --- a/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts +++ b/packages/job-worker/src/playout/abPlayback/abSessionHelper.ts @@ -40,11 +40,50 @@ export class AbSessionHelper { return getRandomString() } + /** + * Get the full session id for an ab playback session with a globally unique sessionName + */ + getUniqueSessionId(session: TimelineObjectAbSessionInfo): string { + const sessionName = `${session.poolName}_${session.sessionName}` + + const uniqueNameSession = this.#knownSessions.find((s) => s.isUniqueName && s.name === sessionName) + if (uniqueNameSession) { + uniqueNameSession.keep = true + return uniqueNameSession.id + } + + // Otherwise define a new session + const sessionId = this.getNewSessionId() + const newSession: ABSessionInfoExt = { + id: sessionId, + name: sessionName, + isUniqueName: true, + keep: true, + } + this.#knownSessions.push(newSession) + return sessionId + } + + /** + * Get the full session id for an ab playback session. + * Note: If sessionNameIsGloballyUnique is set, then the sessionName every reference will be treated as the same session, + * otherwise sessionName should be unique within the segment unless pieces want to share a session + */ + getPieceABSessionId(pieceInstance: ReadonlyDeep, session: TimelineObjectAbSessionInfo): string { + if (session.sessionNameIsGloballyUnique) return this.getUniqueSessionId(session) + + return this.getPieceABSessionIdFromSessionName( + pieceInstance, + this.#validateSessionName(pieceInstance._id, session) + ) + } + /** * Get the full session id for an ab playback session. * Note: sessionName should be unique within the segment unless pieces want to share a session + * Future: This should be private, but is exposed as a deprecated method to blueprints */ - getPieceABSessionId(pieceInstance: ReadonlyDeep, sessionName: string): string { + getPieceABSessionIdFromSessionName(pieceInstance: ReadonlyDeep, sessionName: string): string { const partInstanceIndex = this.#partInstances.findIndex((p) => p._id === pieceInstance.partInstanceId) const partInstance = partInstanceIndex >= 0 ? this.#partInstances[partInstanceIndex] : undefined if (!partInstance) throw new Error('Unknown partInstanceId in call to getPieceABSessionId') @@ -57,19 +96,20 @@ export class AbSessionHelper { return session.id } + // Sessions to consider are those with the same name + const sessionsToConsider = this.#knownSessions.filter((s) => !s.isUniqueName && s.name === sessionName) + // If this is an infinite continuation, then reuse that if (infiniteId) { - const infiniteSession = this.#knownSessions.find( - (s) => s.infiniteInstanceId === infiniteId && s.name === sessionName - ) + const infiniteSession = sessionsToConsider.find((s) => s.infiniteInstanceId === infiniteId) if (infiniteSession) { return preserveSession(infiniteSession) } } // We only want to consider sessions already tagged to this partInstance - const existingSession = this.#knownSessions.find( - (s) => s.partInstanceIds?.includes(pieceInstance.partInstanceId) && s.name === sessionName + const existingSession = sessionsToConsider.find((s) => + s.partInstanceIds?.includes(pieceInstance.partInstanceId) ) if (existingSession) { return preserveSession(existingSession) @@ -82,9 +122,7 @@ export class AbSessionHelper { if (canReuseFromPartInstanceBefore) { // Try and find a session from the part before that we can use const previousPartInstanceId = this.#partInstances[partInstanceIndex - 1]._id - const continuedSession = this.#knownSessions.find( - (s) => s.partInstanceIds?.includes(previousPartInstanceId) && s.name === sessionName - ) + const continuedSession = sessionsToConsider.find((s) => s.partInstanceIds?.includes(previousPartInstanceId)) if (continuedSession) { continuedSession.partInstanceIds = [ ...(continuedSession.partInstanceIds || []), @@ -96,9 +134,7 @@ export class AbSessionHelper { // Find an existing lookahead session to convert const partId = partInstance.part._id - const lookaheadSession = this.#knownSessions.find( - (s) => s.name === sessionName && s.lookaheadForPartId === partId - ) + const lookaheadSession = sessionsToConsider.find((s) => s.lookaheadForPartId === partId) if (lookaheadSession) { lookaheadSession.partInstanceIds = [pieceInstance.partInstanceId] return preserveSession(lookaheadSession) @@ -109,6 +145,7 @@ export class AbSessionHelper { const newSession: ABSessionInfoExt = { id: sessionId, name: sessionName, + isUniqueName: false, infiniteInstanceId: unpartialString(infiniteId), partInstanceIds: !infiniteId ? [pieceInstance.partInstanceId] : [], keep: true, @@ -117,17 +154,38 @@ export class AbSessionHelper { return sessionId } + /** + * Get the full session id for a timelineobject that belongs to an ab playback session + * The same session should also be used in calls for the owning piece + */ + getTimelineObjectAbSessionId( + tlObj: OnGenerateTimelineObjExt, + session: TimelineObjectAbSessionInfo + ): string | undefined { + if (session.sessionNameIsGloballyUnique) return this.getUniqueSessionId(session) + + return this.getTimelineObjectAbSessionIdFromSessionName( + tlObj, + this.#validateSessionName(tlObj.pieceInstanceId || session.sessionName, session) + ) + } + /** * Get the full session id for a timelineobject that belongs to an ab playback session * sessionName should also be used in calls to getPieceABSessionId for the owning piece + * Future: This should be private, but is exposed as a deprecated method to blueprints */ - getTimelineObjectAbSessionId(tlObj: OnGenerateTimelineObjExt, sessionName: string): string | undefined { + getTimelineObjectAbSessionIdFromSessionName( + tlObj: OnGenerateTimelineObjExt, + sessionName: string + ): string | undefined { + // Sessions to consider are those with the same name + const sessionsToConsider = this.#knownSessions.filter((s) => !s.isUniqueName && s.name === sessionName) + // Find an infinite const searchId = tlObj.infinitePieceInstanceId if (searchId) { - const infiniteSession = this.#knownSessions.find( - (s) => s.infiniteInstanceId === searchId && s.name === sessionName - ) + const infiniteSession = sessionsToConsider.find((s) => s.infiniteInstanceId === searchId) if (infiniteSession) { infiniteSession.keep = true return infiniteSession.id @@ -137,9 +195,7 @@ export class AbSessionHelper { // Find an normal partInstance const partInstanceId = tlObj.partInstanceId if (partInstanceId) { - const partInstanceSession = this.#knownSessions.find( - (s) => s.partInstanceIds?.includes(partInstanceId) && s.name === sessionName - ) + const partInstanceSession = sessionsToConsider.find((s) => s.partInstanceIds?.includes(partInstanceId)) if (partInstanceSession) { partInstanceSession.keep = true return partInstanceSession.id @@ -153,9 +209,7 @@ export class AbSessionHelper { const partInstance = this.#partInstances.find((p) => p._id === partInstanceId) if (partInstance) partId = partInstance.part._id - const lookaheadSession = this.#knownSessions.find( - (s) => s.lookaheadForPartId === partId && s.name === sessionName - ) + const lookaheadSession = sessionsToConsider.find((s) => s.lookaheadForPartId === partId) if (lookaheadSession) { lookaheadSession.keep = true if (partInstance) { @@ -167,6 +221,7 @@ export class AbSessionHelper { this.#knownSessions.push({ id: sessionId, name: sessionName, + isUniqueName: false, lookaheadForPartId: partId, partInstanceIds: partInstance ? [partInstanceId] : undefined, keep: true, @@ -181,7 +236,7 @@ export class AbSessionHelper { /** * Make the sessionName unique for the pool, and ensure it isn't set to AUTO */ - validateSessionName(pieceInstanceId: PieceInstanceId | string, session: TimelineObjectAbSessionInfo): string { + #validateSessionName(pieceInstanceId: PieceInstanceId | string, session: TimelineObjectAbSessionInfo): string { const newName = session.sessionName === AB_MEDIA_PLAYER_AUTO ? pieceInstanceId : session.sessionName return `${session.poolName}_${newName}` } diff --git a/packages/job-worker/src/playout/abPlayback/applyAssignments.ts b/packages/job-worker/src/playout/abPlayback/applyAssignments.ts index 03e240df9a..57537d5578 100644 --- a/packages/job-worker/src/playout/abPlayback/applyAssignments.ts +++ b/packages/job-worker/src/playout/abPlayback/applyAssignments.ts @@ -47,10 +47,7 @@ export function applyAbPlayerObjectAssignments( if (obj.abSessions && obj.pieceInstanceId) { for (const session of obj.abSessions) { if (session.poolName === poolName) { - const sessionId = abSessionHelper.getTimelineObjectAbSessionId( - obj, - abSessionHelper.validateSessionName(obj.pieceInstanceId, session) - ) + const sessionId = abSessionHelper.getTimelineObjectAbSessionId(obj, session) if (sessionId) { const existing = groupedObjectsMap.get(sessionId) groupedObjectsMap.set(sessionId, existing ? [...existing, obj] : [obj]) diff --git a/packages/shared-lib/src/core/model/Timeline.ts b/packages/shared-lib/src/core/model/Timeline.ts index 2eeb041172..8c6ece2934 100644 --- a/packages/shared-lib/src/core/model/Timeline.ts +++ b/packages/shared-lib/src/core/model/Timeline.ts @@ -16,6 +16,14 @@ export interface TimelineObjectAbSessionInfo { * The name of the AB Pool this session is for */ poolName: string + + /** + * Whether the `sessionName` of this session is globally unique + * This means that every usage of this name will be treated as the same session, regardless of where it is used + * This should typically only be used when generating a unique id in an adlib-action, if used during ingest + * then replaying a part will often cause the session to be reused which is likely not the desired behaviour + */ + sessionNameIsGloballyUnique?: boolean } export enum TimelineObjHoldMode { From a017246d2a8a26a645660ccc33b2c93086707ba2 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 6 Sep 2024 16:41:22 +0100 Subject: [PATCH 4/9] chore: unit tests --- .../__tests__/abSessionHelper.spec.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts index 8b8d52033c..87936ee58b 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts @@ -94,6 +94,15 @@ describe('AbSessionHelper', () => { sessionName: 'name2', } + const unqiueSession0: TimelineObjectAbSessionInfo = { + ...testSession0, + sessionNameIsGloballyUnique: true, + } + const unqiueSession1: TimelineObjectAbSessionInfo = { + ...testSession1, + sessionNameIsGloballyUnique: true, + } + test('getPieceABSessionId - knownSessions basic', async () => { const { rundownId } = await setupDefaultRundownPlaylist(jobContext) const rundown = (await jobContext.mockCollections.Rundowns.findOne(rundownId)) as DBRundown @@ -345,6 +354,50 @@ describe('AbSessionHelper', () => { expect(abSessionHelper.knownSessions).toHaveLength(2) }) + test('getPieceABSessionId - unique session', async () => { + const { rundownId } = await setupDefaultRundownPlaylist(jobContext) + const rundown = (await jobContext.mockCollections.Rundowns.findOne(rundownId)) as DBRundown + expect(rundown).toBeTruthy() + + const nextPartInstance = createPartInstance('abcdef', 'aaa', 1) + const currentPartInstance = createPartInstance('12345', 'bbb', 0) + const abSessionHelper = getSessionHelper([], currentPartInstance, nextPartInstance) + + // Get the id + const piece0 = createPieceInstance(nextPartInstance._id) + const expectedSessions: ABSessionInfo[] = [ + { + id: getSessionId(0), + name: 'pool0_name0', + isUniqueName: true, + }, + ] + expect(abSessionHelper.getPieceABSessionId(piece0, unqiueSession0)).toEqual(expectedSessions[0].id) + expect(getAllKnownSessions(abSessionHelper)).toEqual(expectedSessions) + expect(abSessionHelper.knownSessions).toHaveLength(1) + + // Should get the same id again + expect(abSessionHelper.getPieceABSessionId(piece0, unqiueSession0)).toEqual(expectedSessions[0].id) + expect(getAllKnownSessions(abSessionHelper)).toEqual(expectedSessions) + expect(abSessionHelper.knownSessions).toHaveLength(1) + + const piece1 = createPieceInstance(nextPartInstance._id) + expect(abSessionHelper.getPieceABSessionId(piece1, unqiueSession0)).toEqual(expectedSessions[0].id) + expect(getAllKnownSessions(abSessionHelper)).toEqual(expectedSessions) + expect(abSessionHelper.knownSessions).toHaveLength(1) + + // Try for the other part + const piece2 = createPieceInstance(currentPartInstance._id) + expect(abSessionHelper.getPieceABSessionId(piece2, unqiueSession0)).toEqual(expectedSessions[0].id) + expect(abSessionHelper.knownSessions).toHaveLength(1) + + // Or the non-unique version + expect( + abSessionHelper.getPieceABSessionId(piece1, { ...unqiueSession0, sessionNameIsGloballyUnique: false }) + ).not.toEqual(expectedSessions[0].id) + expect(abSessionHelper.knownSessions).toHaveLength(2) + }) + test('getTimelineObjectAbSessionId - bad parameters', async () => { const { rundownId } = await setupDefaultRundownPlaylist(jobContext) const rundown = (await jobContext.mockCollections.Rundowns.findOne(rundownId)) as DBRundown @@ -565,4 +618,50 @@ describe('AbSessionHelper', () => { // Ensure the sessions havent changed expect(getAllKnownSessions(abSessionHelper)).toEqual(existingSessions) }) + + test('getTimelineObjectAbSessionId - unique session', async () => { + const { rundownId } = await setupDefaultRundownPlaylist(jobContext) + const rundown = (await jobContext.mockCollections.Rundowns.findOne(rundownId)) as DBRundown + expect(rundown).toBeTruthy() + + const nextPartInstance = createPartInstance('abcdef', 'aaa', 1) + const currentPartInstance = createPartInstance('12345', 'bbb', 10) + + const existingSessions: ABSessionInfo[] = [ + { + id: 'unique0', + name: 'pool0_name0', + isUniqueName: true, + }, + { + id: 'unique1', + name: 'pool0_name1', + isUniqueName: true, + }, + { + id: 'normal0', + name: 'pool0_name0', + isUniqueName: false, + partInstanceIds: [currentPartInstance._id], + }, + ] + + const abSessionHelper = getSessionHelper(existingSessions, currentPartInstance, nextPartInstance) + + // no session recorded for partInstance + const obj1 = createTimelineObject(nextPartInstance._id) + expect(abSessionHelper.getTimelineObjectAbSessionId(obj1, unqiueSession0)).toEqual('unique0') + + // partInstance with session + const obj2 = createTimelineObject(currentPartInstance._id) + expect(abSessionHelper.getTimelineObjectAbSessionId(obj2, unqiueSession0)).toEqual('unique0') + expect(abSessionHelper.getTimelineObjectAbSessionId(obj2, unqiueSession1)).toEqual('unique1') + + // Non unique sessions + expect(abSessionHelper.getTimelineObjectAbSessionId(obj2, testSession0)).toEqual('normal0') + expect(abSessionHelper.getTimelineObjectAbSessionId(obj1, testSession0)).toBeUndefined() + + // Ensure the sessions havent changed + expect(getAllKnownSessions(abSessionHelper)).toEqual(existingSessions) + }) }) From 8d528d70188a8af63506cd9de1f25578bc936449 Mon Sep 17 00:00:00 2001 From: olzzon Date: Wed, 11 Sep 2024 13:12:45 +0200 Subject: [PATCH 5/9] fix: reorder ab assignments if part has been inserted --- .../playout/abPlayback/abPlaybackResolver.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts b/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts index 7d8cc16c26..2f35db444c 100644 --- a/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts +++ b/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts @@ -73,6 +73,23 @@ export function resolveAbAssignmentsFromRequests( return res } + // Check if requests has been inserted since last calculation: + let resetRemainingAssigments = false + res.requests.forEach((req, index) => { + // If the next item is assigned, then we assume that this item has been inserted since last calculation: + if (req.playerId === undefined && res.requests[index + 1]?.playerId !== undefined) { + delete res.requests[index].playerId + // reset previous assignments: + if (index > 0) { + delete res.requests[index - 1].playerId + } + // reset remaining assignments: + resetRemainingAssigments = true + } else if (resetRemainingAssigments) { + delete res.requests[index].playerId + } + }) + let grouped = _.groupBy(res.requests, (r) => r.playerId ?? 'undefined') let pendingRequests = grouped[undefined as any] if (!pendingRequests) { @@ -81,7 +98,7 @@ export function resolveAbAssignmentsFromRequests( } const originalLookaheadAssignments: Record = {} - for (const req of rawRequests) { + for (const req of res.requests) { if (req.lookaheadRank !== undefined && req.playerId !== undefined) { originalLookaheadAssignments[req.id] = req.playerId delete req.playerId From bdaea13807776f04cd5c6c367249f41ab753f296 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 17 Sep 2024 16:37:16 +0100 Subject: [PATCH 6/9] Revert "fix: reorder ab assignments if part has been inserted" This reverts commit 8d528d70188a8af63506cd9de1f25578bc936449. --- .../playout/abPlayback/abPlaybackResolver.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts b/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts index 2f35db444c..7d8cc16c26 100644 --- a/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts +++ b/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts @@ -73,23 +73,6 @@ export function resolveAbAssignmentsFromRequests( return res } - // Check if requests has been inserted since last calculation: - let resetRemainingAssigments = false - res.requests.forEach((req, index) => { - // If the next item is assigned, then we assume that this item has been inserted since last calculation: - if (req.playerId === undefined && res.requests[index + 1]?.playerId !== undefined) { - delete res.requests[index].playerId - // reset previous assignments: - if (index > 0) { - delete res.requests[index - 1].playerId - } - // reset remaining assignments: - resetRemainingAssigments = true - } else if (resetRemainingAssigments) { - delete res.requests[index].playerId - } - }) - let grouped = _.groupBy(res.requests, (r) => r.playerId ?? 'undefined') let pendingRequests = grouped[undefined as any] if (!pendingRequests) { @@ -98,7 +81,7 @@ export function resolveAbAssignmentsFromRequests( } const originalLookaheadAssignments: Record = {} - for (const req of res.requests) { + for (const req of rawRequests) { if (req.lookaheadRank !== undefined && req.playerId !== undefined) { originalLookaheadAssignments[req.id] = req.playerId delete req.playerId From b5348830a6c11a84477f314c7934f2b08f10d29c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 17 Sep 2024 15:13:15 +0100 Subject: [PATCH 7/9] wip: add back ab logging --- .../playout/abPlayback/applyAssignments.ts | 9 ++++++-- .../src/playout/abPlayback/index.ts | 21 +++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/job-worker/src/playout/abPlayback/applyAssignments.ts b/packages/job-worker/src/playout/abPlayback/applyAssignments.ts index 57537d5578..42125a702b 100644 --- a/packages/job-worker/src/playout/abPlayback/applyAssignments.ts +++ b/packages/job-worker/src/playout/abPlayback/applyAssignments.ts @@ -5,7 +5,7 @@ import { ICommonContext, ABTimelineLayerChangeRules, } from '@sofie-automation/blueprints-integration' -import { ABSessionAssignments } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ABSessionAssignment, ABSessionAssignments } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { OnGenerateTimelineObjExt } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { logger } from '../../logging' import * as _ from 'underscore' @@ -105,7 +105,12 @@ export function applyAbPlayerObjectAssignments( logger.debug(`Unexpected sessions are: ${unexpectedSessions.join(', ')}`) } - logger.silly(`ABPlayback calculated assignments for "${poolName}": ${JSON.stringify(newAssignments)}`) + for (const assignment of Object.values(newAssignments)) { + if (!assignment) continue + logger.silly( + `ABPlayback: Assigned session "${poolName}"-"${assignment.sessionId}" to player "${assignment.playerId}" (lookahead: ${assignment.lookahead})` + ) + } return newAssignments } diff --git a/packages/job-worker/src/playout/abPlayback/index.ts b/packages/job-worker/src/playout/abPlayback/index.ts index b27d0bbb03..3b107ded64 100644 --- a/packages/job-worker/src/playout/abPlayback/index.ts +++ b/packages/job-worker/src/playout/abPlayback/index.ts @@ -6,7 +6,7 @@ import { WrappedShowStyleBlueprint } from '../../blueprints/cache' import { ReadonlyDeep } from 'type-fest' import { JobContext, ProcessedShowStyleCompound } from '../../jobs' import { getCurrentTime } from '../../lib' -import { resolveAbAssignmentsFromRequests } from './abPlaybackResolver' +import { resolveAbAssignmentsFromRequests, SessionRequest } from './abPlaybackResolver' import { calculateSessionTimeRanges } from './abPlaybackSessions' import { applyAbPlayerObjectAssignments } from './applyAssignments' import { AbSessionHelper } from './abSessionHelper' @@ -48,6 +48,17 @@ export function applyAbPlaybackForTimeline( ) const previousAbSessionAssignments: Record = playlist.assignedAbSessions || {} + logger.silly(`ABPlayback: Starting AB playback resolver ----------------------------`) + for (const [pool, assignments] of Object.entries(previousAbSessionAssignments)) { + for (const assignment of Object.values(assignments)) { + if (assignment) { + logger.silly( + `ABPlayback: Previous assignment "${pool}"-"${assignment.sessionId}" to player "${assignment.playerId}"` + ) + } + } + } + const newAbSessionsResult: Record = {} const span = context.startSpan('blueprint.abPlaybackResolver') @@ -73,7 +84,13 @@ export function applyAbPlaybackForTimeline( now ) - logger.silly(`ABPlayback resolved sessions for "${poolName}": ${JSON.stringify(assignments)}`) + for (const assignment of Object.values(assignments.requests)) { + logger.silly( + `ABPlayback resolved session "${poolName}"-"${assignment.id}" to player "${ + assignment.playerId + }" (${JSON.stringify(assignment)})` + ) + } if (assignments.failedRequired.length > 0) { logger.warn( `ABPlayback failed to assign sessions for "${poolName}": ${JSON.stringify(assignments.failedRequired)}` From cc8bf3c079fab4a1609e30f42c60cf1309a154b3 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 17 Sep 2024 16:02:04 +0100 Subject: [PATCH 8/9] fix: ensure ab sessions get reordered when adlibbing a fixed duration piece --- .../__tests__/abPlaybackResolver.spec.ts | 41 ++++++++ .../playout/abPlayback/abPlaybackResolver.ts | 96 +++++++++---------- .../src/playout/abPlayback/index.ts | 6 +- 3 files changed, 92 insertions(+), 51 deletions(-) diff --git a/packages/job-worker/src/playout/abPlayback/__tests__/abPlaybackResolver.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abPlaybackResolver.spec.ts index 7b35da41d8..5cc5d4c267 100644 --- a/packages/job-worker/src/playout/abPlayback/__tests__/abPlaybackResolver.spec.ts +++ b/packages/job-worker/src/playout/abPlayback/__tests__/abPlaybackResolver.spec.ts @@ -394,6 +394,47 @@ describe('resolveAbAssignmentsFromRequests', () => { expectGotPlayer(res, 'c', undefined) expectGotPlayer(res, 'd', undefined) }) + test('Run timed adlib bts', () => { + const requests: SessionRequest[] = [ + // current part + { + id: 'a', + start: 1000, + end: 10500, + playerId: 2, + }, + // adlib + { + id: 'b', + start: 10000, + end: 15000, + }, + // lookaheads (in order of future use) + { + id: 'c', + start: Number.POSITIVE_INFINITY, + end: undefined, + playerId: 1, + lookaheadRank: 1, + }, + { + id: 'd', + start: Number.POSITIVE_INFINITY, + end: undefined, + playerId: 2, + lookaheadRank: 2, + }, + ] + + const res = resolveAbAssignmentsFromRequests(resolverOptions, TWO_SLOTS, requests, 10000) + expect(res).toBeTruthy() + expect(res.failedOptional).toEqual([]) + expect(res.failedRequired).toEqual([]) + expectGotPlayer(res, 'a', 2) + expectGotPlayer(res, 'b', 1) + expectGotPlayer(res, 'c', 2) // moved so that it alternates + expectGotPlayer(res, 'd', 1) + }) test('Autonext run bts', () => { const requests: SessionRequest[] = [ diff --git a/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts b/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts index 7d8cc16c26..782a8b9229 100644 --- a/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts +++ b/packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts @@ -2,13 +2,15 @@ import { ABResolverOptions } from '@sofie-automation/blueprints-integration' import { clone } from '@sofie-automation/corelib/dist/lib' import * as _ from 'underscore' +export type PlayerId = number | string + export interface SessionRequest { readonly id: string readonly start: number readonly end: number | undefined readonly optional?: boolean readonly lookaheadRank?: number - playerId?: number | string + playerId?: PlayerId } export interface AssignmentResult { @@ -21,7 +23,7 @@ export interface AssignmentResult { } interface SlotAvailability { - id: number | string + id: PlayerId before: (SessionRequest & { end: number }) | null after: SessionRequest | null clashes: SessionRequest[] @@ -55,7 +57,7 @@ function safeMin(arr: T[], func: (val: T) => number): T | undefined { */ export function resolveAbAssignmentsFromRequests( resolverOptions: ABResolverOptions, - playerIds: Array, + playerIds: PlayerId[], rawRequests: SessionRequest[], now: number // Current time ): AssignmentResult { @@ -80,7 +82,7 @@ export function resolveAbAssignmentsFromRequests( return res } - const originalLookaheadAssignments: Record = {} + const originalLookaheadAssignments: Record = {} for (const req of rawRequests) { if (req.lookaheadRank !== undefined && req.playerId !== undefined) { originalLookaheadAssignments[req.id] = req.playerId @@ -104,7 +106,7 @@ export function resolveAbAssignmentsFromRequests( pendingRequests = grouped[undefined as any] // build map of slots and what they already have assigned - const slots: Map = new Map() + const slots = new Map() _.each(playerIds, (id) => slots.set(id, grouped[id] || [])) const beforeHasGap = (p: SlotAvailability, req: SessionRequest): boolean => @@ -311,16 +313,27 @@ export function resolveAbAssignmentsFromRequests( } } + assignPlayersForLookahead(slots, res, originalLookaheadAssignments, safeNow) + + return res +} + +function assignPlayersForLookahead( + slots: Map, + res: AssignmentResult, + originalLookaheadAssignments: Record, + safeNow: number +) { // Ensure lookahead gets assigned based on priority not some randomness // Includes slots which have either no sessions, or the last has a known end time - const lastSessionPerSlot: Record = {} // playerId, end + const lastSessionPerSlot = new Map() // playerId, end for (const [playerId, sessions] of slots) { const last = _.last(sessions.filter((s) => s.lookaheadRank === undefined)) if (!last) { - lastSessionPerSlot[playerId] = Number.NEGATIVE_INFINITY + lastSessionPerSlot.set(playerId, Number.NEGATIVE_INFINITY) } else if (last.end !== undefined) { // If there is a defined end, then it can be useful after that point of time - lastSessionPerSlot[playerId] = last.end + lastSessionPerSlot.set(playerId, last.end) } } @@ -328,56 +341,39 @@ export function resolveAbAssignmentsFromRequests( const lookaheadsToAssign = _.sortBy( res.requests.filter((r) => r.lookaheadRank !== undefined), (r) => r.lookaheadRank - ).slice(0, Object.keys(lastSessionPerSlot).length) - - // Persist previous players if possible - const remainingLookaheads: SessionRequest[] = [] - for (const req of lookaheadsToAssign) { - delete req.playerId - + ).slice(0, lastSessionPerSlot.size) + + const [playersClearNow, playersClearSoon] = _.partition( + Array.from(lastSessionPerSlot.entries()), + (session) => session[1] < safeNow + ) + + // Assign the players which are clear right now + const playersClearNowIds = new Set(playersClearNow.map((p) => p[0])) + // First persist any previous players + const lookaheadsToAssignNow = lookaheadsToAssign.slice(0, playersClearNow.length) + for (const req of lookaheadsToAssignNow) { const prevPlayer = originalLookaheadAssignments[req.id] - if (prevPlayer === undefined) { - remainingLookaheads.push(req) - } else { - // Ensure the assignment is ok - const slotEnd = lastSessionPerSlot[prevPlayer] - if (slotEnd === undefined || slotEnd >= safeNow) { - // It isnt available for this lookahead, or isnt visible yet - remainingLookaheads.push(req) - } else { - // It is ours, so remove the player from the pool - req.playerId = prevPlayer - delete lastSessionPerSlot[req.playerId] - } - } - } - - // Assign any remaining lookaheads - const sortedSlots = _.sortBy(Object.entries(lastSessionPerSlot), (s) => s[1] ?? 0) - for (let i = 0; i < remainingLookaheads.length; i++) { - const slot = sortedSlots[i] - const req = remainingLookaheads[i] - - if (slot) { - // Check if we were originally given a player index rather than a string Id - if (playerIds.find((id) => typeof id === 'number' && id === Number(slot[0]))) { - req.playerId = Number(slot[0]) - } else { - req.playerId = slot[0] - } + if (prevPlayer !== undefined && playersClearNowIds.delete(prevPlayer)) { + req.playerId = prevPlayer } else { delete req.playerId } } + // Then fill in the blanks + const playersClearNowRemainingIds = Array.from(playersClearNowIds) + for (const req of lookaheadsToAssignNow) { + if (req.playerId === undefined) req.playerId = playersClearNowRemainingIds.shift() + } - return res + // Assign the players which are clear soon. These aren't visible, so don't need to preserve anything + const lookaheadsToAssignSoon = lookaheadsToAssign.slice(playersClearNow.length) + for (const req of lookaheadsToAssignSoon) { + req.playerId = playersClearSoon.shift()?.[0] + } } -function getAvailability( - id: number | string, - thisReq: SessionRequest, - orderedRequests: SessionRequest[] -): SlotAvailability { +function getAvailability(id: PlayerId, thisReq: SessionRequest, orderedRequests: SessionRequest[]): SlotAvailability { const res: SlotAvailability = { id, before: null, diff --git a/packages/job-worker/src/playout/abPlayback/index.ts b/packages/job-worker/src/playout/abPlayback/index.ts index 3b107ded64..0de0ce19a9 100644 --- a/packages/job-worker/src/playout/abPlayback/index.ts +++ b/packages/job-worker/src/playout/abPlayback/index.ts @@ -1,5 +1,9 @@ import { ResolvedPieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { ABSessionAssignments, DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { + ABSessionAssignment, + ABSessionAssignments, + DBRundownPlaylist, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { OnGenerateTimelineObjExt } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { endTrace, sendTrace, startTrace } from '@sofie-automation/corelib/dist/influxdb' import { WrappedShowStyleBlueprint } from '../../blueprints/cache' From 6b37efcb785810cdf58b9de5a58138ba8d2354e3 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Sep 2024 14:39:40 +0100 Subject: [PATCH 9/9] fix: ensure pieces don't claim to have durations when their end has not yet been decided --- packages/job-worker/src/playout/resolvedPieces.ts | 3 ++- packages/job-worker/src/playout/timeline/part.ts | 7 ++++++- packages/job-worker/src/playout/timeline/piece.ts | 6 ++++-- packages/job-worker/src/playout/timeline/rundown.ts | 4 +++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/job-worker/src/playout/resolvedPieces.ts b/packages/job-worker/src/playout/resolvedPieces.ts index 46c8c83067..089ea5f83f 100644 --- a/packages/job-worker/src/playout/resolvedPieces.ts +++ b/packages/job-worker/src/playout/resolvedPieces.ts @@ -47,13 +47,14 @@ export function getResolvedPiecesForPartInstancesOnTimeline( const currentPartStarted = partInstancesInfo.current.partStarted ?? now const nextPartStarted = + partInstancesInfo.current.partInstance.part.autoNext && partInstancesInfo.current.partInstance.part.expectedDuration !== undefined ? currentPartStarted + partInstancesInfo.current.partInstance.part.expectedDuration : null // Calculate the next part if needed let nextResolvedPieces: ResolvedPieceInstance[] = [] - if (partInstancesInfo.next && partInstancesInfo.current.partInstance.part.autoNext && nextPartStarted != null) { + if (partInstancesInfo.next && nextPartStarted != null) { const nowInPart = partInstancesInfo.next.nowInPart nextResolvedPieces = partInstancesInfo.next.pieceInstances.map((instance) => resolvePrunedPieceInstance(nowInPart, instance) diff --git a/packages/job-worker/src/playout/timeline/part.ts b/packages/job-worker/src/playout/timeline/part.ts index 745e529fdf..4a20c2ef23 100644 --- a/packages/job-worker/src/playout/timeline/part.ts +++ b/packages/job-worker/src/playout/timeline/part.ts @@ -61,7 +61,12 @@ export function transformPartIntoTimeline( } break case IBlueprintPieceType.Normal: - pieceEnable = getPieceEnableInsidePart(pieceInstance, partTimings, parentGroup.id) + pieceEnable = getPieceEnableInsidePart( + pieceInstance, + partTimings, + parentGroup.id, + parentGroup.enable.duration !== undefined || parentGroup.enable.end !== undefined + ) break default: assertNever(pieceInstance.piece.pieceType) diff --git a/packages/job-worker/src/playout/timeline/piece.ts b/packages/job-worker/src/playout/timeline/piece.ts index 9e011a3348..cea83bbcf7 100644 --- a/packages/job-worker/src/playout/timeline/piece.ts +++ b/packages/job-worker/src/playout/timeline/piece.ts @@ -90,7 +90,8 @@ export function transformPieceGroupAndObjects( export function getPieceEnableInsidePart( pieceInstance: ReadonlyDeep, partTimings: PartCalculatedTimings, - partGroupId: string + partGroupId: string, + partHasEndTime: boolean ): TSR.Timeline.TimelineEnable { const pieceEnable: TSR.Timeline.TimelineEnable = { ...pieceInstance.piece.enable } if (typeof pieceEnable.start === 'number') { @@ -103,7 +104,8 @@ export function getPieceEnableInsidePart( } } - if (partTimings.toPartPostroll) { + // If the part has an end time, we can consider post-roll + if (partHasEndTime && partTimings.toPartPostroll) { if (!pieceEnable.duration) { // make sure that the control object is shortened correctly pieceEnable.duration = `#${partGroupId} - ${partTimings.toPartPostroll}` diff --git a/packages/job-worker/src/playout/timeline/rundown.ts b/packages/job-worker/src/playout/timeline/rundown.ts index d8ee36ab7d..596e354015 100644 --- a/packages/job-worker/src/playout/timeline/rundown.ts +++ b/packages/job-worker/src/playout/timeline/rundown.ts @@ -266,7 +266,9 @@ function generateCurrentInfinitePieceObjects( const pieceEnable = getPieceEnableInsidePart( pieceInstance, currentPartInstanceTimings, - timingContext.currentPartGroup.id + timingContext.currentPartGroup.id, + timingContext.currentPartGroup.enable.end !== undefined || + timingContext.currentPartGroup.enable.duration !== undefined ) let nowInParent = currentPartInfo.nowInPart // Where is 'now' inside of the infiniteGroup?