From e451a6e228b4ebe1d8ccce090e987379f110b64d Mon Sep 17 00:00:00 2001 From: Balte de Wit Date: Fri, 13 Jan 2023 11:34:52 +0100 Subject: [PATCH 01/11] 7.5.0-release47.4 --- CHANGELOG.md | 15 +++++++++++++++ lerna.json | 2 +- .../timeline-state-resolver-types/CHANGELOG.md | 7 +++++++ .../timeline-state-resolver-types/package.json | 2 +- packages/timeline-state-resolver/CHANGELOG.md | 15 +++++++++++++++ packages/timeline-state-resolver/package.json | 4 ++-- 6 files changed, 41 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc25cf9dc4..c86281f154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [7.5.0-release47.4](https://github.com/nrkno/sofie-timeline-state-resolver/compare/7.5.0-release47.3...7.5.0-release47.4) (2023-01-13) + + +### Bug Fixes + +* add optional parameter to HTTPSend timelineObj: paramsType ([979dc61](https://github.com/nrkno/sofie-timeline-state-resolver/commit/979dc61748c4c371a8b17c7fd8c5929c69f747d9)) +* add support for Node 18 ([6242dd6](https://github.com/nrkno/sofie-timeline-state-resolver/commit/6242dd68f54a491aa71bdfd30b066550d6f7e90e)) +* bug fix: HTTPSend device didn't send GET requests ([8315531](https://github.com/nrkno/sofie-timeline-state-resolver/commit/83155314706497a9c630dbde14d5c5d7e57103cf)) +* prevent in place reverse in setDatastore ([473ab71](https://github.com/nrkno/sofie-timeline-state-resolver/commit/473ab713785325c2062db983c8ece80ea5dede4d)) +* track ccg state internally ([fd5596f](https://github.com/nrkno/sofie-timeline-state-resolver/commit/fd5596fcf975a7a122c6fb21946f13c2e97a4233)) + + + + + # [7.5.0-release47.3](https://github.com/nrkno/sofie-timeline-state-resolver/compare/7.5.0-release47.2...7.5.0-release47.3) (2022-11-07) **Note:** Version bump only for package timeline-state-resolver-packages diff --git a/lerna.json b/lerna.json index 28c3ebf83a..9d3e37b7f0 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "7.5.0-release47.3", + "version": "7.5.0-release47.4", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/timeline-state-resolver-types/CHANGELOG.md b/packages/timeline-state-resolver-types/CHANGELOG.md index bafb4d5a4b..2f638e62a1 100644 --- a/packages/timeline-state-resolver-types/CHANGELOG.md +++ b/packages/timeline-state-resolver-types/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [7.5.0-release47.4](https://github.com/nrkno/sofie-timeline-state-resolver/compare/7.5.0-release47.3...7.5.0-release47.4) (2023-01-13) + +### Bug Fixes + +- add optional parameter to HTTPSend timelineObj: paramsType ([979dc61](https://github.com/nrkno/sofie-timeline-state-resolver/commit/979dc61748c4c371a8b17c7fd8c5929c69f747d9)) +- add support for Node 18 ([6242dd6](https://github.com/nrkno/sofie-timeline-state-resolver/commit/6242dd68f54a491aa71bdfd30b066550d6f7e90e)) + # [7.5.0-release47.3](https://github.com/nrkno/sofie-timeline-state-resolver/compare/7.5.0-release47.2...7.5.0-release47.3) (2022-11-07) **Note:** Version bump only for package timeline-state-resolver-types diff --git a/packages/timeline-state-resolver-types/package.json b/packages/timeline-state-resolver-types/package.json index 5ac47ddadc..b45d3d8ceb 100644 --- a/packages/timeline-state-resolver-types/package.json +++ b/packages/timeline-state-resolver-types/package.json @@ -1,6 +1,6 @@ { "name": "timeline-state-resolver-types", - "version": "7.5.0-release47.3", + "version": "7.5.0-release47.4", "description": "Have timeline, control stuff", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/timeline-state-resolver/CHANGELOG.md b/packages/timeline-state-resolver/CHANGELOG.md index 2af32bba5e..1f2072abd4 100644 --- a/packages/timeline-state-resolver/CHANGELOG.md +++ b/packages/timeline-state-resolver/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [7.5.0-release47.4](https://github.com/nrkno/tv-automation-state-timeline-resolver/compare/7.5.0-release47.3...7.5.0-release47.4) (2023-01-13) + + +### Bug Fixes + +* add optional parameter to HTTPSend timelineObj: paramsType ([979dc61](https://github.com/nrkno/tv-automation-state-timeline-resolver/commit/979dc61748c4c371a8b17c7fd8c5929c69f747d9)) +* add support for Node 18 ([6242dd6](https://github.com/nrkno/tv-automation-state-timeline-resolver/commit/6242dd68f54a491aa71bdfd30b066550d6f7e90e)) +* bug fix: HTTPSend device didn't send GET requests ([8315531](https://github.com/nrkno/tv-automation-state-timeline-resolver/commit/83155314706497a9c630dbde14d5c5d7e57103cf)) +* prevent in place reverse in setDatastore ([473ab71](https://github.com/nrkno/tv-automation-state-timeline-resolver/commit/473ab713785325c2062db983c8ece80ea5dede4d)) +* track ccg state internally ([fd5596f](https://github.com/nrkno/tv-automation-state-timeline-resolver/commit/fd5596fcf975a7a122c6fb21946f13c2e97a4233)) + + + + + # [7.5.0-release47.3](https://github.com/nrkno/tv-automation-state-timeline-resolver/compare/7.5.0-release47.2...7.5.0-release47.3) (2022-11-07) **Note:** Version bump only for package timeline-state-resolver diff --git a/packages/timeline-state-resolver/package.json b/packages/timeline-state-resolver/package.json index 1a62000115..6348f5d1f4 100644 --- a/packages/timeline-state-resolver/package.json +++ b/packages/timeline-state-resolver/package.json @@ -1,6 +1,6 @@ { "name": "timeline-state-resolver", - "version": "7.5.0-release47.3", + "version": "7.5.0-release47.4", "description": "Have timeline, control stuff", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -106,7 +106,7 @@ "sprintf-js": "^1.1.2", "superfly-timeline": "^8.3.1", "threadedclass": "^1.1.1", - "timeline-state-resolver-types": "7.5.0-release47.3", + "timeline-state-resolver-types": "7.5.0-release47.4", "tslib": "^2.3.1", "tv-automation-quantel-gateway-client": "^2.0.5", "underscore": "^1.13.4", From 20a54f45d6eeb8e22a15fc445c5ff2c6ea7b9e4b Mon Sep 17 00:00:00 2001 From: Balte de Wit Date: Mon, 16 Jan 2023 14:26:55 +0100 Subject: [PATCH 02/11] chore: update casparcg-state --- packages/timeline-state-resolver/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/timeline-state-resolver/package.json b/packages/timeline-state-resolver/package.json index 6348f5d1f4..8d49be47b5 100644 --- a/packages/timeline-state-resolver/package.json +++ b/packages/timeline-state-resolver/package.json @@ -90,7 +90,7 @@ "atem-connection": "2.4.0", "atem-state": "^0.12.2", "casparcg-connection": "^5.1.0", - "casparcg-state": "2.1.2", + "casparcg-state": "2.1.3", "debug": "^4.3.1", "deepmerge": "^4.2.2", "emberplus-connection": "^0.1.2", diff --git a/yarn.lock b/yarn.lock index 057ef40e9e..48c8d7f880 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2693,10 +2693,10 @@ casparcg-connection@^5.1.0: xml2js "^0.4.19" xmlbuilder "^9.0.7" -casparcg-state@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/casparcg-state/-/casparcg-state-2.1.2.tgz#bfb31e7d5f320818a5653b5172c35874f933b327" - integrity sha512-JGKfRZ1gwqxTNxd6/g66jldc5N7lpQPG54mX6X85/H/KIGVE1M3rixSdlf7Ib/8s0Hh+X4qj7i7ouDetM+ldMQ== +casparcg-state@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/casparcg-state/-/casparcg-state-2.1.3.tgz#a21d84da19815b9ca31fe4cc31c11907712f22be" + integrity sha512-bhoUwsqMOBQ+zfyrw6TNjp6uTE25N9cU8DqBkEZp7WsixwXlHzxyFWO3i+u1itBKfhNsIw/vM9Y09mmXGY5oZw== dependencies: casparcg-connection "^5.1.0" fast-clone "^1.5.13" From 4481625387af26a4d26415ee1011ec3c736e8702 Mon Sep 17 00:00:00 2001 From: Balte de Wit Date: Mon, 16 Jan 2023 14:36:06 +0100 Subject: [PATCH 03/11] 7.5.0-release47.5 --- CHANGELOG.md | 8 ++++++++ lerna.json | 2 +- packages/timeline-state-resolver-types/CHANGELOG.md | 4 ++++ packages/timeline-state-resolver-types/package.json | 2 +- packages/timeline-state-resolver/CHANGELOG.md | 8 ++++++++ packages/timeline-state-resolver/package.json | 4 ++-- 6 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c86281f154..fe1a0ded78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [7.5.0-release47.5](https://github.com/nrkno/sofie-timeline-state-resolver/compare/7.5.0-release47.4...7.5.0-release47.5) (2023-01-16) + +**Note:** Version bump only for package timeline-state-resolver-packages + + + + + # [7.5.0-release47.4](https://github.com/nrkno/sofie-timeline-state-resolver/compare/7.5.0-release47.3...7.5.0-release47.4) (2023-01-13) diff --git a/lerna.json b/lerna.json index 9d3e37b7f0..33f207ff71 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "7.5.0-release47.4", + "version": "7.5.0-release47.5", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/timeline-state-resolver-types/CHANGELOG.md b/packages/timeline-state-resolver-types/CHANGELOG.md index 2f638e62a1..323f146a61 100644 --- a/packages/timeline-state-resolver-types/CHANGELOG.md +++ b/packages/timeline-state-resolver-types/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [7.5.0-release47.5](https://github.com/nrkno/sofie-timeline-state-resolver/compare/7.5.0-release47.4...7.5.0-release47.5) (2023-01-16) + +**Note:** Version bump only for package timeline-state-resolver-types + # [7.5.0-release47.4](https://github.com/nrkno/sofie-timeline-state-resolver/compare/7.5.0-release47.3...7.5.0-release47.4) (2023-01-13) ### Bug Fixes diff --git a/packages/timeline-state-resolver-types/package.json b/packages/timeline-state-resolver-types/package.json index b45d3d8ceb..29b5655640 100644 --- a/packages/timeline-state-resolver-types/package.json +++ b/packages/timeline-state-resolver-types/package.json @@ -1,6 +1,6 @@ { "name": "timeline-state-resolver-types", - "version": "7.5.0-release47.4", + "version": "7.5.0-release47.5", "description": "Have timeline, control stuff", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/timeline-state-resolver/CHANGELOG.md b/packages/timeline-state-resolver/CHANGELOG.md index 1f2072abd4..1be96233b3 100644 --- a/packages/timeline-state-resolver/CHANGELOG.md +++ b/packages/timeline-state-resolver/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [7.5.0-release47.5](https://github.com/nrkno/tv-automation-state-timeline-resolver/compare/7.5.0-release47.4...7.5.0-release47.5) (2023-01-16) + +**Note:** Version bump only for package timeline-state-resolver + + + + + # [7.5.0-release47.4](https://github.com/nrkno/tv-automation-state-timeline-resolver/compare/7.5.0-release47.3...7.5.0-release47.4) (2023-01-13) diff --git a/packages/timeline-state-resolver/package.json b/packages/timeline-state-resolver/package.json index 8d49be47b5..4b8286c1f6 100644 --- a/packages/timeline-state-resolver/package.json +++ b/packages/timeline-state-resolver/package.json @@ -1,6 +1,6 @@ { "name": "timeline-state-resolver", - "version": "7.5.0-release47.4", + "version": "7.5.0-release47.5", "description": "Have timeline, control stuff", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -106,7 +106,7 @@ "sprintf-js": "^1.1.2", "superfly-timeline": "^8.3.1", "threadedclass": "^1.1.1", - "timeline-state-resolver-types": "7.5.0-release47.4", + "timeline-state-resolver-types": "7.5.0-release47.5", "tslib": "^2.3.1", "tv-automation-quantel-gateway-client": "^2.0.5", "underscore": "^1.13.4", From 5dee681990973495afa2db15cfd1b3e6100bfa3a Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 7 Feb 2023 10:53:28 +0100 Subject: [PATCH 04/11] feat: clone HTTPSend device, add Nora-specific settings and content (WIP) --- .../src/device.ts | 6 + .../src/index.ts | 4 + .../src/noraNRK.ts | 47 +++ .../timeline-state-resolver/src/conductor.ts | 11 + .../noraNRK/__tests__/nora.spec.ts | 213 ++++++++++++ .../src/integrations/noraNRK/index.ts | 306 ++++++++++++++++++ 6 files changed, 587 insertions(+) create mode 100644 packages/timeline-state-resolver-types/src/noraNRK.ts create mode 100644 packages/timeline-state-resolver/src/integrations/noraNRK/__tests__/nora.spec.ts create mode 100644 packages/timeline-state-resolver/src/integrations/noraNRK/index.ts diff --git a/packages/timeline-state-resolver-types/src/device.ts b/packages/timeline-state-resolver-types/src/device.ts index 343ab5843d..8328ff430e 100644 --- a/packages/timeline-state-resolver-types/src/device.ts +++ b/packages/timeline-state-resolver-types/src/device.ts @@ -18,6 +18,7 @@ import { HTTPWatcherOptions, VizMSEOptions, VMixOptions, + NoraNRKOptions, } from '.' import { ShotokuOptions } from './shotoku' import { TelemetricsOptions } from './telemetrics' @@ -74,6 +75,7 @@ export type DeviceOptionsAny = | DeviceOptionsVizMSE | DeviceOptionsShotoku | DeviceOptionsTelemetrics + | DeviceOptionsNoraNRK export interface DeviceOptionsAbstract extends DeviceOptionsBase { type: DeviceType.ABSTRACT @@ -137,3 +139,7 @@ export interface DeviceOptionsVMix extends DeviceOptionsBase { export interface DeviceOptionsTelemetrics extends DeviceOptionsBase { type: DeviceType.TELEMETRICS } + +export interface DeviceOptionsNoraNRK extends DeviceOptionsBase { + type: DeviceType.NORA_NRK +} diff --git a/packages/timeline-state-resolver-types/src/index.ts b/packages/timeline-state-resolver-types/src/index.ts index 997a39e30e..29ed663936 100644 --- a/packages/timeline-state-resolver-types/src/index.ts +++ b/packages/timeline-state-resolver-types/src/index.ts @@ -19,6 +19,7 @@ import { TimelineObjVIZMSEAny } from './vizMSE' import { TimelineObjSingularLiveAny } from './singularLive' import { TimelineObjVMixAny } from './vmix' import { TimelineObjOBSAny } from './obs' +import { TimelineObjNoraNRKAny } from './noraNRK' export * from './abstract' export * from './atem' @@ -40,6 +41,7 @@ export * from './singularLive' export * from './vmix' export * from './obs' export * from './telemetrics' +export * from './noraNRK' export * from './device' export * from './mapping' @@ -72,6 +74,7 @@ export enum DeviceType { OBS = 21, SOFIE_CHEF = 22, TELEMETRICS = 23, + NORA_NRK = 24, } export interface TSRTimelineKeyframe extends Timeline.TimelineKeyframe { @@ -144,6 +147,7 @@ export type TSRTimelineObj = | TimelineObjVMixAny | TimelineObjVIZMSEAny | TimelineObjTelemetricsAny + | TimelineObjNoraNRKAny export type TSRTimeline = Array diff --git a/packages/timeline-state-resolver-types/src/noraNRK.ts b/packages/timeline-state-resolver-types/src/noraNRK.ts new file mode 100644 index 0000000000..fd8e435c47 --- /dev/null +++ b/packages/timeline-state-resolver-types/src/noraNRK.ts @@ -0,0 +1,47 @@ +import { Mapping } from './mapping' +import { TSRTimelineObjBase, DeviceType, TimelineDatastoreReferencesContent } from '.' + +export interface MappingNoraNRK extends Mapping { + device: DeviceType.NORA_NRK +} + +export interface NoraNRKCommandContent { + group: string + groupSuffix?: string + channel: string + + payload: Record + + /** How the params are sent. Ignored for GET since params are sent in querystring. Default is JSON. */ + temporalPriority?: number // default: 0 + /** Commands in the same queue will be sent in order (will wait for the previous to finish before sending next */ + queueId?: string +} +export interface NoraNRKOptions { + // Base URL for relative urls + coreUrl?: string + + // API Key to be added as a query argument ?apiKey= + apiKey?: string + + makeReadyCommands?: NoraNRKCommandContent[] + /** Whether a makeReady should be treated as a reset of the device. It should be assumed clean, with the queue discarded, and state reapplied from empty */ + makeReadyDoesReset?: boolean + + /** Minimum time in ms before a command is resent, set to <= 0 or undefined to disable */ + resendTime?: number +} + +export type TimelineObjNoraNRKAny = TimelineObjNoraNRKRequest +export interface TimelineObjNoraNRKBase extends TSRTimelineObjBase { + content: { + deviceType: DeviceType.NORA_NRK + // type: TimelineContentTypeCasparCg + } +} +export interface TimelineObjNoraNRKRequest extends TimelineObjNoraNRKBase { + content: { + deviceType: DeviceType.NORA_NRK + } & NoraNRKCommandContent & + TimelineDatastoreReferencesContent +} diff --git a/packages/timeline-state-resolver/src/conductor.ts b/packages/timeline-state-resolver/src/conductor.ts index 515396ef88..11f0a4bd23 100644 --- a/packages/timeline-state-resolver/src/conductor.ts +++ b/packages/timeline-state-resolver/src/conductor.ts @@ -51,6 +51,7 @@ import { VizMSEDevice, DeviceOptionsVizMSEInternal } from './integrations/vizMSE import { ShotokuDevice, DeviceOptionsShotokuInternal } from './integrations/shotoku' import { DeviceOptionsSofieChefInternal, SofieChefDevice } from './integrations/sofieChef' import { TelemetricsDevice } from './integrations/telemetrics' +import { NoraNRKDevice, DeviceOptionsNoraNRKInternal } from './integrations/noraNRK' export { DeviceContainer } export { CommandWithContext } @@ -611,6 +612,15 @@ export class Conductor extends EventEmitter { getCurrentTime, threadedClassOptions ) + } else if (deviceOptions.type === DeviceType.NORA_NRK) { + newDevice = await DeviceContainer.create( + '../../dist/devices/nora.js', + 'NoraNRKDevice', + deviceId, + deviceOptions, + getCurrentTime, + threadedClassOptions + ) } else { // @ts-ignore deviceOptions.type is of type "never" const type: any = deviceOptions.type @@ -1493,6 +1503,7 @@ export type DeviceOptionsAnyInternal = | DeviceOptionsShotokuInternal | DeviceOptionsVizMSEInternal | DeviceOptionsTelemetrics + | DeviceOptionsNoraNRKInternal function removeParentFromState(o: TimelineState): TimelineState { for (const key in o) { diff --git a/packages/timeline-state-resolver/src/integrations/noraNRK/__tests__/nora.spec.ts b/packages/timeline-state-resolver/src/integrations/noraNRK/__tests__/nora.spec.ts new file mode 100644 index 0000000000..c0aec29e6b --- /dev/null +++ b/packages/timeline-state-resolver/src/integrations/noraNRK/__tests__/nora.spec.ts @@ -0,0 +1,213 @@ +import { Conductor } from '../../../conductor' +import { NoraNRKDevice } from '..' +import { + Mappings, + DeviceType, + TimelineObjHTTPRequest, + TimelineContentTypeHTTP, + MappingNoraNRK, +} from 'timeline-state-resolver-types' +import { MockTime } from '../../../__tests__/mockTime' +import { ThreadedClass } from 'threadedclass' +import { getMockCall } from '../../../__tests__/lib' + +// let nowActual = Date.now() +describe('NORA Core (NRK)', () => { + const mockTime = new MockTime() + beforeAll(() => { + mockTime.mockDateNow() + }) + beforeEach(() => { + mockTime.init() + }) + test('POST command', async () => { + const commandReceiver0: any = jest.fn(async () => { + return Promise.resolve() + }) + const myLayerMapping0: MappingNoraNRK = { + device: DeviceType.NORA_NRK, + deviceId: 'myNORA', + } + const myLayerMapping: Mappings = { + myLayer0: myLayerMapping0, + } + + const myConductor = new Conductor({ + multiThreadedResolver: false, + getCurrentTime: mockTime.getCurrentTime, + }) + await myConductor.init() + await myConductor.addDevice('myNORA', { + type: DeviceType.NORA_NRK, + options: {}, + commandReceiver: commandReceiver0, + }) + myConductor.setTimelineAndMappings([], myLayerMapping) + await mockTime.advanceTimeToTicks(10100) + + const deviceContainer = myConductor.getDevice('myNORA') + const device = deviceContainer!.device as ThreadedClass + + // Check that no commands has been scheduled: + expect(await device.queue).toHaveLength(0) + + myConductor.setTimelineAndMappings([ + { + id: 'obj0', + enable: { + start: mockTime.now + 1000, // in 1 second + duration: 2000, + }, + layer: 'myLayer0', + content: { + deviceType: DeviceType.HTTPSEND, + type: TimelineContentTypeHTTP.POST, + + url: 'http://superfly.tv', + params: { + a: 1, + b: 2, + }, + }, + }, + ]) + await mockTime.advanceTimeToTicks(10990) + expect(commandReceiver0).toHaveBeenCalledTimes(0) + await mockTime.advanceTimeToTicks(11100) + + expect(commandReceiver0).toHaveBeenCalledTimes(1) + expect(commandReceiver0).toBeCalledWith( + expect.anything(), + expect.objectContaining({ + type: 'post', + url: 'http://superfly.tv', + params: { + a: 1, + b: 2, + }, + }), + expect.anything(), + expect.stringContaining('obj0'), + expect.anything() + ) + expect(getMockCall(commandReceiver0, 0, 2)).toMatch(/added/) // context + await mockTime.advanceTimeToTicks(16000) + expect(commandReceiver0).toHaveBeenCalledTimes(1) + }) + test('POST message, ordering of commands', async () => { + const commandReceiver0: any = jest.fn(async () => { + return Promise.resolve() + }) + const myLayerMapping0: MappingNoraNRK = { + device: DeviceType.NORA_NRK, + deviceId: 'myNORA', + } + const myLayerMapping: Mappings = { + myLayer0: myLayerMapping0, + myLayer1: myLayerMapping0, + myLayer2: myLayerMapping0, + } + + const myConductor = new Conductor({ + multiThreadedResolver: false, + getCurrentTime: mockTime.getCurrentTime, + }) + await myConductor.init() + await myConductor.addDevice('myNORA', { + type: DeviceType.NORA_NRK, + options: {}, + commandReceiver: commandReceiver0, + }) + myConductor.setTimelineAndMappings([], myLayerMapping) + await mockTime.advanceTimeToTicks(10100) + + const deviceContainer = myConductor.getDevice('myNORA') + const device = deviceContainer!.device as ThreadedClass + + // Check that no commands has been scheduled: + expect(await device.queue).toHaveLength(0) + + const timeline: Array = [ + { + id: 'obj0', + enable: { + start: mockTime.now + 1000, // in 1 second + duration: 2000, + }, + layer: 'myLayer0', + content: { + deviceType: DeviceType.HTTPSEND, + type: 'POST' as TimelineContentTypeHTTP.POST, + + url: 'http://superfly.tv/1', + params: {}, + temporalPriority: 1, + }, + }, + { + id: 'obj1', + enable: { + start: mockTime.now + 1000, // in 1 second + duration: 2000, + }, + layer: 'myLayer1', + content: { + deviceType: DeviceType.HTTPSEND, + type: 'POST' as TimelineContentTypeHTTP.POST, + + url: 'http://superfly.tv/2', + params: {}, + temporalPriority: 3, + }, + }, + { + id: 'obj2', + enable: { + start: mockTime.now + 1000, // in 1 second + duration: 2000, + }, + layer: 'myLayer2', + content: { + deviceType: DeviceType.HTTPSEND, + type: 'POST' as TimelineContentTypeHTTP.POST, + + url: 'http://superfly.tv/3', + params: {}, + temporalPriority: 2, + }, + }, + ] + myConductor.setTimelineAndMappings(timeline) + + await mockTime.advanceTimeToTicks(10990) + expect(commandReceiver0).toHaveBeenCalledTimes(0) + await mockTime.advanceTimeToTicks(11100) + + // Expecting to see the ordering below: + expect(commandReceiver0).toHaveBeenCalledTimes(3) + expect(commandReceiver0).toHaveBeenNthCalledWith( + 1, + expect.anything(), + expect.objectContaining({ url: 'http://superfly.tv/1' }), + expect.anything(), + expect.stringContaining('obj0'), + expect.anything() + ) + expect(commandReceiver0).toHaveBeenNthCalledWith( + 2, + expect.anything(), + expect.objectContaining({ url: 'http://superfly.tv/3' }), + expect.anything(), + expect.stringContaining('obj2'), + expect.anything() + ) + expect(commandReceiver0).toHaveBeenNthCalledWith( + 3, + expect.anything(), + expect.objectContaining({ url: 'http://superfly.tv/2' }), + expect.anything(), + expect.stringContaining('obj1'), + expect.anything() + ) + }) +}) diff --git a/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts b/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts new file mode 100644 index 0000000000..0efc36ebab --- /dev/null +++ b/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts @@ -0,0 +1,306 @@ +import * as _ from 'underscore' +import { DeviceWithState, CommandWithContext, DeviceStatus, StatusCode } from './../../devices/device' +import { + DeviceType, + NoraNRKOptions, + NoraNRKCommandContent, + DeviceOptionsNoraNRK, + Mappings, +} from 'timeline-state-resolver-types' +import { DoOnTime, SendMode } from '../../devices/doOnTime' +import got, { OptionsOfTextResponseBody, RequestError } from 'got' + +import { TimelineState, ResolvedTimelineObjectInstance } from 'superfly-timeline' + +import Debug from 'debug' +import { endTrace, startTrace } from '../../lib' +const debug = Debug('timeline-state-resolver:httpsend') + +export interface DeviceOptionsNoraNRKInternal extends DeviceOptionsNoraNRK { + commandReceiver?: CommandReceiver +} +export type CommandReceiver = ( + time: number, + cmd: NoraNRKCommandContent, + context: CommandContext, + timelineObjId: string, + layer?: string +) => Promise +interface Command { + commandName: 'added' | 'changed' | 'removed' + content: NoraNRKCommandContent + context: CommandContext + timelineObjId: string + layer: string +} +type CommandContext = string + +type NoraNRKState = TimelineState + +/** + * This is a Nora (NRK) device, it uses http to control a Nora channel + */ +export class NoraNRKDevice extends DeviceWithState { + private _makeReadyCommands: NoraNRKCommandContent[] + private _resendTime?: number + private _doOnTime: DoOnTime + + private _commandReceiver: CommandReceiver + private activeLayers = new Map() + + constructor(deviceId: string, deviceOptions: DeviceOptionsNoraNRKInternal, getCurrentTime: () => Promise) { + super(deviceId, deviceOptions, getCurrentTime) + if (deviceOptions.options) { + if (deviceOptions.commandReceiver) this._commandReceiver = deviceOptions.commandReceiver + else this._commandReceiver = this._defaultCommandReceiver.bind(this) + } + this._doOnTime = new DoOnTime( + () => { + return this.getCurrentTime() + }, + SendMode.IN_ORDER, + this._deviceOptions + ) + this.handleDoOnTime(this._doOnTime, 'NoraNRK') + } + async init(initOptions: NoraNRKOptions): Promise { + this._makeReadyCommands = initOptions.makeReadyCommands || [] + this._resendTime = initOptions.resendTime && initOptions.resendTime > 1 ? initOptions.resendTime : undefined + + return Promise.resolve(true) // This device doesn't have any initialization procedure + } + /** Called by the Conductor a bit before a .handleState is called */ + prepareForHandleState(newStateTime: number) { + // clear any queued commands later than this time: + this._doOnTime.clearQueueNowAndAfter(newStateTime) + this.cleanUpStates(0, newStateTime) + } + handleState(newState: TimelineState, newMappings: Mappings) { + super.onHandleState(newState, newMappings) + // Handle this new state, at the point in time specified + + const previousStateTime = Math.max(this.getCurrentTime(), newState.time) + const oldState: TimelineState = ( + this.getStateBefore(previousStateTime) || { state: { time: 0, layers: {}, nextEvents: [] } } + ).state + + const convertTrace = startTrace(`device:convertState`, { deviceId: this.deviceId }) + const oldHttpSendState = oldState + const newHttpSendState = this.convertStateToHttpSend(newState) + this.emit('timeTrace', endTrace(convertTrace)) + + const diffTrace = startTrace(`device:diffState`, { deviceId: this.deviceId }) + const commandsToAchieveState: Array = this._diffStates(oldHttpSendState, newHttpSendState) + this.emit('timeTrace', endTrace(diffTrace)) + + // clear any queued commands later than this time: + this._doOnTime.clearQueueNowAndAfter(previousStateTime) + // add the new commands to the queue: + this._addToQueue(commandsToAchieveState, newState.time) + + // store the new state, for later use: + this.setState(newState, newState.time) + } + clearFuture(clearAfterTime: number) { + // Clear any scheduled commands after this time + this._doOnTime.clearQueueAfter(clearAfterTime) + } + async terminate() { + this._doOnTime.dispose() + return Promise.resolve(true) + } + getStatus(): DeviceStatus { + // Good, since this device has no status, really + return { + statusCode: StatusCode.GOOD, + messages: [], + active: this.isActive, + } + } + async makeReady(okToDestroyStuff?: boolean): Promise { + if (okToDestroyStuff) { + const time = this.getCurrentTime() + + this.clearStates() + this._doOnTime.clearQueueAfter(0) + + for (const cmd of this._makeReadyCommands || []) { + await this._commandReceiver(time, cmd, 'makeReady', '') + } + } + } + + get canConnect(): boolean { + return false + } + get connected(): boolean { + return false + } + convertStateToHttpSend(state: TimelineState) { + // convert the timeline state into something we can use + // (won't even use this.mapping) + return state + } + get deviceType() { + return DeviceType.NORA_NRK + } + get deviceName(): string { + return 'NORA Core (NRK) ' + this.deviceId + } + get queue() { + return this._doOnTime.getQueue() + } + /** + * Add commands to queue, to be executed at the right time + */ + private _addToQueue(commandsToAchieveState: Array, time: number) { + _.each(commandsToAchieveState, (cmd: Command) => { + // add the new commands to the queue: + this._doOnTime.queue( + time, + cmd.content.queueId, + (cmd: Command) => { + if (cmd.commandName === 'added' || cmd.commandName === 'changed') { + this.activeLayers.set(cmd.layer, JSON.stringify(cmd.content)) + return this._commandReceiver(time, cmd.content, cmd.context, cmd.timelineObjId, cmd.layer) + } else { + this.activeLayers.delete(cmd.layer) + return null + } + }, + cmd + ) + }) + } + /** + * Compares the new timeline-state with the old one, and generates commands to account for the difference + */ + private _diffStates(oldhttpSendState: TimelineState, newhttpSendState: TimelineState): Array { + // in this httpSend class, let's just cheat: + + const commands: Array = [] + + _.each(newhttpSendState.layers, (newLayer: ResolvedTimelineObjectInstance, layerKey: string) => { + const oldLayer = oldhttpSendState.layers[layerKey] + if (!oldLayer) { + // added! + commands.push({ + timelineObjId: newLayer.id, + commandName: 'added', + content: newLayer.content as NoraNRKCommandContent, + context: `added: ${newLayer.id}`, + layer: layerKey, + }) + } else { + // changed? + if (!_.isEqual(oldLayer.content, newLayer.content)) { + // changed! + commands.push({ + timelineObjId: newLayer.id, + commandName: 'changed', + content: newLayer.content as NoraNRKCommandContent, + context: `changed: ${newLayer.id} (previously: ${oldLayer.id})`, + layer: layerKey, + }) + } + } + }) + // removed + _.each(oldhttpSendState.layers, (oldLayer: ResolvedTimelineObjectInstance, layerKey) => { + const newLayer = newhttpSendState.layers[layerKey] + if (!newLayer) { + // removed! + commands.push({ + timelineObjId: oldLayer.id, + commandName: 'removed', + content: oldLayer.content as NoraNRKCommandContent, + context: `removed: ${oldLayer.id}`, + layer: layerKey, + }) + } + }) + + commands.sort((a, b) => a.layer.localeCompare(b.layer)) + commands.sort((a, b) => { + return (a.content.temporalPriority || 0) - (b.content.temporalPriority || 0) + }) + + return commands + } + + private async _defaultCommandReceiver( + _time: number, + cmd: NoraNRKCommandContent, + context: CommandContext, + timelineObjId: string, + layer?: string + ): Promise { + if (layer) { + const hash = this.activeLayers.get(layer) + if (JSON.stringify(cmd) !== hash) return Promise.resolve() // command is no longer relevant to state + } + const cwc: CommandWithContext = { + context: context, + command: cmd, + timelineObjId: timelineObjId, + } + this.emitDebug(cwc) + + const rebasedUrl = new URL( + `/renders/${cmd.group}${cmd.groupSuffix ?? ''}/${cmd.channel}`, + this._deviceOptions.options?.coreUrl + ) + if (this._deviceOptions.options?.apiKey) { + rebasedUrl.searchParams.append('apiKey', this._deviceOptions.options?.apiKey) + } + + const t = Date.now() + debug(`POST: ${rebasedUrl} ${JSON.stringify(cmd.payload)} (${timelineObjId})`) + + try { + const options: OptionsOfTextResponseBody = {} + + options.json = cmd.payload ?? undefined + + const response = await got.post(rebasedUrl, options) + + if (response.statusCode === 200) { + this.emitDebug( + `NoraNRK: POST: Good statuscode response on url "${rebasedUrl}": ${response.statusCode} (${context})` + ) + } else { + debug(`Bad response for ${rebasedUrl}: ${response.statusCode}`) + this.emit( + 'warning', + `NoraNRK: POST: Bad statuscode response on url "${rebasedUrl}": ${response.statusCode} (${context})` + ) + } + } catch (error) { + const err = error as RequestError // make typescript happy + + this.emit('error', `NoraNRK.response error on POST "${rebasedUrl}" (${context})`, err) + this.emit('commandError', err, cwc) + debug(`Failed ${rebasedUrl}: ${error} (${timelineObjId})`) + + if ('code' in err) { + const retryCodes = [ + 'ETIMEDOUT', + 'ECONNRESET', + 'EADDRINUSE', + 'ECONNREFUSED', + 'EPIPE', + 'ENOTFOUND', + 'ENETUNREACH', + 'EHOSTUNREACH', + 'EAI_AGAIN', + ] + + if (retryCodes.includes(err.code) && this._resendTime) { + const timeLeft = Math.max(this._resendTime - (Date.now() - t), 0) + await new Promise((resolve) => setTimeout(() => resolve(), timeLeft)) + this._defaultCommandReceiver(_time, cmd, context, timelineObjId, layer).catch(() => null) // errors will be emitted + } + } + } + } +} From 3e2c9723b962bd1f4f30b9fff5bf8cf3af5f73e2 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 7 Feb 2023 11:38:10 +0100 Subject: [PATCH 05/11] fix: tests --- .../timeline-state-resolver/src/conductor.ts | 2 +- .../noraNRK/__tests__/nora.spec.ts | 112 ++++++++++++------ 2 files changed, 78 insertions(+), 36 deletions(-) diff --git a/packages/timeline-state-resolver/src/conductor.ts b/packages/timeline-state-resolver/src/conductor.ts index 11f0a4bd23..5527d758a7 100644 --- a/packages/timeline-state-resolver/src/conductor.ts +++ b/packages/timeline-state-resolver/src/conductor.ts @@ -614,7 +614,7 @@ export class Conductor extends EventEmitter { ) } else if (deviceOptions.type === DeviceType.NORA_NRK) { newDevice = await DeviceContainer.create( - '../../dist/devices/nora.js', + '../../dist/integrations/noraNRK/index.js', 'NoraNRKDevice', deviceId, deviceOptions, diff --git a/packages/timeline-state-resolver/src/integrations/noraNRK/__tests__/nora.spec.ts b/packages/timeline-state-resolver/src/integrations/noraNRK/__tests__/nora.spec.ts index c0aec29e6b..9033f71e13 100644 --- a/packages/timeline-state-resolver/src/integrations/noraNRK/__tests__/nora.spec.ts +++ b/packages/timeline-state-resolver/src/integrations/noraNRK/__tests__/nora.spec.ts @@ -1,12 +1,6 @@ import { Conductor } from '../../../conductor' import { NoraNRKDevice } from '..' -import { - Mappings, - DeviceType, - TimelineObjHTTPRequest, - TimelineContentTypeHTTP, - MappingNoraNRK, -} from 'timeline-state-resolver-types' +import { Mappings, DeviceType, MappingNoraNRK, TimelineObjNoraNRKAny } from 'timeline-state-resolver-types' import { MockTime } from '../../../__tests__/mockTime' import { ThreadedClass } from 'threadedclass' import { getMockCall } from '../../../__tests__/lib' @@ -60,13 +54,19 @@ describe('NORA Core (NRK)', () => { }, layer: 'myLayer0', content: { - deviceType: DeviceType.HTTPSEND, - type: TimelineContentTypeHTTP.POST, - - url: 'http://superfly.tv', - params: { - a: 1, - b: 2, + deviceType: DeviceType.NORA_NRK, + + group: 'test', + channel: 'gfx1', + payload: { + manifest: 'nyheter', + template: { + name: '01_navn', + event: 'take', + }, + content: { + navn: 'Firstname Lastname', + }, }, }, }, @@ -79,11 +79,17 @@ describe('NORA Core (NRK)', () => { expect(commandReceiver0).toBeCalledWith( expect.anything(), expect.objectContaining({ - type: 'post', - url: 'http://superfly.tv', - params: { - a: 1, - b: 2, + group: 'test', + channel: 'gfx1', + payload: { + manifest: 'nyheter', + template: { + name: '01_navn', + event: 'take', + }, + content: { + navn: 'Firstname Lastname', + }, }, }), expect.anything(), @@ -127,7 +133,7 @@ describe('NORA Core (NRK)', () => { // Check that no commands has been scheduled: expect(await device.queue).toHaveLength(0) - const timeline: Array = [ + const timeline: Array = [ { id: 'obj0', enable: { @@ -136,11 +142,21 @@ describe('NORA Core (NRK)', () => { }, layer: 'myLayer0', content: { - deviceType: DeviceType.HTTPSEND, - type: 'POST' as TimelineContentTypeHTTP.POST, + deviceType: DeviceType.NORA_NRK, + + group: 'test', + channel: 'gfx1', + payload: { + manifest: 'nyheter', + template: { + name: '01_navn', + event: 'take', + }, + content: { + navn: 'Firstname Lastname', + }, + }, - url: 'http://superfly.tv/1', - params: {}, temporalPriority: 1, }, }, @@ -152,11 +168,21 @@ describe('NORA Core (NRK)', () => { }, layer: 'myLayer1', content: { - deviceType: DeviceType.HTTPSEND, - type: 'POST' as TimelineContentTypeHTTP.POST, + deviceType: DeviceType.NORA_NRK, + + group: 'test', + channel: 'gfx1', + payload: { + manifest: 'nyheter', + template: { + name: '02_navn_alt', + event: 'take', + }, + content: { + navn: 'Firstname Lastname', + }, + }, - url: 'http://superfly.tv/2', - params: {}, temporalPriority: 3, }, }, @@ -168,11 +194,21 @@ describe('NORA Core (NRK)', () => { }, layer: 'myLayer2', content: { - deviceType: DeviceType.HTTPSEND, - type: 'POST' as TimelineContentTypeHTTP.POST, + deviceType: DeviceType.NORA_NRK, + + group: 'test', + channel: 'gfx1', + payload: { + manifest: 'nyheter', + template: { + name: '03_navn_alt_2', + event: 'take', + }, + content: { + navn: 'Firstname Lastname', + }, + }, - url: 'http://superfly.tv/3', - params: {}, temporalPriority: 2, }, }, @@ -188,7 +224,9 @@ describe('NORA Core (NRK)', () => { expect(commandReceiver0).toHaveBeenNthCalledWith( 1, expect.anything(), - expect.objectContaining({ url: 'http://superfly.tv/1' }), + expect.objectContaining({ + payload: expect.objectContaining({ template: expect.objectContaining({ name: '01_navn' }) }), + }), expect.anything(), expect.stringContaining('obj0'), expect.anything() @@ -196,7 +234,9 @@ describe('NORA Core (NRK)', () => { expect(commandReceiver0).toHaveBeenNthCalledWith( 2, expect.anything(), - expect.objectContaining({ url: 'http://superfly.tv/3' }), + expect.objectContaining({ + payload: expect.objectContaining({ template: expect.objectContaining({ name: '03_navn_alt_2' }) }), + }), expect.anything(), expect.stringContaining('obj2'), expect.anything() @@ -204,7 +244,9 @@ describe('NORA Core (NRK)', () => { expect(commandReceiver0).toHaveBeenNthCalledWith( 3, expect.anything(), - expect.objectContaining({ url: 'http://superfly.tv/2' }), + expect.objectContaining({ + payload: expect.objectContaining({ template: expect.objectContaining({ name: '02_navn_alt' }) }), + }), expect.anything(), expect.stringContaining('obj1'), expect.anything() From b242e8d16dbbf256d21b9ed1a3c73dee69eca94c Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 7 Feb 2023 12:48:13 +0100 Subject: [PATCH 06/11] feat: codify the NORA Core payload a bit more --- .../src/noraNRK.ts | 18 +++++++- .../src/integrations/noraNRK/index.ts | 42 ++++++++++++++----- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/packages/timeline-state-resolver-types/src/noraNRK.ts b/packages/timeline-state-resolver-types/src/noraNRK.ts index fd8e435c47..104b62ed49 100644 --- a/packages/timeline-state-resolver-types/src/noraNRK.ts +++ b/packages/timeline-state-resolver-types/src/noraNRK.ts @@ -10,7 +10,23 @@ export interface NoraNRKCommandContent { groupSuffix?: string channel: string - payload: Record + payload: + | { + manifest?: string + template: { + name: string + event: 'take' | 'takeout' | 'preview' + layer: string + [key: string]: string | number | any + } + [key: string]: string | number | any + } + | { + template: { + event: 'takeout' + layer: string + } + } /** How the params are sent. Ignored for GET since params are sent in querystring. Default is JSON. */ temporalPriority?: number // default: 0 diff --git a/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts b/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts index 0efc36ebab..d042c22f75 100644 --- a/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts +++ b/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts @@ -46,7 +46,7 @@ export class NoraNRKDevice extends DeviceWithState() + private targetState = new Map() constructor(deviceId: string, deviceOptions: DeviceOptionsNoraNRKInternal, getCurrentTime: () => Promise) { super(deviceId, deviceOptions, getCurrentTime) @@ -161,10 +161,10 @@ export class NoraNRKDevice extends DeviceWithState { if (cmd.commandName === 'added' || cmd.commandName === 'changed') { - this.activeLayers.set(cmd.layer, JSON.stringify(cmd.content)) + this.targetState.set(cmd.layer, JSON.stringify(cmd.content)) return this._commandReceiver(time, cmd.content, cmd.context, cmd.timelineObjId, cmd.layer) } else { - this.activeLayers.delete(cmd.layer) + this.targetState.delete(cmd.layer) return null } }, @@ -236,7 +236,7 @@ export class NoraNRKDevice extends DeviceWithState { if (layer) { - const hash = this.activeLayers.get(layer) + const hash = this.targetState.get(layer) if (JSON.stringify(cmd) !== hash) return Promise.resolve() // command is no longer relevant to state } const cwc: CommandWithContext = { @@ -246,39 +246,59 @@ export class NoraNRKDevice extends DeviceWithState = cmd.payload + + // this is a "clear-layer" command + if (cmd.payload.template.event === 'takeout' && !('name' in cmd.payload.template)) { + method = 'PUT' + request = got.put + rebasedUrl = new URL( + `/renders/${cmd.group}${cmd.groupSuffix ?? ''}/${cmd.channel}/${cmd.payload.template.layer}`, + this._deviceOptions.options?.coreUrl + ) + payload = { + template: { + event: 'takeout', + }, + } + } + if (this._deviceOptions.options?.apiKey) { rebasedUrl.searchParams.append('apiKey', this._deviceOptions.options?.apiKey) } const t = Date.now() - debug(`POST: ${rebasedUrl} ${JSON.stringify(cmd.payload)} (${timelineObjId})`) + debug(`${method}: ${rebasedUrl} ${JSON.stringify(cmd.payload)} (${timelineObjId})`) try { const options: OptionsOfTextResponseBody = {} - options.json = cmd.payload ?? undefined + options.json = payload ?? undefined - const response = await got.post(rebasedUrl, options) + const response = await request(rebasedUrl, options) if (response.statusCode === 200) { this.emitDebug( - `NoraNRK: POST: Good statuscode response on url "${rebasedUrl}": ${response.statusCode} (${context})` + `NoraNRK: ${method}: Good statuscode response on url "${rebasedUrl}": ${response.statusCode} (${context})` ) } else { debug(`Bad response for ${rebasedUrl}: ${response.statusCode}`) this.emit( 'warning', - `NoraNRK: POST: Bad statuscode response on url "${rebasedUrl}": ${response.statusCode} (${context})` + `NoraNRK: ${method}: Bad statuscode response on url "${rebasedUrl}": ${response.statusCode} (${context})` ) } } catch (error) { const err = error as RequestError // make typescript happy - this.emit('error', `NoraNRK.response error on POST "${rebasedUrl}" (${context})`, err) + this.emit('error', `NoraNRK.response error on ${method} "${rebasedUrl}" (${context})`, err) this.emit('commandError', err, cwc) debug(`Failed ${rebasedUrl}: ${error} (${timelineObjId})`) From 2eed30f9adc0c8e2cea25778b5bf8415a938e788 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 7 Feb 2023 12:59:15 +0100 Subject: [PATCH 07/11] fix: use NORA layers instead of Sofie layers for tracking state --- .../integrations/noraNRK/__tests__/nora.spec.ts | 5 +++++ .../src/integrations/noraNRK/index.ts | 16 +++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/timeline-state-resolver/src/integrations/noraNRK/__tests__/nora.spec.ts b/packages/timeline-state-resolver/src/integrations/noraNRK/__tests__/nora.spec.ts index 9033f71e13..c7e93a53b4 100644 --- a/packages/timeline-state-resolver/src/integrations/noraNRK/__tests__/nora.spec.ts +++ b/packages/timeline-state-resolver/src/integrations/noraNRK/__tests__/nora.spec.ts @@ -61,6 +61,7 @@ describe('NORA Core (NRK)', () => { payload: { manifest: 'nyheter', template: { + layer: 'super', name: '01_navn', event: 'take', }, @@ -84,6 +85,7 @@ describe('NORA Core (NRK)', () => { payload: { manifest: 'nyheter', template: { + layer: 'super', name: '01_navn', event: 'take', }, @@ -149,6 +151,7 @@ describe('NORA Core (NRK)', () => { payload: { manifest: 'nyheter', template: { + layer: 'super', name: '01_navn', event: 'take', }, @@ -175,6 +178,7 @@ describe('NORA Core (NRK)', () => { payload: { manifest: 'nyheter', template: { + layer: 'super', name: '02_navn_alt', event: 'take', }, @@ -201,6 +205,7 @@ describe('NORA Core (NRK)', () => { payload: { manifest: 'nyheter', template: { + layer: 'super', name: '03_navn_alt_2', event: 'take', }, diff --git a/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts b/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts index d042c22f75..dd58f1f65c 100644 --- a/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts +++ b/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts @@ -180,8 +180,9 @@ export class NoraNRKDevice extends DeviceWithState = [] - _.each(newhttpSendState.layers, (newLayer: ResolvedTimelineObjectInstance, layerKey: string) => { - const oldLayer = oldhttpSendState.layers[layerKey] + _.each(newhttpSendState.layers, (newLayer: ResolvedTimelineObjectInstance) => { + const noraLayer = (newLayer.content as NoraNRKCommandContent).payload.template.layer + const oldLayer = oldhttpSendState.layers[noraLayer] if (!oldLayer) { // added! commands.push({ @@ -189,7 +190,7 @@ export class NoraNRKDevice extends DeviceWithState { - const newLayer = newhttpSendState.layers[layerKey] + _.each(oldhttpSendState.layers, (oldLayer: ResolvedTimelineObjectInstance) => { + const noraLayer = (oldLayer.content as NoraNRKCommandContent).payload.template.layer + const newLayer = newhttpSendState.layers[noraLayer] if (!newLayer) { // removed! commands.push({ @@ -215,7 +217,7 @@ export class NoraNRKDevice extends DeviceWithState Date: Tue, 7 Feb 2023 13:02:04 +0100 Subject: [PATCH 08/11] chore: fix log --- .../timeline-state-resolver/src/integrations/noraNRK/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts b/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts index dd58f1f65c..474a9deda5 100644 --- a/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts +++ b/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts @@ -14,7 +14,7 @@ import { TimelineState, ResolvedTimelineObjectInstance } from 'superfly-timeline import Debug from 'debug' import { endTrace, startTrace } from '../../lib' -const debug = Debug('timeline-state-resolver:httpsend') +const debug = Debug('timeline-state-resolver:noranrk') export interface DeviceOptionsNoraNRKInternal extends DeviceOptionsNoraNRK { commandReceiver?: CommandReceiver From 58b0c83dbab97de0e0f7b277d958714ba56fbbf9 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 7 Feb 2023 13:05:23 +0100 Subject: [PATCH 09/11] chore: remove comment --- packages/timeline-state-resolver-types/src/noraNRK.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/timeline-state-resolver-types/src/noraNRK.ts b/packages/timeline-state-resolver-types/src/noraNRK.ts index 104b62ed49..068e02a4a0 100644 --- a/packages/timeline-state-resolver-types/src/noraNRK.ts +++ b/packages/timeline-state-resolver-types/src/noraNRK.ts @@ -28,7 +28,6 @@ export interface NoraNRKCommandContent { } } - /** How the params are sent. Ignored for GET since params are sent in querystring. Default is JSON. */ temporalPriority?: number // default: 0 /** Commands in the same queue will be sent in order (will wait for the previous to finish before sending next */ queueId?: string From 5057fd7a0388ea6816bfaa335d0f4f87a720a8d5 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 13 Feb 2023 14:14:05 +0100 Subject: [PATCH 10/11] fix: move properties around --- .../src/noraNRK.ts | 56 ++++++++++++------- .../src/integrations/noraNRK/index.ts | 3 + 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/timeline-state-resolver-types/src/noraNRK.ts b/packages/timeline-state-resolver-types/src/noraNRK.ts index 068e02a4a0..f756fac2a6 100644 --- a/packages/timeline-state-resolver-types/src/noraNRK.ts +++ b/packages/timeline-state-resolver-types/src/noraNRK.ts @@ -3,35 +3,51 @@ import { TSRTimelineObjBase, DeviceType, TimelineDatastoreReferencesContent } fr export interface MappingNoraNRK extends Mapping { device: DeviceType.NORA_NRK -} -export interface NoraNRKCommandContent { group: string groupSuffix?: string channel: string +} - payload: - | { - manifest?: string - template: { - name: string - event: 'take' | 'takeout' | 'preview' - layer: string - [key: string]: string | number | any - } - [key: string]: string | number | any - } - | { - template: { - event: 'takeout' - layer: string - } - } +export enum TimelineContentTypeNoraNRK { + TEMPLATE = 'template', + LAYER = 'layer', +} +export interface NoraNRKCommandBase { temporalPriority?: number // default: 0 /** Commands in the same queue will be sent in order (will wait for the previous to finish before sending next */ queueId?: string } + +export interface NoraNRKTemplateCommandContent extends NoraNRKCommandBase { + type: TimelineContentTypeNoraNRK.TEMPLATE + // The payload obejct is defined from the NORA API. + payload: { + manifest?: string + template: { + name: string + event: 'take' | 'takeout' | 'preview' + layer: string + [key: string]: string | number | any + } + [key: string]: string | number | any + } +} + +export interface NoraNRKLayerCommandContent extends NoraNRKCommandBase { + type: TimelineContentTypeNoraNRK.LAYER + // The payload obejct is defined from the NORA API. + payload: { + template: { + event: 'takeout' + layer: string + } + } +} + +type NoraNRKCommandContent = NoraNRKTemplateCommandContent | NoraNRKLayerCommandContent + export interface NoraNRKOptions { // Base URL for relative urls coreUrl?: string @@ -51,7 +67,7 @@ export type TimelineObjNoraNRKAny = TimelineObjNoraNRKRequest export interface TimelineObjNoraNRKBase extends TSRTimelineObjBase { content: { deviceType: DeviceType.NORA_NRK - // type: TimelineContentTypeCasparCg + type: TimelineContentTypeNoraNRK } } export interface TimelineObjNoraNRKRequest extends TimelineObjNoraNRKBase { diff --git a/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts b/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts index 474a9deda5..4351916ca3 100644 --- a/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts +++ b/packages/timeline-state-resolver/src/integrations/noraNRK/index.ts @@ -47,6 +47,7 @@ export class NoraNRKDevice extends DeviceWithState() + // private actualState = new Map() // comes from Nora, streamed over a socket constructor(deviceId: string, deviceOptions: DeviceOptionsNoraNRKInternal, getCurrentTime: () => Promise) { super(deviceId, deviceOptions, getCurrentTime) @@ -248,6 +249,8 @@ export class NoraNRKDevice extends DeviceWithState Date: Wed, 15 Feb 2023 11:57:38 +0100 Subject: [PATCH 11/11] chore: doc --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7317dea3b1..8be12e74f0 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Using the input, it resolves the expected state, diffs the state against current * **Quantel** video server * **[vMix](https://www.vmix.com/)** software vision mixer * **VizRT MediaSequencer** graphics system - using the [v-connection](https://github.com/tv2/v-connection) library +* **NRK Nora** graphics system - an in-house graphics system developed by [NRK](https://www.nrk.no) * Arbitrary [OSC](https://en.wikipedia.org/wiki/Open_Sound_Control) compatible devices * Arbitrary HTTP (REST) compatible devices * Arbitrary TCP-socket compatible devices