mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
MPVVideo basic implementation
This commit is contained in:
parent
0055aad30c
commit
b08ee9aa8b
7 changed files with 482 additions and 18 deletions
|
|
@ -1,12 +1,12 @@
|
|||
import React, { StrictMode } from 'react';
|
||||
import { Router } from 'stremio-common';
|
||||
import routerConfig from './routerConfig';
|
||||
import styles from './styles';
|
||||
const React = require('react');
|
||||
const { Router } = require('stremio-common');
|
||||
const routerConfig = require('./routerConfig').default;
|
||||
const styles = require('./styles');
|
||||
|
||||
const App = () => (
|
||||
<StrictMode>
|
||||
<React.StrictMode>
|
||||
<Router className={styles['router']} config={routerConfig} />
|
||||
</StrictMode>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
export default App;
|
||||
module.exports = App;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
import App from './App';
|
||||
const App = require('./App');
|
||||
|
||||
export default App;
|
||||
module.exports = App;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<div id="app"></div>
|
||||
<script type="text/javascript" src="/stremio-web.js"></script>
|
||||
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||
<script type="text/javascript" src="qrc:///stremio-shell.js" onerror="window.runApp();"></script>
|
||||
<script type="text/javascript" src="qrc:///stremio-shell.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
27
src/index.js
27
src/index.js
|
|
@ -1,7 +1,26 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
const App = require('./App');
|
||||
|
||||
window.runApp = () => {
|
||||
const renderApp = () => {
|
||||
ReactDOM.render(<App />, document.getElementById('app'));
|
||||
};
|
||||
|
||||
if (window.qt) {
|
||||
window.shellOnLoad = () => {
|
||||
window.shell.dispatch('mpv', 'setOption', null, 'terminal', 'yes');
|
||||
window.shell.dispatch('mpv', 'setOption', null, 'msg-level', 'all=v');
|
||||
window.shell.dispatch('mpv', 'setProp', null, 'vo', 'opengl-cb');
|
||||
window.shell.dispatch('mpv', 'setProp', null, 'opengl-hwdec-interop', 'auto');
|
||||
window.shell.dispatch('mpv', 'setProp', null, 'cache-default', 15000);
|
||||
window.shell.dispatch('mpv', 'setProp', null, 'cache-backbuffer', 15000);
|
||||
window.shell.dispatch('mpv', 'setProp', null, 'cache-secs', 10);
|
||||
window.shell.dispatch('mpv', 'setProp', null, 'audio-client-name', 'Stremio');
|
||||
window.shell.dispatch('mpv', 'setProp', null, 'title', 'Stremio');
|
||||
window.shell.dispatch('mpv', 'setProp', null, 'audio-fallback-to-null', 'yes');
|
||||
window.shell.dispatch('mpv', 'setProp', null, 'sid', 'no');
|
||||
renderApp();
|
||||
};
|
||||
} else {
|
||||
renderApp();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ class Player extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatch('command', 'load', this.props.stream, {});
|
||||
this.dispatch('command', 'load', this.props.stream, {
|
||||
ipc: window.shell
|
||||
});
|
||||
this.dispatch('setProp', 'subtitleOffset', 18);
|
||||
this.dispatch('command', 'addSubtitleTracks', [{
|
||||
url: 'https://raw.githubusercontent.com/caitp/ng-media/master/example/assets/captions/bunny-en.vtt',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import hat from 'hat';
|
||||
import HTMLVideo from './stremio-video/HTMLVideo';
|
||||
import YouTubeVideo from './stremio-video/YouTubeVideo';
|
||||
import MPVVideo from './stremio-video/MPVVideo';
|
||||
|
||||
class Video extends Component {
|
||||
constructor(props) {
|
||||
|
|
@ -22,7 +23,9 @@ class Video extends Component {
|
|||
}
|
||||
|
||||
selectVideoImplementation = (stream, options) => {
|
||||
if (stream.ytId) {
|
||||
if (options.ipc) {
|
||||
return MPVVideo;
|
||||
} else if (stream.ytId) {
|
||||
return YouTubeVideo;
|
||||
} else {
|
||||
return HTMLVideo;
|
||||
|
|
@ -34,7 +37,11 @@ class Video extends Component {
|
|||
const Video = this.selectVideoImplementation(args[2], args[3]);
|
||||
if (this.video === null || this.video.constructor !== Video) {
|
||||
this.dispatch('command', 'destroy');
|
||||
this.video = new Video({ containerElement: this.containerRef.current });
|
||||
this.video = new Video({
|
||||
...args[3],
|
||||
id: this.id,
|
||||
containerElement: this.containerRef.current
|
||||
});
|
||||
this.video.on('ended', this.props.onEnded);
|
||||
this.video.on('error', this.props.onError);
|
||||
this.video.on('propValue', this.props.onPropValue);
|
||||
|
|
@ -54,7 +61,7 @@ class Video extends Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<div ref={this.containerRef} id={this.id} className={this.props.className} />
|
||||
<div ref={this.containerRef} className={this.props.className} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
436
src/routes/Player/Video/stremio-video/MPVVideo.js
Normal file
436
src/routes/Player/Video/stremio-video/MPVVideo.js
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
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) {
|
||||
ipc.dispatch('mpv', 'setProp', id, propName)
|
||||
.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;
|
||||
Loading…
Reference in a new issue