-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #315 from tv2norge-collab/contribute/EAV-142/fix-v…
…mix-media-reload-retrying-release50 refactor: A series of refactorings and fixes in vMix integration
- Loading branch information
Showing
19 changed files
with
2,654 additions
and
1,610 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
packages/timeline-state-resolver/src/integrations/vmix/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
This is the vMix integration. | ||
|
||
## Shared control | ||
|
||
Similarly to the TriCaster integration, only resources (features) that have corresponding mappings explicitly defined are controlled by the TSR and will have commands generated. Those resources may receive commands resetting them to TSR-defined defaults when no timeline objects are present on corresponding layers. The resources are divided as follows: | ||
- inputs - each input individually, when a corresponding mapping of type `MappingVmixType.Input` exists | ||
- audio channels - each audio channel (input), when a corresponding mapping of type `MappingVmixType.AudioChannel` exists | ||
- outputs - each output individually, when a corresponding mapping of type `MappingVmixType.Output` exists | ||
- overlays - each overlay individually, when a corresponding mapping of type `MappingVmixType.Overlay` exists | ||
- recording - when a mapping of type `MappingVmixType.Recording` exists | ||
- streaming - when a mapping of type `MappingVmixType.Streaming` exists | ||
- external - when a mapping of type `MappingVmixType.External` exists | ||
- mix inputs and main mix - each mix individually when at least one of corresponding mappings of type `MappingVmixType.Program` or `MappingVmixType.Preview` exists | ||
|
||
## Current limitations | ||
|
||
- For most purposes, referencing inputs by numbers is recommended. Mappings refrencing inputs by names are suitable only when the names are known to be unique. However, the state read from vMix primarily uses input numbers, so restart of the TSR when names were used in the mappings and on the timeline, might trigger some unwanted transitions. Mixing string and numeric names may lead to even more unexpected results. | ||
- Adding more than one input with the same `filePath` is not supported. | ||
|
||
## Known bugs | ||
|
||
- Commands adding inputs may be sent multiple times, resulting in the same input being added more than once. Fixed in release51. |
173 changes: 173 additions & 0 deletions
173
packages/timeline-state-resolver/src/integrations/vmix/__tests__/mockState.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import { VMixTransitionType } from 'timeline-state-resolver-types' | ||
import { TSR_INPUT_PREFIX, VMixState, VMixStateExtended } from '../vMixStateDiffer' | ||
|
||
export const ADDED_INPUT_NAME_1 = `${TSR_INPUT_PREFIX}C:\\someVideo.mp4` | ||
export const ADDED_INPUT_NAME_2 = `${TSR_INPUT_PREFIX}C:\\anotherVideo.mp4` | ||
|
||
export function makeMockReportedState(): VMixState { | ||
return { | ||
version: '21.0.0.55', | ||
edition: 'HD', | ||
existingInputs: { | ||
'1': { | ||
number: 1, | ||
type: 'Capture', | ||
state: 'Running', | ||
position: 0, | ||
duration: 0, | ||
loop: false, | ||
transform: { | ||
alpha: -1, | ||
panX: 0, | ||
panY: 0, | ||
zoom: 1, | ||
}, | ||
listFilePaths: undefined, | ||
overlays: undefined, | ||
playing: true, | ||
}, | ||
'2': { | ||
number: 2, | ||
type: 'Capture', | ||
state: 'Running', | ||
position: 0, | ||
duration: 0, | ||
loop: false, | ||
transform: { | ||
alpha: -1, | ||
panX: 0, | ||
panY: 0, | ||
zoom: 1, | ||
}, | ||
listFilePaths: undefined, | ||
overlays: undefined, | ||
playing: true, | ||
}, | ||
}, | ||
existingInputsAudio: { | ||
'1': { | ||
muted: false, | ||
volume: 100, | ||
balance: 0, | ||
audioBuses: 'M', | ||
solo: false, | ||
}, | ||
'2': { | ||
muted: true, | ||
volume: 100, | ||
balance: 0, | ||
audioBuses: 'M,C', | ||
solo: false, | ||
}, | ||
}, | ||
inputsAddedByUs: { | ||
[ADDED_INPUT_NAME_1]: { | ||
number: 1, | ||
type: 'Video', | ||
state: 'Running', | ||
position: 0, | ||
duration: 0, | ||
loop: false, | ||
transform: { | ||
alpha: -1, | ||
panX: 0, | ||
panY: 0, | ||
zoom: 1, | ||
}, | ||
listFilePaths: undefined, | ||
overlays: undefined, | ||
playing: true, | ||
name: ADDED_INPUT_NAME_1, | ||
}, | ||
[ADDED_INPUT_NAME_2]: { | ||
number: 1, | ||
type: 'Video', | ||
state: 'Running', | ||
position: 0, | ||
duration: 0, | ||
loop: false, | ||
transform: { | ||
alpha: -1, | ||
panX: 0, | ||
panY: 0, | ||
zoom: 1, | ||
}, | ||
listFilePaths: undefined, | ||
overlays: undefined, | ||
playing: true, | ||
name: ADDED_INPUT_NAME_2, | ||
}, | ||
}, | ||
inputsAddedByUsAudio: { | ||
'1': { | ||
muted: false, | ||
volume: 100, | ||
balance: 0, | ||
audioBuses: 'M', | ||
solo: false, | ||
}, | ||
'2': { | ||
muted: false, | ||
volume: 100, | ||
balance: 0, | ||
audioBuses: 'M', | ||
solo: false, | ||
}, | ||
}, | ||
overlays: [ | ||
{ number: 1, input: undefined }, | ||
{ number: 2, input: undefined }, | ||
{ number: 3, input: undefined }, | ||
{ number: 4, input: undefined }, | ||
{ number: 5, input: undefined }, | ||
{ number: 6, input: undefined }, | ||
], | ||
mixes: [ | ||
{ | ||
number: 1, | ||
program: 1, | ||
preview: 2, | ||
transition: { | ||
duration: 0, | ||
effect: VMixTransitionType.Cut, | ||
}, | ||
}, | ||
], | ||
fadeToBlack: false, | ||
recording: true, | ||
external: true, | ||
streaming: true, | ||
playlist: false, | ||
multiCorder: false, | ||
fullscreen: false, | ||
audio: [ | ||
{ | ||
volume: 100, | ||
muted: false, | ||
meterF1: 0.04211706, | ||
meterF2: 0.04211706, | ||
headphonesVolume: 74.80521, | ||
}, | ||
], | ||
} | ||
} | ||
|
||
export function makeMockFullState(): VMixStateExtended { | ||
return { | ||
inputLayers: {}, | ||
outputs: { | ||
External2: undefined, | ||
2: undefined, | ||
3: undefined, | ||
4: undefined, | ||
Fullscreen: undefined, | ||
Fullscreen2: undefined, | ||
}, | ||
runningScripts: [], | ||
reportedState: makeMockReportedState(), | ||
} | ||
} | ||
|
||
export function prefixAddedInput(inputName: string): string { | ||
return TSR_INPUT_PREFIX + inputName | ||
} |
114 changes: 114 additions & 0 deletions
114
packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixPollingTimer.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import { VMixPollingTimer } from '../vMixPollingTimer' | ||
|
||
describe('VMixPollingTimer', () => { | ||
beforeEach(() => { | ||
jest.useFakeTimers() | ||
}) | ||
|
||
it('ticks in set intervals', () => { | ||
const interval = 1500 | ||
const timer = new VMixPollingTimer(interval) | ||
|
||
const onTick = jest.fn() | ||
timer.on('tick', onTick) | ||
|
||
timer.start() | ||
expect(onTick).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(interval - 10) | ||
expect(onTick).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(10) // 1500 | ||
expect(onTick).toHaveBeenCalledTimes(1) | ||
onTick.mockClear() | ||
|
||
jest.advanceTimersByTime(interval - 10) // 2990 | ||
expect(onTick).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(10) // 3500 | ||
expect(onTick).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
test('calling start() multiple times does not produce excessive events', () => { | ||
const interval = 1500 | ||
const timer = new VMixPollingTimer(interval) | ||
|
||
const onTick = jest.fn() | ||
timer.on('tick', onTick) | ||
|
||
timer.start() | ||
timer.start() | ||
expect(onTick).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(interval - 10) | ||
expect(onTick).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(10) // 1500 | ||
expect(onTick).toHaveBeenCalledTimes(1) | ||
onTick.mockClear() | ||
|
||
timer.start() | ||
expect(onTick).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(interval - 10) // 2990 | ||
expect(onTick).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(10) // 3500 | ||
expect(onTick).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it('can be stopped', () => { | ||
const interval = 1500 | ||
const timer = new VMixPollingTimer(interval) | ||
|
||
const onTick = jest.fn() | ||
timer.on('tick', onTick) | ||
|
||
timer.start() | ||
expect(onTick).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(interval) // 1500 | ||
expect(onTick).toHaveBeenCalledTimes(1) | ||
onTick.mockClear() | ||
|
||
timer.stop() | ||
|
||
jest.advanceTimersByTime(interval) // 3000 | ||
expect(onTick).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(interval) // 4500 | ||
expect(onTick).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('can be postponed', () => { | ||
const interval = 1500 | ||
const timer = new VMixPollingTimer(interval) | ||
|
||
const onTick = jest.fn() | ||
timer.on('tick', onTick) | ||
|
||
timer.start() | ||
expect(onTick).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(interval) // 1500 | ||
expect(onTick).toHaveBeenCalledTimes(1) | ||
onTick.mockClear() | ||
|
||
const postponeTime = 5000 | ||
timer.postponeNextTick(postponeTime) | ||
|
||
jest.advanceTimersByTime(interval) // 3000 | ||
expect(onTick).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(postponeTime - interval - 10) // 6490 | ||
expect(onTick).not.toHaveBeenCalled() | ||
|
||
jest.advanceTimersByTime(10) // 6500 | ||
expect(onTick).toHaveBeenCalledTimes(1) | ||
onTick.mockClear() | ||
|
||
// it should return to normal interval | ||
jest.advanceTimersByTime(interval) // 8010 | ||
expect(onTick).toHaveBeenCalledTimes(1) | ||
}) | ||
}) |
35 changes: 35 additions & 0 deletions
35
packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixStateDiffer.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { VMixCommand } from 'timeline-state-resolver-types' | ||
import { VMixStateDiffer } from '../vMixStateDiffer' | ||
import { makeMockFullState } from './mockState' | ||
|
||
function createTestee(): VMixStateDiffer { | ||
return new VMixStateDiffer(jest.fn()) | ||
} | ||
|
||
/** | ||
* Note: most of the coverage is still in vmix.spec.ts | ||
*/ | ||
describe('VMixStateDiffer', () => { | ||
it('does not generate commands for identical states', () => { | ||
const differ = createTestee() | ||
|
||
const oldState = makeMockFullState() | ||
const newState = makeMockFullState() | ||
|
||
expect(differ.getCommandsToAchieveState(oldState, newState)).toEqual([]) | ||
}) | ||
|
||
it('resets audio buses when audio starts to be controlled', () => { | ||
const differ = createTestee() | ||
|
||
const oldState = makeMockFullState() | ||
const newState = makeMockFullState() | ||
|
||
newState.reportedState.existingInputsAudio['99'] = differ.getDefaultInputAudioState(99) | ||
|
||
const commands = differ.getCommandsToAchieveState(oldState, newState) | ||
const busCommands = commands.filter((command) => command.command.command === VMixCommand.AUDIO_BUS_OFF) | ||
|
||
expect(busCommands.length).toBe(7) // all but Master | ||
}) | ||
}) |
Oops, something went wrong.