Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: prevent queued Part from hijacking infinite Pieces from the following Part(s) #1396

Open
wants to merge 1 commit into
base: release52
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -381,10 +381,9 @@ export class PartAndPieceInstanceActionService {
throw new Error('New part must contain at least one piece')
}

const newPart: Omit<DBPart, 'segmentId' | 'rundownId'> = {
const newPart: Omit<DBPart, 'segmentId' | 'rundownId' | '_rank'> = {
...rawPart,
_id: getRandomId(),
_rank: 99999, // Corrected in innerStartQueuedAdLib
notes: [],
invalid: false,
invalidReason: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartIns
import { getCurrentTime } from '../../../../lib'
import {
EmptyPieceTimelineObjectsBlob,
Piece,
serializePieceTimelineObjectsBlob,
} from '@sofie-automation/corelib/dist/dataModel/Piece'
import { PlayoutPartInstanceModel } from '../../../../playout/model/PlayoutPartInstanceModel'
Expand Down Expand Up @@ -176,7 +177,9 @@ describe('Test blueprint api context', () => {
return runJobWithPlayoutModel(context, { playlistId }, null, fcn as any)
}

async function setupMyDefaultRundown(): Promise<{
async function setupMyDefaultRundown(
insertExtraContents?: (jobContext: MockJobContext, rundownId: RundownId) => Promise<void>
): Promise<{
jobContext: MockJobContext
playlistId: RundownPlaylistId
rundownId: RundownId
Expand All @@ -200,6 +203,8 @@ describe('Test blueprint api context', () => {

await setupDefaultRundown(context, showStyleCompound, playlistId, rundownId)

if (insertExtraContents) await insertExtraContents(context, rundownId)

const allPartInstances = await generateSparsePieceInstances(context, activationId, rundownId)
expect(allPartInstances).toHaveLength(5)

Expand Down Expand Up @@ -1187,16 +1192,84 @@ describe('Test blueprint api context', () => {
expect(newPartInstance.partInstance.part._rank).toEqual(0.5)
expect(newPartInstance.partInstance.orphaned).toEqual('adlib-part')

const newNextPartInstances = await service.getPieceInstances('next')
expect(newNextPartInstances).toHaveLength(1)
expect(newNextPartInstances[0].partInstanceId).toEqual(
const newNextPieceInstances = await service.getPieceInstances('next')
expect(newNextPieceInstances).toHaveLength(1)
expect(newNextPieceInstances[0].partInstanceId).toEqual(
unprotectString(newPartInstance.partInstance._id)
)

expect(service.nextPartState).toEqual(ActionPartChange.SAFE_CHANGE)
expect(service.currentPartState).toEqual(ActionPartChange.NONE)
})
})

test('queued part does not hijack infinites from following parts', async () => {
// makes sure that infinites which would normally start in the part AFTER the part that is being queued,
// are not starting in the queued part itself

const { jobContext, playlistId, rundownId } = await setupMyDefaultRundown(
async (context, rundownId) => {
const secondPart = await context.mockCollections.Parts.findOne({ externalId: 'MOCK_PART_0_1' })
if (!secondPart) throw Error('could not find mock part')
const piece001: Piece = {
_id: protectString(rundownId + '_piece012'),
externalId: 'MOCK_PIECE_012',
startRundownId: rundownId,
startSegmentId: secondPart.segmentId,
startPartId: secondPart._id,
name: 'Piece 012',
enable: {
start: 0,
},
sourceLayerId: '',
outputLayerId: '',
pieceType: IBlueprintPieceType.Normal,
lifespan: PieceLifespan.OutOnSegmentEnd,
invalid: false,
content: {},
timelineObjectsString: EmptyPieceTimelineObjectsBlob,
}
await context.mockCollections.Pieces.insertOne(piece001)
}
)

const partInstance = (await jobContext.mockCollections.PartInstances.findOne({
rundownId,
})) as DBPartInstance
expect(partInstance).toBeTruthy()
await setPartInstances(jobContext, playlistId, partInstance, undefined)

await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => {
const { service } = await getTestee(jobContext, playoutModel)

const newPiece: IBlueprintPiece = {
name: 'test piece',
sourceLayerId: 'sl1',
outputLayerId: 'o1',
externalId: '-',
enable: { start: 0 },
lifespan: PieceLifespan.OutOnRundownEnd,
content: {
timelineObjects: [],
},
}
const newPart: IBlueprintPart = {
externalId: 'nope',
title: 'something',
}

// Create it with most of the real flow
postProcessPiecesMock.mockImplementationOnce(postProcessPiecesOrig)
insertQueuedPartWithPiecesMock.mockImplementationOnce(insertQueuedPartWithPiecesOrig)
expect((await service.queuePart(newPart, [newPiece]))._id).toEqual(
playoutModel.playlist.nextPartInfo?.partInstanceId
)

const newNextPartInstances = await service.getPieceInstances('next')
expect(newNextPartInstances).toHaveLength(1)
expect(newNextPartInstances[0].piece.name).toBe('test piece')
})
})
})

describe('insertPiece', () => {
Expand Down
1 change: 0 additions & 1 deletion packages/job-worker/src/ingest/__tests__/ingest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1733,7 +1733,6 @@ describe('Test ingest actions for rundowns and segments', () => {
currentPartInstance,
{
_id: protectString(`after_${currentPartInstance.partInstance._id}_part`),
_rank: 0,
externalId: `after_${currentPartInstance.partInstance._id}_externalId`,
title: 'New part',
expectedDurationWithTransition: undefined,
Expand Down
37 changes: 12 additions & 25 deletions packages/job-worker/src/playout/adlibUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@
const span = context.startSpan('innerStartOrQueueAdLibPiece')
let queuedPartInstanceId: PartInstanceId | undefined
if (queue || adLibPiece.toBeQueued) {
const adlibbedPart: Omit<DBPart, 'segmentId' | 'rundownId'> = {
const adlibbedPart: Omit<DBPart, 'segmentId' | 'rundownId' | '_rank'> = {

Check warning on line 43 in packages/job-worker/src/playout/adlibUtils.ts

View check run for this annotation

Codecov / codecov/patch

packages/job-worker/src/playout/adlibUtils.ts#L43

Added line #L43 was not covered by tests
_id: getRandomId(),
_rank: 99999, // Corrected in innerStartQueuedAdLib
externalId: '',
title: adLibPiece.name,
expectedDuration: adLibPiece.expectedDuration,
Expand Down Expand Up @@ -187,41 +186,29 @@
return fullPiece
}

function updateRankForAdlibbedPartInstance(
_context: JobContext,
playoutModel: PlayoutModel,
newPartInstance: PlayoutPartInstanceModel
) {
const currentPartInstance = playoutModel.currentPartInstance
if (!currentPartInstance) throw new Error('CurrentPartInstance not found')

// Parts are always integers spaced by one, and orphaned PartInstances will be decimals spaced between two Part
// so we can predict a 'safe' rank to get the desired position with some simple maths
newPartInstance.setRank(
getRank(
currentPartInstance.partInstance.part._rank,
Math.floor(currentPartInstance.partInstance.part._rank + 1)
)
)

updatePartInstanceRanksAfterAdlib(playoutModel, currentPartInstance, newPartInstance)
}

export async function insertQueuedPartWithPieces(
context: JobContext,
playoutModel: PlayoutModel,
rundown: PlayoutRundownModel,
currentPartInstance: PlayoutPartInstanceModel,
newPart: Omit<DBPart, 'segmentId' | 'rundownId'>,
newPart: Omit<DBPart, 'segmentId' | 'rundownId' | '_rank'>,
initialPieces: Omit<PieceInstancePiece, 'startPartId'>[],
fromAdlibId: PieceId | undefined
): Promise<PlayoutPartInstanceModel> {
const span = context.startSpan('insertQueuedPartWithPieces')

// Parts are always integers spaced by one, and orphaned PartInstances will be decimals spaced between two Part
// so we can predict a 'safe' rank to get the desired position with some simple maths
const newRank = getRank(
currentPartInstance.partInstance.part._rank,
Math.floor(currentPartInstance.partInstance.part._rank + 1)
)

const newPartFull: DBPart = {
...newPart,
segmentId: currentPartInstance.partInstance.segmentId,
rundownId: currentPartInstance.partInstance.rundownId,
_rank: newRank,
}

// Find any rundown defined infinites that we should inherit
Expand All @@ -237,13 +224,13 @@
)

const newPartInstance = playoutModel.createAdlibbedPartInstance(
newPart,
newPartFull,
initialPieces,
fromAdlibId,
infinitePieceInstances
)

updateRankForAdlibbedPartInstance(context, playoutModel, newPartInstance)
updatePartInstanceRanksAfterAdlib(playoutModel, currentPartInstance, newPartInstance)

await setNextPart(context, playoutModel, newPartInstance, false)

Expand Down
Loading