Skip to content

Commit 0320ffd

Browse files
authored
Merge pull request #1274 from bbc/upstream/fix-postroll-and-autonext-timing
Fix: UI Timing: Postroll and autonext timing
2 parents d51eb70 + 89df068 commit 0320ffd

File tree

3 files changed

+290
-3
lines changed

3 files changed

+290
-3
lines changed

meteor/client/lib/__tests__/rundownTiming.test.ts

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,6 +1262,289 @@ describe('rundown Timing Calculator', () => {
12621262
)
12631263
})
12641264

1265+
it('Handles part with autonext', () => {
1266+
const timing = new RundownTimingCalculator()
1267+
const playlist: DBRundownPlaylist = makeMockPlaylist()
1268+
playlist.timing = {
1269+
type: 'forward-time' as any,
1270+
expectedStart: 0,
1271+
expectedDuration: 40000,
1272+
}
1273+
const rundownId1 = 'rundown1'
1274+
const segmentId1 = 'segment1'
1275+
const segmentId2 = 'segment2'
1276+
const segmentsMap: Map<SegmentId, DBSegment> = new Map()
1277+
segmentsMap.set(protectString<SegmentId>(segmentId1), makeMockSegment(segmentId1, 0, rundownId1))
1278+
segmentsMap.set(protectString<SegmentId>(segmentId2), makeMockSegment(segmentId2, 0, rundownId1))
1279+
const parts: DBPart[] = []
1280+
parts.push(
1281+
makeMockPart('part1', 0, rundownId1, segmentId1, {
1282+
budgetDuration: 2000,
1283+
expectedDuration: 1000,
1284+
})
1285+
)
1286+
parts.push(
1287+
makeMockPart('part2', 0, rundownId1, segmentId1, {
1288+
budgetDuration: 3000,
1289+
expectedDuration: 1000,
1290+
})
1291+
)
1292+
parts.push(
1293+
makeMockPart('part3', 0, rundownId1, segmentId2, {
1294+
budgetDuration: 3000,
1295+
expectedDuration: 1000,
1296+
})
1297+
)
1298+
parts.push(makeMockPart('part4', 0, rundownId1, segmentId2, { expectedDuration: 1000 }))
1299+
// set autonext and create partInstances
1300+
parts[0].autoNext = true
1301+
const partInstance1 = wrapPartToTemporaryInstance(protectString(''), parts[0])
1302+
partInstance1.isTemporary = false
1303+
partInstance1.timings = {
1304+
plannedStartedPlayback: 0,
1305+
}
1306+
const partInstance2 = wrapPartToTemporaryInstance(protectString(''), parts[1])
1307+
partInstance2.isTemporary = false
1308+
partInstance2.timings = {
1309+
plannedStartedPlayback: 1000, // start after part1's expectedDuration
1310+
}
1311+
const partInstances = [partInstance1, partInstance2, ...convertPartsToPartInstances([parts[2], parts[3]])]
1312+
const partInstancesMap: Map<PartId, PartInstance> = new Map()
1313+
const rundown = makeMockRundown(rundownId1, playlist)
1314+
const rundowns = [rundown]
1315+
// at t = 0
1316+
const result = timing.updateDurations(
1317+
0,
1318+
false,
1319+
playlist,
1320+
rundowns,
1321+
undefined,
1322+
partInstances,
1323+
partInstancesMap,
1324+
segmentsMap,
1325+
DEFAULT_DURATION,
1326+
[]
1327+
)
1328+
expect(result).toEqual(
1329+
literal<RundownTimingContext>({
1330+
currentPartInstanceId: null,
1331+
isLowResolution: false,
1332+
asDisplayedPlaylistDuration: 4000,
1333+
asPlayedPlaylistDuration: 8000,
1334+
currentPartWillAutoNext: false,
1335+
currentTime: 0,
1336+
rundownExpectedDurations: {
1337+
[rundownId1]: 4000,
1338+
},
1339+
rundownAsPlayedDurations: {
1340+
[rundownId1]: 8000,
1341+
},
1342+
partCountdown: {
1343+
part1: 0,
1344+
part2: 1000,
1345+
part3: 5000,
1346+
part4: 6000,
1347+
},
1348+
partDisplayDurations: {
1349+
part1_tmp_instance: 1000,
1350+
part2_tmp_instance: 1000,
1351+
part3: 1000,
1352+
part4: 1000,
1353+
},
1354+
partDisplayStartsAt: {
1355+
part1_tmp_instance: 0,
1356+
part2_tmp_instance: 1000,
1357+
part3: 2000,
1358+
part4: 3000,
1359+
},
1360+
partDurations: {
1361+
part1_tmp_instance: 1000,
1362+
part2_tmp_instance: 1000,
1363+
part3: 1000,
1364+
part4: 1000,
1365+
},
1366+
partExpectedDurations: {
1367+
part1_tmp_instance: 1000,
1368+
part2_tmp_instance: 1000,
1369+
part3: 1000,
1370+
part4: 1000,
1371+
},
1372+
partPlayed: {
1373+
part1_tmp_instance: 0,
1374+
part2_tmp_instance: 0,
1375+
part3: 0,
1376+
part4: 0,
1377+
},
1378+
partStartsAt: {
1379+
part1_tmp_instance: 0,
1380+
part2_tmp_instance: 1000,
1381+
part3: 2000,
1382+
part4: 3000,
1383+
},
1384+
remainingPlaylistDuration: 8000,
1385+
totalPlaylistDuration: 8000,
1386+
breakIsLastRundown: undefined,
1387+
remainingTimeOnCurrentPart: undefined,
1388+
rundownsBeforeNextBreak: undefined,
1389+
segmentBudgetDurations: {
1390+
[segmentId1]: 5000,
1391+
[segmentId2]: 3000,
1392+
},
1393+
segmentStartedPlayback: {},
1394+
})
1395+
)
1396+
})
1397+
1398+
it('Handles part with postroll', () => {
1399+
const timing = new RundownTimingCalculator()
1400+
const playlist: DBRundownPlaylist = makeMockPlaylist()
1401+
playlist.timing = {
1402+
type: 'forward-time' as any,
1403+
expectedStart: 0,
1404+
expectedDuration: 40000,
1405+
}
1406+
const rundownId1 = 'rundown1'
1407+
const segmentId1 = 'segment1'
1408+
const segmentId2 = 'segment2'
1409+
const segmentsMap: Map<SegmentId, DBSegment> = new Map()
1410+
segmentsMap.set(protectString<SegmentId>(segmentId1), makeMockSegment(segmentId1, 0, rundownId1))
1411+
segmentsMap.set(protectString<SegmentId>(segmentId2), makeMockSegment(segmentId2, 0, rundownId1))
1412+
const parts: DBPart[] = []
1413+
parts.push(
1414+
makeMockPart('part1', 0, rundownId1, segmentId1, {
1415+
budgetDuration: 2000,
1416+
expectedDuration: 2000,
1417+
})
1418+
)
1419+
parts.push(
1420+
makeMockPart('part2', 0, rundownId1, segmentId1, {
1421+
budgetDuration: 3000,
1422+
expectedDuration: 2000,
1423+
})
1424+
)
1425+
parts.push(
1426+
makeMockPart('part3', 0, rundownId1, segmentId2, {
1427+
budgetDuration: 3000,
1428+
expectedDuration: 1000,
1429+
})
1430+
)
1431+
parts.push(makeMockPart('part4', 0, rundownId1, segmentId2, { expectedDuration: 1000 }))
1432+
// set autonext and create partInstances
1433+
parts[0].autoNext = true
1434+
const partInstance1 = wrapPartToTemporaryInstance(protectString(''), parts[0])
1435+
partInstance1.isTemporary = false
1436+
partInstance1.timings = {
1437+
plannedStartedPlayback: 0,
1438+
reportedStartedPlayback: 0,
1439+
reportedStoppedPlayback: 2000,
1440+
}
1441+
partInstance1.partPlayoutTimings = {
1442+
inTransitionStart: 0,
1443+
toPartDelay: 0,
1444+
toPartPostroll: 500,
1445+
fromPartRemaining: 0,
1446+
fromPartPostroll: 0,
1447+
}
1448+
const partInstance2 = wrapPartToTemporaryInstance(protectString(''), parts[1])
1449+
partInstance2.isTemporary = false
1450+
partInstance2.timings = {
1451+
plannedStartedPlayback: 2000, // start after part1's expectedDuration
1452+
reportedStartedPlayback: 2000,
1453+
}
1454+
partInstance2.partPlayoutTimings = {
1455+
inTransitionStart: 0,
1456+
toPartDelay: 0,
1457+
toPartPostroll: 0,
1458+
fromPartRemaining: 500,
1459+
fromPartPostroll: 500,
1460+
}
1461+
const partInstances = [partInstance1, partInstance2, ...convertPartsToPartInstances([parts[2], parts[3]])]
1462+
const partInstancesMap: Map<PartId, PartInstance> = new Map()
1463+
const rundown = makeMockRundown(rundownId1, playlist)
1464+
const rundowns = [rundown]
1465+
// at t = 0
1466+
const result = timing.updateDurations(
1467+
3000,
1468+
false,
1469+
playlist,
1470+
rundowns,
1471+
undefined,
1472+
partInstances,
1473+
partInstancesMap,
1474+
segmentsMap,
1475+
DEFAULT_DURATION,
1476+
[]
1477+
)
1478+
expect(result).toEqual(
1479+
literal<RundownTimingContext>({
1480+
currentPartInstanceId: null,
1481+
isLowResolution: false,
1482+
asDisplayedPlaylistDuration: 6000,
1483+
asPlayedPlaylistDuration: 8000,
1484+
currentPartWillAutoNext: false,
1485+
currentTime: 3000,
1486+
rundownExpectedDurations: {
1487+
[rundownId1]: 6000,
1488+
},
1489+
rundownAsPlayedDurations: {
1490+
[rundownId1]: 8000,
1491+
},
1492+
partCountdown: {
1493+
part1: 4000,
1494+
part2: 6000,
1495+
part3: 6000,
1496+
part4: 7000,
1497+
},
1498+
partDisplayDurations: {
1499+
part1_tmp_instance: 2000,
1500+
part2_tmp_instance: 2000,
1501+
part3: 1000,
1502+
part4: 1000,
1503+
},
1504+
partDisplayStartsAt: {
1505+
part1_tmp_instance: 0,
1506+
part2_tmp_instance: 2000,
1507+
part3: 4000,
1508+
part4: 5000,
1509+
},
1510+
partDurations: {
1511+
part1_tmp_instance: 2000,
1512+
part2_tmp_instance: 2000,
1513+
part3: 1000,
1514+
part4: 1000,
1515+
},
1516+
partExpectedDurations: {
1517+
part1_tmp_instance: 2000,
1518+
part2_tmp_instance: 2000,
1519+
part3: 1000,
1520+
part4: 1000,
1521+
},
1522+
partPlayed: {
1523+
part1_tmp_instance: 0,
1524+
part2_tmp_instance: 1000,
1525+
part3: 0,
1526+
part4: 0,
1527+
},
1528+
partStartsAt: {
1529+
part1_tmp_instance: 0,
1530+
part2_tmp_instance: 2000,
1531+
part3: 4000,
1532+
part4: 5000,
1533+
},
1534+
remainingPlaylistDuration: 8000,
1535+
totalPlaylistDuration: 8000,
1536+
breakIsLastRundown: undefined,
1537+
remainingTimeOnCurrentPart: undefined,
1538+
rundownsBeforeNextBreak: undefined,
1539+
segmentBudgetDurations: {
1540+
[segmentId1]: 5000,
1541+
[segmentId2]: 3000,
1542+
},
1543+
segmentStartedPlayback: {},
1544+
})
1545+
)
1546+
})
1547+
12651548
it('Back-time: Can find the next expectedStart rundown anchor when it is in a future segment', () => {
12661549
const timing = new RundownTimingCalculator()
12671550
const playlist: DBRundownPlaylist = makeMockPlaylist()

meteor/client/lib/rundownTiming.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,11 @@ export class RundownTimingCalculator {
219219
totalRundownDuration += calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0
220220
}
221221

222-
const lastStartedPlayback = partInstance.timings?.plannedStartedPlayback
222+
// note: lastStartedPlayback that lies in the future means it hasn't started yet (like from autonext)
223+
const lastStartedPlayback =
224+
(partInstance.timings?.plannedStartedPlayback ?? 0) <= now
225+
? partInstance.timings?.plannedStartedPlayback
226+
: undefined
223227
const playOffset = partInstance.timings?.playOffset || 0
224228

225229
let partDuration = 0

packages/corelib/src/playout/timings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ export function getPartTimingsOrDefaults(
158158
}
159159

160160
function calculateExpectedDurationWithTransition(rawDuration: number, timings: PartCalculatedTimings): number {
161-
// toPartDelay needs to be subtracted, because it is added to `fromPartRemaining` when the `fromPartRemaining` value is calculated.
162-
return Math.max(0, rawDuration - (timings.fromPartRemaining - timings.toPartDelay))
161+
// toPartDelay and fromPartPostroll needs to be subtracted, because it is added to `fromPartRemaining` when the `fromPartRemaining` value is calculated.
162+
return Math.max(0, rawDuration - (timings.fromPartRemaining - timings.toPartDelay - timings.fromPartPostroll))
163163
}
164164

165165
export type CalculateExpectedDurationPart = Pick<DBPart, 'inTransition' | 'expectedDuration'>

0 commit comments

Comments
 (0)