var EventEmitter = require('events'); var MPV_CRITICAL_ERROR_CODES = []; function MPVVideo(options) { var ipc = options && options.ipc; var id = options && options.id; if (!ipc) { throw new Error('ipc parameter is required'); } if (typeof id !== 'string') { throw new Error('id parameter is required'); } var ready = false; var loaded = false; var destroyed = false; var events = new EventEmitter(); var observedProps = {}; var dispatchArgsReadyQueue = []; events.on('error', function() { }); ipc.dispatch('mpv', 'createChannel', id) .then(function() { if (destroyed) { return; } ready = true; Promise.all([getProp('volume'), getProp('mute')]).then(function(values) { if (destroyed) { return; } onPropChanged('volume', values[0]); onPropChanged('mute', values[1]); }); ipc.on('mpvEvent', onMpvEvent); flushDispatchArgsQueue(dispatchArgsReadyQueue); }) .catch(function(error) { onChannelError(error); }); function mapPausedValue(pause) { if (!loaded || typeof pause !== 'boolean') { return null; } return pause; } function mapTimeValue(timePos) { if (!loaded || isNaN(timePos) || timePos === null) { return null; } return Math.round(timePos * 1000); } function mapDurationValue(duration) { if (!loaded || isNaN(duration) || duration === null) { return null; } return Math.round(duration * 1000); } function mapBufferingValue(seeking, pausedForCache) { if (!loaded || (seeking === null && pausedForCache === null)) { return null; } return !!seeking || !!pausedForCache; } function mapVolumeValue(volume) { if (!ready || destroyed || isNaN(volume) || volume === null) { return null; } return volume; } function mapMutedValue(mute) { if (!ready || destroyed || typeof mute !== 'boolean') { return null; } return mute; } function onError(error) { if (destroyed || !error) { return; } Object.freeze(error); events.emit('error', error); if (error.critical) { dispatch('command', 'stop'); } } function onEnded() { if (destroyed) { return; } events.emit('ended'); } function onPropChanged(propName, propValue) { switch (propName) { case 'pause': { if (observedProps['paused']) { events.emit('propChanged', 'paused', mapPausedValue(propValue)); } break; } case 'time-pos': { if (observedProps['time']) { events.emit('propChanged', 'time', mapTimeValue(propValue)); } break; } case 'duration': { if (observedProps['duration']) { events.emit('propChanged', 'duration', mapDurationValue(propValue)); } break; } case 'seeking': { if (observedProps['buffering']) { events.emit('propChanged', 'buffering', mapBufferingValue(propValue, null)); } break; } case 'paused-for-cache': { if (observedProps['buffering']) { events.emit('propChanged', 'buffering', mapBufferingValue(null, propValue)); } break; } case 'volume': { if (observedProps['volume']) { events.emit('propChanged', 'volume', mapVolumeValue(propValue)); } break; } case 'mute': { if (observedProps['muted']) { events.emit('propChanged', 'muted', mapMutedValue(propValue)); } break; } } } function onMpvEvent(data) { if (destroyed) { return; } if (!data || data.channelId !== id) { onChannelError(ipc.errors.mpv_channel_id_expired); return; } switch (data.eventName) { case 'error': { onError(Object.assign({}, data, { critical: MPV_CRITICAL_ERROR_CODES.indexOf(data.code) !== -1 })); break; } case 'ended': { onEnded(); break; } case 'propChanged': { onPropChanged(data.propName, data.propValue); break; } } } function onChannelError(error) { if (destroyed) { return; } onError(Object.assign({}, error, { critical: true })); dispatch('command', 'destroy'); } function observeProp(propName) { ipc.dispatch('mpv', 'observeProp', id, propName) .catch(function(error) { onChannelError(error); }); } function getProp(propName) { return ipc.dispatch('mpv', 'getProp', id, propName) .catch(function(error) { onChannelError(error); return null; }); } function setProp(propName, propValue) { ipc.dispatch('mpv', 'setProp', id, propName, propValue) .catch(function(error) { onChannelError(error); }); } function command() { ipc.dispatch.apply(null, ['mpv', 'command', id].concat(Array.from(arguments))) .catch(function(error) { onChannelError(error); }); } function flushDispatchArgsQueue(dispatchArgsQueue) { if (destroyed) { return; } while (dispatchArgsQueue.length > 0) { var args = dispatchArgsQueue.shift(); dispatch.apply(null, args); } } function on(eventName, listener) { if (destroyed) { throw new Error('Video is destroyed'); } events.on(eventName, listener); } function dispatch() { if (destroyed) { throw new Error('Video is destroyed'); } switch (arguments[0]) { case 'observeProp': { switch (arguments[1]) { case 'paused': { observeProp('pause'); getProp('pause').then(function(pause) { if (destroyed) { return; } observedProps['paused'] = true; events.emit('propValue', 'paused', mapPausedValue(pause)); }); return; } case 'time': { observeProp('time-pos'); getProp('time-pos').then(function(timePos) { if (destroyed) { return; } observedProps['time'] = true; events.emit('propValue', 'time', mapTimeValue(timePos)); }); return; } case 'duration': { observeProp('duration'); getProp('duration').then(function(duration) { if (destroyed) { return; } observedProps['duration'] = true; events.emit('propValue', 'duration', mapDurationValue(duration)); }); return; } case 'buffering': { observeProp('seeking'); observeProp('paused-for-cache'); Promise.all([getProp('seeking'), getProp('paused-for-cache')]).then(function(values) { if (destroyed) { return; } observedProps['buffering'] = true; events.emit('propValue', 'buffering', mapBufferingValue(values[0], values[1])); }); return; } case 'volume': { observeProp('volume'); getProp('volume').then(function(volume) { if (destroyed) { return; } observedProps['volume'] = true; events.emit('propValue', 'volume', mapVolumeValue(volume)); }); return; } case 'muted': { observeProp('mute'); getProp('mute').then(function(mute) { if (destroyed) { return; } observedProps['muted'] = true; events.emit('propValue', 'muted', mapMutedValue(mute)); }); return; } } } case 'setProp': { switch (arguments[1]) { case 'paused': { if (loaded) { setProp('pause', !!arguments[2]); } return; } case 'time': { if (loaded && !isNaN(arguments[2]) && arguments[2] !== null) { setProp('time-pos', arguments[2] / 1000); } return; } case 'volume': { if (ready) { if (!isNaN(arguments[2]) && arguments[2] !== null) { setProp('mute', false); setProp('volume', Math.max(0, Math.min(100, arguments[2]))); } } else { dispatchArgsReadyQueue.push(Array.from(arguments)); } return; } case 'muted': { if (ready) { setProp('mute', !!arguments[2]); } else { dispatchArgsReadyQueue.push(Array.from(arguments)); } return; } } } case 'command': { switch (arguments[1]) { case 'stop': { loaded = false; if (ready) { command('stop'); } onPropChanged('pause', null); onPropChanged('time-pos', null); onPropChanged('duration', null); onPropChanged('seeking', null); onPropChanged('paused-for-cache', null); return; } case 'load': { if (ready) { dispatch('command', 'stop'); loaded = true; var startTime = !isNaN(arguments[3].time) && arguments[3].time !== null ? Math.round(arguments[3].time / 1000) : 0; command('loadfile', arguments[2].url, 'replace', 'time-pos=' + startTime); setProp('pause', arguments[3].autoplay === false); Promise.all([ getProp('pause'), getProp('time-pos'), getProp('duration'), getProp('seeking'), getProp('paused-for-cache') ]).then(function(values) { if (destroyed) { return; } onPropChanged('pause', values[0]); onPropChanged('time-pos', values[1]); onPropChanged('duration', values[2]); onPropChanged('seeking', values[3]); onPropChanged('paused-for-cache', values[4]); }); } else { dispatchArgsReadyQueue.push(Array.from(arguments)); } return; } case 'destroy': { dispatch('command', 'stop'); destroyed = true; onPropChanged('volume', null); onPropChanged('mute', null); events.removeAllListeners(); events.on('error', function() { }); ipc.off('mpvEvent', onMpvEvent); dispatchArgsReadyQueue = []; return; } } } } throw new Error('Invalid dispatch call: ' + Array.from(arguments).map(String)); } this.on = on; this.dispatch = dispatch; Object.freeze(this); } MPVVideo.manifest = Object.freeze({ name: 'MPVVideo', embedded: true, props: Object.freeze(['paused', 'time', 'duration', 'volume', 'muted', 'buffering']) }); Object.freeze(MPVVideo); module.exports = MPVVideo;