diff --git a/src/index.html b/src/index.html index 552e72414..bfd3f278a 100644 --- a/src/index.html +++ b/src/index.html @@ -4,7 +4,10 @@ + + + Stremio - All you can watch! <%= htmlWebpackPlugin.tags.headTags %> diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index e08157426..b16334c49 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -18,7 +18,7 @@ const useSettings = require('./useSettings'); const styles = require('./styles'); const Player = ({ urlParams, queryParams }) => { - const { chromecast } = useServices(); + const { chromecast, shell } = useServices(); const [forceTranscoding, maxAudioChannels] = React.useMemo(() => { return [ queryParams.has('forceTranscoding'), @@ -258,6 +258,7 @@ const Player = ({ urlParams, queryParams }) => { } }, { chromecastTransport: chromecast.active ? chromecast.transport : null, + shellTransport: shell.active ? shell.transport : null, }); } }, [streamingServer.baseUrl, player.selected, player.metaItem, forceTranscoding, maxAudioChannels, casting]); diff --git a/src/services/Shell/Shell.js b/src/services/Shell/Shell.js index bb4d9051f..1e0c31943 100644 --- a/src/services/Shell/Shell.js +++ b/src/services/Shell/Shell.js @@ -1,14 +1,31 @@ // Copyright (C) 2017-2022 Smart code 203358507 const EventEmitter = require('eventemitter3'); +const ShellTransport = require('./ShellTransport'); function Shell() { let active = false; let error = null; let starting = false; + let transport = null; const events = new EventEmitter(); + function onTransportInit() { + active = true; + error = null; + starting = false; + onStateChanged(); + } + function onTransportInitError(err) { + console.error(err); + active = false; + error = new Error(err); + starting = false; + onStateChanged(); + transport = null; + } + function onStateChanged() { events.emit('stateChanged'); } @@ -34,6 +51,13 @@ function Shell() { get: function() { return starting; } + }, + transport: { + configurable: false, + enumerable: true, + get: function() { + return transport; + } } }); @@ -43,8 +67,10 @@ function Shell() { } active = false; - error = new Error('Stremio Shell API not available'); - starting = false; + starting = true; + transport = new ShellTransport(); + transport.on('init', onTransportInit); + transport.on('init-error', onTransportInitError); onStateChanged(); }; this.stop = function() { diff --git a/src/services/Shell/ShellTransport.js b/src/services/Shell/ShellTransport.js new file mode 100644 index 000000000..ff2ac845c --- /dev/null +++ b/src/services/Shell/ShellTransport.js @@ -0,0 +1,121 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +const EventEmitter = require('eventemitter3'); + +let shellAvailable = false; +const shellEvents = new EventEmitter(); + +const QtMsgTypes = { + signal: 1, + propertyUpdate: 2, + init: 3, + idle: 4, + debug: 5, + invokeMethod: 6, + connectToSignal: 7, + disconnectFromSignal: 8, + setProperty: 9, + response: 10, +}; +const QtObjId = 'transport'; // the ID of our transport object + +window.initShellComm = function () { + delete window.initShellComm; + shellEvents.emit('availabilityChanged'); +}; + +const initialize = () => { + if(!window.qt) return Promise.reject('Qt API not found'); + return new Promise((resolve) => { + function onShellAvailabilityChanged() { + shellEvents.off('availabilityChanged', onShellAvailabilityChanged); + shellAvailable = true; + resolve(); + } + if (shellAvailable) { + onShellAvailabilityChanged(); + } else { + shellEvents.on('availabilityChanged', onShellAvailabilityChanged); + } + }); +}; + +function ShellTransport() { + const events = new EventEmitter(); + + this.props = {}; + + const shell = this; + initialize() + .then(() => { + const transport = window.qt && window.qt.webChannelTransport; + if (!transport) throw 'no viable transport found (qt.webChannelTransport)'; + + let id = 0; + function send(msg) { + msg.id = id++; + transport.send(JSON.stringify(msg)); + } + + transport.onmessage = function (message) { + const msg = JSON.parse(message.data); + if (msg.id === 0) { + const obj = msg.data[QtObjId]; + + obj.properties.slice(1).forEach(function (prop) { + shell.props[prop[1]] = prop[3]; + }); + if (typeof shell.props.shellVersion === 'string') { + shell.shellVersionArr = ( + shell.props.shellVersion.match(/(\d+)\.(\d+)\.(\d+)/) || [] + ) + .slice(1, 4) + .map(Number); + } + events.emit('received-props', shell.props); + + obj.signals.forEach(function (sig) { + send({ + type: QtMsgTypes.connectToSignal, + object: QtObjId, + signal: sig[1], + }); + }); + + const onEvent = obj.methods.filter(function (x) { + return x[0] === 'onEvent'; + })[0]; + + shell.send = function (ev, args) { + send({ + type: QtMsgTypes.invokeMethod, + object: QtObjId, + method: onEvent[1], + args: [ev, args || {}], + }); + }; + + shell.send('app-ready', {}); // signal that we're ready to take events + } + + if (msg.object === QtObjId && msg.type === QtMsgTypes.signal) + events.emit(msg.args[0], msg.args[1]); + events.emit('init'); + }; + send({ type: QtMsgTypes.init }); + }) .catch((error) => { + events.emit('init-error', error); + }); + + this.on = function(name, listener) { + events.on(name, listener); + }; + this.off = function(name, listener) { + events.off(name, listener); + }; + this.removeAllListeners = function() { + events.removeAllListeners(); + }; +} + +module.exports = ShellTransport;