Skip to content

Commit ab2d17e

Browse files
committed
fix: Quick-MOS: Add support for Sofie Snapshots
1 parent f19d38a commit ab2d17e

File tree

5 files changed

+168
-52
lines changed

5 files changed

+168
-52
lines changed

packages/quick-mos/input/config.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,30 @@ export const config: Config = {
55
// This is the NCS-id, you might need to specify it in your mos-client that connects to Quick-MOS.
66
mosID: 'quick.mos',
77
acceptsConnections: true,
8-
openRelay: true,
8+
openRelay: {
9+
options: {
10+
id: 'testid',
11+
host: 'testhost',
12+
// ports: {
13+
// Set these if you have a mos-client running on other ports than standard:
14+
// lower: 11540,
15+
// upper: 11541,
16+
// query: 11542,
17+
// },
18+
},
19+
},
920
profiles: {
1021
'0': true,
1122
'1': true,
1223
'2': true,
1324
'3': true,
1425
},
1526
// Set these if you want quick-mos to run on other ports than standard:
16-
// ports: {
17-
// lower: 11540,
18-
// upper: 11541,
19-
// query: 11542,
20-
// },
27+
ports: {
28+
lower: 11540,
29+
upper: 11541,
30+
query: 11542,
31+
},
2132

2233
// Set to true to turn on debug-logging:
2334
debug: false,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Quick-MOS will monitor the contents of this folder.
2+
3+
Put any RunningOrders that you want the Quick-MOS server to expose in here.
4+
5+
Quick-MOS supports
6+
7+
- ts files (see examples in the folder)
8+
- json files (see examples in the folder)
9+
- Sofie Playlist/Rundown Snapshots

packages/quick-mos/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.0.0",
44
"private": true,
55
"description": "Read rundowns from files, use mos-connection and send mos commands",
6-
"main": "dist/index.js",
6+
"main": "src/index.ts",
77
"license": "MIT",
88
"repository": {
99
"type": "git",
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { getMosTypes, IMOSROFullStory, IMOSROStory, IMOSRunningOrder } from '@mos-connection/model'
2+
3+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
4+
5+
export function convertFromSofieSnapshot(
6+
filePath: string,
7+
snapShotData: any
8+
): { ro: IMOSRunningOrder; stories: IMOSROFullStory[]; readyToAir: boolean }[] {
9+
const output: { ro: IMOSRunningOrder; stories: IMOSROFullStory[]; readyToAir: boolean }[] = []
10+
const mosTypes = getMosTypes(true)
11+
12+
const snapshot = snapShotData.ingestData
13+
14+
const rundownData = snapshot.filter((e: any) => e.type === 'rundown')
15+
const segmentData = snapshot.filter((e: any) => e.type === 'segment')
16+
const partData = snapshot.filter((e: any) => e.type === 'part')
17+
18+
if (rundownData.length === 0) throw new Error(`Got ${rundownData.length} rundown ingest data. Can't continue`)
19+
20+
for (const seg of segmentData) {
21+
let parts = partData.filter((e: any) => e.segmentId === seg.segmentId)
22+
parts = parts.map((e: any) => e.data)
23+
parts = parts.sort((a: any, b: any) => b.rank - a.rank)
24+
25+
seg.data.parts = parts
26+
}
27+
28+
rundownData.forEach((rundown: any, rundownIndex: number) => {
29+
const segments0 = segmentData.filter((e: any) => e.rundownId === rundown.rundownId)
30+
31+
let segments = segments0.map((s: any) => s.data)
32+
segments = segments.sort((a: any, b: any) => b.rank - a.rank)
33+
34+
const fullStories: IMOSROFullStory[] = []
35+
const stories: IMOSROStory[] = []
36+
37+
segments.sort((a: any, b: any) => (a.rank || 0) - (b.rank || 0))
38+
39+
for (const segment of segments) {
40+
segment.parts.sort((a: any, b: any) => (a.rank || 0) - (b.rank || 0))
41+
42+
for (const part of segment.parts) {
43+
fullStories.push(part.payload)
44+
stories.push({
45+
ID: part.payload.ID,
46+
Slug: part.name,
47+
Items: [],
48+
})
49+
}
50+
}
51+
52+
const runningOrder: IMOSRunningOrder = {
53+
...rundown.data.payload,
54+
ID: mosTypes.mosString128.create(filePath.replace(/\W/g, '_') + `_${rundownIndex}`),
55+
Stories: stories,
56+
EditorialStart: mosTypes.mosTime.create(rundown.data.payload.EditorialStart),
57+
EditorialDuration: mosTypes.mosDuration.create(rundown.data.payload.EditorialDuration),
58+
}
59+
60+
output.push({
61+
ro: runningOrder,
62+
stories: fixStoryBody(fullStories),
63+
readyToAir: rundown.data.readyToAir || false,
64+
})
65+
})
66+
return output
67+
}
68+
69+
function fixStoryBody(stories: any[]) {
70+
for (const story of stories) {
71+
for (const item of story.Body) {
72+
if (item.Type === 'p' && item.Content) {
73+
if (item.Content['@type'] === 'element') {
74+
delete item.Content
75+
} else if (item.Content['@type'] === 'text') {
76+
item.Content = item.Content['text']
77+
}
78+
}
79+
}
80+
}
81+
return stories
82+
}

packages/quick-mos/src/index.ts

Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@ import {
1717
} from '@mos-connection/connector'
1818
import { diffLists, ListEntry, OperationType } from './mosDiff'
1919
import * as crypto from 'crypto'
20+
import { convertFromSofieSnapshot } from './convertFromSofieSnapshot'
2021

2122
console.log('Starting Quick-MOS')
2223

2324
const DELAY_TIME = 300 // ms
2425

25-
// const tsr = new TSRHandler(console.log)
26-
2726
const watcher = chokidar.watch('input/**', { ignored: /^\./, persistent: true })
2827

2928
const simulateFrequentEditing = false
@@ -80,31 +79,15 @@ function triggerReload() {
8079
}
8180
}, DELAY_TIME)
8281
}
83-
function loadFile(requirePath: string) {
82+
function loadFile(requirePath: string): any {
8483
delete require.cache[require.resolve(requirePath)]
8584
// eslint-disable-next-line @typescript-eslint/no-var-requires
86-
const mosData = require(requirePath)
87-
if (mosData.runningOrder?.EditorialStart && !mosTypes.mosTime.is(mosData.runningOrder.EditorialStart)) {
88-
mosData.runningOrder.EditorialStart = mosTypes.mosTime.create(mosData.runningOrder.EditorialStart._time)
89-
}
90-
91-
if (mosData.runningOrder?.EditorialDuration && !mosTypes.mosDuration.is(mosData.runningOrder.EditorialDuration)) {
92-
let s = mosData.runningOrder.EditorialDuration._duration
93-
const hh = Math.floor(s / 3600)
94-
s -= hh * 3600
95-
96-
const mm = Math.floor(s / 60)
97-
s -= mm * 60
98-
99-
const ss = Math.floor(s)
100-
101-
mosData.runningOrder.EditorialDuration = mosTypes.mosDuration.create(hh + ':' + mm + ':' + ss)
102-
}
85+
const content = require(requirePath)
10386

104-
return mosData
87+
return content
10588
}
10689
const monitors: { [id: string]: MOSMonitor } = {}
107-
const runningOrderIds: { [id: string]: number } = {}
90+
const runningOrderIds: { [id: string]: string } = {}
10891

10992
async function reloadInner() {
11093
const newConfig: Config = loadFile('../input/config.ts').config
@@ -133,7 +116,7 @@ async function reloadInner() {
133116
mos.mosConnection.onConnection((mosDevice: MosDevice) => {
134117
console.log('new mos connection', mosDevice.ID)
135118

136-
mosDevice.onGetMachineInfo(async () => {
119+
mosDevice.onRequestMachineInfo(async () => {
137120
const machineInfo: IMOSListMachInfo = {
138121
manufacturer: mosTypes.mosString128.create('<<<Mock Manufacturer>>>'),
139122
model: mosTypes.mosString128.create('<<<Mock model>>>'),
@@ -196,13 +179,14 @@ async function reloadInner() {
196179
// mosDevice.onMosReqSearchableSchema((username: string) => Promise<IMOSSearchableSchema>): void;
197180
// mosDevice.onMosReqObjectList((objList: IMosRequestObjectList) => Promise<IMosObjectList>): void;
198181
// mosDevice.onMosReqObjectAction((action: string, obj: IMOSObject) => Promise<IMOSAck>): void;
199-
mosDevice.onROReqAll(async () => {
182+
mosDevice.onRequestAllRunningOrders(async () => {
200183
const ros = fetchRunningOrders()
201-
return Promise.resolve(ros.map((r) => r.ro))
184+
if (!ros) return []
185+
return ros.map((r) => r.ro)
202186
})
203187
mosDevice.onRequestRunningOrder(async (roId) => {
204-
const ro = monitors[mosId].resendRunningOrder(roId as any as string)
205-
return Promise.resolve(ro)
188+
const ro = monitors[mosId].resendRunningOrder(mosTypes.mosString128.stringify(roId))
189+
return ro
206190
})
207191
// mosDevice.onROStory((story: IMOSROFullStory) => Promise<IMOSROAck>): void;
208192
setTimeout(() => {
@@ -222,57 +206,84 @@ async function reloadInner() {
222206
}
223207
function refreshFiles() {
224208
// Check data
225-
const t = Date.now()
226-
_.each(fetchRunningOrders(), (r) => {
209+
const timestamp = `${Date.now()}`
210+
for (const r of fetchRunningOrders() || []) {
227211
const runningOrder = r.ro
228212
const stories = r.stories
229213
const readyToAir = r.readyToAir
230214

231215
const id = mosTypes.mosString128.stringify(runningOrder.ID)
232-
runningOrderIds[id] = t
216+
runningOrderIds[id] = timestamp
233217
if (_.isEmpty(monitors)) {
234218
fakeOnUpdatedRunningOrder(runningOrder, stories)
235219
} else {
236220
_.each(monitors, (monitor) => {
237221
monitor.onUpdatedRunningOrder(runningOrder, stories, readyToAir)
238222
})
239223
}
240-
})
241-
_.each(runningOrderIds, (oldT, id) => {
242-
if (oldT !== t) {
224+
}
225+
for (const [oldT, id] of Object.entries<string>(runningOrderIds)) {
226+
if (oldT !== timestamp) {
243227
_.each(monitors, (monitor) => {
244228
monitor.onDeletedRunningOrder(id)
245229
})
246230
}
247-
})
231+
}
248232
}
249233
function fetchRunningOrders() {
250234
const runningOrders: { ro: IMOSRunningOrder; stories: IMOSROFullStory[]; readyToAir: boolean }[] = []
251-
_.each(getAllFilesInDirectory('input/runningorders'), (filePath) => {
235+
for (const filePath of getAllFilesInDirectory('input/runningorders')) {
252236
const requirePath = '../' + filePath.replace(/\\/g, '/')
253237
try {
254238
if (
255239
requirePath.match(/[/\\]_/) || // ignore and folders files that begin with "_"
256240
requirePath.match(/[/\\]lib\.ts/) // ignore lib files
257241
) {
258-
return
242+
continue
259243
}
260244
if (filePath.match(/(\.ts|.json)$/)) {
261245
const fileContents = loadFile(requirePath)
262-
const ro: IMOSRunningOrder = fileContents.runningOrder
263-
ro.ID = mosTypes.mosString128.create(filePath.replace(/\W/g, '_'))
264246

265-
runningOrders.push({
266-
ro,
267-
stories: fileContents.fullStories,
268-
readyToAir: fileContents.READY_TO_AIR,
269-
})
247+
if (fileContents.runningOrder) {
248+
const ro = fileContents.runningOrder
249+
ro.ID = mosTypes.mosString128.create(filePath.replace(/\W/g, '_'))
250+
251+
if (ro.EditorialStart && !mosTypes.mosTime.is(ro.EditorialStart)) {
252+
ro.EditorialStart = mosTypes.mosTime.create(ro.EditorialStart._time)
253+
}
254+
255+
if (
256+
ro.EditorialDuration &&
257+
!mosTypes.mosDuration.is(ro.EditorialDuration) &&
258+
typeof ro.EditorialDuration._duration === 'number'
259+
) {
260+
ro.EditorialDuration = mosTypes.mosDuration.create(ro.EditorialDuration._duration)
261+
}
262+
263+
runningOrders.push({
264+
ro,
265+
stories: fileContents.stories,
266+
readyToAir: fileContents.READY_TO_AIR,
267+
})
268+
} else if (fileContents.snapshot && fileContents.snapshot.type === 'rundownplaylist') {
269+
// Is a Sofie snapshot
270+
convertFromSofieSnapshot(filePath, fileContents).forEach(({ ro, stories, readyToAir }) => {
271+
runningOrders.push({
272+
ro,
273+
stories,
274+
readyToAir,
275+
})
276+
})
277+
} else {
278+
throw new Error('Unsupported file')
279+
}
270280
}
271281
} catch (err) {
272282
console.log(`Error when parsing file "${requirePath}"`)
273283
throw err
274284
}
275-
})
285+
}
286+
276287
return runningOrders
277288
}
278289
function getAllFilesInDirectory(dir: string): string[] {
@@ -372,7 +383,10 @@ class MOSMonitor {
372383
this.triggerCheckQueue()
373384
}, 100)
374385
return local.ro
375-
} else throw new Error(`ro ${roId} not found`)
386+
} else {
387+
console.log('ros', Object.keys(this.ros))
388+
throw new Error(`ro ${roId} not found`)
389+
}
376390
}
377391
onUpdatedRunningOrder(ro: IMOSRunningOrder, fullStories: IMOSROFullStory[], readyToAir: boolean | undefined): void {
378392
// compare with

0 commit comments

Comments
 (0)