@stremio/stremio-video 0.0.8 → 0.0.12

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.
@@ -1,92 +1,42 @@
1
1
  var EventEmitter = require('eventemitter3');
2
+ var url = require('url');
3
+ var hat = require('hat');
2
4
  var cloneDeep = require('lodash.clonedeep');
3
5
  var deepFreeze = require('deep-freeze');
4
- var convertStreamToURL = require('./convertStreamToURL');
5
- var createTranscoder = require('./createTranscoder');
6
- var transcodeNextSegment = require('./transcodeNextSegment');
6
+ var convertStream = require('./convertStream');
7
7
  var ERROR = require('../error');
8
8
 
9
- var STARVATION_THRESHOLD = 25000;
10
- var STARVATION_TIMEOUT = 1000;
11
-
12
9
  function withStreamingServer(Video) {
13
10
  function VideoWithStreamingServer(options) {
14
11
  options = options || {};
15
12
 
16
13
  var video = new Video(options);
17
14
  video.on('error', onVideoError);
18
- video.on('propChanged', onPropEvent.bind(null, 'propChanged'));
19
- video.on('propValue', onPropEvent.bind(null, 'propValue'));
15
+ video.on('propValue', onVideoPropEvent.bind(null, 'propValue'));
16
+ video.on('propChanged', onVideoPropEvent.bind(null, 'propChanged'));
20
17
  Video.manifest.events
21
18
  .filter(function(eventName) {
22
- return !['error', 'propChanged', 'propValue'].includes(eventName);
19
+ return !['error', 'propValue', 'propChanged'].includes(eventName);
23
20
  })
24
21
  .forEach(function(eventName) {
25
- video.on(eventName, onOtherEvent(eventName));
22
+ video.on(eventName, onOtherVideoEvent(eventName));
26
23
  });
27
24
 
28
- var videoState = {
29
- time: null,
30
- duration: null
31
- };
25
+ var self = this;
32
26
  var loadArgs = null;
33
- var transcoder = null;
34
- var transcodingNextSegment = false;
35
- var starvationHandlerTimeoutId = null;
36
- var lastStarvationDuration = null;
27
+ var loaded = false;
28
+ var actionsQueue = [];
37
29
  var events = new EventEmitter();
38
30
  var destroyed = false;
39
31
  var observedProps = {
40
32
  stream: false
41
33
  };
42
34
 
43
- function isStarving() {
44
- return transcoder !== null &&
45
- !transcoder.ended &&
46
- !transcodingNextSegment &&
47
- starvationHandlerTimeoutId === null &&
48
- videoState.time !== null &&
49
- videoState.duration !== null &&
50
- videoState.duration !== lastStarvationDuration &&
51
- videoState.time + STARVATION_THRESHOLD > videoState.duration;
52
- }
53
- function onStarving() {
54
- transcodingNextSegment = true;
55
- lastStarvationDuration = videoState.duration;
56
- var loadingТranscoder = transcoder;
57
- transcodeNextSegment(transcoder.streamingServerURL, transcoder.hash)
58
- .then(function(resp) {
59
- if (loadingТranscoder !== transcoder) {
60
- return;
61
- }
62
-
63
- if (resp.error) {
64
- if (resp.error.code !== 21) {
65
- throw resp.error;
66
- }
67
-
68
- command('load', Object.assign({}, loadArgs, {
69
- time: videoState.time
70
- }));
71
- return;
72
- }
73
-
74
- transcoder.ended = resp.ended;
75
- transcodingNextSegment = false;
76
- if (isStarving()) {
77
- onStarving();
78
- }
79
- })
80
- .catch(function(error) {
81
- if (loadingТranscoder !== transcoder) {
82
- return;
83
- }
84
-
85
- onError(Object.assign({}, ERROR.WITH_STREAMING_SERVER.TRANSCODING_FAILED, {
86
- critical: true,
87
- error: error
88
- }));
89
- });
35
+ function flushActionsQueue() {
36
+ while (actionsQueue.length > 0) {
37
+ var action = actionsQueue.shift();
38
+ self.dispatch.call(self, action);
39
+ }
90
40
  }
91
41
  function onVideoError(error) {
92
42
  events.emit('error', error);
@@ -94,41 +44,17 @@ function withStreamingServer(Video) {
94
44
  command('unload');
95
45
  }
96
46
  }
97
- function onPropEvent(eventName, propName, propValue) {
98
- switch (propName) {
99
- case 'time': {
100
- videoState.time = propValue;
101
- if (isStarving()) {
102
- onStarving();
103
- }
104
- break;
105
- }
106
- case 'duration': {
107
- videoState.duration = propValue;
108
- clearTimeout(starvationHandlerTimeoutId);
109
- starvationHandlerTimeoutId = !transcodingNextSegment ?
110
- setTimeout(function() {
111
- starvationHandlerTimeoutId = null;
112
- if (isStarving()) {
113
- onStarving();
114
- }
115
- }, STARVATION_TIMEOUT)
116
- :
117
- null;
118
- break;
119
- }
120
- }
121
-
47
+ function onVideoPropEvent(eventName, propName, propValue) {
122
48
  events.emit(eventName, propName, getProp(propName, propValue));
123
49
  }
124
- function onOtherEvent(eventName) {
50
+ function onOtherVideoEvent(eventName) {
125
51
  return function() {
126
52
  events.emit.apply(events, [eventName].concat(Array.from(arguments)));
127
53
  };
128
54
  }
129
55
  function onPropChanged(propName) {
130
56
  if (observedProps[propName]) {
131
- events.emit('propChanged', propName, getProp(propName));
57
+ events.emit('propChanged', propName, getProp(propName, null));
132
58
  }
133
59
  }
134
60
  function onError(error) {
@@ -143,24 +69,6 @@ function withStreamingServer(Video) {
143
69
  case 'stream': {
144
70
  return loadArgs !== null ? loadArgs.stream : null;
145
71
  }
146
- case 'time': {
147
- return videoPropValue !== null && transcoder !== null ?
148
- videoPropValue + transcoder.timeOffset
149
- :
150
- videoPropValue;
151
- }
152
- case 'duration': {
153
- return transcoder !== null ?
154
- transcoder.duration
155
- :
156
- videoPropValue;
157
- }
158
- case 'buffered': {
159
- return videoPropValue !== null && transcoder !== null ?
160
- videoPropValue + transcoder.timeOffset
161
- :
162
- videoPropValue;
163
- }
164
72
  default: {
165
73
  return videoPropValue;
166
74
  }
@@ -169,7 +77,7 @@ function withStreamingServer(Video) {
169
77
  function observeProp(propName) {
170
78
  switch (propName) {
171
79
  case 'stream': {
172
- events.emit('propValue', propName, getProp(propName));
80
+ events.emit('propValue', propName, getProp(propName, null));
173
81
  observedProps[propName] = true;
174
82
  return true;
175
83
  }
@@ -178,27 +86,6 @@ function withStreamingServer(Video) {
178
86
  }
179
87
  }
180
88
  }
181
- function setProp(propName, propValue) {
182
- switch (propName) {
183
- case 'time': {
184
- if (transcoder !== null) {
185
- if (propValue !== null && isFinite(propValue)) {
186
- var commandArgs = Object.assign({}, loadArgs, {
187
- time: parseInt(propValue, 10)
188
- });
189
- command('load', commandArgs);
190
- }
191
-
192
- return true;
193
- }
194
-
195
- return false;
196
- }
197
- default: {
198
- return false;
199
- }
200
- }
201
- }
202
89
  function command(commandName, commandArgs) {
203
90
  switch (commandName) {
204
91
  case 'load': {
@@ -207,84 +94,106 @@ function withStreamingServer(Video) {
207
94
  video.dispatch({ type: 'command', commandName: 'unload' });
208
95
  loadArgs = commandArgs;
209
96
  onPropChanged('stream');
210
- convertStreamToURL(commandArgs.streamingServerURL, commandArgs.stream, commandArgs.seriesInfo)
97
+ convertStream(commandArgs.streamingServerURL, commandArgs.stream, commandArgs.seriesInfo)
211
98
  .then(function(mediaURL) {
212
- return (commandArgs.forceTranscoding ? Promise.resolve(false) : Video.canPlayStream({ url: mediaURL }))
213
- .catch(function(error) {
214
- throw Object.assign({}, ERROR.UNKNOWN_ERROR, {
215
- error: error,
216
- stream: commandArgs.stream
217
- });
218
- })
219
- .then(function(canPlay) {
220
- if (canPlay) {
221
- return {
222
- transcoder: null,
223
- loadArgsExt: {
224
- stream: {
225
- url: mediaURL
226
- }
227
- }
228
- };
229
- }
99
+ var id = hat();
100
+ var queryParams = new URLSearchParams([['mediaURL', mediaURL]]);
101
+ if (commandArgs.forceTranscoding) {
102
+ queryParams.set('forceTranscoding', '1');
103
+ }
104
+ if (commandArgs.audioChannels !== null && isFinite(commandArgs.audioChannels)) {
105
+ queryParams.set('audioChannels', commandArgs.audioChannels);
106
+ }
230
107
 
231
- var time = commandArgs.time !== null && isFinite(commandArgs.time) ? parseInt(commandArgs.time, 10) : 0;
232
- return createTranscoder(commandArgs.streamingServerURL, mediaURL, time, commandArgs.audioChannels)
233
- .then(function(transcoder) {
234
- return {
235
- transcoder: transcoder,
236
- loadArgsExt: {
237
- time: 0,
238
- stream: {
239
- url: transcoder.url,
240
- behaviorHints: {
241
- headers: {
242
- 'content-type': 'application/vnd.apple.mpegurl'
243
- }
244
- }
245
- }
246
- }
247
- };
108
+ return {
109
+ url: url.resolve(commandArgs.streamingServerURL, '/hlsv2/' + id + '/master.m3u8?' + queryParams.toString()),
110
+ subtitles: Array.isArray(commandArgs.stream.subtitles) ?
111
+ commandArgs.stream.subtitles.map(function(track) {
112
+ return Object.assign({}, track, {
113
+ url: typeof track.url === 'string' ?
114
+ url.resolve(commandArgs.streamingServerURL, '/subtitles.vtt?' + new URLSearchParams([['from', track.url]]).toString())
115
+ :
116
+ track.url
248
117
  });
249
- });
118
+ })
119
+ :
120
+ [],
121
+ behaviorHints: {
122
+ headers: {
123
+ 'content-type': 'application/vnd.apple.mpegurl'
124
+ }
125
+ }
126
+ };
250
127
  })
251
- .then(function(result) {
128
+ .then(function(stream) {
252
129
  if (commandArgs !== loadArgs) {
253
130
  return;
254
131
  }
255
132
 
256
- transcoder = result.transcoder;
257
133
  video.dispatch({
258
134
  type: 'command',
259
135
  commandName: 'load',
260
- commandArgs: Object.assign({}, commandArgs, result.loadArgsExt)
136
+ commandArgs: Object.assign({}, commandArgs, {
137
+ stream: stream
138
+ })
261
139
  });
140
+ loaded = true;
141
+ flushActionsQueue();
262
142
  })
263
143
  .catch(function(error) {
264
144
  if (commandArgs !== loadArgs) {
265
145
  return;
266
146
  }
267
147
 
268
- onError(Object.assign({}, error, {
269
- critical: true
148
+ onError(Object.assign({}, ERROR.WITH_STREAMING_SERVER.CONVERT_FAILED, {
149
+ error: error,
150
+ critical: true,
151
+ stream: commandArgs.stream,
152
+ streamingServerURL: commandArgs.streamingServerURL
270
153
  }));
271
154
  });
272
155
  } else {
273
156
  onError(Object.assign({}, ERROR.UNSUPPORTED_STREAM, {
274
157
  critical: true,
275
- stream: commandArgs ? commandArgs.stream : null
158
+ stream: commandArgs ? commandArgs.stream : null,
159
+ streamingServerURL: commandArgs && typeof commandArgs.streamingServerURL === 'string' ? commandArgs.streamingServerURL : null
276
160
  }));
277
161
  }
278
162
 
279
163
  return true;
280
164
  }
165
+ case 'addExtraSubtitlesTracks': {
166
+ if (loadArgs && commandArgs && Array.isArray(commandArgs.tracks)) {
167
+ if (loaded) {
168
+ video.dispatch({
169
+ type: 'command',
170
+ commandName: 'addExtraSubtitlesTracks',
171
+ commandArgs: Object.assign({}, commandArgs, {
172
+ tracks: commandArgs.tracks.map(function(track) {
173
+ return Object.assign({}, track, {
174
+ url: typeof track.url === 'string' ?
175
+ url.resolve(loadArgs.streamingServerURL, '/subtitles.vtt?' + new URLSearchParams([['from', track.url]]).toString())
176
+ :
177
+ track.url
178
+ });
179
+ })
180
+ })
181
+ });
182
+ } else {
183
+ actionsQueue.push({
184
+ type: 'command',
185
+ commandName: 'addExtraSubtitlesTracks',
186
+ commandArgs: commandArgs
187
+ });
188
+ }
189
+ }
190
+
191
+ return true;
192
+ }
281
193
  case 'unload': {
282
- clearTimeout(starvationHandlerTimeoutId);
283
194
  loadArgs = null;
284
- transcoder = null;
285
- transcodingNextSegment = false;
286
- starvationHandlerTimeoutId = null;
287
- lastStarvationDuration = null;
195
+ loaded = false;
196
+ actionsQueue = [];
288
197
  onPropChanged('stream');
289
198
  return false;
290
199
  }
@@ -296,6 +205,16 @@ function withStreamingServer(Video) {
296
205
  return true;
297
206
  }
298
207
  default: {
208
+ if (!loaded) {
209
+ actionsQueue.push({
210
+ type: 'command',
211
+ commandName: commandName,
212
+ commandArgs: commandArgs
213
+ });
214
+
215
+ return true;
216
+ }
217
+
299
218
  return false;
300
219
  }
301
220
  }
@@ -323,13 +242,6 @@ function withStreamingServer(Video) {
323
242
 
324
243
  break;
325
244
  }
326
- case 'setProp': {
327
- if (setProp(action.propName, action.propValue)) {
328
- return;
329
- }
330
-
331
- break;
332
- }
333
245
  case 'command': {
334
246
  if (command(action.commandName, action.commandArgs)) {
335
247
  return;
@@ -353,9 +265,9 @@ function withStreamingServer(Video) {
353
265
  external: Video.manifest.external,
354
266
  props: Video.manifest.props.concat(['stream'])
355
267
  .filter(function(value, index, array) { return array.indexOf(value) === index; }),
356
- commands: Video.manifest.commands.concat(['load', 'unload', 'destroy'])
268
+ commands: Video.manifest.commands.concat(['load', 'unload', 'destroy', 'addExtraSubtitlesTracks'])
357
269
  .filter(function(value, index, array) { return array.indexOf(value) === index; }),
358
- events: Video.manifest.events.concat(['error'])
270
+ events: Video.manifest.events.concat(['propValue', 'propChanged', 'error'])
359
271
  .filter(function(value, index, array) { return array.indexOf(value) === index; })
360
272
  };
361
273
 
@@ -1,27 +0,0 @@
1
- var ERROR = require('../error');
2
- var subtitlesParser = require('./subtitlesParser');
3
-
4
- function fetchSubtitles(track) {
5
- return fetch(track.url)
6
- .then(function(resp) {
7
- return resp.text();
8
- })
9
- .catch(function(error) {
10
- throw Object.assign({}, ERROR.WITH_HTML_SUBTITLES.FETCH_FAILED, {
11
- track: track,
12
- error: error
13
- });
14
- })
15
- .then(function(text) {
16
- var cuesByTime = subtitlesParser.parse(text);
17
- if (cuesByTime.times.length === 0) {
18
- throw Object.assign({}, ERROR.WITH_HTML_SUBTITLES.PARSE_FAILED, {
19
- track: track
20
- });
21
- }
22
-
23
- return cuesByTime;
24
- });
25
- }
26
-
27
- module.exports = fetchSubtitles;
@@ -1,142 +0,0 @@
1
- var url = require('url');
2
- var magnet = require('magnet-uri');
3
- var parseVideoName = require('video-name-parser');
4
- var ERROR = require('../error');
5
-
6
- var MEDIA_FILE_EXTENTIONS = /.mkv$|.avi$|.mp4$|.wmv$|.vp8$|.mov$|.mpg$|.ts$|.m3u8$|.webm$|.flac$|.mp3$|.wav$|.wma$|.aac$|.ogg$/i;
7
-
8
- function inferTorrentFileIdx(streamingServerURL, infoHash, sources, seriesInfo) {
9
- return fetch(url.resolve(streamingServerURL, '/' + encodeURIComponent(infoHash) + '/create'), {
10
- method: 'POST',
11
- headers: {
12
- 'content-type': 'application/json'
13
- },
14
- body: JSON.stringify({
15
- torrent: {
16
- infoHash: infoHash,
17
- peerSearch: {
18
- sources: ['dht:' + infoHash].concat(Array.isArray(sources) ? sources : []),
19
- min: 40,
20
- max: 150
21
- }
22
- }
23
- })
24
- }).then(function(resp) {
25
- return resp.json();
26
- }).catch(function(error) {
27
- throw Object.assign({}, ERROR.WITH_STREAMING_SERVER.TORRENT_CREATE_FAILED, {
28
- infoHash: infoHash,
29
- sources: sources,
30
- error: error
31
- });
32
- }).then(function(resp) {
33
- if (!resp || !Array.isArray(resp.files) || resp.files.some(function(file) { return !file || typeof file.path !== 'string' || file.length === null || !isFinite(file.length); })) {
34
- throw Object.assign({}, ERROR.WITH_STREAMING_SERVER.TORRENT_CREATE_FAILED, {
35
- infoHash: infoHash,
36
- sources: sources,
37
- files: resp.files,
38
- error: new Error('Invalid files')
39
- });
40
- }
41
-
42
- var mediaFiles = resp.files.filter(function(file) {
43
- return file.path.match(MEDIA_FILE_EXTENTIONS);
44
- });
45
- if (mediaFiles.length === 0) {
46
- throw Object.assign({}, ERROR.WITH_STREAMING_SERVER.NO_MEDIA_FILES_FOUND, {
47
- infoHash: infoHash,
48
- sources: sources,
49
- files: resp.files
50
- });
51
- }
52
-
53
- var mediaFilesForEpisode = seriesInfo ?
54
- mediaFiles.filter(function(file) {
55
- try {
56
- var info = parseVideoName(file.path);
57
- return info.season !== null &&
58
- isFinite(info.season) &&
59
- info.season === seriesInfo.season &&
60
- Array.isArray(info.episode) &&
61
- info.episode.indexOf(seriesInfo.episode) !== -1;
62
- } catch (e) {
63
- return false;
64
- }
65
- })
66
- :
67
- [];
68
- var selectedFile = (mediaFilesForEpisode.length > 0 ? mediaFilesForEpisode : mediaFiles)
69
- .reduce(function(result, file) {
70
- if (!result || file.length > result.length) {
71
- return file;
72
- }
73
-
74
- return result;
75
- }, null);
76
- return resp.files.indexOf(selectedFile);
77
- });
78
- }
79
-
80
- function convertStreamToURL(streamingServerURL, stream, seriesInfo) {
81
- return new Promise(function(resolve, reject) {
82
- if (typeof stream.url === 'string') {
83
- if (stream.url.indexOf('magnet:') === 0) {
84
- var parsedMagnetURI;
85
- try {
86
- parsedMagnetURI = magnet.decode(stream.url);
87
- } catch (error) {
88
- reject(Object.assign({}, ERROR.WITH_STREAMING_SERVER.STREAM_CONVERT_FAILED, {
89
- stream: stream,
90
- error: error
91
- }));
92
- return;
93
- }
94
- if (parsedMagnetURI && typeof parsedMagnetURI.infoHash === 'string') {
95
- var sources = Array.isArray(parsedMagnetURI.announce) ?
96
- parsedMagnetURI.announce.map(function(source) {
97
- return 'tracker:' + source;
98
- })
99
- :
100
- [];
101
- inferTorrentFileIdx(streamingServerURL, parsedMagnetURI.infoHash, sources, seriesInfo)
102
- .then(function(fileIdx) {
103
- resolve(url.resolve(streamingServerURL, '/' + encodeURIComponent(stream.infoHash) + '/' + encodeURIComponent(fileIdx)));
104
- })
105
- .catch(function(error) {
106
- reject(Object.assign({}, error, {
107
- stream: stream
108
- }));
109
- });
110
- return;
111
- }
112
- } else {
113
- resolve(stream.url);
114
- return;
115
- }
116
- }
117
-
118
- if (typeof stream.infoHash === 'string') {
119
- if (stream.fileIdx !== null && isFinite(stream.fileIdx)) {
120
- resolve(url.resolve(streamingServerURL, '/' + encodeURIComponent(stream.infoHash) + '/' + encodeURIComponent(stream.fileIdx)));
121
- return;
122
- } else {
123
- inferTorrentFileIdx(streamingServerURL, stream.infoHash, stream.announce, seriesInfo)
124
- .then(function(fileIdx) {
125
- resolve(url.resolve(streamingServerURL, '/' + encodeURIComponent(stream.infoHash) + '/' + encodeURIComponent(fileIdx)));
126
- })
127
- .catch(function(error) {
128
- reject(Object.assign({}, error, {
129
- stream: stream
130
- }));
131
- });
132
- return;
133
- }
134
- }
135
-
136
- reject(Object.assign({}, ERROR.WITH_STREAMING_SERVER.STREAM_CONVERT_FAILED, {
137
- stream: stream
138
- }));
139
- });
140
- }
141
-
142
- module.exports = convertStreamToURL;
@@ -1,46 +0,0 @@
1
- var url = require('url');
2
- var ERROR = require('../error');
3
-
4
- function createTranscoder(streamingServerURL, mediaURL, time, audioChannels) {
5
- var queryParams = new URLSearchParams([
6
- ['url', mediaURL],
7
- ['time', time]
8
- ]);
9
- if (audioChannels !== null && isFinite(audioChannels)) {
10
- queryParams.set('audioChannels', audioChannels);
11
- }
12
- return fetch(url.resolve(streamingServerURL, '/transcode/create') + '?' + queryParams.toString())
13
- .then(function(resp) {
14
- return resp.json();
15
- })
16
- .then(function(resp) {
17
- if (resp.error) {
18
- throw resp.error;
19
- }
20
-
21
- if (typeof resp.hash !== 'string' || (resp.duration !== null && !isFinite(resp.duration)) || typeof resp.ended !== 'boolean') {
22
- throw new Error('Inavalid response: ' + JSON.stringify(resp));
23
- }
24
-
25
- return Object.assign({}, resp, {
26
- streamingServerURL: streamingServerURL,
27
- timeOffset: resp.videoTimeOffset !== null && isFinite(resp.videoTimeOffset) ?
28
- resp.videoTimeOffset
29
- :
30
- resp.audioTimeOffset !== null && isFinite(resp.audioTimeOffset) ?
31
- resp.audioTimeOffset
32
- :
33
- 0,
34
- url: url.resolve(streamingServerURL, '/transcode/' + encodeURIComponent(resp.hash) + '/playlist.m3u8')
35
- });
36
- })
37
- .catch(function(error) {
38
- throw Object.assign({}, ERROR.WITH_STREAMING_SERVER.TRANSCODER_CREATE_FAILED, {
39
- error: error,
40
- mediaURL: mediaURL,
41
- time: time
42
- });
43
- });
44
- }
45
-
46
- module.exports = createTranscoder;
@@ -1,17 +0,0 @@
1
- var url = require('url');
2
-
3
- function transcodeNextSegment(streamingServerURL, hash) {
4
- return fetch(url.resolve(streamingServerURL, '/transcode/next') + '?' + new URLSearchParams([['hash', hash]]).toString())
5
- .then(function(resp) {
6
- return resp.json();
7
- })
8
- .then(function(resp) {
9
- if (!resp.error && typeof resp.ended !== 'boolean') {
10
- throw new Error('Inavalid response: ' + JSON.stringify(resp));
11
- }
12
-
13
- return resp;
14
- });
15
- }
16
-
17
- module.exports = transcodeNextSegment;