From ec7bb07c124c8248e29c84596ff81dffac34c712 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Wed, 22 Apr 2020 12:02:40 +0300 Subject: [PATCH 01/72] handle loading args in queue --- src/video/withStreamingServer.js | 185 +++++++++++++++++++------------ 1 file changed, 113 insertions(+), 72 deletions(-) diff --git a/src/video/withStreamingServer.js b/src/video/withStreamingServer.js index 491ac5421..ae9a85403 100644 --- a/src/video/withStreamingServer.js +++ b/src/video/withStreamingServer.js @@ -7,8 +7,11 @@ function withStreamingServer(Video) { function StreamingServerVideo(options) { var video = new Video(options); var events = new EventEmitter(); + var destroyed = false; + var loaded = false; var stream = null; + var dispatchArgsLoadingQueue = []; events.on('error', function() { }); @@ -20,10 +23,15 @@ function withStreamingServer(Video) { Object.freeze(error); events.emit('error', error); if (error.critical) { - video.dispatch({ commandName: 'stop' }); + dispatch({ commandName: 'stop' }); + } + } + function flushDispatchArgsQueue(dispatchArgsQueue) { + while (dispatchArgsQueue.length > 0) { + var args = dispatchArgsQueue.shift(); + dispatch(args); } } - function on(eventName, listener) { if (!destroyed) { events.on(eventName, listener); @@ -32,88 +40,121 @@ function withStreamingServer(Video) { video.on(eventName, listener); } function dispatch(args) { - if (!destroyed && args && args.commandName === 'load') { - stream = null; - video.dispatch({ commandName: 'stop' }); - if (args.commandArgs && args.commandArgs.stream && typeof args.commandArgs.stream.infoHash === 'string' && typeof args.commandArgs.streamingServerUrl === 'string') { - stream = args.commandArgs.stream; - if (stream.fileIdx !== null && !isNaN(stream.fileIdx)) { - video.dispatch({ - commandName: 'load', - commandArgs: { - autoplay: args.commandArgs.autoplay, - time: args.commandArgs.time, - stream: { - url: UrlUtils.resolve(args.commandArgs.streamingServerUrl, stream.infoHash + '/' + String(stream.fileIdx)) - } - } - }); - } else { - fetch(UrlUtils.resolve(args.commandArgs.streamingServerUrl, stream.infoHash + '/create'), { - method: 'POST', - headers: { - 'content-type': 'application/json' - }, - body: JSON.stringify({ - torrent: { - infoHash: stream.infoHash - } - }) - }).then(function(resp) { - return resp.json(); - }).then(function(resp) { - if (stream !== args.commandArgs.stream) { + if (!destroyed && args) { + if (typeof args.commandName === 'string') { + switch (args.commandName) { + case 'addSubtitlesTracks': { + if (!loaded && stream !== null) { + dispatchArgsLoadingQueue.push(args); return; } - if (!Array.isArray(resp.files) || resp.files.length === 0) { - onError({ - message: 'Unable to get files from torrent', - critical: true - }); - return; - } + break; + } + case 'stop': { + loaded = false; + stream = null; + dispatchArgsLoadingQueue = []; + break; + } + case 'load': { + dispatch({ commandName: 'stop' }); + if (args.commandArgs && typeof args.commandArgs.streamingServerUrl === 'string' && args.commandArgs.stream) { + if (typeof args.commandArgs.stream.infoHash === 'string') { + stream = args.commandArgs.stream; + if (stream.fileIdx !== null && !isNaN(stream.fileIdx)) { + video.dispatch({ + commandName: 'load', + commandArgs: { + autoplay: args.commandArgs.autoplay, + time: args.commandArgs.time, + stream: { + url: UrlUtils.resolve(args.commandArgs.streamingServerUrl, stream.infoHash + '/' + String(stream.fileIdx)) + } + } + }); + loaded = true; + } else { + fetch(UrlUtils.resolve(args.commandArgs.streamingServerUrl, stream.infoHash + '/create'), { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + torrent: { + infoHash: stream.infoHash + } + }) + }).then(function(resp) { + return resp.json(); + }).then(function(resp) { + if (stream !== args.commandArgs.stream) { + return; + } - var fileIdx = resp.files.reduce((fileIdx, _, index, files) => { - if (files[index].length > files[fileIdx].length) { - return index; - } + if (!Array.isArray(resp.files) || resp.files.length === 0) { + onError({ + message: 'Unable to get files from torrent', + critical: true + }); + return; + } - return fileIdx; - }, 0); - video.dispatch({ - commandName: 'load', - commandArgs: { - autoplay: args.commandArgs.autoplay, - time: args.commandArgs.time, - stream: { - url: UrlUtils.resolve(args.commandArgs.streamingServerUrl, stream.infoHash + '/' + String(fileIdx)) + var fileIdx = resp.files.reduce((fileIdx, _, index, files) => { + if (files[index].length > files[fileIdx].length) { + return index; + } + + return fileIdx; + }, 0); + video.dispatch({ + commandName: 'load', + commandArgs: { + autoplay: args.commandArgs.autoplay, + time: args.commandArgs.time, + stream: { + url: UrlUtils.resolve(args.commandArgs.streamingServerUrl, stream.infoHash + '/' + String(fileIdx)) + } + } + }); + loaded = true; + flushDispatchArgsQueue(dispatchArgsLoadingQueue); + }).catch(function(error) { + if (stream !== args.commandArgs.stream) { + return; + } + + onError({ + message: 'Unable to get files from torrent', + critical: true, + error: error + }); + }); } + + return; } - }); - }).catch(function(error) { - if (stream !== args.commandArgs.stream) { - return; } - onError({ - message: 'Unable to get files from torrent', - critical: true, - error: error - }); - }); + break; + } + case 'destroy': { + dispatch({ commandName: 'stop' }); + destroyed = true; + events.removeAllListeners(); + events.on('error', function() { }); + break; + } + } + } else if (typeof args.propName === 'string') { + if (!loaded && stream !== null && ['paused', 'time', 'selectedSubtitlesTrackId', 'subtitlesDelay'].indexOf(args.propName) !== -1) { + dispatchArgsLoadingQueue.push(args); + return; } } - } else { - if (args && args.commandName === 'destroy') { - destroyed = true; - stream = null; - events.removeAllListeners(); - events.on('error', function() { }); - } - - video.dispatch(args); } + + video.dispatch(args); } this.on = on; From 146a80d310b5a255b4d4a06190a59e5a38dde5f2 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Thu, 23 Apr 2020 15:02:09 +0300 Subject: [PATCH 02/72] integrate video-name-parser and extract pure functions from middleware impl --- src/video/withStreamingServer.js | 251 ++++++++++++++++--------------- 1 file changed, 132 insertions(+), 119 deletions(-) diff --git a/src/video/withStreamingServer.js b/src/video/withStreamingServer.js index ae9a85403..95739517b 100644 --- a/src/video/withStreamingServer.js +++ b/src/video/withStreamingServer.js @@ -2,6 +2,66 @@ var UrlUtils = require('url'); var EventEmitter = require('events'); +var parseVideoName = require('video-name-parser'); + +var VIDEO_FILE_EXTENTIONS = /.mkv$|.avi$|.mp4$|.wmv$|.vp8$|.mov$|.mpg$|.ts$|.webm$/i; + +function createTorrent(streamingServerUrl, infoHash, sources) { + return fetch(UrlUtils.resolve(streamingServerUrl, `/${encodeURIComponent(infoHash)}/create`), { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + torrent: { + infoHash, + peerSearch: { + sources: [`dht:${infoHash}`].concat(Array.isArray(sources) ? sources : []), + min: 40, + max: 150 + } + } + }) + }).then(function(resp) { + return resp.json(); + }).catch(function(error) { + throw { + message: 'Unable to get files from torrent', + critical: true, + error: error + }; + }).then(function(resp) { + if (!resp || !Array.isArray(resp.files) || resp.files.length === 0) { + throw { + message: 'Unable to get files from torrent', + critical: true + }; + } + + return resp; + }); +} + +function guessFileIdx(files, seriesInfo) { + var videoFilesForEpisode = files.filter(function(file) { + if (seriesInfo && file.path.match(VIDEO_FILE_EXTENTIONS)) { + try { + var info = parseVideoName(file.path); + return !isNaN(info.season) && Array.isArray(info.episode) && + info.season === seriesInfo.season && info.episode.indexOf(seriesInfo.episode) !== -1; + } catch (e) { + return false; + } + } + }); + var largestFile = (videoFilesForEpisode.length > 0 ? videoFilesForEpisode : files) + .reduce((result, file) => { + if (!result || file.length > result.length) { + return file; + } + + return result; + }, null); + return files.indexOf(largestFile); +} function withStreamingServer(Video) { function StreamingServerVideo(options) { @@ -9,156 +69,109 @@ function withStreamingServer(Video) { var events = new EventEmitter(); var destroyed = false; - var loaded = false; var stream = null; - var dispatchArgsLoadingQueue = []; events.on('error', function() { }); function onError(error) { - if (!error) { - return; - } - - Object.freeze(error); events.emit('error', error); if (error.critical) { - dispatch({ commandName: 'stop' }); + stop(); + video.dispatch({ commandName: 'stop' }); } } - function flushDispatchArgsQueue(dispatchArgsQueue) { - while (dispatchArgsQueue.length > 0) { - var args = dispatchArgsQueue.shift(); - dispatch(args); - } + function stop() { + stream = null; } - function on(eventName, listener) { + function load(args) { + video.dispatch({ commandName: 'stop' }); + stream = args.stream; + new Promise(function(resolve, reject) { + if (typeof args.stream.ytId === 'string') { + resolve(UrlUtils.resolve(args.streamingServerUrl, `/yt/${encodeURIComponent(args.stream.ytId)}?${new URLSearchParams([['request', Date.now()]])}`)); + return; + } + + if (typeof args.stream.infoHash === 'string') { + if (args.stream.fileIdx !== null && !isNaN(args.stream.fileIdx)) { + resolve(UrlUtils.resolve(args.streamingServerUrl, `/${args.stream.infoHash}/${args.stream.fileIdx}`)); + } else { + createTorrent(args.streamingServerUrl, args.stream.infoHash, args.stream.sources) + .then(function(resp) { + var fileIdx = guessFileIdx(resp.files, args.stream.seriesInfo); + resolve(UrlUtils.resolve(args.streamingServerUrl, `/${args.stream.infoHash}/${fileIdx}`)); + }) + .catch(function(error) { + reject(error); + }); + } + return; + } + + reject({ + message: 'Unable to play stream', + critical: true, + stream: args.stream + }); + }).then(function(url) { + if (destroyed || args.stream !== stream) { + return; + } + + video.dispatch({ + commandName: 'load', + commandArgs: { + autoplay: args.autoplay, + time: args.time, + stream: { + url: url + } + } + }); + }).catch(function(error) { + if (destroyed || args.stream !== stream) { + return; + } + + onError(error); + }); + } + function destroy() { + stop(); + destroyed = true; + events.removeAllListeners(); + events.on('error', function() { }); + } + + this.on = function(eventName, listener) { if (!destroyed) { events.on(eventName, listener); } video.on(eventName, listener); - } - function dispatch(args) { + }; + this.dispatch = function(args) { if (!destroyed && args) { if (typeof args.commandName === 'string') { switch (args.commandName) { - case 'addSubtitlesTracks': { - if (!loaded && stream !== null) { - dispatchArgsLoadingQueue.push(args); - return; - } - - break; - } case 'stop': { - loaded = false; - stream = null; - dispatchArgsLoadingQueue = []; + stop(); break; } case 'load': { - dispatch({ commandName: 'stop' }); - if (args.commandArgs && typeof args.commandArgs.streamingServerUrl === 'string' && args.commandArgs.stream) { - if (typeof args.commandArgs.stream.infoHash === 'string') { - stream = args.commandArgs.stream; - if (stream.fileIdx !== null && !isNaN(stream.fileIdx)) { - video.dispatch({ - commandName: 'load', - commandArgs: { - autoplay: args.commandArgs.autoplay, - time: args.commandArgs.time, - stream: { - url: UrlUtils.resolve(args.commandArgs.streamingServerUrl, stream.infoHash + '/' + String(stream.fileIdx)) - } - } - }); - loaded = true; - } else { - fetch(UrlUtils.resolve(args.commandArgs.streamingServerUrl, stream.infoHash + '/create'), { - method: 'POST', - headers: { - 'content-type': 'application/json' - }, - body: JSON.stringify({ - torrent: { - infoHash: stream.infoHash - } - }) - }).then(function(resp) { - return resp.json(); - }).then(function(resp) { - if (stream !== args.commandArgs.stream) { - return; - } - - if (!Array.isArray(resp.files) || resp.files.length === 0) { - onError({ - message: 'Unable to get files from torrent', - critical: true - }); - return; - } - - var fileIdx = resp.files.reduce((fileIdx, _, index, files) => { - if (files[index].length > files[fileIdx].length) { - return index; - } - - return fileIdx; - }, 0); - video.dispatch({ - commandName: 'load', - commandArgs: { - autoplay: args.commandArgs.autoplay, - time: args.commandArgs.time, - stream: { - url: UrlUtils.resolve(args.commandArgs.streamingServerUrl, stream.infoHash + '/' + String(fileIdx)) - } - } - }); - loaded = true; - flushDispatchArgsQueue(dispatchArgsLoadingQueue); - }).catch(function(error) { - if (stream !== args.commandArgs.stream) { - return; - } - - onError({ - message: 'Unable to get files from torrent', - critical: true, - error: error - }); - }); - } - - return; - } - } - - break; + load(args.commandArgs); + return; } case 'destroy': { - dispatch({ commandName: 'stop' }); - destroyed = true; - events.removeAllListeners(); - events.on('error', function() { }); + destroy(); break; } } - } else if (typeof args.propName === 'string') { - if (!loaded && stream !== null && ['paused', 'time', 'selectedSubtitlesTrackId', 'subtitlesDelay'].indexOf(args.propName) !== -1) { - dispatchArgsLoadingQueue.push(args); - return; - } } } video.dispatch(args); - } - - this.on = on; - this.dispatch = dispatch; + }; Object.freeze(this); } From 5bf74786c6fb2b713d794b24fb5a165d60f8a117 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Thu, 30 Apr 2020 11:39:17 +0300 Subject: [PATCH 03/72] video directory removed --- src/video/HTMLSubtitles.js | 314 ------------- src/video/HTMLVideo.js | 460 ------------------- src/video/MPVVideo.js | 438 ------------------ src/video/README.MD | 3 - src/video/YouTubeVideo.js | 685 ---------------------------- src/video/binarySearchUpperBound.js | 26 -- src/video/colorConverter.js | 18 - src/video/index.js | 13 - src/video/subtitlesParser.js | 61 --- src/video/subtitlesRenderer.js | 22 - src/video/withStreamingServer.js | 190 -------- webpack.config.js | 3 +- 12 files changed, 1 insertion(+), 2232 deletions(-) delete mode 100644 src/video/HTMLSubtitles.js delete mode 100644 src/video/HTMLVideo.js delete mode 100644 src/video/MPVVideo.js delete mode 100644 src/video/README.MD delete mode 100644 src/video/YouTubeVideo.js delete mode 100644 src/video/binarySearchUpperBound.js delete mode 100644 src/video/colorConverter.js delete mode 100644 src/video/index.js delete mode 100644 src/video/subtitlesParser.js delete mode 100644 src/video/subtitlesRenderer.js delete mode 100644 src/video/withStreamingServer.js diff --git a/src/video/HTMLSubtitles.js b/src/video/HTMLSubtitles.js deleted file mode 100644 index 0132f8eef..000000000 --- a/src/video/HTMLSubtitles.js +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright (C) 2017-2020 Smart code 203358507 - -var EventEmitter = require('events'); -var subtitlesParser = require('./subtitlesParser'); -var subtitlesRenderer = require('./subtitlesRenderer'); -var colorConverter = require('./colorConverter'); - -var COLOR_REGEX = /^#[A-Fa-f0-9]{8}$/; -var ERROR_CODE = Object.freeze({ - FETCH_FAILED: 70, - PARSE_FAILED: 71 -}); -var SIZE_COEF = 25; - -function HTMLSubtitles(options) { - var containerElement = options && options.containerElement; - if (!(containerElement instanceof HTMLElement) || !containerElement.hasAttribute('id')) { - throw new Error('Instance of HTMLElement with id attribute required'); - } - if (!document.body.contains(containerElement)) { - throw new Error('Container element not attached to body'); - } - - var destroyed = false; - var events = new EventEmitter(); - var cuesByTime = null; - var tracks = Object.freeze([]); - var selectedTrackId = null; - var delay = null; - var stylesElement = document.createElement('style'); - var subtitlesElement = document.createElement('div'); - - events.on('error', function() { }); - containerElement.appendChild(stylesElement); - var subtitlesContainerStylesIndex = stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles { position: absolute; right: 0; bottom: 0; left: 0; z-index: 0; text-align: center; }', stylesElement.sheet.cssRules.length); - var subtitlesCueStylesIndex = stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles .cue { display: inline-block; padding: 0.2em; text-shadow: 0 0 0.03em #222222ff, 0 0 0.03em #222222ff, 0 0 0.03em #222222ff, 0 0 0.03em #222222ff, 0 0 0.03em #222222ff; background-color: #00000000; color: #ffffffff; font-size: 4vmin; }', stylesElement.sheet.cssRules.length); - stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles .cue * { font-size: inherit; }', stylesElement.sheet.cssRules.length); - containerElement.appendChild(subtitlesElement); - subtitlesElement.classList.add('subtitles'); - - function on(eventName, listener) { - if (destroyed) { - return; - } - - events.on(eventName, listener); - } - function addTracks(extraTracks) { - if (destroyed || !Array.isArray(extraTracks)) { - return; - } - - tracks = extraTracks - .filter(function(track) { - return track && - typeof track.url === 'string' && - track.url.length > 0 && - typeof track.origin === 'string' && - track.origin.length > 0 && - track.origin !== 'EMBEDDED IN VIDEO'; - }) - .map(function(track) { - return Object.freeze(Object.assign({}, track, { - id: track.url - })); - }) - .concat(tracks) - .filter(function(track, index, tracks) { - for (var i = 0; i < tracks.length; i++) { - if (tracks[i].id === track.id) { - return i === index; - } - } - - return false; - }); - Object.freeze(tracks); - events.emit('propChanged', 'tracks'); - } - function updateText(mediaTime) { - while (subtitlesElement.hasChildNodes()) { - subtitlesElement.removeChild(subtitlesElement.lastChild); - } - - if (cuesByTime === null || isNaN(mediaTime) || mediaTime === null) { - return; - } - - var time = mediaTime + delay; - subtitlesRenderer.render(cuesByTime, time) - .forEach(function(cueNode) { - cueNode.classList.add('cue'); - subtitlesElement.append(cueNode, document.createElement('br')); - }); - } - function clearTracks() { - updateText(NaN); - cuesByTime = null; - tracks = Object.freeze([]); - selectedTrackId = null; - delay = null; - events.emit('propChanged', 'tracks'); - events.emit('propChanged', 'selectedTrackId'); - events.emit('propChanged', 'delay'); - } - function destroy() { - destroyed = true; - clearTracks(); - events.emit('propChanged', 'size'); - events.emit('propChanged', 'textColor'); - events.emit('propChanged', 'backgroundColor'); - events.emit('propChanged', 'outlineColor'); - events.emit('propChanged', 'offset'); - events.removeAllListeners(); - events.on('error', function() { }); - containerElement.removeChild(stylesElement); - containerElement.removeChild(subtitlesElement); - } - - Object.defineProperties(this, { - tracks: { - configurable: false, - enumerable: true, - get: function() { - return Object.freeze(tracks.slice()); - } - }, - selectedTrackId: { - configurable: false, - enumerable: true, - get: function() { - return selectedTrackId; - }, - set: function(value) { - if (destroyed) { - return; - } - - cuesByTime = null; - selectedTrackId = null; - delay = null; - updateText(NaN); - var selecterdTrack = tracks.find(function(track) { - return track.id === value; - }); - if (selecterdTrack) { - selectedTrackId = selecterdTrack.id; - delay = 0; - fetch(selecterdTrack.url) - .then(function(resp) { - return resp.text(); - }) - .catch(function(error) { - events.emit('error', Object.freeze({ - code: ERROR_CODE.FETCH_FAILED, - message: 'Failed to fetch subtitles from ' + selecterdTrack.origin, - track: selecterdTrack, - error: error - })); - }) - .then(function(text) { - if (typeof text === 'string' && selectedTrackId === selecterdTrack.id) { - cuesByTime = subtitlesParser.parse(text); - if (cuesByTime.times.length === 0) { - throw new Error('parse failed'); - } - - events.emit('trackLoaded', selecterdTrack); - } - }) - .catch(function(error) { - events.emit('error', Object.freeze({ - code: ERROR_CODE.PARSE_FAILED, - message: 'Failed to parse subtitles from ' + selecterdTrack.origin, - track: selecterdTrack, - error: error - })); - }); - } - - events.emit('propChanged', 'selectedTrackId'); - events.emit('propChanged', 'delay'); - } - }, - delay: { - configurable: false, - enumerable: true, - get: function() { - return delay; - }, - set: function(value) { - if (destroyed || isNaN(value) || value === null || selectedTrackId === null) { - return; - } - - delay = parseInt(value); - updateText(NaN); - events.emit('propChanged', 'delay'); - } - }, - size: { - configurable: false, - enumerable: true, - get: function() { - if (destroyed) { - return null; - } - - return parseInt(stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.fontSize) * SIZE_COEF - }, - set: function(value) { - if (destroyed || isNaN(value) || value === null) { - return; - } - - stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.fontSize = Math.floor(value / SIZE_COEF) + 'vmin'; - events.emit('propChanged', 'size'); - } - }, - offset: { - configurable: false, - enumerable: true, - get: function() { - if (destroyed) { - return null; - } - - return parseInt(stylesElement.sheet.cssRules[subtitlesContainerStylesIndex].style.bottom); - }, - set: function(value) { - if (destroyed || isNaN(value) || value === null) { - return; - } - - stylesElement.sheet.cssRules[subtitlesContainerStylesIndex].style.bottom = Math.max(0, Math.min(100, parseInt(value))) + '%'; - events.emit('propChanged', 'offset'); - } - }, - textColor: { - configurable: false, - enumerable: true, - get: function() { - if (destroyed) { - return null; - } - - return colorConverter.rgbaToHex(stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.color); - }, - set: function(value) { - if (destroyed || typeof value !== 'string' || value.length !== 9 || !value.match(COLOR_REGEX)) { - return; - } - - stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.color = value; - events.emit('propChanged', 'textColor'); - } - }, - backgroundColor: { - configurable: false, - enumerable: true, - get: function() { - if (destroyed) { - return null; - } - - return colorConverter.rgbaToHex(stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.backgroundColor); - }, - set: function(value) { - if (destroyed || typeof value !== 'string' || value.length !== 9 || !value.match(COLOR_REGEX)) { - return; - } - - stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.backgroundColor = value; - events.emit('propChanged', 'backgroundColor'); - } - }, - outlineColor: { - configurable: false, - enumerable: false, - get: function() { - if (destroyed) { - return null; - } - - return colorConverter.rgbaToHex(stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.textShadow); - }, - set: function(value) { - if (destroyed || typeof value !== 'string' || value.length !== 9 || !value.match(COLOR_REGEX)) { - return; - } - - stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.textShadow = - value + ' 0 0 0.03em,' + - value + ' 0 0 0.03em,' + - value + ' 0 0 0.03em,' + - value + ' 0 0 0.03em,' + - value + ' 0 0 0.03em'; - events.emit('propChanged', 'outlineColor'); - } - } - }); - - this.on = on; - this.addTracks = addTracks; - this.updateText = updateText; - this.clearTracks = clearTracks; - this.destroy = destroy; - - Object.freeze(this); -}; - -Object.freeze(HTMLSubtitles); - -module.exports = HTMLSubtitles; diff --git a/src/video/HTMLVideo.js b/src/video/HTMLVideo.js deleted file mode 100644 index da206c02a..000000000 --- a/src/video/HTMLVideo.js +++ /dev/null @@ -1,460 +0,0 @@ -// Copyright (C) 2017-2020 Smart code 203358507 - -var EventEmitter = require('events'); -var HTMLSubtitles = require('./HTMLSubtitles'); - -function HTMLVideo(options) { - var containerElement = options && options.containerElement; - if (!(containerElement instanceof HTMLElement) || !containerElement.hasAttribute('id')) { - throw new Error('Instance of HTMLElement with id attribute required'); - } - if (!document.body.contains(containerElement)) { - throw new Error('Container element not attached to body'); - } - - var destroyed = false; - var loaded = false; - var events = new EventEmitter(); - var observedProps = {}; - var subtitles = new HTMLSubtitles({ containerElement: containerElement }); - var stylesElement = document.createElement('style'); - var videoElement = document.createElement('video'); - - events.on('error', function() { }); - subtitles.on('propChanged', onSubtitlesPropChanged); - subtitles.on('trackLoaded', onSubtitlesTrackLoaded); - subtitles.on('error', onSubtitlesError); - containerElement.appendChild(stylesElement); - stylesElement.sheet.insertRule('#' + containerElement.id + ' .video { position: absolute; width: 100%; height: 100%; z-index: -1; background-color: black; }', stylesElement.sheet.cssRules.length); - containerElement.appendChild(videoElement); - videoElement.classList.add('video'); - videoElement.crossOrigin = 'anonymous'; - videoElement.controls = false; - videoElement.onpause = function() { - onVideoPropChanged('paused'); - }; - videoElement.onplay = function() { - onVideoPropChanged('paused'); - }; - videoElement.ontimeupdate = function() { - onVideoPropChanged('currentTime'); - }; - videoElement.ondurationchange = function() { - onVideoPropChanged('duration'); - }; - videoElement.onwaiting = function() { - onVideoPropChanged('readyState'); - }; - videoElement.onplaying = function() { - onVideoPropChanged('readyState'); - }; - videoElement.onloadeddata = function() { - onVideoPropChanged('readyState'); - }; - videoElement.onvolumechange = function() { - onVideoPropChanged('volume'); - onVideoPropChanged('muted'); - }; - videoElement.onended = function() { - onVideoEnded(); - }; - videoElement.onerror = function() { - onVideoError(); - }; - - function onSubtitlesPropChanged(propName) { - switch (propName) { - case 'tracks': { - if (observedProps['subtitlesTracks']) { - events.emit('propChanged', 'subtitlesTracks', getProp('subtitlesTracks')); - } - - break; - } - case 'selectedTrackId': { - if (observedProps['selectedSubtitlesTrackId']) { - events.emit('propChanged', 'selectedSubtitlesTrackId', getProp('selectedSubtitlesTrackId')); - } - - break; - } - case 'delay': { - subtitles.updateText(getProp('time')); - if (observedProps['subtitlesDelay']) { - events.emit('propChanged', 'subtitlesDelay', getProp('subtitlesDelay')); - } - - break; - } - case 'size': { - if (observedProps['subtitlesSize']) { - events.emit('propChanged', 'subtitlesSize', getProp('subtitlesSize')); - } - - break; - } - case 'offset': { - if (observedProps['subtitlesOffset']) { - events.emit('propChanged', 'subtitlesOffset', getProp('subtitlesOffset')); - } - - break; - } - case 'textColor': { - if (observedProps['subtitlesTextColor']) { - events.emit('propChanged', 'subtitlesTextColor', getProp('subtitlesTextColor')); - } - - break; - } - case 'backgroundColor': { - if (observedProps['subtitlesBackgroundColor']) { - events.emit('propChanged', 'subtitlesBackgroundColor', getProp('subtitlesBackgroundColor')); - } - - break; - } - case 'outlineColor': { - if (observedProps['subtitlesOutlineColor']) { - events.emit('propChanged', 'subtitlesOutlineColor', getProp('subtitlesOutlineColor')); - } - - break; - } - } - } - function onSubtitlesTrackLoaded(track) { - subtitles.updateText(getProp('time')); - events.emit('subtitlesTrackLoaded', track); - } - function onSubtitlesError(error) { - onError(Object.assign({}, error, { - critical: false - })); - } - function onVideoPropChanged(propName) { - switch (propName) { - case 'paused': { - if (observedProps['paused']) { - events.emit('propChanged', 'paused', getProp('paused')); - } - - break; - } - case 'currentTime': { - subtitles.updateText(getProp('time')); - if (observedProps['time']) { - events.emit('propChanged', 'time', getProp('time')); - } - - break; - } - case 'duration': { - if (observedProps['duration']) { - events.emit('propChanged', 'duration', getProp('duration')); - } - - break; - } - case 'readyState': { - if (observedProps['buffering']) { - events.emit('propChanged', 'buffering', getProp('buffering')); - } - - break; - } - case 'volume': { - if (observedProps['volume']) { - events.emit('propChanged', 'volume', getProp('volume')); - } - - break; - } - case 'muted': { - if (observedProps['muted']) { - events.emit('propChanged', 'muted', getProp('muted')); - } - - break; - } - } - } - function onVideoEnded() { - events.emit('ended'); - } - function onVideoError() { - onError({ - code: videoElement.error.code, - message: videoElement.error.message, - critical: true - }); - } - function onError(error) { - if (!error) { - return; - } - - Object.freeze(error); - events.emit('error', error); - if (error.critical) { - command('stop'); - } - } - function getProp(propName) { - switch (propName) { - case 'paused': { - if (!loaded) { - return null; - } - - return !!videoElement.paused; - } - case 'time': { - if (!loaded || isNaN(videoElement.currentTime) || videoElement.currentTime === null) { - return null; - } - - return Math.floor(videoElement.currentTime * 1000); - } - case 'duration': { - if (!loaded || isNaN(videoElement.duration) || videoElement.duration === null) { - return null; - } - - return Math.floor(videoElement.duration * 1000); - } - case 'buffering': { - if (!loaded) { - return null; - } - - return videoElement.readyState < videoElement.HAVE_FUTURE_DATA; - } - case 'volume': { - if (destroyed || isNaN(videoElement.volume) || videoElement.volume === null) { - return null; - } - - return Math.floor(videoElement.volume * 100); - } - case 'muted': { - if (destroyed) { - return null; - } - - return !!videoElement.muted; - } - case 'subtitlesTracks': { - return subtitles.tracks; - } - case 'selectedSubtitlesTrackId': { - return subtitles.selectedTrackId; - } - case 'subtitlesDelay': { - return subtitles.delay; - } - case 'subtitlesSize': { - return subtitles.size; - } - case 'subtitlesOffset': { - return subtitles.offset; - } - case 'subtitlesTextColor': { - return subtitles.textColor; - } - case 'subtitlesBackgroundColor': { - return subtitles.backgroundColor; - } - case 'subtitlesOutlineColor': { - return subtitles.outlineColor; - } - default: { - throw new Error('getProp not supported: ' + propName); - } - } - } - function observeProp(propName) { - if (HTMLVideo.manifest.props.indexOf(propName) === -1) { - throw new Error('observeProp not supported: ' + propName); - } - - events.emit('propValue', propName, getProp(propName)); - observedProps[propName] = true; - } - function setProp(propName, propValue) { - switch (propName) { - case 'paused': { - if (loaded) { - if (!!propValue) { - videoElement.pause(); - } else { - videoElement.play(); - } - } - - break; - } - case 'time': { - if (loaded && !isNaN(propValue) && propValue !== null) { - videoElement.currentTime = parseInt(propValue) / 1000; - } - - break; - } - case 'volume': { - if (!isNaN(propValue) && propValue !== null) { - videoElement.muted = false; - videoElement.volume = Math.max(0, Math.min(100, parseInt(propValue))) / 100; - } - - break; - } - case 'muted': { - videoElement.muted = !!propValue; - break; - } - case 'selectedSubtitlesTrackId': { - if (loaded) { - subtitles.selectedTrackId = propValue; - } - - break; - } - case 'subtitlesDelay': { - if (loaded) { - subtitles.delay = propValue; - } - - break; - } - case 'subtitlesSize': { - subtitles.size = propValue; - break; - } - case 'subtitlesOffset': { - subtitles.offset = propValue; - break; - } - case 'subtitlesTextColor': { - subtitles.textColor = propValue; - break; - } - case 'subtitlesBackgroundColor': { - subtitles.backgroundColor = propValue; - break; - } - case 'subtitlesOutlineColor': { - subtitles.outlineColor = propValue; - break; - } - default: { - throw new Error('setProp not supported: ' + propName); - } - } - } - function command(commandName, commandArgs) { - switch (commandName) { - case 'addSubtitlesTracks': { - if (loaded && commandArgs) { - subtitles.addTracks(commandArgs.tracks); - } - - break; - } - case 'stop': { - loaded = false; - videoElement.removeAttribute('src'); - videoElement.load(); - videoElement.currentTime = 0; - onVideoPropChanged('paused'); - onVideoPropChanged('currentTime'); - onVideoPropChanged('duration'); - onVideoPropChanged('readyState'); - subtitles.clearTracks(); - break; - } - case 'load': { - if (commandArgs && commandArgs.stream && typeof commandArgs.stream.url === 'string') { - command('stop'); - videoElement.autoplay = typeof commandArgs.autoplay === 'boolean' ? commandArgs.autoplay : true; - videoElement.currentTime = !isNaN(commandArgs.time) && commandArgs.time !== null ? parseInt(commandArgs.time) / 1000 : 0; - videoElement.src = commandArgs.stream.url; - loaded = true; - onVideoPropChanged('paused'); - onVideoPropChanged('currentTime'); - onVideoPropChanged('duration'); - onVideoPropChanged('readyState'); - } - - break; - } - case 'destroy': { - command('stop'); - destroyed = true; - onVideoPropChanged('volume'); - onVideoPropChanged('muted'); - subtitles.destroy(); - events.removeAllListeners(); - events.on('error', function() { }); - videoElement.onpause = null; - videoElement.onplay = null; - videoElement.ontimeupdate = null; - videoElement.ondurationchange = null; - videoElement.onwaiting = null; - videoElement.onplaying = null; - videoElement.onloadeddata = null; - videoElement.onvolumechange = null; - videoElement.onended = null; - videoElement.onerror = null; - containerElement.removeChild(videoElement); - containerElement.removeChild(stylesElement); - break; - } - default: { - throw new Error('command not supported: ' + commandName); - } - } - } - function on(eventName, listener) { - if (destroyed) { - throw new Error('Video is destroyed'); - } - - events.on(eventName, listener); - } - function dispatch(args) { - if (destroyed) { - throw new Error('Video is destroyed'); - } - - if (args) { - if (typeof args.commandName === 'string') { - command(args.commandName, args.commandArgs); - return; - } else if (typeof args.propName === 'string') { - setProp(args.propName, args.propValue); - return; - } else if (typeof args.observedPropName === 'string') { - observeProp(args.observedPropName); - return; - } - } - - throw new Error('Invalid dispatch call: ' + JSON.stringify(args)); - } - - this.on = on; - this.dispatch = dispatch; - - Object.freeze(this); -}; - -HTMLVideo.manifest = Object.freeze({ - name: 'HTMLVideo', - embedded: true, - props: Object.freeze(['paused', 'time', 'duration', 'buffering', 'volume', 'muted', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesSize', 'subtitlesDelay', 'subtitlesOffset', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor']) -}); - -Object.freeze(HTMLVideo); - -module.exports = HTMLVideo; diff --git a/src/video/MPVVideo.js b/src/video/MPVVideo.js deleted file mode 100644 index a1ace472b..000000000 --- a/src/video/MPVVideo.js +++ /dev/null @@ -1,438 +0,0 @@ -// Copyright (C) 2017-2020 Smart code 203358507 - -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; diff --git a/src/video/README.MD b/src/video/README.MD deleted file mode 100644 index ac775d66d..000000000 --- a/src/video/README.MD +++ /dev/null @@ -1,3 +0,0 @@ -# stremio-video - -### TODO move this folder in a separate repo \ No newline at end of file diff --git a/src/video/YouTubeVideo.js b/src/video/YouTubeVideo.js deleted file mode 100644 index 5b2958803..000000000 --- a/src/video/YouTubeVideo.js +++ /dev/null @@ -1,685 +0,0 @@ -// Copyright (C) 2017-2020 Smart code 203358507 - -var EventEmitter = require('events'); -var HTMLSubtitles = require('./HTMLSubtitles'); - -function YouTubeVideo(options) { - var containerElement = options && options.containerElement; - if (!(containerElement instanceof HTMLElement) || !containerElement.hasAttribute('id')) { - throw new Error('Instance of HTMLElement with id attribute required'); - } - - var self = this; - var ready = false; - var loaded = false; - var destroyed = false; - var events = new EventEmitter(); - var dispatchArgsReadyQueue = []; - var dispatchArgsLoadedQueue = []; - var pausedObserved = false; - var timeObserved = false; - var durationObserved = false; - var bufferingObserved = false; - var volumeObserved = false; - var propChangedIntervalId = window.setInterval(onPropChangedInterval, 100); - var embeddedSubtitlesSelectedTrackId = null; - var subtitles = new HTMLSubtitles(containerElement); - var video = null; - var scriptElement = document.createElement('script'); - var stylesElement = document.createElement('style'); - var videoContainer = document.createElement('div'); - - events.on('error', function() { }); - subtitles.on('error', onSubtitlesError); - subtitles.on('load', updateSubtitleText); - scriptElement.type = 'text/javascript'; - scriptElement.src = 'https://www.youtube.com/iframe_api'; - scriptElement.onload = onYouTubePlayerApiLoaded; - scriptElement.onerror = onYouTubePlayerApiError; - containerElement.appendChild(scriptElement); - containerElement.appendChild(stylesElement); - stylesElement.sheet.insertRule('#' + containerElement.id + ' .video { position: absolute; width: 100%; height: 100%; z-index: -1; }', stylesElement.sheet.cssRules.length); - containerElement.appendChild(videoContainer); - videoContainer.classList.add('video'); - - function getPaused() { - if (!loaded) { - return null; - } - - return video.getPlayerState() !== YT.PlayerState.PLAYING; - } - function getTime() { - if (!loaded || isNaN(video.getCurrentTime()) || video.getCurrentTime() === null) { - return null; - } - - return Math.floor(video.getCurrentTime() * 1000); - } - function getDuration() { - if (!loaded || isNaN(video.getDuration()) || video.getDuration() === null) { - return null; - } - - return Math.floor(video.getDuration() * 1000); - } - function getBuffering() { - if (!loaded) { - return null; - } - - return video.getPlayerState() === YT.PlayerState.BUFFERING; - } - function getVolume() { - if (!ready || destroyed || isNaN(video.getVolume()) || video.getVolume() === null) { - return null; - } - - return video.isMuted() ? 0 : video.getVolume(); - } - function getSubtitlesTracks() { - if (!loaded) { - return Object.freeze([]); - } - - var embeddedTracks = (video.getOption('captions', 'tracklist') || []) - .map(function(track) { - return Object.freeze({ - id: track.languageCode, - origin: 'EMBEDDED IN VIDEO', - label: track.languageName - }); - }); - var extraTracks = subtitles.dispatch('getProp', 'tracks'); - var allTracks = embeddedTracks.concat(extraTracks) - .filter(function(track, index, tracks) { - for (var i = 0; i < tracks.length; i++) { - if (tracks[i].id === track.id) { - return i === index; - } - } - - return false; - }); - return Object.freeze(allTracks); - } - function getSelectedSubtitlesTrackId() { - if (!loaded) { - return null; - } - - return embeddedSubtitlesSelectedTrackId !== null ? - embeddedSubtitlesSelectedTrackId - : - subtitles.dispatch('getProp', 'selectedTrackId'); - } - function getSubtitlesDelay() { - if (!loaded) { - return null; - } - - return embeddedSubtitlesSelectedTrackId !== null ? - null - : - subtitles.dispatch('getProp', 'delay'); - } - function getsubtitlesSize() { - if (!ready || destroyed) { - return null; - } - - return subtitles.dispatch('getProp', 'size'); - } - function getSubtitlesDarkBackground() { - if (!ready || destroyed) { - return null; - } - - return embeddedSubtitlesSelectedTrackId !== null ? - null - : - subtitles.dispatch('getProp', 'darkBackground'); - } - function getSubtitleOffset() { - if (!ready || destroyed) { - return null; - } - - return embeddedSubtitlesSelectedTrackId !== null ? - null - : - subtitles.dispatch('getProp', 'offset'); - } - function onEnded() { - events.emit('ended'); - } - function onError(error) { - Object.freeze(error); - events.emit('error', error); - if (error.critical) { - self.dispatch('command', 'stop'); - } - } - function onPausedChanged() { - events.emit('propChanged', 'paused', getPaused()); - } - function onTimeChanged() { - events.emit('propChanged', 'time', getTime()); - } - function onDurationChanged() { - events.emit('propChanged', 'duration', getDuration()); - } - function onBufferingChanged() { - events.emit('propChanged', 'buffering', getBuffering()); - } - function onVolumeChanged() { - events.emit('propChanged', 'volume', getVolume()); - } - function onSubtitlesTracksChanged() { - events.emit('propChanged', 'subtitlesTracks', getSubtitlesTracks()); - } - function onSelectedSubtitlesTrackIdChanged() { - events.emit('propChanged', 'selectedSubtitlesTrackId', getSelectedSubtitlesTrackId()); - } - function onSubtitlesDelayChanged() { - events.emit('propChanged', 'subtitlesDelay', getSubtitlesDelay()); - } - function onsubtitlesSizeChanged() { - events.emit('propChanged', 'subtitlesSize', getsubtitlesSize()); - } - function onSubtitlesDarkBackgroundChanged() { - events.emit('propChanged', 'subtitlesDarkBackground', getSubtitlesDarkBackground()); - } - function onSubtitleOffsetChanged() { - events.emit('propChanged', 'subtitleOffset', getSubtitleOffset()); - } - function onSubtitlesError(error) { - var code; - var message; - switch (error.code) { - case HTMLSubtitles.ERROR.SUBTITLES_FETCH_FAILED: { - code = HTMLSubtitles.ERROR.SUBTITLES_FETCH_FAILED; - message = 'Failed to fetch subtitles from ' + error.track.origin; - break; - } - case HTMLSubtitles.ERROR.SUBTITLES_PARSE_FAILED: { - code = HTMLSubtitles.ERROR.SUBTITLES_PARSE_FAILED; - message = 'Failed to parse subtitles from ' + error.track.origin; - break; - } - default: { - code = -1; - message = 'Unknown subtitles error'; - } - } - - onError({ - code: code, - message: message, - critical: false - }); - } - function onYouTubePlayerApiError() { - onError({ - code: YouTubeVideo.ERROR.API_LOAD_FAILED, - message: 'YouTube player API failed to load', - critical: true - }); - } - function onYouTubePlayerApiLoaded() { - if (destroyed) { - return; - } - - if (!YT) { - onYouTubePlayerApiError(); - return; - } - - YT.ready(function() { - if (destroyed) { - return; - } - - video = new YT.Player(videoContainer, { - height: '100%', - width: '100%', - playerVars: { - autoplay: 1, - cc_load_policy: 3, - controls: 0, - disablekb: 1, - enablejsapi: 1, - fs: 0, - iv_load_policy: 3, - loop: 0, - modestbranding: 1, - playsinline: 1, - rel: 0 - }, - events: { - onError: onVideoError, - onReady: onVideoReady, - onStateChange: onVideoStateChange, - onApiChange: onVideoApiChange - } - }); - }); - } - function onVideoError(error) { - var code; - var message; - switch (error.data) { - case YouTubeVideo.ERROR.INVALID_REQUEST: { - code = YouTubeVideo.ERROR.INVALID_REQUEST; - message = 'Invalid request'; - break; - } - case YouTubeVideo.ERROR.CONTENT_CANNOT_BE_PLAYED: { - code = YouTubeVideo.ERROR.CONTENT_CANNOT_BE_PLAYED; - message = 'The requested content cannot be played'; - break; - } - case YouTubeVideo.ERROR.REMOVED_VIDEO: { - code = YouTubeVideo.ERROR.REMOVED_VIDEO; - message = 'The video has been removed or marked as private'; - break; - } - case YouTubeVideo.ERROR.CONTENT_CANNOT_BE_EMBEDDED1: - case YouTubeVideo.ERROR.CONTENT_CANNOT_BE_EMBEDDED2: { - code = YouTubeVideo.ERROR.CONTENT_CANNOT_BE_EMBEDDED1; - message = 'The video cannot be played in embedded players'; - break; - } - default: { - code = -1; - message = 'Unknown video error'; - } - } - - onError({ - code: code, - message: message, - critical: true - }); - } - function onVideoReady() { - ready = true; - onVolumeChanged(); - onsubtitlesSizeChanged(); - onSubtitlesDarkBackgroundChanged(); - onSubtitleOffsetChanged(); - flushDispatchArgsQueue(dispatchArgsReadyQueue); - } - function onVideoStateChange(state) { - if (bufferingObserved) { - onBufferingChanged(); - } - - switch (state.data) { - case YT.PlayerState.ENDED: { - onEnded(); - break; - } - case YT.PlayerState.PAUSED: - case YT.PlayerState.PLAYING: { - if (pausedObserved) { - onPausedChanged(); - } - - if (timeObserved) { - onTimeChanged(); - } - - if (durationObserved) { - onDurationChanged(); - } - - break; - } - case YT.PlayerState.UNSTARTED: { - if (pausedObserved) { - onPausedChanged(); - } - - break; - } - } - } - function onVideoApiChange() { - video.loadModule('captions'); - onSubtitlesTracksChanged(); - } - function onPropChangedInterval() { - if (timeObserved) { - onTimeChanged(); - } - - if (durationObserved) { - onDurationChanged(); - } - - if (volumeObserved) { - onVolumeChanged(); - } - - updateSubtitleText(); - } - function updateSubtitleText() { - subtitles.dispatch('command', 'updateText', getTime()); - } - function flushDispatchArgsQueue(dispatchArgsQueue) { - while (dispatchArgsQueue.length > 0) { - var args = dispatchArgsQueue.shift(); - self.dispatch.apply(self, args); - } - } - - this.on = function(eventName, listener) { - if (destroyed) { - throw new Error('Unable to add ' + eventName + ' listener'); - } - - events.on(eventName, listener); - }; - - this.dispatch = function() { - if (destroyed) { - throw new Error('Unable to dispatch ' + arguments[0]); - } - - switch (arguments[0]) { - case 'observeProp': { - switch (arguments[1]) { - case 'paused': { - events.emit('propValue', 'paused', getPaused()); - pausedObserved = true; - return; - } - case 'time': { - events.emit('propValue', 'time', getTime()); - timeObserved = true; - return; - } - case 'duration': { - events.emit('propValue', 'duration', getDuration()); - durationObserved = true; - return; - } - case 'buffering': { - events.emit('propValue', 'buffering', getBuffering()); - bufferingObserved = true; - return; - } - case 'volume': { - events.emit('propValue', 'volume', getVolume()); - volumeObserved = true; - return; - } - case 'subtitlesTracks': { - events.emit('propValue', 'subtitlesTracks', getSubtitlesTracks()); - return; - } - case 'selectedSubtitlesTrackId': { - events.emit('propValue', 'selectedSubtitlesTrackId', getSelectedSubtitlesTrackId()); - return; - } - case 'subtitlesDelay': { - events.emit('propValue', 'subtitlesDelay', getSubtitlesDelay()); - return; - } - case 'subtitlesSize': { - events.emit('propValue', 'subtitlesSize', getsubtitlesSize()); - return; - } - case 'subtitlesDarkBackground': { - events.emit('propValue', 'subtitlesDarkBackground', getSubtitlesDarkBackground()); - return; - } - case 'subtitleOffset': { - events.emit('propValue', 'subtitleOffset', getSubtitleOffset()); - return; - } - default: { - throw new Error('observeProp not supported: ' + arguments[1]); - } - } - } - case 'setProp': { - switch (arguments[1]) { - case 'paused': { - if (loaded) { - arguments[2] ? video.pauseVideo() : video.playVideo(); - } else { - dispatchArgsLoadedQueue.push(Array.from(arguments)); - } - - return; - } - case 'time': { - if (loaded) { - if (!isNaN(arguments[2]) && arguments[2] !== null) { - video.seekTo(arguments[2] / 1000); - } - } else { - dispatchArgsLoadedQueue.push(Array.from(arguments)); - } - - return; - } - case 'volume': { - if (ready) { - if (!isNaN(arguments[2]) && arguments[2] !== null) { - video.unMute(); - video.setVolume(Math.max(0, Math.min(100, arguments[2]))); - } - } else { - dispatchArgsReadyQueue.push(Array.from(arguments)); - } - - return; - } - case 'selectedSubtitlesTrackId': { - if (loaded) { - embeddedSubtitlesSelectedTrackId = null; - var tracks = getSubtitlesTracks(); - for (var i = 0; i < tracks.length; i++) { - if (tracks[i].id === arguments[2] && tracks[i].origin === 'EMBEDDED IN VIDEO') { - embeddedSubtitlesSelectedTrackId = tracks[i].id; - break; - } - } - - video.setOption('captions', 'track', { languageCode: arguments[2] }); - subtitles.dispatch('setProp', 'selectedTrackId', arguments[2]); - onSubtitlesDelayChanged(); - onSubtitlesDarkBackgroundChanged(); - onSelectedSubtitlesTrackIdChanged(); - updateSubtitleText(); - } else { - dispatchArgsLoadedQueue.push(Array.from(arguments)); - } - - return; - } - case 'subtitlesDelay': { - if (loaded) { - subtitles.dispatch('setProp', 'delay', arguments[2]); - onSubtitlesDelayChanged(); - updateSubtitleText(); - } else { - dispatchArgsLoadedQueue.push(Array.from(arguments)); - } - - return; - } - case 'subtitlesSize': { - if (ready) { - subtitles.dispatch('setProp', 'size', arguments[2]); - video.setOption('captions', 'fontSize', Math.max(1, Math.min(5, Math.floor(arguments[2]))) - 2); - onsubtitlesSizeChanged(); - } else { - dispatchArgsReadyQueue.push(Array.from(arguments)); - } - - return; - } - case 'subtitlesDarkBackground': { - if (ready) { - subtitles.dispatch('setProp', 'darkBackground', arguments[2]); - onSubtitlesDarkBackgroundChanged(); - } else { - dispatchArgsReadyQueue.push(Array.from(arguments)); - } - - return; - } - case 'subtitleOffset': { - if (ready) { - subtitles.dispatch('setProp', 'offset', arguments[2]); - onSubtitleOffsetChanged(); - } else { - dispatchArgsReadyQueue.push(Array.from(arguments)); - } - - return; - } - default: { - throw new Error('setProp not supported: ' + arguments[1]); - } - } - } - case 'command': { - switch (arguments[1]) { - case 'addSubtitlesTracks': { - if (loaded) { - subtitles.dispatch('command', 'addTracks', arguments[2]); - onSubtitlesTracksChanged(); - } else { - dispatchArgsLoadedQueue.push(Array.from(arguments)); - } - - return; - } - case 'mute': { - if (ready) { - video.mute(); - } else { - dispatchArgsReadyQueue.push(Array.from(arguments)); - } - - return; - } - case 'unmute': { - if (ready) { - video.unMute(); - if (video.getVolume() === 0) { - video.setVolume(50); - } - } else { - dispatchArgsReadyQueue.push(Array.from(arguments)); - } - - return; - } - case 'stop': { - loaded = false; - dispatchArgsLoadedQueue = []; - subtitles.dispatch('command', 'clearTracks'); - if (ready) { - video.stopVideo(); - } - onPausedChanged(); - onTimeChanged(); - onDurationChanged(); - onBufferingChanged(); - onSubtitlesTracksChanged(); - onSelectedSubtitlesTrackIdChanged(); - onSubtitlesDelayChanged(); - updateSubtitleText(); - return; - } - case 'load': { - if (ready) { - var dispatchArgsLoadedQueueCopy = dispatchArgsLoadedQueue.slice(); - self.dispatch('command', 'stop'); - dispatchArgsLoadedQueue = dispatchArgsLoadedQueueCopy; - var autoplay = typeof arguments[3].autoplay === 'boolean' ? arguments[3].autoplay : true; - var time = !isNaN(arguments[3].time) && arguments[3].time !== null ? arguments[3].time / 1000 : 0; - if (autoplay) { - video.loadVideoById({ - videoId: arguments[2].ytId, - startSeconds: time - }); - } else { - video.cueVideoById({ - videoId: arguments[2].ytId, - startSeconds: time - }); - } - loaded = true; - onPausedChanged(); - onTimeChanged(); - onDurationChanged(); - onBufferingChanged(); - onSubtitlesTracksChanged(); - onSelectedSubtitlesTrackIdChanged(); - onSubtitlesDelayChanged(); - updateSubtitleText(); - flushDispatchArgsQueue(dispatchArgsLoadedQueue); - } else { - dispatchArgsReadyQueue.push(Array.from(arguments)); - } - - return; - } - case 'destroy': { - self.dispatch('command', 'stop'); - destroyed = true; - onVolumeChanged(); - onsubtitlesSizeChanged(); - onSubtitlesDarkBackgroundChanged(); - onSubtitleOffsetChanged(); - events.removeAllListeners(); - clearInterval(propChangedIntervalId); - if (ready) { - video.destroy(); - } - containerElement.removeChild(scriptElement); - containerElement.removeChild(videoContainer); - containerElement.removeChild(stylesElement); - subtitles.dispatch('command', 'destroy'); - return; - } - default: { - throw new Error('command not supported: ' + arguments[1]); - } - } - } - default: { - throw new Error('Invalid dispatch call: ' + Array.from(arguments).map(String)); - } - } - }; - - Object.freeze(this); -}; - -YouTubeVideo.ERROR = Object.freeze({ - API_LOAD_FAILED: 12, - INVALID_REQUEST: 2, - CONTENT_CANNOT_BE_PLAYED: 5, - REMOVED_VIDEO: 100, - CONTENT_CANNOT_BE_EMBEDDED1: 101, - CONTENT_CANNOT_BE_EMBEDDED2: 150 -}); - -YouTubeVideo.manifest = Object.freeze({ - name: 'YouTubeVideo', - embedded: true, - props: Object.freeze(['paused', 'time', 'duration', 'volume', 'buffering', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesSize', 'subtitlesDelay', 'subtitlesDarkBackground', 'subtitleOffset']) -}); - -Object.freeze(YouTubeVideo); - -module.exports = YouTubeVideo; diff --git a/src/video/binarySearchUpperBound.js b/src/video/binarySearchUpperBound.js deleted file mode 100644 index 35f6f1882..000000000 --- a/src/video/binarySearchUpperBound.js +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (C) 2017-2020 Smart code 203358507 - -function binarySearchUpperBound(array, value) { - if (value < array[0] || array[array.length - 1] < value) { - return -1; - } - - var left = 0; - var right = array.length - 1; - var index = -1; - while (left <= right) { - var middle = Math.floor((left + right) / 2); - if (array[middle] > value) { - right = middle - 1; - } else if (array[middle] < value) { - left = middle + 1; - } else { - index = middle; - left = middle + 1; - } - } - - return index !== -1 ? index : right; -} - -module.exports = binarySearchUpperBound; diff --git a/src/video/colorConverter.js b/src/video/colorConverter.js deleted file mode 100644 index 6b4f4ad44..000000000 --- a/src/video/colorConverter.js +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (C) 2017-2020 Smart code 203358507 - -function padWithZero(str) { - return ('0' + str).slice(-2); -} - -function rgbaToHex(rgbaString) { - var values = rgbaString.split('(')[1].split(')')[0].split(','); - var red = parseInt(values[0]).toString(16); - var green = parseInt(values[1]).toString(16); - var blue = parseInt(values[2]).toString(16); - var alpha = Math.round((values[3] || 1) * 255).toString(16); - return '#' + padWithZero(red) + padWithZero(green) + padWithZero(blue) + padWithZero(alpha); -} - -module.exports = { - rgbaToHex -}; diff --git a/src/video/index.js b/src/video/index.js deleted file mode 100644 index f85cef1d7..000000000 --- a/src/video/index.js +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (C) 2017-2020 Smart code 203358507 - -var HTMLVideo = require('./HTMLVideo'); -var MPVVideo = require('./MPVVideo'); -var YouTubeVideo = require('./YouTubeVideo'); -var withStreamingServer = require('./withStreamingServer'); - -module.exports = { - HTMLVideo, - MPVVideo, - YouTubeVideo, - withStreamingServer -}; diff --git a/src/video/subtitlesParser.js b/src/video/subtitlesParser.js deleted file mode 100644 index ead1613b4..000000000 --- a/src/video/subtitlesParser.js +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (C) 2017-2020 Smart code 203358507 - -var VTTJS = require('vtt.js'); -var binarySearchUpperBound = require('./binarySearchUpperBound'); - -function parse(text) { - var nativeVTTCue = window.VTTCue; - window.VTTCue = VTTJS.VTTCue; - var parser = new VTTJS.WebVTT.Parser(window, VTTJS.WebVTT.StringDecoder()); - var cues = []; - var cuesByTime = {}; - parser.oncue = function(c) { - var cue = Object.freeze({ - startTime: (c.startTime * 1000) | 0, - endTime: (c.endTime * 1000) | 0, - text: c.text - }); - cues.push(cue); - cuesByTime[cue.startTime] = cuesByTime[cue.startTime] || []; - cuesByTime[cue.endTime] = cuesByTime[cue.endTime] || []; - }; - parser.parse(text); - parser.flush(); - window.VTTCue = nativeVTTCue; - cuesByTime.times = Object.keys(cuesByTime) - .map(function(time) { - return parseInt(time); - }) - .sort(function(t1, t2) { - return t1 - t2; - }); - Object.freeze(cues); - Object.freeze(cuesByTime); - Object.freeze(cuesByTime.times); - for (var i = 0; i < cues.length; i++) { - cuesByTime[cues[i].startTime].push(cues[i]); - var startTimeIndex = binarySearchUpperBound(cuesByTime.times, cues[i].startTime); - for (var j = startTimeIndex + 1; j < cuesByTime.times.length; j++) { - if (cues[i].endTime <= cuesByTime.times[j]) { - break; - } - - cuesByTime[cuesByTime.times[j]].push(cues[i]); - } - } - - for (var i = 0; i < cuesByTime.times.length; i++) { - cuesByTime[cuesByTime.times[i]].sort(function(c1, c2) { - return c1.startTime - c2.startTime || - c1.endTime - c2.endTime; - }); - - Object.freeze(cuesByTime[cuesByTime.times[i]]); - } - - return cuesByTime; -} - -module.exports = Object.freeze({ - parse: parse -}); diff --git a/src/video/subtitlesRenderer.js b/src/video/subtitlesRenderer.js deleted file mode 100644 index 12d9dc1f8..000000000 --- a/src/video/subtitlesRenderer.js +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (C) 2017-2020 Smart code 203358507 - -var VTTJS = require('vtt.js'); -var binarySearchUpperBound = require('./binarySearchUpperBound'); - -function render(cuesByTime, time) { - var nodes = []; - var timeIndex = binarySearchUpperBound(cuesByTime.times, time); - if (timeIndex !== -1) { - var cuesForTime = cuesByTime[cuesByTime.times[timeIndex]]; - for (var i = 0; i < cuesForTime.length; i++) { - var node = VTTJS.WebVTT.convertCueToDOMTree(window, cuesForTime[i].text); - nodes.push(node); - } - } - - return Object.freeze(nodes); -} - -module.exports = Object.freeze({ - render: render -}); diff --git a/src/video/withStreamingServer.js b/src/video/withStreamingServer.js deleted file mode 100644 index 95739517b..000000000 --- a/src/video/withStreamingServer.js +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (C) 2017-2020 Smart code 203358507 - -var UrlUtils = require('url'); -var EventEmitter = require('events'); -var parseVideoName = require('video-name-parser'); - -var VIDEO_FILE_EXTENTIONS = /.mkv$|.avi$|.mp4$|.wmv$|.vp8$|.mov$|.mpg$|.ts$|.webm$/i; - -function createTorrent(streamingServerUrl, infoHash, sources) { - return fetch(UrlUtils.resolve(streamingServerUrl, `/${encodeURIComponent(infoHash)}/create`), { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ - torrent: { - infoHash, - peerSearch: { - sources: [`dht:${infoHash}`].concat(Array.isArray(sources) ? sources : []), - min: 40, - max: 150 - } - } - }) - }).then(function(resp) { - return resp.json(); - }).catch(function(error) { - throw { - message: 'Unable to get files from torrent', - critical: true, - error: error - }; - }).then(function(resp) { - if (!resp || !Array.isArray(resp.files) || resp.files.length === 0) { - throw { - message: 'Unable to get files from torrent', - critical: true - }; - } - - return resp; - }); -} - -function guessFileIdx(files, seriesInfo) { - var videoFilesForEpisode = files.filter(function(file) { - if (seriesInfo && file.path.match(VIDEO_FILE_EXTENTIONS)) { - try { - var info = parseVideoName(file.path); - return !isNaN(info.season) && Array.isArray(info.episode) && - info.season === seriesInfo.season && info.episode.indexOf(seriesInfo.episode) !== -1; - } catch (e) { - return false; - } - } - }); - var largestFile = (videoFilesForEpisode.length > 0 ? videoFilesForEpisode : files) - .reduce((result, file) => { - if (!result || file.length > result.length) { - return file; - } - - return result; - }, null); - return files.indexOf(largestFile); -} - -function withStreamingServer(Video) { - function StreamingServerVideo(options) { - var video = new Video(options); - var events = new EventEmitter(); - - var destroyed = false; - var stream = null; - - events.on('error', function() { }); - - function onError(error) { - events.emit('error', error); - if (error.critical) { - stop(); - video.dispatch({ commandName: 'stop' }); - } - } - function stop() { - stream = null; - } - function load(args) { - video.dispatch({ commandName: 'stop' }); - stream = args.stream; - new Promise(function(resolve, reject) { - if (typeof args.stream.ytId === 'string') { - resolve(UrlUtils.resolve(args.streamingServerUrl, `/yt/${encodeURIComponent(args.stream.ytId)}?${new URLSearchParams([['request', Date.now()]])}`)); - return; - } - - if (typeof args.stream.infoHash === 'string') { - if (args.stream.fileIdx !== null && !isNaN(args.stream.fileIdx)) { - resolve(UrlUtils.resolve(args.streamingServerUrl, `/${args.stream.infoHash}/${args.stream.fileIdx}`)); - } else { - createTorrent(args.streamingServerUrl, args.stream.infoHash, args.stream.sources) - .then(function(resp) { - var fileIdx = guessFileIdx(resp.files, args.stream.seriesInfo); - resolve(UrlUtils.resolve(args.streamingServerUrl, `/${args.stream.infoHash}/${fileIdx}`)); - }) - .catch(function(error) { - reject(error); - }); - } - return; - } - - reject({ - message: 'Unable to play stream', - critical: true, - stream: args.stream - }); - }).then(function(url) { - if (destroyed || args.stream !== stream) { - return; - } - - video.dispatch({ - commandName: 'load', - commandArgs: { - autoplay: args.autoplay, - time: args.time, - stream: { - url: url - } - } - }); - }).catch(function(error) { - if (destroyed || args.stream !== stream) { - return; - } - - onError(error); - }); - } - function destroy() { - stop(); - destroyed = true; - events.removeAllListeners(); - events.on('error', function() { }); - } - - this.on = function(eventName, listener) { - if (!destroyed) { - events.on(eventName, listener); - } - - video.on(eventName, listener); - }; - this.dispatch = function(args) { - if (!destroyed && args) { - if (typeof args.commandName === 'string') { - switch (args.commandName) { - case 'stop': { - stop(); - break; - } - case 'load': { - load(args.commandArgs); - return; - } - case 'destroy': { - destroy(); - break; - } - } - } - } - - video.dispatch(args); - }; - - Object.freeze(this); - } - - StreamingServerVideo.manifest = Object.freeze({ - name: Video.manifest.name + 'WithStreamingServer', - embedded: true, - props: Object.freeze(Video.manifest.props) - }); - - Object.freeze(StreamingServerVideo); - - return StreamingServerVideo; -} - -module.exports = withStreamingServer; diff --git a/webpack.config.js b/webpack.config.js index 73e2db8be..8dfa4b8c7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -106,8 +106,7 @@ module.exports = (env, argv) => ({ extensions: ['.js', '.json', '.less', '.wasm'], alias: { 'stremio': path.resolve(__dirname, 'src'), - 'stremio-router': path.resolve(__dirname, 'src/router'), - 'stremio-video': path.resolve(__dirname, 'src/video') + 'stremio-router': path.resolve(__dirname, 'src/router') } }, devServer: { From f517de30fadefb2570155a6e190916249837bb8a Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Thu, 30 Apr 2020 11:41:35 +0300 Subject: [PATCH 04/72] hat and vtt.js deps removed --- package.json | 4 +--- yarn.lock | 10 ---------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/package.json b/package.json index 0eb032d12..ebfe90a9e 100755 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "a-color-picker": "1.2.1", "classnames": "2.2.6", "events": "1.1.1", - "hat": "0.0.3", "lodash.debounce": "4.0.8", "lodash.isequal": "4.5.0", "lodash.throttle": "4.1.1", @@ -30,8 +29,7 @@ "react-focus-lock": "2.2.1", "spatial-navigation-polyfill": "git+https://git@github.com/Stremio/spatial-navigation.git#40204ad9942fe786794c62f99ea5ab2b52b24096", "stremio-colors": "git+https://git@github.com/Stremio/stremio-colors.git#v3.0.0", - "stremio-icons": "git+https://git@github.com/Stremio/stremio-icons.git#v2.0.2", - "vtt.js": "0.13.0" + "stremio-icons": "git+https://git@github.com/Stremio/stremio-icons.git#v2.0.2" }, "devDependencies": { "@babel/core": "7.8.7", diff --git a/yarn.lock b/yarn.lock index 401b2efa8..e1a30fe31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5940,11 +5940,6 @@ hastscript@^5.0.0: property-information "^5.0.0" space-separated-tokens "^1.0.0" -hat@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/hat/-/hat-0.0.3.tgz#bb014a9e64b3788aed8005917413d4ff3d502d8a" - integrity sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo= - he@1.2.x, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -11507,11 +11502,6 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -vtt.js@0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/vtt.js/-/vtt.js-0.13.0.tgz#955c667b34d5325b2012cb9e8ba9bad6e0b11ff8" - integrity sha1-lVxmezTVMlsgEsuei6m61uCxH/g= - w3c-hr-time@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" From a13f902dfc89bf4e63f8a29d845c8b8f4dc8983f Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Thu, 30 Apr 2020 11:57:41 +0300 Subject: [PATCH 05/72] drop id attribute of Video --- src/routes/Player/Video/Video.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/routes/Player/Video/Video.js b/src/routes/Player/Video/Video.js index 3d2e70bee..a0ff9f1a1 100644 --- a/src/routes/Player/Video/Video.js +++ b/src/routes/Player/Video/Video.js @@ -2,7 +2,6 @@ const React = require('react'); const PropTypes = require('prop-types'); -const hat = require('hat'); const { useLiveRef } = require('stremio/common'); const selectVideoImplementation = require('./selectVideoImplementation'); @@ -15,7 +14,6 @@ const Video = React.forwardRef(({ className, ...props }, ref) => { const onImplementationChangedRef = useLiveRef(props.onImplementationChanged); const containerElementRef = React.useRef(null); const videoRef = React.useRef(null); - const id = React.useMemo(() => `video-${hat()}`, []); const dispatch = React.useCallback((args) => { if (args && args.commandName === 'load' && args.commandArgs) { const Video = selectVideoImplementation(args.commandArgs.shell, args.commandArgs.stream); @@ -24,7 +22,6 @@ const Video = React.forwardRef(({ className, ...props }, ref) => { } else if (videoRef.current === null || videoRef.current.constructor !== Video) { dispatch({ commandName: 'destroy' }); videoRef.current = new Video({ - id: id, containerElement: containerElementRef.current, shell: args.commandArgs.shell }); @@ -75,7 +72,7 @@ const Video = React.forwardRef(({ className, ...props }, ref) => { }; }, []); return ( -
+
); }); From 82dd0b33a46366ba9f9930f77e3383af557d02bc Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Thu, 30 Apr 2020 12:03:24 +0300 Subject: [PATCH 06/72] adapt video messages to match the new spec --- src/routes/Player/Player.js | 43 ++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 80a57a2de..a558a63f9 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -58,13 +58,13 @@ const Player = ({ urlParams }) => { }, []); const onImplementationChanged = React.useCallback((manifest) => { manifest.props.forEach((propName) => { - dispatch({ observedPropName: propName }); + dispatch({ type: 'observeProp', propName }); }); - dispatch({ propName: 'subtitlesSize', propValue: settings.subtitles_size }); - dispatch({ propName: 'subtitlesTextColor', propValue: settings.subtitles_text_color }); - dispatch({ propName: 'subtitlesBackgroundColor', propValue: settings.subtitles_background_color }); - dispatch({ propName: 'subtitlesOutlineColor', propValue: settings.subtitles_outline_color }); - dispatch({ propName: 'subtitlesOffset', propValue: settings.subtitles_offset }); + dispatch({ type: 'setProp', propName: 'subtitlesSize', propValue: settings.subtitles_size }); + dispatch({ type: 'setProp', propName: 'subtitlesTextColor', propValue: settings.subtitles_text_color }); + dispatch({ type: 'setProp', propName: 'subtitlesBackgroundColor', propValue: settings.subtitles_background_color }); + dispatch({ type: 'setProp', propName: 'subtitlesOutlineColor', propValue: settings.subtitles_outline_color }); + dispatch({ type: 'setProp', propName: 'subtitlesOffset', propValue: settings.subtitles_offset }); }, [settings.subtitles_size, settings.subtitles_text_color, settings.subtitles_background_color, settings.subtitles_outline_color, settings.subtitles_offset]); const onPropChanged = React.useCallback((propName, propValue) => { setVideoState({ [propName]: propValue }); @@ -106,30 +106,30 @@ const Player = ({ urlParams }) => { }); }, []); const onPlayRequested = React.useCallback(() => { - dispatch({ propName: 'paused', propValue: false }); + dispatch({ type: 'setProp', propName: 'paused', propValue: false }); }, []); const onPlayRequestedDebounced = React.useCallback(debounce(onPlayRequested, 200), []); const onPauseRequested = React.useCallback(() => { - dispatch({ propName: 'paused', propValue: true }); + dispatch({ type: 'setProp', propName: 'paused', propValue: true }); }, []); const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []); const onMuteRequested = React.useCallback(() => { - dispatch({ propName: 'muted', propValue: true }); + dispatch({ type: 'setProp', propName: 'muted', propValue: true }); }, []); const onUnmuteRequested = React.useCallback(() => { - dispatch({ propName: 'muted', propValue: false }); + dispatch({ type: 'setProp', propName: 'muted', propValue: false }); }, []); const onVolumeChangeRequested = React.useCallback((volume) => { - dispatch({ propName: 'volume', propValue: volume }); + dispatch({ type: 'setProp', propName: 'volume', propValue: volume }); }, []); const onSeekRequested = React.useCallback((time) => { - dispatch({ propName: 'time', propValue: time }); + dispatch({ type: 'setProp', propName: 'time', propValue: time }); }, []); const onSubtitlesTrackSelected = React.useCallback((trackId) => { - dispatch({ propName: 'selectedSubtitlesTrackId', propValue: trackId }); + dispatch({ type: 'setProp', propName: 'selectedSubtitlesTrackId', propValue: trackId }); }, []); const onSubtitlesDelayChanged = React.useCallback((delay) => { - dispatch({ propName: 'subtitlesDelay', propValue: delay }); + dispatch({ type: 'setProp', propName: 'subtitlesDelay', propValue: delay }); }, []); const onSubtitlesSizeChanged = React.useCallback((size) => { updateSettings({ subtitles_size: size }); @@ -177,9 +177,10 @@ const Player = ({ urlParams }) => { useDeepEqualEffect(() => { setError(null); if (player.selected === null) { - dispatch({ commandName: 'stop' }); + dispatch({ type: 'command', commandName: 'unload' }); } else { dispatch({ + type: 'command', commandName: 'load', commandArgs: { stream: player.selected.stream, @@ -193,6 +194,7 @@ const Player = ({ urlParams }) => { }); if (Array.isArray(player.selected.stream.subtitles)) { dispatch({ + type: 'command', commandName: 'addSubtitlesTracks', commandArgs: { tracks: player.selected.stream.subtitles.map(({ url, lang }) => ({ @@ -207,6 +209,7 @@ const Player = ({ urlParams }) => { }, [player.selected]); useDeepEqualEffect(() => { dispatch({ + type: 'command', commandName: 'addSubtitlesTracks', commandArgs: { tracks: player.subtitles_resources @@ -222,19 +225,19 @@ const Player = ({ urlParams }) => { }); }, [player.selected, player.subtitles_resources]); React.useEffect(() => { - dispatch({ propName: 'subtitlesSize', propValue: settings.subtitles_size }); + dispatch({ type: 'setProp', propName: 'subtitlesSize', propValue: settings.subtitles_size }); }, [settings.subtitles_size]); React.useEffect(() => { - dispatch({ propName: 'subtitlesTextColor', propValue: settings.subtitles_text_color }); + dispatch({ type: 'setProp', propName: 'subtitlesTextColor', propValue: settings.subtitles_text_color }); }, [settings.subtitles_text_color]); React.useEffect(() => { - dispatch({ propName: 'subtitlesBackgroundColor', propValue: settings.subtitles_background_color }); + dispatch({ type: 'setProp', propName: 'subtitlesBackgroundColor', propValue: settings.subtitles_background_color }); }, [settings.subtitles_background_color]); React.useEffect(() => { - dispatch({ propName: 'subtitlesOutlineColor', propValue: settings.subtitles_outline_color }); + dispatch({ type: 'setProp', propName: 'subtitlesOutlineColor', propValue: settings.subtitles_outline_color }); }, [settings.subtitles_outline_color]); React.useEffect(() => { - dispatch({ propName: 'subtitlesOffset', propValue: settings.subtitles_offset }); + dispatch({ type: 'setProp', propName: 'subtitlesOffset', propValue: settings.subtitles_offset }); }, [settings.subtitles_offset]); React.useEffect(() => { if (videoState.time !== null && !isNaN(videoState.time) && videoState.duration !== null && !isNaN(videoState.duration)) { From 0e64751ac72554253dec129e2803db82e412ea44 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Thu, 30 Apr 2020 13:38:13 +0300 Subject: [PATCH 07/72] video element styles updated --- src/routes/Player/Video/Video.js | 10 +++++++--- src/routes/Player/Video/styles.less | 10 ++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 src/routes/Player/Video/styles.less diff --git a/src/routes/Player/Video/Video.js b/src/routes/Player/Video/Video.js index a0ff9f1a1..30aa1f76d 100644 --- a/src/routes/Player/Video/Video.js +++ b/src/routes/Player/Video/Video.js @@ -2,8 +2,10 @@ const React = require('react'); const PropTypes = require('prop-types'); +const classnames = require('classnames'); const { useLiveRef } = require('stremio/common'); const selectVideoImplementation = require('./selectVideoImplementation'); +const styles = require('./styles'); const Video = React.forwardRef(({ className, ...props }, ref) => { const onEndedRef = useLiveRef(props.onEnded); @@ -12,7 +14,7 @@ const Video = React.forwardRef(({ className, ...props }, ref) => { const onPropChangedRef = useLiveRef(props.onPropChanged); const onSubtitlesTrackLoadedRef = useLiveRef(props.onSubtitlesTrackLoaded); const onImplementationChangedRef = useLiveRef(props.onImplementationChanged); - const containerElementRef = React.useRef(null); + const videoElementRef = React.useRef(null); const videoRef = React.useRef(null); const dispatch = React.useCallback((args) => { if (args && args.commandName === 'load' && args.commandArgs) { @@ -22,7 +24,7 @@ const Video = React.forwardRef(({ className, ...props }, ref) => { } else if (videoRef.current === null || videoRef.current.constructor !== Video) { dispatch({ commandName: 'destroy' }); videoRef.current = new Video({ - containerElement: containerElementRef.current, + containerElement: videoElementRef.current, shell: args.commandArgs.shell }); videoRef.current.on('ended', () => { @@ -72,7 +74,9 @@ const Video = React.forwardRef(({ className, ...props }, ref) => { }; }, []); return ( -
+
+
+
); }); diff --git a/src/routes/Player/Video/styles.less b/src/routes/Player/Video/styles.less new file mode 100644 index 000000000..a88cc35d1 --- /dev/null +++ b/src/routes/Player/Video/styles.less @@ -0,0 +1,10 @@ +.video-container { + .video { + width: 100%; + height: 100%; + + * { + font-size: inherit; + } + } +} \ No newline at end of file From 94bef3e0646adaafba11b06c0c150b34b7882c0e Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Thu, 30 Apr 2020 13:38:35 +0300 Subject: [PATCH 08/72] streamingServerURL case fixed --- src/routes/Player/Player.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index a558a63f9..5053ed7ab 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -184,7 +184,7 @@ const Player = ({ urlParams }) => { commandName: 'load', commandArgs: { stream: player.selected.stream, - streamingServerUrl: settings.streaming_server_url, + streamingServerURL: settings.streaming_server_url, autoplay: true, time: player.lib_item !== null && player.selected.video_id !== null && player.lib_item.state.video_id === player.selected.video_id ? player.lib_item.state.timeOffset From 254e34b01b43e6f71b5b79deed4588aaa81d7ef9 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Thu, 30 Apr 2020 13:47:03 +0300 Subject: [PATCH 09/72] selectVideoImplementation updated --- .../Player/Video/selectVideoImplementation.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/routes/Player/Video/selectVideoImplementation.js b/src/routes/Player/Video/selectVideoImplementation.js index c84586a10..787b5378d 100644 --- a/src/routes/Player/Video/selectVideoImplementation.js +++ b/src/routes/Player/Video/selectVideoImplementation.js @@ -1,19 +1,21 @@ // Copyright (C) 2017-2020 Smart code 203358507 -const { HTMLVideo, YouTubeVideo, MPVVideo, withStreamingServer } = require('stremio-video'); +const { HTMLVideo, YouTubeVideo, MPVVideo, withStreamingServer, withHTMLSubtitles } = require('stremio-video'); const selectVideoImplementation = (shell, stream) => { + // TODO handle stream.behaviorHints + // TODO handle IFrameVideo + // TODO handle ChromecastVideo + if (shell) { - return MPVVideo; + return withHTMLSubtitles(withStreamingServer(MPVVideo)); } if (stream) { - if (typeof stream.url === 'string') { - return HTMLVideo; - } else if (typeof stream.ytId === 'string') { - return YouTubeVideo; - } else if (typeof stream.infoHash === 'string') { - return withStreamingServer(HTMLVideo); + if (typeof stream.ytId === 'string') { + return withHTMLSubtitles(YouTubeVideo); + } else if (typeof stream.url === 'string' || typeof stream.infoHash === 'string') { + return withHTMLSubtitles(withStreamingServer(HTMLVideo)); } } From cd1ee34418d25bdadbebd228caf74eb712b5e35c Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Mon, 11 May 2020 16:48:00 +0300 Subject: [PATCH 10/72] clean everything in build dir before new build --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 8dfa4b8c7..71fbae7e8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -152,7 +152,7 @@ module.exports = (env, argv) => ({ new MiniCssExtractPlugin(), new CleanWebpackPlugin({ verbose: true, - cleanOnceBeforeBuildPatterns: [], + cleanOnceBeforeBuildPatterns: ['*'], cleanAfterEveryBuildPatterns: ['./main.js', './main.css'] }) ] From a141ad2d9a5acdf833b71b81b97a223fe04ec6b9 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Tue, 12 May 2020 11:00:17 +0300 Subject: [PATCH 11/72] cast and chrome added to eslint globals --- .eslintrc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 0f7f26836..c2d000b22 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,7 +10,9 @@ }, "globals": { "YT": "readonly", - "FB": "readonly" + "FB": "readonly", + "cast": "readonly", + "chrome": "readonly" }, "env": { "node": true, From 8b40d015f572a9e4b2c6dca4dfb5c2838766b4e4 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Tue, 12 May 2020 15:42:46 +0300 Subject: [PATCH 12/72] basic chromecast service implemented --- src/services/Chromecast/Chromecast.js | 133 ++++++++++++++++++++++++++ src/services/Chromecast/index.js | 5 + src/services/index.js | 2 + 3 files changed, 140 insertions(+) create mode 100644 src/services/Chromecast/Chromecast.js create mode 100644 src/services/Chromecast/index.js diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js new file mode 100644 index 000000000..1e3832710 --- /dev/null +++ b/src/services/Chromecast/Chromecast.js @@ -0,0 +1,133 @@ +// Copyright (C) 2017-2020 Smart code 203358507 + +const EventEmitter = require('events'); + +const RECEIVER_APPLICATION_ID = '1634F54B'; +const CUSTOM_MESSAGE_NAMESPACE = 'urn:x-cast:com.stremio'; + +let castAPIAvailable = null; +const castAPIEvents = new EventEmitter(); +window['__onGCastApiAvailable'] = function(available) { + delete window['__onGCastApiAvailable']; + castAPIAvailable = available; + castAPIEvents.emit('availabilityChanged'); +}; + +function Chromecast() { + let active = false; + let error = null; + let starting = false; + + const events = new EventEmitter(); + events.on('error', () => { }); + + function onCastAPIAvailabilityChange() { + if (castAPIAvailable) { + active = true; + error = null; + } else { + active = false; + error = new Error('Google Cast API not available'); + } + + starting = false; + onStateChanged(); + } + function onStateChanged() { + if (active) { + const context = cast.framework.CastContext.getInstance(); + context.setOptions({ + autoJoinPolicy: chrome.cast.AutoJoinPolicy.PAGE_SCOPED, + // TODO language: '' + receiverApplicationId: RECEIVER_APPLICATION_ID, + resumeSavedSession: false + }); + // context.addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, onSessionStateChange); + } else if (castAPIAvailable) { + const context = cast.framework.CastContext.getInstance(); + context.setOptions({ + autoJoinPolicy: chrome.cast.AutoJoinPolicy.PAGE_SCOPED, + // TODO language: '' + receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, + resumeSavedSession: false + }); + // context.removeEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, onSessionStateChange); + // context.removeEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, onSessionStateChange); + } + + events.emit('stateChanged'); + } + function start() { + if (active || error instanceof Error || starting) { + return; + } + + starting = true; + if (castAPIAvailable !== null) { + onCastAPIAvailabilityChange(); + } else { + castAPIEvents.on('availabilityChanged', onCastAPIAvailabilityChange); + } + } + function stop() { + castAPIEvents.off('availabilityChanged', onCastAPIAvailabilityChange); + active = false; + error = null; + starting = false; + onStateChanged(); + } + function on(name, listener) { + events.on(name, listener); + } + function off(name, listener) { + events.off(name, listener); + } + function dispatch(action) { + if (!active || !action) { + return; + } + + switch (action.type) { + case 'message': { + const castSession = cast.framework.CastContext.getInstance().getCurrentSession(); + if (castSession) { + castSession.sendMessage(CUSTOM_MESSAGE_NAMESPACE, action.message); + } + + return; + } + } + } + + Object.defineProperties(this, { + active: { + configurable: false, + enumerable: true, + get: function() { + return active; + } + }, + error: { + configurable: false, + enumerable: true, + get: function() { + return error; + } + }, + starting: { + configurable: false, + enumerable: true, + get: function() { + return starting; + } + } + }); + + this.start = start; + this.stop = stop; + this.on = on; + this.off = off; + this.dispatch = dispatch; +} + +module.exports = Chromecast; diff --git a/src/services/Chromecast/index.js b/src/services/Chromecast/index.js new file mode 100644 index 000000000..a4d3d8e8f --- /dev/null +++ b/src/services/Chromecast/index.js @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2020 Smart code 203358507 + +const Chromecast = require('./Chromecast'); + +module.exports = Chromecast; diff --git a/src/services/index.js b/src/services/index.js index 726939d0f..3c4c75556 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -1,11 +1,13 @@ // Copyright (C) 2017-2020 Smart code 203358507 +const Chromecast = require('./Chromecast'); const Core = require('./Core'); const KeyboardNavigation = require('./KeyboardNavigation'); const { ServicesProvider, useServices } = require('./ServicesContext'); const Shell = require('./Shell'); module.exports = { + Chromecast, Core, KeyboardNavigation, ServicesProvider, From 215bd791448c81a023081b273d60db49bb936611 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Tue, 12 May 2020 15:43:17 +0300 Subject: [PATCH 13/72] chromecast service started in App --- src/App/App.js | 52 +++++++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/App/App.js b/src/App/App.js index fc38da179..27219c1e6 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -3,7 +3,7 @@ require('spatial-navigation-polyfill'); const React = require('react'); const { Router } = require('stremio-router'); -const { Core, KeyboardNavigation, ServicesProvider, Shell } = require('stremio/services'); +const { Core, Shell, Chromecast, KeyboardNavigation, ServicesProvider } = require('stremio/services'); const { NotFound } = require('stremio/routes'); const { ToastProvider } = require('stremio/common'); const CoreEventsToaster = require('./CoreEventsToaster'); @@ -15,47 +15,55 @@ const App = () => { return NotFound; }, []); const services = React.useMemo(() => ({ - keyboardNavigation: new KeyboardNavigation(), + core: new Core(), shell: new Shell(), - core: new Core() + chromecast: new Chromecast(), + keyboardNavigation: new KeyboardNavigation() }), []); - const [shellInitialized, setShellInitialized] = React.useState(false); const [coreInitialized, setCoreInitialized] = React.useState(false); + const [shellInitialized, setShellInitialized] = React.useState(false); + const [chromecastInitialized, setChromecastInitialized] = React.useState(false); React.useEffect(() => { + const onCoreStateChanged = () => { + services.core.dispatch({ + action: 'Load', + args: { + model: 'Ctx' + } + }); + setCoreInitialized(services.core.active); + }; const onShellStateChanged = () => { setShellInitialized(services.shell.active || services.shell.error instanceof Error); }; - const onCoreStateChanged = () => { - if (services.core.active) { - services.core.dispatch({ - action: 'Load', - args: { - model: 'Ctx' - } - }); - } - setCoreInitialized(services.core.active); + const onChromecastStateChange = () => { + setChromecastInitialized(services.chromecast.active || services.chromecast.error instanceof Error); }; - services.shell.on('stateChanged', onShellStateChanged); services.core.on('stateChanged', onCoreStateChanged); - services.keyboardNavigation.start(); - services.shell.start(); + services.shell.on('stateChanged', onShellStateChanged); + services.chromecast.on('stateChanged', onChromecastStateChange); services.core.start(); - window.shell = services.shell; + services.shell.start(); + services.chromecast.start(); + services.keyboardNavigation.start(); window.core = services.core; + window.shell = services.shell; + window.services = services; return () => { - services.keyboardNavigation.stop(); - services.shell.stop(); services.core.stop(); - services.shell.off('stateChanged', onShellStateChanged); + services.shell.stop(); + services.chromecast.stop(); + services.keyboardNavigation.stop(); services.core.off('stateChanged', onCoreStateChanged); + services.shell.off('stateChanged', onShellStateChanged); + services.chromecast.off('stateChanged', onChromecastStateChange); }; }, []); return ( { - shellInitialized && coreInitialized ? + coreInitialized && shellInitialized && chromecastInitialized ? Date: Tue, 12 May 2020 15:55:30 +0300 Subject: [PATCH 14/72] Shall service api unified with Chromecast --- src/services/Shell/Shell.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/services/Shell/Shell.js b/src/services/Shell/Shell.js index fde67cde4..549eb35cb 100644 --- a/src/services/Shell/Shell.js +++ b/src/services/Shell/Shell.js @@ -6,6 +6,7 @@ function Shell() { let active = false; let error = null; let starting = false; + const events = new EventEmitter(); events.on('error', () => { }); @@ -17,12 +18,10 @@ function Shell() { return; } - starting = true; - setTimeout(() => { - error = new Error('Unable to init stremio shell'); - starting = false; - onStateChanged(); - }); + active = false; + error = new Error('Stremio shell not available'); + starting = false; + onStateChanged(); } function stop() { active = false; @@ -58,6 +57,13 @@ function Shell() { get: function() { return error; } + }, + starting: { + configurable: false, + enumerable: true, + get: function() { + return starting; + } } }); @@ -66,8 +72,6 @@ function Shell() { this.on = on; this.off = off; this.dispatch = dispatch; - - Object.freeze(this); } module.exports = Shell; From 92ad1168112ef7ea754ff63e9ddeaa22522a82ee Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Tue, 12 May 2020 16:41:19 +0300 Subject: [PATCH 15/72] Core service api updated to be consistent with other services --- src/services/Core/Core.js | 66 +++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/src/services/Core/Core.js b/src/services/Core/Core.js index 5be0dd337..fc7d14ac0 100644 --- a/src/services/Core/Core.js +++ b/src/services/Core/Core.js @@ -1,13 +1,14 @@ // Copyright (C) 2017-2020 Smart code 203358507 const EventEmitter = require('events'); -const { default: init, StremioCoreWeb } = require('@stremio/stremio-core-web'); +const { default: initialize, StremioCoreWeb } = require('@stremio/stremio-core-web'); function Core() { let active = false; let error = null; let starting = false; - let stremio_core = null; + let core = null; + const events = new EventEmitter(); events.on('error', () => { }); @@ -20,37 +21,37 @@ function Core() { } starting = true; - init() - .then(() => { - if (starting) { - stremio_core = new StremioCoreWeb(({ name, args } = {}) => { - if (active) { - try { - events.emit(name, args); - } catch (e) { - /* eslint-disable-next-line no-console */ - console.error(e); - } + initialize().then(() => { + if (starting) { + core = new StremioCoreWeb(({ name, args } = {}) => { + if (active) { + try { + events.emit(name, args); + } catch (e) { + /* eslint-disable-next-line no-console */ + console.error(e); } - }); - active = true; - onStateChanged(); - } - }) - .catch((e) => { - error = new Error('Unable to init stremio-core-web'); - error.error = e; - onStateChanged(); - }) - .then(() => { + } + }); + active = true; + error = null; starting = false; - }); + onStateChanged(); + } + }).catch((error) => { + core = null; + active = false; + error = new Error('Unable to init stremio-core-web'); + error.error = error; + starting = false; + onStateChanged(); + }); } function stop() { + core = null; active = false; error = null; starting = false; - stremio_core = null; onStateChanged(); } function on(name, listener) { @@ -64,14 +65,14 @@ function Core() { return false; } - return stremio_core.dispatch(action, model); + return core.dispatch(action, model); } function getState(model) { if (!active) { return null; } - return stremio_core.get_state(model); + return core.get_state(model); } Object.defineProperties(this, { @@ -88,6 +89,13 @@ function Core() { get: function() { return error; } + }, + starting: { + configurable: false, + enumerable: true, + get: function() { + return starting; + } } }); @@ -97,8 +105,6 @@ function Core() { this.off = off; this.dispatch = dispatch; this.getState = getState; - - Object.freeze(this); } module.exports = Core; From 3966f0ada6ea91f4cc464c6cb4e5f0f7733b4727 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Tue, 12 May 2020 16:50:23 +0300 Subject: [PATCH 16/72] load cast sender sdk --- src/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.html b/src/index.html index 67b70ad11..2df264a6f 100755 --- a/src/index.html +++ b/src/index.html @@ -16,6 +16,7 @@ + \ No newline at end of file From 32a0767a17e36f734b8193eb4fc31258d5809d86 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Tue, 12 May 2020 16:52:44 +0300 Subject: [PATCH 17/72] stremio video added to deps --- package.json | 3 ++- yarn.lock | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ebfe90a9e..bc117cfc9 100755 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "react-focus-lock": "2.2.1", "spatial-navigation-polyfill": "git+https://git@github.com/Stremio/spatial-navigation.git#40204ad9942fe786794c62f99ea5ab2b52b24096", "stremio-colors": "git+https://git@github.com/Stremio/stremio-colors.git#v3.0.0", - "stremio-icons": "git+https://git@github.com/Stremio/stremio-icons.git#v2.0.2" + "stremio-icons": "git+https://git@github.com/Stremio/stremio-icons.git#v2.0.2", + "stremio-video": "git+ssh://git@github.com/Stremio/stremio-video.git#8d18b9d92ca950e09918713dbaca0e0386e714c7" }, "devDependencies": { "@babel/core": "7.8.7", diff --git a/yarn.lock b/yarn.lock index e1a30fe31..4a3afb9e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7643,6 +7643,14 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +magnet-uri@5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/magnet-uri/-/magnet-uri-5.2.4.tgz#7afe5b736af04445aff744c93a890a3710077688" + integrity sha512-VYaJMxhr8B9BrCiNINUsuhaEe40YnG+AQBwcqUKO66lSVaI9I3A1iH/6EmEwRI8OYUg5Gt+4lLE7achg676lrg== + dependencies: + thirty-two "^1.0.1" + uniq "^1.0.1" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -10737,6 +10745,16 @@ stream-shift@^1.0.0: version "2.0.2" resolved "git+https://git@github.com/Stremio/stremio-icons.git#923bbcd5bedcbc3a10d0ae96a3d35d2638bf0ae9" +"stremio-video@git+ssh://git@github.com/Stremio/stremio-video.git#8d18b9d92ca950e09918713dbaca0e0386e714c7": + version "0.0.1" + resolved "git+ssh://git@github.com/Stremio/stremio-video.git#8d18b9d92ca950e09918713dbaca0e0386e714c7" + dependencies: + events "1.1.1" + magnet-uri "5.2.4" + url "0.11.0" + video-name-parser "1.4.6" + vtt.js "0.13.0" + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -11056,6 +11074,11 @@ text-table@0.2.0, text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +thirty-two@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a" + integrity sha1-TKL//AKlEpDSdEueP1V2k8prYno= + throat@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" @@ -11382,7 +11405,7 @@ url-parse@^1.4.3: querystringify "^2.1.1" requires-port "^1.0.0" -url@^0.11.0: +url@0.11.0, url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= @@ -11497,11 +11520,21 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +video-name-parser@1.4.6: + version "1.4.6" + resolved "https://registry.yarnpkg.com/video-name-parser/-/video-name-parser-1.4.6.tgz#8e7926ab2ba9253fed290b399e453d3b0702c687" + integrity sha512-ZdeYjh8X4ms1EzjY/UoiTZ6JWbi8SYyOPGY0jESSLq2BAmdc5sZHi+F8J19Qz0y7H1WSpaltojsCkO1p2dH4YA== + vm-browserify@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vtt.js@0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/vtt.js/-/vtt.js-0.13.0.tgz#955c667b34d5325b2012cb9e8ba9bad6e0b11ff8" + integrity sha1-lVxmezTVMlsgEsuei6m61uCxH/g= + w3c-hr-time@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" From c0d5c5a31464fcf8609a3612793e1691a8fdcf95 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Tue, 12 May 2020 17:02:29 +0300 Subject: [PATCH 18/72] remove not needed Object.freeze --- src/services/KeyboardNavigation/KeyboardNavigation.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/services/KeyboardNavigation/KeyboardNavigation.js b/src/services/KeyboardNavigation/KeyboardNavigation.js index bebcb6f90..2225ce185 100644 --- a/src/services/KeyboardNavigation/KeyboardNavigation.js +++ b/src/services/KeyboardNavigation/KeyboardNavigation.js @@ -86,8 +86,6 @@ function KeyboardNavigation() { this.start = start; this.stop = stop; - - Object.freeze(this); } module.exports = KeyboardNavigation; From 1c78265d862b20d3529b979efec19b857bd3781f Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Wed, 13 May 2020 13:01:28 +0300 Subject: [PATCH 19/72] availability changed handler name fixed --- src/services/Chromecast/Chromecast.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js index 1e3832710..7bbe20ad4 100644 --- a/src/services/Chromecast/Chromecast.js +++ b/src/services/Chromecast/Chromecast.js @@ -21,7 +21,7 @@ function Chromecast() { const events = new EventEmitter(); events.on('error', () => { }); - function onCastAPIAvailabilityChange() { + function onCastAPIAvailabilityChanged() { if (castAPIAvailable) { active = true; error = null; @@ -42,7 +42,7 @@ function Chromecast() { receiverApplicationId: RECEIVER_APPLICATION_ID, resumeSavedSession: false }); - // context.addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, onSessionStateChange); + // context.addEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, onCastStateChanged); } else if (castAPIAvailable) { const context = cast.framework.CastContext.getInstance(); context.setOptions({ @@ -51,8 +51,7 @@ function Chromecast() { receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, resumeSavedSession: false }); - // context.removeEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, onSessionStateChange); - // context.removeEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, onSessionStateChange); + // context.removeEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, onCastStateChanged); } events.emit('stateChanged'); @@ -64,13 +63,13 @@ function Chromecast() { starting = true; if (castAPIAvailable !== null) { - onCastAPIAvailabilityChange(); + onCastAPIAvailabilityChanged(); } else { - castAPIEvents.on('availabilityChanged', onCastAPIAvailabilityChange); + castAPIEvents.on('availabilityChanged', onCastAPIAvailabilityChanged); } } function stop() { - castAPIEvents.off('availabilityChanged', onCastAPIAvailabilityChange); + castAPIEvents.off('availabilityChanged', onCastAPIAvailabilityChanged); active = false; error = null; starting = false; From 47009fd45d13fbfcb9b2862b68a02fc9a2d6a7c9 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Wed, 13 May 2020 13:20:03 +0300 Subject: [PATCH 20/72] sesstion state handler added --- src/services/Chromecast/Chromecast.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js index 7bbe20ad4..fd1f1fe19 100644 --- a/src/services/Chromecast/Chromecast.js +++ b/src/services/Chromecast/Chromecast.js @@ -33,6 +33,10 @@ function Chromecast() { starting = false; onStateChanged(); } + function onCastStateChanged(event) { + } + function onSesstionStateChanged(event) { + } function onStateChanged() { if (active) { const context = cast.framework.CastContext.getInstance(); @@ -42,7 +46,8 @@ function Chromecast() { receiverApplicationId: RECEIVER_APPLICATION_ID, resumeSavedSession: false }); - // context.addEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, onCastStateChanged); + context.addEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, onCastStateChanged); + context.addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, onSesstionStateChanged); } else if (castAPIAvailable) { const context = cast.framework.CastContext.getInstance(); context.setOptions({ @@ -51,7 +56,8 @@ function Chromecast() { receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, resumeSavedSession: false }); - // context.removeEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, onCastStateChanged); + context.removeEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, onCastStateChanged); + context.removeEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, onSesstionStateChanged); } events.emit('stateChanged'); From 5d87e7783b97e5123215fe2937ca18943cbc9bb0 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Wed, 13 May 2020 14:01:55 +0300 Subject: [PATCH 21/72] handle castState in chromecast service --- src/services/Chromecast/Chromecast.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js index fd1f1fe19..2d72a01d1 100644 --- a/src/services/Chromecast/Chromecast.js +++ b/src/services/Chromecast/Chromecast.js @@ -3,7 +3,7 @@ const EventEmitter = require('events'); const RECEIVER_APPLICATION_ID = '1634F54B'; -const CUSTOM_MESSAGE_NAMESPACE = 'urn:x-cast:com.stremio'; +const MESSAGE_NAMESPACE = 'urn:x-cast:com.stremio'; let castAPIAvailable = null; const castAPIEvents = new EventEmitter(); @@ -17,23 +17,30 @@ function Chromecast() { let active = false; let error = null; let starting = false; + let castState = null; const events = new EventEmitter(); events.on('error', () => { }); function onCastAPIAvailabilityChanged() { if (castAPIAvailable) { + const context = cast.framework.CastContext.getInstance(); active = true; error = null; + starting = false; + castState = context.getCastState(); } else { active = false; error = new Error('Google Cast API not available'); + starting = false; + castState = null; } - starting = false; onStateChanged(); } function onCastStateChanged(event) { + castState = event.castState; + events.emit(cast.framework.CastContextEventType.CAST_STATE_CHANGED); } function onSesstionStateChanged(event) { } @@ -72,6 +79,7 @@ function Chromecast() { onCastAPIAvailabilityChanged(); } else { castAPIEvents.on('availabilityChanged', onCastAPIAvailabilityChanged); + onStateChanged(); } } function stop() { @@ -79,6 +87,7 @@ function Chromecast() { active = false; error = null; starting = false; + castState = null; onStateChanged(); } function on(name, listener) { @@ -96,7 +105,7 @@ function Chromecast() { case 'message': { const castSession = cast.framework.CastContext.getInstance().getCurrentSession(); if (castSession) { - castSession.sendMessage(CUSTOM_MESSAGE_NAMESPACE, action.message); + castSession.sendMessage(MESSAGE_NAMESPACE, action.message); } return; @@ -125,6 +134,13 @@ function Chromecast() { get: function() { return starting; } + }, + castState: { + configurable: false, + enumerable: true, + get: function() { + return castState; + } } }); From 7a806d930a8e7024502cba54164790679c4b4e17 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Wed, 13 May 2020 16:58:37 +0300 Subject: [PATCH 22/72] remove castState variable from Chromecast service state --- src/services/Chromecast/Chromecast.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js index 2d72a01d1..fadc7a916 100644 --- a/src/services/Chromecast/Chromecast.js +++ b/src/services/Chromecast/Chromecast.js @@ -17,29 +17,24 @@ function Chromecast() { let active = false; let error = null; let starting = false; - let castState = null; const events = new EventEmitter(); events.on('error', () => { }); function onCastAPIAvailabilityChanged() { if (castAPIAvailable) { - const context = cast.framework.CastContext.getInstance(); active = true; error = null; starting = false; - castState = context.getCastState(); } else { active = false; error = new Error('Google Cast API not available'); starting = false; - castState = null; } onStateChanged(); } - function onCastStateChanged(event) { - castState = event.castState; + function onCastStateChanged() { events.emit(cast.framework.CastContextEventType.CAST_STATE_CHANGED); } function onSesstionStateChanged(event) { @@ -87,7 +82,6 @@ function Chromecast() { active = false; error = null; starting = false; - castState = null; onStateChanged(); } function on(name, listener) { @@ -139,7 +133,11 @@ function Chromecast() { configurable: false, enumerable: true, get: function() { - return castState; + if (!castAPIAvailable) { + return null; + } + + return cast.framework.CastContext.getInstance().getCastState(); } } }); From 1bb77baade14ee38ef52ed7799f6cdbfb1353001 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Wed, 13 May 2020 17:04:29 +0300 Subject: [PATCH 23/72] cast session prop added --- src/services/Chromecast/Chromecast.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js index fadc7a916..5984bb99d 100644 --- a/src/services/Chromecast/Chromecast.js +++ b/src/services/Chromecast/Chromecast.js @@ -37,7 +37,8 @@ function Chromecast() { function onCastStateChanged() { events.emit(cast.framework.CastContextEventType.CAST_STATE_CHANGED); } - function onSesstionStateChanged(event) { + function onSesstionStateChanged() { + events.emit(cast.framework.CastContextEventType.SESSION_STATE_CHANGED); } function onStateChanged() { if (active) { @@ -139,6 +140,17 @@ function Chromecast() { return cast.framework.CastContext.getInstance().getCastState(); } + }, + castSession: { + configurable: false, + enumerable: true, + get: function() { + if (!castAPIAvailable) { + return null; + } + + return cast.framework.CastContext.getInstance().getCurrentSession(); + } } }); From 53d3f61d9f6b9b26dfe911e72ed5addb45afedf7 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Wed, 13 May 2020 17:36:11 +0300 Subject: [PATCH 24/72] castSessionState prop added to Chromecast service --- src/services/Chromecast/Chromecast.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js index 5984bb99d..d3a45bb58 100644 --- a/src/services/Chromecast/Chromecast.js +++ b/src/services/Chromecast/Chromecast.js @@ -141,6 +141,17 @@ function Chromecast() { return cast.framework.CastContext.getInstance().getCastState(); } }, + castSessionState: { + configurable: false, + enumerable: true, + get: function() { + if (!castAPIAvailable) { + return null; + } + + return cast.framework.CastContext.getInstance().getSessionState(); + } + }, castSession: { configurable: false, enumerable: true, From 8aaea9010803c3b891bafd52a00629a4f24617ae Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Wed, 13 May 2020 17:41:09 +0300 Subject: [PATCH 25/72] add/remove message listener on cast session --- src/services/Chromecast/Chromecast.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js index d3a45bb58..5f4130c32 100644 --- a/src/services/Chromecast/Chromecast.js +++ b/src/services/Chromecast/Chromecast.js @@ -37,7 +37,21 @@ function Chromecast() { function onCastStateChanged() { events.emit(cast.framework.CastContextEventType.CAST_STATE_CHANGED); } - function onSesstionStateChanged() { + function onMessageReceived(event) { + + } + function onSesstionStateChanged(event) { + switch (event.sessionState) { + case cast.framework.SessionState.SESSION_STARTED: { + event.session.addMessageListener(MESSAGE_NAMESPACE, onMessageReceived); + break; + } + case cast.framework.SessionState.SESSION_ENDING: { + event.session.removeMessageListener(MESSAGE_NAMESPACE, onMessageReceived); + break; + } + } + events.emit(cast.framework.CastContextEventType.SESSION_STATE_CHANGED); } function onStateChanged() { From 7e223b84f0d6776b7030fab50cf5b0ec37217073 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Wed, 13 May 2020 18:21:07 +0300 Subject: [PATCH 26/72] handle session start failed --- src/services/Chromecast/Chromecast.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js index 5f4130c32..6e52dece8 100644 --- a/src/services/Chromecast/Chromecast.js +++ b/src/services/Chromecast/Chromecast.js @@ -50,6 +50,10 @@ function Chromecast() { event.session.removeMessageListener(MESSAGE_NAMESPACE, onMessageReceived); break; } + case cast.framework.SessionState.SESSION_START_FAILED: { + events.emit('error', { code: event.errorCode }); + break; + } } events.emit(cast.framework.CastContextEventType.SESSION_STATE_CHANGED); From 9b3ce2f31c162fb84b93277fa310d01be50e0232 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Thu, 14 May 2020 11:19:27 +0300 Subject: [PATCH 27/72] onMessageReceived implemented --- src/services/Chromecast/Chromecast.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js index 6e52dece8..d0d1052fd 100644 --- a/src/services/Chromecast/Chromecast.js +++ b/src/services/Chromecast/Chromecast.js @@ -37,8 +37,10 @@ function Chromecast() { function onCastStateChanged() { events.emit(cast.framework.CastContextEventType.CAST_STATE_CHANGED); } - function onMessageReceived(event) { - + function onMessageReceived(_, message) { + try { + events.emit('message', JSON.parse(message)); + } catch (error) { } } function onSesstionStateChanged(event) { switch (event.sessionState) { From 04909cb6ee75db75d210cc6cc3febd0c5937d412 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Thu, 14 May 2020 12:22:23 +0300 Subject: [PATCH 28/72] errors handled in chromecast service --- src/services/Chromecast/Chromecast.js | 110 +++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js index d0d1052fd..a2e30ce3b 100644 --- a/src/services/Chromecast/Chromecast.js +++ b/src/services/Chromecast/Chromecast.js @@ -13,6 +13,57 @@ window['__onGCastApiAvailable'] = function(available) { castAPIEvents.emit('availabilityChanged'); }; +const CAST_ERROR = { + CANCEL: { + code: 1, + message: 'The operation was canceled by the user' + }, + TIMEOUT: { + code: 2, + message: 'The operation timed out' + }, + API_NOT_INITIALIZED: { + code: 3, + message: 'The API is not initialized' + }, + INVALID_PARAMETER: { + code: 4, + message: 'The parameters to the operation were not valid' + }, + EXTENSION_NOT_COMPATIBLE: { + code: 5, + message: 'The API script is not compatible with the installed Cast extension' + }, + EXTENSION_MISSING: { + code: 6, + message: 'The Cast extension is not available' + }, + RECEIVER_UNAVAILABLE: { + code: 7, + message: 'No receiver was compatible with the session request' + }, + SESSION_ERROR: { + code: 8, + message: 'A session could not be created, or a session was invalid' + }, + CHANNEL_ERROR: { + code: 9, + message: 'A channel to the receiver is not available' + }, + LOAD_MEDIA_FAILED: { + code: 10, + message: 'Load media failed' + }, + INVALID_MESSAGE: { + code: 11, + message: 'Invalid message received' + }, + UNKNOWN: { + code: 100, + message: 'Unknown error' + } +}; + function Chromecast() { let active = false; let error = null; @@ -37,10 +88,65 @@ function Chromecast() { function onCastStateChanged() { events.emit(cast.framework.CastContextEventType.CAST_STATE_CHANGED); } + function onCastError(code) { + switch (code) { + case chrome.cast.ErrorCode.CANCEL: { + events.emit('error', CAST_ERROR.CANCEL); + break; + } + case chrome.cast.ErrorCode.TIMEOUT: { + events.emit('error', CAST_ERROR.TIMEOUT); + break; + } + case chrome.cast.ErrorCode.API_NOT_INITIALIZED: { + events.emit('error', CAST_ERROR.API_NOT_INITIALIZED); + break; + } + case chrome.cast.ErrorCode.INVALID_PARAMETER: { + events.emit('error', CAST_ERROR.INVALID_PARAMETER); + break; + } + case chrome.cast.ErrorCode.EXTENSION_NOT_COMPATIBLE: { + events.emit('error', CAST_ERROR.EXTENSION_NOT_COMPATIBLE); + break; + } + case chrome.cast.ErrorCode.EXTENSION_MISSING: { + events.emit('error', CAST_ERROR.EXTENSION_MISSING); + break; + } + case chrome.cast.ErrorCode.RECEIVER_UNAVAILABLE: { + events.emit('error', CAST_ERROR.RECEIVER_UNAVAILABLE); + break; + } + case chrome.cast.ErrorCode.SESSION_ERROR: { + events.emit('error', CAST_ERROR.SESSION_ERROR); + break; + } + case chrome.cast.ErrorCode.CHANNEL_ERROR: { + events.emit('error', CAST_ERROR.CHANNEL_ERROR); + break; + } + case chrome.cast.ErrorCode.LOAD_MEDIA_FAILED: { + events.emit('error', CAST_ERROR.LOAD_MEDIA_FAILED); + break; + } + default: { + events.emit('error', { + ...CAST_ERROR.UNKNOWN, + error: { code } + }); + } + } + } function onMessageReceived(_, message) { try { events.emit('message', JSON.parse(message)); - } catch (error) { } + } catch (error) { + events.emit('error', { + ...CAST_ERROR.INVALID_MESSAGE, + error + }); + } } function onSesstionStateChanged(event) { switch (event.sessionState) { @@ -53,7 +159,7 @@ function Chromecast() { break; } case cast.framework.SessionState.SESSION_START_FAILED: { - events.emit('error', { code: event.errorCode }); + onCastError(event.errorCode); break; } } From 6dcd51d615b50c29b234d01c9764173968c35336 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Thu, 14 May 2020 16:06:54 +0300 Subject: [PATCH 29/72] all session events handled --- src/services/Chromecast/Chromecast.js | 32 +++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js index a2e30ce3b..c1430910c 100644 --- a/src/services/Chromecast/Chromecast.js +++ b/src/services/Chromecast/Chromecast.js @@ -85,8 +85,8 @@ function Chromecast() { onStateChanged(); } - function onCastStateChanged() { - events.emit(cast.framework.CastContextEventType.CAST_STATE_CHANGED); + function onCastStateChanged(event) { + events.emit(cast.framework.CastContextEventType.CAST_STATE_CHANGED, event); } function onCastError(code) { switch (code) { @@ -148,14 +148,40 @@ function Chromecast() { }); } } + function onApplicationStatusChanged(event) { + events.emit(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, event); + } + function onApplicationMetadataChanged(event) { + events.emit(cast.framework.CastSession.APPLICATION_METADATA_CHANGED, event); + } + function onActiveInputStateChanged(event) { + events.emit(cast.framework.CastSession.ACTIVE_INPUT_STATE_CHANGED, event); + } + function onVolumeChanged(event) { + events.emit(cast.framework.CastSession.VOLUME_CHANGED, event); + } + function onMediaSessionChanged(event) { + events.emit(cast.framework.CastSession.MEDIA_SESSION, event); + } function onSesstionStateChanged(event) { + events.emit(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, event); switch (event.sessionState) { case cast.framework.SessionState.SESSION_STARTED: { event.session.addMessageListener(MESSAGE_NAMESPACE, onMessageReceived); + event.session.addEventListener(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, onApplicationStatusChanged); + event.session.addEventListener(cast.framework.CastSession.APPLICATION_METADATA_CHANGED, onApplicationMetadataChanged); + event.session.addEventListener(cast.framework.CastSession.ACTIVE_INPUT_STATE_CHANGED, onActiveInputStateChanged); + event.session.addEventListener(cast.framework.CastSession.VOLUME_CHANGED, onVolumeChanged); + event.session.addEventListener(cast.framework.CastSession.MEDIA_SESSION, onMediaSessionChanged); break; } case cast.framework.SessionState.SESSION_ENDING: { event.session.removeMessageListener(MESSAGE_NAMESPACE, onMessageReceived); + event.session.removeEventListener(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, onApplicationStatusChanged); + event.session.removeEventListener(cast.framework.CastSession.APPLICATION_METADATA_CHANGED, onApplicationMetadataChanged); + event.session.removeEventListener(cast.framework.CastSession.ACTIVE_INPUT_STATE_CHANGED, onActiveInputStateChanged); + event.session.removeEventListener(cast.framework.CastSession.VOLUME_CHANGED, onVolumeChanged); + event.session.removeEventListener(cast.framework.CastSession.MEDIA_SESSION, onMediaSessionChanged); break; } case cast.framework.SessionState.SESSION_START_FAILED: { @@ -163,8 +189,6 @@ function Chromecast() { break; } } - - events.emit(cast.framework.CastContextEventType.SESSION_STATE_CHANGED); } function onStateChanged() { if (active) { From 016ec88402f7df6dc694155a5de3b5a7fe6ef5a7 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Fri, 15 May 2020 14:06:40 +0300 Subject: [PATCH 30/72] chromecast receiver id added to constants --- src/common/CONSTANTS.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/CONSTANTS.js b/src/common/CONSTANTS.js index cc16a76ce..2bbf49f94 100644 --- a/src/common/CONSTANTS.js +++ b/src/common/CONSTANTS.js @@ -1,5 +1,6 @@ // Copyright (C) 2017-2020 Smart code 203358507 +const CHROMECAST_RECEIVER_APP_ID = '1634F54B'; const SUBTITLES_SIZES = [75, 100, 125, 150, 175, 200, 250]; const SUBTITLES_FONTS = ['Roboto', 'Arial', 'Halvetica', 'Times New Roman', 'Verdana', 'Courier', 'Lucida Console', 'sans-serif', 'serif', 'monospace']; const CATALOG_PREVIEW_SIZE = 10; @@ -23,6 +24,7 @@ const TYPE_PRIORITIES = { }; module.exports = { + CHROMECAST_RECEIVER_APP_ID, SUBTITLES_SIZES, SUBTITLES_FONTS, CATALOG_PREVIEW_SIZE, From 8c5b9e27c1da56dea574d6ea497b7e50152da870 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Fri, 15 May 2020 14:48:35 +0300 Subject: [PATCH 31/72] chromecast transport decoupled from chromecast service --- src/services/Chromecast/Chromecast.js | 289 ++---------------- .../Chromecast/ChromecastTransport.js | 204 +++++++++++++ 2 files changed, 237 insertions(+), 256 deletions(-) create mode 100644 src/services/Chromecast/ChromecastTransport.js diff --git a/src/services/Chromecast/Chromecast.js b/src/services/Chromecast/Chromecast.js index c1430910c..1ae25964d 100644 --- a/src/services/Chromecast/Chromecast.js +++ b/src/services/Chromecast/Chromecast.js @@ -1,9 +1,7 @@ // Copyright (C) 2017-2020 Smart code 203358507 const EventEmitter = require('events'); - -const RECEIVER_APPLICATION_ID = '1634F54B'; -const MESSAGE_NAMESPACE = 'urn:x-cast:com.stremio'; +const ChromecastTransport = require('./ChromecastTransport'); let castAPIAvailable = null; const castAPIEvents = new EventEmitter(); @@ -13,61 +11,11 @@ window['__onGCastApiAvailable'] = function(available) { castAPIEvents.emit('availabilityChanged'); }; -const CAST_ERROR = { - CANCEL: { - code: 1, - message: 'The operation was canceled by the user' - }, - TIMEOUT: { - code: 2, - message: 'The operation timed out' - }, - API_NOT_INITIALIZED: { - code: 3, - message: 'The API is not initialized' - }, - INVALID_PARAMETER: { - code: 4, - message: 'The parameters to the operation were not valid' - }, - EXTENSION_NOT_COMPATIBLE: { - code: 5, - message: 'The API script is not compatible with the installed Cast extension' - }, - EXTENSION_MISSING: { - code: 6, - message: 'The Cast extension is not available' - }, - RECEIVER_UNAVAILABLE: { - code: 7, - message: 'No receiver was compatible with the session request' - }, - SESSION_ERROR: { - code: 8, - message: 'A session could not be created, or a session was invalid' - }, - CHANNEL_ERROR: { - code: 9, - message: 'A channel to the receiver is not available' - }, - LOAD_MEDIA_FAILED: { - code: 10, - message: 'Load media failed' - }, - INVALID_MESSAGE: { - code: 11, - message: 'Invalid message received' - }, - UNKNOWN: { - code: 100, - message: 'Unknown error' - } -}; - function Chromecast() { let active = false; let error = null; let starting = false; + let transport = null; const events = new EventEmitter(); events.on('error', () => { }); @@ -77,186 +25,19 @@ function Chromecast() { active = true; error = null; starting = false; + transport = new ChromecastTransport(); } else { active = false; error = new Error('Google Cast API not available'); starting = false; + transport = null; } onStateChanged(); } - function onCastStateChanged(event) { - events.emit(cast.framework.CastContextEventType.CAST_STATE_CHANGED, event); - } - function onCastError(code) { - switch (code) { - case chrome.cast.ErrorCode.CANCEL: { - events.emit('error', CAST_ERROR.CANCEL); - break; - } - case chrome.cast.ErrorCode.TIMEOUT: { - events.emit('error', CAST_ERROR.TIMEOUT); - break; - } - case chrome.cast.ErrorCode.API_NOT_INITIALIZED: { - events.emit('error', CAST_ERROR.API_NOT_INITIALIZED); - break; - } - case chrome.cast.ErrorCode.INVALID_PARAMETER: { - events.emit('error', CAST_ERROR.INVALID_PARAMETER); - break; - } - case chrome.cast.ErrorCode.EXTENSION_NOT_COMPATIBLE: { - events.emit('error', CAST_ERROR.EXTENSION_NOT_COMPATIBLE); - break; - } - case chrome.cast.ErrorCode.EXTENSION_MISSING: { - events.emit('error', CAST_ERROR.EXTENSION_MISSING); - break; - } - case chrome.cast.ErrorCode.RECEIVER_UNAVAILABLE: { - events.emit('error', CAST_ERROR.RECEIVER_UNAVAILABLE); - break; - } - case chrome.cast.ErrorCode.SESSION_ERROR: { - events.emit('error', CAST_ERROR.SESSION_ERROR); - break; - } - case chrome.cast.ErrorCode.CHANNEL_ERROR: { - events.emit('error', CAST_ERROR.CHANNEL_ERROR); - break; - } - case chrome.cast.ErrorCode.LOAD_MEDIA_FAILED: { - events.emit('error', CAST_ERROR.LOAD_MEDIA_FAILED); - break; - } - default: { - events.emit('error', { - ...CAST_ERROR.UNKNOWN, - error: { code } - }); - } - } - } - function onMessageReceived(_, message) { - try { - events.emit('message', JSON.parse(message)); - } catch (error) { - events.emit('error', { - ...CAST_ERROR.INVALID_MESSAGE, - error - }); - } - } - function onApplicationStatusChanged(event) { - events.emit(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, event); - } - function onApplicationMetadataChanged(event) { - events.emit(cast.framework.CastSession.APPLICATION_METADATA_CHANGED, event); - } - function onActiveInputStateChanged(event) { - events.emit(cast.framework.CastSession.ACTIVE_INPUT_STATE_CHANGED, event); - } - function onVolumeChanged(event) { - events.emit(cast.framework.CastSession.VOLUME_CHANGED, event); - } - function onMediaSessionChanged(event) { - events.emit(cast.framework.CastSession.MEDIA_SESSION, event); - } - function onSesstionStateChanged(event) { - events.emit(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, event); - switch (event.sessionState) { - case cast.framework.SessionState.SESSION_STARTED: { - event.session.addMessageListener(MESSAGE_NAMESPACE, onMessageReceived); - event.session.addEventListener(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, onApplicationStatusChanged); - event.session.addEventListener(cast.framework.CastSession.APPLICATION_METADATA_CHANGED, onApplicationMetadataChanged); - event.session.addEventListener(cast.framework.CastSession.ACTIVE_INPUT_STATE_CHANGED, onActiveInputStateChanged); - event.session.addEventListener(cast.framework.CastSession.VOLUME_CHANGED, onVolumeChanged); - event.session.addEventListener(cast.framework.CastSession.MEDIA_SESSION, onMediaSessionChanged); - break; - } - case cast.framework.SessionState.SESSION_ENDING: { - event.session.removeMessageListener(MESSAGE_NAMESPACE, onMessageReceived); - event.session.removeEventListener(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, onApplicationStatusChanged); - event.session.removeEventListener(cast.framework.CastSession.APPLICATION_METADATA_CHANGED, onApplicationMetadataChanged); - event.session.removeEventListener(cast.framework.CastSession.ACTIVE_INPUT_STATE_CHANGED, onActiveInputStateChanged); - event.session.removeEventListener(cast.framework.CastSession.VOLUME_CHANGED, onVolumeChanged); - event.session.removeEventListener(cast.framework.CastSession.MEDIA_SESSION, onMediaSessionChanged); - break; - } - case cast.framework.SessionState.SESSION_START_FAILED: { - onCastError(event.errorCode); - break; - } - } - } function onStateChanged() { - if (active) { - const context = cast.framework.CastContext.getInstance(); - context.setOptions({ - autoJoinPolicy: chrome.cast.AutoJoinPolicy.PAGE_SCOPED, - // TODO language: '' - receiverApplicationId: RECEIVER_APPLICATION_ID, - resumeSavedSession: false - }); - context.addEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, onCastStateChanged); - context.addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, onSesstionStateChanged); - } else if (castAPIAvailable) { - const context = cast.framework.CastContext.getInstance(); - context.setOptions({ - autoJoinPolicy: chrome.cast.AutoJoinPolicy.PAGE_SCOPED, - // TODO language: '' - receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, - resumeSavedSession: false - }); - context.removeEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, onCastStateChanged); - context.removeEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, onSesstionStateChanged); - } - events.emit('stateChanged'); } - function start() { - if (active || error instanceof Error || starting) { - return; - } - - starting = true; - if (castAPIAvailable !== null) { - onCastAPIAvailabilityChanged(); - } else { - castAPIEvents.on('availabilityChanged', onCastAPIAvailabilityChanged); - onStateChanged(); - } - } - function stop() { - castAPIEvents.off('availabilityChanged', onCastAPIAvailabilityChanged); - active = false; - error = null; - starting = false; - onStateChanged(); - } - function on(name, listener) { - events.on(name, listener); - } - function off(name, listener) { - events.off(name, listener); - } - function dispatch(action) { - if (!active || !action) { - return; - } - - switch (action.type) { - case 'message': { - const castSession = cast.framework.CastContext.getInstance().getCurrentSession(); - if (castSession) { - castSession.sendMessage(MESSAGE_NAMESPACE, action.message); - } - - return; - } - } - } Object.defineProperties(this, { active: { @@ -280,46 +61,42 @@ function Chromecast() { return starting; } }, - castState: { + transport: { configurable: false, enumerable: true, get: function() { - if (!castAPIAvailable) { - return null; - } - - return cast.framework.CastContext.getInstance().getCastState(); - } - }, - castSessionState: { - configurable: false, - enumerable: true, - get: function() { - if (!castAPIAvailable) { - return null; - } - - return cast.framework.CastContext.getInstance().getSessionState(); - } - }, - castSession: { - configurable: false, - enumerable: true, - get: function() { - if (!castAPIAvailable) { - return null; - } - - return cast.framework.CastContext.getInstance().getCurrentSession(); + return transport; } } }); - this.start = start; - this.stop = stop; - this.on = on; - this.off = off; - this.dispatch = dispatch; + this.start = function() { + if (active || error instanceof Error || starting) { + return; + } + + starting = true; + if (castAPIAvailable !== null) { + onCastAPIAvailabilityChanged(); + } else { + castAPIEvents.on('availabilityChanged', onCastAPIAvailabilityChanged); + onStateChanged(); + } + }; + this.stop = function() { + castAPIEvents.off('availabilityChanged', onCastAPIAvailabilityChanged); + active = false; + error = null; + starting = false; + transport = null; + onStateChanged(); + }; + this.on = function(name, listener) { + events.on(name, listener); + }; + this.off = function(name, listener) { + events.off(name, listener); + }; } module.exports = Chromecast; diff --git a/src/services/Chromecast/ChromecastTransport.js b/src/services/Chromecast/ChromecastTransport.js new file mode 100644 index 000000000..e33340bb9 --- /dev/null +++ b/src/services/Chromecast/ChromecastTransport.js @@ -0,0 +1,204 @@ +// Copyright (C) 2017-2020 Smart code 203358507 + +const EventEmitter = require('events'); + +const MESSAGE_NAMESPACE = 'urn:x-cast:com.stremio'; +const CAST_ERROR = { + UNKNOWN: { + code: 300, + message: 'Unknown error' + }, + CANCEL: { + code: 301, + message: 'The operation was canceled by the user' + }, + TIMEOUT: { + code: 302, + message: 'The operation timed out' + }, + API_NOT_INITIALIZED: { + code: 303, + message: 'The API is not initialized' + }, + INVALID_PARAMETER: { + code: 304, + message: 'The parameters to the operation were not valid' + }, + EXTENSION_NOT_COMPATIBLE: { + code: 305, + message: 'The API script is not compatible with the installed Cast extension' + }, + EXTENSION_MISSING: { + code: 306, + message: 'The Cast extension is not available' + }, + RECEIVER_UNAVAILABLE: { + code: 307, + message: 'No receiver was compatible with the session request' + }, + SESSION_ERROR: { + code: 308, + message: 'A session could not be created, or a session was invalid' + }, + CHANNEL_ERROR: { + code: 309, + message: 'A channel to the receiver is not available' + }, + LOAD_MEDIA_FAILED: { + code: 310, + message: 'Load media failed' + }, + INVALID_MESSAGE: { + code: 350, + message: 'Invalid message received' + } +}; + +function ChromecastTransport() { + const events = new EventEmitter(); + events.on('error', () => { }); + + cast.framework.CastContext.getInstance().addEventListener( + cast.framework.CastContextEventType.CAST_STATE_CHANGED, + onCastStateChanged + ); + cast.framework.CastContext.getInstance().addEventListener( + cast.framework.CastContextEventType.SESSION_STATE_CHANGED, + onSesstionStateChanged + ); + + function onCastError(code) { + switch (code) { + case chrome.cast.ErrorCode.CANCEL: { + events.emit('error', CAST_ERROR.CANCEL); + break; + } + case chrome.cast.ErrorCode.TIMEOUT: { + events.emit('error', CAST_ERROR.TIMEOUT); + break; + } + case chrome.cast.ErrorCode.API_NOT_INITIALIZED: { + events.emit('error', CAST_ERROR.API_NOT_INITIALIZED); + break; + } + case chrome.cast.ErrorCode.INVALID_PARAMETER: { + events.emit('error', CAST_ERROR.INVALID_PARAMETER); + break; + } + case chrome.cast.ErrorCode.EXTENSION_NOT_COMPATIBLE: { + events.emit('error', CAST_ERROR.EXTENSION_NOT_COMPATIBLE); + break; + } + case chrome.cast.ErrorCode.EXTENSION_MISSING: { + events.emit('error', CAST_ERROR.EXTENSION_MISSING); + break; + } + case chrome.cast.ErrorCode.RECEIVER_UNAVAILABLE: { + events.emit('error', CAST_ERROR.RECEIVER_UNAVAILABLE); + break; + } + case chrome.cast.ErrorCode.SESSION_ERROR: { + events.emit('error', CAST_ERROR.SESSION_ERROR); + break; + } + case chrome.cast.ErrorCode.CHANNEL_ERROR: { + events.emit('error', CAST_ERROR.CHANNEL_ERROR); + break; + } + case chrome.cast.ErrorCode.LOAD_MEDIA_FAILED: { + events.emit('error', CAST_ERROR.LOAD_MEDIA_FAILED); + break; + } + default: { + events.emit('error', { + ...CAST_ERROR.UNKNOWN, + error: { code } + }); + } + } + } + function onMessage(_, message) { + try { + events.emit('message', JSON.parse(message)); + } catch (error) { + events.emit('error', { + ...CAST_ERROR.INVALID_MESSAGE, + error + }); + } + } + function onApplicationStatusChanged(event) { + events.emit(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, event); + } + function onApplicationMetadataChanged(event) { + events.emit(cast.framework.CastSession.APPLICATION_METADATA_CHANGED, event); + } + function onActiveInputStateChanged(event) { + events.emit(cast.framework.CastSession.ACTIVE_INPUT_STATE_CHANGED, event); + } + function onVolumeChanged(event) { + events.emit(cast.framework.CastSession.VOLUME_CHANGED, event); + } + function onMediaSessionChanged(event) { + events.emit(cast.framework.CastSession.MEDIA_SESSION, event); + } + function onCastStateChanged(event) { + events.emit(cast.framework.CastContextEventType.CAST_STATE_CHANGED, event); + } + function onSesstionStateChanged(event) { + events.emit(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, event); + switch (event.sessionState) { + case cast.framework.SessionState.SESSION_STARTED: { + event.session.addMessageListener(MESSAGE_NAMESPACE, onMessage); + event.session.addEventListener(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, onApplicationStatusChanged); + event.session.addEventListener(cast.framework.CastSession.APPLICATION_METADATA_CHANGED, onApplicationMetadataChanged); + event.session.addEventListener(cast.framework.CastSession.ACTIVE_INPUT_STATE_CHANGED, onActiveInputStateChanged); + event.session.addEventListener(cast.framework.CastSession.VOLUME_CHANGED, onVolumeChanged); + event.session.addEventListener(cast.framework.CastSession.MEDIA_SESSION, onMediaSessionChanged); + break; + } + case cast.framework.SessionState.SESSION_ENDING: { + event.session.removeMessageListener(MESSAGE_NAMESPACE, onMessage); + event.session.removeEventListener(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, onApplicationStatusChanged); + event.session.removeEventListener(cast.framework.CastSession.APPLICATION_METADATA_CHANGED, onApplicationMetadataChanged); + event.session.removeEventListener(cast.framework.CastSession.ACTIVE_INPUT_STATE_CHANGED, onActiveInputStateChanged); + event.session.removeEventListener(cast.framework.CastSession.VOLUME_CHANGED, onVolumeChanged); + event.session.removeEventListener(cast.framework.CastSession.MEDIA_SESSION, onMediaSessionChanged); + break; + } + case cast.framework.SessionState.SESSION_START_FAILED: { + onCastError(event.errorCode); + break; + } + } + } + + this.on = function(name, listener) { + events.on(name, listener); + }; + this.off = function(name, listener) { + events.off(name, listener); + }; + this.dispatch = function(action) { + if (action) { + switch (action.type) { + case 'setOptions': { + cast.framework.CastContext.getInstance().setOptions(action.options); + return; + } + case 'message': { + const castSession = cast.framework.CastContext.getInstance().getCurrentSession(); + if (castSession) { + castSession.sendMessage(MESSAGE_NAMESPACE, action.message); + } + + return; + } + } + } + + throw new Error('Invalid action dispatched: ' + JSON.stringify(action)); + }; +} + +module.exports = ChromecastTransport; From 29609b8fdbb6dc5db4a0295be2d8974cd66e988b Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Fri, 15 May 2020 14:49:38 +0300 Subject: [PATCH 32/72] dispatch setOptions when chromecast service became active --- src/App/App.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/App/App.js b/src/App/App.js index 27219c1e6..3056e18dd 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -5,7 +5,7 @@ const React = require('react'); const { Router } = require('stremio-router'); const { Core, Shell, Chromecast, KeyboardNavigation, ServicesProvider } = require('stremio/services'); const { NotFound } = require('stremio/routes'); -const { ToastProvider } = require('stremio/common'); +const { ToastProvider, CONSTANTS } = require('stremio/common'); const CoreEventsToaster = require('./CoreEventsToaster'); const routerViewsConfig = require('./routerViewsConfig'); const styles = require('./styles'); @@ -22,7 +22,6 @@ const App = () => { }), []); const [coreInitialized, setCoreInitialized] = React.useState(false); const [shellInitialized, setShellInitialized] = React.useState(false); - const [chromecastInitialized, setChromecastInitialized] = React.useState(false); React.useEffect(() => { const onCoreStateChanged = () => { services.core.dispatch({ @@ -37,7 +36,17 @@ const App = () => { setShellInitialized(services.shell.active || services.shell.error instanceof Error); }; const onChromecastStateChange = () => { - setChromecastInitialized(services.chromecast.active || services.chromecast.error instanceof Error); + if (services.chromecast.active) { + services.chromecast.transport.dispatch({ + type: 'setOptions', + options: { + receiverApplicationId: CONSTANTS.CHROMECAST_RECEIVER_APP_ID, + autoJoinPolicy: chrome.cast.AutoJoinPolicy.PAGE_SCOPED, + resumeSavedSession: false, + language: null + } + }); + } }; services.core.on('stateChanged', onCoreStateChanged); services.shell.on('stateChanged', onShellStateChanged); @@ -48,7 +57,6 @@ const App = () => { services.keyboardNavigation.start(); window.core = services.core; window.shell = services.shell; - window.services = services; return () => { services.core.stop(); services.shell.stop(); @@ -63,7 +71,7 @@ const App = () => { { - coreInitialized && shellInitialized && chromecastInitialized ? + coreInitialized && shellInitialized ? Date: Fri, 15 May 2020 15:03:16 +0300 Subject: [PATCH 33/72] stateChanged event emitted from KeyboardNavigation --- .../KeyboardNavigation/KeyboardNavigation.js | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/services/KeyboardNavigation/KeyboardNavigation.js b/src/services/KeyboardNavigation/KeyboardNavigation.js index 2225ce185..ded82ce20 100644 --- a/src/services/KeyboardNavigation/KeyboardNavigation.js +++ b/src/services/KeyboardNavigation/KeyboardNavigation.js @@ -1,8 +1,13 @@ // Copyright (C) 2017-2020 Smart code 203358507 +const EventEmitter = require('events'); + function KeyboardNavigation() { let active = false; + const events = new EventEmitter(); + events.on('error', () => { }); + function onKeyDown(event) { if (event.keyboardNavigationPrevented || event.target.tagName === 'INPUT') { return; @@ -61,17 +66,8 @@ function KeyboardNavigation() { } } } - function start() { - if (active) { - return; - } - - window.addEventListener('keydown', onKeyDown); - active = true; - } - function stop() { - window.removeEventListener('keydown', onKeyDown); - active = false; + function onStateChanged() { + events.emit('stateChanged'); } Object.defineProperties(this, { @@ -84,8 +80,20 @@ function KeyboardNavigation() { } }); - this.start = start; - this.stop = stop; + this.start = function() { + if (active) { + return; + } + + window.addEventListener('keydown', onKeyDown); + active = true; + onStateChanged(); + }; + this.stop = function() { + window.removeEventListener('keydown', onKeyDown); + active = false; + onStateChanged(); + }; } module.exports = KeyboardNavigation; From 130b95c9321e9d6dc93fc102f7d0cbf5148bcf54 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Fri, 15 May 2020 15:36:42 +0300 Subject: [PATCH 34/72] KeyboardNavigation renamed to KeyboardShortcuts --- src/App/App.js | 8 ++++---- src/services/KeyboardNavigation/index.js | 5 ----- .../KeyboardShortcuts.js} | 6 +++--- src/services/KeyboardShortcuts/index.js | 5 +++++ src/services/index.js | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 src/services/KeyboardNavigation/index.js rename src/services/{KeyboardNavigation/KeyboardNavigation.js => KeyboardShortcuts/KeyboardShortcuts.js} (94%) create mode 100644 src/services/KeyboardShortcuts/index.js diff --git a/src/App/App.js b/src/App/App.js index 3056e18dd..d521a4a34 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -3,7 +3,7 @@ require('spatial-navigation-polyfill'); const React = require('react'); const { Router } = require('stremio-router'); -const { Core, Shell, Chromecast, KeyboardNavigation, ServicesProvider } = require('stremio/services'); +const { Core, Shell, Chromecast, KeyboardShortcuts, ServicesProvider } = require('stremio/services'); const { NotFound } = require('stremio/routes'); const { ToastProvider, CONSTANTS } = require('stremio/common'); const CoreEventsToaster = require('./CoreEventsToaster'); @@ -18,7 +18,7 @@ const App = () => { core: new Core(), shell: new Shell(), chromecast: new Chromecast(), - keyboardNavigation: new KeyboardNavigation() + keyboardShortcuts: new KeyboardShortcuts() }), []); const [coreInitialized, setCoreInitialized] = React.useState(false); const [shellInitialized, setShellInitialized] = React.useState(false); @@ -54,14 +54,14 @@ const App = () => { services.core.start(); services.shell.start(); services.chromecast.start(); - services.keyboardNavigation.start(); + services.keyboardShortcuts.start(); window.core = services.core; window.shell = services.shell; return () => { services.core.stop(); services.shell.stop(); services.chromecast.stop(); - services.keyboardNavigation.stop(); + services.keyboardShortcuts.stop(); services.core.off('stateChanged', onCoreStateChanged); services.shell.off('stateChanged', onShellStateChanged); services.chromecast.off('stateChanged', onChromecastStateChange); diff --git a/src/services/KeyboardNavigation/index.js b/src/services/KeyboardNavigation/index.js deleted file mode 100644 index 28a24290c..000000000 --- a/src/services/KeyboardNavigation/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2020 Smart code 203358507 - -const KeyboardNavigation = require('./KeyboardNavigation'); - -module.exports = KeyboardNavigation; diff --git a/src/services/KeyboardNavigation/KeyboardNavigation.js b/src/services/KeyboardShortcuts/KeyboardShortcuts.js similarity index 94% rename from src/services/KeyboardNavigation/KeyboardNavigation.js rename to src/services/KeyboardShortcuts/KeyboardShortcuts.js index ded82ce20..e1047a0ea 100644 --- a/src/services/KeyboardNavigation/KeyboardNavigation.js +++ b/src/services/KeyboardShortcuts/KeyboardShortcuts.js @@ -2,14 +2,14 @@ const EventEmitter = require('events'); -function KeyboardNavigation() { +function KeyboardShortcuts() { let active = false; const events = new EventEmitter(); events.on('error', () => { }); function onKeyDown(event) { - if (event.keyboardNavigationPrevented || event.target.tagName === 'INPUT') { + if (event.keyboardShortcutPrevented || event.target.tagName === 'INPUT') { return; } @@ -96,4 +96,4 @@ function KeyboardNavigation() { }; } -module.exports = KeyboardNavigation; +module.exports = KeyboardShortcuts; diff --git a/src/services/KeyboardShortcuts/index.js b/src/services/KeyboardShortcuts/index.js new file mode 100644 index 000000000..cebb92926 --- /dev/null +++ b/src/services/KeyboardShortcuts/index.js @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2020 Smart code 203358507 + +const KeyboardShortcuts = require('./KeyboardShortcuts'); + +module.exports = KeyboardShortcuts; diff --git a/src/services/index.js b/src/services/index.js index 3c4c75556..93e87c471 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -2,14 +2,14 @@ const Chromecast = require('./Chromecast'); const Core = require('./Core'); -const KeyboardNavigation = require('./KeyboardNavigation'); +const KeyboardShortcuts = require('./KeyboardShortcuts'); const { ServicesProvider, useServices } = require('./ServicesContext'); const Shell = require('./Shell'); module.exports = { Chromecast, Core, - KeyboardNavigation, + KeyboardShortcuts, ServicesProvider, useServices, Shell From 4868e9521f614d2ae9c4319a22a12994f6993600 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Fri, 15 May 2020 17:26:33 +0300 Subject: [PATCH 35/72] Core transport decoupled from Core service --- src/App/App.js | 15 ++- src/App/CoreEventsToaster.js | 4 +- .../AddonDetailsModal/AddonDetailsModal.js | 4 +- src/common/LibItem/LibItem.js | 2 +- .../HorizontalNavBar/NavMenu/NavMenu.js | 2 +- .../NotificationsMenu/useNotifications.js | 8 +- src/common/useInLibrary.js | 6 +- src/common/useModelState.js | 14 +- src/common/useProfile.js | 2 +- src/routes/Addons/useAddons.js | 2 +- .../Board/useContinueWatchingPreview.js | 2 +- src/routes/Discover/useDiscover.js | 2 +- src/routes/Intro/Intro.js | 12 +- src/routes/Player/Player.js | 4 +- src/routes/Player/usePlayer.js | 4 +- src/routes/Player/useSettings.js | 2 +- src/routes/Settings/Settings.js | 4 +- .../Settings/useProfileSettingsInputs.js | 20 +-- src/routes/Settings/useStreamingServer.js | 4 +- .../useStreamingServerSettingsInputs.js | 4 +- src/services/Core/Core.js | 127 +++++++++--------- src/services/Core/CoreTransport.js | 33 +++++ 22 files changed, 155 insertions(+), 122 deletions(-) create mode 100644 src/services/Core/CoreTransport.js diff --git a/src/App/App.js b/src/App/App.js index d521a4a34..67ec390df 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -24,12 +24,15 @@ const App = () => { const [shellInitialized, setShellInitialized] = React.useState(false); React.useEffect(() => { const onCoreStateChanged = () => { - services.core.dispatch({ - action: 'Load', - args: { - model: 'Ctx' - } - }); + if (services.core.active) { + services.core.transport.dispatch({ + action: 'Load', + args: { + model: 'Ctx' + } + }); + } + setCoreInitialized(services.core.active); }; const onShellStateChanged = () => { diff --git a/src/App/CoreEventsToaster.js b/src/App/CoreEventsToaster.js index e11a627b2..bbdf68b78 100644 --- a/src/App/CoreEventsToaster.js +++ b/src/App/CoreEventsToaster.js @@ -19,9 +19,9 @@ const CoreEventsToaster = () => { }); } }; - core.on('Event', onEvent); + core.transport.on('Event', onEvent); return () => { - core.off('Event', onEvent); + core.transport.off('Event', onEvent); }; }, []); return null; diff --git a/src/common/AddonDetailsModal/AddonDetailsModal.js b/src/common/AddonDetailsModal/AddonDetailsModal.js index bf452b9fb..3d03d7819 100644 --- a/src/common/AddonDetailsModal/AddonDetailsModal.js +++ b/src/common/AddonDetailsModal/AddonDetailsModal.js @@ -26,7 +26,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { } }; const installOnClick = (event) => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'InstallAddon', @@ -42,7 +42,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { } }; const uninstallOnClick = (event) => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'UninstallAddon', diff --git a/src/common/LibItem/LibItem.js b/src/common/LibItem/LibItem.js index 8b14ca317..687f6e6a2 100644 --- a/src/common/LibItem/LibItem.js +++ b/src/common/LibItem/LibItem.js @@ -52,7 +52,7 @@ const LibItem = ({ id, ...props }) => { } case 'dismiss': { if (typeof id === 'string') { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'RewindLibraryItem', diff --git a/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenu.js b/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenu.js index 0333f1afa..6f32a2d09 100644 --- a/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenu.js +++ b/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenu.js @@ -26,7 +26,7 @@ const NavMenu = (props) => { event.nativeEvent.togglePopupPrevented = true; }, []); const logoutButtonOnClick = React.useCallback(() => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'Logout' diff --git a/src/common/NavBar/HorizontalNavBar/NotificationsMenu/useNotifications.js b/src/common/NavBar/HorizontalNavBar/NotificationsMenu/useNotifications.js index 3304befca..ed194af63 100644 --- a/src/common/NavBar/HorizontalNavBar/NotificationsMenu/useNotifications.js +++ b/src/common/NavBar/HorizontalNavBar/NotificationsMenu/useNotifications.js @@ -8,18 +8,18 @@ const useNotifications = () => { const { core } = useServices(); React.useEffect(() => { const onNewState = () => { - const state = core.getState(); + const state = core.transport.getState(); setNotifications(state.notifications.groups); }; - core.on('NewModel', onNewState); - core.dispatch({ + core.transport.on('NewModel', onNewState); + core.transport.dispatch({ action: 'Load', args: { load: 'Notifications' } }); return () => { - core.off('NewModel', onNewState); + core.transport.off('NewModel', onNewState); }; }, []); return notifications; diff --git a/src/common/useInLibrary.js b/src/common/useInLibrary.js index 4b1b467b4..74912339c 100644 --- a/src/common/useInLibrary.js +++ b/src/common/useInLibrary.js @@ -7,14 +7,14 @@ const useModelState = require('stremio/common/useModelState'); const useInLibrary = (metaItem) => { const { core } = useServices(); const initLibraryItemsState = React.useCallback(() => { - return core.getState('library_items'); + return core.transport.getState('library_items'); }, []); const libraryItems = useModelState({ model: 'library_items', init: initLibraryItemsState }); const addToLibrary = React.useCallback((metaItem) => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'AddToLibrary', @@ -23,7 +23,7 @@ const useInLibrary = (metaItem) => { }); }, []); const removeFromLibrary = React.useCallback((metaItem) => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'RemoveFromLibrary', diff --git a/src/common/useModelState.js b/src/common/useModelState.js index 19c670bbf..d28a9e0ab 100644 --- a/src/common/useModelState.js +++ b/src/common/useModelState.js @@ -13,26 +13,26 @@ const useModelState = ({ model, init, action, timeout, onNewState, map, mapWithC const routeFocused = useRouteFocused(); const [state, setState] = useDeepEqualState(init); React.useLayoutEffect(() => { - core.dispatch(action, modelRef.current); + core.transport.dispatch(action, modelRef.current); }, [action]); React.useLayoutEffect(() => { return () => { - core.dispatch({ action: 'Unload' }, modelRef.current); + core.transport.dispatch({ action: 'Unload' }, modelRef.current); }; }, []); React.useLayoutEffect(() => { const onNewStateThrottled = throttle(() => { - const state = core.getState(modelRef.current); + const state = core.transport.getState(modelRef.current); if (typeof onNewState === 'function') { const action = onNewState(state); - const handled = core.dispatch(action, modelRef.current); + const handled = core.transport.dispatch(action, modelRef.current); if (handled) { return; } } if (typeof mapWithCtx === 'function') { - const ctx = core.getState('ctx'); + const ctx = core.transport.getState('ctx'); setState(mapWithCtx(state, ctx)); } else if (typeof map === 'function') { setState(map(state)); @@ -41,14 +41,14 @@ const useModelState = ({ model, init, action, timeout, onNewState, map, mapWithC } }, timeout); if (routeFocused) { - core.on('NewState', onNewStateThrottled); + core.transport.on('NewState', onNewStateThrottled); if (mountedRef.current) { onNewStateThrottled.call(); } } return () => { onNewStateThrottled.cancel(); - core.off('NewState', onNewStateThrottled); + core.transport.off('NewState', onNewStateThrottled); }; }, [routeFocused, timeout, onNewState, map, mapWithCtx]); React.useLayoutEffect(() => { diff --git a/src/common/useProfile.js b/src/common/useProfile.js index 6a520ae98..ded6b9e64 100644 --- a/src/common/useProfile.js +++ b/src/common/useProfile.js @@ -11,7 +11,7 @@ const mapProfileState = (ctx) => { const useProfile = () => { const { core } = useServices(); const initProfileState = React.useCallback(() => { - const ctx = core.getState('ctx'); + const ctx = core.transport.getState('ctx'); return mapProfileState(ctx); }, []); const profile = useModelState({ diff --git a/src/routes/Addons/useAddons.js b/src/routes/Addons/useAddons.js index 7a1098f17..11f3d600c 100644 --- a/src/routes/Addons/useAddons.js +++ b/src/routes/Addons/useAddons.js @@ -126,7 +126,7 @@ const useAddons = (urlParams) => { } }; } else { - const addons = core.getState('addons'); + const addons = core.transport.getState('addons'); if (addons.selectable.catalogs.length > 0) { return { action: 'Load', diff --git a/src/routes/Board/useContinueWatchingPreview.js b/src/routes/Board/useContinueWatchingPreview.js index 544cb20e3..5a47f60fa 100644 --- a/src/routes/Board/useContinueWatchingPreview.js +++ b/src/routes/Board/useContinueWatchingPreview.js @@ -24,7 +24,7 @@ const mapContinueWatchingPreviewState = (continue_watching_preview) => { const useContinueWatchingPreview = () => { const { core } = useServices(); const initContinueWatchingPreviewState = React.useMemo(() => { - return mapContinueWatchingPreviewState(core.getState('continue_watching_preview')); + return mapContinueWatchingPreviewState(core.transport.getState('continue_watching_preview')); }, []); return useModelState({ model: 'continue_watching_preview', diff --git a/src/routes/Discover/useDiscover.js b/src/routes/Discover/useDiscover.js index 3cc96d617..67db7419e 100644 --- a/src/routes/Discover/useDiscover.js +++ b/src/routes/Discover/useDiscover.js @@ -90,7 +90,7 @@ const useDiscover = (urlParams, queryParams) => { } }; } else { - const discover = core.getState('discover'); + const discover = core.transport.getState('discover'); if (discover.selectable.types.length > 0) { return { action: 'Load', diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index b9fd84185..11404fa02 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -92,7 +92,7 @@ const Intro = ({ queryParams }) => { if (!user || typeof user.fbLoginToken !== 'string' || typeof user.email !== 'string') { throw new Error('Login failed at getting token from Stremio'); } - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'Authenticate', @@ -123,7 +123,7 @@ const Intro = ({ queryParams }) => { return; } openLoaderModal(); - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'Authenticate', @@ -140,7 +140,7 @@ const Intro = ({ queryParams }) => { dispatch({ type: 'error', error: 'You must accept the Terms of Service' }); return; } - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'Logout' @@ -170,7 +170,7 @@ const Intro = ({ queryParams }) => { return; } openLoaderModal(); - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'Authenticate', @@ -270,10 +270,10 @@ const Intro = ({ queryParams }) => { } }; if (routeFocused) { - core.on('Event', onEvent); + core.transport.on('Event', onEvent); } return () => { - core.off('Event', onEvent); + core.transport.off('Event', onEvent); }; }, [routeFocused]); React.useEffect(() => { diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 5053ed7ab..629489ccf 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -70,9 +70,9 @@ const Player = ({ urlParams }) => { setVideoState({ [propName]: propValue }); }, []); const onEnded = React.useCallback(() => { - core.dispatch({ action: 'Unload' }, 'player'); + core.transport.dispatch({ action: 'Unload' }, 'player'); if (player.lib_item !== null) { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'RewindLibraryItem', diff --git a/src/routes/Player/usePlayer.js b/src/routes/Player/usePlayer.js index 12635675f..63b8af790 100644 --- a/src/routes/Player/usePlayer.js +++ b/src/routes/Player/usePlayer.js @@ -135,7 +135,7 @@ const usePlayer = (urlParams) => { } }, [urlParams]); const updateLibraryItemState = React.useCallback((time, duration) => { - core.dispatch({ + core.transport.dispatch({ action: 'Player', args: { action: 'UpdateLibraryItemState', @@ -144,7 +144,7 @@ const usePlayer = (urlParams) => { }, 'player'); }, []); const pushToLibrary = React.useCallback(() => { - core.dispatch({ + core.transport.dispatch({ action: 'Player', args: { action: 'PushToLibrary' diff --git a/src/routes/Player/useSettings.js b/src/routes/Player/useSettings.js index b75a9ec59..3db0d891f 100644 --- a/src/routes/Player/useSettings.js +++ b/src/routes/Player/useSettings.js @@ -6,7 +6,7 @@ const { useServices } = require('stremio/services'); const useSettings = (profile) => { const { core } = useServices(); const updateSettings = React.useCallback((settings) => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 92f91c05a..07662b59f 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -38,7 +38,7 @@ const Settings = () => { torrentProfileSelect } = useStreamingServerSettingsInputs(streamingServer); const logoutButtonOnClick = React.useCallback(() => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'Logout' @@ -58,7 +58,7 @@ const Settings = () => { // TODO }, []); const reloadStreamingServer = React.useCallback(() => { - core.dispatch({ + core.transport.dispatch({ action: 'StreamingServer', args: { action: 'Reload' diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js index dc89c2a2a..ff22a50d3 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/useProfileSettingsInputs.js @@ -18,7 +18,7 @@ const useProfileSettingsInputs = (profile) => { profile.settings.interface_language; }, onSelect: (event) => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', @@ -43,7 +43,7 @@ const useProfileSettingsInputs = (profile) => { profile.settings.subtitles_language; }, onSelect: (event) => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', @@ -65,7 +65,7 @@ const useProfileSettingsInputs = (profile) => { return `${profile.settings.subtitles_size}%`; }, onSelect: (event) => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', @@ -80,7 +80,7 @@ const useProfileSettingsInputs = (profile) => { const subtitlesTextColorInput = useDeepEqualMemo(() => ({ value: profile.settings.subtitles_text_color, onChange: (event) => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', @@ -95,7 +95,7 @@ const useProfileSettingsInputs = (profile) => { const subtitlesBackgroundColorInput = useDeepEqualMemo(() => ({ value: profile.settings.subtitles_background_color, onChange: (event) => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', @@ -110,7 +110,7 @@ const useProfileSettingsInputs = (profile) => { const subtitlesOutlineColorInput = useDeepEqualMemo(() => ({ value: profile.settings.subtitles_outline_color, onChange: (event) => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', @@ -125,7 +125,7 @@ const useProfileSettingsInputs = (profile) => { const bingeWatchingCheckbox = useDeepEqualMemo(() => ({ checked: profile.settings.binge_watching, onClick: () => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', @@ -140,7 +140,7 @@ const useProfileSettingsInputs = (profile) => { const playInBackgroundCheckbox = useDeepEqualMemo(() => ({ checked: profile.settings.play_in_background, onClick: () => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', @@ -155,7 +155,7 @@ const useProfileSettingsInputs = (profile) => { const playInExternalPlayerCheckbox = useDeepEqualMemo(() => ({ checked: profile.settings.play_in_external_player, onClick: () => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', @@ -170,7 +170,7 @@ const useProfileSettingsInputs = (profile) => { const hardwareDecodingCheckbox = useDeepEqualMemo(() => ({ checked: profile.settings.hardware_decoding, onClick: () => { - core.dispatch({ + core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', diff --git a/src/routes/Settings/useStreamingServer.js b/src/routes/Settings/useStreamingServer.js index fffbf5980..af1f7526a 100644 --- a/src/routes/Settings/useStreamingServer.js +++ b/src/routes/Settings/useStreamingServer.js @@ -7,10 +7,10 @@ const { useModelState } = require('stremio/common'); const useStreamingServer = () => { const { core } = useServices(); const initStreamingServer = React.useCallback(() => { - return core.getState('streaming_server'); + return core.transport.getState('streaming_server'); }, []); const loadStreamingServerAction = React.useMemo(() => { - const streamingServer = core.getState('streaming_server'); + const streamingServer = core.transport.getState('streaming_server'); if (streamingServer.selected === null) { return { action: 'StreamingServer', diff --git a/src/routes/Settings/useStreamingServerSettingsInputs.js b/src/routes/Settings/useStreamingServerSettingsInputs.js index e0b76700c..e98372585 100644 --- a/src/routes/Settings/useStreamingServerSettingsInputs.js +++ b/src/routes/Settings/useStreamingServerSettingsInputs.js @@ -60,7 +60,7 @@ const useStreaminServerSettingsInputs = (streaminServer) => { return cacheSizeToString(streaminServer.settings.content.cacheSize); }, onSelect: (event) => { - core.dispatch({ + core.transport.dispatch({ action: 'StreamingServer', args: { action: 'UpdateSettings', @@ -115,7 +115,7 @@ const useStreaminServerSettingsInputs = (streaminServer) => { }, 'custom'); }, onSelect: (event) => { - core.dispatch({ + core.transport.dispatch({ action: 'StreamingServer', args: { action: 'UpdateSettings', diff --git a/src/services/Core/Core.js b/src/services/Core/Core.js index fc7d14ac0..bae59bd9c 100644 --- a/src/services/Core/Core.js +++ b/src/services/Core/Core.js @@ -1,78 +1,47 @@ // Copyright (C) 2017-2020 Smart code 203358507 const EventEmitter = require('events'); -const { default: initialize, StremioCoreWeb } = require('@stremio/stremio-core-web'); +const { default: initializeCoreAPI } = require('@stremio/stremio-core-web'); +const CoreTransport = require('./CoreTransport'); + +let coreAPIAvailable = null; +const coreAPIEvents = new EventEmitter(); +initializeCoreAPI() + .then(() => { + coreAPIAvailable = true; + coreAPIEvents.emit('availabilityChanged'); + }) + .catch(() => { + coreAPIAvailable = false; + coreAPIEvents.emit('availabilityChanged'); + }); function Core() { let active = false; let error = null; let starting = false; - let core = null; + let transport = null; const events = new EventEmitter(); events.on('error', () => { }); - function onStateChanged() { - events.emit('stateChanged'); - } - function start() { - if (active || error instanceof Error || starting) { - return; + function onCoreAPIAvailabilityChanged() { + if (coreAPIAvailable) { + active = true; + error = null; + starting = false; + transport = new CoreTransport(); + } else { + active = false; + error = new Error('Stremio Core API not available'); + starting = false; + transport = null; } - starting = true; - initialize().then(() => { - if (starting) { - core = new StremioCoreWeb(({ name, args } = {}) => { - if (active) { - try { - events.emit(name, args); - } catch (e) { - /* eslint-disable-next-line no-console */ - console.error(e); - } - } - }); - active = true; - error = null; - starting = false; - onStateChanged(); - } - }).catch((error) => { - core = null; - active = false; - error = new Error('Unable to init stremio-core-web'); - error.error = error; - starting = false; - onStateChanged(); - }); - } - function stop() { - core = null; - active = false; - error = null; - starting = false; onStateChanged(); } - function on(name, listener) { - events.on(name, listener); - } - function off(name, listener) { - events.off(name, listener); - } - function dispatch(action, model) { - if (!active || typeof action === 'undefined') { - return false; - } - - return core.dispatch(action, model); - } - function getState(model) { - if (!active) { - return null; - } - - return core.get_state(model); + function onStateChanged() { + events.emit('stateChanged'); } Object.defineProperties(this, { @@ -96,15 +65,43 @@ function Core() { get: function() { return starting; } + }, + transport: { + configurable: false, + enumerable: true, + get: function() { + return transport; + } } }); - this.start = start; - this.stop = stop; - this.on = on; - this.off = off; - this.dispatch = dispatch; - this.getState = getState; + this.start = function() { + if (active || error instanceof Error || starting) { + return; + } + + starting = true; + if (coreAPIAvailable !== null) { + onCoreAPIAvailabilityChanged(); + } else { + coreAPIEvents.on('availabilityChanged', onCoreAPIAvailabilityChanged); + onStateChanged(); + } + }; + this.stop = function() { + coreAPIEvents.off('availabilityChanged', onCoreAPIAvailabilityChanged); + active = false; + error = null; + starting = false; + transport = null; + onStateChanged(); + }; + this.on = function(name, listener) { + events.on(name, listener); + }; + this.off = function(name, listener) { + events.off(name, listener); + }; } module.exports = Core; diff --git a/src/services/Core/CoreTransport.js b/src/services/Core/CoreTransport.js new file mode 100644 index 000000000..1763ff30d --- /dev/null +++ b/src/services/Core/CoreTransport.js @@ -0,0 +1,33 @@ +// Copyright (C) 2017-2020 Smart code 203358507 + +const EventEmitter = require('events'); +const { StremioCoreWeb } = require('@stremio/stremio-core-web'); + +function CoreTransport() { + const events = new EventEmitter(); + events.on('error', () => { }); + + const core = new StremioCoreWeb(({ name, args }) => { + try { + events.emit(name, args); + } catch (error) { + /* eslint-disable-next-line no-console */ + console.error(error); + } + }); + + this.on = function(name, listener) { + events.on(name, listener); + }; + this.off = function(name, listener) { + events.off(name, listener); + }; + this.dispatch = function(action, model) { + return core.dispatch(action, model); + }; + this.getState = function(model) { + return core.get_state(model); + }; +} + +module.exports = CoreTransport; From ae1b39d6650986f8089e44f226135953d3dd4089 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Fri, 15 May 2020 17:27:06 +0300 Subject: [PATCH 36/72] attach services to window for debugging --- src/App/App.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/App/App.js b/src/App/App.js index 67ec390df..8d1e4d187 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -58,8 +58,7 @@ const App = () => { services.shell.start(); services.chromecast.start(); services.keyboardShortcuts.start(); - window.core = services.core; - window.shell = services.shell; + window.services = services; return () => { services.core.stop(); services.shell.stop(); From 14d251af923e60f4c1d1e84d36c5bf50a2f865cf Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Fri, 15 May 2020 17:30:58 +0300 Subject: [PATCH 37/72] Shell service code reformat --- src/services/Shell/Shell.js | 56 +++++++++++++++---------------------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/src/services/Shell/Shell.js b/src/services/Shell/Shell.js index 549eb35cb..4e551aa11 100644 --- a/src/services/Shell/Shell.js +++ b/src/services/Shell/Shell.js @@ -13,35 +13,6 @@ function Shell() { function onStateChanged() { events.emit('stateChanged'); } - function start() { - if (active || error instanceof Error || starting) { - return; - } - - active = false; - error = new Error('Stremio shell not available'); - starting = false; - onStateChanged(); - } - function stop() { - active = false; - error = null; - starting = false; - onStateChanged(); - } - function on(name, listener) { - events.on(name, listener); - } - function off(name, listener) { - events.off(name, listener); - } - function dispatch() { - if (!active) { - return; - } - - // TODO - } Object.defineProperties(this, { active: { @@ -67,11 +38,28 @@ function Shell() { } }); - this.start = start; - this.stop = stop; - this.on = on; - this.off = off; - this.dispatch = dispatch; + this.start = function() { + if (active || error instanceof Error || starting) { + return; + } + + active = false; + error = new Error('Stremio Shell API not available'); + starting = false; + onStateChanged(); + }; + this.stop = function() { + active = false; + error = null; + starting = false; + onStateChanged(); + }; + this.on = function(name, listener) { + events.on(name, listener); + }; + this.off = function(name, listener) { + events.off(name, listener); + }; } module.exports = Shell; From ba70f6c88ca656da8ed985351a556e2f3dc15207 Mon Sep 17 00:00:00 2001 From: nklhrstv Date: Fri, 15 May 2020 18:11:37 +0300 Subject: [PATCH 38/72] activate chromecast button when chromecast became available --- src/routes/Player/ControlBar/ControlBar.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index cf38fa70b..f675f944c 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -5,6 +5,7 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const Icon = require('stremio-icons/dom'); const { Button } = require('stremio/common'); +const { useServices } = require('stremio/services'); const SeekBar = require('./SeekBar'); const VolumeSlider = require('./VolumeSlider'); const styles = require('./styles'); @@ -28,6 +29,8 @@ const ControlBar = ({ onToggleInfoMenu, ...props }) => { + const { chromecast } = useServices(); + const [chromecastActive, setChromecastActive] = React.useState(() => chromecast.active); const onSubtitlesButtonMouseDown = React.useCallback((event) => { event.nativeEvent.subtitlesMenuClosePrevented = true; }, []); @@ -66,6 +69,15 @@ const ControlBar = ({ onToggleInfoMenu(); } }, [onToggleInfoMenu]); + React.useEffect(() => { + const onStateChanged = () => { + setChromecastActive(chromecast.active); + }; + chromecast.on('stateChanged', onStateChanged); + return () => { + chromecast.off('stateChanged', onStateChanged); + }; + }, []); return (
- - -