Skip to content

Commit

Permalink
Merge pull request #107 from nrkno/fix/sofie-snapshots
Browse files Browse the repository at this point in the history
Quick-MOS: Add support for Sofie Snapshots (SOFIE-3554)
  • Loading branch information
nytamin authored Oct 16, 2024
2 parents a43c217 + a4e410e commit a348b97
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 58 deletions.
4 changes: 1 addition & 3 deletions packages/connector/src/MosConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,9 +544,7 @@ export class MosConnection extends EventEmitter<MosConnectionEvents> implements
{
ID: this.mosTypes.mosString128.create('0'),
Revision: 0,
Description: this.mosTypes.mosString128.create(
`MosDevice "${ncsID + '_' + mosID}" not found`
),
Description: this.mosTypes.mosString128.create(`Internal error: ${err}`),
Status: IMOSAckStatus.NACK,
},
this.mosTypes.strict
Expand Down
2 changes: 2 additions & 0 deletions packages/model/src/mosTypes/__tests__/mosDuration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ describe('MosDuration', () => {
expect(mosTypes.mosDuration.is(null)).toBe(false)
expect(mosTypes.mosDuration.is('abc')).toBe(false)
expect(mosTypes.mosDuration.is(123)).toBe(false)

expect(mosTypes.mosDuration.is({ _mosDuration: 1234 })).toBe(true)
})
test('stringify', () => {
const mosTypes = getMosTypes(true)
Expand Down
2 changes: 1 addition & 1 deletion packages/model/src/mosTypes/mosDuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface IMOSDuration {
export function create(anyValue: AnyValue, strict: boolean): IMOSDuration {
let value: number
if (typeof anyValue === 'number') {
value = anyValue
value = anyValue // seconds
} else if (typeof anyValue === 'string') {
const m = /(\d+):(\d+):(\d+)/.exec(anyValue)
if (!m) throw new Error(`MosDuration: Invalid input format: "${anyValue}"!`)
Expand Down
4 changes: 2 additions & 2 deletions packages/model/src/mosTypes/mosTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,14 @@ export function create(timestamp: AnyValue, strict: boolean): IMOSTime {
} else if (!strict) {
time = new Date()
} else {
throw new Error(`MosTime: Invalid input: "${timestamp}"`)
throw new Error(`MosTime: Invalid input: ${JSON.stringify(timestamp)}`)
}
} else {
throw new Error(`MosTime: Invalid input: "${timestamp}"`)
}

if (isNaN(time.getTime())) {
throw new Error(`MosTime: Invalid timestamp: "${timestamp}"`)
throw new Error(`MosTime: Invalid timestamp: ${JSON.stringify(timestamp)}`)
}

const iMosTime: IMOSTime = {
Expand Down
23 changes: 17 additions & 6 deletions packages/quick-mos/input/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,30 @@ export const config: Config = {
// This is the NCS-id, you might need to specify it in your mos-client that connects to Quick-MOS.
mosID: 'quick.mos',
acceptsConnections: true,
openRelay: true,
openRelay: {
options: {
id: 'testid',
host: 'testhost',
// ports: {
// Set these if you have a mos-client running on other ports than standard:
// lower: 11540,
// upper: 11541,
// query: 11542,
// },
},
},
profiles: {
'0': true,
'1': true,
'2': true,
'3': true,
},
// Set these if you want quick-mos to run on other ports than standard:
// ports: {
// lower: 11540,
// upper: 11541,
// query: 11542,
// },
ports: {
lower: 11540,
upper: 11541,
query: 11542,
},

// Set to true to turn on debug-logging:
debug: false,
Expand Down
9 changes: 9 additions & 0 deletions packages/quick-mos/input/runningorders/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Quick-MOS will monitor the contents of this folder.

Put any RunningOrders that you want the Quick-MOS server to expose in here.

Quick-MOS supports

- ts files (see examples in the folder)
- json files (see examples in the folder)
- Sofie Playlist/Rundown Snapshots
2 changes: 1 addition & 1 deletion packages/quick-mos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"description": "Read rundowns from files, use mos-connection and send mos commands",
"main": "dist/index.js",
"main": "src/index.ts",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
82 changes: 82 additions & 0 deletions packages/quick-mos/src/convertFromSofieSnapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { getMosTypes, IMOSROFullStory, IMOSROStory, IMOSRunningOrder } from '@mos-connection/model'

/* eslint-disable @typescript-eslint/explicit-module-boundary-types */

export function convertFromSofieSnapshot(
filePath: string,
snapShotData: any
): { ro: IMOSRunningOrder; stories: IMOSROFullStory[]; readyToAir: boolean }[] {
const output: { ro: IMOSRunningOrder; stories: IMOSROFullStory[]; readyToAir: boolean }[] = []
const mosTypes = getMosTypes(true)

const snapshot = snapShotData.ingestData

const rundownData = snapshot.filter((e: any) => e.type === 'rundown')
const segmentData = snapshot.filter((e: any) => e.type === 'segment')
const partData = snapshot.filter((e: any) => e.type === 'part')

if (rundownData.length === 0) throw new Error(`Got ${rundownData.length} rundown ingest data. Can't continue`)

for (const seg of segmentData) {
let parts = partData.filter((e: any) => e.segmentId === seg.segmentId)
parts = parts.map((e: any) => e.data)
parts = parts.sort((a: any, b: any) => b.rank - a.rank)

seg.data.parts = parts
}

rundownData.forEach((rundown: any, rundownIndex: number) => {
const segments0 = segmentData.filter((e: any) => e.rundownId === rundown.rundownId)

let segments = segments0.map((s: any) => s.data)
segments = segments.sort((a: any, b: any) => b.rank - a.rank)

const fullStories: IMOSROFullStory[] = []
const stories: IMOSROStory[] = []

segments.sort((a: any, b: any) => (a.rank || 0) - (b.rank || 0))

for (const segment of segments) {
segment.parts.sort((a: any, b: any) => (a.rank || 0) - (b.rank || 0))

for (const part of segment.parts) {
fullStories.push(part.payload)
stories.push({
ID: part.payload.ID,
Slug: part.name,
Items: [],
})
}
}

const runningOrder: IMOSRunningOrder = {
...rundown.data.payload,
ID: mosTypes.mosString128.create(filePath.replace(/\W/g, '_') + `_${rundownIndex}`),
Stories: stories,
EditorialStart: mosTypes.mosTime.create(rundown.data.payload.EditorialStart),
EditorialDuration: mosTypes.mosDuration.create(rundown.data.payload.EditorialDuration),
}

output.push({
ro: runningOrder,
stories: fixStoryBody(fullStories),
readyToAir: rundown.data.readyToAir || false,
})
})
return output
}

function fixStoryBody(stories: any[]) {
for (const story of stories) {
for (const item of story.Body) {
if (item.Type === 'p' && item.Content) {
if (item.Content['@type'] === 'element') {
delete item.Content
} else if (item.Content['@type'] === 'text') {
item.Content = item.Content['text']
}
}
}
}
return stories
}
105 changes: 60 additions & 45 deletions packages/quick-mos/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable node/no-unpublished-import */
import * as chokidar from 'chokidar'

import * as fs from 'fs'
Expand All @@ -17,13 +18,12 @@ import {
} from '@mos-connection/connector'
import { diffLists, ListEntry, OperationType } from './mosDiff'
import * as crypto from 'crypto'
import { convertFromSofieSnapshot } from './convertFromSofieSnapshot'

console.log('Starting Quick-MOS')

const DELAY_TIME = 300 // ms

// const tsr = new TSRHandler(console.log)

const watcher = chokidar.watch('input/**', { ignored: /^\./, persistent: true })

const simulateFrequentEditing = false
Expand Down Expand Up @@ -80,31 +80,15 @@ function triggerReload() {
}
}, DELAY_TIME)
}
function loadFile(requirePath: string) {
function loadFile(requirePath: string): any {
delete require.cache[require.resolve(requirePath)]
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mosData = require(requirePath)
if (mosData.runningOrder?.EditorialStart && !mosTypes.mosTime.is(mosData.runningOrder.EditorialStart)) {
mosData.runningOrder.EditorialStart = mosTypes.mosTime.create(mosData.runningOrder.EditorialStart._time)
}

if (mosData.runningOrder?.EditorialDuration && !mosTypes.mosDuration.is(mosData.runningOrder.EditorialDuration)) {
let s = mosData.runningOrder.EditorialDuration._duration
const hh = Math.floor(s / 3600)
s -= hh * 3600

const mm = Math.floor(s / 60)
s -= mm * 60

const ss = Math.floor(s)

mosData.runningOrder.EditorialDuration = mosTypes.mosDuration.create(hh + ':' + mm + ':' + ss)
}
const content = require(requirePath)

return mosData
return content
}
const monitors: { [id: string]: MOSMonitor } = {}
const runningOrderIds: { [id: string]: number } = {}
const runningOrderIds: { [id: string]: string } = {}

async function reloadInner() {
const newConfig: Config = loadFile('../input/config.ts').config
Expand Down Expand Up @@ -133,7 +117,7 @@ async function reloadInner() {
mos.mosConnection.onConnection((mosDevice: MosDevice) => {
console.log('new mos connection', mosDevice.ID)

mosDevice.onGetMachineInfo(async () => {
mosDevice.onRequestMachineInfo(async () => {
const machineInfo: IMOSListMachInfo = {
manufacturer: mosTypes.mosString128.create('<<<Mock Manufacturer>>>'),
model: mosTypes.mosString128.create('<<<Mock model>>>'),
Expand Down Expand Up @@ -196,13 +180,14 @@ async function reloadInner() {
// mosDevice.onMosReqSearchableSchema((username: string) => Promise<IMOSSearchableSchema>): void;
// mosDevice.onMosReqObjectList((objList: IMosRequestObjectList) => Promise<IMosObjectList>): void;
// mosDevice.onMosReqObjectAction((action: string, obj: IMOSObject) => Promise<IMOSAck>): void;
mosDevice.onROReqAll(async () => {
mosDevice.onRequestAllRunningOrders(async () => {
const ros = fetchRunningOrders()
return Promise.resolve(ros.map((r) => r.ro))
if (!ros) return []
return ros.map((r) => r.ro)
})
mosDevice.onRequestRunningOrder(async (roId) => {
const ro = monitors[mosId].resendRunningOrder(roId as any as string)
return Promise.resolve(ro)
const ro = monitors[mosId].resendRunningOrder(mosTypes.mosString128.stringify(roId))
return ro
})
// mosDevice.onROStory((story: IMOSROFullStory) => Promise<IMOSROAck>): void;
setTimeout(() => {
Expand All @@ -222,57 +207,84 @@ async function reloadInner() {
}
function refreshFiles() {
// Check data
const t = Date.now()
_.each(fetchRunningOrders(), (r) => {
const timestamp = `${Date.now()}`
for (const r of fetchRunningOrders() || []) {
const runningOrder = r.ro
const stories = r.stories
const readyToAir = r.readyToAir

const id = mosTypes.mosString128.stringify(runningOrder.ID)
runningOrderIds[id] = t
runningOrderIds[id] = timestamp
if (_.isEmpty(monitors)) {
fakeOnUpdatedRunningOrder(runningOrder, stories)
} else {
_.each(monitors, (monitor) => {
monitor.onUpdatedRunningOrder(runningOrder, stories, readyToAir)
})
}
})
_.each(runningOrderIds, (oldT, id) => {
if (oldT !== t) {
}
for (const [oldT, id] of Object.entries<string>(runningOrderIds)) {
if (oldT !== timestamp) {
_.each(monitors, (monitor) => {
monitor.onDeletedRunningOrder(id)
})
}
})
}
}
function fetchRunningOrders() {
const runningOrders: { ro: IMOSRunningOrder; stories: IMOSROFullStory[]; readyToAir: boolean }[] = []
_.each(getAllFilesInDirectory('input/runningorders'), (filePath) => {
for (const filePath of getAllFilesInDirectory('input/runningorders')) {
const requirePath = '../' + filePath.replace(/\\/g, '/')
try {
if (
requirePath.match(/[/\\]_/) || // ignore and folders files that begin with "_"
requirePath.match(/[/\\]lib\.ts/) // ignore lib files
) {
return
continue
}
if (filePath.match(/(\.ts|.json)$/)) {
const fileContents = loadFile(requirePath)
const ro: IMOSRunningOrder = fileContents.runningOrder
ro.ID = mosTypes.mosString128.create(filePath.replace(/\W/g, '_'))

runningOrders.push({
ro,
stories: fileContents.fullStories,
readyToAir: fileContents.READY_TO_AIR,
})
if (fileContents.runningOrder) {
const ro = fileContents.runningOrder
ro.ID = mosTypes.mosString128.create(filePath.replace(/\W/g, '_'))

if (ro.EditorialStart && !mosTypes.mosTime.is(ro.EditorialStart)) {
ro.EditorialStart = mosTypes.mosTime.create(ro.EditorialStart._time)
}

if (
ro.EditorialDuration &&
!mosTypes.mosDuration.is(ro.EditorialDuration) &&
typeof ro.EditorialDuration._duration === 'number'
) {
ro.EditorialDuration = mosTypes.mosDuration.create(ro.EditorialDuration._duration)
}

runningOrders.push({
ro,
stories: fileContents.stories,
readyToAir: fileContents.READY_TO_AIR,
})
} else if (fileContents.snapshot && fileContents.snapshot.type === 'rundownplaylist') {
// Is a Sofie snapshot
convertFromSofieSnapshot(filePath, fileContents).forEach(({ ro, stories, readyToAir }) => {
runningOrders.push({
ro,
stories,
readyToAir,
})
})
} else {
throw new Error('Unsupported file')
}
}
} catch (err) {
console.log(`Error when parsing file "${requirePath}"`)
throw err
}
})
}

return runningOrders
}
function getAllFilesInDirectory(dir: string): string[] {
Expand Down Expand Up @@ -372,7 +384,10 @@ class MOSMonitor {
this.triggerCheckQueue()
}, 100)
return local.ro
} else throw new Error(`ro ${roId} not found`)
} else {
console.log('ros', Object.keys(this.ros))
throw new Error(`ro ${roId} not found`)
}
}
onUpdatedRunningOrder(ro: IMOSRunningOrder, fullStories: IMOSROFullStory[], readyToAir: boolean | undefined): void {
// compare with
Expand Down

0 comments on commit a348b97

Please sign in to comment.