Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions meteor/server/api/deviceTriggers/PieceInstancesObserver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Meteor } from 'meteor/meteor'
import { RundownPlaylistActivationId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { RundownPlaylists, ShowStyleBases, PieceInstances, PartInstances } from '../../collections'
import { logger } from '../../logging'
import { rundownPlaylistFieldSpecifier } from './reactiveContentCache'
import {
ContentCache,
createReactiveContentCache,
partInstanceFieldSpecifier,
pieceInstanceFieldSpecifier,
} from './reactiveContentCacheForPieceInstances'
import { waitForAllObserversReady } from '../../publications/lib/lib'

const REACTIVITY_DEBOUNCE = 20

type ChangedHandler = (cache: ContentCache) => () => void

export class PieceInstancesObserver {
#observers: Meteor.LiveQueryHandle[] = []
#cache: ContentCache
#cancelCache: () => void
#cleanup: (() => void) | undefined
#disposed = false

constructor(onChanged: ChangedHandler) {
const { cache, cancel: cancelCache } = createReactiveContentCache(() => {
this.#cleanup = onChanged(cache)
if (this.#disposed) this.#cleanup()
}, REACTIVITY_DEBOUNCE)

this.#cache = cache
this.#cancelCache = cancelCache
}

static async create(
activationId: RundownPlaylistActivationId,
showStyleBaseId: ShowStyleBaseId,
onChanged: ChangedHandler
): Promise<PieceInstancesObserver> {
logger.silly(`Creating PieceInstancesObserver for activationId "${activationId}"`)

const observer = new PieceInstancesObserver(onChanged)

await observer.initObservers(activationId, showStyleBaseId)

return observer
}

private async initObservers(activationId: RundownPlaylistActivationId, showStyleBaseId: ShowStyleBaseId) {
this.#observers = await waitForAllObserversReady([
RundownPlaylists.observeChanges(
{
activationId,
},
this.#cache.RundownPlaylists.link(),
{
projection: rundownPlaylistFieldSpecifier,
}
),
ShowStyleBases.observeChanges(showStyleBaseId, this.#cache.ShowStyleBases.link()),
PieceInstances.observeChanges(
{
playlistActivationId: activationId,
reset: { $ne: true },
disabled: { $ne: true },
reportedStoppedPlayback: { $exists: false },
'piece.virtual': { $ne: true },
},
this.#cache.PieceInstances.link(),
{
projection: pieceInstanceFieldSpecifier,
}
),
PartInstances.observeChanges(
{
playlistActivationId: activationId,
reset: { $ne: true },
'timings.reportedStoppedPlayback': { $ne: true },
},
this.#cache.PartInstances.link(),
{
projection: partInstanceFieldSpecifier,
}
),
])
}

public get cache(): ContentCache {
return this.#cache
}

public stop = (): void => {
this.#disposed = true
this.#cancelCache()
this.#observers.forEach((observer) => observer.stop())
this.#cleanup?.()
this.#cleanup = undefined
}
}
28 changes: 27 additions & 1 deletion meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,20 @@ import { protectString } from '../../lib/tempLib'
import { StudioActionManager, StudioActionManagers } from './StudioActionManagers'
import { DeviceTriggerMountedActionAdlibsPreview, DeviceTriggerMountedActions } from './observer'
import { ContentCache } from './reactiveContentCache'
import { ContentCache as PieceInstancesContentCache } from './reactiveContentCacheForPieceInstances'
import { logger } from '../../logging'
import { SomeAction, SomeBlueprintTrigger } from '@sofie-automation/blueprints-integration'
import { DeviceActions } from '@sofie-automation/shared-lib/dist/core/model/ShowStyle'
import { DummyReactiveVar } from '@sofie-automation/meteor-lib/dist/triggers/reactive-var'
import { MeteorTriggersContext } from './triggersContext'
import { TagsService } from './TagsService'

export class StudioDeviceTriggerManager {
#lastShowStyleBaseId: ShowStyleBaseId | null = null

constructor(public studioId: StudioId) {
lastCache: ContentCache | undefined

constructor(public studioId: StudioId, protected tagsService: TagsService) {
if (StudioActionManagers.get(studioId)) {
logger.error(`A StudioActionManager for "${studioId}" already exists`)
return
Expand All @@ -45,6 +49,7 @@ export class StudioDeviceTriggerManager {

async updateTriggers(cache: ContentCache, showStyleBaseId: ShowStyleBaseId): Promise<void> {
const studioId = this.studioId
this.lastCache = cache
this.#lastShowStyleBaseId = showStyleBaseId

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

this.tagsService.clearObservedTags()

for (const rawTriggeredAction of allTriggeredActions) {
const triggeredAction = convertDocument(rawTriggeredAction)

Expand Down Expand Up @@ -163,6 +170,8 @@ export class StudioDeviceTriggerManager {
sourceLayerType: undefined,
sourceLayerName: undefined,
styleClassNames: triggeredAction.styleClassNames,
isActive: undefined,
isNext: undefined,
}),
})
} else {
Expand All @@ -174,6 +183,9 @@ export class StudioDeviceTriggerManager {
)

addedPreviewIds.push(adLibPreviewId)

this.tagsService.observeTallyTags(adLib)
const { isActive, isNext } = this.tagsService.getTallyStateFromTags(adLib)
return DeviceTriggerMountedActionAdlibsPreview.upsertAsync(adLibPreviewId, {
$set: literal<PreviewWrappedAdLib>({
...adLib,
Expand All @@ -192,6 +204,8 @@ export class StudioDeviceTriggerManager {
}
: undefined,
styleClassNames: triggeredAction.styleClassNames,
isActive,
isNext,
}),
})
})
Expand Down Expand Up @@ -224,6 +238,18 @@ export class StudioDeviceTriggerManager {
actionManager.deleteActionsOtherThan(touchedActionIds)
}

protected async updateTriggersFromLastCache(): Promise<void> {
if (!this.lastCache || !this.#lastShowStyleBaseId) return
return this.updateTriggers(this.lastCache, this.#lastShowStyleBaseId)
}

async updatePieceInstances(cache: PieceInstancesContentCache, showStyleBaseId: ShowStyleBaseId): Promise<void> {
const shouldUpdateTriggers = this.tagsService.updatePieceInstances(cache, showStyleBaseId)
if (shouldUpdateTriggers) {
await this.updateTriggersFromLastCache()
}
}

async clearTriggers(): Promise<void> {
const studioId = this.studioId
const showStyleBaseId = this.#lastShowStyleBaseId
Expand Down
40 changes: 34 additions & 6 deletions meteor/server/api/deviceTriggers/StudioObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowSt
import { logger } from '../../logging'
import { observerChain } from '../../publications/lib/observerChain'
import { ContentCache } from './reactiveContentCache'
import { ContentCache as PieceInstancesContentCache } from './reactiveContentCacheForPieceInstances'
import { RundownContentObserver } from './RundownContentObserver'
import { RundownsObserver } from './RundownsObserver'
import { RundownPlaylists, Rundowns, ShowStyleBases } from '../../collections'
import { PromiseDebounce } from '../../publications/lib/PromiseDebounce'
import { MinimalMongoCursor } from '../../collections/implementations/asyncCollection'
import { PieceInstancesObserver } from './PieceInstancesObserver'

type ChangedHandler = (showStyleBaseId: ShowStyleBaseId, cache: ContentCache) => () => void
type RundownContentChangeHandler = (showStyleBaseId: ShowStyleBaseId, cache: ContentCache) => () => void
type PieceInstancesChangeHandler = (showStyleBaseId: ShowStyleBaseId, cache: PieceInstancesContentCache) => () => void

const REACTIVITY_DEBOUNCE = 20

Expand Down Expand Up @@ -60,18 +63,26 @@ export class StudioObserver extends EventEmitter {
#playlistInStudioLiveQuery: Meteor.LiveQueryHandle
#showStyleOfRundownLiveQuery: Meteor.LiveQueryHandle | undefined
#rundownsLiveQuery: Meteor.LiveQueryHandle | undefined
#pieceInstancesLiveQuery: Meteor.LiveQueryHandle | undefined

showStyleBaseId: ShowStyleBaseId | undefined

currentProps: StudioObserverProps | undefined = undefined
nextProps: StudioObserverProps | undefined = undefined

#changed: ChangedHandler
#rundownContentChanged: RundownContentChangeHandler
#pieceInstancesChanged: PieceInstancesChangeHandler

#disposed = false

constructor(studioId: StudioId, onChanged: ChangedHandler) {
constructor(
studioId: StudioId,
onRundownContentChanged: RundownContentChangeHandler,
pieceInstancesChanged: PieceInstancesChangeHandler
) {
super()
this.#changed = onChanged
this.#rundownContentChanged = onRundownContentChanged
this.#pieceInstancesChanged = pieceInstancesChanged
this.#playlistInStudioLiveQuery = observerChain()
.next(
'activePlaylist',
Expand Down Expand Up @@ -172,6 +183,9 @@ export class StudioObserver extends EventEmitter {
this.#rundownsLiveQuery?.stop()
this.#rundownsLiveQuery = undefined
this.showStyleBaseId = showStyleBaseId

this.#pieceInstancesLiveQuery?.stop()
this.#pieceInstancesLiveQuery = undefined
return
}

Expand All @@ -186,28 +200,42 @@ export class StudioObserver extends EventEmitter {
this.#rundownsLiveQuery?.stop()
this.#rundownsLiveQuery = undefined

this.#pieceInstancesLiveQuery?.stop()
this.#pieceInstancesLiveQuery = undefined

this.showStyleBaseId = showStyleBaseId

this.currentProps = this.nextProps
this.nextProps = undefined

const { activePlaylistId } = this.currentProps
const { activePlaylistId, activationId } = this.currentProps

this.showStyleBaseId = showStyleBaseId

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

const obs1 = await RundownContentObserver.create(activePlaylistId, showStyleBaseId, rundownIds, (cache) => {
return this.#changed(showStyleBaseId, cache)
return this.#rundownContentChanged(showStyleBaseId, cache)
})

return () => {
obs1.stop()
}
})

this.#pieceInstancesLiveQuery = await PieceInstancesObserver.create(activationId, showStyleBaseId, (cache) => {
const cleanupChanges = this.#pieceInstancesChanged(showStyleBaseId, cache)

return () => {
cleanupChanges?.()
}
})

if (this.#disposed) {
// If we were disposed of while waiting for the observer to be created, stop it immediately
this.#rundownsLiveQuery.stop()
this.#pieceInstancesLiveQuery.stop()
}
}, REACTIVITY_DEBOUNCE)

Expand Down
Loading
Loading