diff --git a/packages/blueprints-integration/src/documents/piece.ts b/packages/blueprints-integration/src/documents/piece.ts index d537c2dda7..42a77db175 100644 --- a/packages/blueprints-integration/src/documents/piece.ts +++ b/packages/blueprints-integration/src/documents/piece.ts @@ -35,6 +35,11 @@ export interface IBlueprintPiece * User editing definitions for this piece */ userEditOperations?: UserEditingDefinition[] + + /** + * Whether to stop this piece before the 'keepalive' period of the part + */ + excludeDuringPartKeepalive?: boolean } export interface IBlueprintPieceDB extends IBlueprintPiece { diff --git a/packages/corelib/src/playout/__tests__/timings.test.ts b/packages/corelib/src/playout/__tests__/timings.test.ts index 443fab8af6..29d3c0670b 100644 --- a/packages/corelib/src/playout/__tests__/timings.test.ts +++ b/packages/corelib/src/playout/__tests__/timings.test.ts @@ -80,6 +80,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 0, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -102,6 +103,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 0, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -124,6 +126,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -146,6 +149,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 289, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -168,6 +172,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 289, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -191,6 +196,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 452, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -214,6 +220,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 452, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -238,6 +245,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 452, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -262,6 +270,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 452, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -286,6 +295,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 2256, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -310,6 +320,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 2256, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -331,6 +342,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 500, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -345,6 +357,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 500, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -365,6 +378,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 500, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -387,6 +401,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 500, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -409,6 +424,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 500, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -431,6 +447,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 823, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -453,6 +470,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 823, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -476,6 +494,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 500 + 452, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -499,6 +518,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 500 + 452, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -523,6 +543,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 500 + 452, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -547,6 +568,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 500 + 452, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -571,6 +593,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 2256, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -595,6 +618,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 2256, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -624,6 +648,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 500, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -653,6 +678,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 500 + 452, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -682,6 +708,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 500 + 452, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 452, }) ) }) @@ -710,6 +737,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 500, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -738,6 +766,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 500, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -764,6 +793,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 5000, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 5000, }) ) }) @@ -790,6 +820,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 500, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -816,6 +847,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 500, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -843,6 +875,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 5000, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 5000, }) ) }) @@ -869,6 +902,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 500, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 0, }) ) }) @@ -895,6 +929,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 500, fromPartPostroll: 231 + 0, toPartPostroll: 0 + 0, + fromPartKeepalive: 0, }) ) }) @@ -924,6 +959,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 500 - 345 + 628, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 628, }) ) }) @@ -950,6 +986,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 500 - 345 + 628, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 628, }) ) }) @@ -976,6 +1013,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 628, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 628, }) ) }) @@ -1002,6 +1040,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 628, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 628, }) ) }) @@ -1030,6 +1069,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 500 - 345 + 628, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 628, }) ) }) @@ -1058,6 +1098,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 500 - 345 + 628, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 628, }) ) }) @@ -1086,6 +1127,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 987, fromPartPostroll: 0, toPartPostroll: 0, + fromPartKeepalive: 628, }) ) }) @@ -1114,6 +1156,7 @@ describe('Part Playout Timings', () => { fromPartRemaining: 231 + 987, fromPartPostroll: 231, toPartPostroll: 0, + fromPartKeepalive: 628, }) ) }) diff --git a/packages/corelib/src/playout/timings.ts b/packages/corelib/src/playout/timings.ts index 77209d90e7..b2bd4b63ad 100644 --- a/packages/corelib/src/playout/timings.ts +++ b/packages/corelib/src/playout/timings.ts @@ -58,6 +58,7 @@ export interface PartCalculatedTimings { toPartPostroll: number fromPartRemaining: number // How long after the start of toPartGroup should fromPartGroup continue? fromPartPostroll: number + fromPartKeepalive: number } export type CalculateTimingsPiece = Pick @@ -117,6 +118,7 @@ export function calculatePartTimings( // The old part needs to continue for a while fromPartRemaining: takeOffset + fromPartPostroll, fromPartPostroll: fromPartPostroll, + fromPartKeepalive: 0, } } else { // The amount of time needed to complete the outTransition before the 'take' point @@ -136,6 +138,7 @@ export function calculatePartTimings( toPartPostroll: toPartPostroll, fromPartRemaining: takeOffset + inTransition.previousPartKeepaliveDuration + fromPartPostroll, fromPartPostroll: fromPartPostroll, + fromPartKeepalive: inTransition.previousPartKeepaliveDuration, } } } diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index f0fb167a19..5c7d6a012d 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -97,6 +97,7 @@ export const IBlueprintPieceObjectsSampleKeys = allKeysOfObject notInVision: true, abSessions: true, userEditOperations: true, + excludeDuringPartKeepalive: true, }) // Compile a list of the keys which are allowed to be set @@ -239,6 +240,7 @@ export function convertPieceToBlueprints(piece: ReadonlyDeep extendOnHold: piece.extendOnHold, notInVision: piece.notInVision, userEditOperations: translateUserEditsToBlueprint(piece.userEditOperations), + excludeDuringPartKeepalive: piece.excludeDuringPartKeepalive, } return obj diff --git a/packages/job-worker/src/playout/timeline/__tests__/__snapshots__/rundown.test.ts.snap b/packages/job-worker/src/playout/timeline/__tests__/__snapshots__/rundown.test.ts.snap new file mode 100644 index 0000000000..60c9724e57 --- /dev/null +++ b/packages/job-worker/src/playout/timeline/__tests__/__snapshots__/rundown.test.ts.snap @@ -0,0 +1,2350 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`buildTimelineObjsForRundown current and previous parts 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + "last_part", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "previous_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece9", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece9.end + 0", + "start": "#piece_group_control_piece9.start - 0", + }, + "id": "piece_group_piece9", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "end": "#part_group_part0.start + 0", + "start": 1235, + }, + "id": "part_group_part9", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part9", + "priority": -1, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": undefined, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": undefined, + "previousPartOverlap": 0, + }, +} +`; + +exports[`buildTimelineObjsForRundown current part with startedPlayback 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + "last_part", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": 5678, + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": undefined, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": 5678, + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": undefined, + }, +} +`; + +exports[`buildTimelineObjsForRundown infinite pieces infinite continuing from previous 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + "last_part", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "previous_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece9", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece9.end + 0", + "start": "#piece_group_control_piece9.start - 0", + }, + "id": "piece_group_piece9", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "end": "#part_group_part0.start + 0", + "start": 1235, + }, + "id": "part_group_part9", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part9", + "priority": -1, + }, + { + "children": [ + { + "classes": [ + "current_part", + "continues_infinite", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece6b", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece6b.end + 0", + "start": "#piece_group_control_piece6b.start - 0", + }, + "id": "piece_group_piece6b", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": 123, + }, + "id": "part_group_piece6b_infinite", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 1, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": undefined, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": undefined, + "previousPartOverlap": 0, + }, +} +`; + +exports[`buildTimelineObjsForRundown infinite pieces infinite continuing into next with autonext 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece6", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece6.end + 0", + "start": "#piece_group_control_piece6.start - 0", + }, + "id": "piece_group_piece6", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": 123, + }, + "id": "part_group_piece6_infinite", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 1, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "duration": 5000, + "start": 1235, + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part1", + "layer": "group_first_object", + "partInstanceId": "part1", + "priority": 0, + }, + { + "classes": [ + "next_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece1", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece1.end + 0", + "start": "#piece_group_control_piece1.start - 0", + }, + "id": "piece_group_piece1", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "#part_group_part0.end - 0", + }, + "id": "part_group_part1", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part1", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": 5000, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "duration": 5000, + "start": 1235, + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "#part_group_part0.end - 0", + }, + "id": "part_group_part1", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part1", + "priority": 5, + }, + "nextPartOverlap": 0, + }, +} +`; + +exports[`buildTimelineObjsForRundown infinite pieces infinite ending with previous 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + "last_part", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "previous_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece9", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece9.end + 0", + "start": "#piece_group_control_piece9.start - 0", + }, + "id": "piece_group_piece9", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + { + "classes": [ + "previous_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece6", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece6.end + 0", + "start": "#piece_group_control_piece6.start - 0", + }, + "id": "piece_group_piece6", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "end": "#part_group_part0.start + 0", + "start": 1235, + }, + "id": "part_group_part9", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part9", + "priority": -1, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": undefined, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": undefined, + "previousPartOverlap": 0, + }, +} +`; + +exports[`buildTimelineObjsForRundown infinite pieces infinite ending with previous excludeDuringPartKeepalive=true 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + "last_part", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "previous_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece9", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece9.end + 0", + "start": "#piece_group_control_piece9.start - 0", + }, + "id": "piece_group_piece9", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "previous_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece6", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece6.end + 0", + "start": "#piece_group_control_piece6.start - 0", + }, + "id": "piece_group_piece6", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "end": "#part_group_part9.end - 0", + "start": 0, + }, + "id": "part_group_part9_no_keepalive", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part9", + "priority": 5, + }, + ], + "enable": { + "end": "#part_group_part0.start + 0", + "start": 1235, + }, + "id": "part_group_part9", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part9", + "priority": -1, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": undefined, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": undefined, + "previousPartOverlap": 0, + }, +} +`; + +exports[`buildTimelineObjsForRundown infinite pieces infinite starting in current 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + "last_part", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "previous_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece9", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece9.end + 0", + "start": "#piece_group_control_piece9.start - 0", + }, + "id": "piece_group_piece9", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "end": "#part_group_part0.start + 0", + "start": 1235, + }, + "id": "part_group_part9", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part9", + "priority": -1, + }, + { + "children": [ + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece1", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece1.end + 0", + "start": "#piece_group_control_piece1.start - 0", + }, + "id": "piece_group_piece1", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "#part_group_part0.start", + }, + "id": "part_group_piece1_infinite", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 1, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": undefined, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": undefined, + "previousPartOverlap": 0, + }, +} +`; + +exports[`buildTimelineObjsForRundown infinite pieces infinite stopping in current with autonext 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece6", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece6.end + 0", + "start": "#piece_group_control_piece6.start - 0", + }, + "id": "piece_group_piece6", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "end": "#part_group_part0.end + 0", + "start": 123, + }, + "id": "part_group_piece6_infinite", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 1, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "duration": 5000, + "start": 1235, + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part1", + "layer": "group_first_object", + "partInstanceId": "part1", + "priority": 0, + }, + { + "classes": [ + "next_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece1", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece1.end + 0", + "start": "#piece_group_control_piece1.start - 0", + }, + "id": "piece_group_piece1", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "#part_group_part0.end - 0", + }, + "id": "part_group_part1", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part1", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": 5000, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "duration": 5000, + "start": 1235, + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "#part_group_part0.end - 0", + }, + "id": "part_group_part1", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part1", + "priority": 5, + }, + "nextPartOverlap": 0, + }, +} +`; + +exports[`buildTimelineObjsForRundown infinite pieces infinite stopping in current with autonext excludeDuringPartKeepalive=true 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece6", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece6.end + 0", + "start": "#piece_group_control_piece6.start - 0", + }, + "id": "piece_group_piece6", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "end": "#part_group_part0.end + -100", + "start": 123, + }, + "id": "part_group_piece6_infinite", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 1, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "duration": 5000, + "start": 1235, + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part1", + "layer": "group_first_object", + "partInstanceId": "part1", + "priority": 0, + }, + { + "classes": [ + "next_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece1", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece1.end + 0", + "start": "#piece_group_control_piece1.start - 0", + }, + "id": "piece_group_piece1", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "#part_group_part0.end - 0", + }, + "id": "part_group_part1", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part1", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": 5000, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "duration": 5000, + "start": 1235, + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "#part_group_part0.end - 0", + }, + "id": "part_group_part1", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part1", + "priority": 5, + }, + "nextPartOverlap": 0, + }, +} +`; + +exports[`buildTimelineObjsForRundown next part no autonext 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": undefined, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": undefined, + }, +} +`; + +exports[`buildTimelineObjsForRundown next part with autonext 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "duration": 5000, + "start": "now", + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part1", + "layer": "group_first_object", + "partInstanceId": "part1", + "priority": 0, + }, + { + "classes": [ + "next_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece1", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece1.end + 0", + "start": "#piece_group_control_piece1.start - 0", + }, + "id": "piece_group_piece1", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "#part_group_part0.end - 0", + }, + "id": "part_group_part1", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part1", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": 5000, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "duration": 5000, + "start": "now", + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "#part_group_part0.end - 0", + }, + "id": "part_group_part1", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part1", + "priority": 5, + }, + "nextPartOverlap": 0, + }, +} +`; + +exports[`buildTimelineObjsForRundown overlap and keepalive autonext into next part 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "duration": 5000, + "start": "now", + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part1", + "layer": "group_first_object", + "partInstanceId": "part1", + "priority": 0, + }, + { + "classes": [ + "next_part", + ], + "enable": { + "start": 500, + }, + "id": "piece_group_control_piece1", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece1.end + 0", + "start": "#piece_group_control_piece1.start - 0", + }, + "id": "piece_group_piece1", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "#part_group_part0.end - 900", + }, + "id": "part_group_part1", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part1", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": 5000, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "duration": 5000, + "start": "now", + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "#part_group_part0.end - 900", + }, + "id": "part_group_part1", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part1", + "priority": 5, + }, + "nextPartOverlap": 900, + }, +} +`; + +exports[`buildTimelineObjsForRundown overlap and keepalive autonext into next part with excludeDuringPartKeepalive 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece9", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece9.end + 0", + "start": "#piece_group_control_piece9.start - 0", + }, + "id": "piece_group_piece9", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "end": "#part_group_part0.end - 100", + "start": 0, + }, + "id": "part_group_part0_no_keepalive", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + ], + "enable": { + "duration": 5000, + "start": 1235, + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part1", + "layer": "group_first_object", + "partInstanceId": "part1", + "priority": 0, + }, + { + "classes": [ + "next_part", + ], + "enable": { + "start": 500, + }, + "id": "piece_group_control_piece1", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece1.end + 0", + "start": "#piece_group_control_piece1.start - 0", + }, + "id": "piece_group_piece1", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "#part_group_part0.end - 900", + }, + "id": "part_group_part1", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part1", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": 5000, + "currentPartGroup": { + "children": 4, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "duration": 5000, + "start": 1235, + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "#part_group_part0.end - 900", + }, + "id": "part_group_part1", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part1", + "priority": 5, + }, + "nextPartOverlap": 900, + }, +} +`; + +exports[`buildTimelineObjsForRundown overlap and keepalive current and previous parts 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + "last_part", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "previous_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece9", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece9.end + 0", + "start": "#piece_group_control_piece9.start - 0", + }, + "id": "piece_group_piece9", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + { + "classes": [ + "previous_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece8", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece8.end + 0", + "start": "#piece_group_control_piece8.start - 0", + }, + "id": "piece_group_piece8", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "end": "#part_group_part0.start + 900", + "start": 1235, + }, + "id": "part_group_part9", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part9", + "priority": -1, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 500, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": undefined, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": undefined, + "previousPartOverlap": 900, + }, +} +`; + +exports[`buildTimelineObjsForRundown overlap and keepalive current and previous parts with excludeDuringPartKeepalive 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + "last_part", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "previous_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece9", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece9.end + 0", + "start": "#piece_group_control_piece9.start - 0", + }, + "id": "piece_group_piece9", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + { + "children": [ + { + "classes": [ + "previous_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece8", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece8.end + 0", + "start": "#piece_group_control_piece8.start - 0", + }, + "id": "piece_group_piece8", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "end": "#part_group_part9.end - 100", + "start": 0, + }, + "id": "part_group_part9_no_keepalive", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part9", + "priority": 5, + }, + ], + "enable": { + "end": "#part_group_part0.start + 900", + "start": 1235, + }, + "id": "part_group_part9", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part9", + "priority": -1, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 500, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": undefined, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": undefined, + "previousPartOverlap": 900, + }, +} +`; + +exports[`buildTimelineObjsForRundown simple current part 1`] = ` +{ + "timeline": [ + { + "classes": [ + "rundown_active", + "last_part", + ], + "enable": { + "while": 1, + }, + "id": "mockPlaylist_status", + "layer": "rundown_status", + "partInstanceId": null, + "priority": 0, + }, + { + "children": [ + { + "enable": { + "start": 0, + }, + "id": "part_group_firstobject_part0", + "layer": "group_first_object", + "partInstanceId": "part0", + "priority": 0, + }, + { + "classes": [ + "current_part", + ], + "enable": { + "start": 0, + }, + "id": "piece_group_control_piece0", + "isPieceTimeline": true, + "priority": 5, + }, + { + "children": [], + "enable": { + "end": "#piece_group_control_piece0.end + 0", + "start": "#piece_group_control_piece0.start - 0", + }, + "id": "piece_group_piece0", + "isPieceTimeline": true, + "layer": "", + "priority": 0, + }, + ], + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isPieceTimeline": true, + "layer": "", + "partInstanceId": "part0", + "priority": 5, + }, + ], + "timingContext": { + "currentPartDuration": undefined, + "currentPartGroup": { + "children": 3, + "content": { + "deviceType": "ABSTRACT", + "objects": [], + "type": "group", + }, + "enable": { + "start": "now", + }, + "id": "part_group_part0", + "isGroup": true, + "layer": "", + "metaData": { + "isPieceTimeline": true, + }, + "objectType": "rundown", + "partInstanceId": "part0", + "priority": 5, + }, + "nextPartGroup": undefined, + }, +} +`; diff --git a/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts b/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts new file mode 100644 index 0000000000..003ee434b7 --- /dev/null +++ b/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts @@ -0,0 +1,853 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { DBRundownPlaylist, SelectedPartInstance } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { setupDefaultJobEnvironment } from '../../../__mocks__/context' +import { buildTimelineObjsForRundown, RundownTimelineResult, RundownTimelineTimingContext } from '../rundown' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { SelectedPartInstancesTimelineInfo, SelectedPartInstanceTimelineInfo } from '../generate' +import { PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { transformTimeline } from '@sofie-automation/corelib/dist/playout/timeline' +import { deleteAllUndefinedProperties, getRandomId } from '@sofie-automation/corelib/dist/lib' +import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' +import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { IBlueprintPieceType, PieceLifespan } from '@sofie-automation/blueprints-integration' +import { getPartGroupId } from '@sofie-automation/corelib/dist/playout/ids' + +const DEFAULT_PART_TIMINGS: PartCalculatedTimings = Object.freeze({ + inTransitionStart: null, + toPartDelay: 0, + toPartPostroll: 0, + fromPartRemaining: 0, + fromPartPostroll: 0, + fromPartKeepalive: 0, +}) + +function transformTimelineIntoSimplifiedForm(res: RundownTimelineResult) { + const deepTimeline = transformTimeline(res.timeline) + + function simplifyTimelineObject(obj: any): any { + const newObj = { + id: obj.id, + enable: obj.enable, + layer: obj.layer, + partInstanceId: obj.partInstanceId, + priority: obj.priority, + children: obj.children?.map(simplifyTimelineObject), + isPieceTimeline: obj.metaData?.isPieceTimeline, + classes: obj.classes?.length > 0 ? obj.classes : undefined, + } + + deleteAllUndefinedProperties(newObj) + + return newObj + } + + return { + timeline: deepTimeline.map(simplifyTimelineObject), + timingContext: res.timingContext + ? ({ + ...res.timingContext, + currentPartGroup: { + ...res.timingContext.currentPartGroup, + children: res.timingContext.currentPartGroup.children.length as any, + }, + nextPartGroup: res.timingContext.nextPartGroup + ? { + ...res.timingContext.nextPartGroup, + children: res.timingContext.nextPartGroup.children.length as any, + } + : undefined, + } satisfies RundownTimelineTimingContext) + : undefined, + } +} + +/** + * This is a set of tests to get a general overview of the shape of the generated timeline. + * It is not intended to look in much detail at everything, it is expected that methods used + * inside of this will have their own tests to stress difference scenarios. + */ +describe('buildTimelineObjsForRundown', () => { + function createMockPlaylist(selectedPartInfos: SelectedPartInstancesTimelineInfo): DBRundownPlaylist { + function convertSelectedPartInstance( + info: SelectedPartInstanceTimelineInfo | undefined + ): SelectedPartInstance | null { + if (!info) return null + return { + partInstanceId: info.partInstance._id, + rundownId: info.partInstance.rundownId, + manuallySelected: false, + consumesQueuedSegmentId: false, + } + } + return { + _id: protectString('mockPlaylist'), + nextPartInfo: convertSelectedPartInstance(selectedPartInfos.next), + currentPartInfo: convertSelectedPartInstance(selectedPartInfos.current), + previousPartInfo: convertSelectedPartInstance(selectedPartInfos.previous), + activationId: protectString('mockActivationId'), + rehearsal: false, + } as Partial as any + } + function createMockPartInstance( + id: string, + partProps?: Partial, + partInstanceProps?: Partial + ): DBPartInstance { + return { + _id: protectString(id), + part: { + ...partProps, + } as Partial as any, + ...partInstanceProps, + } as Partial as any + } + function createMockPieceInstance( + id: string, + pieceProps?: Partial, + pieceInstanceProps?: Partial + ): PieceInstanceWithTimings { + return { + _id: protectString(id), + + piece: { + enable: { start: 0 }, + pieceType: IBlueprintPieceType.Normal, + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + ...pieceProps, + } as Partial as any, + + resolvedEndCap: undefined, + priority: 5, + + ...pieceInstanceProps, + } as Partial as any + } + function createMockInfinitePieceInstance( + id: string, + pieceProps?: Partial, + pieceInstanceProps?: Partial, + infiniteIndex = 0 + ): PieceInstanceWithTimings { + return createMockPieceInstance( + id, + { + lifespan: PieceLifespan.OutOnSegmentEnd, + ...pieceProps, + }, + { + plannedStartedPlayback: 123, + ...pieceInstanceProps, + infinite: { + infinitePieceId: getRandomId(), + infiniteInstanceId: getRandomId(), + infiniteInstanceIndex: infiniteIndex, + fromPreviousPart: infiniteIndex !== 0, + }, + } + ) + } + function continueInfinitePiece(piece: PieceInstanceWithTimings): PieceInstanceWithTimings { + if (!piece.infinite) throw new Error('Not an infinite piece!') + return { + ...piece, + _id: protectString(piece._id + 'b'), + infinite: { + ...piece.infinite, + fromPreviousPart: true, + infiniteInstanceIndex: piece.infinite.infiniteInstanceIndex + 1, + }, + } + } + + it('playlist with no parts', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = {} + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).toHaveLength(1) + expect(objs.timingContext).toBeUndefined() + expect(objs.timeline).toEqual([ + { + classes: ['rundown_active', 'before_first_part', 'last_part'], + content: { + deviceType: 'ABSTRACT', + }, + enable: { + while: 1, + }, + id: 'mockPlaylist_status', + layer: 'rundown_status', + metaData: undefined, + objectType: 'rundown', + partInstanceId: null, + priority: 0, + }, + ]) + }) + + it('with previous and but no current part', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + previous: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance('part0'), + pieceInstances: [], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).toHaveLength(1) + expect(objs.timingContext).toBeUndefined() + }) + + it('simple current part', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance('part0'), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + + expect(objs.timingContext?.currentPartGroup.enable).toEqual({ + start: 'now', + }) + }) + + it('current part with startedPlayback', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance( + 'part0', + {}, + { + timings: { + plannedStartedPlayback: 5678, + }, + } + ), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + + expect(objs.timingContext?.currentPartGroup.enable).toEqual({ + start: 5678, + }) + }) + + it('next part no autonext', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance('part0'), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + next: { + nowInPart: 0, + partStarted: undefined, + partInstance: createMockPartInstance('part1'), + pieceInstances: [createMockPieceInstance('piece1')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + + // make sure the next part was not generated + expect(objs.timingContext?.nextPartGroup).toBeUndefined() + const nextPartGroupId = getPartGroupId(selectedPartInfos.next!.partInstance) + expect(objs.timeline.find((obj) => obj.id === nextPartGroupId)).toBeUndefined() + }) + + it('next part with autonext', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance('part0', { autoNext: true, expectedDuration: 5000 }), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + next: { + nowInPart: 0, + partStarted: undefined, + partInstance: createMockPartInstance('part1'), + pieceInstances: [createMockPieceInstance('piece1')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).toBeTruthy() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + + // make sure the next part was generated + expect(objs.timingContext?.nextPartGroup).toBeTruthy() + const nextPartGroupId = getPartGroupId(selectedPartInfos.next!.partInstance) + expect(objs.timeline.find((obj) => obj.id === nextPartGroupId)).toBeTruthy() + }) + + it('current and previous parts', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + previous: { + nowInPart: 9999, + partStarted: 1234, + partInstance: createMockPartInstance( + 'part9', + { autoNext: true, expectedDuration: 5000 }, + { + timings: { + plannedStartedPlayback: 1235, + }, + } + ), + pieceInstances: [createMockPieceInstance('piece9')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance('part0'), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + + // make sure the previous part was generated + const previousPartGroupId = getPartGroupId(selectedPartInfos.previous!.partInstance) + expect(objs.timeline.find((obj) => obj.id === previousPartGroupId)).toBeTruthy() + expect(objs.timingContext?.previousPartOverlap).not.toBeUndefined() + }) + + describe('overlap and keepalive', () => { + it('current and previous parts', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + previous: { + nowInPart: 9999, + partStarted: 1234, + partInstance: createMockPartInstance( + 'part9', + { autoNext: true, expectedDuration: 5000 }, + { + timings: { + plannedStartedPlayback: 1235, + }, + } + ), + pieceInstances: [createMockPieceInstance('piece9'), createMockPieceInstance('piece8')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance('part0'), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: { + inTransitionStart: 200, + toPartDelay: 500, + toPartPostroll: 0, + fromPartRemaining: 500 + 400, + fromPartPostroll: 400, + fromPartKeepalive: 100, + }, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + + // make sure the previous part was generated + const previousPartGroupId = getPartGroupId(selectedPartInfos.previous!.partInstance) + expect(objs.timeline.find((obj) => obj.id === previousPartGroupId)).toBeTruthy() + expect(objs.timingContext?.previousPartOverlap).not.toBeUndefined() + }) + + it('current and previous parts with excludeDuringPartKeepalive', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + previous: { + nowInPart: 9999, + partStarted: 1234, + partInstance: createMockPartInstance( + 'part9', + { autoNext: true, expectedDuration: 5000 }, + { + timings: { + plannedStartedPlayback: 1235, + }, + } + ), + pieceInstances: [ + createMockPieceInstance('piece9'), + createMockPieceInstance('piece8', { + excludeDuringPartKeepalive: true, + }), + ], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance('part0'), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: { + inTransitionStart: 200, + toPartDelay: 500, + toPartPostroll: 0, + fromPartRemaining: 500 + 400, + fromPartPostroll: 400, + fromPartKeepalive: 100, + }, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + + // make sure the previous part was generated + const previousPartGroupId = getPartGroupId(selectedPartInfos.previous!.partInstance) + expect(objs.timeline.find((obj) => obj.id === previousPartGroupId)).toBeTruthy() + expect(objs.timingContext?.previousPartOverlap).not.toBeUndefined() + }) + + it('autonext into next part', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance('part0', { autoNext: true, expectedDuration: 5000 }), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + next: { + nowInPart: 0, + partStarted: undefined, + partInstance: createMockPartInstance('part1'), + pieceInstances: [createMockPieceInstance('piece1')], + calculatedTimings: { + inTransitionStart: 200, + toPartDelay: 500, + toPartPostroll: 0, + fromPartRemaining: 500 + 400, + fromPartPostroll: 400, + fromPartKeepalive: 100, + }, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).toBeTruthy() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + + // make sure the next part was generated + expect(objs.timingContext?.nextPartGroup).toBeTruthy() + const nextPartGroupId = getPartGroupId(selectedPartInfos.next!.partInstance) + expect(objs.timeline.find((obj) => obj.id === nextPartGroupId)).toBeTruthy() + }) + + it('autonext into next part with excludeDuringPartKeepalive', () => { + const context = setupDefaultJobEnvironment() + + jest.spyOn(global.Date, 'now').mockImplementation(() => 3000) + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance( + 'part0', + { autoNext: true, expectedDuration: 5000 }, + { + timings: { + plannedStartedPlayback: 1235, + }, + } + ), + pieceInstances: [ + createMockPieceInstance('piece0'), + createMockPieceInstance('piece9', { + excludeDuringPartKeepalive: true, + }), + ], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + next: { + nowInPart: 0, + partStarted: undefined, + partInstance: createMockPartInstance( + 'part1', + {}, + { + timings: { + plannedStartedPlayback: 5000, + }, + } + ), + pieceInstances: [createMockPieceInstance('piece1')], + calculatedTimings: { + inTransitionStart: 200, + toPartDelay: 500, + toPartPostroll: 0, + fromPartRemaining: 500 + 400, + fromPartPostroll: 400, + fromPartKeepalive: 100, + }, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + + // make sure the previous part was generated + expect(objs.timingContext?.nextPartGroup).toBeTruthy() + const nextPartGroupId = getPartGroupId(selectedPartInfos.next!.partInstance) + expect(objs.timeline.find((obj) => obj.id === nextPartGroupId)).toBeTruthy() + }) + }) + + describe('infinite pieces', () => { + const PREVIOUS_PART_INSTANCE: SelectedPartInstanceTimelineInfo = { + nowInPart: 9999, + partStarted: 1234, + partInstance: createMockPartInstance( + 'part9', + { autoNext: true, expectedDuration: 5000 }, + { + timings: { + plannedStartedPlayback: 1235, + }, + } + ), + pieceInstances: [createMockPieceInstance('piece9')], + calculatedTimings: DEFAULT_PART_TIMINGS, + } + + it('infinite starting in current', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + previous: PREVIOUS_PART_INSTANCE, + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance('part0'), + pieceInstances: [ + createMockPieceInstance('piece0'), + createMockInfinitePieceInstance('piece1', {}, { plannedStartedPlayback: undefined }), + ], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + }) + + it('infinite ending with previous', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + previous: { + ...PREVIOUS_PART_INSTANCE, + pieceInstances: [ + ...PREVIOUS_PART_INSTANCE.pieceInstances, + createMockInfinitePieceInstance('piece6', {}, {}, 1), + ], + }, + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance('part0'), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + }) + + it('infinite ending with previous excludeDuringPartKeepalive=true', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + previous: { + ...PREVIOUS_PART_INSTANCE, + pieceInstances: [ + ...PREVIOUS_PART_INSTANCE.pieceInstances, + createMockInfinitePieceInstance('piece6', { excludeDuringPartKeepalive: true }, {}, 1), + ], + }, + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance('part0'), + pieceInstances: [createMockPieceInstance('piece0')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + }) + + it('infinite continuing from previous', () => { + const context = setupDefaultJobEnvironment() + + const infinitePiece = createMockInfinitePieceInstance('piece6') + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + previous: { + ...PREVIOUS_PART_INSTANCE, + pieceInstances: [...PREVIOUS_PART_INSTANCE.pieceInstances, infinitePiece], + }, + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance('part0'), + pieceInstances: [createMockPieceInstance('piece0'), continueInfinitePiece(infinitePiece)], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + }) + + it('infinite continuing into next with autonext', () => { + const context = setupDefaultJobEnvironment() + + const infinitePiece = createMockInfinitePieceInstance('piece6') + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance( + 'part0', + { autoNext: true, expectedDuration: 5000 }, + { + timings: { + plannedStartedPlayback: 1235, + }, + } + ), + pieceInstances: [createMockPieceInstance('piece0'), infinitePiece], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + next: { + nowInPart: 0, + partStarted: undefined, + partInstance: createMockPartInstance( + 'part1', + {}, + { + timings: { + plannedStartedPlayback: 5000, + }, + } + ), + pieceInstances: [createMockPieceInstance('piece1'), continueInfinitePiece(infinitePiece)], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + }) + + it('infinite stopping in current with autonext', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance( + 'part0', + { autoNext: true, expectedDuration: 5000 }, + { + timings: { + plannedStartedPlayback: 1235, + }, + } + ), + pieceInstances: [createMockPieceInstance('piece0'), createMockInfinitePieceInstance('piece6')], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + next: { + nowInPart: 0, + partStarted: undefined, + partInstance: createMockPartInstance( + 'part1', + {}, + { + timings: { + plannedStartedPlayback: 5000, + }, + } + ), + pieceInstances: [createMockPieceInstance('piece1')], + calculatedTimings: { + ...DEFAULT_PART_TIMINGS, + fromPartKeepalive: 100, + }, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + }) + + it('infinite stopping in current with autonext excludeDuringPartKeepalive=true', () => { + const context = setupDefaultJobEnvironment() + + const selectedPartInfos: SelectedPartInstancesTimelineInfo = { + current: { + nowInPart: 1234, + partStarted: 5678, + partInstance: createMockPartInstance( + 'part0', + { autoNext: true, expectedDuration: 5000 }, + { + timings: { + plannedStartedPlayback: 1235, + }, + } + ), + pieceInstances: [ + createMockPieceInstance('piece0'), + createMockInfinitePieceInstance('piece6', { excludeDuringPartKeepalive: true }), + ], + calculatedTimings: DEFAULT_PART_TIMINGS, + }, + next: { + nowInPart: 0, + partStarted: undefined, + partInstance: createMockPartInstance( + 'part1', + {}, + { + timings: { + plannedStartedPlayback: 5000, + }, + } + ), + pieceInstances: [createMockPieceInstance('piece1')], + calculatedTimings: { + ...DEFAULT_PART_TIMINGS, + fromPartKeepalive: 100, + }, + }, + } + + const playlist = createMockPlaylist(selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + + expect(objs.timeline).not.toHaveLength(0) + expect(objs.timingContext).not.toBeUndefined() + expect(transformTimelineIntoSimplifiedForm(objs)).toMatchSnapshot() + }) + }) +}) diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 8c71749fd6..dd686abdb2 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -337,12 +337,7 @@ async function getTimelineRundown( logger.warn(`Missing Baseline objects for Rundown "${activeRundown.rundown._id}"`) } - const rundownTimelineResult = buildTimelineObjsForRundown( - context, - playoutModel, - activeRundown.rundown, - partInstancesInfo - ) + const rundownTimelineResult = buildTimelineObjsForRundown(context, playoutModel.playlist, partInstancesInfo) timelineObjs = timelineObjs.concat(rundownTimelineResult.timeline) timelineObjs = timelineObjs.concat(await pLookaheadObjs) diff --git a/packages/job-worker/src/playout/timeline/part.ts b/packages/job-worker/src/playout/timeline/part.ts index 4a20c2ef23..2c9dddbc3d 100644 --- a/packages/job-worker/src/playout/timeline/part.ts +++ b/packages/job-worker/src/playout/timeline/part.ts @@ -13,11 +13,12 @@ import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' import { getPartGroupId, getPartFirstObjectId } from '@sofie-automation/corelib/dist/playout/ids' import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { PieceTimelineMetadata } from './pieceGroup' -import { PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings' import { JobContext } from '../../jobs' import { ReadonlyDeep } from 'type-fest' import { getPieceEnableInsidePart, transformPieceGroupAndObjects } from './piece' import { PlayoutChangedType } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' +import { SelectedPartInstanceTimelineInfo } from './generate' +import { PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings' export function transformPartIntoTimeline( context: JobContext, @@ -25,61 +26,43 @@ export function transformPartIntoTimeline( pieceInstances: ReadonlyDeep>, pieceGroupFirstObjClasses: string[], parentGroup: TimelineObjGroupPart & OnGenerateTimelineObjExt, - nowInParentGroup: number, - partTimings: PartCalculatedTimings, - isInHold: boolean, - outTransition: IBlueprintPartOutTransition | null + partInfo: SelectedPartInstanceTimelineInfo, + nextPartTimings: PartCalculatedTimings | null, + isInHold: boolean ): Array { const span = context.startSpan('transformPartIntoTimeline') + + const nowInParentGroup = partInfo.nowInPart + const partTimings = partInfo.calculatedTimings + const outTransition = partInfo.partInstance.part.outTransition ?? null + + let parentGroupNoKeepalive: (TimelineObjGroupPart & OnGenerateTimelineObjExt) | undefined + const timelineObjs: Array = [] for (const pieceInstance of pieceInstances) { if (pieceInstance.disabled) continue - let pieceEnable: TSR.Timeline.TimelineEnable | undefined - switch (pieceInstance.piece.pieceType) { - case IBlueprintPieceType.InTransition: - if (typeof partTimings.inTransitionStart === 'number') { - // Respect the start time of the piece, in case there is a reason for it being non-zero - const startOffset = - typeof pieceInstance.piece.enable.start === 'number' ? pieceInstance.piece.enable.start : 0 - - pieceEnable = { - start: partTimings.inTransitionStart + startOffset, - duration: pieceInstance.piece.enable.duration, - } - } - break - case IBlueprintPieceType.OutTransition: - if (outTransition) { - pieceEnable = { - start: `#${parentGroup.id}.end - ${outTransition.duration}`, - } - if (partTimings.toPartPostroll) { - pieceEnable.start += ' - ' + partTimings.toPartPostroll - } - } - break - case IBlueprintPieceType.Normal: - pieceEnable = getPieceEnableInsidePart( - pieceInstance, - partTimings, - parentGroup.id, - parentGroup.enable.duration !== undefined || parentGroup.enable.end !== undefined - ) - break - default: - assertNever(pieceInstance.piece.pieceType) - break - } + const pieceEnable = getPieceEnableForPieceInstance(partTimings, outTransition, parentGroup, pieceInstance) // Not able to enable this piece if (!pieceEnable) continue + // Determine which group to add to + let partGroupToAddTo = parentGroup + if (pieceInstance.piece.excludeDuringPartKeepalive) { + if (!parentGroupNoKeepalive) { + // Only generate the no-keepalive group if is is needed + parentGroupNoKeepalive = createPartNoKeepaliveGroup(parentGroup, nextPartTimings) + timelineObjs.push(parentGroupNoKeepalive) + } + partGroupToAddTo = parentGroupNoKeepalive + } + timelineObjs.push( ...transformPieceGroupAndObjects( playlistId, - parentGroup, + partGroupToAddTo, nowInParentGroup, pieceInstance, pieceEnable, @@ -94,6 +77,49 @@ export function transformPartIntoTimeline( return timelineObjs } +function getPieceEnableForPieceInstance( + partTimings: PartCalculatedTimings, + outTransition: IBlueprintPartOutTransition | null, + parentGroup: TimelineObjGroupPart & OnGenerateTimelineObjExt, + pieceInstance: ReadonlyDeep +): TSR.Timeline.TimelineEnable | undefined { + switch (pieceInstance.piece.pieceType) { + case IBlueprintPieceType.InTransition: { + if (typeof partTimings.inTransitionStart !== 'number') return undefined + // Respect the start time of the piece, in case there is a reason for it being non-zero + const startOffset = + typeof pieceInstance.piece.enable.start === 'number' ? pieceInstance.piece.enable.start : 0 + + return { + start: partTimings.inTransitionStart + startOffset, + duration: pieceInstance.piece.enable.duration, + } + } + case IBlueprintPieceType.OutTransition: { + if (!outTransition) return undefined + + const pieceEnable: TSR.Timeline.TimelineEnable = { + start: `#${parentGroup.id}.end - ${outTransition.duration}`, + } + if (partTimings.toPartPostroll) { + pieceEnable.start += ' - ' + partTimings.toPartPostroll + } + + return pieceEnable + } + case IBlueprintPieceType.Normal: + return getPieceEnableInsidePart( + pieceInstance, + partTimings, + parentGroup.id, + parentGroup.enable.duration !== undefined || parentGroup.enable.end !== undefined + ) + default: + assertNever(pieceInstance.piece.pieceType) + return undefined + } +} + export interface PartEnable { start: number | 'now' | string duration?: number @@ -154,3 +180,32 @@ export function createPartGroupFirstObject( priority: 0, }) } + +export function createPartNoKeepaliveGroup( + partGroup: TimelineObjGroupPart & OnGenerateTimelineObjExt, + nextPartTimings: PartCalculatedTimings | null +): TimelineObjGroupPart & OnGenerateTimelineObjExt { + const keepaliveDuration = nextPartTimings?.fromPartKeepalive ?? 0 + + return { + id: `${partGroup.id}_no_keepalive`, + objectType: TimelineObjType.RUNDOWN, + enable: { + start: 0, + end: `#${partGroup.id}.end - ${keepaliveDuration}`, + }, + priority: 5, + layer: '', // These should coexist + content: { + deviceType: TSR.DeviceType.ABSTRACT, + type: TimelineContentTypeOther.GROUP, + }, + children: [], + isGroup: true, + partInstanceId: partGroup.partInstanceId, + metaData: literal({ + isPieceTimeline: true, + }), + inGroup: partGroup.id, + } +} diff --git a/packages/job-worker/src/playout/timeline/pieceGroup.ts b/packages/job-worker/src/playout/timeline/pieceGroup.ts index bf4da90ef8..45ceeb43e7 100644 --- a/packages/job-worker/src/playout/timeline/pieceGroup.ts +++ b/packages/job-worker/src/playout/timeline/pieceGroup.ts @@ -42,8 +42,11 @@ export function createPieceGroupAndCap( partGroup?: TimelineObjRundown, pieceStartOffset?: number ): { + /** The 'control' object which defines the bounds of the group. This triggers the timing, and does not include and pre/postroll */ controlObj: TimelineObjPieceAbstract & OnGenerateTimelineObjExt + /** The 'group' object that should contain all the content. This uses the control object for its timing, and adds the pre/postroll. */ childGroup: TimelineObjGroupRundown & OnGenerateTimelineObjExt + /** Any additional objects which are used to determine points in time that the piece should start/end relative to. */ capObjs: Array> } { const controlObj = literal>({ diff --git a/packages/job-worker/src/playout/timeline/rundown.ts b/packages/job-worker/src/playout/timeline/rundown.ts index 0e7a107986..ad2f3562dd 100644 --- a/packages/job-worker/src/playout/timeline/rundown.ts +++ b/packages/job-worker/src/playout/timeline/rundown.ts @@ -22,11 +22,9 @@ import { JobContext } from '../../jobs' import { ReadonlyDeep } from 'type-fest' import { SelectedPartInstancesTimelineInfo, SelectedPartInstanceTimelineInfo } from './generate' import { createPartGroup, createPartGroupFirstObject, PartEnable, transformPartIntoTimeline } from './part' -import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { literal, normalizeArrayToMapFunc } from '@sofie-automation/corelib/dist/lib' import { getCurrentTime } from '../../lib' import _ = require('underscore') -import { PlayoutModel } from '../model/PlayoutModel' import { getPieceEnableInsidePart, transformPieceGroupAndObjects } from './piece' import { logger } from '../../logging' @@ -50,14 +48,12 @@ export interface RundownTimelineResult { export function buildTimelineObjsForRundown( context: JobContext, - playoutModel: PlayoutModel, - _activeRundown: ReadonlyDeep, + activePlaylist: ReadonlyDeep, partInstancesInfo: SelectedPartInstancesTimelineInfo ): RundownTimelineResult { const span = context.startSpan('buildTimelineObjsForRundown') const timelineObjs: Array = [] - const activePlaylist = playoutModel.playlist const currentTime = getCurrentTime() timelineObjs.push( @@ -102,136 +98,110 @@ export function buildTimelineObjsForRundown( logger.info(`No next part and no current part set on RundownPlaylist "${activePlaylist._id}".`) } - let timingContext: RundownTimelineTimingContext | undefined - // Currently playing: - if (partInstancesInfo.current) { - const [currentInfinitePieces, currentNormalItems] = _.partition( - partInstancesInfo.current.pieceInstances, - (l) => !!(l.infinite && (l.piece.lifespan !== PieceLifespan.WithinPart || l.infinite.fromHold)) - ) - - // Find all the infinites in each of the selected parts - const currentInfinitePieceIds = new Set( - _.compact(currentInfinitePieces.map((l) => l.infinite?.infiniteInstanceId)) - ) - const nextPartInfinites = new Map() - if (partInstancesInfo.current.partInstance.part.autoNext && partInstancesInfo.next) { - partInstancesInfo.next.pieceInstances.forEach((piece) => { - if (piece.infinite) { - nextPartInfinites.set(piece.infinite.infiniteInstanceId, piece) - } - }) + if (!partInstancesInfo.current) { + if (span) span.end() + return { + timeline: timelineObjs, + timingContext: undefined, } + } - const previousPartInfinites: Map = - partInstancesInfo.previous - ? normalizeArrayToMapFunc(partInstancesInfo.previous.pieceInstances, (inst) => - inst.infinite ? inst.infinite.infiniteInstanceId : undefined - ) - : new Map() - - // The startTime of this start is used as the reference point for the calculated timings, so we can use 'now' and everything will lie after this point - const currentPartEnable: PartEnable = { start: 'now' } - if (partInstancesInfo.current.partInstance.timings?.plannedStartedPlayback) { - // If we are recalculating the currentPart, then ensure it doesnt think it is starting now - currentPartEnable.start = partInstancesInfo.current.partInstance.timings.plannedStartedPlayback - } + const [currentInfinitePieces, currentNormalItems] = _.partition( + partInstancesInfo.current.pieceInstances, + (l) => !!(l.infinite && (l.piece.lifespan !== PieceLifespan.WithinPart || l.infinite.fromHold)) + ) - if ( - partInstancesInfo.next && - partInstancesInfo.current.partInstance.part.autoNext && - partInstancesInfo.current.partInstance.part.expectedDuration !== undefined - ) { - // If there is a valid autonext out of the current part, then calculate the duration - currentPartEnable.duration = - partInstancesInfo.current.partInstance.part.expectedDuration + - partInstancesInfo.current.calculatedTimings.toPartDelay + - partInstancesInfo.current.calculatedTimings.toPartPostroll // autonext should have the postroll added to it to not confuse the timeline - - if ( - typeof currentPartEnable.start === 'number' && - currentPartEnable.start + currentPartEnable.duration < getCurrentTime() - ) { - logger.warn('Prevented setting the end of an autonext in the past') - // note - this will cause a small glitch on air where the next part is skipped into because this calculation does not account - // for the time it takes between timeline generation and timeline execution. That small glitch is preferable to setting the time - // very far in the past however. To do this properly we should support setting the "end" to "now" and have that calculated after - // timeline generation as we do for start times. - currentPartEnable.duration = getCurrentTime() - currentPartEnable.start + // Find all the infinites in each of the selected parts + const currentInfinitePieceIds = new Set(_.compact(currentInfinitePieces.map((l) => l.infinite?.infiniteInstanceId))) + const nextPartInfinites = new Map() + if (partInstancesInfo.current.partInstance.part.autoNext && partInstancesInfo.next) { + partInstancesInfo.next.pieceInstances.forEach((piece) => { + if (piece.infinite) { + nextPartInfinites.set(piece.infinite.infiniteInstanceId, piece) } - } - const currentPartGroup = createPartGroup(partInstancesInfo.current.partInstance, currentPartEnable) + }) + } - timingContext = { - currentPartGroup, - currentPartDuration: currentPartEnable.duration, - } + const previousPartInfinites: Map = + partInstancesInfo.previous + ? normalizeArrayToMapFunc(partInstancesInfo.previous.pieceInstances, (inst) => + inst.infinite ? inst.infinite.infiniteInstanceId : undefined + ) + : new Map() - // Start generating objects - if (partInstancesInfo.previous) { - timelineObjs.push( - ...generatePreviousPartInstanceObjects( - context, - activePlaylist, - partInstancesInfo.previous, - currentInfinitePieceIds, - timingContext, - partInstancesInfo.current.calculatedTimings - ) - ) - } + // The startTime of this start is used as the reference point for the calculated timings, so we can use 'now' and everything will lie after this point + const currentPartEnable = createCurrentPartGroupEnable(partInstancesInfo.current, !!partInstancesInfo.next) + const currentPartGroup = createPartGroup(partInstancesInfo.current.partInstance, currentPartEnable) - // any continued infinite lines need to skip the group, as they need a different start trigger - for (const infinitePiece of currentInfinitePieces) { - timelineObjs.push( - ...generateCurrentInfinitePieceObjects( - activePlaylist, - partInstancesInfo.current, - previousPartInfinites, - nextPartInfinites, - timingContext, - infinitePiece, - currentTime, - partInstancesInfo.current.calculatedTimings - ) - ) - } + const timingContext: RundownTimelineTimingContext = { + currentPartGroup, + currentPartDuration: currentPartEnable.duration, + } - const groupClasses: string[] = ['current_part'] + // Start generating objects + if (partInstancesInfo.previous) { timelineObjs.push( - currentPartGroup, - createPartGroupFirstObject( - activePlaylist._id, - partInstancesInfo.current.partInstance, - currentPartGroup, - partInstancesInfo.previous?.partInstance - ), - ...transformPartIntoTimeline( + ...generatePreviousPartInstanceObjects( context, - activePlaylist._id, - currentNormalItems, - groupClasses, - currentPartGroup, - partInstancesInfo.current.nowInPart, + activePlaylist, + partInstancesInfo.previous, + currentInfinitePieceIds, + timingContext, + partInstancesInfo.current.calculatedTimings + ) + ) + } + + // any continued infinite lines need to skip the group, as they need a different start trigger + for (const infinitePiece of currentInfinitePieces) { + timelineObjs.push( + ...generateCurrentInfinitePieceObjects( + activePlaylist, + partInstancesInfo.current, + previousPartInfinites, + nextPartInfinites, + timingContext, + infinitePiece, + currentTime, partInstancesInfo.current.calculatedTimings, - activePlaylist.holdState === RundownHoldState.ACTIVE, - partInstancesInfo.current.partInstance.part.outTransition ?? null + partInstancesInfo.next?.calculatedTimings ?? null ) ) + } + + const groupClasses: string[] = ['current_part'] + timelineObjs.push( + currentPartGroup, + createPartGroupFirstObject( + activePlaylist._id, + partInstancesInfo.current.partInstance, + currentPartGroup, + partInstancesInfo.previous?.partInstance + ), + ...transformPartIntoTimeline( + context, + activePlaylist._id, + currentNormalItems, + groupClasses, + currentPartGroup, + partInstancesInfo.current, + partInstancesInfo.next?.calculatedTimings ?? null, + activePlaylist.holdState === RundownHoldState.ACTIVE + ) + ) - // only add the next objects into the timeline if the current partgroup has a duration, and can autoNext - if (partInstancesInfo.next && currentPartEnable.duration) { - timelineObjs.push( - ...generateNextPartInstanceObjects( - context, - activePlaylist, - partInstancesInfo.current, - partInstancesInfo.next, - timingContext - ) + // only add the next objects into the timeline if the current partgroup has a duration, and can autoNext + if (partInstancesInfo.next && currentPartEnable.duration) { + timelineObjs.push( + ...generateNextPartInstanceObjects( + context, + activePlaylist, + partInstancesInfo.current, + partInstancesInfo.next, + timingContext ) - } + ) } if (span) span.end() @@ -241,6 +211,44 @@ export function buildTimelineObjsForRundown( } } +function createCurrentPartGroupEnable( + currentPartInfo: SelectedPartInstanceTimelineInfo, + hasNextPart: boolean +): PartEnable { + // The startTime of this start is used as the reference point for the calculated timings, so we can use 'now' and everything will lie after this point + const currentPartEnable: PartEnable = { start: 'now' } + if (currentPartInfo.partInstance.timings?.plannedStartedPlayback) { + // If we are recalculating the currentPart, then ensure it doesnt think it is starting now + currentPartEnable.start = currentPartInfo.partInstance.timings.plannedStartedPlayback + } + + if ( + hasNextPart && + currentPartInfo.partInstance.part.autoNext && + currentPartInfo.partInstance.part.expectedDuration !== undefined + ) { + // If there is a valid autonext out of the current part, then calculate the duration + currentPartEnable.duration = + currentPartInfo.partInstance.part.expectedDuration + + currentPartInfo.calculatedTimings.toPartDelay + + currentPartInfo.calculatedTimings.toPartPostroll // autonext should have the postroll added to it to not confuse the timeline + + if ( + typeof currentPartEnable.start === 'number' && + currentPartEnable.start + currentPartEnable.duration < getCurrentTime() + ) { + logger.warn('Prevented setting the end of an autonext in the past') + // note - this will cause a small glitch on air where the next part is skipped into because this calculation does not account + // for the time it takes between timeline generation and timeline execution. That small glitch is preferable to setting the time + // very far in the past however. To do this properly we should support setting the "end" to "now" and have that calculated after + // timeline generation as we do for start times. + currentPartEnable.duration = getCurrentTime() - currentPartEnable.start + } + } + + return currentPartEnable +} + export function getInfinitePartGroupId(pieceInstanceId: PieceInstanceId): string { return getPartGroupId(protectString(unprotectString(pieceInstanceId))) + '_infinite' } @@ -253,7 +261,8 @@ function generateCurrentInfinitePieceObjects( timingContext: RundownTimelineTimingContext, pieceInstance: PieceInstanceWithTimings, currentTime: Time, - currentPartInstanceTimings: PartCalculatedTimings + currentPartInstanceTimings: PartCalculatedTimings, + nextPartInstanceTimings: PartCalculatedTimings | null ): Array { if (!pieceInstance.infinite) { // Type guard, should never be hit @@ -343,14 +352,17 @@ function generateCurrentInfinitePieceObjects( infiniteGroup.enable.duration === undefined && infiniteGroup.enable.end === undefined ) { + let endOffset = 0 + + if (currentPartInstanceTimings.fromPartPostroll) endOffset -= currentPartInstanceTimings.fromPartPostroll + + if (pieceInstance.piece.postrollDuration) endOffset += pieceInstance.piece.postrollDuration + + if (pieceInstance.piece.excludeDuringPartKeepalive && nextPartInstanceTimings) + endOffset -= nextPartInstanceTimings.fromPartKeepalive + // cap relative to the currentPartGroup - infiniteGroup.enable.end = `#${timingContext.currentPartGroup.id}.end` - if (currentPartInstanceTimings.fromPartPostroll) { - infiniteGroup.enable.end += ' - ' + currentPartInstanceTimings.fromPartPostroll - } - if (pieceInstance.piece.postrollDuration) { - infiniteGroup.enable.end += ' + ' + pieceInstance.piece.postrollDuration - } + infiniteGroup.enable.end = `#${timingContext.currentPartGroup.id}.end + ${endOffset}` } // Still show objects flagged as 'HoldMode.EXCEPT' if this is a infinite continuation as they belong to the previous too @@ -408,10 +420,9 @@ function generatePreviousPartInstanceObjects( previousContinuedPieces, groupClasses, previousPartGroup, - previousPartInfo.nowInPart, - previousPartInfo.calculatedTimings, - activePlaylist.holdState === RundownHoldState.ACTIVE, - previousPartInfo.partInstance.part.outTransition ?? null + previousPartInfo, + currentPartInstanceTimings, + activePlaylist.holdState === RundownHoldState.ACTIVE ), ] } else { @@ -452,10 +463,9 @@ function generateNextPartInstanceObjects( nextPieceInstances, groupClasses, nextPartGroup, - 0, - nextPartInfo.calculatedTimings, - false, - nextPartInfo.partInstance.part.outTransition ?? null + nextPartInfo, + null, + false ), ] } diff --git a/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts b/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts index 262fe8922c..e723f8de46 100644 --- a/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts +++ b/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts @@ -1453,6 +1453,7 @@ describe('rundown Timing Calculator', () => { toPartPostroll: 500, fromPartRemaining: 0, fromPartPostroll: 0, + fromPartKeepalive: 0, } const partInstance2 = wrapPartToTemporaryInstance(protectString(''), parts[1]) partInstance2.isTemporary = false @@ -1466,6 +1467,7 @@ describe('rundown Timing Calculator', () => { toPartPostroll: 0, fromPartRemaining: 500, fromPartPostroll: 500, + fromPartKeepalive: 0, } const partInstances = [partInstance1, partInstance2, ...convertPartsToPartInstances([parts[2], parts[3]])] const partInstancesMap: Map = new Map()