Skip to content

Commit 56f1399

Browse files
committed
Merge remote-tracking branch 'upstream/release52' into upstream/segment-timing
2 parents a1d1f42 + f8a1d75 commit 56f1399

File tree

9 files changed

+230
-12
lines changed

9 files changed

+230
-12
lines changed

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

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

347+
/**
348+
* Returns any segmentId's that are found between 2 quickloop markers, none will be returned if
349+
* the end is before the start.
350+
* @param start A quickloop marker
351+
* @param end A quickloop marker
352+
*/
353+
getSegmentsBetweenQuickLoopMarker(start: QuickLoopMarker, end: QuickLoopMarker): SegmentId[]
354+
347355
calculatePartTimings(
348356
fromPartInstance: PlayoutPartInstanceModel | null,
349357
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
@@ -594,7 +594,11 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
594594

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

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

599603
this.#playlistHasChanged = true
600604
}
@@ -793,6 +797,10 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
793797
this.#playlistHasChanged = true
794798
}
795799

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

798806
/** @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/live-status-gateway/api/schemas/activePlaylist.yaml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,32 @@ $defs:
2929
$ref: '#/$defs/part'
3030
publicData:
3131
description: Optional arbitrary data
32-
required: [event, id, name, rundownIds, currentPart, currentSegment, nextPart]
32+
timing:
33+
description: Timing information about the active playlist
34+
type: object
35+
properties:
36+
timingMode:
37+
description: 'Timing mode for the playlist.'
38+
type: string
39+
enum:
40+
- none
41+
- forward-time
42+
- back-time
43+
startedPlayback:
44+
description: Unix timestamp of when the playlist started (milliseconds)
45+
type: number
46+
expectedStart:
47+
description: Unix timestamp of when the playlist is expected to start (milliseconds). Required when the timingMode is set to forward-time.
48+
type: number
49+
expectedDurationMs:
50+
description: Duration of the playlist in ms
51+
type: number
52+
expectedEnd:
53+
description: Unix timestamp of when the playlist is expected to end (milliseconds) Required when the timingMode is set to back-time.
54+
type: number
55+
required: [timingMode]
56+
additionalProperties: false
57+
required: [event, id, name, rundownIds, currentPart, currentSegment, nextPart, timing]
3358
additionalProperties: false
3459
examples:
3560
- event: activePlaylist
@@ -44,6 +69,10 @@ $defs:
4469
$ref: '#/$defs/part/examples/0'
4570
publicData:
4671
category: 'Evening News'
72+
timing:
73+
timingMode: 'forward-time'
74+
expectedStart: 1728895750727
75+
expectedDurationMs: 180000
4776
partBase:
4877
type: object
4978
properties:

packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
1212
import { SegmentHandler } from '../../collections/segmentHandler'
1313
import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment'
1414
import { CountdownType } from '@sofie-automation/blueprints-integration'
15+
import { PlaylistTimingType } from '@sofie-automation/blueprints-integration'
1516

1617
function makeEmptyTestPartInstances(): SelectedPartInstances {
1718
return {
@@ -49,6 +50,9 @@ describe('ActivePlaylistTopic', () => {
4950
currentSegment: null,
5051
rundownIds: unprotectStringArray(playlist.rundownIdsInOrder),
5152
publicData: undefined,
53+
timing: {
54+
timingMode: PlaylistTimingType.None,
55+
},
5256
}
5357

5458
// eslint-disable-next-line @typescript-eslint/unbound-method
@@ -139,6 +143,9 @@ describe('ActivePlaylistTopic', () => {
139143
},
140144
rundownIds: unprotectStringArray(playlist.rundownIdsInOrder),
141145
publicData: { a: 'b' },
146+
timing: {
147+
timingMode: PlaylistTimingType.None,
148+
},
142149
}
143150

144151
// eslint-disable-next-line @typescript-eslint/unbound-method

packages/live-status-gateway/src/topics/activePlaylistTopic.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { SelectedPieceInstances, PieceInstancesHandler, PieceInstanceMin } from
1717
import { PieceStatus, toPieceStatus } from './helpers/pieceStatus'
1818
import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment'
1919
import { SegmentHandler } from '../collections/segmentHandler'
20+
import { PlaylistTimingType } from '@sofie-automation/blueprints-integration'
2021

2122
const THROTTLE_PERIOD_MS = 100
2223

@@ -47,6 +48,13 @@ export interface ActivePlaylistStatus {
4748
currentSegment: CurrentSegmentStatus | null
4849
nextPart: PartStatus | null
4950
publicData: unknown
51+
timing: {
52+
timingMode: PlaylistTimingType
53+
startedPlayback?: number
54+
expectedStart?: number
55+
expectedDurationMs?: number
56+
expectedEnd?: number
57+
}
5058
}
5159

5260
export class ActivePlaylistTopic
@@ -146,6 +154,19 @@ export class ActivePlaylistTopic
146154
})
147155
: null,
148156
publicData: this._activePlaylist.publicData,
157+
timing: {
158+
timingMode: this._activePlaylist.timing.type,
159+
startedPlayback: this._activePlaylist.startedPlayback,
160+
expectedDurationMs: this._activePlaylist.timing.expectedDuration,
161+
expectedStart:
162+
this._activePlaylist.timing.type !== PlaylistTimingType.None
163+
? this._activePlaylist.timing.expectedStart
164+
: undefined,
165+
expectedEnd:
166+
this._activePlaylist.timing.type !== PlaylistTimingType.None
167+
? this._activePlaylist.timing.expectedEnd
168+
: undefined,
169+
},
149170
})
150171
: literal<ActivePlaylistStatus>({
151172
event: 'activePlaylist',
@@ -156,6 +177,9 @@ export class ActivePlaylistTopic
156177
currentSegment: null,
157178
nextPart: null,
158179
publicData: undefined,
180+
timing: {
181+
timingMode: PlaylistTimingType.None,
182+
},
159183
})
160184

161185
this.sendMessage(subscribers, message)

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
@@ -436,10 +438,13 @@ export class RundownTimingCalculator {
436438
0
437439
}
438440
if (segmentUsesBudget) {
439-
waitAccumulator += Math.min(waitDuration, Math.max(segmentBudgetDurationLeft, 0))
441+
const wait = Math.min(waitDuration, Math.max(segmentBudgetDurationLeft, 0))
442+
waitAccumulator += wait
440443
segmentBudgetDurationLeft -= waitDuration
444+
waitPerPart[unprotectString(partId)] = wait + Math.max(0, segmentBudgetDurationLeft)
441445
} else {
442446
waitAccumulator += waitDuration
447+
waitPerPart[unprotectString(partId)] = waitDuration
443448
}
444449

445450
// remaining is the sum of unplayed lines + whatever is left of the current segment
@@ -480,7 +485,9 @@ export class RundownTimingCalculator {
480485
})
481486

482487
// This is where the waitAccumulator-generated data in the linearSegLines is used to calculate the countdowns.
488+
// at this point the "waitAccumulator" should be the total sum of all the "waits" in the rundown
483489
let localAccum = 0
490+
let timeTillEndLoop: undefined | number = undefined
484491
for (let i = 0; i < this.linearParts.length; i++) {
485492
if (i < nextAIndex) {
486493
// this is a line before next line
@@ -517,6 +524,11 @@ export class RundownTimingCalculator {
517524
// and add the currentRemaining countdown, since we are currentRemaining + diff between next and
518525
// this away from this line.
519526
this.linearParts[i][1] = (this.linearParts[i][1] || 0) - localAccum + currentRemaining
527+
528+
if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])]) {
529+
timeTillEndLoop = timeTillEndLoop ?? this.linearParts[i][1] ?? undefined
530+
}
531+
520532
if (nextRundownAnchor === undefined) {
521533
nextRundownAnchor = getSegmentRundownAnchorFromPart(
522534
this.linearParts[i][0],
@@ -527,13 +539,22 @@ export class RundownTimingCalculator {
527539
}
528540
}
529541
}
530-
// contiunation of linearParts calculations for looping playlists
542+
// at this point the localAccumulator should be the sum of waits before the next line
543+
// continuation of linearParts calculations for looping playlists
531544
if (isLoopRunning(playlist)) {
545+
// we track the sum of all the "waits" that happen in the loop
546+
let waitInLoop = 0
547+
// if timeTillEndLoop was undefined then we can assume the end of the loop is the last line in the rundown
548+
timeTillEndLoop = timeTillEndLoop ?? waitAccumulator - localAccum + currentRemaining
532549
for (let i = 0; i < nextAIndex; i++) {
533550
if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])]) continue
534-
// offset the parts before the on air line by the countdown for the end of the rundown
535-
this.linearParts[i][1] =
536-
(this.linearParts[i][1] || 0) + waitAccumulator - localAccum + currentRemaining
551+
552+
// this countdown is the wait until the loop ends + whatever waits occur before this part but inside the loop
553+
this.linearParts[i][1] = timeTillEndLoop + waitInLoop
554+
555+
// add the wait from this part to the waitInLoop (the lookup here should still work by the definition of a "wait")
556+
waitInLoop += waitPerPart[unprotectString(this.linearParts[i][0])] ?? 0
557+
537558
if (nextRundownAnchor === undefined) {
538559
nextRundownAnchor = getSegmentRundownAnchorFromPart(
539560
this.linearParts[i][0],

0 commit comments

Comments
 (0)