Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vMix): support setting text in vMix titles #359

Merged
merged 3 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading