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__/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/__tests__/abSessionHelper.spec.ts b/packages/job-worker/src/playout/abPlayback/__tests__/abSessionHelper.spec.ts index b3d494fb4a..87936ee58b 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,28 @@ 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', + } + + 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 @@ -89,16 +110,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 +130,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 +164,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 +172,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 +212,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 +257,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 +281,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 +329,72 @@ 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) + }) + + 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) }) @@ -336,14 +403,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 +423,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 +491,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 +532,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,26 +601,67 @@ 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 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) + }) }) 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/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/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..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' @@ -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]) @@ -108,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..0de0ce19a9 100644 --- a/packages/job-worker/src/playout/abPlayback/index.ts +++ b/packages/job-worker/src/playout/abPlayback/index.ts @@ -1,12 +1,16 @@ 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' 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 +52,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 +88,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)}` 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? 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 {