Skip to content
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 @@ -1069,4 +1069,348 @@ describe('findLookaheadObjectsForPart', () => {
)
expect(rehearsalInRehearsal).toHaveLength(1)
})

describe('single piece with multiple objects on the same layer', () => {
const rundownId: RundownId = protectString('rundown0')
const layer0 = 'layer0'
const partInstanceId = protectString('partInstance0')

test('all objects from a single piece on the same layer are returned', () => {
// A single piece that contributes two objects to the same layer should expose both
// objects in the lookahead result
const partInfo = {
part: definePart(rundownId),
usesInTransition: false,
pieces: literal<PieceInstance[]>([
{
...defaultPieceInstanceProps,
rundownId,
piece: {
...defaultPieceInstanceProps.piece,
timelineObjectsString: serializePieceTimelineObjectsBlob([
{
id: 'obj0',
enable: { start: 0 },
layer: layer0,
content: { deviceType: TSR.DeviceType.ABSTRACT },
priority: 0,
},
{
id: 'obj1',
enable: { start: 100 },
layer: layer0,
content: { deviceType: TSR.DeviceType.ABSTRACT },
priority: 0,
},
]),
},
},
]),
}

const objects = findLookaheadObjectsForPart(
context,
null,
layer0,
undefined,
partInfo,
partInstanceId,
DEFAULT_PLAYOUT_STATE
)

expect(stripObjectProperties(objects)).toStrictEqual([
{
id: 'obj0',
layer: layer0,
pieceInstanceId: 'piece0_instance',
infinitePieceInstanceId: undefined,
partInstanceId: partInstanceId,
},
{
id: 'obj1',
layer: layer0,
pieceInstanceId: 'piece0_instance',
infinitePieceInstanceId: undefined,
partInstanceId: partInstanceId,
},
])
})

test('all objects from a start=0 Normal piece are filtered when the transition has an object on this layer', () => {
// Note: this is sane, but maybe not 100% correct. There are times when it would be desirable to preserve the Normal piece's objects if they start in the part after the transition will have ended
const partInfo = {
part: definePart(rundownId),
usesInTransition: true,
pieces: literal<PieceInstance[]>([
{
...defaultPieceInstanceProps,
rundownId,
piece: {
...defaultPieceInstanceProps.piece,
timelineObjectsString: serializePieceTimelineObjectsBlob([
{
id: 'obj0',
enable: { start: 0 },
layer: layer0,
content: { deviceType: TSR.DeviceType.ABSTRACT },
priority: 0,
},
{
id: 'obj1',
enable: { start: 100 },
layer: layer0,
content: { deviceType: TSR.DeviceType.ABSTRACT },
priority: 0,
},
]),
},
},
{
// Transition piece with an object on the same layer.
...defaultPieceInstanceProps,
_id: protectString('piece1_instance'),
rundownId,
piece: {
...defaultPieceInstanceProps.piece,
_id: protectString('piece1'),
pieceType: IBlueprintPieceType.InTransition,
timelineObjectsString: serializePieceTimelineObjectsBlob([
{
id: 'trans0',
enable: { start: 0 },
layer: layer0,
content: { deviceType: TSR.DeviceType.ABSTRACT },
priority: 0,
},
]),
},
},
]),
}

const previousPart: DBPart = { disableNextInTransition: false, classesForNext: undefined } as any
const objects = findLookaheadObjectsForPart(
context,
partInstanceId,
layer0,
previousPart,
partInfo,
partInstanceId,
DEFAULT_PLAYOUT_STATE
)

// obj0 and obj1 are both filtered because their parent piece is Normal + start=0 and
// `hasTransitionObj` is truthy. Only the transition object should remain.
expect(stripObjectProperties(objects)).toStrictEqual([
{
id: 'trans0',
layer: layer0,
pieceInstanceId: 'piece1_instance',
infinitePieceInstanceId: undefined,
partInstanceId: partInstanceId,
},
])
})

test('all objects from a start=0 Normal piece are included when the transition has no object on this layer', () => {
const partInfo = {
part: definePart(rundownId),
usesInTransition: true,
pieces: literal<PieceInstance[]>([
{
...defaultPieceInstanceProps,
rundownId,
piece: {
...defaultPieceInstanceProps.piece,
timelineObjectsString: serializePieceTimelineObjectsBlob([
{
id: 'obj0',
enable: { start: 0 },
layer: layer0,
content: { deviceType: TSR.DeviceType.ABSTRACT },
priority: 0,
},
{
id: 'obj1',
enable: { start: 100 },
layer: layer0,
content: { deviceType: TSR.DeviceType.ABSTRACT },
priority: 0,
},
]),
},
},
{
// Transition piece whose only object is on a *different* layer.
...defaultPieceInstanceProps,
_id: protectString('piece1_instance'),
rundownId,
piece: {
...defaultPieceInstanceProps.piece,
_id: protectString('piece1'),
pieceType: IBlueprintPieceType.InTransition,
timelineObjectsString: serializePieceTimelineObjectsBlob([
{
id: 'trans0',
enable: { start: 0 },
layer: 'other_layer',
content: { deviceType: TSR.DeviceType.ABSTRACT },
priority: 0,
},
]),
},
},
]),
}

const previousPart: DBPart = { disableNextInTransition: false, classesForNext: undefined } as any
const objects = findLookaheadObjectsForPart(
context,
partInstanceId,
layer0,
previousPart,
partInfo,
partInstanceId,
DEFAULT_PLAYOUT_STATE
)

// `hasTransitionObj` is falsy for layer0, so the Normal piece's objects must not be
// skipped. Both obj0 and obj1 should appear.
expect(stripObjectProperties(objects)).toStrictEqual([
{
id: 'obj0',
layer: layer0,
pieceInstanceId: 'piece0_instance',
infinitePieceInstanceId: undefined,
partInstanceId: partInstanceId,
},
{
id: 'obj1',
layer: layer0,
pieceInstanceId: 'piece0_instance',
infinitePieceInstanceId: undefined,
partInstanceId: partInstanceId,
},
])
})

test('multiple objects from a later-starting piece all appear alongside filtered start=0 objects', () => {
// Three pieces: one Normal at start=0 (should be filtered), one Normal at start=500
// that contributes TWO objects (both should appear), and an InTransition piece with an
// object on the layer (should appear + triggers the start=0 filter).
const partInfo = {
part: definePart(rundownId),
usesInTransition: true,
// Sort so that InTransition comes first in the iteration order, matching production behaviour.
pieces: sortPieceInstancesByStart(
literal<PieceInstance[]>([
{
...defaultPieceInstanceProps,
rundownId,
piece: {
...defaultPieceInstanceProps.piece,
enable: { start: 0 },
timelineObjectsString: serializePieceTimelineObjectsBlob([
{
id: 'obj0',
enable: { start: 0 },
layer: layer0,
content: { deviceType: TSR.DeviceType.ABSTRACT },
priority: 0,
},
]),
},
},
{
// Normal piece at start=500 — contributes TWO objects.
...defaultPieceInstanceProps,
_id: protectString('piece1_instance'),
rundownId,
piece: {
...defaultPieceInstanceProps.piece,
_id: protectString('piece1'),
enable: { start: 500 },
timelineObjectsString: serializePieceTimelineObjectsBlob([
{
id: 'obj1',
enable: { start: 0 },
layer: layer0,
content: { deviceType: TSR.DeviceType.ABSTRACT },
priority: 0,
},
{
id: 'obj2',
enable: { start: 0 },
layer: layer0,
content: { deviceType: TSR.DeviceType.ABSTRACT },
priority: 0,
},
]),
},
},
{
// Transition piece. InTransition sorts before Normal pieces at the same start.
...defaultPieceInstanceProps,
_id: protectString('piece2_instance'),
rundownId,
piece: {
...defaultPieceInstanceProps.piece,
_id: protectString('piece2'),
pieceType: IBlueprintPieceType.InTransition,
enable: { start: 0 },
timelineObjectsString: serializePieceTimelineObjectsBlob([
{
id: 'trans0',
enable: { start: 0 },
layer: layer0,
content: { deviceType: TSR.DeviceType.ABSTRACT },
priority: 0,
},
]),
},
},
]),
0
),
}

const previousPart: DBPart = { disableNextInTransition: false, classesForNext: undefined } as any
const objects = findLookaheadObjectsForPart(
context,
partInstanceId,
layer0,
previousPart,
partInfo,
partInstanceId,
DEFAULT_PLAYOUT_STATE
)

// obj0 (Normal, start=0) is filtered because `hasTransitionObj` is truthy.
// trans0 (InTransition) is always included.
// obj1 and obj2 (Normal, start=500) are not filtered since start != 0.
expect(stripObjectProperties(objects)).toStrictEqual([
{
id: 'trans0',
layer: layer0,
pieceInstanceId: 'piece2_instance',
infinitePieceInstanceId: undefined,
partInstanceId: partInstanceId,
},
{
id: 'obj1',
layer: layer0,
pieceInstanceId: 'piece1_instance',
infinitePieceInstanceId: undefined,
partInstanceId: partInstanceId,
},
{
id: 'obj2',
layer: layer0,
pieceInstanceId: 'piece1_instance',
infinitePieceInstanceId: undefined,
partInstanceId: partInstanceId,
},
])
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,46 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString'
import { PlayoutModel } from '../../../model/PlayoutModel.js'
import { JobContext } from '../../../../jobs/index.js'

/**
* Generates a piece with a single timeline object on the given layer.
* Use this instead of `makePiece` when a test only cares about search-distance
* or basic lookahead presence, and does not need to verify multi-object offset
* computation. Keeps each piece to exactly one object so that `lookaheadDepth`
* behaves as "one object per part" (the same as the pre-multi-object behaviour).
*/
export function makeSimplePiece({
partId,
layer,
start = 0,
}: {
partId: string
layer: string
start?: number
}): Piece {
return literal<Partial<Piece>>({
_id: protectString(`piece_simple_${partId}_${layer}`),
startRundownId: protectString('r1'),
startPartId: protectString(partId),
enable: { start },
outputLayerId: layer,
pieceType: IBlueprintPieceType.Normal,
timelineObjectsString: protectString<PieceTimelineObjectsBlob>(
JSON.stringify([
{
id: `piece_simple_${partId}_${layer}_obj`,
layer,
enable: { start: 0 },
content: {
deviceType: TSR.DeviceType.CASPARCG,
type: TSR.TimelineContentTypeCasparCg.MEDIA,
file: 'AMB',
},
},
])
),
}) as Piece
}

export function makePiece({
partId,
layer,
Expand Down
Loading
Loading