Skip to content

Commit a6e9a9b

Browse files
authored
Merge pull request Sofie-Automation#1283 from bbc/upstream/fix-quickloop-timers
2 parents e26a3d2 + 77cc59b commit a6e9a9b

File tree

5 files changed

+166
-8
lines changed

5 files changed

+166
-8
lines changed

packages/job-worker/src/playout/model/PlayoutModel.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,14 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa
343343
*/
344344
setQuickLoopMarker(type: 'start' | 'end', marker: QuickLoopMarker | null): void
345345

346+
/**
347+
* Returns any segmentId's that are found between 2 quickloop markers, none will be returned if
348+
* the end is before the start.
349+
* @param start A quickloop marker
350+
* @param end A quickloop marker
351+
*/
352+
getSegmentsBetweenQuickLoopMarker(start: QuickLoopMarker, end: QuickLoopMarker): SegmentId[]
353+
346354
calculatePartTimings(
347355
fromPartInstance: PlayoutPartInstanceModel | null,
348356
toPartInstance: PlayoutPartInstanceModel,

packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,11 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
593593

594594
if (regenerateActivationId) this.playlistImpl.activationId = getRandomId()
595595

596-
if (this.playlistImpl.quickLoop?.running) this.playlistImpl.quickLoop.running = false
596+
// reset quickloop if applicable:
597+
if (this.playlist.quickLoop && !this.playlist.quickLoop.locked) {
598+
this.setQuickLoopMarker('start', null)
599+
this.setQuickLoopMarker('end', null)
600+
}
597601

598602
this.#playlistHasChanged = true
599603
}
@@ -792,6 +796,10 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
792796
this.#playlistHasChanged = true
793797
}
794798

799+
getSegmentsBetweenQuickLoopMarker(start: QuickLoopMarker, end: QuickLoopMarker): SegmentId[] {
800+
return this.quickLoopService.getSegmentsBetweenMarkers(start, end)
801+
}
802+
795803
/** Lifecycle */
796804

797805
/** @deprecated */

packages/job-worker/src/playout/model/services/QuickLoopService.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
99
import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep'
1010
import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
11-
import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids'
11+
import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids'
1212
import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment'
1313
import { PlayoutPartInstanceModel } from '../PlayoutPartInstanceModel'
1414
import { JobContext } from '../../../jobs'
@@ -149,6 +149,58 @@ export class QuickLoopService {
149149
return quickLoopProps
150150
}
151151

152+
getSegmentsBetweenMarkers(startMarker: QuickLoopMarker, endMarker: QuickLoopMarker): SegmentId[] {
153+
const segments = this.playoutModel.getAllOrderedSegments()
154+
const segmentIds: SegmentId[] = []
155+
156+
let passedStart = false
157+
let seenLastRundown = false
158+
159+
for (const s of segments) {
160+
if (
161+
(!passedStart &&
162+
((startMarker.type === QuickLoopMarkerType.PART && s.getPart(startMarker.id)) ||
163+
(startMarker.type === QuickLoopMarkerType.SEGMENT && s.segment._id === startMarker.id) ||
164+
(startMarker.type === QuickLoopMarkerType.RUNDOWN &&
165+
s.segment.rundownId === startMarker.id))) ||
166+
startMarker.type === QuickLoopMarkerType.PLAYLIST
167+
) {
168+
// the start marker is inside this segment, is this segment, or this is the first segment that is in the loop
169+
// segments from here on are included in the loop
170+
passedStart = true
171+
}
172+
173+
if (endMarker.type === QuickLoopMarkerType.RUNDOWN) {
174+
// last rundown needs to be inclusive so we need to break once the rundownId is not equal to segment's rundownId
175+
if (s.segment.rundownId === endMarker.id) {
176+
if (!passedStart) {
177+
// we hit the end before the start so quit now:
178+
break
179+
}
180+
seenLastRundown = true
181+
} else if (seenLastRundown) {
182+
// we have passed the last rundown
183+
break
184+
}
185+
}
186+
187+
if (passedStart) {
188+
// passed the start but we have not seen the end yet
189+
segmentIds.push(s.segment._id)
190+
}
191+
192+
if (
193+
(endMarker.type === QuickLoopMarkerType.PART && s.getPart(endMarker.id)) ||
194+
(endMarker.type === QuickLoopMarkerType.SEGMENT && s.segment._id === endMarker.id)
195+
) {
196+
// the endMarker is in this segment or this segment is the end marker
197+
break
198+
}
199+
}
200+
201+
return segmentIds
202+
}
203+
152204
private areMarkersFlipped(startPosition: MarkerPosition, endPosition: MarkerPosition) {
153205
return compareMarkerPositions(startPosition, endPosition) < 0
154206
}

packages/job-worker/src/playout/quickLoopMarkers.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { runJobWithPlayoutModel } from './lock'
55
import { updateTimeline } from './timeline/generate'
66
import { selectNextPart } from './selectNextPart'
77
import { setNextPart } from './setNext'
8+
import { resetPartInstancesWithPieceInstances } from './lib'
9+
import { QuickLoopMarker, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
10+
import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids'
11+
import { clone } from 'underscore'
812

913
export async function handleSetQuickLoopMarker(context: JobContext, data: SetQuickLoopMarkerProps): Promise<void> {
1014
return runJobWithPlayoutModel(
@@ -17,9 +21,74 @@ export async function handleSetQuickLoopMarker(context: JobContext, data: SetQui
1721
async (playoutModel) => {
1822
const playlist = playoutModel.playlist
1923
if (!playlist.activationId) throw new Error(`Playlist has no activationId!`)
20-
const wasQuickLoopRunning = playoutModel.playlist.quickLoop?.running
24+
const oldProps = clone(playoutModel.playlist.quickLoop)
25+
const wasQuickLoopRunning = oldProps?.running
2126
playoutModel.setQuickLoopMarker(data.type, data.marker)
2227

28+
const markerChanged = (
29+
markerA: QuickLoopMarker | undefined,
30+
markerB: QuickLoopMarker | undefined
31+
): boolean => {
32+
if (!markerA || !markerB) return false
33+
34+
if (
35+
(markerA.type === QuickLoopMarkerType.RUNDOWN ||
36+
markerA.type === QuickLoopMarkerType.SEGMENT ||
37+
markerA.type === QuickLoopMarkerType.PART) &&
38+
(markerB.type === QuickLoopMarkerType.RUNDOWN ||
39+
markerB.type === QuickLoopMarkerType.SEGMENT ||
40+
markerB.type === QuickLoopMarkerType.PART)
41+
) {
42+
return markerA.id !== markerB.id
43+
}
44+
45+
return false
46+
}
47+
48+
if (playlist.currentPartInfo) {
49+
// rundown is on air
50+
let segmentsToReset: SegmentId[] = []
51+
52+
if (
53+
playlist.quickLoop?.start &&
54+
oldProps?.start &&
55+
markerChanged(oldProps.start, playlist.quickLoop.start)
56+
) {
57+
// start marker changed
58+
segmentsToReset = playoutModel.getSegmentsBetweenQuickLoopMarker(
59+
playlist.quickLoop.start,
60+
oldProps.start
61+
)
62+
} else if (
63+
playlist.quickLoop?.end &&
64+
oldProps?.end &&
65+
markerChanged(oldProps.end, playlist.quickLoop.end)
66+
) {
67+
// end marker changed
68+
segmentsToReset = playoutModel.getSegmentsBetweenQuickLoopMarker(
69+
oldProps.end,
70+
playlist.quickLoop.end
71+
)
72+
} else if (playlist.quickLoop?.start && playlist.quickLoop.end && !(oldProps?.start && oldProps.end)) {
73+
// a new loop was created
74+
segmentsToReset = playoutModel.getSegmentsBetweenQuickLoopMarker(
75+
playlist.quickLoop.start,
76+
playlist.quickLoop.end
77+
)
78+
}
79+
80+
// reset segments that have been added to the loop and are not on-air
81+
resetPartInstancesWithPieceInstances(context, playoutModel, {
82+
segmentId: {
83+
$in: segmentsToReset.filter(
84+
(segmentId) =>
85+
segmentId !== playoutModel.currentPartInstance?.partInstance.segmentId &&
86+
segmentId !== playoutModel.nextPartInstance?.partInstance.segmentId
87+
),
88+
},
89+
})
90+
}
91+
2392
if (wasQuickLoopRunning) {
2493
const nextPart = selectNextPart(
2594
context,

packages/webui/src/client/lib/rundownTiming.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ export class RundownTimingCalculator {
107107
let remainingRundownDuration = 0
108108
let asPlayedRundownDuration = 0
109109
let asDisplayedRundownDuration = 0
110+
// the "wait" for a part is defined as its asPlayedDuration or its displayDuration or its expectedDuration
111+
const waitPerPart: Record<string, number> = {}
110112
let waitAccumulator = 0
111113
let currentRemaining = 0
112114
let startsAtAccumulator = 0
@@ -438,10 +440,13 @@ export class RundownTimingCalculator {
438440
0
439441
}
440442
if (segmentUsesBudget) {
441-
waitAccumulator += Math.min(waitDuration, Math.max(segmentBudgetDurationLeft, 0))
443+
const wait = Math.min(waitDuration, Math.max(segmentBudgetDurationLeft, 0))
444+
waitAccumulator += wait
442445
segmentBudgetDurationLeft -= waitDuration
446+
waitPerPart[unprotectString(partId)] = wait + Math.max(0, segmentBudgetDurationLeft)
443447
} else {
444448
waitAccumulator += waitDuration
449+
waitPerPart[unprotectString(partId)] = waitDuration
445450
}
446451

447452
// remaining is the sum of unplayed lines + whatever is left of the current segment
@@ -482,7 +487,9 @@ export class RundownTimingCalculator {
482487
})
483488

484489
// This is where the waitAccumulator-generated data in the linearSegLines is used to calculate the countdowns.
490+
// at this point the "waitAccumulator" should be the total sum of all the "waits" in the rundown
485491
let localAccum = 0
492+
let timeTillEndLoop: undefined | number = undefined
486493
for (let i = 0; i < this.linearParts.length; i++) {
487494
if (i < nextAIndex) {
488495
// this is a line before next line
@@ -519,6 +526,11 @@ export class RundownTimingCalculator {
519526
// and add the currentRemaining countdown, since we are currentRemaining + diff between next and
520527
// this away from this line.
521528
this.linearParts[i][1] = (this.linearParts[i][1] || 0) - localAccum + currentRemaining
529+
530+
if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])]) {
531+
timeTillEndLoop = timeTillEndLoop ?? this.linearParts[i][1] ?? undefined
532+
}
533+
522534
if (nextRundownAnchor === undefined) {
523535
nextRundownAnchor = getSegmentRundownAnchorFromPart(
524536
this.linearParts[i][0],
@@ -529,13 +541,22 @@ export class RundownTimingCalculator {
529541
}
530542
}
531543
}
532-
// contiunation of linearParts calculations for looping playlists
544+
// at this point the localAccumulator should be the sum of waits before the next line
545+
// continuation of linearParts calculations for looping playlists
533546
if (isLoopRunning(playlist)) {
547+
// we track the sum of all the "waits" that happen in the loop
548+
let waitInLoop = 0
549+
// if timeTillEndLoop was undefined then we can assume the end of the loop is the last line in the rundown
550+
timeTillEndLoop = timeTillEndLoop ?? waitAccumulator - localAccum + currentRemaining
534551
for (let i = 0; i < nextAIndex; i++) {
535552
if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])]) continue
536-
// offset the parts before the on air line by the countdown for the end of the rundown
537-
this.linearParts[i][1] =
538-
(this.linearParts[i][1] || 0) + waitAccumulator - localAccum + currentRemaining
553+
554+
// this countdown is the wait until the loop ends + whatever waits occur before this part but inside the loop
555+
this.linearParts[i][1] = timeTillEndLoop + waitInLoop
556+
557+
// add the wait from this part to the waitInLoop (the lookup here should still work by the definition of a "wait")
558+
waitInLoop += waitPerPart[unprotectString(this.linearParts[i][0])] ?? 0
559+
539560
if (nextRundownAnchor === undefined) {
540561
nextRundownAnchor = getSegmentRundownAnchorFromPart(
541562
this.linearParts[i][0],

0 commit comments

Comments
 (0)