Skip to content

Commit 9387259

Browse files
committed
Merge branch 'feat--Reduce-ingest-visual-noise' into bbc-main-autonext
2 parents 7a45d83 + 51e8dba commit 9387259

20 files changed

Lines changed: 471 additions & 46 deletions

File tree

meteor/server/publications/partInstancesUI/publication.ts

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ export async function manipulateUIPartInstancesPublicationData(
136136
updateProps: Partial<ReadonlyDeep<UIPartInstancesUpdateProps>> | undefined
137137
): Promise<void> {
138138
// Prepare data for publication:
139+
// - With quickLoop invalidation: refresh all cached PartInstances.
140+
// - Without quickLoop invalidation: refresh only explicitly invalidated PartInstances and
141+
// those in explicitly invalidated segments.
139142

140143
if (updateProps?.newCache !== undefined) {
141144
state.contentCache = updateProps.newCache ?? undefined
@@ -186,25 +189,60 @@ export async function manipulateUIPartInstancesPublicationData(
186189
collection.remove(partId) // if it still exists, it will be replaced in the next step
187190
})
188191

192+
const invalidateQuickLoop = !!updateProps?.invalidateQuickLoop
193+
const invalidatedSegmentIds = updateProps?.invalidateSegmentIds ?? []
194+
const invalidatedPartInstanceIds = updateProps?.invalidatePartInstanceIds ?? []
195+
196+
if (!invalidateQuickLoop && invalidatedSegmentIds.length === 0 && invalidatedPartInstanceIds.length === 0) {
197+
return
198+
}
199+
189200
const invalidatedSegmentsSet = new Set(updateProps?.invalidateSegmentIds ?? [])
190201
const invalidatedPartInstancesSet = new Set(updateProps?.invalidatePartInstanceIds ?? [])
202+
const processedPartInstanceIds = new Set<PartInstanceId>()
203+
204+
const replacePartInstance = (partInstance: DBPartInstance): void => {
205+
processedPartInstanceIds.add(partInstance._id)
206+
modifyPartInstanceForQuickLoop(
207+
partInstance,
208+
segmentRanks,
209+
rundownRanks,
210+
playlist,
211+
studioSettings.settings,
212+
quickLoopStartPosition,
213+
quickLoopEndPosition
214+
)
215+
collection.replace(partInstance)
216+
}
217+
218+
if (!invalidateQuickLoop) {
219+
for (const partInstanceId of invalidatedPartInstanceIds) {
220+
const partInstance = state.contentCache.PartInstances.findOne(partInstanceId)
221+
if (partInstance) replacePartInstance(partInstance)
222+
}
223+
224+
if (invalidatedSegmentIds.length > 0) {
225+
state.contentCache.PartInstances.find({
226+
segmentId: {
227+
$in: invalidatedSegmentIds,
228+
},
229+
}).forEach((partInstance) => {
230+
if (!processedPartInstanceIds.has(partInstance._id)) {
231+
replacePartInstance(partInstance)
232+
}
233+
})
234+
}
235+
236+
return
237+
}
191238

192239
state.contentCache.PartInstances.find({}).forEach((partInstance) => {
193240
if (
194241
updateProps?.invalidateQuickLoop ||
195242
invalidatedSegmentsSet.has(partInstance.segmentId) ||
196243
invalidatedPartInstancesSet.has(partInstance._id)
197244
) {
198-
modifyPartInstanceForQuickLoop(
199-
partInstance,
200-
segmentRanks,
201-
rundownRanks,
202-
playlist,
203-
studioSettings.settings,
204-
quickLoopStartPosition,
205-
quickLoopEndPosition
206-
)
207-
collection.replace(partInstance)
245+
replacePartInstance(partInstance)
208246
}
209247
})
210248
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import type { IStudioContext } from './studioContext.js'
33
import type { IngestDefaultChangesOptions, MutableIngestRundown, NrcsIngestChangeDetails } from '../ingest.js'
44

55
export interface IProcessIngestDataContext extends IStudioContext {
6+
/**
7+
* Request that `syncIngestUpdateToPartInstance` is executed for current/next part instances
8+
* during commit, even when ingest changes would normally not select any part instances.
9+
*/
10+
requestSyncIngestUpdateToPartInstances(): void
11+
612
/**
713
* Perform the default syncing of changes from the ingest data to the rundown.
814
*

packages/job-worker/src/blueprints/__tests__/context.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { unprotectString } from '@sofie-automation/corelib/dist/protectedString'
33
import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context.js'
44
import { getShowStyleConfigRef, getStudioConfigRef } from '../configRefs.js'
55
import { CommonContext } from '../context/CommonContext.js'
6+
import { ProcessIngestDataContext } from '../context/ProcessIngestDataContext.js'
67
import { StudioContext } from '../context/StudioContext.js'
78
import { ShowStyleContext } from '../context/ShowStyleContext.js'
89

@@ -134,4 +135,21 @@ describe('Test blueprint api context', () => {
134135
describe('SegmentUserContext', () => {
135136
// TODO?
136137
})
138+
139+
describe('ProcessIngestDataContext', () => {
140+
test('requestSyncIngestUpdateToPartInstances', () => {
141+
const context = new ProcessIngestDataContext(
142+
{ name: 'processIngestData', identifier: unprotectString(jobContext.studioId) },
143+
jobContext.studio,
144+
jobContext.getStudioBlueprintConfig()
145+
)
146+
147+
expect(context.consumeSyncIngestUpdateToPartInstancesRequest()).toBe(false)
148+
149+
context.requestSyncIngestUpdateToPartInstances()
150+
151+
expect(context.consumeSyncIngestUpdateToPartInstancesRequest()).toBe(true)
152+
expect(context.consumeSyncIngestUpdateToPartInstancesRequest()).toBe(false)
153+
})
154+
})
137155
})

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ import {
2020
* Custom updates of the IngestRundown are done by calling methods on the mutableIngestRundown itself.
2121
*/
2222
export class ProcessIngestDataContext extends StudioContext implements IProcessIngestDataContext {
23+
#syncIngestUpdateToPartInstancesRequested = false
24+
25+
requestSyncIngestUpdateToPartInstances(): void {
26+
this.#syncIngestUpdateToPartInstancesRequested = true
27+
}
28+
29+
consumeSyncIngestUpdateToPartInstancesRequest(): boolean {
30+
const requested = this.#syncIngestUpdateToPartInstancesRequested
31+
this.#syncIngestUpdateToPartInstancesRequested = false
32+
return requested
33+
}
34+
2335
defaultApplyIngestChanges<TRundownPayload, TSegmentPayload, TPartPayload>(
2436
mutableIngestRundown: MutableIngestRundown<TRundownPayload, TSegmentPayload, TPartPayload>,
2537
nrcsIngestRundown: IngestRundown,

packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ export class MutableIngestRundownImpl<
386386
segmentsUpdatedRanks,
387387
segmentsToRegenerate,
388388
regenerateRundown,
389+
forceSyncIngestUpdateToPartInstances: false,
389390
segmentExternalIdChanges: segmentExternalIdChanges,
390391
},
391392

packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestRundownImpl.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ describe('MutableIngestRundownImpl', () => {
109109
segmentsUpdatedRanks: {},
110110
segmentsToRegenerate: [],
111111
regenerateRundown: false,
112+
forceSyncIngestUpdateToPartInstances: false,
112113

113114
segmentExternalIdChanges: {},
114115
},

packages/job-worker/src/ingest/__tests__/ingest.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import '../../__mocks__/_extendJest.js'
22
import {
33
IBlueprintPiece,
4+
IngestChangeType,
45
IngestPart,
56
IngestRundown,
67
IngestSegment,
@@ -51,6 +52,7 @@ import { PlayoutPartInstanceModel } from '../../playout/model/PlayoutPartInstanc
5152
import { NrcsIngestCacheType } from '@sofie-automation/corelib/dist/dataModel/NrcsIngestDataCache'
5253
import { wrapGenericIngestJob, wrapGenericIngestJobWithPrecheck } from '../jobWrappers.js'
5354
import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
55+
import { ProcessIngestDataContext } from '../../blueprints/context/ProcessIngestDataContext.js'
5456

5557
const handleRemovedRundownWrapped = wrapGenericIngestJob(handleRemovedRundown)
5658
const handleUpdatedRundownWrapped = wrapGenericIngestJob(handleUpdatedRundown)
@@ -1608,6 +1610,77 @@ describe('Test ingest actions for rundowns and segments', () => {
16081610
}
16091611
})
16101612

1613+
test('processIngestData can force syncIngestUpdateToPartInstance for active rundown', async () => {
1614+
const rundownData = clone(rundownData1)
1615+
const syncIngestUpdateToPartInstance = jest.fn()
1616+
1617+
context.updateStudioBlueprint({
1618+
processIngestData: async (
1619+
blueprintContext,
1620+
mutableIngestRundown,
1621+
nrcsIngestRundown,
1622+
_previous,
1623+
ingestChanges
1624+
) => {
1625+
if (ingestChanges.source !== IngestChangeType.Ingest) {
1626+
throw new Error(`Expected ingest changes for this test`)
1627+
}
1628+
1629+
blueprintContext.defaultApplyIngestChanges(mutableIngestRundown, nrcsIngestRundown, ingestChanges)
1630+
;(blueprintContext as ProcessIngestDataContext).requestSyncIngestUpdateToPartInstances()
1631+
},
1632+
})
1633+
context.updateShowStyleBlueprint({
1634+
syncIngestUpdateToPartInstance,
1635+
})
1636+
1637+
try {
1638+
await handleUpdatedRundownWrapped(context, {
1639+
rundownExternalId: rundownData.externalId,
1640+
ingestRundown: rundownData,
1641+
isCreateAction: true,
1642+
rundownSource: createRundownSource(device),
1643+
})
1644+
1645+
const rundown = (await context.mockCollections.Rundowns.findOne({ externalId: externalId })) as DBRundown
1646+
expect(rundown).toBeTruthy()
1647+
1648+
await handleActivateRundownPlaylist(context, {
1649+
playlistId: rundown.playlistId,
1650+
rehearsal: true,
1651+
})
1652+
await handleTakeNextPart(context, { playlistId: rundown.playlistId, fromPartInstanceId: null })
1653+
1654+
const selectedInstances = await getSelectedPartInstances(
1655+
context,
1656+
(await context.mockCollections.RundownPlaylists.findOne(rundown.playlistId)) as DBRundownPlaylist
1657+
)
1658+
expect(selectedInstances.currentPartInstance?.part.externalId).toBe('part0')
1659+
expect(selectedInstances.nextPartInstance?.part.externalId).toBe('part1')
1660+
1661+
syncIngestUpdateToPartInstance.mockClear()
1662+
1663+
await handleUpdatedPartWrapped(context, {
1664+
rundownExternalId: rundownData.externalId,
1665+
ingestPart: {
1666+
externalId: 'part2',
1667+
name: 'Part 2 updated',
1668+
rank: 0,
1669+
payload: undefined,
1670+
},
1671+
segmentExternalId: 'segment1',
1672+
isCreateAction: false,
1673+
})
1674+
1675+
expect(syncIngestUpdateToPartInstance).toHaveBeenCalled()
1676+
const playStatuses = syncIngestUpdateToPartInstance.mock.calls.map((call) => call[3])
1677+
expect(playStatuses.some((status) => status === 'current' || status === 'next')).toBe(true)
1678+
} finally {
1679+
context.updateStudioBlueprint({ processIngestData: undefined as any })
1680+
context.updateShowStyleBlueprint({ syncIngestUpdateToPartInstance: undefined as any })
1681+
}
1682+
})
1683+
16111684
test('previous partinstance getting removed if an adlib part', async () => {
16121685
const rundownData: IngestRundown = {
16131686
externalId: externalId,

packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
1616
import { protectString } from '@sofie-automation/corelib/dist/protectedString'
1717
import { PlayoutModelImpl } from '../../playout/model/implementation/PlayoutModelImpl.js'
1818
import { PlaylistTimingType, ShowStyleBlueprintManifest } from '@sofie-automation/blueprints-integration'
19-
import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids'
19+
import { RundownPlaylistId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids'
2020
import {
2121
DBRundownPlaylist,
2222
SelectedPartInstance,
@@ -82,6 +82,55 @@ describe('SyncChangesToPartInstancesWorker', () => {
8282
expect(instancesToSync).toHaveLength(0)
8383
})
8484

85+
test('Current partInstance is skipped when changedSegmentIds does not include its segment', async () => {
86+
const context = setupDefaultJobEnvironment()
87+
88+
const affectedSegmentId = protectString<SegmentId>('affectedSegmentId')
89+
const unchangedSegmentId = protectString<SegmentId>('unchangedSegmentId')
90+
91+
const currentPartInstance = mock<PlayoutPartInstanceModel>(
92+
{
93+
partInstance: {
94+
_id: protectString('currentPartInstanceId'),
95+
segmentId: unchangedSegmentId,
96+
rundownId: protectString('mockRundownId'),
97+
takeCount: 0,
98+
rehearsal: false,
99+
playlistActivationId: protectString('mockPlaylistActivationId'),
100+
segmentPlayoutId: protectString('mockSegmentPlayoutId'),
101+
part: createMockPart('currentPartId'),
102+
} satisfies PlayoutPartInstanceModel['partInstance'],
103+
},
104+
mockOptions
105+
)
106+
107+
const playoutModel = mock<PlayoutModel>(
108+
{
109+
currentPartInstance,
110+
nextPartInstance: null,
111+
previousPartInstance: null,
112+
findPart: jest.fn(() => createMockPart('currentPartId')),
113+
},
114+
mockOptions
115+
)
116+
117+
const ingestModel = createMockIngestModelReadonly()
118+
const rundownModel = mock<PlayoutRundownModel>(
119+
{
120+
rundown: {
121+
_id: protectString('mockRundownId'),
122+
} as any,
123+
},
124+
mockOptions
125+
)
126+
127+
const instancesToSync = findInstancesToSync(context, playoutModel, ingestModel, rundownModel, [
128+
affectedSegmentId,
129+
])
130+
131+
expect(instancesToSync).toHaveLength(0)
132+
})
133+
85134
// TODO - this needs a lot more fleshing out
86135
})
87136

packages/job-worker/src/ingest/commit.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,17 @@ export async function CommitIngestOperation(
229229
await validateAdlibTestingSegment(context, playoutModel)
230230

231231
try {
232+
const syncChangedSegmentIds = data.forceSyncIngestUpdateToPartInstances
233+
? undefined
234+
: data.changedSegmentIds
235+
if (data.forceSyncIngestUpdateToPartInstances) {
236+
logger.debug(
237+
`Forcing syncChangesToPartInstances for rundown "${ingestModel.rundownId}" without segment filtering`
238+
)
239+
}
240+
232241
// sync changes to the 'selected' partInstances
233-
await syncChangesToPartInstances(context, playoutModel, ingestModel)
242+
await syncChangesToPartInstances(context, playoutModel, ingestModel, syncChangedSegmentIds)
234243

235244
// update the quickloop in case we did any changes to things involving marker
236245
playoutModel.updateQuickLoopState()

packages/job-worker/src/ingest/generationRundown.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export async function updateRundownFromIngestData(
9393
span?.end()
9494
return literal<CommitIngestDataExt>({
9595
changedSegmentIds: regenerateSegmentsChanges?.changedSegmentIds ?? [],
96+
forceSyncIngestUpdateToPartInstances: false,
9697
removedSegmentIds: regenerateSegmentsChanges?.removedSegmentIds ?? [],
9798
renamedSegments: new Map(),
9899

0 commit comments

Comments
 (0)