Skip to content

Commit bd3aeaa

Browse files
authored
Merge pull request Sofie-Automation#1354 from bbc/upstream/feat-move-next-in-quickloop
2 parents 462f20f + b87cd9f commit bd3aeaa

File tree

17 files changed

+197
-34
lines changed

17 files changed

+197
-34
lines changed

meteor/server/api/userActions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ class ServerUserActionAPI
191191
eventTime: Time,
192192
rundownPlaylistId: RundownPlaylistId,
193193
partDelta: number,
194-
segmentDelta: number
194+
segmentDelta: number,
195+
ignoreQuickLoop: boolean | null
195196
) {
196197
return ServerClientAPI.runUserActionInLogForPlaylistOnWorker(
197198
this,
@@ -208,6 +209,7 @@ class ServerUserActionAPI
208209
playlistId: rundownPlaylistId,
209210
partDelta: partDelta,
210211
segmentDelta: segmentDelta,
212+
ignoreQuickLoop: ignoreQuickLoop ?? undefined,
211213
}
212214
)
213215
}

meteor/server/migration/upgrades/defaultSystemActionTriggers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ export const DEFAULT_CORE_TRIGGERS: IBlueprintDefaultCoreSystemTriggers = {
281281
],
282282
parts: 1,
283283
segments: 0,
284+
ignoreQuickLoop: false,
284285
},
285286
},
286287
triggers: {
@@ -305,6 +306,7 @@ export const DEFAULT_CORE_TRIGGERS: IBlueprintDefaultCoreSystemTriggers = {
305306
],
306307
parts: 0,
307308
segments: 1,
309+
ignoreQuickLoop: false,
308310
},
309311
},
310312
triggers: {
@@ -329,6 +331,7 @@ export const DEFAULT_CORE_TRIGGERS: IBlueprintDefaultCoreSystemTriggers = {
329331
],
330332
parts: -1,
331333
segments: 0,
334+
ignoreQuickLoop: false,
332335
},
333336
},
334337
triggers: {
@@ -353,6 +356,7 @@ export const DEFAULT_CORE_TRIGGERS: IBlueprintDefaultCoreSystemTriggers = {
353356
],
354357
parts: 0,
355358
segments: -1,
359+
ignoreQuickLoop: false,
356360
},
357361
},
358362
triggers: {

packages/blueprints-integration/src/context/adlibActionContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export interface IActionExecutionContext
3131
// getNextShowStyleConfig(): Readonly<{ [key: string]: ConfigItemValue }>
3232

3333
/** Move the next part through the rundown. Can move by either a number of parts, or segments in either direction. */
34-
moveNextPart(partDelta: number, segmentDelta: number): Promise<void>
34+
moveNextPart(partDelta: number, segmentDelta: number, ignoreQuickloop?: boolean): Promise<void>
3535
/** Set flag to perform take after executing the current action. Returns state of the flag after each call. */
3636
takeAfterExecuteAction(take: boolean): Promise<boolean>
3737
/** Inform core that a take out of the current partinstance should be blocked until the specified time */

packages/blueprints-integration/src/context/onSetAsNextContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,5 @@ export interface IOnSetAsNextContext extends IShowStyleUserContext, IEventContex
7676
* Multiple calls of this inside one call to `onSetAsNext` will replace earlier calls.
7777
* @returns Whether a new Part was found using the provided offset
7878
*/
79-
moveNextPart(partDelta: number, segmentDelta: number): Promise<boolean>
79+
moveNextPart(partDelta: number, segmentDelta: number, ignoreQuickLoop?: boolean): Promise<boolean>
8080
}

packages/blueprints-integration/src/triggers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ export interface IMoveNextAction extends ITriggeredActionBase {
230230
* @memberof IMoveNextAction
231231
*/
232232
parts: number
233+
/**
234+
* When moving the next part it should ignore any of the boundaries set by the QuickLoop feature
235+
*
236+
* @type {boolean}
237+
* @memberof IMoveNextAction
238+
*/
239+
ignoreQuickLoop: boolean
233240
}
234241

235242
export interface ICreateSnapshotForDebugAction extends ITriggeredActionBase {

packages/corelib/src/worker/studio.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ export interface StopPiecesOnSourceLayersProps extends RundownPlayoutPropsBase {
231231
export interface MoveNextPartProps extends RundownPlayoutPropsBase {
232232
partDelta: number
233233
segmentDelta: number
234+
ignoreQuickLoop?: boolean
234235
}
235236
export type ActivateHoldProps = RundownPlayoutPropsBase
236237
export type DeactivateHoldProps = RundownPlayoutPropsBase

packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class OnSetAsNextContext
121121
return this.partAndPieceInstanceService.removePieceInstances('next', pieceInstanceIds)
122122
}
123123

124-
async moveNextPart(partDelta: number, segmentDelta: number): Promise<boolean> {
124+
async moveNextPart(partDelta: number, segmentDelta: number, ignoreQuickLoop?: boolean): Promise<boolean> {
125125
if (typeof partDelta !== 'number') throw new Error('partDelta must be a number')
126126
if (typeof segmentDelta !== 'number') throw new Error('segmentDelta must be a number')
127127

@@ -132,7 +132,13 @@ export class OnSetAsNextContext
132132
}
133133

134134
this.pendingMoveNextPart = {
135-
selectedPart: selectNewPartWithOffsets(this.jobContext, this.playoutModel, partDelta, segmentDelta),
135+
selectedPart: selectNewPartWithOffsets(
136+
this.jobContext,
137+
this.playoutModel,
138+
partDelta,
139+
segmentDelta,
140+
ignoreQuickLoop
141+
),
136142
}
137143

138144
return !!this.pendingMoveNextPart.selectedPart

packages/job-worker/src/blueprints/context/adlibActions.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,14 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct
157157
return this.partAndPieceInstanceService.queuePart(rawPart, rawPieces)
158158
}
159159

160-
async moveNextPart(partDelta: number, segmentDelta: number): Promise<void> {
161-
const selectedPart = selectNewPartWithOffsets(this._context, this._playoutModel, partDelta, segmentDelta)
160+
async moveNextPart(partDelta: number, segmentDelta: number, ignoreQuickloop?: boolean): Promise<void> {
161+
const selectedPart = selectNewPartWithOffsets(
162+
this._context,
163+
this._playoutModel,
164+
partDelta,
165+
segmentDelta,
166+
ignoreQuickloop
167+
)
162168
if (selectedPart) await setNextPartFromPart(this._context, this._playoutModel, selectedPart, true)
163169
}
164170

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,25 @@ export interface PlayoutModelReadonly extends StudioPlayoutModelBaseReadonly {
167167
*/
168168
getRundownIds(): RundownId[]
169169

170+
/**
171+
* Returns any segmentId's that are found between 2 quickloop markers, none will be returned if
172+
* the end is before the start.
173+
* @param start A quickloop marker
174+
* @param end A quickloop marker
175+
*/
176+
getSegmentsBetweenQuickLoopMarker(start: QuickLoopMarker, end: QuickLoopMarker): SegmentId[]
177+
178+
/**
179+
* Returns any segmentId's that are found between 2 quickloop markers, none will be returned if
180+
* the end is before the start.
181+
* @param start A quickloop marker
182+
* @param end A quickloop marker
183+
*/
184+
getPartsBetweenQuickLoopMarker(
185+
start: QuickLoopMarker,
186+
end: QuickLoopMarker
187+
): { parts: PartId[]; segments: SegmentId[] }
188+
170189
/**
171190
* Search for a PieceInstance in the RundownPlaylist
172191
* @param id Id of the PieceInstance
@@ -350,14 +369,6 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa
350369
*/
351370
setQuickLoopMarker(type: 'start' | 'end', marker: QuickLoopMarker | null): void
352371

353-
/**
354-
* Returns any segmentId's that are found between 2 quickloop markers, none will be returned if
355-
* the end is before the start.
356-
* @param start A quickloop marker
357-
* @param end A quickloop marker
358-
*/
359-
getSegmentsBetweenQuickLoopMarker(start: QuickLoopMarker, end: QuickLoopMarker): SegmentId[]
360-
361372
calculatePartTimings(
362373
fromPartInstance: PlayoutPartInstanceModel | null,
363374
toPartInstance: PlayoutPartInstanceModel,

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,16 @@ export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly {
240240
return undefined
241241
}
242242

243+
getSegmentsBetweenQuickLoopMarker(start: QuickLoopMarker, end: QuickLoopMarker): SegmentId[] {
244+
return this.quickLoopService.getSegmentsBetweenMarkers(start, end)
245+
}
246+
getPartsBetweenQuickLoopMarker(
247+
start: QuickLoopMarker,
248+
end: QuickLoopMarker
249+
): { parts: PartId[]; segments: SegmentId[] } {
250+
return this.quickLoopService.getPartsBetweenMarkers(start, end)
251+
}
252+
243253
#isMultiGatewayMode: boolean | undefined = undefined
244254
public get isMultiGatewayMode(): boolean {
245255
if (this.#isMultiGatewayMode === undefined) {
@@ -828,10 +838,6 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
828838
this.#playlistHasChanged = true
829839
}
830840

831-
getSegmentsBetweenQuickLoopMarker(start: QuickLoopMarker, end: QuickLoopMarker): SegmentId[] {
832-
return this.quickLoopService.getSegmentsBetweenMarkers(start, end)
833-
}
834-
835841
/** Notifications */
836842

837843
async getAllNotifications(

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

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings'
99
import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep'
1010
import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
11-
import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids'
11+
import { PartId, 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'
@@ -150,6 +150,7 @@ export class QuickLoopService {
150150
}
151151

152152
getSegmentsBetweenMarkers(startMarker: QuickLoopMarker, endMarker: QuickLoopMarker): SegmentId[] {
153+
// note - this function could be refactored to call getPartsBetweenMarkers instead but it will be less efficient
153154
const segments = this.playoutModel.getAllOrderedSegments()
154155
const segmentIds: SegmentId[] = []
155156

@@ -201,6 +202,71 @@ export class QuickLoopService {
201202
return segmentIds
202203
}
203204

205+
getPartsBetweenMarkers(
206+
startMarker: QuickLoopMarker,
207+
endMarker: QuickLoopMarker
208+
): { parts: PartId[]; segments: SegmentId[] } {
209+
const parts = this.playoutModel.getAllOrderedParts()
210+
const segmentIds: SegmentId[] = []
211+
const partIds: PartId[] = []
212+
213+
let passedStart = false
214+
let seenLastRundown = false
215+
let seenLastSegment = false
216+
217+
for (const p of parts) {
218+
if (
219+
!passedStart &&
220+
((startMarker.type === QuickLoopMarkerType.PART && p._id === startMarker.id) ||
221+
(startMarker.type === QuickLoopMarkerType.SEGMENT && p.segmentId === startMarker.id) ||
222+
(startMarker.type === QuickLoopMarkerType.RUNDOWN && p.rundownId === startMarker.id) ||
223+
startMarker.type === QuickLoopMarkerType.PLAYLIST)
224+
) {
225+
// the start marker is this part, this is the first part in the loop, or this is the first segment that is in the loop
226+
// segments from here on are included in the loop
227+
passedStart = true
228+
}
229+
230+
if (endMarker.type === QuickLoopMarkerType.RUNDOWN) {
231+
// last rundown needs to be inclusive so we need to break once the rundownId is not equal to segment's rundownId
232+
if (p.rundownId === endMarker.id) {
233+
if (!passedStart) {
234+
// we hit the end before the start so quit now:
235+
break
236+
}
237+
seenLastRundown = true
238+
} else if (seenLastRundown) {
239+
// we have passed the last rundown
240+
break
241+
}
242+
} else if (endMarker.type === QuickLoopMarkerType.SEGMENT) {
243+
// last segment needs to be inclusive so we need to break once the segmentId changes but not before
244+
if (p.segmentId === endMarker.id) {
245+
if (!passedStart) {
246+
// we hit the end before the start so quit now:
247+
break
248+
}
249+
seenLastSegment = true
250+
} else if (seenLastSegment) {
251+
// we have passed the last rundown
252+
break
253+
}
254+
}
255+
256+
if (passedStart) {
257+
if (segmentIds.slice(-1)[0] !== p.segmentId) segmentIds.push(p.segmentId)
258+
partIds.push(p._id)
259+
}
260+
261+
if (endMarker.type === QuickLoopMarkerType.PART && p._id === endMarker.id) {
262+
// the endMarker is this part so we can quit now
263+
break
264+
}
265+
}
266+
267+
return { parts: partIds, segments: segmentIds }
268+
}
269+
204270
private areMarkersFlipped(startPosition: MarkerPosition, endPosition: MarkerPosition) {
205271
return compareMarkerPositions(startPosition, endPosition) < 0
206272
}

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

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export function selectNewPartWithOffsets(
1111
_context: JobContext,
1212
playoutModel: PlayoutModelReadonly,
1313
partDelta: number,
14-
segmentDelta: number
14+
segmentDelta: number,
15+
ignoreQuickLoop = false
1516
): ReadonlyDeep<DBPart> | null {
1617
const playlist = playoutModel.playlist
1718

@@ -23,8 +24,21 @@ export function selectNewPartWithOffsets(
2324
if (!refPart || !refPartInstance)
2425
throw new Error(`RundownPlaylist "${playlist._id}" has no next and no current part!`)
2526

26-
const rawSegments = playoutModel.getAllOrderedSegments()
27-
const rawParts = playoutModel.getAllOrderedParts()
27+
let rawSegments = playoutModel.getAllOrderedSegments()
28+
let rawParts = playoutModel.getAllOrderedParts()
29+
let allowWrap = false // whether we should wrap to the first part if the curIndex + delta exceeds the total number of parts
30+
31+
if (!ignoreQuickLoop && playlist.quickLoop?.start && playlist.quickLoop.end) {
32+
const partsInQuickloop = playoutModel.getPartsBetweenQuickLoopMarker(
33+
playlist.quickLoop.start,
34+
playlist.quickLoop.end
35+
)
36+
if (partsInQuickloop.parts.includes(refPart._id)) {
37+
rawParts = rawParts.filter((p) => partsInQuickloop.parts.includes(p._id))
38+
rawSegments = rawSegments.filter((s) => partsInQuickloop.segments.includes(s.segment._id))
39+
allowWrap = true
40+
}
41+
}
2842

2943
if (segmentDelta) {
3044
// Ignores horizontalDelta
@@ -37,7 +51,14 @@ export function selectNewPartWithOffsets(
3751
const refSegmentIndex = considerSegments.findIndex((s) => s.segment._id === refPart.segmentId)
3852
if (refSegmentIndex === -1) throw new Error(`Segment "${refPart.segmentId}" not found!`)
3953

40-
const targetSegmentIndex = refSegmentIndex + segmentDelta
54+
let targetSegmentIndex = refSegmentIndex + segmentDelta
55+
if (allowWrap) {
56+
targetSegmentIndex = targetSegmentIndex % considerSegments.length
57+
if (targetSegmentIndex < 0) {
58+
// -1 becomes last segment
59+
targetSegmentIndex = considerSegments.length + targetSegmentIndex
60+
}
61+
}
4162
const targetSegment = considerSegments[targetSegmentIndex]
4263
if (!targetSegment) return null
4364

@@ -64,7 +85,6 @@ export function selectNewPartWithOffsets(
6485
}
6586
}
6687

67-
// TODO - looping playlists
6888
if (selectedPart) {
6989
// Switch to that part
7090
return selectedPart
@@ -88,7 +108,11 @@ export function selectNewPartWithOffsets(
88108
}
89109

90110
// Get the past we are after
91-
const targetPartIndex = refPartIndex + partDelta
111+
let targetPartIndex = allowWrap ? (refPartIndex + partDelta) % playabaleParts.length : refPartIndex + partDelta
112+
if (allowWrap) {
113+
targetPartIndex = targetPartIndex % playabaleParts.length
114+
if (targetPartIndex < 0) targetPartIndex = playabaleParts.length + targetPartIndex // -1 becomes last part
115+
}
92116
let targetPart = playabaleParts[targetPartIndex]
93117
if (targetPart && targetPart._id === currentPartInstance?.part._id) {
94118
// Cant go to the current part (yet)

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,13 @@ export async function handleMoveNextPart(context: JobContext, data: MoveNextPart
6868
}
6969
},
7070
async (playoutModel) => {
71-
const selectedPart = selectNewPartWithOffsets(context, playoutModel, data.partDelta, data.segmentDelta)
71+
const selectedPart = selectNewPartWithOffsets(
72+
context,
73+
playoutModel,
74+
data.partDelta,
75+
data.segmentDelta,
76+
data.ignoreQuickLoop
77+
)
7278
if (!selectedPart) return null
7379

7480
await setNextPartFromPart(context, playoutModel, selectedPart, true)

packages/meteor-lib/src/api/userActions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ export interface NewUserActionAPI {
5858
eventTime: Time,
5959
rundownPlaylistId: RundownPlaylistId,
6060
partDelta: number,
61-
segmentDelta: number
61+
segmentDelta: number,
62+
ignoreQuickLoop?: boolean
6263
): Promise<ClientAPI.ClientResponse<PartId | null>>
6364
prepareForBroadcast(
6465
userEvent: string,

0 commit comments

Comments
 (0)