Skip to content

Commit

Permalink
Merge pull request #39 from Stremio/add-video-params
Browse files Browse the repository at this point in the history
Add Video Params Required For Subtitles Requests
  • Loading branch information
jaruba authored Feb 2, 2023
2 parents d5fd870 + a2c917c commit 9150139
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 28 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stremio/stremio-video",
"version": "0.0.24",
"version": "0.0.25-rc.1",
"description": "Abstraction layer on top of different media players",
"author": "Smart Code OOD",
"main": "src/index.js",
Expand Down
4 changes: 3 additions & 1 deletion src/ChromecastSenderVideo/ChromecastSenderVideo.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ function ChromecastSenderVideo(options) {
volume: false,
muted: false,
playbackSpeed: false,
videoParams: false,
extraSubtitlesTracks: false,
selectedExtraSubtitlesTrackId: false,
extraSubtitlesDelay: false,
Expand Down Expand Up @@ -126,6 +127,7 @@ function ChromecastSenderVideo(options) {
onPropChanged('volume', null);
onPropChanged('muted', null);
onPropChanged('playbackSpeed', null);
onPropChanged('videoParams', null);
onPropChanged('extraSubtitlesTracks', []);
onPropChanged('selectedExtraSubtitlesTrackId', null);
onPropChanged('extraSubtitlesDelay', null);
Expand Down Expand Up @@ -190,7 +192,7 @@ ChromecastSenderVideo.canPlayStream = function() {
ChromecastSenderVideo.manifest = {
name: 'ChromecastSenderVideo',
external: true,
props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed', 'extraSubtitlesTracks', 'selectedExtraSubtitlesTrackId', 'extraSubtitlesDelay', 'extraSubtitlesSize', 'extraSubtitlesOffset', 'extraSubtitlesTextColor', 'extraSubtitlesBackgroundColor', 'extraSubtitlesOutlineColor'],
props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed', 'videoParams', 'extraSubtitlesTracks', 'selectedExtraSubtitlesTrackId', 'extraSubtitlesDelay', 'extraSubtitlesSize', 'extraSubtitlesOffset', 'extraSubtitlesTextColor', 'extraSubtitlesBackgroundColor', 'extraSubtitlesOutlineColor'],
commands: ['load', 'unload', 'destroy', 'addExtraSubtitlesTracks'],
events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded', 'extraSubtitlesTrackLoaded', 'implementationChanged']
};
Expand Down
11 changes: 6 additions & 5 deletions src/StremioVideo/selectVideoImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var IFrameVideo = require('../IFrameVideo');
var YouTubeVideo = require('../YouTubeVideo');
var withStreamingServer = require('../withStreamingServer');
var withHTMLSubtitles = require('../withHTMLSubtitles');
var withVideoParams = require('../withVideoParams');

function selectVideoImplementation(commandArgs, options) {
if (!commandArgs.stream || typeof commandArgs.stream.externalUrl === 'string') {
Expand All @@ -18,11 +19,11 @@ function selectVideoImplementation(commandArgs, options) {
}

if (typeof commandArgs.stream.ytId === 'string') {
return withHTMLSubtitles(YouTubeVideo);
return withVideoParams(withHTMLSubtitles(YouTubeVideo));
}

if (typeof commandArgs.stream.playerFrameUrl === 'string') {
return IFrameVideo;
return withVideoParams(IFrameVideo);
}

if (options.shellTransport) {
Expand All @@ -41,12 +42,12 @@ function selectVideoImplementation(commandArgs, options) {

if (typeof commandArgs.stream.url === 'string') {
if (typeof global.webOS !== 'undefined') {
return withHTMLSubtitles(WebOsVideo);
return withVideoParams(withHTMLSubtitles(WebOsVideo));
}
if (typeof global.tizen !== 'undefined') {
return withHTMLSubtitles(TizenVideo);
return withVideoParams(withHTMLSubtitles(TizenVideo));
}
return withHTMLSubtitles(HTMLVideo);
return withVideoParams(withHTMLSubtitles(HTMLVideo));
}

return null;
Expand Down
22 changes: 22 additions & 0 deletions src/withStreamingServer/fetchVideoParams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
var url = require('url');

function fetchVideoParams(streamingServerURL, mediaURL) {
var queryParams = new URLSearchParams([['videoUrl', mediaURL]]);
return fetch(url.resolve(streamingServerURL, '/opensubHash?' + queryParams.toString()))
.then(function(resp) {
if (resp.ok) {
return resp.json();
}

throw new Error(resp.status + ' (' + resp.statusText + ')');
})
.then(function(resp) {
if (resp.error) {
throw new Error(resp.error);
}

return resp.result;
});
}

module.exports = fetchVideoParams;
76 changes: 55 additions & 21 deletions src/withStreamingServer/withStreamingServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var cloneDeep = require('lodash.clonedeep');
var deepFreeze = require('deep-freeze');
var mediaCapabilities = require('../mediaCapabilities');
var convertStream = require('./convertStream');
var fetchVideoParams = require('./fetchVideoParams');
var ERROR = require('../error');

function withStreamingServer(Video) {
Expand All @@ -27,10 +28,12 @@ function withStreamingServer(Video) {
var loadArgs = null;
var loaded = false;
var actionsQueue = [];
var videoParams = null;
var events = new EventEmitter();
var destroyed = false;
var observedProps = {
stream: false
stream: false,
videoParams: false
};

function flushActionsQueue() {
Expand Down Expand Up @@ -70,14 +73,18 @@ function withStreamingServer(Video) {
case 'stream': {
return loadArgs !== null ? loadArgs.stream : null;
}
case 'videoParams': {
return videoParams;
}
default: {
return videoPropValue;
}
}
}
function observeProp(propName) {
switch (propName) {
case 'stream': {
case 'stream':
case 'videoParams': {
events.emit('propValue', propName, getProp(propName, null));
observedProps[propName] = true;
return true;
Expand Down Expand Up @@ -127,7 +134,10 @@ function withStreamingServer(Video) {
.then(function(canPlay) {
if (canPlay) {
return {
url: mediaURL
mediaURL: mediaURL,
stream: {
url: mediaURL
}
};
}

Expand All @@ -148,27 +158,30 @@ function withStreamingServer(Video) {
queryParams.set('maxAudioChannels', maxAudioChannels);

return {
url: url.resolve(commandArgs.streamingServerURL, '/hlsv2/' + id + '/master.m3u8?' + queryParams.toString()),
subtitles: Array.isArray(commandArgs.stream.subtitles) ?
commandArgs.stream.subtitles.map(function(track) {
return Object.assign({}, track, {
url: typeof track.url === 'string' ?
url.resolve(commandArgs.streamingServerURL, '/subtitles.vtt?' + new URLSearchParams([['from', track.url]]).toString())
:
track.url
});
})
:
[],
behaviorHints: {
headers: {
'content-type': 'application/vnd.apple.mpegurl'
mediaURL: mediaURL,
stream: {
url: url.resolve(commandArgs.streamingServerURL, '/hlsv2/' + id + '/master.m3u8?' + queryParams.toString()),
subtitles: Array.isArray(commandArgs.stream.subtitles) ?
commandArgs.stream.subtitles.map(function(track) {
return Object.assign({}, track, {
url: typeof track.url === 'string' ?
url.resolve(commandArgs.streamingServerURL, '/subtitles.vtt?' + new URLSearchParams([['from', track.url]]).toString())
:
track.url
});
})
:
[],
behaviorHints: {
headers: {
'content-type': 'application/vnd.apple.mpegurl'
}
}
}
};
});
})
.then(function(stream) {
.then(function(result) {
if (commandArgs !== loadArgs) {
return;
}
Expand All @@ -177,11 +190,30 @@ function withStreamingServer(Video) {
type: 'command',
commandName: 'load',
commandArgs: Object.assign({}, commandArgs, {
stream: stream
stream: result.stream
})
});
loaded = true;
flushActionsQueue();
fetchVideoParams(commandArgs.streamingServerURL, result.mediaURL)
.then(function(result) {
if (commandArgs !== loadArgs) {
return;
}

videoParams = result;
onPropChanged('videoParams');
})
.catch(function(error) {
if (commandArgs !== loadArgs) {
return;
}

// eslint-disable-next-line no-console
console.error(error);
videoParams = { hash: null, size: null };
onPropChanged('videoParams');
});
})
.catch(function(error) {
if (commandArgs !== loadArgs) {
Expand Down Expand Up @@ -237,7 +269,9 @@ function withStreamingServer(Video) {
loadArgs = null;
loaded = false;
actionsQueue = [];
videoParams = null;
onPropChanged('stream');
onPropChanged('videoParams');
return false;
}
case 'destroy': {
Expand Down Expand Up @@ -336,7 +370,7 @@ function withStreamingServer(Video) {
VideoWithStreamingServer.manifest = {
name: Video.manifest.name + 'WithStreamingServer',
external: Video.manifest.external,
props: Video.manifest.props.concat(['stream'])
props: Video.manifest.props.concat(['stream', 'videoParams'])
.filter(function(value, index, array) { return array.indexOf(value) === index; }),
commands: Video.manifest.commands.concat(['load', 'unload', 'destroy', 'addExtraSubtitlesTracks'])
.filter(function(value, index, array) { return array.indexOf(value) === index; }),
Expand Down
3 changes: 3 additions & 0 deletions src/withVideoParams/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
var withVideoParams = require('./withVideoParams');

module.exports = withVideoParams;
140 changes: 140 additions & 0 deletions src/withVideoParams/withVideoParams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
var EventEmitter = require('eventemitter3');
var cloneDeep = require('lodash.clonedeep');
var deepFreeze = require('deep-freeze');

function withVideoParams(Video) {
function VideoWithVideoParams(options) {
options = options || {};

var video = new Video(options);
video.on('propValue', onVideoPropEvent.bind(null, 'propValue'));
video.on('propChanged', onVideoPropEvent.bind(null, 'propChanged'));
Video.manifest.events
.filter(function(eventName) {
return !['propValue', 'propChanged'].includes(eventName);
})
.forEach(function(eventName) {
video.on(eventName, onOtherVideoEvent(eventName));
});

var stream = null;
var events = new EventEmitter();
var destroyed = false;
var observedProps = {
videoParams: false
};

function onVideoPropEvent(eventName, propName, propValue) {
if (propName !== 'videoParams') {
events.emit(eventName, propName, getProp(propName, propValue));
}
if (propName === 'stream') {
stream = propValue;
onPropChanged('videoParams');
}
}
function onOtherVideoEvent(eventName) {
return function() {
events.emit.apply(events, [eventName].concat(Array.from(arguments)));
};
}
function onPropChanged(propName) {
if (observedProps[propName]) {
events.emit('propChanged', propName, getProp(propName, null));
}
}
function getProp(propName, videoPropValue) {
switch (propName) {
case 'videoParams': {
if (stream === null) {
return null;
}

return { hash: null, size: null };
}
default: {
return videoPropValue;
}
}
}
function observeProp(propName) {
switch (propName) {
case 'videoParams': {
events.emit('propValue', propName, getProp(propName, null));
observedProps[propName] = true;
return true;
}
default: {
return false;
}
}
}
function command(commandName) {
switch (commandName) {
case 'destroy': {
destroyed = true;
video.dispatch({ type: 'command', commandName: 'destroy' });
events.removeAllListeners();
return true;
}
default: {
return false;
}
}
}

this.on = function(eventName, listener) {
if (destroyed) {
throw new Error('Video is destroyed');
}

events.on(eventName, listener);
};
this.dispatch = function(action) {
if (destroyed) {
throw new Error('Video is destroyed');
}

if (action) {
action = deepFreeze(cloneDeep(action));
switch (action.type) {
case 'observeProp': {
if (observeProp(action.propName)) {
return;
}

break;
}
case 'command': {
if (command(action.commandName, action.commandArgs)) {
return;
}

break;
}
}
}

video.dispatch(action);
};
}

VideoWithVideoParams.canPlayStream = function(stream, options) {
return Video.canPlayStream(stream, options);
};

VideoWithVideoParams.manifest = {
name: Video.manifest.name + 'WithVideoParams',
external: Video.manifest.external,
props: Video.manifest.props.concat(['videoParams'])
.filter(function(value, index, array) { return array.indexOf(value) === index; }),
commands: Video.manifest.commands.concat(['destroy'])
.filter(function(value, index, array) { return array.indexOf(value) === index; }),
events: Video.manifest.events.concat(['propValue', 'propChanged'])
.filter(function(value, index, array) { return array.indexOf(value) === index; })
};

return VideoWithVideoParams;
}

module.exports = withVideoParams;

0 comments on commit 9150139

Please sign in to comment.