@stremio/stremio-video 0.0.20-rc.7 → 0.0.22

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stremio/stremio-video",
3
- "version": "0.0.20-rc.7",
3
+ "version": "0.0.22",
4
4
  "description": "Abstraction layer on top of different media players",
5
5
  "author": "Smart Code OOD",
6
6
  "main": "src/index.js",
@@ -13,6 +13,8 @@
13
13
  "lint": "eslint src"
14
14
  },
15
15
  "dependencies": {
16
+ "buffer": "6.0.3",
17
+ "color": "4.2.3",
16
18
  "deep-freeze": "0.0.1",
17
19
  "eventemitter3": "4.0.7",
18
20
  "hat": "0.0.3",
@@ -21,8 +23,7 @@
21
23
  "magnet-uri": "6.2.0",
22
24
  "url": "0.11.0",
23
25
  "video-name-parser": "1.4.6",
24
- "vtt.js": "github:jaruba/vtt.js#e4f5f5603730866bacb174a93f51b734c9f29e6a",
25
- "color": "4.2.3"
26
+ "vtt.js": "github:jaruba/vtt.js#e4f5f5603730866bacb174a93f51b734c9f29e6a"
26
27
  },
27
28
  "devDependencies": {
28
29
  "eslint": "7.32.0"
@@ -45,6 +45,7 @@ function ChromecastSenderVideo(options) {
45
45
  var destroyed = false;
46
46
  var observedProps = {
47
47
  stream: false,
48
+ loaded: false,
48
49
  paused: false,
49
50
  time: false,
50
51
  duration: false,
@@ -107,6 +108,7 @@ function ChromecastSenderVideo(options) {
107
108
  case 'destroy': {
108
109
  destroyed = true;
109
110
  onPropChanged('stream', null);
111
+ onPropChanged('loaded', null);
110
112
  onPropChanged('paused', null);
111
113
  onPropChanged('time', null);
112
114
  onPropChanged('duration', null);
@@ -188,7 +190,7 @@ ChromecastSenderVideo.canPlayStream = function() {
188
190
  ChromecastSenderVideo.manifest = {
189
191
  name: 'ChromecastSenderVideo',
190
192
  external: true,
191
- props: ['stream', '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'],
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'],
192
194
  commands: ['load', 'unload', 'destroy', 'addExtraSubtitlesTracks'],
193
195
  events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded', 'extraSubtitlesTrackLoaded', 'implementationChanged']
194
196
  };
@@ -47,10 +47,12 @@ function HTMLVideo(options) {
47
47
  onPropChanged('buffered');
48
48
  };
49
49
  videoElement.onseeking = function() {
50
+ onPropChanged('time');
50
51
  onPropChanged('buffering');
51
52
  onPropChanged('buffered');
52
53
  };
53
54
  videoElement.onseeked = function() {
55
+ onPropChanged('time');
54
56
  onPropChanged('buffering');
55
57
  onPropChanged('buffered');
56
58
  };
@@ -59,6 +61,7 @@ function HTMLVideo(options) {
59
61
  onPropChanged('buffered');
60
62
  };
61
63
  videoElement.onplaying = function() {
64
+ onPropChanged('time');
62
65
  onPropChanged('buffering');
63
66
  onPropChanged('buffered');
64
67
  };
@@ -70,6 +73,9 @@ function HTMLVideo(options) {
70
73
  onPropChanged('buffering');
71
74
  onPropChanged('buffered');
72
75
  };
76
+ videoElement.onloadedmetadata = function() {
77
+ onPropChanged('loaded');
78
+ };
73
79
  videoElement.onloadeddata = function() {
74
80
  onPropChanged('buffering');
75
81
  onPropChanged('buffered');
@@ -98,6 +104,7 @@ function HTMLVideo(options) {
98
104
  var subtitlesOffset = 0;
99
105
  var observedProps = {
100
106
  stream: false,
107
+ loaded: false,
101
108
  paused: false,
102
109
  time: false,
103
110
  duration: false,
@@ -122,6 +129,13 @@ function HTMLVideo(options) {
122
129
  case 'stream': {
123
130
  return stream;
124
131
  }
132
+ case 'loaded': {
133
+ if (stream === null) {
134
+ return null;
135
+ }
136
+
137
+ return videoElement.readyState >= videoElement.HAVE_METADATA;
138
+ }
125
139
  case 'paused': {
126
140
  if (stream === null) {
127
141
  return null;
@@ -355,6 +369,7 @@ function HTMLVideo(options) {
355
369
  case 'paused': {
356
370
  if (stream !== null) {
357
371
  propValue ? videoElement.pause() : videoElement.play();
372
+ onPropChanged('paused');
358
373
  }
359
374
 
360
375
  break;
@@ -362,6 +377,7 @@ function HTMLVideo(options) {
362
377
  case 'time': {
363
378
  if (stream !== null && propValue !== null && isFinite(propValue)) {
364
379
  videoElement.currentTime = parseInt(propValue, 10) / 1000;
380
+ onPropChanged('time');
365
381
  }
366
382
 
367
383
  break;
@@ -377,6 +393,7 @@ function HTMLVideo(options) {
377
393
  return track.id === propValue;
378
394
  });
379
395
  if (selecterdSubtitlesTrack) {
396
+ onPropChanged('selectedSubtitlesTrackId');
380
397
  events.emit('subtitlesTrackLoaded', selecterdSubtitlesTrack);
381
398
  }
382
399
  }
@@ -450,6 +467,7 @@ function HTMLVideo(options) {
450
467
  });
451
468
  hls.audioTrack = selecterdAudioTrack ? parseInt(selecterdAudioTrack.id.split('_').pop(), 10) : -1;
452
469
  if (selecterdAudioTrack) {
470
+ onPropChanged('selectedAudioTrackId');
453
471
  events.emit('audioTrackLoaded', selecterdAudioTrack);
454
472
  }
455
473
  }
@@ -460,17 +478,21 @@ function HTMLVideo(options) {
460
478
  if (propValue !== null && isFinite(propValue)) {
461
479
  videoElement.muted = false;
462
480
  videoElement.volume = Math.max(0, Math.min(100, parseInt(propValue, 10))) / 100;
481
+ onPropChanged('muted');
482
+ onPropChanged('volume');
463
483
  }
464
484
 
465
485
  break;
466
486
  }
467
487
  case 'muted': {
468
488
  videoElement.muted = !!propValue;
489
+ onPropChanged('muted');
469
490
  break;
470
491
  }
471
492
  case 'playbackSpeed': {
472
493
  if (propValue !== null && isFinite(propValue)) {
473
494
  videoElement.playbackRate = parseFloat(propValue);
495
+ onPropChanged('playbackSpeed');
474
496
  }
475
497
 
476
498
  break;
@@ -484,6 +506,7 @@ function HTMLVideo(options) {
484
506
  if (commandArgs && commandArgs.stream && typeof commandArgs.stream.url === 'string') {
485
507
  stream = commandArgs.stream;
486
508
  onPropChanged('stream');
509
+ onPropChanged('loaded');
487
510
  videoElement.autoplay = typeof commandArgs.autoplay === 'boolean' ? commandArgs.autoplay : true;
488
511
  videoElement.currentTime = commandArgs.time !== null && isFinite(commandArgs.time) ? parseInt(commandArgs.time, 10) / 1000 : 0;
489
512
  onPropChanged('paused');
@@ -547,6 +570,7 @@ function HTMLVideo(options) {
547
570
  videoElement.load();
548
571
  videoElement.currentTime = 0;
549
572
  onPropChanged('stream');
573
+ onPropChanged('loaded');
550
574
  onPropChanged('paused');
551
575
  onPropChanged('time');
552
576
  onPropChanged('duration');
@@ -646,7 +670,7 @@ HTMLVideo.canPlayStream = function(stream) {
646
670
  HTMLVideo.manifest = {
647
671
  name: 'HTMLVideo',
648
672
  external: false,
649
- props: ['stream', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed'],
673
+ props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed'],
650
674
  commands: ['load', 'unload', 'destroy'],
651
675
  events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded']
652
676
  };
@@ -24,6 +24,7 @@ function IFrameVideo(options) {
24
24
  var destroyed = false;
25
25
  var observedProps = {
26
26
  stream: false,
27
+ loaded: false,
27
28
  paused: false,
28
29
  time: false,
29
30
  duration: false,
@@ -95,6 +96,7 @@ function IFrameVideo(options) {
95
96
  iframeElement.onload = null;
96
97
  iframeElement.removeAttribute('src');
97
98
  onPropChanged('stream', null);
99
+ onPropChanged('loaded', null);
98
100
  onPropChanged('paused', null);
99
101
  onPropChanged('time', null);
100
102
  onPropChanged('duration', null);
@@ -108,15 +110,6 @@ function IFrameVideo(options) {
108
110
  case 'destroy': {
109
111
  command('unload');
110
112
  destroyed = true;
111
- onPropChanged('stream', null);
112
- onPropChanged('paused', null);
113
- onPropChanged('time', null);
114
- onPropChanged('duration', null);
115
- onPropChanged('buffering', null);
116
- onPropChanged('buffered', null);
117
- onPropChanged('volume', null);
118
- onPropChanged('muted', null);
119
- onPropChanged('playbackSpeed', null);
120
113
  events.removeAllListeners();
121
114
  containerElement.removeChild(iframeElement);
122
115
  return true;
@@ -169,7 +162,7 @@ IFrameVideo.canPlayStream = function(stream) {
169
162
  IFrameVideo.manifest = {
170
163
  name: 'IFrameVideo',
171
164
  external: true,
172
- props: ['stream', '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'],
165
+ 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'],
173
166
  commands: ['load', 'unload', 'destroy', 'addExtraSubtitlesTracks'],
174
167
  events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded', 'extraSubtitlesTrackLoaded', 'implementationChanged']
175
168
  };
@@ -0,0 +1,436 @@
1
+ var EventEmitter = require('eventemitter3');
2
+ var cloneDeep = require('lodash.clonedeep');
3
+ var deepFreeze = require('deep-freeze');
4
+ var ERROR = require('../error');
5
+
6
+ var SUBS_SCALE_FACTOR = 0.0066;
7
+
8
+ var stremioToMPVProps = {
9
+ 'stream': null,
10
+ 'paused': 'pause',
11
+ 'time': 'time-pos',
12
+ 'duration': 'duration',
13
+ 'buffering': 'buffering',
14
+ 'volume': 'volume',
15
+ 'muted': 'mute',
16
+ 'playbackSpeed': 'speed',
17
+ 'audioTracks': 'audioTracks',
18
+ 'selectedAudioTrackId': 'aid',
19
+ 'subtitlesTracks': 'subtitlesTracks',
20
+ 'selectedSubtitlesTrackId': 'sid',
21
+ 'subtitlesSize': 'sub-scale',
22
+ 'subtitlesTextColor': 'sub-color',
23
+ 'subtitlesBackgroundColor': 'sub-back-color',
24
+ 'subtitlesOutlineColor': 'sub-border-color',
25
+ };
26
+
27
+ function ShellVideo(options) {
28
+ options = options || {};
29
+
30
+ var ipc = options.shellTransport;
31
+
32
+ var stremioProps = {};
33
+ Object.keys(stremioToMPVProps).forEach(function(key) {
34
+ if(stremioToMPVProps[key]) {
35
+ stremioProps[stremioToMPVProps[key]] = key;
36
+ }
37
+ });
38
+
39
+ ipc.send('mpv-command', ['stop']);
40
+ ipc.send('mpv-observe-prop', 'path');
41
+
42
+ ipc.send('mpv-observe-prop', 'time-pos');
43
+ ipc.send('mpv-observe-prop', 'volume');
44
+ ipc.send('mpv-observe-prop', 'pause');
45
+ ipc.send('mpv-observe-prop', 'seeking');
46
+ ipc.send('mpv-observe-prop', 'eof-reached');
47
+
48
+ ipc.send('mpv-observe-prop', 'duration');
49
+ ipc.send('mpv-observe-prop', 'metadata');
50
+ ipc.send('mpv-observe-prop', 'video-params'); // video width/height
51
+ ipc.send('mpv-observe-prop', 'track-list');
52
+
53
+ ipc.send('mpv-observe-prop', 'paused-for-cache');
54
+ ipc.send('mpv-observe-prop', 'cache-buffering-state');
55
+
56
+ ipc.send('mpv-observe-prop', 'aid');
57
+ ipc.send('mpv-observe-prop', 'vid');
58
+ ipc.send('mpv-observe-prop', 'sid');
59
+ ipc.send('mpv-observe-prop', 'sub-scale');
60
+ ipc.send('mpv-observe-prop', 'sub-pos');
61
+ ipc.send('mpv-observe-prop', 'speed');
62
+
63
+ ipc.send('mpv-observe-prop', 'mpv-version');
64
+ ipc.send('mpv-observe-prop', 'ffmpeg-version');
65
+
66
+ var events = new EventEmitter();
67
+ var destroyed = false;
68
+ var stream = null;
69
+ // var selectedSubtitlesTrackId = null;
70
+ var observedProps = {};
71
+ var continueFrom = 0;
72
+
73
+ var avgDuration = 0;
74
+ var minClipDuration = 30;
75
+ var props = { };
76
+
77
+ function setBackground(visible) {
78
+ // This is a bit of a hack but there is no better way so far
79
+ var bg = visible ? '' : 'transparent';
80
+ for(var container = options.containerElement; container; container = container.parentElement) {
81
+ container.style.background = bg;
82
+ }
83
+ }
84
+ function logProp(args) {
85
+ // eslint-disable-next-line no-console
86
+ console.log(args.name+': '+args.data);
87
+ }
88
+ function embeddedProp(args) {
89
+ return args.data ? 'EMBEDDED_' + args.data.toString() : null;
90
+ }
91
+
92
+ var last_time = 0;
93
+ ipc.on('mpv-prop-change', function(args) {
94
+ switch (args.name) {
95
+ case 'mpv-version':
96
+ case 'ffmpeg-version': {
97
+ props[args.name] = logProp(args);
98
+ break;
99
+ }
100
+ case 'duration': {
101
+ var intDuration = args.data | 0;
102
+ // Accumulate average duration over time. if it is greater than minClipDuration
103
+ // and equal to the currently reported duration, it is returned as video length.
104
+ // If the reported duration changes over time the average duration is always
105
+ // smaller than the currently reported one so we set the video length to 0 as
106
+ // this is a live stream.
107
+ props[args.name] = args.data >= minClipDuration && (!avgDuration || intDuration === avgDuration) ? Math.round(args.data * 1000) : null;
108
+ // The average duration is calculated using right bit shifting by one of the sum of
109
+ // the previous average and the currently reported value. This method is not very precise
110
+ // as we get integer value but we avoid floating point errors. JS uses 32 bit values
111
+ // for bitwise maths so the maximum supported video duration is 1073741823 (2 ^ 30 - 1)
112
+ // which is around 34 years of playback time.
113
+ avgDuration = avgDuration ? (avgDuration + intDuration) >> 1 : intDuration;
114
+ break;
115
+ }
116
+ case 'time-pos': {
117
+ props[args.name] = Math.round(args.data*1000);
118
+ if(continueFrom) {
119
+ ipc.send('mpv-set-prop', ['time-pos', continueFrom]);
120
+ props[args.name] = Math.round(continueFrom);
121
+ continueFrom = 0;
122
+ }
123
+ break;
124
+ }
125
+ case 'sub-scale': {
126
+ props[args.name] = Math.round(args.data / SUBS_SCALE_FACTOR);
127
+ break;
128
+ }
129
+ case 'paused-for-cache':
130
+ case 'seeking':
131
+ {
132
+ if(props.buffering !== args.data) {
133
+ props.buffering = args.data;
134
+ onPropChanged('buffering');
135
+ }
136
+ break;
137
+ }
138
+ case 'aid':
139
+ case 'sid':
140
+ case 'vid': {
141
+ props[args.name] = embeddedProp(args);
142
+ break;
143
+ }
144
+ // In that case onPropChanged() is manually invoked as track-list contains all
145
+ // the tracks but we have different event for each track type
146
+ case 'track-list': {
147
+ props.audioTracks = args.data.filter(function(x) { return x.type === 'audio'; })
148
+ .map(function(x, index) {
149
+ return {
150
+ id: 'EMBEDDED_' + x.id,
151
+ lang: x.lang === undefined ? 'Track' + (index + 1) : x.lang,
152
+ label: x.title === undefined || x.lang === undefined ? '' : x.title || x.lang,
153
+ origin: 'EMBEDDED',
154
+ embedded: true,
155
+ mode: x.id === props.aid ? 'showing' : 'disabled',
156
+ };
157
+ });
158
+ onPropChanged('audioTracks');
159
+
160
+ props.subtitlesTracks = args.data
161
+ .filter(function(x) { return x.type === 'sub'; })
162
+ .map(function(x, index) {
163
+ return {
164
+ id: 'EMBEDDED_' + x.id,
165
+ lang: x.lang === undefined ? 'Track ' + (index + 1) : x.lang,
166
+ label: x.title === undefined || x.lang === undefined ? '' : x.title || x.lang,
167
+ origin: 'EMBEDDED',
168
+ embedded: true,
169
+ mode: x.id === props.sid ? 'showing' : 'disabled',
170
+ };
171
+ });
172
+ onPropChanged('subtitlesTracks');
173
+ break;
174
+ }
175
+ default: {
176
+ props[args.name] = args.data;
177
+ break;
178
+ }
179
+ }
180
+
181
+ // Cap time update to update only when a second passes
182
+ var current_time = args.name === 'time-pos' ? Math.floor(props['time-pos'] / 1000) : null;
183
+ if((!current_time || last_time !== current_time)&& stremioProps[args.name]) {
184
+ if(current_time) {
185
+ last_time = current_time;
186
+ }
187
+ onPropChanged(stremioProps[args.name]);
188
+ }
189
+ });
190
+ ipc.on('mpv-event-ended', function(args) {
191
+ if (args.error) onError(args.error);
192
+ else onEnded();
193
+ });
194
+
195
+ function getProp(propName) {
196
+ if(stremioToMPVProps[propName]) return props[stremioToMPVProps[propName]];
197
+ // eslint-disable-next-line no-console
198
+ console.log('Unsupported prop requested', propName);
199
+ return null;
200
+ }
201
+ function onError(error) {
202
+ events.emit('error', error);
203
+ if (error.critical) {
204
+ command('unload');
205
+ }
206
+ }
207
+ function onEnded() {
208
+ events.emit('ended');
209
+ }
210
+ function onPropChanged(propName) {
211
+ if (observedProps[propName]) {
212
+ events.emit('propChanged', propName, getProp(propName));
213
+ }
214
+ }
215
+ function observeProp(propName) {
216
+ events.emit('propValue', propName, getProp(propName));
217
+ observedProps[propName] = true;
218
+ }
219
+ function setProp(propName, propValue) {
220
+ switch (propName) {
221
+ case 'paused': {
222
+ if (stream !== null) {
223
+ ipc.send('mpv-set-prop', ['pause', propValue]);
224
+ }
225
+
226
+ break;
227
+ }
228
+ case 'time': {
229
+ if (stream !== null && propValue !== null && isFinite(propValue)) {
230
+ ipc.send('mpv-set-prop', ['time-pos', propValue/1000]);
231
+ }
232
+
233
+ break;
234
+ }
235
+ case 'playbackSpeed': {
236
+ if (stream !== null && propValue !== null && isFinite(propValue)) {
237
+ ipc.send('mpv-set-prop', ['speed', propValue]);
238
+ }
239
+ break;
240
+ }
241
+ case 'volume': {
242
+ if (stream !== null && propValue !== null && isFinite(propValue)) {
243
+ props.mute = false;
244
+ ipc.send('mpv-set-prop', ['mute', 'no']);
245
+ ipc.send('mpv-set-prop', ['volume', propValue]);
246
+ onPropChanged('muted');
247
+ onPropChanged('volume');
248
+ }
249
+ break;
250
+ }
251
+ case 'muted': {
252
+ if (stream !== null) {
253
+ ipc.send('mpv-set-prop', ['mute', propValue ? 'yes' : 'no']);
254
+ props.mute = propValue;
255
+ onPropChanged('muted');
256
+ }
257
+ break;
258
+ }
259
+ case 'selectedAudioTrackId': {
260
+ if (stream !== null) {
261
+ var actualId = propValue.slice('EMBEDDED_'.length);
262
+ ipc.send('mpv-set-prop', ['aid', actualId]);
263
+ }
264
+ break;
265
+ }
266
+ case 'selectedSubtitlesTrackId': {
267
+ if (stream !== null) {
268
+ if(propValue) {
269
+ var actualId = propValue.slice('EMBEDDED_'.length);
270
+ ipc.send('mpv-set-prop', ['sid', actualId]);
271
+ events.emit('subtitlesTrackLoaded', propValue);
272
+ } else {
273
+ // turn off subs
274
+ ipc.send('mpv-set-prop', ['sid', 'no']);
275
+ props.sid = null;
276
+ }
277
+ }
278
+ onPropChanged('selectedSubtitlesTrackId');
279
+ break;
280
+ }
281
+ case 'subtitlesSize': {
282
+ ipc.send('mpv-set-prop', [stremioToMPVProps[propName], propValue * SUBS_SCALE_FACTOR]);
283
+ break;
284
+ }
285
+ case 'subtitlesOffset': {
286
+ ipc.send('mpv-set-prop', [stremioToMPVProps[propName], propValue]);
287
+ break;
288
+ }
289
+ case 'subtitlesTextColor':
290
+ case 'subtitlesBackgroundColor':
291
+ case 'subtitlesOutlineColor':
292
+ {
293
+ // MPV accepts color in #AARRGGBB
294
+ var argb = propValue.replace(/^#(\w{6})(\w{2})$/, '#$2$1');
295
+ ipc.send('mpv-set-prop', [stremioToMPVProps[propName], argb]);
296
+ break;
297
+ }
298
+ default: {
299
+ // eslint-disable-next-line no-console
300
+ console.log('Unhandled setProp for', propName);
301
+ }
302
+ }
303
+ }
304
+ function command(commandName, commandArgs) {
305
+ switch (commandName) {
306
+ case 'load': {
307
+ command('unload');
308
+ if (commandArgs && commandArgs.stream && typeof commandArgs.stream.url === 'string') {
309
+ stream = commandArgs.stream;
310
+ onPropChanged('stream');
311
+ continueFrom = commandArgs.time !== null && isFinite(commandArgs.time) ? parseInt(commandArgs.time, 10) / 1000 : 0;
312
+
313
+ setBackground(false);
314
+
315
+ ipc.send('mpv-set-prop', ['no-sub-ass']);
316
+
317
+ // opengl-cb is an alias for the new name "libmpv", as shown in mpv's video/out/vo.c aliases
318
+ // opengl is an alias for the new name "gpu"
319
+ // When on Windows we use d3d for the rendering in separate window
320
+ var windowRenderer = navigator.platform === 'Win32' ? 'direct3d' : 'opengl';
321
+ var videoOutput = options.mpvSeparateWindow ? windowRenderer : 'opengl-cb';
322
+ var separateWindow = options.mpvSeparateWindow ? 'yes' : 'no';
323
+ ipc.send('mpv-set-prop', ['vo', videoOutput]);
324
+ ipc.send('mpv-set-prop', ['osc', separateWindow]);
325
+ ipc.send('mpv-set-prop', ['input-defalt-bindings', separateWindow]);
326
+ ipc.send('mpv-set-prop', ['input-vo-keyboard', separateWindow]);
327
+
328
+ ipc.send('mpv-command', ['loadfile', stream.url]);
329
+ ipc.send('mpv-set-prop', ['pause', false]);
330
+ ipc.send('mpv-set-prop', ['speed', props.speed]);
331
+ ipc.send('mpv-set-prop', ['aid', props.aid]);
332
+ ipc.send('mpv-set-prop', ['mute', 'no']);
333
+
334
+ onPropChanged('paused');
335
+ onPropChanged('time');
336
+ onPropChanged('duration');
337
+ onPropChanged('buffering');
338
+ onPropChanged('volume');
339
+ onPropChanged('muted');
340
+ onPropChanged('subtitlesTracks');
341
+ onPropChanged('selectedSubtitlesTrackId');
342
+ } else {
343
+ onError(Object.assign({}, ERROR.UNSUPPORTED_STREAM, {
344
+ critical: true,
345
+ stream: commandArgs ? commandArgs.stream : null
346
+ }));
347
+ }
348
+
349
+ break;
350
+ }
351
+ case 'unload': {
352
+ props = {
353
+ mute: false,
354
+ speed: 1,
355
+ subtitlesTracks: [],
356
+ buffering: true,
357
+ aid: null,
358
+ sid: null,
359
+ };
360
+ continueFrom = 0;
361
+ avgDuration = 0;
362
+ ipc.send('mpv-command', ['stop']);
363
+ onPropChanged('stream');
364
+ onPropChanged('paused');
365
+ onPropChanged('time');
366
+ onPropChanged('duration');
367
+ onPropChanged('buffering');
368
+ onPropChanged('volume');
369
+ onPropChanged('muted');
370
+ onPropChanged('subtitlesTracks');
371
+ onPropChanged('selectedSubtitlesTrackId');
372
+ setBackground(true);
373
+ break;
374
+ }
375
+ case 'destroy': {
376
+ command('unload');
377
+ destroyed = true;
378
+ events.removeAllListeners();
379
+ break;
380
+ }
381
+ }
382
+ }
383
+
384
+ this.on = function (eventName, listener) {
385
+ if (destroyed) {
386
+ throw new Error('Video is destroyed');
387
+ }
388
+
389
+ events.on(eventName, listener);
390
+ };
391
+ this.dispatch = function (action) {
392
+ if (destroyed) {
393
+ throw new Error('Video is destroyed');
394
+ }
395
+
396
+ if (action) {
397
+ action = deepFreeze(cloneDeep(action));
398
+ switch (action.type) {
399
+ case 'observeProp': {
400
+ observeProp(action.propName);
401
+ break;
402
+ }
403
+ case 'setProp': {
404
+ setProp(action.propName, action.propValue);
405
+ return;
406
+ }
407
+ case 'command': {
408
+ command(
409
+ action.commandName,
410
+ action.commandArgs
411
+ );
412
+ return;
413
+ }
414
+ }
415
+ }
416
+ };
417
+ }
418
+ ShellVideo.canPlayStream = function() {
419
+ return Promise.resolve(true);
420
+ };
421
+
422
+ ShellVideo.manifest = {
423
+ name: 'ShellVideo',
424
+ external: false,
425
+ props: Object.keys(stremioToMPVProps),
426
+ commands: ['load', 'unload', 'destroy'],
427
+ events: [
428
+ 'propValue',
429
+ 'propChanged',
430
+ 'ended',
431
+ 'error',
432
+ 'subtitlesTrackLoaded',
433
+ ],
434
+ };
435
+
436
+ module.exports = ShellVideo;
@@ -0,0 +1,3 @@
1
+ var ShellVideo = require('./ShellVideo');
2
+
3
+ module.exports = ShellVideo;
@@ -1,4 +1,5 @@
1
1
  var ChromecastSenderVideo = require('../ChromecastSenderVideo');
2
+ var ShellVideo = require('../ShellVideo');
2
3
  var HTMLVideo = require('../HTMLVideo');
3
4
  var TizenVideo = require('../TizenVideo');
4
5
  var IFrameVideo = require('../IFrameVideo');
@@ -23,6 +24,10 @@ function selectVideoImplementation(commandArgs, options) {
23
24
  return IFrameVideo;
24
25
  }
25
26
 
27
+ if (options.shellTransport) {
28
+ return withStreamingServer(withHTMLSubtitles(ShellVideo));
29
+ }
30
+
26
31
  if (typeof commandArgs.streamingServerURL === 'string') {
27
32
  if (typeof global.tizen !== 'undefined') {
28
33
  return withStreamingServer(withHTMLSubtitles(TizenVideo));
@@ -17,6 +17,8 @@ function TizenVideo(options) {
17
17
  throw new Error('Container element required to be instance of HTMLElement');
18
18
  }
19
19
 
20
+ var promiseAudioTrackChange = false;
21
+
20
22
  var size = 100;
21
23
  var offset = 0;
22
24
  var textColor = 'rgb(255, 255, 255)';
@@ -151,7 +153,14 @@ function TizenVideo(options) {
151
153
  return null;
152
154
  }
153
155
 
154
- return !!(window.webapis.avplay.getState() === 'PAUSED');
156
+ var isPaused = !!(window.webapis.avplay.getState() === 'PAUSED');
157
+
158
+ if (!isPaused && promiseAudioTrackChange) {
159
+ window.webapis.avplay.setSelectTrack('AUDIO', parseInt(promiseAudioTrackChange.replace('EMBEDDED_', '')));
160
+ promiseAudioTrackChange = false;
161
+ }
162
+
163
+ return isPaused;
155
164
  }
156
165
  case 'time': {
157
166
  var currentTime = window.webapis.avplay.getCurrentTime();
@@ -301,8 +310,12 @@ function TizenVideo(options) {
301
310
  return null;
302
311
  }
303
312
 
313
+ if (promiseAudioTrackChange) {
314
+ return promiseAudioTrackChange;
315
+ }
316
+
304
317
  var currentTracks = window.webapis.avplay.getCurrentStreamInfo();
305
- var currentIndex;
318
+ var currentIndex = false;
306
319
 
307
320
  for (var i = 0; i < currentTracks.length; i++) {
308
321
  if (currentTracks[i].type === 'AUDIO') {
@@ -312,7 +325,7 @@ function TizenVideo(options) {
312
325
  }
313
326
  }
314
327
 
315
- return currentIndex ? 'EMBEDDED_' + String(currentIndex) : null;
328
+ return currentIndex !== false ? 'EMBEDDED_' + String(currentIndex) : null;
316
329
  }
317
330
  case 'playbackSpeed': {
318
331
  if (destroyed || videoSpeed === null || !isFinite(videoSpeed)) {
@@ -375,6 +388,16 @@ function TizenVideo(options) {
375
388
 
376
389
  onPropChanged('paused');
377
390
 
391
+ // the paused state is usually correct, but i have seen it not change on tizen 3
392
+ // which causes all kinds of issues in the UI: (only happens with some videos)
393
+ var lastKnownProp = getProp('paused');
394
+
395
+ setTimeout(function() {
396
+ if (getProp('paused') !== lastKnownProp) {
397
+ onPropChanged('paused');
398
+ }
399
+ }, 1000);
400
+
378
401
  break;
379
402
  }
380
403
  case 'time': {
@@ -493,8 +516,17 @@ function TizenVideo(options) {
493
516
  return track.id === propValue;
494
517
  });
495
518
 
496
- window.webapis.avplay.setSelectTrack('AUDIO', parseInt(currentAudioTrack.replace('EMBEDDED_', '')));
519
+ if (getProp('paused')) {
520
+ // issues before this logic:
521
+ // tizen 3 does not allow changing audio track when paused
522
+ // tizen 5 does, but it will only change getProp('selectedAudioTrackId') after playback starts
497
523
 
524
+ // will be changed on next play event, until then we will overwrite the result of getProp('selectedAudioTrackId')
525
+ promiseAudioTrackChange = propValue;
526
+ onPropChanged('selectedAudioTrackId');
527
+ } else {
528
+ window.webapis.avplay.setSelectTrack('AUDIO', parseInt(currentAudioTrack.replace('EMBEDDED_', '')));
529
+ }
498
530
  if (selectedAudioTrack) {
499
531
  events.emit('audioTrackLoaded', selectedAudioTrack);
500
532
  onPropChanged('selectedAudioTrackId');
@@ -39,6 +39,7 @@ function YouTubeVideo(options) {
39
39
  var selectedSubtitlesTrackId = null;
40
40
  var observedProps = {
41
41
  stream: false,
42
+ loaded: false,
42
43
  paused: false,
43
44
  time: false,
44
45
  duration: false,
@@ -190,6 +191,13 @@ function YouTubeVideo(options) {
190
191
  case 'stream': {
191
192
  return stream;
192
193
  }
194
+ case 'loaded': {
195
+ if (stream === null) {
196
+ return null;
197
+ }
198
+
199
+ return true;
200
+ }
193
201
  case 'paused': {
194
202
  if (stream === null || typeof video.getPlayerState !== 'function') {
195
203
  return null;
@@ -360,6 +368,7 @@ function YouTubeVideo(options) {
360
368
  if (ready) {
361
369
  stream = commandArgs.stream;
362
370
  onPropChanged('stream');
371
+ onPropChanged('loaded');
363
372
  var autoplay = typeof commandArgs.autoplay === 'boolean' ? commandArgs.autoplay : true;
364
373
  var time = commandArgs.time !== null && isFinite(commandArgs.time) ? parseInt(commandArgs.time, 10) / 1000 : 0;
365
374
  if (autoplay && typeof video.loadVideoById === 'function') {
@@ -397,6 +406,7 @@ function YouTubeVideo(options) {
397
406
  pendingLoadArgs = null;
398
407
  stream = null;
399
408
  onPropChanged('stream');
409
+ onPropChanged('loaded');
400
410
  selectedSubtitlesTrackId = null;
401
411
  if (ready && typeof video.stopVideo === 'function') {
402
412
  video.stopVideo();
@@ -467,7 +477,7 @@ YouTubeVideo.canPlayStream = function(stream) {
467
477
  YouTubeVideo.manifest = {
468
478
  name: 'YouTubeVideo',
469
479
  external: false,
470
- props: ['stream', 'paused', 'time', 'duration', 'buffering', 'volume', 'muted', 'subtitlesTracks', 'selectedSubtitlesTrackId'],
480
+ props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'volume', 'muted', 'subtitlesTracks', 'selectedSubtitlesTrackId'],
471
481
  commands: ['load', 'unload', 'destroy'],
472
482
  events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded']
473
483
  };
package/src/error.js CHANGED
@@ -58,7 +58,7 @@ var ERROR = {
58
58
  WITH_STREAMING_SERVER: {
59
59
  CONVERT_FAILED: {
60
60
  code: 60,
61
- message: 'Unable to convert stream'
61
+ message: 'Streaming server failed to convert torrent stream'
62
62
  }
63
63
  },
64
64
  UNKNOWN_ERROR: {