From 4dd927c128aa0455255c39c0313b45b7acf3f528 Mon Sep 17 00:00:00 2001 From: ianshade Date: Wed, 27 Nov 2024 12:57:59 +0100 Subject: [PATCH] Merge remote-tracking branch 'upstream/release52' into contribute/EAV-411 --- .../src/integrations/vmix.ts | 13 +++ .../vmix/__tests__/connection.spec.ts | 29 ++++++ .../vmix/__tests__/vMixStateDiffer.spec.ts | 44 +++++++++ .../vMixTimelineStateConverter.spec.ts | 96 ++++++++++++++++++- .../src/integrations/vmix/connection.ts | 12 +++ .../src/integrations/vmix/vMixCommands.ts | 12 +++ .../src/integrations/vmix/vMixStateDiffer.ts | 24 +++++ .../vmix/vMixTimelineStateConverter.ts | 9 +- 8 files changed, 234 insertions(+), 5 deletions(-) diff --git a/packages/timeline-state-resolver-types/src/integrations/vmix.ts b/packages/timeline-state-resolver-types/src/integrations/vmix.ts index 569ff1910..f01424f2a 100644 --- a/packages/timeline-state-resolver-types/src/integrations/vmix.ts +++ b/packages/timeline-state-resolver-types/src/integrations/vmix.ts @@ -46,6 +46,8 @@ export enum VMixCommand { LIST_REMOVE_ALL = 'LIST_REMOVE_ALL', RESTART_INPUT = 'RESTART_INPUT', SET_TEXT = 'SET_TEXT', + BROWSER_NAVIGATE = 'BROWSER_NAVIGATE', + SELECT_INDEX = 'SELECT_INDEX', } export type TimelineContentVMixAny = @@ -194,6 +196,16 @@ export interface TimelineContentVMixInput extends TimelineContentVMixBase { * Titles (GT): Sets the values of text fields by name */ text?: VMixText + + /** The URL for Browser input */ + url?: string + + /** + * Photos, List: Selects item in List + * Virtual Set: Zooms to selected preset using the current speed settings + * starts from 1 + */ + index?: number } export interface TimelineContentVMixOutput extends TimelineContentVMixBase { @@ -335,4 +347,5 @@ export enum VMixInputType { Flash = 'Flash', PowerPoint = 'PowerPoint', List = 'List', + Browser = 'Browser', } diff --git a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/connection.spec.ts b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/connection.spec.ts index 52788eb4b..50a10a25e 100644 --- a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/connection.spec.ts +++ b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/connection.spec.ts @@ -109,4 +109,33 @@ describe('VMixCommandSender', () => { selectedName: 'myTitle.Text', }) }) + + it('sends url', async () => { + const { sender, mockConnection } = createTestee() + await sender.sendCommand({ + command: VMixCommand.BROWSER_NAVIGATE, + input: 5, + value: 'https://example.com', + }) + + expect(mockConnection.sendCommandFunction).toHaveBeenCalledTimes(1) + expect(mockConnection.sendCommandFunction).toHaveBeenLastCalledWith('BrowserNavigate', { + input: 5, + value: 'https%3A%2F%2Fexample.com', + }) + }) + it('selects index', async () => { + const { sender, mockConnection } = createTestee() + await sender.sendCommand({ + command: VMixCommand.SELECT_INDEX, + input: 5, + value: 3, + }) + + expect(mockConnection.sendCommandFunction).toHaveBeenCalledTimes(1) + expect(mockConnection.sendCommandFunction).toHaveBeenLastCalledWith('SelectIndex', { + input: 5, + value: 3, + }) + }) }) diff --git a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixStateDiffer.spec.ts b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixStateDiffer.spec.ts index 48baf6919..e2c37102c 100644 --- a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixStateDiffer.spec.ts +++ b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixStateDiffer.spec.ts @@ -280,4 +280,48 @@ describe('VMixStateDiffer', () => { fieldName: 'myTitle.Text', }) }) + + it('sets browser url', () => { + const differ = createTestee() + + const oldState = makeMockFullState() + const newState = makeMockFullState() + + oldState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99) + + newState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99) + const url = 'https://example.com' + newState.reportedState.existingInputs['99'].url = url + + const commands = differ.getCommandsToAchieveState(Date.now(), oldState, newState) + + expect(commands.length).toBe(1) + expect(commands[0].command).toMatchObject({ + command: VMixCommand.BROWSER_NAVIGATE, + input: '99', + value: url, + }) + }) + + it('sets index', () => { + const differ = createTestee() + + const oldState = makeMockFullState() + const newState = makeMockFullState() + + oldState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99) + + newState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99) + const index = 3 + newState.reportedState.existingInputs['99'].index = index + + const commands = differ.getCommandsToAchieveState(Date.now(), oldState, newState) + + expect(commands.length).toBe(1) + expect(commands[0].command).toMatchObject({ + command: VMixCommand.SELECT_INDEX, + input: '99', + value: index, + }) + }) }) diff --git a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixTimelineStateConverter.spec.ts b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixTimelineStateConverter.spec.ts index 4faf80885..72e59eddc 100644 --- a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixTimelineStateConverter.spec.ts +++ b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixTimelineStateConverter.spec.ts @@ -8,6 +8,7 @@ import { TimelineContentTypeVMix, TimelineContentVMixAny, VMixInputType, + VMixTransitionType, } from 'timeline-state-resolver-types' import { VMixTimelineStateConverter } from '../vMixTimelineStateConverter' import { VMixOutput, VMixStateDiffer } from '../vMixStateDiffer' @@ -136,14 +137,101 @@ describe('VMixTimelineStateConverter', () => { expect(result.reportedState.inputsAddedByUsAudio[prefixAddedInput(filePath)]).toBeUndefined() }) - it('supports text', () => { + it('allows overriding transitions in usual layer order', () => { const converter = createTestee() - const text = { 'myTitle.Text': 'SomeValue', 'myTitle.Foo': 'Bar' } + const result = converter.getVMixStateFromTimelineState( + wrapInTimelineState({ + pgm0: wrapInTimelineObject('pgm0', { + deviceType: DeviceType.VMIX, + type: TimelineContentTypeVMix.PROGRAM, + input: 2, + }), + pgm1: wrapInTimelineObject('pgm1', { + deviceType: DeviceType.VMIX, + type: TimelineContentTypeVMix.PROGRAM, + transition: { + duration: 500, + effect: VMixTransitionType.Fade, + }, + }), + }), + { + pgm0: wrapInMapping({ + mappingType: MappingVmixType.Program, + }), + pgm1: wrapInMapping({ + mappingType: MappingVmixType.Program, + }), + } + ) + expect(result.reportedState.mixes[0]?.transition).toEqual({ + duration: 500, + effect: VMixTransitionType.Fade, + }) + expect(result.reportedState.mixes[0]?.program).toEqual(2) + }) + + it('does not allow overriding transitions in reverse layer order', () => { + const converter = createTestee() + const result = converter.getVMixStateFromTimelineState( + wrapInTimelineState({ + pgm0: wrapInTimelineObject('pgm0', { + deviceType: DeviceType.VMIX, + type: TimelineContentTypeVMix.PROGRAM, + transition: { + duration: 500, + effect: VMixTransitionType.Fade, + }, + }), + pgm1: wrapInTimelineObject('pgm1', { + deviceType: DeviceType.VMIX, + type: TimelineContentTypeVMix.PROGRAM, + input: 2, + }), + }), + { + pgm0: wrapInMapping({ + mappingType: MappingVmixType.Program, + }), + pgm1: wrapInMapping({ + mappingType: MappingVmixType.Program, + }), + } + ) + expect(result.reportedState.mixes[0]?.transition).toEqual({ + duration: 0, + effect: VMixTransitionType.Cut, + }) + expect(result.reportedState.mixes[0]?.program).toEqual(2) + }) + it('supports url', () => { + const converter = createTestee() + const url = 'https://example.com' + const result = converter.getVMixStateFromTimelineState( + wrapInTimelineState({ + inp0: wrapInTimelineObject('inp0', { + deviceType: DeviceType.VMIX, + url, + type: TimelineContentTypeVMix.INPUT, + }), + }), + { + inp0: wrapInMapping({ + mappingType: MappingVmixType.Input, + index: '1', + }), + } + ) + expect(result.reportedState.existingInputs['1'].url).toEqual(url) + }) + it('supports index', () => { + const converter = createTestee() + const index = 3 const result = converter.getVMixStateFromTimelineState( wrapInTimelineState({ inp0: wrapInTimelineObject('inp0', { deviceType: DeviceType.VMIX, - text, + index, type: TimelineContentTypeVMix.INPUT, }), }), @@ -154,7 +242,7 @@ describe('VMixTimelineStateConverter', () => { }), } ) - expect(result.reportedState.existingInputs['1'].text).toEqual(text) + expect(result.reportedState.existingInputs['1'].index).toEqual(index) }) // TODO: maybe we can't trust the defaults when adding an input? Make this test pass eventually diff --git a/packages/timeline-state-resolver/src/integrations/vmix/connection.ts b/packages/timeline-state-resolver/src/integrations/vmix/connection.ts index da0005912..7ee80451c 100644 --- a/packages/timeline-state-resolver/src/integrations/vmix/connection.ts +++ b/packages/timeline-state-resolver/src/integrations/vmix/connection.ts @@ -256,6 +256,10 @@ export class VMixCommandSender { return this.restart(command.input) case VMixCommand.SET_TEXT: return this.setText(command.input, command.value, command.fieldName) + case VMixCommand.BROWSER_NAVIGATE: + return this.browserNavigate(command.input, command.value) + case VMixCommand.SELECT_INDEX: + return this.selectIndex(command.input, command.value) default: throw new Error(`vmixAPI: Command ${((command || {}) as any).command} not implemented`) } @@ -467,6 +471,14 @@ export class VMixCommandSender { return this.sendCommandFunction(`SetText`, { input, value, selectedName: fieldName }) } + public async browserNavigate(input: string | number, value: string): Promise { + return this.sendCommandFunction(`BrowserNavigate`, { input, value: encodeURIComponent(value) }) + } + + public async selectIndex(input: string | number, value: number): Promise { + return this.sendCommandFunction(`SelectIndex`, { input, value }) + } + private async sendCommandFunction(func: string, args: SentCommandArgs) { return this.vMixConnection.sendCommandFunction(func, args) } diff --git a/packages/timeline-state-resolver/src/integrations/vmix/vMixCommands.ts b/packages/timeline-state-resolver/src/integrations/vmix/vMixCommands.ts index f4fbaa80c..446067d36 100644 --- a/packages/timeline-state-resolver/src/integrations/vmix/vMixCommands.ts +++ b/packages/timeline-state-resolver/src/integrations/vmix/vMixCommands.ts @@ -209,6 +209,16 @@ export interface VMixStateCommandSetText extends VMixStateCommandBase { fieldName: string value: string } +export interface VMixStateCommandBrowserNavigate extends VMixStateCommandBase { + command: VMixCommand.BROWSER_NAVIGATE + input: string | number + value: string +} +export interface VMixStateCommanSelectIndex extends VMixStateCommandBase { + command: VMixCommand.SELECT_INDEX + input: string | number + value: number +} export type VMixStateCommand = | VMixStateCommandPreviewInput | VMixStateCommandTransition @@ -255,6 +265,8 @@ export type VMixStateCommand = | VMixStateCommandListRemoveAll | VMixStateCommandRestart | VMixStateCommandSetText + | VMixStateCommandBrowserNavigate + | VMixStateCommanSelectIndex export enum CommandContext { None = 'none', diff --git a/packages/timeline-state-resolver/src/integrations/vmix/vMixStateDiffer.ts b/packages/timeline-state-resolver/src/integrations/vmix/vMixStateDiffer.ts index 966f01b2d..86cd41b6b 100644 --- a/packages/timeline-state-resolver/src/integrations/vmix/vMixStateDiffer.ts +++ b/packages/timeline-state-resolver/src/integrations/vmix/vMixStateDiffer.ts @@ -80,6 +80,8 @@ export interface VMixInput { listFilePaths?: string[] restart?: boolean text?: VMixText + url?: string + index?: number } export interface VMixInputAudio { @@ -601,6 +603,28 @@ export class VMixStateDiffer { } } } + if (input.url !== undefined && oldInput.url !== input.url) { + commands.push({ + command: { + command: VMixCommand.BROWSER_NAVIGATE, + input: key, + value: input.url, + }, + context: CommandContext.None, + timelineId: '', + }) + } + if (input.index !== undefined && oldInput.index !== input.index) { + commands.push({ + command: { + command: VMixCommand.SELECT_INDEX, + input: key, + value: input.index, + }, + context: CommandContext.None, + timelineId: '', + }) + } return { preTransitionCommands, postTransitionCommands } } diff --git a/packages/timeline-state-resolver/src/integrations/vmix/vMixTimelineStateConverter.ts b/packages/timeline-state-resolver/src/integrations/vmix/vMixTimelineStateConverter.ts index 8d44ab71b..faecbc444 100644 --- a/packages/timeline-state-resolver/src/integrations/vmix/vMixTimelineStateConverter.ts +++ b/packages/timeline-state-resolver/src/integrations/vmix/vMixTimelineStateConverter.ts @@ -71,6 +71,11 @@ export class VMixTimelineStateConverter { this._switchToInput(content.input, deviceState, mixProgram, content.transition) } else if (content.inputLayer) { this._switchToInput(content.inputLayer, deviceState, mixProgram, content.transition, true) + } else if (content.transition) { + const mixState = deviceState.reportedState.mixes[mixProgram] + if (mixState) { + mixState.transition = content.transition + } } } break @@ -144,6 +149,8 @@ export class VMixTimelineStateConverter { listFilePaths: content.listFilePaths, restart: content.restart, text: content.text, + url: content.url, + index: content.index, }, { key: mapping.options.index, filePath: content.filePath }, layerName @@ -239,7 +246,7 @@ export class VMixTimelineStateConverter { mixState.preview = mixState.program mixState.program = input - mixState.transition = transition || { effect: VMixTransitionType.Cut, duration: 0 } + mixState.transition = transition ?? { effect: VMixTransitionType.Cut, duration: 0 } mixState.layerToProgram = layerToProgram } }