stremio-web/src/video/MPVVideo.js
2019-09-25 14:56:10 +03:00

436 lines
14 KiB
JavaScript

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;