@stremio/stremio-video 0.0.23 → 0.0.25-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ChromecastSenderVideo/ChromecastSenderVideo.js +3 -1
- package/src/StremioVideo/selectVideoImplementation.js +6 -5
- package/src/mediaCapabilities.js +94 -0
- package/src/withStreamingServer/convertStream.js +5 -5
- package/src/withStreamingServer/createTorrent.js +9 -4
- package/src/withStreamingServer/fetchVideoParams.js +95 -0
- package/src/withStreamingServer/withStreamingServer.js +129 -28
- package/src/withVideoParams/index.js +3 -0
- package/src/withVideoParams/withVideoParams.js +143 -0
package/package.json
CHANGED
|
@@ -63,6 +63,7 @@ function ChromecastSenderVideo(options) {
|
|
|
63
63
|
volume: false,
|
|
64
64
|
muted: false,
|
|
65
65
|
playbackSpeed: false,
|
|
66
|
+
videoParams: false,
|
|
66
67
|
extraSubtitlesTracks: false,
|
|
67
68
|
selectedExtraSubtitlesTrackId: false,
|
|
68
69
|
extraSubtitlesDelay: false,
|
|
@@ -126,6 +127,7 @@ function ChromecastSenderVideo(options) {
|
|
|
126
127
|
onPropChanged('volume', null);
|
|
127
128
|
onPropChanged('muted', null);
|
|
128
129
|
onPropChanged('playbackSpeed', null);
|
|
130
|
+
onPropChanged('videoParams', null);
|
|
129
131
|
onPropChanged('extraSubtitlesTracks', []);
|
|
130
132
|
onPropChanged('selectedExtraSubtitlesTrackId', null);
|
|
131
133
|
onPropChanged('extraSubtitlesDelay', null);
|
|
@@ -190,7 +192,7 @@ ChromecastSenderVideo.canPlayStream = function() {
|
|
|
190
192
|
ChromecastSenderVideo.manifest = {
|
|
191
193
|
name: 'ChromecastSenderVideo',
|
|
192
194
|
external: true,
|
|
193
|
-
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'],
|
|
195
|
+
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'],
|
|
194
196
|
commands: ['load', 'unload', 'destroy', 'addExtraSubtitlesTracks'],
|
|
195
197
|
events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded', 'extraSubtitlesTrackLoaded', 'implementationChanged']
|
|
196
198
|
};
|
|
@@ -7,6 +7,7 @@ var IFrameVideo = require('../IFrameVideo');
|
|
|
7
7
|
var YouTubeVideo = require('../YouTubeVideo');
|
|
8
8
|
var withStreamingServer = require('../withStreamingServer');
|
|
9
9
|
var withHTMLSubtitles = require('../withHTMLSubtitles');
|
|
10
|
+
var withVideoParams = require('../withVideoParams');
|
|
10
11
|
|
|
11
12
|
function selectVideoImplementation(commandArgs, options) {
|
|
12
13
|
if (!commandArgs.stream || typeof commandArgs.stream.externalUrl === 'string') {
|
|
@@ -18,11 +19,11 @@ function selectVideoImplementation(commandArgs, options) {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
if (typeof commandArgs.stream.ytId === 'string') {
|
|
21
|
-
return withHTMLSubtitles(YouTubeVideo);
|
|
22
|
+
return withVideoParams(withHTMLSubtitles(YouTubeVideo));
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
if (typeof commandArgs.stream.playerFrameUrl === 'string') {
|
|
25
|
-
return IFrameVideo;
|
|
26
|
+
return withVideoParams(IFrameVideo);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
if (options.shellTransport) {
|
|
@@ -41,12 +42,12 @@ function selectVideoImplementation(commandArgs, options) {
|
|
|
41
42
|
|
|
42
43
|
if (typeof commandArgs.stream.url === 'string') {
|
|
43
44
|
if (typeof global.webOS !== 'undefined') {
|
|
44
|
-
return withHTMLSubtitles(WebOsVideo);
|
|
45
|
+
return withVideoParams(withHTMLSubtitles(WebOsVideo));
|
|
45
46
|
}
|
|
46
47
|
if (typeof global.tizen !== 'undefined') {
|
|
47
|
-
return withHTMLSubtitles(TizenVideo);
|
|
48
|
+
return withVideoParams(withHTMLSubtitles(TizenVideo));
|
|
48
49
|
}
|
|
49
|
-
return withHTMLSubtitles(HTMLVideo);
|
|
50
|
+
return withVideoParams(withHTMLSubtitles(HTMLVideo));
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
return null;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
var VIDEO_CODEC_CONFIGS = [
|
|
2
|
+
{
|
|
3
|
+
codec: 'h264',
|
|
4
|
+
mime: 'video/mp4; codecs="avc1.42E01E"',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
codec: 'h265',
|
|
8
|
+
mime: 'video/mp4; codecs="hev1.1.6.L150.B0"',
|
|
9
|
+
aliases: ['hevc']
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
codec: 'vp8',
|
|
13
|
+
mime: 'video/mp4; codecs="vp8"'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
codec: 'vp9',
|
|
17
|
+
mime: 'video/mp4; codecs="vp9"'
|
|
18
|
+
}
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
var AUDIO_CODEC_CONFIGS = [
|
|
22
|
+
{
|
|
23
|
+
codec: 'aac',
|
|
24
|
+
mime: 'audio/mp4; codecs="mp4a.40.2"'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
codec: 'mp3',
|
|
28
|
+
mime: 'audio/mp4; codecs="mp3"'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
codec: 'ac3',
|
|
32
|
+
mime: 'audio/mp4; codecs="ac-3"'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
codec: 'eac3',
|
|
36
|
+
mime: 'audio/mp4; codecs="ec-3"'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
codec: 'vorbis',
|
|
40
|
+
mime: 'audio/mp4; codecs="vorbis"'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
codec: 'opus',
|
|
44
|
+
mime: 'audio/mp4; codecs="opus"'
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function canPlay(config, options) {
|
|
49
|
+
return options.mediaElement.canPlayType(config.mime) ?
|
|
50
|
+
[config.codec].concat(config.aliases || [])
|
|
51
|
+
:
|
|
52
|
+
[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getMaxAudioChannels() {
|
|
56
|
+
if (/firefox/i.test(window.navigator.userAgent)) {
|
|
57
|
+
return 6;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!window.AudioContext) {
|
|
61
|
+
return 2;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var maxChannelCount = new AudioContext().destination.maxChannelCount;
|
|
65
|
+
return maxChannelCount > 0 ? maxChannelCount : 2;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getMediaCapabilities() {
|
|
69
|
+
var mediaElement = document.createElement('video');
|
|
70
|
+
var formats = ['mp4'];
|
|
71
|
+
var videoCodecs = VIDEO_CODEC_CONFIGS
|
|
72
|
+
.map(function(config) {
|
|
73
|
+
return canPlay(config, { mediaElement: mediaElement });
|
|
74
|
+
})
|
|
75
|
+
.reduce(function(result, value) {
|
|
76
|
+
return result.concat(value);
|
|
77
|
+
}, []);
|
|
78
|
+
var audioCodecs = AUDIO_CODEC_CONFIGS
|
|
79
|
+
.map(function(config) {
|
|
80
|
+
return canPlay(config, { mediaElement: mediaElement });
|
|
81
|
+
})
|
|
82
|
+
.reduce(function(result, value) {
|
|
83
|
+
return result.concat(value);
|
|
84
|
+
}, []);
|
|
85
|
+
var maxAudioChannels = getMaxAudioChannels();
|
|
86
|
+
return {
|
|
87
|
+
formats: formats,
|
|
88
|
+
videoCodecs: videoCodecs,
|
|
89
|
+
audioCodecs: audioCodecs,
|
|
90
|
+
maxAudioChannels: maxAudioChannels
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = getMediaCapabilities();
|
|
@@ -23,14 +23,14 @@ function convertStream(streamingServerURL, stream, seriesInfo) {
|
|
|
23
23
|
:
|
|
24
24
|
[];
|
|
25
25
|
createTorrent(streamingServerURL, parsedMagnetURI.infoHash, null, sources, seriesInfo)
|
|
26
|
-
.then(function(
|
|
27
|
-
resolve(url);
|
|
26
|
+
.then(function(torrent) {
|
|
27
|
+
resolve({ url: torrent.url, infoHash: torrent.infoHash, fileIdx: torrent.fileIdx });
|
|
28
28
|
})
|
|
29
29
|
.catch(function(error) {
|
|
30
30
|
reject(error);
|
|
31
31
|
});
|
|
32
32
|
} else {
|
|
33
|
-
resolve(stream.url);
|
|
33
|
+
resolve({ url: stream.url });
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
return;
|
|
@@ -38,8 +38,8 @@ function convertStream(streamingServerURL, stream, seriesInfo) {
|
|
|
38
38
|
|
|
39
39
|
if (typeof stream.infoHash === 'string') {
|
|
40
40
|
createTorrent(streamingServerURL, stream.infoHash, stream.fileIdx, stream.announce, seriesInfo)
|
|
41
|
-
.then(function(
|
|
42
|
-
resolve(url);
|
|
41
|
+
.then(function(torrent) {
|
|
42
|
+
resolve({ url: torrent.url, infoHash: torrent.infoHash, fileIdx: torrent.fileIdx });
|
|
43
43
|
})
|
|
44
44
|
.catch(function(error) {
|
|
45
45
|
reject(error);
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
var url = require('url');
|
|
2
2
|
|
|
3
|
-
function
|
|
3
|
+
function buildTorrent(streamingServerURL, infoHash, fileIdx, sources) {
|
|
4
4
|
var query = Array.isArray(sources) && sources.length > 0 ?
|
|
5
5
|
'?' + new URLSearchParams(sources.map(function(source) {
|
|
6
6
|
return ['tr', source];
|
|
7
7
|
}))
|
|
8
8
|
:
|
|
9
9
|
'';
|
|
10
|
-
return
|
|
10
|
+
return {
|
|
11
|
+
url: url.resolve(streamingServerURL, '/' + encodeURIComponent(infoHash) + '/' + encodeURIComponent(fileIdx)) + query,
|
|
12
|
+
infoHash: infoHash,
|
|
13
|
+
fileIdx: fileIdx,
|
|
14
|
+
sources: sources
|
|
15
|
+
};
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
function createTorrent(streamingServerURL, infoHash, fileIdx, sources, seriesInfo) {
|
|
14
19
|
if ((!Array.isArray(sources) || sources.length === 0) && (fileIdx !== null && isFinite(fileIdx))) {
|
|
15
|
-
return Promise.resolve(
|
|
20
|
+
return Promise.resolve(buildTorrent(streamingServerURL, infoHash, fileIdx, sources));
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
var body = {
|
|
@@ -58,7 +63,7 @@ function createTorrent(streamingServerURL, infoHash, fileIdx, sources, seriesInf
|
|
|
58
63
|
|
|
59
64
|
throw new Error(resp.status + ' (' + resp.statusText + ')');
|
|
60
65
|
}).then(function(resp) {
|
|
61
|
-
return
|
|
66
|
+
return buildTorrent(streamingServerURL, infoHash, body.guessFileIdx ? resp.guessedFileIdx : fileIdx, body.peerSearch ? body.peerSearch.sources : []);
|
|
62
67
|
});
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
var url = require('url');
|
|
2
|
+
|
|
3
|
+
function fetchOpensubtitlesParams(streamingServerURL, mediaURL, behaviorHints) {
|
|
4
|
+
var hash = behaviorHints && typeof behaviorHints.videoHash === 'string' ? behaviorHints.videoHash : null;
|
|
5
|
+
var size = behaviorHints && isFinite(behaviorHints.videoSize) ? behaviorHints.videoSize : null;
|
|
6
|
+
if (typeof hash === 'string' && size !== null && isFinite(size)) {
|
|
7
|
+
return Promise.resolve({ hash: hash, size: size });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
var queryParams = new URLSearchParams([['videoUrl', mediaURL]]);
|
|
11
|
+
return fetch(url.resolve(streamingServerURL, '/opensubHash?' + queryParams.toString()))
|
|
12
|
+
.then(function(resp) {
|
|
13
|
+
if (resp.ok) {
|
|
14
|
+
return resp.json();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
throw new Error(resp.status + ' (' + resp.statusText + ')');
|
|
18
|
+
})
|
|
19
|
+
.then(function(resp) {
|
|
20
|
+
if (resp.error) {
|
|
21
|
+
throw new Error(resp.error);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
hash: typeof hash === 'string' ?
|
|
26
|
+
hash
|
|
27
|
+
:
|
|
28
|
+
resp.result && typeof resp.result.hash === 'string' ?
|
|
29
|
+
resp.result.hash
|
|
30
|
+
:
|
|
31
|
+
null,
|
|
32
|
+
size: size !== null && isFinite(size) ?
|
|
33
|
+
size
|
|
34
|
+
:
|
|
35
|
+
resp.result && typeof resp.result.size ?
|
|
36
|
+
resp.result.size
|
|
37
|
+
:
|
|
38
|
+
null
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fetchFilename(streamingServerURL, mediaURL, infoHash, fileIdx, behaviorHints) {
|
|
44
|
+
if (behaviorHints && typeof behaviorHints.filename === 'string') {
|
|
45
|
+
return Promise.resolve(behaviorHints.filename);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (infoHash) {
|
|
49
|
+
return fetch(url.resolve(streamingServerURL, '/' + encodeURIComponent(infoHash) + '/' + encodeURIComponent(fileIdx) + '/stats.json'))
|
|
50
|
+
.then(function(resp) {
|
|
51
|
+
if (resp.ok) {
|
|
52
|
+
return resp.json();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new Error(resp.status + ' (' + resp.statusText + ')');
|
|
56
|
+
})
|
|
57
|
+
.then(function(resp) {
|
|
58
|
+
if (!resp || typeof resp.streamName !== 'string') {
|
|
59
|
+
throw new Error('Could not retrieve filename from torrent');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return resp.streamName;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return Promise.resolve(decodeURIComponent(mediaURL.split('/').pop()));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fetchVideoParams(streamingServerURL, mediaURL, infoHash, fileIdx, behaviorHints) {
|
|
70
|
+
return Promise.allSettled([
|
|
71
|
+
fetchOpensubtitlesParams(streamingServerURL, mediaURL, behaviorHints),
|
|
72
|
+
fetchFilename(streamingServerURL, mediaURL, infoHash, fileIdx, behaviorHints)
|
|
73
|
+
]).then(function(results) {
|
|
74
|
+
var result = { hash: null, size: null, filename: null };
|
|
75
|
+
|
|
76
|
+
if (results[0].status === 'fulfilled') {
|
|
77
|
+
result.hash = results[0].value.hash;
|
|
78
|
+
result.size = results[0].value.size;
|
|
79
|
+
} else if (results[0].reason) {
|
|
80
|
+
// eslint-disable-next-line no-console
|
|
81
|
+
console.error(results[0].reason);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (results[1].status === 'fulfilled') {
|
|
85
|
+
result.filename = results[1].value;
|
|
86
|
+
} else if (results[1].reason) {
|
|
87
|
+
// eslint-disable-next-line no-console
|
|
88
|
+
console.error(results[1].reason);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = fetchVideoParams;
|
|
@@ -3,7 +3,9 @@ var url = require('url');
|
|
|
3
3
|
var hat = require('hat');
|
|
4
4
|
var cloneDeep = require('lodash.clonedeep');
|
|
5
5
|
var deepFreeze = require('deep-freeze');
|
|
6
|
+
var mediaCapabilities = require('../mediaCapabilities');
|
|
6
7
|
var convertStream = require('./convertStream');
|
|
8
|
+
var fetchVideoParams = require('./fetchVideoParams');
|
|
7
9
|
var ERROR = require('../error');
|
|
8
10
|
|
|
9
11
|
function withStreamingServer(Video) {
|
|
@@ -26,10 +28,12 @@ function withStreamingServer(Video) {
|
|
|
26
28
|
var loadArgs = null;
|
|
27
29
|
var loaded = false;
|
|
28
30
|
var actionsQueue = [];
|
|
31
|
+
var videoParams = null;
|
|
29
32
|
var events = new EventEmitter();
|
|
30
33
|
var destroyed = false;
|
|
31
34
|
var observedProps = {
|
|
32
|
-
stream: false
|
|
35
|
+
stream: false,
|
|
36
|
+
videoParams: false
|
|
33
37
|
};
|
|
34
38
|
|
|
35
39
|
function flushActionsQueue() {
|
|
@@ -69,6 +73,9 @@ function withStreamingServer(Video) {
|
|
|
69
73
|
case 'stream': {
|
|
70
74
|
return loadArgs !== null ? loadArgs.stream : null;
|
|
71
75
|
}
|
|
76
|
+
case 'videoParams': {
|
|
77
|
+
return videoParams;
|
|
78
|
+
}
|
|
72
79
|
default: {
|
|
73
80
|
return videoPropValue;
|
|
74
81
|
}
|
|
@@ -76,7 +83,8 @@ function withStreamingServer(Video) {
|
|
|
76
83
|
}
|
|
77
84
|
function observeProp(propName) {
|
|
78
85
|
switch (propName) {
|
|
79
|
-
case 'stream':
|
|
86
|
+
case 'stream':
|
|
87
|
+
case 'videoParams': {
|
|
80
88
|
events.emit('propValue', propName, getProp(propName, null));
|
|
81
89
|
observedProps[propName] = true;
|
|
82
90
|
return true;
|
|
@@ -95,8 +103,33 @@ function withStreamingServer(Video) {
|
|
|
95
103
|
loadArgs = commandArgs;
|
|
96
104
|
onPropChanged('stream');
|
|
97
105
|
convertStream(commandArgs.streamingServerURL, commandArgs.stream, commandArgs.seriesInfo)
|
|
98
|
-
.then(function(
|
|
99
|
-
|
|
106
|
+
.then(function(result) {
|
|
107
|
+
var mediaURL = result.url;
|
|
108
|
+
var infoHash = result.infoHash;
|
|
109
|
+
var fileIdx = result.fileIdx;
|
|
110
|
+
var formats = Array.isArray(commandArgs.formats) ?
|
|
111
|
+
commandArgs.formats
|
|
112
|
+
:
|
|
113
|
+
mediaCapabilities.formats;
|
|
114
|
+
var videoCodecs = Array.isArray(commandArgs.videoCodecs) ?
|
|
115
|
+
commandArgs.videoCodecs
|
|
116
|
+
:
|
|
117
|
+
mediaCapabilities.videoCodecs;
|
|
118
|
+
var audioCodecs = Array.isArray(commandArgs.audioCodecs) ?
|
|
119
|
+
commandArgs.audioCodecs
|
|
120
|
+
:
|
|
121
|
+
mediaCapabilities.audioCodecs;
|
|
122
|
+
var maxAudioChannels = commandArgs.maxAudioChannels !== null && isFinite(commandArgs.maxAudioChannels) ?
|
|
123
|
+
commandArgs.maxAudioChannels
|
|
124
|
+
:
|
|
125
|
+
mediaCapabilities.maxAudioChannels;
|
|
126
|
+
var canPlayStreamOptions = Object.assign({}, commandArgs, {
|
|
127
|
+
formats: formats,
|
|
128
|
+
videoCodecs: videoCodecs,
|
|
129
|
+
audioCodecs: audioCodecs,
|
|
130
|
+
maxAudioChannels: maxAudioChannels
|
|
131
|
+
});
|
|
132
|
+
return (commandArgs.forceTranscoding ? Promise.resolve(false) : VideoWithStreamingServer.canPlayStream({ url: mediaURL }, canPlayStreamOptions))
|
|
100
133
|
.catch(function(error) {
|
|
101
134
|
console.warn('Media probe error', error);
|
|
102
135
|
return false;
|
|
@@ -104,7 +137,12 @@ function withStreamingServer(Video) {
|
|
|
104
137
|
.then(function(canPlay) {
|
|
105
138
|
if (canPlay) {
|
|
106
139
|
return {
|
|
107
|
-
|
|
140
|
+
mediaURL: mediaURL,
|
|
141
|
+
infoHash: infoHash,
|
|
142
|
+
fileIdx: fileIdx,
|
|
143
|
+
stream: {
|
|
144
|
+
url: mediaURL
|
|
145
|
+
}
|
|
108
146
|
};
|
|
109
147
|
}
|
|
110
148
|
|
|
@@ -113,32 +151,44 @@ function withStreamingServer(Video) {
|
|
|
113
151
|
if (commandArgs.forceTranscoding) {
|
|
114
152
|
queryParams.set('forceTranscoding', '1');
|
|
115
153
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
154
|
+
|
|
155
|
+
videoCodecs.forEach(function(videoCodec) {
|
|
156
|
+
queryParams.append('videoCodecs', videoCodec);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
audioCodecs.forEach(function(audioCodec) {
|
|
160
|
+
queryParams.append('audioCodecs', audioCodec);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
queryParams.set('maxAudioChannels', maxAudioChannels);
|
|
119
164
|
|
|
120
165
|
return {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
166
|
+
mediaURL: mediaURL,
|
|
167
|
+
infoHash: infoHash,
|
|
168
|
+
fileIdx: fileIdx,
|
|
169
|
+
stream: {
|
|
170
|
+
url: url.resolve(commandArgs.streamingServerURL, '/hlsv2/' + id + '/master.m3u8?' + queryParams.toString()),
|
|
171
|
+
subtitles: Array.isArray(commandArgs.stream.subtitles) ?
|
|
172
|
+
commandArgs.stream.subtitles.map(function(track) {
|
|
173
|
+
return Object.assign({}, track, {
|
|
174
|
+
url: typeof track.url === 'string' ?
|
|
175
|
+
url.resolve(commandArgs.streamingServerURL, '/subtitles.vtt?' + new URLSearchParams([['from', track.url]]).toString())
|
|
176
|
+
:
|
|
177
|
+
track.url
|
|
178
|
+
});
|
|
179
|
+
})
|
|
180
|
+
:
|
|
181
|
+
[],
|
|
182
|
+
behaviorHints: {
|
|
183
|
+
headers: {
|
|
184
|
+
'content-type': 'application/vnd.apple.mpegurl'
|
|
185
|
+
}
|
|
136
186
|
}
|
|
137
187
|
}
|
|
138
188
|
};
|
|
139
189
|
});
|
|
140
190
|
})
|
|
141
|
-
.then(function(
|
|
191
|
+
.then(function(result) {
|
|
142
192
|
if (commandArgs !== loadArgs) {
|
|
143
193
|
return;
|
|
144
194
|
}
|
|
@@ -147,11 +197,30 @@ function withStreamingServer(Video) {
|
|
|
147
197
|
type: 'command',
|
|
148
198
|
commandName: 'load',
|
|
149
199
|
commandArgs: Object.assign({}, commandArgs, {
|
|
150
|
-
stream: stream
|
|
200
|
+
stream: result.stream
|
|
151
201
|
})
|
|
152
202
|
});
|
|
153
203
|
loaded = true;
|
|
154
204
|
flushActionsQueue();
|
|
205
|
+
fetchVideoParams(commandArgs.streamingServerURL, result.mediaURL, result.infoHash, result.fileIdx, commandArgs.stream.behaviorHints)
|
|
206
|
+
.then(function(result) {
|
|
207
|
+
if (commandArgs !== loadArgs) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
videoParams = result;
|
|
212
|
+
onPropChanged('videoParams');
|
|
213
|
+
})
|
|
214
|
+
.catch(function(error) {
|
|
215
|
+
if (commandArgs !== loadArgs) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// eslint-disable-next-line no-console
|
|
220
|
+
console.error(error);
|
|
221
|
+
videoParams = { hash: null, size: null, filename: null };
|
|
222
|
+
onPropChanged('videoParams');
|
|
223
|
+
});
|
|
155
224
|
})
|
|
156
225
|
.catch(function(error) {
|
|
157
226
|
if (commandArgs !== loadArgs) {
|
|
@@ -207,7 +276,9 @@ function withStreamingServer(Video) {
|
|
|
207
276
|
loadArgs = null;
|
|
208
277
|
loaded = false;
|
|
209
278
|
actionsQueue = [];
|
|
279
|
+
videoParams = null;
|
|
210
280
|
onPropChanged('stream');
|
|
281
|
+
onPropChanged('videoParams');
|
|
211
282
|
return false;
|
|
212
283
|
}
|
|
213
284
|
case 'destroy': {
|
|
@@ -269,14 +340,44 @@ function withStreamingServer(Video) {
|
|
|
269
340
|
};
|
|
270
341
|
}
|
|
271
342
|
|
|
272
|
-
VideoWithStreamingServer.canPlayStream = function(stream) {
|
|
273
|
-
return Video.canPlayStream(stream)
|
|
343
|
+
VideoWithStreamingServer.canPlayStream = function(stream, options) {
|
|
344
|
+
return Video.canPlayStream(stream)
|
|
345
|
+
.then(function(canPlay) {
|
|
346
|
+
if (!canPlay) {
|
|
347
|
+
throw new Error('Fallback using /hlsv2/probe');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return canPlay;
|
|
351
|
+
})
|
|
352
|
+
.catch(function() {
|
|
353
|
+
var queryParams = new URLSearchParams([['mediaURL', stream.url]]);
|
|
354
|
+
return fetch(url.resolve(options.streamingServerURL, '/hlsv2/probe?' + queryParams.toString()))
|
|
355
|
+
.then(function(resp) {
|
|
356
|
+
return resp.json();
|
|
357
|
+
})
|
|
358
|
+
.then(function(probe) {
|
|
359
|
+
var isFormatSupported = options.formats.some(function(format) {
|
|
360
|
+
return probe.format.name.indexOf(format) !== -1;
|
|
361
|
+
});
|
|
362
|
+
var areStreamsSupported = probe.streams.every(function(stream) {
|
|
363
|
+
if (stream.track === 'audio') {
|
|
364
|
+
return stream.channels <= options.maxAudioChannels &&
|
|
365
|
+
options.audioCodecs.indexOf(stream.codec) !== -1;
|
|
366
|
+
} else if (stream.track === 'video') {
|
|
367
|
+
return options.videoCodecs.indexOf(stream.codec) !== -1;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return true;
|
|
371
|
+
});
|
|
372
|
+
return isFormatSupported && areStreamsSupported;
|
|
373
|
+
});
|
|
374
|
+
});
|
|
274
375
|
};
|
|
275
376
|
|
|
276
377
|
VideoWithStreamingServer.manifest = {
|
|
277
378
|
name: Video.manifest.name + 'WithStreamingServer',
|
|
278
379
|
external: Video.manifest.external,
|
|
279
|
-
props: Video.manifest.props.concat(['stream'])
|
|
380
|
+
props: Video.manifest.props.concat(['stream', 'videoParams'])
|
|
280
381
|
.filter(function(value, index, array) { return array.indexOf(value) === index; }),
|
|
281
382
|
commands: Video.manifest.commands.concat(['load', 'unload', 'destroy', 'addExtraSubtitlesTracks'])
|
|
282
383
|
.filter(function(value, index, array) { return array.indexOf(value) === index; }),
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
var EventEmitter = require('eventemitter3');
|
|
2
|
+
var cloneDeep = require('lodash.clonedeep');
|
|
3
|
+
var deepFreeze = require('deep-freeze');
|
|
4
|
+
|
|
5
|
+
function withVideoParams(Video) {
|
|
6
|
+
function VideoWithVideoParams(options) {
|
|
7
|
+
options = options || {};
|
|
8
|
+
|
|
9
|
+
var video = new Video(options);
|
|
10
|
+
video.on('propValue', onVideoPropEvent.bind(null, 'propValue'));
|
|
11
|
+
video.on('propChanged', onVideoPropEvent.bind(null, 'propChanged'));
|
|
12
|
+
Video.manifest.events
|
|
13
|
+
.filter(function(eventName) {
|
|
14
|
+
return !['propValue', 'propChanged'].includes(eventName);
|
|
15
|
+
})
|
|
16
|
+
.forEach(function(eventName) {
|
|
17
|
+
video.on(eventName, onOtherVideoEvent(eventName));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
var stream = null;
|
|
21
|
+
var events = new EventEmitter();
|
|
22
|
+
var destroyed = false;
|
|
23
|
+
var observedProps = {
|
|
24
|
+
videoParams: false
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function onVideoPropEvent(eventName, propName, propValue) {
|
|
28
|
+
if (propName !== 'videoParams') {
|
|
29
|
+
events.emit(eventName, propName, getProp(propName, propValue));
|
|
30
|
+
}
|
|
31
|
+
if (propName === 'stream') {
|
|
32
|
+
stream = propValue;
|
|
33
|
+
onPropChanged('videoParams');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function onOtherVideoEvent(eventName) {
|
|
37
|
+
return function() {
|
|
38
|
+
events.emit.apply(events, [eventName].concat(Array.from(arguments)));
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function onPropChanged(propName) {
|
|
42
|
+
if (observedProps[propName]) {
|
|
43
|
+
events.emit('propChanged', propName, getProp(propName, null));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function getProp(propName, videoPropValue) {
|
|
47
|
+
switch (propName) {
|
|
48
|
+
case 'videoParams': {
|
|
49
|
+
if (stream === null) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
var hash = stream.behaviorHints && typeof stream.behaviorHints.videoHash === 'string' ? stream.behaviorHints.videoHash : null;
|
|
54
|
+
var size = stream.behaviorHints && stream.behaviorHints.videoSize !== null && isFinite(stream.behaviorHints.videoSize) ? stream.behaviorHints.videoSize : null;
|
|
55
|
+
var filename = stream.behaviorHints && typeof stream.behaviorHints.filename === 'string' ? stream.behaviorHints.filename : null;
|
|
56
|
+
return { hash: hash, size: size, filename: filename };
|
|
57
|
+
}
|
|
58
|
+
default: {
|
|
59
|
+
return videoPropValue;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function observeProp(propName) {
|
|
64
|
+
switch (propName) {
|
|
65
|
+
case 'videoParams': {
|
|
66
|
+
events.emit('propValue', propName, getProp(propName, null));
|
|
67
|
+
observedProps[propName] = true;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
default: {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function command(commandName) {
|
|
76
|
+
switch (commandName) {
|
|
77
|
+
case 'destroy': {
|
|
78
|
+
destroyed = true;
|
|
79
|
+
video.dispatch({ type: 'command', commandName: 'destroy' });
|
|
80
|
+
events.removeAllListeners();
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
default: {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.on = function(eventName, listener) {
|
|
90
|
+
if (destroyed) {
|
|
91
|
+
throw new Error('Video is destroyed');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
events.on(eventName, listener);
|
|
95
|
+
};
|
|
96
|
+
this.dispatch = function(action) {
|
|
97
|
+
if (destroyed) {
|
|
98
|
+
throw new Error('Video is destroyed');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (action) {
|
|
102
|
+
action = deepFreeze(cloneDeep(action));
|
|
103
|
+
switch (action.type) {
|
|
104
|
+
case 'observeProp': {
|
|
105
|
+
if (observeProp(action.propName)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case 'command': {
|
|
112
|
+
if (command(action.commandName, action.commandArgs)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
video.dispatch(action);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
VideoWithVideoParams.canPlayStream = function(stream, options) {
|
|
126
|
+
return Video.canPlayStream(stream, options);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
VideoWithVideoParams.manifest = {
|
|
130
|
+
name: Video.manifest.name + 'WithVideoParams',
|
|
131
|
+
external: Video.manifest.external,
|
|
132
|
+
props: Video.manifest.props.concat(['videoParams'])
|
|
133
|
+
.filter(function(value, index, array) { return array.indexOf(value) === index; }),
|
|
134
|
+
commands: Video.manifest.commands.concat(['destroy'])
|
|
135
|
+
.filter(function(value, index, array) { return array.indexOf(value) === index; }),
|
|
136
|
+
events: Video.manifest.events.concat(['propValue', 'propChanged'])
|
|
137
|
+
.filter(function(value, index, array) { return array.indexOf(value) === index; })
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return VideoWithVideoParams;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = withVideoParams;
|