mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-14 11:36:05 +00:00
436 lines
14 KiB
JavaScript
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;
|