Skip to content

Commit 672f2bd

Browse files
authored
feat: mos status flow rework (Sofie-Automation#1356)
1 parent 317020e commit 672f2bd

File tree

50 files changed

+2326
-746
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2326
-746
lines changed

.github/workflows/node.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -502,16 +502,19 @@ jobs:
502502
- node-version: 22.x
503503
package-name: job-worker
504504
send-coverage: true
505-
# No tests for the gateways yet
505+
# No tests for some gateways yet
506506
# - node-version: 22.x
507507
# package-name: playout-gateway
508-
# - node-version: 22.x
509-
# package-name: mos-gateway
508+
# send-coverage: true
509+
- node-version: 22.x
510+
package-name: mos-gateway
511+
send-coverage: true
510512
- node-version: 22.x
511513
package-name: live-status-gateway
512514
send-coverage: true
513515
- node-version: 22.x
514516
package-name: webui
517+
send-coverage: true
515518
# manual meteor-lib as it only needs a couple of versions
516519
- node-version: 22.x
517520
package-name: meteor-lib

meteor/server/collections/collection.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FindOptions, MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo'
1+
import { FindOptions, MongoModifier, MongoQuery, ObserveChangesOptions } from '@sofie-automation/corelib/dist/mongo'
22
import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString'
33
import { Meteor } from 'meteor/meteor'
44
import { Mongo } from 'meteor/mongo'
@@ -234,7 +234,8 @@ export interface AsyncOnlyReadOnlyMongoCollection<DBInterface extends { _id: Pro
234234
observeChanges(
235235
selector: MongoQuery<DBInterface> | DBInterface['_id'],
236236
callbacks: PromisifyCallbacks<ObserveChangesCallbacks<DBInterface>>,
237-
options?: Omit<FindOptions<DBInterface>, 'fields'>
237+
findOptions?: Omit<FindOptions<DBInterface>, 'fields'>,
238+
callbackOptions?: ObserveChangesOptions
238239
): Promise<Meteor.LiveQueryHandle>
239240

240241
/**

meteor/server/collections/implementations/asyncCollection.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo'
1+
import { MongoModifier, MongoQuery, ObserveChangesOptions } from '@sofie-automation/corelib/dist/mongo'
22
import { ProtectedString, protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString'
33
import { Meteor } from 'meteor/meteor'
44
import { Mongo } from 'meteor/mongo'
@@ -141,7 +141,8 @@ export class WrappedAsyncMongoCollection<DBInterface extends { _id: ProtectedStr
141141
async observeChanges(
142142
selector: MongoQuery<DBInterface> | DBInterface['_id'],
143143
callbacks: PromisifyCallbacks<ObserveChangesCallbacks<DBInterface>>,
144-
options?: FindOptions<DBInterface>
144+
findOptions?: FindOptions<DBInterface>,
145+
callbackOptions?: ObserveChangesOptions
145146
): Promise<Meteor.LiveQueryHandle> {
146147
const span = profiler.startSpan(`MongoCollection.${this.name}.observeChanges`)
147148
if (span) {
@@ -152,8 +153,8 @@ export class WrappedAsyncMongoCollection<DBInterface extends { _id: ProtectedStr
152153
}
153154
try {
154155
const res = await this._collection
155-
.find((selector ?? {}) as any, options as any)
156-
.observeChangesAsync(callbacks)
156+
.find((selector ?? {}) as any, findOptions as any)
157+
.observeChangesAsync(callbacks, callbackOptions)
157158
if (span) span.end()
158159
return res
159160
} catch (e) {

meteor/server/publications/_publications.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import './lib/lib'
33

44
import './buckets'
55
import './blueprintUpgradeStatus/publication'
6+
import './ingestStatus/publication'
67
import './packageManager/expectedPackages/publication'
78
import './packageManager/packageContainers'
89
import './packageManager/playoutContext'
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import type { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids'
2+
import { NrcsIngestCacheType } from '@sofie-automation/corelib/dist/dataModel/NrcsIngestDataCache'
3+
import {
4+
IngestRundownStatus,
5+
IngestPartPlaybackStatus,
6+
IngestRundownActiveStatus,
7+
IngestPartStatus,
8+
IngestPartNotifyItemReady,
9+
} from '@sofie-automation/shared-lib/dist/ingest/rundownStatus'
10+
import type { ReadonlyDeep } from 'type-fest'
11+
import _ from 'underscore'
12+
import type { ContentCache, PartCompact, PartInstanceCompact, PlaylistCompact } from './reactiveContentCache'
13+
import { ReactiveCacheCollection } from '../lib/ReactiveCacheCollection'
14+
import { unprotectString } from '@sofie-automation/corelib/dist/protectedString'
15+
16+
export function createIngestRundownStatus(
17+
cache: ReadonlyDeep<ContentCache>,
18+
rundownId: RundownId
19+
): IngestRundownStatus | null {
20+
const rundown = cache.Rundowns.findOne(rundownId)
21+
if (!rundown) return null
22+
23+
const newDoc: IngestRundownStatus = {
24+
_id: rundownId,
25+
externalId: rundown.externalId,
26+
27+
active: IngestRundownActiveStatus.INACTIVE,
28+
29+
segments: [],
30+
}
31+
32+
const playlist = cache.Playlists.findOne({
33+
_id: rundown.playlistId,
34+
activationId: { $exists: true },
35+
})
36+
37+
if (playlist) {
38+
newDoc.active = playlist.rehearsal ? IngestRundownActiveStatus.REHEARSAL : IngestRundownActiveStatus.ACTIVE
39+
}
40+
41+
const nrcsSegments = cache.NrcsIngestData.find({ rundownId, type: NrcsIngestCacheType.SEGMENT }).fetch()
42+
for (const nrcsSegment of nrcsSegments) {
43+
const nrcsParts = cache.NrcsIngestData.find({
44+
rundownId,
45+
segmentId: nrcsSegment.segmentId,
46+
type: NrcsIngestCacheType.PART,
47+
}).fetch()
48+
49+
newDoc.segments.push({
50+
externalId: nrcsSegment.data.externalId,
51+
parts: _.compact(
52+
nrcsParts.map((nrcsPart) => {
53+
if (!nrcsPart.partId || !nrcsPart.segmentId) return null
54+
55+
const parts = cache.Parts.find({
56+
rundownId: rundownId,
57+
$or: [
58+
{
59+
externalId: nrcsPart.data.externalId,
60+
ingestNotifyPartExternalId: { $exists: false },
61+
},
62+
{
63+
ingestNotifyPartExternalId: nrcsPart.data.externalId,
64+
},
65+
],
66+
}).fetch()
67+
const partInstances = findPartInstancesForIngestPart(
68+
playlist,
69+
rundownId,
70+
cache.PartInstances,
71+
nrcsPart.data.externalId
72+
)
73+
74+
return createIngestPartStatus(playlist, partInstances, parts, nrcsPart.data.externalId)
75+
})
76+
),
77+
})
78+
}
79+
80+
return newDoc
81+
}
82+
83+
function findPartInstancesForIngestPart(
84+
playlist: PlaylistCompact | undefined,
85+
rundownId: RundownId,
86+
partInstancesCache: ReadonlyDeep<ReactiveCacheCollection<PartInstanceCompact>>,
87+
partExternalId: string
88+
) {
89+
const result: Record<string, PartInstanceCompact> = {}
90+
if (!playlist) return result
91+
92+
const candidatePartInstances = partInstancesCache
93+
.find({
94+
rundownId: rundownId,
95+
$or: [
96+
{
97+
'part.externalId': partExternalId,
98+
'part.ingestNotifyPartExternalId': { $exists: false },
99+
},
100+
{
101+
'part.ingestNotifyPartExternalId': partExternalId,
102+
},
103+
],
104+
})
105+
.fetch()
106+
107+
for (const partInstance of candidatePartInstances) {
108+
if (partInstance.rundownId !== rundownId) continue
109+
// Ignore the next partinstance
110+
if (partInstance._id === playlist.nextPartInfo?.partInstanceId) continue
111+
112+
const partId = unprotectString(partInstance.part._id)
113+
114+
// The current part instance is the most important
115+
if (partInstance._id === playlist.currentPartInfo?.partInstanceId) {
116+
result[partId] = partInstance
117+
continue
118+
}
119+
120+
// Take the part with the highest takeCount
121+
const existingEntry = result[partId]
122+
if (!existingEntry || existingEntry.takeCount < partInstance.takeCount) {
123+
result[partId] = partInstance
124+
}
125+
}
126+
127+
return result
128+
}
129+
130+
function createIngestPartStatus(
131+
playlist: PlaylistCompact | undefined,
132+
partInstances: Record<string, PartInstanceCompact>,
133+
parts: PartCompact[],
134+
ingestPartExternalId: string
135+
): IngestPartStatus {
136+
// Determine the playback status from the PartInstance
137+
let playbackStatus = IngestPartPlaybackStatus.UNKNOWN
138+
139+
let isReady: boolean | null = null // Start off as null, the first value will make this true or false
140+
141+
const itemsReady: IngestPartNotifyItemReady[] = []
142+
143+
const updateStatusWithPart = (part: PartCompact) => {
144+
// If the part affects the ready status, update it
145+
if (typeof part.ingestNotifyPartReady === 'boolean') {
146+
isReady = (isReady ?? true) && part.ingestNotifyPartReady
147+
}
148+
149+
// Include the items
150+
if (part.ingestNotifyItemsReady) {
151+
itemsReady.push(...part.ingestNotifyItemsReady)
152+
}
153+
}
154+
155+
// Loop through the partInstances, starting off the state
156+
if (playlist) {
157+
for (const partInstance of Object.values<PartInstanceCompact>(partInstances)) {
158+
if (!partInstance) continue
159+
160+
if (partInstance.part.shouldNotifyCurrentPlayingPart) {
161+
const isCurrentPartInstance = playlist.currentPartInfo?.partInstanceId === partInstance._id
162+
163+
if (isCurrentPartInstance) {
164+
// If the current, it is playing
165+
playbackStatus = IngestPartPlaybackStatus.PLAY
166+
} else if (playbackStatus === IngestPartPlaybackStatus.UNKNOWN) {
167+
// If not the current, but has been played, it is stopped
168+
playbackStatus = IngestPartPlaybackStatus.STOP
169+
}
170+
}
171+
172+
updateStatusWithPart(partInstance.part)
173+
}
174+
}
175+
176+
for (const part of parts) {
177+
// Check if the part has already been handled by a partInstance
178+
if (partInstances[unprotectString(part._id)]) continue
179+
180+
updateStatusWithPart(part)
181+
}
182+
183+
return {
184+
externalId: ingestPartExternalId,
185+
186+
isReady: isReady,
187+
itemsReady: itemsReady,
188+
189+
playbackStatus,
190+
}
191+
}

0 commit comments

Comments
 (0)