timeline-state-resolver
Version:
Have timeline, control stuff
340 lines • 14.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VMixDevice = void 0;
const _ = require("underscore");
const device_1 = require("./../../devices/device");
const doOnTime_1 = require("../../devices/doOnTime");
const connection_1 = require("./connection");
const timeline_state_resolver_types_1 = require("timeline-state-resolver-types");
const vMixStateDiffer_1 = require("./vMixStateDiffer");
const vMixTimelineStateConverter_1 = require("./vMixTimelineStateConverter");
const vMixXmlStateParser_1 = require("./vMixXmlStateParser");
const vMixPollingTimer_1 = require("./vMixPollingTimer");
const vMixStateSynchronizer_1 = require("./vMixStateSynchronizer");
const lib_1 = require("../../lib");
/**
* Default time, in milliseconds, for when we should poll vMix to query its actual state.
*/
const DEFAULT_VMIX_POLL_INTERVAL = 10 * 1000;
/**
* How long to wait, in milliseconds, to poll vMix's state after we send commands to it.
*/
const BACKOFF_VMIX_POLL_INTERVAL = 5 * 1000;
/**
* This is a VMixDevice, it sends commands when it feels like it
*/
class VMixDevice extends device_1.DeviceWithState {
constructor(deviceId, deviceOptions, getCurrentTime) {
super(deviceId, deviceOptions, getCurrentTime);
this._commandReceiver = this._defaultCommandReceiver.bind(this);
this._connected = false;
this._initialized = false;
this._expectingStateAfterConnecting = false;
this._expectingPolledState = false;
this._pollingTimer = null;
if (deviceOptions.options) {
if (deviceOptions.commandReceiver)
this._commandReceiver = deviceOptions.commandReceiver;
else
this._commandReceiver = this._defaultCommandReceiver.bind(this);
}
this._doOnTime = new doOnTime_1.DoOnTime(() => {
return this.getCurrentTime();
}, doOnTime_1.SendMode.IN_ORDER, this._deviceOptions);
this._doOnTime.on('error', (e) => this.emit('error', 'VMix.doOnTime', e));
this._doOnTime.on('slowCommand', (msg) => this.emit('slowCommand', this.deviceName + ': ' + msg));
this._doOnTime.on('slowSentCommand', (info) => this.emit('slowSentCommand', info));
this._doOnTime.on('slowFulfilledCommand', (info) => this.emit('slowFulfilledCommand', info));
this._stateDiffer = new vMixStateDiffer_1.VMixStateDiffer(this.getCurrentTime.bind(this), (commands) => this.addToQueue(commands, this.getCurrentTime()));
this._timelineStateConverter = new vMixTimelineStateConverter_1.VMixTimelineStateConverter(() => this._stateDiffer.getDefaultState(), (inputNumber) => this._stateDiffer.getDefaultInputState(inputNumber), (inputNumber) => this._stateDiffer.getDefaultInputAudioState(inputNumber));
this._xmlStateParser = new vMixXmlStateParser_1.VMixXmlStateParser();
this._stateSynchronizer = new vMixStateSynchronizer_1.VMixStateSynchronizer();
}
async init(options) {
this._vMixConnection = new connection_1.VMixConnection(options.host, options.port, false);
this._vMixCommandSender = new connection_1.VMixCommandSender(this._vMixConnection);
this._vMixConnection.on('connected', () => {
// We are not resetting the state at this point and waiting for the state to arrive. Otherwise, we risk
// going back and forth on reconnections
this._setConnected(true);
this._expectingStateAfterConnecting = true;
this.emitDebug('connected');
this._pollingTimer?.start();
this._requestVMixState('VMix init');
});
this._vMixConnection.on('disconnected', () => {
this._setConnected(false);
this._pollingTimer?.stop();
this.emitDebug('disconnected');
});
this._vMixConnection.on('error', (e) => this.emit('error', 'VMix', e));
this._vMixConnection.on('data', (data) => this._onDataReceived(data));
// this._vmix.on('debug', (...args) => this.emitDebug(...args))
this._vMixConnection.connect();
const pollTime = typeof options.pollInterval === 'number' && options.pollInterval >= 0 // options.pollInterval === 0 disables the polling
? options.pollInterval
: DEFAULT_VMIX_POLL_INTERVAL;
if (pollTime) {
this._pollingTimer = new vMixPollingTimer_1.VMixPollingTimer(pollTime);
this._pollingTimer.on('tick', () => {
this._expectingPolledState = true;
this._requestVMixState('VMix poll');
});
}
return true;
}
_onDataReceived(data) {
if (data.message !== 'Completed')
this.emitDebug(data);
if (data.command === 'XML' && data.body) {
if (!this._initialized) {
this._initialized = true;
this.emit('connectionChanged', this.getStatus());
}
const realState = this._xmlStateParser.parseVMixState(data.body);
if (this._expectingStateAfterConnecting) {
this._setFullState(realState);
this._expectingStateAfterConnecting = false;
// resync all tl states
this.clearStates();
this.emit('resyncStates');
}
else if (this._expectingPolledState) {
this._setPartialInputState(realState);
this._expectingPolledState = false;
}
}
}
_connectionChanged() {
this.emit('connectionChanged', this.getStatus());
}
_setConnected(connected) {
if (this._connected !== connected) {
this._connected = connected;
this._connectionChanged();
}
}
/**
* Updates the entire state when we (re)connect
* @param realState State as reported by vMix itself.
*/
_setFullState(realState) {
const time = this.getCurrentTime();
const oldState = (this.getStateBefore(time) ?? { state: this._stateDiffer.getDefaultState() })
.state;
oldState.reportedState = realState;
this.setState(oldState, time);
this.emit('resetResolver');
}
/**
* Runs when we receive XML state from vMix,
* generally as the result a poll (if polling/enforcement is enabled).
* @param realState State as reported by vMix itself.
*/
_setPartialInputState(realState) {
const time = this.getCurrentTime();
let expectedState = (this.getStateBefore(time) ?? { state: this._stateDiffer.getDefaultState() })
.state;
expectedState = this._stateSynchronizer.applyRealState(expectedState, realState);
this.setState(expectedState, time);
this.emit('resetResolver');
}
/** Called by the Conductor a bit before a .handleState is called */
prepareForHandleState(newStateTime) {
// clear any queued commands later than this time:
this._doOnTime.clearQueueNowAndAfter(newStateTime);
this.cleanUpStates(0, newStateTime);
}
handleState(newState, newMappings) {
super.onHandleState(newState, newMappings);
if (!this._initialized) {
// before it's initialized don't do anything
this.emit('warning', 'VMix not initialized yet');
return;
}
const previousStateTime = Math.max(this.getCurrentTime(), newState.time);
const oldState = (this.getStateBefore(previousStateTime) ?? { state: this._stateDiffer.getDefaultState() }).state;
const newVMixState = this._timelineStateConverter.getVMixStateFromTimelineState(newState, newMappings // is this safe? why is the TriCaster integration filtering?
);
const commandsToAchieveState = this._stateDiffer.getCommandsToAchieveState(newState.time, oldState, newVMixState);
// clear any queued commands later than this time:
this._doOnTime.clearQueueNowAndAfter(previousStateTime);
// add the new commands to the queue:
this.addToQueue(commandsToAchieveState, newState.time);
// store the new state, for later use:
this.setState(newVMixState, newState.time);
this.emitDebugState(newVMixState);
}
clearFuture(clearAfterTime) {
// Clear any scheduled commands after this time
this._doOnTime.clearQueueAfter(clearAfterTime);
}
async terminate() {
this._doOnTime.dispose();
this._vMixConnection.removeAllListeners();
this._vMixConnection.disconnect();
this._pollingTimer?.stop();
}
getStatus() {
let statusCode = device_1.StatusCode.GOOD;
const messages = [];
if (!this._connected) {
statusCode = device_1.StatusCode.BAD;
messages.push('Not connected');
}
else if (!this._initialized) {
statusCode = device_1.StatusCode.BAD;
messages.push('Not initialized');
}
return {
statusCode: statusCode,
messages: messages,
active: this.isActive,
};
}
async makeReady(okToDestroyStuff) {
if (okToDestroyStuff) {
// do something?
}
}
async executeAction(actionId, payload) {
switch (actionId) {
case timeline_state_resolver_types_1.VmixActions.LastPreset:
return this._lastPreset();
case timeline_state_resolver_types_1.VmixActions.OpenPreset:
return this._openPreset(payload);
case timeline_state_resolver_types_1.VmixActions.SavePreset:
return this._savePreset(payload);
case timeline_state_resolver_types_1.VmixActions.StartExternal:
return this._startExternalOutput();
case timeline_state_resolver_types_1.VmixActions.StopExternal:
return this._stopExternalOutput();
default:
return (0, lib_1.actionNotFoundMessage)(actionId);
}
}
_checkPresetAction(payload, payloadRequired) {
const connectionError = this._checkConnectionForAction();
if (connectionError)
return connectionError;
if (payloadRequired) {
if (!payload || typeof payload !== 'object') {
return {
result: timeline_state_resolver_types_1.ActionExecutionResultCode.Error,
response: (0, lib_1.t)('Action payload is invalid'),
};
}
if (!payload.filename) {
return {
result: timeline_state_resolver_types_1.ActionExecutionResultCode.Error,
response: (0, lib_1.t)('No preset filename specified'),
};
}
}
return;
}
async _lastPreset() {
const presetActionCheckResult = this._checkPresetAction();
if (presetActionCheckResult)
return presetActionCheckResult;
await this._vMixCommandSender.lastPreset();
return {
result: timeline_state_resolver_types_1.ActionExecutionResultCode.Ok,
};
}
async _openPreset(payload) {
const presetActionCheckResult = this._checkPresetAction(payload, true);
if (presetActionCheckResult)
return presetActionCheckResult;
await this._vMixCommandSender.openPreset(payload.filename);
return {
result: timeline_state_resolver_types_1.ActionExecutionResultCode.Ok,
};
}
async _savePreset(payload) {
const presetActionCheckResult = this._checkPresetAction(payload, true);
if (presetActionCheckResult)
return presetActionCheckResult;
await this._vMixCommandSender.savePreset(payload.filename);
return {
result: timeline_state_resolver_types_1.ActionExecutionResultCode.Ok,
};
}
async _startExternalOutput() {
const connectionError = this._checkConnectionForAction();
if (connectionError)
return connectionError;
await this._vMixCommandSender.startExternal();
return {
result: timeline_state_resolver_types_1.ActionExecutionResultCode.Ok,
};
}
async _stopExternalOutput() {
const connectionError = this._checkConnectionForAction();
if (connectionError)
return connectionError;
await this._vMixCommandSender.stopExternal();
return {
result: timeline_state_resolver_types_1.ActionExecutionResultCode.Ok,
};
}
_checkConnectionForAction() {
if (!this._vMixConnection.connected) {
return {
result: timeline_state_resolver_types_1.ActionExecutionResultCode.Error,
response: (0, lib_1.t)('Cannot perform VMix action without a connection'),
};
}
return undefined;
}
get canConnect() {
return false;
}
get connected() {
return false;
}
get deviceType() {
return timeline_state_resolver_types_1.DeviceType.VMIX;
}
get deviceName() {
return 'VMix ' + this.deviceId;
}
get queue() {
return this._doOnTime.getQueue();
}
addToQueue(commandsToAchieveState, time) {
_.each(commandsToAchieveState, (cmd) => {
// add the new commands to the queue:
this._doOnTime.queue(time, undefined, async (cmd) => {
return this._commandReceiver(time, cmd, cmd.context, cmd.timelineId);
}, cmd);
});
}
async _defaultCommandReceiver(_time, cmd, context, timelineObjId) {
// Do not poll or retry while we are sending commands, instead always do it closely after.
// This is potentially an issue while producing a show, because it is theoretically possible
// that the operator keeps performing actions/takes within 5 seconds of one another and
// therefore this timeout keeps getting reset and never expires.
// For now, we classify this as an extreme outlier edge case and acknowledge that this system
// does not support it.
this._expectingPolledState = false;
this._pollingTimer?.postponeNextTick(BACKOFF_VMIX_POLL_INTERVAL);
const cwc = {
context: context,
command: cmd,
timelineObjId: timelineObjId,
};
this.emitDebug(cwc);
return this._vMixCommandSender.sendCommand(cmd.command).catch((error) => {
this.emit('commandError', error, cwc);
});
}
/**
* Request vMix's XML status.
*/
_requestVMixState(context) {
this._vMixConnection.requestVMixState().catch((e) => this.emit('error', context, e));
}
}
exports.VMixDevice = VMixDevice;
//# sourceMappingURL=index.js.map