Skip to content

Commit 2165350

Browse files
authored
Merge pull request Sofie-Automation#1704 from nrkno/contrib/fix/looping-rundown
2 parents 0e54356 + f0201af commit 2165350

8 files changed

Lines changed: 128 additions & 27 deletions

File tree

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

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { JSONBlobStringify, PieceLifespan, StatusCode } from '@sofie-automation/blueprints-integration'
1+
import {
2+
ForceQuickLoopAutoNext,
3+
JSONBlobStringify,
4+
PieceLifespan,
5+
StatusCode,
6+
} from '@sofie-automation/blueprints-integration'
27
import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece'
38
import {
49
PartInstanceId,
@@ -14,6 +19,7 @@ import {
1419
} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice'
1520
import { EmptyPieceTimelineObjectsBlob, Piece } from '@sofie-automation/corelib/dist/dataModel/Piece'
1621
import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance'
22+
import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
1723
import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown'
1824
import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece'
1925
import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment'
@@ -178,6 +184,76 @@ describe('PlayoutModelImpl', () => {
178184
})
179185
})
180186
})
187+
188+
describe('quickLoop for locked playlist loops', () => {
189+
it('keeps the quickLoop locked state on activation/reset/deactivation', async () => {
190+
const rundownId = protectString('rundown01')
191+
const playlistId = await context.mockCollections.RundownPlaylists.insertOne({
192+
...defaultRundownPlaylist(protectString(`playlist_${rundownId}`), context.studioId),
193+
quickLoop: {
194+
start: { type: QuickLoopMarkerType.PLAYLIST },
195+
end: { type: QuickLoopMarkerType.PLAYLIST },
196+
locked: true,
197+
running: false,
198+
forceAutoNext: ForceQuickLoopAutoNext.DISABLED,
199+
},
200+
})
201+
202+
const playlist = await context.mockCollections.RundownPlaylists.findOne(playlistId)
203+
204+
const rundown: DBRundown = defaultRundown(
205+
unprotectString(rundownId),
206+
context.studioId,
207+
null,
208+
playlistId,
209+
showStyleCompound._id,
210+
showStyleCompound.showStyleVariantId
211+
)
212+
rundown._id = rundownId
213+
await context.mockCollections.Rundowns.insertOne(rundown)
214+
215+
const peripheralDevices = [setupMockPlayoutGateway(protectString('playoutGateway0'))]
216+
217+
const { partInstances, groupedPieceInstances, rundowns } = await getPlayoutModelImplArugments(
218+
context,
219+
playlistId,
220+
rundownId
221+
)
222+
223+
if (!playlist) throw new Error('Playlist not found!')
224+
225+
await runWithPlaylistLock(context, playlistId, async (lock) => {
226+
const model = new PlayoutModelImpl(
227+
context,
228+
lock,
229+
playlistId,
230+
peripheralDevices,
231+
playlist,
232+
partInstances,
233+
groupedPieceInstances,
234+
rundowns,
235+
undefined
236+
)
237+
238+
const activationId = model.activatePlaylist(false)
239+
expect(model.playlist.quickLoop?.locked).toBe(true)
240+
expect(model.playlist.quickLoop?.start?.type).toBe(QuickLoopMarkerType.PLAYLIST)
241+
expect(model.playlist.quickLoop?.end?.type).toBe(QuickLoopMarkerType.PLAYLIST)
242+
243+
model.resetPlaylist(true)
244+
expect(model.playlist.quickLoop?.locked).toBe(true)
245+
expect(model.playlist.quickLoop?.start?.type).toBe(QuickLoopMarkerType.PLAYLIST)
246+
expect(model.playlist.quickLoop?.end?.type).toBe(QuickLoopMarkerType.PLAYLIST)
247+
248+
model.deactivatePlaylist()
249+
expect(model.playlist.quickLoop?.locked).toBe(true)
250+
expect(model.playlist.quickLoop?.start?.type).toBe(QuickLoopMarkerType.PLAYLIST)
251+
expect(model.playlist.quickLoop?.end?.type).toBe(QuickLoopMarkerType.PLAYLIST)
252+
expect(model.playlist.activationId).not.toBe(activationId)
253+
expect(model.playlist.currentPartInfo).toBeNull()
254+
})
255+
})
256+
})
181257
})
182258

183259
async function getPlayoutModelImplArugments(

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,13 @@ export class QuickLoopService {
142142
}
143143

144144
getUpdatedPropsByClearingMarkers(): QuickLoopProps | undefined {
145-
if (!this.playoutModel.playlist.quickLoop || this.playoutModel.playlist.quickLoop.locked) return undefined
145+
if (!this.playoutModel.playlist.quickLoop) return undefined
146+
147+
if (this.playoutModel.playlist.quickLoop.locked) {
148+
const quickLoopProps = clone(this.playoutModel.playlist.quickLoop)
149+
quickLoopProps.running = false
150+
return quickLoopProps
151+
}
146152

147153
const quickLoopProps = clone(this.playoutModel.playlist.quickLoop)
148154
delete quickLoopProps.start

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ export class RundownTimingCalculator {
140140
let lastSegmentIds: { segmentId: SegmentId; segmentPlayoutId: SegmentPlayoutId } | undefined = undefined
141141
let nextRundownAnchor: number | undefined = undefined
142142

143+
const entirePlaylistIsLooping = isEntirePlaylistLooping(playlist)
144+
143145
if (playlist) {
144146
const breakProps = currentRundown ? this.getRundownsBeforeNextBreak(rundowns, currentRundown) : undefined
145147

@@ -509,7 +511,7 @@ export class RundownTimingCalculator {
509511
// this is a line before next line
510512
localAccum = this.linearParts[i][1] || 0
511513
// only null the values if not looping, if looping, these will be offset by the countdown for the last part
512-
if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])]) {
514+
if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistIsLooping) {
513515
this.linearParts[i][1] = null // we use null to express 'will not probably be played out, if played in order'
514516
}
515517
} else if (i === currentAIndex) {
@@ -541,7 +543,7 @@ export class RundownTimingCalculator {
541543
// this away from this line.
542544
this.linearParts[i][1] = (this.linearParts[i][1] || 0) - localAccum + currentRemaining
543545

544-
if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])]) {
546+
if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistIsLooping) {
545547
timeTillEndLoop = timeTillEndLoop ?? this.linearParts[i][1] ?? undefined
546548
}
547549

@@ -563,7 +565,7 @@ export class RundownTimingCalculator {
563565
// if timeTillEndLoop was undefined then we can assume the end of the loop is the last line in the rundown
564566
timeTillEndLoop = timeTillEndLoop ?? waitAccumulator - localAccum + currentRemaining
565567
for (let i = 0; i < nextAIndex; i++) {
566-
if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])]) continue
568+
if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistIsLooping) continue
567569

568570
// this countdown is the wait until the loop ends + whatever waits occur before this part but inside the loop
569571
this.linearParts[i][1] = timeTillEndLoop + waitInLoop

packages/webui/src/client/ui/SegmentList/LinePart.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface IProps {
3131
isQuickLoopEnd: boolean
3232
// isLastSegment?: boolean
3333
// isLastPartInSegment?: boolean
34+
isEntirePlaylistLooping: boolean
3435
isPlaylistLooping: boolean
3536
indicatorColumns: Record<string, ISourceLayerExtended[]>
3637
adLibIndicatorColumns: Record<string, ISourceLayerExtended[]>
@@ -57,6 +58,7 @@ export function LinePart({
5758
indicatorColumns,
5859
adLibIndicatorColumns,
5960
isPlaylistLooping,
61+
isEntirePlaylistLooping,
6062
isQuickLoopStart,
6163
isQuickLoopEnd,
6264
onContextMenu,
@@ -80,7 +82,8 @@ export function LinePart({
8082

8183
const timingId = getPartInstanceTimingId(part.instance)
8284
const isInsideQuickLoop = (timingDurations.partsInQuickLoop || {})[timingId]
83-
const isOutsideActiveQuickLoop = isPlaylistLooping && !isInsideQuickLoop && !isNextPart && !hasAlreadyPlayed
85+
const isOutsideActiveQuickLoop =
86+
isPlaylistLooping && !isInsideQuickLoop && !isEntirePlaylistLooping && !isNextPart && !hasAlreadyPlayed
8487

8588
const getPartContext = useCallback(() => {
8689
const partElement = document.querySelector('#' + SegmentTimelinePartElementId + part.instance._id)

packages/webui/src/client/ui/SegmentList/SegmentList.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ import { NoteSeverity } from '@sofie-automation/blueprints-integration'
1818
import { PieceUi } from '@sofie-automation/corelib/src/dataModel/Piece.js'
1919
import { ISourceLayerExtended } from '@sofie-automation/corelib/src/dataModel/ShowStyleBase.js'
2020
import {
21-
isLoopRunning,
22-
isQuickLoopStart,
23-
isQuickLoopEnd,
21+
isLoopRunning as getIsLoopRunning,
22+
isQuickLoopStart as getIsQuickLoopStart,
23+
isQuickLoopEnd as getIsQuickLoopEnd,
24+
isEntirePlaylistLooping as getIsEntirePlaylistLooping,
2425
} from '@sofie-automation/corelib/src/playout/stateCacheResolver.js'
2526

2627
interface IProps {
@@ -139,6 +140,9 @@ const SegmentListInner = React.forwardRef<HTMLDivElement, IProps>(function Segme
139140
// if (isLivePart) currentPartIndex = index
140141
// if (isNextPart) nextPartIndex = index
141142

143+
const isPlaylistLooping = getIsLoopRunning(props.playlist)
144+
const isEntirePlaylistLooping = getIsEntirePlaylistLooping(props.playlist)
145+
142146
if (part.instance.part.invalid && part.instance.part.gap) return null
143147

144148
const partComponent = (
@@ -161,9 +165,10 @@ const SegmentListInner = React.forwardRef<HTMLDivElement, IProps>(function Segme
161165
doesPlaylistHaveNextPart={playlistHasNextPart}
162166
onPieceDoubleClick={props.onPieceDoubleClick}
163167
onContextMenu={props.onContextMenu}
164-
isPlaylistLooping={isLoopRunning(props.playlist)}
165-
isQuickLoopStart={isQuickLoopStart(part.partId, props.playlist)}
166-
isQuickLoopEnd={isQuickLoopEnd(part.partId, props.playlist)}
168+
isPlaylistLooping={isPlaylistLooping}
169+
isEntirePlaylistLooping={isEntirePlaylistLooping}
170+
isQuickLoopStart={getIsQuickLoopStart(part.partId, props.playlist)}
171+
isQuickLoopEnd={getIsQuickLoopEnd(part.partId, props.playlist)}
167172
/>
168173
)
169174

packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ import { logger } from '../../lib/logging.js'
4242
import { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js'
4343
import { PieceUi } from '@sofie-automation/corelib/src/dataModel/Piece.js'
4444
import {
45-
isLoopRunning,
46-
isEndOfLoopingShow,
47-
isQuickLoopStart,
48-
isQuickLoopEnd,
45+
isLoopRunning as getIsLoopRunning,
46+
isEndOfLoopingShow as getIsEndOfLoopingShow,
47+
isQuickLoopStart as getIsQuickLoopStart,
48+
isQuickLoopEnd as getIsQuickLoopEnd,
49+
isEntirePlaylistLooping as getIsEntirePlaylistLooping,
4950
} from '@sofie-automation/corelib/src/playout/stateCacheResolver.js'
5051

5152
interface IProps {
@@ -206,7 +207,8 @@ export const SegmentStoryboard = React.memo(
206207
squishedPartsNum > 1 ? Math.max(4, (spaceLeft - PART_WIDTH) / (squishedPartsNum - 1)) : null
207208

208209
const playlistHasNextPart = !!props.playlist.nextPartInfo
209-
const playlistIsLooping = isLoopRunning(props.playlist)
210+
const isPlaylistLooping = getIsLoopRunning(props.playlist)
211+
const isEntirePlaylistLooping = getIsEntirePlaylistLooping(props.playlist)
210212

211213
renderedParts.forEach((part, index) => {
212214
const isLivePart = part.instance._id === props.playlist.currentPartInfo?.partInstanceId
@@ -227,15 +229,16 @@ export const SegmentStoryboard = React.memo(
227229
isNextPart={isNextPart}
228230
isLastPartInSegment={part.instance._id === lastValidPartId}
229231
isLastSegment={props.isLastSegment}
230-
isEndOfLoopingShow={isEndOfLoopingShow(
232+
isEndOfLoopingShow={getIsEndOfLoopingShow(
231233
props.playlist,
232234
props.isLastSegment,
233235
part.instance._id === lastValidPartId,
234236
part.instance.part
235237
)}
236-
isQuickLoopStart={isQuickLoopStart(part.partId, props.playlist)}
237-
isQuickLoopEnd={isQuickLoopEnd(part.partId, props.playlist)}
238-
isPlaylistLooping={playlistIsLooping}
238+
isQuickLoopStart={getIsQuickLoopStart(part.partId, props.playlist)}
239+
isQuickLoopEnd={getIsQuickLoopEnd(part.partId, props.playlist)}
240+
isPlaylistLooping={isPlaylistLooping}
241+
isEntirePlaylistLooping={isEntirePlaylistLooping}
239242
doesPlaylistHaveNextPart={playlistHasNextPart}
240243
displayLiveLineCounter={props.displayLiveLineCounter}
241244
inHold={!!(props.playlist.holdState && props.playlist.holdState !== RundownHoldState.COMPLETE)}

packages/webui/src/client/ui/SegmentStoryboard/StoryboardPart.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ interface IProps {
3434
isLastSegment?: boolean
3535
isLastPartInSegment?: boolean
3636
isPlaylistLooping?: boolean
37+
isEntirePlaylistLooping?: boolean
3738
isEndOfLoopingShow?: boolean
3839
isQuickLoopStart: boolean
3940
isQuickLoopEnd: boolean
@@ -56,6 +57,7 @@ export function StoryboardPart({
5657
isLastPartInSegment,
5758
isLastSegment,
5859
isPlaylistLooping,
60+
isEntirePlaylistLooping,
5961
isEndOfLoopingShow,
6062
isQuickLoopStart,
6163
isQuickLoopEnd,
@@ -127,7 +129,7 @@ export function StoryboardPart({
127129
const isInvalid = part.instance.part.invalid
128130
const isFloated = part.instance.part.floated
129131
const isInsideQuickLoop = timingDurations.partsInQuickLoop?.[getPartInstanceTimingId(part.instance)] ?? false
130-
const isOutsideActiveQuickLoop = !isInsideQuickLoop && isPlaylistLooping && !isNextPart
132+
const isOutsideActiveQuickLoop = !isInsideQuickLoop && isPlaylistLooping && !isEntirePlaylistLooping && !isNextPart
131133

132134
return (
133135
<ContextMenuTrigger

packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ import { Events as MOSEvents } from '../../../lib/data/mos/plugin-support.js'
4242
import { UIStudio } from '@sofie-automation/corelib/src/dataModel/Studio.js'
4343
import { PieceUi } from '@sofie-automation/corelib/src/dataModel/Piece.js'
4444
import {
45-
isLoopRunning,
45+
isLoopRunning as getIsLoopRunning,
4646
isQuickLoopStart as getIsQuickLoopStart,
4747
isQuickLoopEnd as getIsQuickLoopEnd,
48-
isEndOfLoopingShow,
48+
isEndOfLoopingShow as getIsEndOfLoopingShow,
49+
isEntirePlaylistLooping as getIsEntirePlaylistLooping,
4950
} from '@sofie-automation/corelib/src/playout/stateCacheResolver.js'
5051

5152
export const SegmentTimelineLineElementId = 'rundown__segment__line__'
@@ -673,8 +674,8 @@ export class SegmentTimelinePartClass extends React.Component<Translated<WithTim
673674
this.props.isLastSegment &&
674675
this.props.isLastInSegment &&
675676
(!this.state.isLive || (this.state.isLive && !this.props.playlist.nextPartInfo))
676-
const isPlaylistLooping = isLoopRunning(this.props.playlist)
677-
const isPartEndOfLoopingShow = isEndOfLoopingShow(
677+
const isPlaylistLooping = getIsLoopRunning(this.props.playlist)
678+
const isPartEndOfLoopingShow = getIsEndOfLoopingShow(
678679
this.props.playlist,
679680
this.props.isLastSegment,
680681
this.props.isLastInSegment,
@@ -692,7 +693,10 @@ export class SegmentTimelinePartClass extends React.Component<Translated<WithTim
692693
}
693694

694695
const isOutsideActiveQuickLoop =
695-
!this.state.isInQuickLoop && isLoopRunning(this.props.playlist) && !this.state.isNext
696+
!this.state.isInQuickLoop &&
697+
getIsLoopRunning(this.props.playlist) &&
698+
!getIsEntirePlaylistLooping(this.props.playlist) &&
699+
!this.state.isNext
696700
const isQuickLoopStart = getIsQuickLoopStart(this.props.part.partId, this.props.playlist)
697701
const isQuickLoopEnd = getIsQuickLoopEnd(this.props.part.partId, this.props.playlist)
698702

0 commit comments

Comments
 (0)