UNPKG

timeline-state-resolver

Version:
823 lines • 36.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VMixStateDiffer = exports.TSR_INPUT_PREFIX = void 0; const timeline_state_resolver_types_1 = require("timeline-state-resolver-types"); const vMixCommands_1 = require("./vMixCommands"); const _ = require("underscore"); const VMixInputHandler_1 = require("./VMixInputHandler"); /** Prefix of media input added by TSR. Only those with this prefix can be removed by this implementation */ exports.TSR_INPUT_PREFIX = 'TSR_MEDIA_'; class VMixStateDiffer { constructor(getCurrentTime, queueNow) { this.getCurrentTime = getCurrentTime; this.queueNow = queueNow; this.inputHandler = new VMixInputHandler_1.VMixInputHandler({ getCurrentTime: this.getCurrentTime, addToQueue: this.queueNow, }); } getCommandsToAchieveState(time, oldVMixState, newVMixState) { let commands = []; const inputCommands = this._resolveInputsState(oldVMixState, newVMixState); commands = commands.concat(inputCommands.preTransitionCommands); commands = commands.concat(this._resolveMixState(oldVMixState, newVMixState)); commands = commands.concat(this._resolveOverlaysState(oldVMixState, newVMixState)); commands = commands.concat(inputCommands.postTransitionCommands); commands = commands.concat(this._resolveInputsAudioState(oldVMixState, newVMixState)); commands = commands.concat(this._resolveRecordingState(oldVMixState.reportedState, newVMixState.reportedState)); commands = commands.concat(this._resolveStreamingState(oldVMixState.reportedState, newVMixState.reportedState)); commands = commands.concat(this._resolveExternalState(oldVMixState.reportedState, newVMixState.reportedState)); commands = commands.concat(this._resolveOutputsState(oldVMixState, newVMixState)); commands = commands.concat(this._resolveAddedByUsInputsRemovalState(time, oldVMixState.reportedState, newVMixState.reportedState)); commands = commands.concat(this._resolveScriptsState(oldVMixState, newVMixState)); return commands; } getDefaultState() { return { reportedState: { version: '', edition: '', existingInputs: {}, existingInputsAudio: {}, inputsAddedByUs: {}, inputsAddedByUsAudio: {}, overlays: [], mixes: [], fadeToBlack: false, faderPosition: 0, recording: undefined, external: undefined, streaming: undefined, playlist: false, multiCorder: false, fullscreen: false, audio: [], }, outputs: { '2': undefined, '3': undefined, '4': undefined, External2: undefined, Fullscreen: undefined, Fullscreen2: undefined, }, inputLayers: {}, runningScripts: [], }; } getDefaultInputState(inputNumber) { return { number: Number(inputNumber) || undefined, position: 0, loop: false, playing: false, transform: { zoom: 1, panX: 0, panY: 0, alpha: 255, }, layers: {}, }; } getDefaultInputAudioState(inputNumber) { return { number: Number(inputNumber) || undefined, muted: true, volume: 100, balance: 0, fade: 0, audioBuses: 'M', audioAuto: true, }; } _resolveMixState(oldVMixState, newVMixState) { const commands = []; newVMixState.reportedState.mixes.forEach((_mix, i) => { /** * It is *not* guaranteed to have all mixes present in the vMix state because it's a sparse array. */ const oldMixState = oldVMixState.reportedState.mixes[i]; const newMixState = newVMixState.reportedState.mixes[i]; if (newMixState?.program !== undefined) { let nextInput = newMixState.program; let changeOnLayer = false; if (newMixState.layerToProgram) { nextInput = newVMixState.inputLayers[newMixState.program]; changeOnLayer = newVMixState.inputLayers[newMixState.program] !== oldVMixState.inputLayers[newMixState.program]; } if (oldMixState?.program !== newMixState.program || changeOnLayer) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.TRANSITION, effect: changeOnLayer ? timeline_state_resolver_types_1.VMixTransitionType.Cut : newMixState.transition.effect, input: nextInput, duration: changeOnLayer ? 0 : newMixState.transition.duration, mix: i, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } if (oldMixState?.program === newMixState?.program && // if we're not switching what is on program, because it could break a transition newMixState?.preview !== undefined && newMixState.preview !== oldMixState?.preview) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.PREVIEW_INPUT, input: newMixState.preview, mix: i, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } }); // Only set fader bar position if no other transitions are happening if (oldVMixState.reportedState.mixes[0]?.program === newVMixState.reportedState.mixes[0]?.program) { if (newVMixState.reportedState.faderPosition !== oldVMixState.reportedState.faderPosition) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.FADER, value: newVMixState.reportedState.faderPosition || 0, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); // newVMixState.reportedState.program = undefined // newVMixState.reportedState.preview = undefined newVMixState.reportedState.fadeToBlack = false; } } if (oldVMixState.reportedState.fadeToBlack !== newVMixState.reportedState.fadeToBlack) { // Danger: Fade to black is toggled, we can't explicitly say that we want it on or off commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.FADE_TO_BLACK, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } return commands; } _resolveInputsState(oldVMixState, newVMixState) { const preTransitionCommands = []; const postTransitionCommands = []; _.map(newVMixState.reportedState.existingInputs, (input, key) => this._resolveExistingInputState(oldVMixState.reportedState.existingInputs[key], input, key, oldVMixState)).forEach((commands) => { preTransitionCommands.push(...commands.preTransitionCommands); postTransitionCommands.push(...commands.postTransitionCommands); }); _.map(newVMixState.reportedState.inputsAddedByUs, (input, key) => this._resolveAddedByUsInputState(oldVMixState.reportedState.inputsAddedByUs[key], input, key, oldVMixState)).forEach((commands) => { preTransitionCommands.push(...commands.preTransitionCommands); postTransitionCommands.push(...commands.postTransitionCommands); }); return { preTransitionCommands, postTransitionCommands }; } _resolveExistingInputState(oldInput, input, key, oldVMixState) { oldInput ?? (oldInput = {}); // if we just started controlling it (e.g. due to mappings change), we don't know anything about the input return this._resolveInputState(oldVMixState, oldInput, input, key); } _resolveInputState(oldVMixState, oldInput, input, key) { if (input.name === undefined) { input.name = key; } const preTransitionCommands = []; const postTransitionCommands = []; /** * If an input is currently on air, then we delay changes to it until after the transition has began. * Note the word "began", instead of "completed". * * This mostly helps in the case of CUT transitions, where in theory everything happens * on the same frame but, in reality, thanks to how vMix processes API commands, * things take place over the course of a few frames. */ const commands = this._isInUse(oldVMixState, oldInput) ? postTransitionCommands : preTransitionCommands; // It is important that the operations on listFilePaths happen before most other operations. // Consider the case where we want to change the contents of a List input AND set it to playing. // If we set it to playing first, it will automatically be forced to stop playing when // we dispatch LIST_REMOVE_ALL. // So, order of operations matters here. if (!_.isEqual(oldInput.listFilePaths, input.listFilePaths)) { // vMix has a quirk that we are working around here: // When a List input has no items, its Play/Pause button becomes inactive and // clicking it does nothing. However, if the List was playing when it was emptied, // it'll remain in a playing state. This means that as soon as new content is // added to the playlist, it will immediately begin playing. This feels like a // bug/mistake/otherwise unwanted behavior in every scenario. To work around this, // we automatically dispatch a PAUSE_INPUT command before emptying the playlist, // but only if there's no new content being added afterward. if (!input.listFilePaths || (Array.isArray(input.listFilePaths) && input.listFilePaths.length <= 0)) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.PAUSE_INPUT, input: input.name, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.LIST_REMOVE_ALL, input: input.name, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); if (Array.isArray(input.listFilePaths)) { for (const filePath of input.listFilePaths) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.LIST_ADD, input: input.name, value: filePath, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } } if (input.playing !== undefined && oldInput.playing !== input.playing && !input.playing) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.PAUSE_INPUT, input: input.name, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (oldInput.position !== input.position) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_POSITION, input: key, value: input.position ? input.position : 0, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (input.restart !== undefined && oldInput.restart !== input.restart && input.restart) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.RESTART_INPUT, input: key, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (input.loop !== undefined && oldInput.loop !== input.loop) { if (input.loop) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.LOOP_ON, input: input.name, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } else { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.LOOP_OFF, input: input.name, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } if (input.transform !== undefined && !_.isEqual(oldInput.transform, input.transform)) { if (oldInput.transform === undefined || input.transform.zoom !== oldInput.transform.zoom) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_ZOOM, input: key, value: input.transform.zoom, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (oldInput.transform === undefined || input.transform.alpha !== oldInput.transform.alpha) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_ALPHA, input: key, value: input.transform.alpha, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (oldInput.transform === undefined || input.transform.panX !== oldInput.transform.panX) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_PAN_X, input: key, value: input.transform.panX, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (oldInput.transform === undefined || input.transform.panY !== oldInput.transform.panY) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_PAN_Y, input: key, value: input.transform.panY, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } if (input.layers !== undefined && !_.isEqual(oldInput.layers, input.layers)) { for (const [indexString, layer] of Object.entries(input.layers)) { const index = Number(indexString); const oldLayer = oldInput.layers?.[index]; if (layer.input !== oldLayer?.input) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_LAYER_INPUT, input: key, value: layer.input, index, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (layer.panX !== undefined && layer.panX !== oldLayer?.panX) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_LAYER_PAN_X, input: key, value: layer.panX, index, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (layer.panY !== undefined && layer.panY !== oldLayer?.panY) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_LAYER_PAN_Y, input: key, value: layer.panY, index, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (layer.zoom !== undefined && layer.zoom !== oldLayer?.zoom) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_LAYER_ZOOM, input: key, value: layer.zoom, index, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if ((layer.cropLeft !== undefined || layer.cropTop !== undefined || layer.cropRight !== undefined || layer.cropBottom !== undefined) && (layer.cropLeft !== oldLayer?.cropLeft || layer.cropTop !== oldLayer?.cropTop || layer.cropRight !== oldLayer?.cropRight || layer.cropBottom !== oldLayer?.cropBottom)) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_LAYER_CROP, input: key, cropLeft: layer.cropLeft ?? 0, cropTop: layer.cropTop ?? 0, cropRight: layer.cropRight ?? 1, cropBottom: layer.cropBottom ?? 1, index, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } for (const index of Object.keys(oldInput.layers ?? {})) { if (!input.layers?.[index]) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_LAYER_INPUT, input: key, value: '', index: Number(index), }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } } if (input.playing !== undefined && oldInput.playing !== input.playing && input.playing) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.PLAY_INPUT, input: input.name, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (input.text !== undefined) { for (const [fieldName, value] of Object.entries(input.text)) { if (oldInput?.text?.[fieldName] !== value) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_TEXT, input: key, value, fieldName, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } } if (input.url !== undefined && oldInput.url !== input.url) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.BROWSER_NAVIGATE, input: key, value: input.url, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (input.index !== undefined && oldInput.index !== input.index) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SELECT_INDEX, input: key, value: input.index, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (input.images !== undefined) { for (const [fieldName, value] of Object.entries(input.images)) { if (oldInput?.images?.[fieldName] !== value) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_IMAGE, input: key, value, fieldName, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } } return { preTransitionCommands, postTransitionCommands }; } _resolveInputsAudioState(oldVMixState, newVMixState) { const commands = []; for (const [key, input] of Object.entries(newVMixState.reportedState.existingInputsAudio)) { this._resolveInputAudioState(oldVMixState.reportedState.existingInputsAudio[key] ?? {}, // if we just started controlling it (e.g. due to mappings change), we don't know anything about the input input, commands, key); } for (const [key, input] of Object.entries(newVMixState.reportedState.inputsAddedByUsAudio)) { this._resolveInputAudioState(oldVMixState.reportedState.inputsAddedByUsAudio[key] ?? this.getDefaultInputAudioState(key), // we assume that a new input has all parameters default input, commands, key); } return commands; } _resolveInputAudioState(oldInput, input, commands, key) { if (input.muted !== undefined && oldInput.muted !== input.muted && input.muted) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.AUDIO_OFF, input: key, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (oldInput.volume !== input.volume && input.volume !== undefined) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.AUDIO_VOLUME, input: key, value: input.volume, fade: input.fade, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (oldInput.balance !== input.balance && input.balance !== undefined) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.AUDIO_BALANCE, input: key, value: input.balance, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } if (input.audioAuto !== undefined && oldInput.audioAuto !== input.audioAuto) { if (!input.audioAuto) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.AUDIO_AUTO_OFF, input: key, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } else { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.AUDIO_AUTO_ON, input: key, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } if (input.audioBuses !== undefined && oldInput.audioBuses !== input.audioBuses) { const oldBuses = (oldInput.audioBuses || 'M,A,B,C,D,E,F,G').split(',').filter((x) => x); const newBuses = input.audioBuses.split(',').filter((x) => x); _.difference(newBuses, oldBuses).forEach((bus) => { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.AUDIO_BUS_ON, input: key, value: bus, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); }); _.difference(oldBuses, newBuses).forEach((bus) => { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.AUDIO_BUS_OFF, input: key, value: bus, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); }); } if (input.muted !== undefined && oldInput.muted !== input.muted && !input.muted) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.AUDIO_ON, input: key, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } _resolveAddedByUsInputState(oldInput, input, key, oldVMixState) { if (input.name === undefined) { input.name = key; } if (oldInput == null && input.type !== undefined) { this.inputHandler.addInput(key, input.type, input.name); } oldInput ?? (oldInput = this.getDefaultInputState(0)); // or {} but we assume that a new input has all parameters default return this._resolveInputState(oldVMixState, oldInput, input, key); } _resolveAddedByUsInputsRemovalState(time, oldVMixState, newVMixState) { const commands = []; _.difference(Object.keys(oldVMixState.inputsAddedByUs), Object.keys(newVMixState.inputsAddedByUs)).forEach((input) => { this.inputHandler.removeInput(time, input); }); return commands; } _resolveOverlaysState(oldVMixState, newVMixState) { const commands = []; newVMixState.reportedState.overlays.forEach((overlay, index) => { const oldOverlay = oldVMixState.reportedState.overlays[index]; if (overlay != null && (oldOverlay == null || oldOverlay?.input !== overlay.input)) { if (overlay.input === undefined) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.OVERLAY_INPUT_OUT, value: overlay.number, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } else { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.OVERLAY_INPUT_IN, input: overlay.input, value: overlay.number, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } }); return commands; } _resolveRecordingState(oldVMixState, newVMixState) { const commands = []; if (newVMixState.recording != null && oldVMixState.recording !== newVMixState.recording) { if (newVMixState.recording) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.START_RECORDING, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } else { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.STOP_RECORDING, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } return commands; } _resolveStreamingState(oldVMixState, newVMixState) { const commands = []; if (newVMixState.streaming != null && oldVMixState.streaming !== newVMixState.streaming) { if (newVMixState.streaming) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.START_STREAMING, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } else { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.STOP_STREAMING, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } return commands; } _resolveExternalState(oldVMixState, newVMixState) { const commands = []; if (newVMixState.external != null && oldVMixState.external !== newVMixState.external) { if (newVMixState.external) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.START_EXTERNAL, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } else { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.STOP_EXTERNAL, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } return commands; } _resolveOutputsState(oldVMixState, newVMixState) { const commands = []; for (const [name, output] of Object.entries({ ...newVMixState.outputs })) { const nameKey = name; const oldOutput = nameKey in oldVMixState.outputs ? oldVMixState.outputs[nameKey] : undefined; if (output != null && !_.isEqual(output, oldOutput)) { const value = output.source === 'Program' ? 'Output' : output.source; commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SET_OUPUT, value, input: output.input, name, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } } return commands; } _resolveScriptsState(oldVMixState, newVMixState) { const commands = []; _.map(newVMixState.runningScripts, (name) => { const alreadyRunning = oldVMixState.runningScripts.includes(name); if (!alreadyRunning) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SCRIPT_START, value: name, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } }); _.map(oldVMixState.runningScripts, (name) => { const noLongerDesired = !newVMixState.runningScripts.includes(name); if (noLongerDesired) { commands.push({ command: { command: timeline_state_resolver_types_1.VMixCommand.SCRIPT_STOP, value: name, }, context: vMixCommands_1.CommandContext.None, timelineId: '', }); } }); return commands; } /** * Checks if TSR thinks an input is currently in-use. * Not guaranteed to align with reality. */ _isInUse(state, input) { for (const mix of state.reportedState.mixes) { if (mix == null) continue; if (mix.program === input.number || mix.program === input.name) { // The input is in program in some mix, so stop the search and return true. return true; } if (typeof mix.program === 'undefined') continue; const pgmInput = state.reportedState.existingInputs[mix.program] ?? state.reportedState.inputsAddedByUs[mix.program]; if (!pgmInput || !pgmInput.layers) continue; for (const layer of Object.keys(pgmInput.layers)) { const layerInput = pgmInput.layers[layer]; if (layerInput.input === input.name || layerInput.input === input.number) { // Input is in program as a layer of a Multi View of something else that is in program, // so stop the search and return true. return true; } } } for (const overlay of state.reportedState.overlays) { if (overlay == null) continue; if (overlay.input === input.name || overlay.input === input.number) { // Input is in program as an overlay (DSK), // so stop the search and return true. return true; } } for (const output of Object.values({ ...state.outputs })) { if (output == null) continue; if (output.input === input.name || output.input === input.number) { // Input might not technically be in PGM, but it's being used by an output, // so stop the search and return true. return true; } } return false; } } exports.VMixStateDiffer = VMixStateDiffer; //# sourceMappingURL=vMixStateDiffer.js.map