Skip to content

Commit 007d4bc

Browse files
committed
Merge remote-tracking branch 'origin/feat/SOFIE-464-multiple-lookahead-objects-from-piece' into bbc-main
2 parents ab40675 + 8e84fb3 commit 007d4bc

5 files changed

Lines changed: 484 additions & 39 deletions

File tree

packages/job-worker/src/playout/lookahead/__tests__/findObjects.test.ts

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,4 +1069,348 @@ describe('findLookaheadObjectsForPart', () => {
10691069
)
10701070
expect(rehearsalInRehearsal).toHaveLength(1)
10711071
})
1072+
1073+
describe('single piece with multiple objects on the same layer', () => {
1074+
const rundownId: RundownId = protectString('rundown0')
1075+
const layer0 = 'layer0'
1076+
const partInstanceId = protectString('partInstance0')
1077+
1078+
test('all objects from a single piece on the same layer are returned', () => {
1079+
// A single piece that contributes two objects to the same layer should expose both
1080+
// objects in the lookahead result
1081+
const partInfo = {
1082+
part: definePart(rundownId),
1083+
usesInTransition: false,
1084+
pieces: literal<PieceInstance[]>([
1085+
{
1086+
...defaultPieceInstanceProps,
1087+
rundownId,
1088+
piece: {
1089+
...defaultPieceInstanceProps.piece,
1090+
timelineObjectsString: serializePieceTimelineObjectsBlob([
1091+
{
1092+
id: 'obj0',
1093+
enable: { start: 0 },
1094+
layer: layer0,
1095+
content: { deviceType: TSR.DeviceType.ABSTRACT },
1096+
priority: 0,
1097+
},
1098+
{
1099+
id: 'obj1',
1100+
enable: { start: 100 },
1101+
layer: layer0,
1102+
content: { deviceType: TSR.DeviceType.ABSTRACT },
1103+
priority: 0,
1104+
},
1105+
]),
1106+
},
1107+
},
1108+
]),
1109+
}
1110+
1111+
const objects = findLookaheadObjectsForPart(
1112+
context,
1113+
null,
1114+
layer0,
1115+
undefined,
1116+
partInfo,
1117+
partInstanceId,
1118+
DEFAULT_PLAYOUT_STATE
1119+
)
1120+
1121+
expect(stripObjectProperties(objects)).toStrictEqual([
1122+
{
1123+
id: 'obj0',
1124+
layer: layer0,
1125+
pieceInstanceId: 'piece0_instance',
1126+
infinitePieceInstanceId: undefined,
1127+
partInstanceId: partInstanceId,
1128+
},
1129+
{
1130+
id: 'obj1',
1131+
layer: layer0,
1132+
pieceInstanceId: 'piece0_instance',
1133+
infinitePieceInstanceId: undefined,
1134+
partInstanceId: partInstanceId,
1135+
},
1136+
])
1137+
})
1138+
1139+
test('all objects from a start=0 Normal piece are filtered when the transition has an object on this layer', () => {
1140+
// 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
1141+
const partInfo = {
1142+
part: definePart(rundownId),
1143+
usesInTransition: true,
1144+
pieces: literal<PieceInstance[]>([
1145+
{
1146+
...defaultPieceInstanceProps,
1147+
rundownId,
1148+
piece: {
1149+
...defaultPieceInstanceProps.piece,
1150+
timelineObjectsString: serializePieceTimelineObjectsBlob([
1151+
{
1152+
id: 'obj0',
1153+
enable: { start: 0 },
1154+
layer: layer0,
1155+
content: { deviceType: TSR.DeviceType.ABSTRACT },
1156+
priority: 0,
1157+
},
1158+
{
1159+
id: 'obj1',
1160+
enable: { start: 100 },
1161+
layer: layer0,
1162+
content: { deviceType: TSR.DeviceType.ABSTRACT },
1163+
priority: 0,
1164+
},
1165+
]),
1166+
},
1167+
},
1168+
{
1169+
// Transition piece with an object on the same layer.
1170+
...defaultPieceInstanceProps,
1171+
_id: protectString('piece1_instance'),
1172+
rundownId,
1173+
piece: {
1174+
...defaultPieceInstanceProps.piece,
1175+
_id: protectString('piece1'),
1176+
pieceType: IBlueprintPieceType.InTransition,
1177+
timelineObjectsString: serializePieceTimelineObjectsBlob([
1178+
{
1179+
id: 'trans0',
1180+
enable: { start: 0 },
1181+
layer: layer0,
1182+
content: { deviceType: TSR.DeviceType.ABSTRACT },
1183+
priority: 0,
1184+
},
1185+
]),
1186+
},
1187+
},
1188+
]),
1189+
}
1190+
1191+
const previousPart: DBPart = { disableNextInTransition: false, classesForNext: undefined } as any
1192+
const objects = findLookaheadObjectsForPart(
1193+
context,
1194+
partInstanceId,
1195+
layer0,
1196+
previousPart,
1197+
partInfo,
1198+
partInstanceId,
1199+
DEFAULT_PLAYOUT_STATE
1200+
)
1201+
1202+
// obj0 and obj1 are both filtered because their parent piece is Normal + start=0 and
1203+
// `hasTransitionObj` is truthy. Only the transition object should remain.
1204+
expect(stripObjectProperties(objects)).toStrictEqual([
1205+
{
1206+
id: 'trans0',
1207+
layer: layer0,
1208+
pieceInstanceId: 'piece1_instance',
1209+
infinitePieceInstanceId: undefined,
1210+
partInstanceId: partInstanceId,
1211+
},
1212+
])
1213+
})
1214+
1215+
test('all objects from a start=0 Normal piece are included when the transition has no object on this layer', () => {
1216+
const partInfo = {
1217+
part: definePart(rundownId),
1218+
usesInTransition: true,
1219+
pieces: literal<PieceInstance[]>([
1220+
{
1221+
...defaultPieceInstanceProps,
1222+
rundownId,
1223+
piece: {
1224+
...defaultPieceInstanceProps.piece,
1225+
timelineObjectsString: serializePieceTimelineObjectsBlob([
1226+
{
1227+
id: 'obj0',
1228+
enable: { start: 0 },
1229+
layer: layer0,
1230+
content: { deviceType: TSR.DeviceType.ABSTRACT },
1231+
priority: 0,
1232+
},
1233+
{
1234+
id: 'obj1',
1235+
enable: { start: 100 },
1236+
layer: layer0,
1237+
content: { deviceType: TSR.DeviceType.ABSTRACT },
1238+
priority: 0,
1239+
},
1240+
]),
1241+
},
1242+
},
1243+
{
1244+
// Transition piece whose only object is on a *different* layer.
1245+
...defaultPieceInstanceProps,
1246+
_id: protectString('piece1_instance'),
1247+
rundownId,
1248+
piece: {
1249+
...defaultPieceInstanceProps.piece,
1250+
_id: protectString('piece1'),
1251+
pieceType: IBlueprintPieceType.InTransition,
1252+
timelineObjectsString: serializePieceTimelineObjectsBlob([
1253+
{
1254+
id: 'trans0',
1255+
enable: { start: 0 },
1256+
layer: 'other_layer',
1257+
content: { deviceType: TSR.DeviceType.ABSTRACT },
1258+
priority: 0,
1259+
},
1260+
]),
1261+
},
1262+
},
1263+
]),
1264+
}
1265+
1266+
const previousPart: DBPart = { disableNextInTransition: false, classesForNext: undefined } as any
1267+
const objects = findLookaheadObjectsForPart(
1268+
context,
1269+
partInstanceId,
1270+
layer0,
1271+
previousPart,
1272+
partInfo,
1273+
partInstanceId,
1274+
DEFAULT_PLAYOUT_STATE
1275+
)
1276+
1277+
// `hasTransitionObj` is falsy for layer0, so the Normal piece's objects must not be
1278+
// skipped. Both obj0 and obj1 should appear.
1279+
expect(stripObjectProperties(objects)).toStrictEqual([
1280+
{
1281+
id: 'obj0',
1282+
layer: layer0,
1283+
pieceInstanceId: 'piece0_instance',
1284+
infinitePieceInstanceId: undefined,
1285+
partInstanceId: partInstanceId,
1286+
},
1287+
{
1288+
id: 'obj1',
1289+
layer: layer0,
1290+
pieceInstanceId: 'piece0_instance',
1291+
infinitePieceInstanceId: undefined,
1292+
partInstanceId: partInstanceId,
1293+
},
1294+
])
1295+
})
1296+
1297+
test('multiple objects from a later-starting piece all appear alongside filtered start=0 objects', () => {
1298+
// Three pieces: one Normal at start=0 (should be filtered), one Normal at start=500
1299+
// that contributes TWO objects (both should appear), and an InTransition piece with an
1300+
// object on the layer (should appear + triggers the start=0 filter).
1301+
const partInfo = {
1302+
part: definePart(rundownId),
1303+
usesInTransition: true,
1304+
// Sort so that InTransition comes first in the iteration order, matching production behaviour.
1305+
pieces: sortPieceInstancesByStart(
1306+
literal<PieceInstance[]>([
1307+
{
1308+
...defaultPieceInstanceProps,
1309+
rundownId,
1310+
piece: {
1311+
...defaultPieceInstanceProps.piece,
1312+
enable: { start: 0 },
1313+
timelineObjectsString: serializePieceTimelineObjectsBlob([
1314+
{
1315+
id: 'obj0',
1316+
enable: { start: 0 },
1317+
layer: layer0,
1318+
content: { deviceType: TSR.DeviceType.ABSTRACT },
1319+
priority: 0,
1320+
},
1321+
]),
1322+
},
1323+
},
1324+
{
1325+
// Normal piece at start=500 — contributes TWO objects.
1326+
...defaultPieceInstanceProps,
1327+
_id: protectString('piece1_instance'),
1328+
rundownId,
1329+
piece: {
1330+
...defaultPieceInstanceProps.piece,
1331+
_id: protectString('piece1'),
1332+
enable: { start: 500 },
1333+
timelineObjectsString: serializePieceTimelineObjectsBlob([
1334+
{
1335+
id: 'obj1',
1336+
enable: { start: 0 },
1337+
layer: layer0,
1338+
content: { deviceType: TSR.DeviceType.ABSTRACT },
1339+
priority: 0,
1340+
},
1341+
{
1342+
id: 'obj2',
1343+
enable: { start: 0 },
1344+
layer: layer0,
1345+
content: { deviceType: TSR.DeviceType.ABSTRACT },
1346+
priority: 0,
1347+
},
1348+
]),
1349+
},
1350+
},
1351+
{
1352+
// Transition piece. InTransition sorts before Normal pieces at the same start.
1353+
...defaultPieceInstanceProps,
1354+
_id: protectString('piece2_instance'),
1355+
rundownId,
1356+
piece: {
1357+
...defaultPieceInstanceProps.piece,
1358+
_id: protectString('piece2'),
1359+
pieceType: IBlueprintPieceType.InTransition,
1360+
enable: { start: 0 },
1361+
timelineObjectsString: serializePieceTimelineObjectsBlob([
1362+
{
1363+
id: 'trans0',
1364+
enable: { start: 0 },
1365+
layer: layer0,
1366+
content: { deviceType: TSR.DeviceType.ABSTRACT },
1367+
priority: 0,
1368+
},
1369+
]),
1370+
},
1371+
},
1372+
]),
1373+
0
1374+
),
1375+
}
1376+
1377+
const previousPart: DBPart = { disableNextInTransition: false, classesForNext: undefined } as any
1378+
const objects = findLookaheadObjectsForPart(
1379+
context,
1380+
partInstanceId,
1381+
layer0,
1382+
previousPart,
1383+
partInfo,
1384+
partInstanceId,
1385+
DEFAULT_PLAYOUT_STATE
1386+
)
1387+
1388+
// obj0 (Normal, start=0) is filtered because `hasTransitionObj` is truthy.
1389+
// trans0 (InTransition) is always included.
1390+
// obj1 and obj2 (Normal, start=500) are not filtered since start != 0.
1391+
expect(stripObjectProperties(objects)).toStrictEqual([
1392+
{
1393+
id: 'trans0',
1394+
layer: layer0,
1395+
pieceInstanceId: 'piece2_instance',
1396+
infinitePieceInstanceId: undefined,
1397+
partInstanceId: partInstanceId,
1398+
},
1399+
{
1400+
id: 'obj1',
1401+
layer: layer0,
1402+
pieceInstanceId: 'piece1_instance',
1403+
infinitePieceInstanceId: undefined,
1404+
partInstanceId: partInstanceId,
1405+
},
1406+
{
1407+
id: 'obj2',
1408+
layer: layer0,
1409+
pieceInstanceId: 'piece1_instance',
1410+
infinitePieceInstanceId: undefined,
1411+
partInstanceId: partInstanceId,
1412+
},
1413+
])
1414+
})
1415+
})
10721416
})

packages/job-worker/src/playout/lookahead/__tests__/lookaheadOffset/constants.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,46 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString'
55
import { PlayoutModel } from '../../../model/PlayoutModel.js'
66
import { JobContext } from '../../../../jobs/index.js'
77

8+
/**
9+
* Generates a piece with a single timeline object on the given layer.
10+
* Use this instead of `makePiece` when a test only cares about search-distance
11+
* or basic lookahead presence, and does not need to verify multi-object offset
12+
* computation. Keeps each piece to exactly one object so that `lookaheadDepth`
13+
* behaves as "one object per part" (the same as the pre-multi-object behaviour).
14+
*/
15+
export function makeSimplePiece({
16+
partId,
17+
layer,
18+
start = 0,
19+
}: {
20+
partId: string
21+
layer: string
22+
start?: number
23+
}): Piece {
24+
return literal<Partial<Piece>>({
25+
_id: protectString(`piece_simple_${partId}_${layer}`),
26+
startRundownId: protectString('r1'),
27+
startPartId: protectString(partId),
28+
enable: { start },
29+
outputLayerId: layer,
30+
pieceType: IBlueprintPieceType.Normal,
31+
timelineObjectsString: protectString<PieceTimelineObjectsBlob>(
32+
JSON.stringify([
33+
{
34+
id: `piece_simple_${partId}_${layer}_obj`,
35+
layer,
36+
enable: { start: 0 },
37+
content: {
38+
deviceType: TSR.DeviceType.CASPARCG,
39+
type: TSR.TimelineContentTypeCasparCg.MEDIA,
40+
file: 'AMB',
41+
},
42+
},
43+
])
44+
),
45+
}) as Piece
46+
}
47+
848
export function makePiece({
949
partId,
1050
layer,

0 commit comments

Comments
 (0)