Skip to content

Commit

Permalink
Merge pull request #359 from tv2norge-collab/contribute/EAV-411
Browse files Browse the repository at this point in the history
feat(vMix): support setting text in vMix titles
  • Loading branch information
jstarpl authored Dec 10, 2024
2 parents a7369c9 + 65b0dcc commit 97957d7
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 12 deletions.
10 changes: 10 additions & 0 deletions packages/timeline-state-resolver-types/src/integrations/vmix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export enum VMixCommand {
LIST_ADD = 'LIST_ADD',
LIST_REMOVE_ALL = 'LIST_REMOVE_ALL',
RESTART_INPUT = 'RESTART_INPUT',
SET_TEXT = 'SET_TEXT',
BROWSER_NAVIGATE = 'BROWSER_NAVIGATE',
SELECT_INDEX = 'SELECT_INDEX',
}
Expand Down Expand Up @@ -191,6 +192,11 @@ export interface TimelineContentVMixInput extends TimelineContentVMixBase {
/** If media should start from the beginning or resume from where it left off */
restart?: boolean

/**
* Titles (GT): Sets the values of text fields by name
*/
text?: VMixText

/** The URL for Browser input */
url?: string

Expand Down Expand Up @@ -255,6 +261,10 @@ export interface VMixInputOverlays {
[index: number]: number | string
}

export interface VMixText {
[index: string]: string
}

export interface VMixLayer {
input: string | number

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,23 @@ describe('VMixCommandSender', () => {
})
})

it('sets text', async () => {
const { sender, mockConnection } = createTestee()
await sender.sendCommand({
command: VMixCommand.SET_TEXT,
input: 5,
value: 'Foo',
fieldName: 'myTitle.Text',
})

expect(mockConnection.sendCommandFunction).toHaveBeenCalledTimes(1)
expect(mockConnection.sendCommandFunction).toHaveBeenLastCalledWith('SetText', {
input: 5,
value: 'Foo',
selectedName: 'myTitle.Text',
})
})

it('sends url', async () => {
const { sender, mockConnection } = createTestee()
await sender.sendCommand({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,140 @@ describe('VMixStateDiffer', () => {
})
})

it('sets text', () => {
const differ = createTestee()

const oldState = makeMockFullState()
const newState = makeMockFullState()

oldState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99)

newState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99)

newState.reportedState.existingInputs['99'].text = {
'myTitle.Text': 'SomeValue',
}

const commands = differ.getCommandsToAchieveState(Date.now(), oldState, newState)

expect(commands.length).toBe(1)
expect(commands[0].command).toMatchObject({
command: VMixCommand.SET_TEXT,
input: '99',
value: 'SomeValue',
fieldName: 'myTitle.Text',
})
})

it('sets multiple texts', () => {
const differ = createTestee()

const oldState = makeMockFullState()
const newState = makeMockFullState()

oldState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99)

newState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99)

newState.reportedState.existingInputs['99'].text = {
'myTitle.Text': 'SomeValue',
'myTitle.Foo': 'Bar',
}

const commands = differ.getCommandsToAchieveState(Date.now(), oldState, newState)

expect(commands.length).toBe(2)
expect(commands[0].command).toMatchObject({
command: VMixCommand.SET_TEXT,
input: '99',
value: 'SomeValue',
fieldName: 'myTitle.Text',
})
expect(commands[1].command).toMatchObject({
command: VMixCommand.SET_TEXT,
input: '99',
value: 'Bar',
fieldName: 'myTitle.Foo',
})
})

it('does not unset text', () => {
// it would have to be explicitly set to an empty string on the timeline
const differ = createTestee()

const oldState = makeMockFullState()
const newState = makeMockFullState()

oldState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99)
oldState.reportedState.existingInputs['99'].text = {
'myTitle.Text': 'SomeValue',
'myTitle.Foo': 'Bar',
}

newState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99)
newState.reportedState.existingInputs['99'].text = {
'myTitle.Foo': 'Bar',
}

const commands = differ.getCommandsToAchieveState(Date.now(), oldState, newState)

expect(commands.length).toBe(0)
})

it('updates text', () => {
const differ = createTestee()

const oldState = makeMockFullState()
const newState = makeMockFullState()

oldState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99)
oldState.reportedState.existingInputs['99'].text = {
'myTitle.Text': 'SomeValue',
}

newState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99)
newState.reportedState.existingInputs['99'].text = {
'myTitle.Text': 'Bar',
}

const commands = differ.getCommandsToAchieveState(Date.now(), oldState, newState)

expect(commands.length).toBe(1)
expect(commands[0].command).toMatchObject({
command: VMixCommand.SET_TEXT,
input: '99',
value: 'Bar',
fieldName: 'myTitle.Text',
})
})

it('updates text to an empty string', () => {
const differ = createTestee()

const oldState = makeMockFullState()
const newState = makeMockFullState()

oldState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99)
oldState.reportedState.existingInputs['99'].text = {
'myTitle.Text': 'SomeValue',
}

newState.reportedState.existingInputs['99'] = differ.getDefaultInputState(99)
newState.reportedState.existingInputs['99'].text = {
'myTitle.Text': '',
}

const commands = differ.getCommandsToAchieveState(Date.now(), oldState, newState)

expect(commands.length).toBe(1)
expect(commands[0].command).toMatchObject({
command: VMixCommand.SET_TEXT,
input: '99',
value: '',
fieldName: 'myTitle.Text',
})
})

it('sets browser url', () => {
const differ = createTestee()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,27 @@ describe('VMixTimelineStateConverter', () => {
expect(result.reportedState.inputsAddedByUsAudio[prefixAddedInput(filePath)]).toBeUndefined()
})

it('supports text', () => {
const converter = createTestee()
const text = { 'myTitle.Text': 'SomeValue', 'myTitle.Foo': 'Bar' }
const result = converter.getVMixStateFromTimelineState(
wrapInTimelineState({
inp0: wrapInTimelineObject('inp0', {
deviceType: DeviceType.VMIX,
text,
type: TimelineContentTypeVMix.INPUT,
}),
}),
{
inp0: wrapInMapping({
mappingType: MappingVmixType.Input,
index: '1',
}),
}
)
expect(result.reportedState.existingInputs['1'].text).toEqual(text)
})

it('allows overriding transitions in usual layer order', () => {
const converter = createTestee()
const result = converter.getVMixStateFromTimelineState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,31 @@ describe('VMixXmlStateParser', () => {
},
})
})

it('parses text (titles)', () => {
const parser = new VMixXmlStateParser()

const parsedState = parser.parseVMixState(
makeMockVMixXmlState([
'<input key="a97b8de1-807a-4c14-8eb9-3de0129b41e3" number="1" type="Capture" title="Cam 0" state="Running" position="0" duration="0" loop="False" muted="False" volume="100" balance="0" solo="False" audiobusses="M" meterF1="0.03034842" meterF2="0.03034842"></input>',
`<input key="ca9bc59f-f698-41fe-b17d-1e1743cfee88" number="2" type="GT" title="gfx.gtzip" shortTitle="gfx.gtzip" state="Paused" position="0" duration="0" loop="False" >
gfx.gtzip
<text index="0" name="TextBlock1.Text">SomeText</text>
<text index="1" name="AnotherBlock.Text">Foo</text>
</input>`,
'<input key="1d70bc59-6517-4571-a0c5-932e30311f01" number="3" type="Capture" title="Cam 2" state="Running" position="0" duration="0" loop="False" muted="False" volume="100" balance="0" solo="False" audiobusses="M" meterF1="0.03034842" meterF2="0.03034842"></input>',
])
)

expect(parsedState).toMatchObject<Partial<VMixState>>({
existingInputs: {
'2': {
text: {
'TextBlock1.Text': 'SomeText',
'AnotherBlock.Text': 'Foo',
},
},
},
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,13 @@ export type ConnectionEvents = {
error: [error: Error]
}

/**
* This TSR integration polls the state of vMix and merges that into our last-known state.
* However, not all state properties can be retried from vMix's API.
* Therefore, there are some properties that we must "carry over" from our last-known state, every time.
* These are those property keys for the Input state objects.
*/
export type InferredPartialInputStateKeys = 'filePath' | 'fade' | 'audioAuto' | 'restart'

interface SentCommandArgs {
input?: string | number
value?: string | number
extra?: string
duration?: number
mix?: number
selectedName?: string
}

export class VMixConnection extends EventEmitter<ConnectionEvents> {
Expand Down Expand Up @@ -76,9 +69,10 @@ export class VMixConnection extends EventEmitter<ConnectionEvents> {
const val = args.value !== undefined ? `&Value=${args.value}` : ''
const dur = args.duration !== undefined ? `&Duration=${args.duration}` : ''
const mix = args.mix !== undefined ? `&Mix=${args.mix}` : ''
const selectedName = args.selectedName !== undefined ? `&SelectedName=${args.selectedName}` : ''
const ext = args.extra !== undefined ? args.extra : ''

const queryString = `${inp}${val}${dur}${mix}${ext}`.slice(1) // remove the first &
const queryString = `${inp}${val}${dur}${mix}${ext}${selectedName}`.slice(1) // remove the first &
let command = `FUNCTION ${func}`

if (queryString) {
Expand Down Expand Up @@ -260,6 +254,8 @@ export class VMixCommandSender {
return this.listRemoveAll(command.input)
case VMixCommand.RESTART_INPUT:
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:
Expand Down Expand Up @@ -471,6 +467,10 @@ export class VMixCommandSender {
return this.sendCommandFunction(`Restart`, { input })
}

public async setText(input: string | number, value: string, fieldName: string): Promise<any> {
return this.sendCommandFunction(`SetText`, { input, value, selectedName: fieldName })
}

public async browserNavigate(input: string | number, value: string): Promise<any> {
return this.sendCommandFunction(`BrowserNavigate`, { input, value: encodeURIComponent(value) })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ export interface VMixStateCommandRestart extends VMixStateCommandBase {
command: VMixCommand.RESTART_INPUT
input: string | number
}
export interface VMixStateCommandSetText extends VMixStateCommandBase {
command: VMixCommand.SET_TEXT
input: string | number
fieldName: string
value: string
}
export interface VMixStateCommandBrowserNavigate extends VMixStateCommandBase {
command: VMixCommand.BROWSER_NAVIGATE
input: string | number
Expand Down Expand Up @@ -258,6 +264,7 @@ export type VMixStateCommand =
| VMixStateCommandListAdd
| VMixStateCommandListRemoveAll
| VMixStateCommandRestart
| VMixStateCommandSetText
| VMixStateCommandBrowserNavigate
| VMixStateCommanSelectIndex

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
VMixTransition,
VMixTransitionType,
VMixLayer,
VMixText,
} from 'timeline-state-resolver-types'
import { CommandContext, VMixStateCommandWithContext } from './vMixCommands'
import _ = require('underscore')
Expand Down Expand Up @@ -78,6 +79,7 @@ export interface VMixInput {
layers?: VMixLayers
listFilePaths?: string[]
restart?: boolean
text?: VMixText
url?: string
index?: number
}
Expand Down Expand Up @@ -585,6 +587,22 @@ export class VMixStateDiffer {
timelineId: '',
})
}
if (input.text !== undefined) {
for (const [fieldName, value] of Object.entries<string>(input.text)) {
if (oldInput?.text?.[fieldName] !== value) {
commands.push({
command: {
command: VMixCommand.SET_TEXT,
input: key,
value,
fieldName,
},
context: CommandContext.None,
timelineId: '',
})
}
}
}
if (input.url !== undefined && oldInput.url !== input.url) {
commands.push({
command: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export class VMixTimelineStateConverter {
(content.overlays ? this._convertDeprecatedInputOverlays(content.overlays) : undefined),
listFilePaths: content.listFilePaths,
restart: content.restart,
text: content.text,
url: content.url,
index: content.index,
},
Expand Down
Loading

0 comments on commit 97957d7

Please sign in to comment.