Skip to content

Commit 3f21504

Browse files
committed
feat(EAV-296): implement tally for device trigger previews
1 parent 94af13a commit 3f21504

File tree

9 files changed

+693
-14
lines changed

9 files changed

+693
-14
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Meteor } from 'meteor/meteor'
2+
import { RundownPlaylistActivationId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids'
3+
import { RundownPlaylists, ShowStyleBases, PieceInstances, PartInstances } from '../../collections'
4+
import { logger } from '../../logging'
5+
import { rundownPlaylistFieldSpecifier } from './reactiveContentCache'
6+
import {
7+
ContentCache,
8+
createReactiveContentCache,
9+
partInstanceFieldSpecifier,
10+
pieceInstanceFieldSpecifier,
11+
} from './reactiveContentCacheForPieceInstances'
12+
import { waitForAllObserversReady } from '../../publications/lib/lib'
13+
14+
const REACTIVITY_DEBOUNCE = 20
15+
16+
type ChangedHandler = (cache: ContentCache) => () => void
17+
18+
export class PieceInstancesObserver {
19+
#observers: Meteor.LiveQueryHandle[] = []
20+
#cache: ContentCache
21+
#cancelCache: () => void
22+
#cleanup: (() => void) | undefined
23+
#disposed = false
24+
25+
constructor(onChanged: ChangedHandler) {
26+
const { cache, cancel: cancelCache } = createReactiveContentCache(() => {
27+
this.#cleanup = onChanged(cache)
28+
if (this.#disposed) this.#cleanup()
29+
}, REACTIVITY_DEBOUNCE)
30+
31+
this.#cache = cache
32+
this.#cancelCache = cancelCache
33+
}
34+
35+
static async create(
36+
activationId: RundownPlaylistActivationId,
37+
showStyleBaseId: ShowStyleBaseId,
38+
onChanged: ChangedHandler
39+
): Promise<PieceInstancesObserver> {
40+
logger.silly(`Creating PieceInstancesObserver for activationId "${activationId}"`)
41+
42+
const observer = new PieceInstancesObserver(onChanged)
43+
44+
await observer.initObservers(activationId, showStyleBaseId)
45+
46+
return observer
47+
}
48+
49+
private async initObservers(activationId: RundownPlaylistActivationId, showStyleBaseId: ShowStyleBaseId) {
50+
this.#observers = await waitForAllObserversReady([
51+
RundownPlaylists.observeChanges(
52+
{
53+
activationId,
54+
},
55+
this.#cache.RundownPlaylists.link(),
56+
{
57+
projection: rundownPlaylistFieldSpecifier,
58+
}
59+
),
60+
ShowStyleBases.observeChanges(showStyleBaseId, this.#cache.ShowStyleBases.link()),
61+
PieceInstances.observeChanges(
62+
{
63+
playlistActivationId: activationId,
64+
reset: { $ne: true },
65+
disabled: { $ne: true },
66+
reportedStoppedPlayback: { $exists: false },
67+
'piece.virtual': { $ne: true },
68+
},
69+
this.#cache.PieceInstances.link(),
70+
{
71+
projection: pieceInstanceFieldSpecifier,
72+
}
73+
),
74+
PartInstances.observeChanges(
75+
{
76+
playlistActivationId: activationId,
77+
reset: { $ne: true },
78+
'timings.reportedStoppedPlayback': { $ne: true },
79+
},
80+
this.#cache.PartInstances.link(),
81+
{
82+
projection: partInstanceFieldSpecifier,
83+
}
84+
),
85+
])
86+
}
87+
88+
public get cache(): ContentCache {
89+
return this.#cache
90+
}
91+
92+
public stop = (): void => {
93+
this.#disposed = true
94+
this.#cancelCache()
95+
this.#observers.forEach((observer) => observer.stop())
96+
this.#cleanup?.()
97+
}
98+
}

meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,20 @@ import { protectString } from '../../lib/tempLib'
2525
import { StudioActionManager, StudioActionManagers } from './StudioActionManagers'
2626
import { DeviceTriggerMountedActionAdlibsPreview, DeviceTriggerMountedActions } from './observer'
2727
import { ContentCache } from './reactiveContentCache'
28+
import { ContentCache as PieceInstancesContentCache } from './reactiveContentCacheForPieceInstances'
2829
import { logger } from '../../logging'
2930
import { SomeAction, SomeBlueprintTrigger } from '@sofie-automation/blueprints-integration'
3031
import { DeviceActions } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle'
3132
import { DummyReactiveVar } from '@sofie-automation/meteor-lib/dist/triggers/reactive-var'
3233
import { MeteorTriggersContext } from './triggersContext'
34+
import { TagsService } from './TagsService'
3335

3436
export class StudioDeviceTriggerManager {
3537
#lastShowStyleBaseId: ShowStyleBaseId | null = null
3638

37-
constructor(public studioId: StudioId) {
39+
lastCache: ContentCache | undefined
40+
41+
constructor(public studioId: StudioId, protected tagsService: TagsService) {
3842
if (StudioActionManagers.get(studioId)) {
3943
logger.error(`A StudioActionManager for "${studioId}" already exists`)
4044
return
@@ -45,6 +49,7 @@ export class StudioDeviceTriggerManager {
4549

4650
async updateTriggers(cache: ContentCache, showStyleBaseId: ShowStyleBaseId): Promise<void> {
4751
const studioId = this.studioId
52+
this.lastCache = cache
4853
this.#lastShowStyleBaseId = showStyleBaseId
4954

5055
const [showStyleBase, rundownPlaylist] = await Promise.all([
@@ -79,6 +84,8 @@ export class StudioDeviceTriggerManager {
7984
const upsertedDeviceTriggerMountedActionIds: DeviceTriggerMountedActionId[] = []
8085
const touchedActionIds: DeviceActionId[] = []
8186

87+
this.tagsService.clearObservedTags()
88+
8289
for (const rawTriggeredAction of allTriggeredActions) {
8390
const triggeredAction = convertDocument(rawTriggeredAction)
8491

@@ -163,6 +170,8 @@ export class StudioDeviceTriggerManager {
163170
sourceLayerType: undefined,
164171
sourceLayerName: undefined,
165172
styleClassNames: triggeredAction.styleClassNames,
173+
isCurrent: undefined,
174+
isNext: undefined,
166175
}),
167176
})
168177
} else {
@@ -174,6 +183,9 @@ export class StudioDeviceTriggerManager {
174183
)
175184

176185
addedPreviewIds.push(adLibPreviewId)
186+
187+
this.tagsService.observeTallyTags(adLib)
188+
const { isCurrent, isNext } = this.tagsService.getTallyStateFromTags(adLib)
177189
return DeviceTriggerMountedActionAdlibsPreview.upsertAsync(adLibPreviewId, {
178190
$set: literal<PreviewWrappedAdLib>({
179191
...adLib,
@@ -192,6 +204,8 @@ export class StudioDeviceTriggerManager {
192204
}
193205
: undefined,
194206
styleClassNames: triggeredAction.styleClassNames,
207+
isCurrent,
208+
isNext,
195209
}),
196210
})
197211
})
@@ -224,6 +238,18 @@ export class StudioDeviceTriggerManager {
224238
actionManager.deleteActionsOtherThan(touchedActionIds)
225239
}
226240

241+
protected async updateTriggersFromLastCache(): Promise<void> {
242+
if (!this.lastCache || !this.#lastShowStyleBaseId) return
243+
return this.updateTriggers(this.lastCache, this.#lastShowStyleBaseId)
244+
}
245+
246+
async updatePieceInstances(cache: PieceInstancesContentCache, showStyleBaseId: ShowStyleBaseId): Promise<void> {
247+
const shouldUpdateTriggers = this.tagsService.updatePieceInstances(cache, showStyleBaseId)
248+
if (shouldUpdateTriggers) {
249+
await this.updateTriggersFromLastCache()
250+
}
251+
}
252+
227253
async clearTriggers(): Promise<void> {
228254
const studioId = this.studioId
229255
const showStyleBaseId = this.#lastShowStyleBaseId

meteor/server/api/deviceTriggers/StudioObserver.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowSt
1616
import { logger } from '../../logging'
1717
import { observerChain } from '../../publications/lib/observerChain'
1818
import { ContentCache } from './reactiveContentCache'
19+
import { ContentCache as PieceInstancesContentCache } from './reactiveContentCacheForPieceInstances'
1920
import { RundownContentObserver } from './RundownContentObserver'
2021
import { RundownsObserver } from './RundownsObserver'
2122
import { RundownPlaylists, Rundowns, ShowStyleBases } from '../../collections'
2223
import { PromiseDebounce } from '../../publications/lib/PromiseDebounce'
2324
import { MinimalMongoCursor } from '../../collections/implementations/asyncCollection'
25+
import { PieceInstancesObserver } from './PieceInstancesObserver'
2426

25-
type ChangedHandler = (showStyleBaseId: ShowStyleBaseId, cache: ContentCache) => () => void
27+
type RundownContentChangeHandler = (showStyleBaseId: ShowStyleBaseId, cache: ContentCache) => () => void
28+
type PieceInstancesChangeHandler = (showStyleBaseId: ShowStyleBaseId, cache: PieceInstancesContentCache) => () => void
2629

2730
const REACTIVITY_DEBOUNCE = 20
2831

@@ -60,18 +63,26 @@ export class StudioObserver extends EventEmitter {
6063
#playlistInStudioLiveQuery: Meteor.LiveQueryHandle
6164
#showStyleOfRundownLiveQuery: Meteor.LiveQueryHandle | undefined
6265
#rundownsLiveQuery: Meteor.LiveQueryHandle | undefined
66+
#pieceInstancesLiveQuery: Meteor.LiveQueryHandle | undefined
67+
6368
showStyleBaseId: ShowStyleBaseId | undefined
6469

6570
currentProps: StudioObserverProps | undefined = undefined
6671
nextProps: StudioObserverProps | undefined = undefined
6772

68-
#changed: ChangedHandler
73+
#rundownContentChanged: RundownContentChangeHandler
74+
#pieceInstancesChanged: PieceInstancesChangeHandler
6975

7076
#disposed = false
7177

72-
constructor(studioId: StudioId, onChanged: ChangedHandler) {
78+
constructor(
79+
studioId: StudioId,
80+
onRundownContentChanged: RundownContentChangeHandler,
81+
pieceInstancesChanged: PieceInstancesChangeHandler
82+
) {
7383
super()
74-
this.#changed = onChanged
84+
this.#rundownContentChanged = onRundownContentChanged
85+
this.#pieceInstancesChanged = pieceInstancesChanged
7586
this.#playlistInStudioLiveQuery = observerChain()
7687
.next(
7788
'activePlaylist',
@@ -172,6 +183,9 @@ export class StudioObserver extends EventEmitter {
172183
this.#rundownsLiveQuery?.stop()
173184
this.#rundownsLiveQuery = undefined
174185
this.showStyleBaseId = showStyleBaseId
186+
187+
this.#pieceInstancesLiveQuery?.stop()
188+
this.#pieceInstancesLiveQuery = undefined
175189
return
176190
}
177191

@@ -186,25 +200,38 @@ export class StudioObserver extends EventEmitter {
186200
this.#rundownsLiveQuery?.stop()
187201
this.#rundownsLiveQuery = undefined
188202

203+
this.#pieceInstancesLiveQuery?.stop()
204+
this.#pieceInstancesLiveQuery = undefined
205+
206+
this.showStyleBaseId = showStyleBaseId
207+
189208
this.currentProps = this.nextProps
190209
this.nextProps = undefined
191210

192-
const { activePlaylistId } = this.currentProps
211+
const { activePlaylistId, activationId } = this.currentProps
193212

194213
this.showStyleBaseId = showStyleBaseId
195214

196215
this.#rundownsLiveQuery = await RundownsObserver.create(activePlaylistId, async (rundownIds) => {
197216
logger.silly(`Creating new RundownContentObserver`)
198217

199218
const obs1 = await RundownContentObserver.create(activePlaylistId, showStyleBaseId, rundownIds, (cache) => {
200-
return this.#changed(showStyleBaseId, cache)
219+
return this.#rundownContentChanged(showStyleBaseId, cache)
201220
})
202221

203222
return () => {
204223
obs1.stop()
205224
}
206225
})
207226

227+
this.#pieceInstancesLiveQuery = await PieceInstancesObserver.create(activationId, showStyleBaseId, (cache) => {
228+
const cleanupChanges = this.#pieceInstancesChanged(showStyleBaseId, cache)
229+
230+
return () => {
231+
cleanupChanges?.()
232+
}
233+
})
234+
208235
if (this.#disposed) {
209236
// If we were disposed of while waiting for the observer to be created, stop it immediately
210237
this.#rundownsLiveQuery.stop()

0 commit comments

Comments
 (0)