Skip to content

Commit 963542a

Browse files
scriptorianSimon Rogers
andauthored
feat: Add support for Gateway configuration from the studio API (Sofie-Automation#1539)
Co-authored-by: Simon Rogers <[email protected]>
1 parent 672f2bd commit 963542a

File tree

11 files changed

+120
-49
lines changed

11 files changed

+120
-49
lines changed

meteor/server/api/rest/v1/studios.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Meteor } from 'meteor/meteor'
99
import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client'
1010
import { PeripheralDevices, RundownPlaylists, Studios } from '../../../collections'
1111
import { APIStudioFrom, studioFrom, validateAPIBlueprintConfigForStudio } from './typeConversion'
12-
import { runUpgradeForStudio, validateConfigForStudio } from '../../../migration/upgrades'
12+
import { runUpgradeForStudio, updateStudioBaseline, validateConfigForStudio } from '../../../migration/upgrades'
1313
import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
1414
import { ServerClientAPI } from '../../client'
1515
import { assertNever, literal } from '@sofie-automation/corelib/dist/lib'
@@ -62,6 +62,7 @@ class StudiosServerAPI implements StudiosRestAPI {
6262
checkValidation(`addStudio ${newStudioId}`, validation.messages)
6363

6464
await runUpgradeForStudio(newStudioId)
65+
await updateStudioBaseline(newStudioId)
6566
return ClientAPI.responseSuccess(unprotectString(newStudioId), 200)
6667
}
6768

@@ -81,7 +82,7 @@ class StudiosServerAPI implements StudiosRestAPI {
8182
_event: string,
8283
studioId: StudioId,
8384
apiStudio: APIStudio
84-
): Promise<ClientAPI.ClientResponse<void>> {
85+
): Promise<ClientAPI.ClientResponse<string | false>> {
8586
const blueprintConfigValidation = await validateAPIBlueprintConfigForStudio(apiStudio)
8687
checkValidation(`addOrUpdateStudio ${studioId}`, blueprintConfigValidation)
8788

@@ -105,13 +106,15 @@ class StudiosServerAPI implements StudiosRestAPI {
105106

106107
await Studios.upsertAsync(studioId, newStudio)
107108

108-
// wait for the upsert to complete before validation and upgrade read from the studios collection
109-
await new Promise<void>((resolve) => setTimeout(() => resolve(), 200))
110-
111109
const validation = await validateConfigForStudio(studioId)
112110
checkValidation(`addOrUpdateStudio ${studioId}`, validation.messages)
113111

114-
return ClientAPI.responseSuccess(await runUpgradeForStudio(studioId))
112+
// wait for the upsert to complete before upgrade
113+
await new Promise<void>((resolve) => setTimeout(() => resolve(), 200))
114+
115+
await runUpgradeForStudio(studioId)
116+
117+
return ClientAPI.responseSuccess(await updateStudioBaseline(studioId))
115118
}
116119

117120
async getStudioConfig(
@@ -130,7 +133,7 @@ class StudiosServerAPI implements StudiosRestAPI {
130133
_event: string,
131134
studioId: StudioId,
132135
config: object
133-
): Promise<ClientAPI.ClientResponse<void>> {
136+
): Promise<ClientAPI.ClientResponse<string | false>> {
134137
const existingStudio = await Studios.findOneAsync(studioId)
135138
if (!existingStudio) {
136139
throw new Meteor.Error(404, `Studio ${studioId} not found`)
@@ -147,13 +150,15 @@ class StudiosServerAPI implements StudiosRestAPI {
147150

148151
await Studios.upsertAsync(studioId, newStudio)
149152

150-
// wait for the upsert to complete before validation and upgrade read from the studios collection
151-
await new Promise<void>((resolve) => setTimeout(() => resolve(), 200))
152-
153153
const validation = await validateConfigForStudio(studioId)
154154
checkValidation(`updateStudioConfig ${studioId}`, validation.messages)
155155

156-
return ClientAPI.responseSuccess(await runUpgradeForStudio(studioId))
156+
// wait for the upsert to complete before upgrade
157+
await new Promise<void>((resolve) => setTimeout(() => resolve(), 200))
158+
159+
await runUpgradeForStudio(studioId)
160+
161+
return ClientAPI.responseSuccess(await updateStudioBaseline(studioId))
157162
}
158163

159164
async deleteStudio(
@@ -413,7 +418,7 @@ export function registerRoutes(registerRoute: APIRegisterHook<StudiosRestAPI>):
413418
}
414419
)
415420

416-
registerRoute<{ studioId: string }, APIStudio, void>(
421+
registerRoute<{ studioId: string }, APIStudio, string | false>(
417422
'put',
418423
'/studios/:studioId',
419424
new Map([
@@ -444,7 +449,7 @@ export function registerRoutes(registerRoute: APIRegisterHook<StudiosRestAPI>):
444449
}
445450
)
446451

447-
registerRoute<{ studioId: string }, object, void>(
452+
registerRoute<{ studioId: string }, object, string | false>(
448453
'put',
449454
'/studios/:studioId/config',
450455
new Map([

meteor/server/api/rest/v1/typeConversion.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export async function showStyleBaseFrom(
119119
outputLayersWithOverrides: outputLayers,
120120
sourceLayersWithOverrides: sourceLayers,
121121
blueprintConfigWithOverrides: blueprintConfig,
122-
_rundownVersionHash: '',
122+
_rundownVersionHash: showStyleBase?._rundownVersionHash ?? '',
123123
lastBlueprintConfig: undefined,
124124
lastBlueprintFixUpHash: undefined,
125125
}
@@ -339,14 +339,15 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P
339339
? updateOverrides(studio.settingsWithOverrides, studioSettings)
340340
: wrapDefaultObject(studioSettings),
341341
supportedShowStyleBase: apiStudio.supportedShowStyleBase?.map((id) => protectString<ShowStyleBaseId>(id)) ?? [],
342-
mappingsWithOverrides: wrapDefaultObject({}),
343-
routeSetsWithOverrides: wrapDefaultObject({}),
344-
_rundownVersionHash: '',
342+
mappingsWithOverrides: studio?.mappingsWithOverrides ?? wrapDefaultObject({}),
343+
mappingsHash: studio?.mappingsHash,
344+
routeSetsWithOverrides: studio?.routeSetsWithOverrides ?? wrapDefaultObject({}),
345+
_rundownVersionHash: studio?._rundownVersionHash ?? '',
345346
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
346347
packageContainersWithOverrides: wrapDefaultObject({}),
347348
previewContainerIds: [],
348349
thumbnailContainerIds: [],
349-
peripheralDeviceSettings: {
350+
peripheralDeviceSettings: studio?.peripheralDeviceSettings ?? {
350351
deviceSettings: wrapDefaultObject({}),
351352
playoutDevices: wrapDefaultObject({}),
352353
ingestDevices: wrapDefaultObject({}),

meteor/server/lib/rest/v1/studios.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export interface StudiosRestAPI {
5555
event: string,
5656
studioId: StudioId,
5757
studio: APIStudio
58-
): Promise<ClientAPI.ClientResponse<void>>
58+
): Promise<ClientAPI.ClientResponse<string | false>>
5959
/**
6060
* Gets a Studio config, if it exists.
6161
*
@@ -83,7 +83,7 @@ export interface StudiosRestAPI {
8383
event: string,
8484
studioId: StudioId,
8585
config: object
86-
): Promise<ClientAPI.ClientResponse<void>>
86+
): Promise<ClientAPI.ClientResponse<string | false>>
8787
/**
8888
* Deletes a Studio.
8989
*

meteor/server/migration/upgrades/studio.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,19 @@ export async function runUpgradeForStudio(studioId: StudioId): Promise<void> {
7979
span?.end()
8080
}
8181
}
82+
83+
export async function updateStudioBaseline(studioId: StudioId): Promise<string | false> {
84+
logger.info(`Running baseline update for Studio "${studioId}"`)
85+
await getStudio(studioId)
86+
87+
const queuedJob = await QueueStudioJob(StudioJobs.UpdateStudioBaseline, studioId, undefined)
88+
89+
const span = profiler.startSpan('queued-job')
90+
try {
91+
const res = await queuedJob.complete
92+
// explicitly await before returning
93+
return res
94+
} finally {
95+
span?.end()
96+
}
97+
}

packages/blueprints-integration/src/api/studio.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,11 @@ export interface BlueprintResultApplyStudioConfig {
155155
/** Parent device settings */
156156
parentDevices: Record<string, BlueprintParentDeviceSettings>
157157
/** Playout-gateway subdevices */
158-
playoutDevices: Record<string, { parentDeviceName?: string; options: TSR.DeviceOptionsAny }>
158+
playoutDevices: Record<string, { parentConfigId?: string; options: TSR.DeviceOptionsAny }>
159159
/** Ingest-gateway subdevices, the types here depend on the gateway you use */
160-
ingestDevices: Record<string, { parentDeviceName?: string; options: BlueprintMosDeviceConfig | unknown }>
160+
ingestDevices: Record<string, { parentConfigId?: string; options: BlueprintMosDeviceConfig | unknown }>
161161
/** Input-gateway subdevices */
162-
inputDevices: Record<string, { parentDeviceName?: string; options: unknown }>
162+
inputDevices: Record<string, { parentConfigId?: string; options: unknown }>
163163
/** Route Sets */
164164
routeSets?: Record<string, StudioRouteSet>
165165
/** Route Set Exclusivity Groups */

packages/job-worker/src/db/collections.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export interface IDirectCollections {
102102
NrcsIngestDataCache: ICollection<NrcsIngestDataCacheObj>
103103
Parts: ICollection<DBPart>
104104
PartInstances: ICollection<DBPartInstance>
105-
PeripheralDevices: IReadOnlyCollection<PeripheralDevice>
105+
PeripheralDevices: ICollection<PeripheralDevice>
106106
PeripheralDeviceCommands: ICollection<PeripheralDeviceCommand>
107107
Pieces: ICollection<Piece>
108108
PieceInstances: ICollection<PieceInstance>

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

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
StudioRouteSetExclusivityGroup,
1919
} from '@sofie-automation/corelib/dist/dataModel/Studio'
2020
import { Complete, clone, literal } from '@sofie-automation/corelib/dist/lib'
21-
import { protectString } from '@sofie-automation/corelib/dist/protectedString'
21+
import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString'
2222
import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
2323
import { wrapTranslatableMessageFromBlueprints } from '@sofie-automation/corelib/dist/TranslatableMessage'
2424
import {
@@ -31,6 +31,7 @@ import { JobContext } from '../jobs/index.js'
3131
import { FixUpBlueprintConfigContext } from '@sofie-automation/corelib/dist/fixUpBlueprintConfig/context'
3232
import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants'
3333
import { PERIPHERAL_SUBTYPE_PROCESS, PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice'
34+
import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids'
3435

3536
/**
3637
* Run the Blueprint applyConfig for the studio
@@ -64,20 +65,53 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data
6465
])
6566
)
6667

67-
const peripheralDevices = (await context.directCollections.PeripheralDevices.findFetch(
68-
{ subType: PERIPHERAL_SUBTYPE_PROCESS, 'studioAndConfigId.studioId': context.studioId },
68+
const allPeripheralDevices = (await context.directCollections.PeripheralDevices.findFetch(
69+
{ subType: PERIPHERAL_SUBTYPE_PROCESS },
6970
{ projection: { _id: 1, studioAndConfigId: 1 } }
7071
)) as Array<Pick<PeripheralDevice, '_id' | 'studioAndConfigId'>>
7172

73+
const configIdMap = new Map<string, PeripheralDeviceId>() // configId -> deviceId
74+
for (const pd of allPeripheralDevices) {
75+
if (pd.studioAndConfigId) configIdMap.set(pd.studioAndConfigId.configId, pd._id)
76+
}
77+
78+
// Assign configId and name to peripheral devices
79+
for (const configId in parentDevices) {
80+
const peripheralDevice = allPeripheralDevices.find((pd) => unprotectString(pd._id).startsWith(configId))
81+
if (peripheralDevice) {
82+
if (configIdMap.has(configId)) {
83+
// Need to ensure there is only one reference to a configId in the peripheralDevices collection
84+
const existingPeripheralDeviceId = configIdMap.get(configId)
85+
await context.directCollections.PeripheralDevices.update(
86+
{
87+
studioAndConfigId: { studioId: context.studioId, configId: configId },
88+
_id: { $ne: existingPeripheralDeviceId ?? protectString('') },
89+
},
90+
{
91+
$unset: {
92+
studioAndConfigId: 1,
93+
},
94+
}
95+
)
96+
configIdMap.delete(configId)
97+
}
98+
await context.directCollections.PeripheralDevices.update(peripheralDevice._id, {
99+
$set: {
100+
studioAndConfigId: { studioId: context.studioId, configId: configId },
101+
},
102+
})
103+
configIdMap.set(configId, peripheralDevice._id)
104+
}
105+
}
106+
72107
const playoutDevices = Object.fromEntries(
73-
Object.entries<{ parentDeviceName?: string; options: TSR.DeviceOptionsAny }>(result.playoutDevices ?? {}).map(
108+
Object.entries<{ parentConfigId?: string; options: TSR.DeviceOptionsAny }>(result.playoutDevices ?? {}).map(
74109
(dev) => {
110+
const parentConfigId = dev[1].parentConfigId
75111
return [
76112
dev[0],
77113
literal<Complete<StudioPlayoutDevice>>({
78-
peripheralDeviceId: peripheralDevices.find(
79-
(p) => p.studioAndConfigId?.configId === dev[1].parentDeviceName
80-
)?._id,
114+
peripheralDeviceId: parentConfigId ? configIdMap.get(parentConfigId) : undefined,
81115
options: dev[1].options,
82116
}),
83117
]
@@ -86,27 +120,25 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data
86120
)
87121

88122
const ingestDevices = Object.fromEntries(
89-
Object.entries<{ parentDeviceName?: string; options: unknown }>(result.ingestDevices ?? {}).map((dev) => {
123+
Object.entries<{ parentConfigId?: string; options: unknown }>(result.ingestDevices ?? {}).map((dev) => {
124+
const parentConfigId = dev[1].parentConfigId
90125
return [
91126
dev[0],
92127
literal<Complete<StudioIngestDevice>>({
93-
peripheralDeviceId: peripheralDevices.find(
94-
(p) => p.studioAndConfigId?.configId === dev[1].parentDeviceName
95-
)?._id,
128+
peripheralDeviceId: parentConfigId ? configIdMap.get(parentConfigId) : undefined,
96129
options: dev[1].options,
97130
}),
98131
]
99132
})
100133
)
101134

102135
const inputDevices = Object.fromEntries(
103-
Object.entries<{ parentDeviceName?: string; options: unknown }>(result.inputDevices ?? {}).map((dev) => {
136+
Object.entries<{ parentConfigId?: string; options: unknown }>(result.inputDevices ?? {}).map((dev) => {
137+
const parentConfigId = dev[1].parentConfigId
104138
return [
105139
dev[0],
106140
literal<Complete<StudioInputDevice>>({
107-
peripheralDeviceId: peripheralDevices.find(
108-
(p) => p.studioAndConfigId?.configId === dev[1].parentDeviceName
109-
)?._id,
141+
peripheralDeviceId: parentConfigId ? configIdMap.get(parentConfigId) : undefined,
110142
options: dev[1].options,
111143
}),
112144
]

packages/mos-gateway/src/$schemas/options.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@
4444
"default": 10542
4545
}
4646
},
47-
"required": ["lower", "upper", "query"],
47+
"required": [],
4848
"additionalProperties": false
4949
}
5050
},
51-
"required": ["mosId"],
51+
"required": ["mosId", "ports"],
5252
"additionalProperties": false
5353
}

packages/webui/src/client/ui/Settings/SettingsMenu.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,7 @@ function SettingsMenuPeripheralDevice({ device }: Readonly<SettingsMenuPeriphera
530530
<p>
531531
{device.connected ? t('Connected') : t('Disconnected')}, {t('Status')}:{' '}
532532
{statusCodeString(t, device.status.statusCode)}
533+
{configIdString(t, device.studioAndConfigId?.configId)}
533534
</p>
534535
</NavLink>
535536
<hr className="vsubtle" />
@@ -558,3 +559,8 @@ function statusCodeString(t: TFunction, statusCode: StatusCode): string {
558559
return t('Fatal')
559560
}
560561
}
562+
563+
function configIdString(t: TFunction, configId: string | undefined): string {
564+
if (configId) return t(', Config ID: ') + configId
565+
else return t(', Unconfigured')
566+
}

packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function GenericSubDevicesTable({
5050
device._id,
5151
literal<PeripheralDeviceTranslated>({
5252
_id: device._id,
53-
name: device.name || unprotectString(device._id),
53+
name: device.studioAndConfigId?.configId || device.name || unprotectString(device._id),
5454
subdeviceConfigSchema: device.configManifest?.subdeviceConfigSchema,
5555
subdeviceManifest: device.configManifest?.subdeviceManifest ?? {},
5656
})
@@ -115,7 +115,7 @@ export function GenericSubDevicesTable({
115115
<thead>
116116
<tr className="hl">
117117
<th key="ID">ID</th>
118-
<th key="Parent">{t('Parent')}</th>
118+
<th key="Parent">{t('Parent Config ID')}</th>
119119
<th key="Type">{t('Type')}</th>
120120
<th key="action">&nbsp;</th>
121121
</tr>
@@ -282,7 +282,7 @@ function SubDeviceEditRow({
282282
<td colSpan={99}>
283283
<div className="properties-grid">
284284
<LabelAndOverridesForDropdown
285-
label={t('Peripheral Device ID')}
285+
label={t('Parent Config ID')}
286286
item={item}
287287
overrideHelper={overrideHelper}
288288
itemKey={'peripheralDeviceId'}

0 commit comments

Comments
 (0)