@stremio/stremio-video 0.0.48 → 0.0.50

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.48",
3
+ "version": "0.0.50",
4
4
  "description": "Abstraction layer on top of different media players",
5
5
  "author": "Smart Code OOD",
6
6
  "main": "src/index.js",
@@ -2,6 +2,7 @@ var EventEmitter = require('eventemitter3');
2
2
  var cloneDeep = require('lodash.clonedeep');
3
3
  var deepFreeze = require('deep-freeze');
4
4
  var selectVideoImplementation = require('./selectVideoImplementation');
5
+ var platform = require('../platform');
5
6
  var ERROR = require('../error');
6
7
 
7
8
  function StremioVideo() {
@@ -25,6 +26,9 @@ function StremioVideo() {
25
26
  action = deepFreeze(cloneDeep(action));
26
27
  options = options || {};
27
28
  if (action.type === 'command' && action.commandName === 'load' && action.commandArgs) {
29
+ if (action.commandArgs.platform) {
30
+ platform.set(action.commandArgs.platform);
31
+ }
28
32
  var Video = selectVideoImplementation(action.commandArgs, options);
29
33
  if (video !== null && video.constructor !== Video) {
30
34
  video.dispatch({ type: 'command', commandName: 'destroy' });
@@ -2,6 +2,7 @@ var ChromecastSenderVideo = require('../ChromecastSenderVideo');
2
2
  var ShellVideo = require('../ShellVideo');
3
3
  var HTMLVideo = require('../HTMLVideo');
4
4
  var TizenVideo = require('../TizenVideo');
5
+ var TitanVideo = require('../TitanVideo');
5
6
  var WebOsVideo = require('../WebOsVideo');
6
7
  var IFrameVideo = require('../IFrameVideo');
7
8
  var YouTubeVideo = require('../YouTubeVideo');
@@ -37,6 +38,9 @@ function selectVideoImplementation(commandArgs, options) {
37
38
  if (commandArgs.platform === 'webOS') {
38
39
  return withStreamingServer(withHTMLSubtitles(WebOsVideo));
39
40
  }
41
+ if (commandArgs.platform === 'Titan' || commandArgs.platform === 'NetTV') {
42
+ return withStreamingServer(withHTMLSubtitles(TitanVideo));
43
+ }
40
44
  return withStreamingServer(withHTMLSubtitles(HTMLVideo));
41
45
  }
42
46
 
@@ -47,6 +51,9 @@ function selectVideoImplementation(commandArgs, options) {
47
51
  if (commandArgs.platform === 'webOS') {
48
52
  return withVideoParams(withHTMLSubtitles(WebOsVideo));
49
53
  }
54
+ if (commandArgs.platform === 'Titan' || commandArgs.platform === 'NetTV') {
55
+ return withVideoParams(withHTMLSubtitles(TitanVideo));
56
+ }
50
57
  return withVideoParams(withHTMLSubtitles(HTMLVideo));
51
58
  }
52
59
 
@@ -0,0 +1,667 @@
1
+ var EventEmitter = require('eventemitter3');
2
+ var cloneDeep = require('lodash.clonedeep');
3
+ var deepFreeze = require('deep-freeze');
4
+ var Color = require('color');
5
+ var ERROR = require('../error');
6
+
7
+ function TitanVideo(options) {
8
+ options = options || {};
9
+
10
+ var containerElement = options.containerElement;
11
+ if (!(containerElement instanceof HTMLElement)) {
12
+ throw new Error('Container element required to be instance of HTMLElement');
13
+ }
14
+
15
+ var styleElement = document.createElement('style');
16
+ containerElement.appendChild(styleElement);
17
+ styleElement.sheet.insertRule('video::cue { font-size: 4vmin; color: rgb(255, 255, 255); background-color: rgba(0, 0, 0, 0); text-shadow: rgb(34, 34, 34) 1px 1px 0.1em; }');
18
+ var videoElement = document.createElement('video');
19
+ videoElement.style.width = '100%';
20
+ videoElement.style.height = '100%';
21
+ videoElement.style.backgroundColor = 'black';
22
+ videoElement.controls = false;
23
+ videoElement.playsInline = true;
24
+ videoElement.onerror = function() {
25
+ onVideoError();
26
+ };
27
+ videoElement.onended = function() {
28
+ onEnded();
29
+ };
30
+ videoElement.onpause = function() {
31
+ onPropChanged('paused');
32
+ };
33
+ videoElement.onplay = function() {
34
+ onPropChanged('paused');
35
+ };
36
+ videoElement.ontimeupdate = function() {
37
+ onPropChanged('time');
38
+ onPropChanged('buffered');
39
+ };
40
+ videoElement.ondurationchange = function() {
41
+ onPropChanged('duration');
42
+ };
43
+ videoElement.onwaiting = function() {
44
+ onPropChanged('buffering');
45
+ onPropChanged('buffered');
46
+ };
47
+ videoElement.onseeking = function() {
48
+ onPropChanged('time');
49
+ onPropChanged('buffering');
50
+ onPropChanged('buffered');
51
+ };
52
+ videoElement.onseeked = function() {
53
+ onPropChanged('time');
54
+ onPropChanged('buffering');
55
+ onPropChanged('buffered');
56
+ };
57
+ videoElement.onstalled = function() {
58
+ onPropChanged('buffering');
59
+ onPropChanged('buffered');
60
+ };
61
+ videoElement.onplaying = function() {
62
+ onPropChanged('time');
63
+ onPropChanged('buffering');
64
+ onPropChanged('buffered');
65
+ };
66
+ videoElement.oncanplay = function() {
67
+ onPropChanged('buffering');
68
+ onPropChanged('buffered');
69
+ };
70
+ videoElement.canplaythrough = function() {
71
+ onPropChanged('buffering');
72
+ onPropChanged('buffered');
73
+ };
74
+ videoElement.onloadedmetadata = function() {
75
+ onPropChanged('loaded');
76
+ };
77
+ videoElement.onloadeddata = function() {
78
+ onPropChanged('buffering');
79
+ onPropChanged('buffered');
80
+ };
81
+ videoElement.onvolumechange = function() {
82
+ onPropChanged('volume');
83
+ onPropChanged('muted');
84
+ };
85
+ videoElement.onratechange = function() {
86
+ onPropChanged('playbackSpeed');
87
+ };
88
+ videoElement.textTracks.onchange = function() {
89
+ onPropChanged('subtitlesTracks');
90
+ onPropChanged('selectedSubtitlesTrackId');
91
+ onCueChange();
92
+ Array.from(videoElement.textTracks).forEach(function(track) {
93
+ track.oncuechange = onCueChange;
94
+ });
95
+ };
96
+ containerElement.appendChild(videoElement);
97
+
98
+ var events = new EventEmitter();
99
+ var destroyed = false;
100
+ var stream = null;
101
+ var subtitlesOffset = 0;
102
+ var observedProps = {
103
+ stream: false,
104
+ loaded: false,
105
+ paused: false,
106
+ time: false,
107
+ duration: false,
108
+ buffering: false,
109
+ buffered: false,
110
+ subtitlesTracks: false,
111
+ selectedSubtitlesTrackId: false,
112
+ subtitlesOffset: false,
113
+ subtitlesSize: false,
114
+ subtitlesTextColor: false,
115
+ subtitlesBackgroundColor: false,
116
+ subtitlesOutlineColor: false,
117
+ audioTracks: false,
118
+ selectedAudioTrackId: false,
119
+ volume: false,
120
+ muted: false,
121
+ playbackSpeed: false
122
+ };
123
+
124
+ function getProp(propName) {
125
+ switch (propName) {
126
+ case 'stream': {
127
+ return stream;
128
+ }
129
+ case 'loaded': {
130
+ if (stream === null) {
131
+ return null;
132
+ }
133
+
134
+ return videoElement.readyState >= videoElement.HAVE_METADATA;
135
+ }
136
+ case 'paused': {
137
+ if (stream === null) {
138
+ return null;
139
+ }
140
+
141
+ return !!videoElement.paused;
142
+ }
143
+ case 'time': {
144
+ if (stream === null || videoElement.currentTime === null || !isFinite(videoElement.currentTime)) {
145
+ return null;
146
+ }
147
+
148
+ return Math.floor(videoElement.currentTime * 1000);
149
+ }
150
+ case 'duration': {
151
+ if (stream === null || videoElement.duration === null || !isFinite(videoElement.duration)) {
152
+ return null;
153
+ }
154
+
155
+ return Math.floor(videoElement.duration * 1000);
156
+ }
157
+ case 'buffering': {
158
+ if (stream === null) {
159
+ return null;
160
+ }
161
+
162
+ return videoElement.readyState < videoElement.HAVE_FUTURE_DATA;
163
+ }
164
+ case 'buffered': {
165
+ if (stream === null) {
166
+ return null;
167
+ }
168
+
169
+ var time = videoElement.currentTime !== null && isFinite(videoElement.currentTime) ? videoElement.currentTime : 0;
170
+ for (var i = 0; i < videoElement.buffered.length; i++) {
171
+ if (videoElement.buffered.start(i) <= time && time <= videoElement.buffered.end(i)) {
172
+ return Math.floor(videoElement.buffered.end(i) * 1000);
173
+ }
174
+ }
175
+
176
+ return Math.floor(time * 1000);
177
+ }
178
+ case 'subtitlesTracks': {
179
+ if (stream === null) {
180
+ return [];
181
+ }
182
+
183
+ if (!videoElement.textTracks || !Array.from(videoElement.textTracks).length) {
184
+ return [];
185
+ }
186
+
187
+ return Array.from(videoElement.textTracks)
188
+ .filter(function(track) {
189
+ return track.kind === 'subtitles';
190
+ })
191
+ .map(function(track, index) {
192
+ return Object.freeze({
193
+ id: 'EMBEDDED_' + String(index),
194
+ lang: track.language,
195
+ label: track.label || track.language,
196
+ origin: 'EMBEDDED',
197
+ embedded: true
198
+ });
199
+ });
200
+ }
201
+ case 'selectedSubtitlesTrackId': {
202
+ if (stream === null) {
203
+ return null;
204
+ }
205
+
206
+ if (!videoElement.textTracks || !Array.from(videoElement.textTracks).length) {
207
+ return null;
208
+ }
209
+
210
+ return Array.from(videoElement.textTracks)
211
+ .reduce(function(result, track, index) {
212
+ if (result === null && track.mode === 'showing') {
213
+ return 'EMBEDDED_' + String(index);
214
+ }
215
+
216
+ return result;
217
+ }, null);
218
+ }
219
+ case 'subtitlesOffset': {
220
+ if (destroyed) {
221
+ return null;
222
+ }
223
+
224
+ return subtitlesOffset;
225
+ }
226
+ case 'subtitlesSize': {
227
+ if (destroyed) {
228
+ return null;
229
+ }
230
+
231
+ return parseInt(styleElement.sheet.cssRules[0].style.fontSize, 10) * 25;
232
+ }
233
+ case 'subtitlesTextColor': {
234
+ if (destroyed) {
235
+ return null;
236
+ }
237
+
238
+ return styleElement.sheet.cssRules[0].style.color;
239
+ }
240
+ case 'subtitlesBackgroundColor': {
241
+ if (destroyed) {
242
+ return null;
243
+ }
244
+
245
+ return styleElement.sheet.cssRules[0].style.backgroundColor;
246
+ }
247
+ case 'subtitlesOutlineColor': {
248
+ if (destroyed) {
249
+ return null;
250
+ }
251
+
252
+ return styleElement.sheet.cssRules[0].style.textShadow.slice(0, styleElement.sheet.cssRules[0].style.textShadow.indexOf(')') + 1);
253
+ }
254
+ case 'audioTracks': {
255
+ if (stream === null) {
256
+ return [];
257
+ }
258
+
259
+ if (!videoElement.audioTracks || !Array.from(videoElement.audioTracks).length) {
260
+ return [];
261
+ }
262
+
263
+ return Array.from(videoElement.audioTracks)
264
+ .map(function(track, index) {
265
+ return Object.freeze({
266
+ id: 'EMBEDDED_' + String(index),
267
+ lang: track.language,
268
+ label: track.label || track.language,
269
+ origin: 'EMBEDDED',
270
+ embedded: true
271
+ });
272
+ });
273
+ }
274
+ case 'selectedAudioTrackId': {
275
+
276
+ if (stream === null) {
277
+ return null;
278
+ }
279
+
280
+ if (!videoElement.audioTracks || !Array.from(videoElement.audioTracks).length) {
281
+ return null;
282
+ }
283
+
284
+ return Array.from(videoElement.audioTracks)
285
+ .reduce(function(result, track, index) {
286
+ if (result === null && track.enabled) {
287
+ return 'EMBEDDED_' + String(index);
288
+ }
289
+
290
+ return result;
291
+ }, null);
292
+ }
293
+ case 'volume': {
294
+ if (destroyed || videoElement.volume === null || !isFinite(videoElement.volume)) {
295
+ return null;
296
+ }
297
+
298
+ return Math.floor(videoElement.volume * 100);
299
+ }
300
+ case 'muted': {
301
+ if (destroyed) {
302
+ return null;
303
+ }
304
+
305
+ return !!videoElement.muted;
306
+ }
307
+ case 'playbackSpeed': {
308
+ if (destroyed || videoElement.playbackRate === null || !isFinite(videoElement.playbackRate)) {
309
+ return null;
310
+ }
311
+
312
+ return videoElement.playbackRate;
313
+ }
314
+ default: {
315
+ return null;
316
+ }
317
+ }
318
+ }
319
+ function onCueChange() {
320
+ Array.from(videoElement.textTracks).forEach(function(track) {
321
+ Array.from(track.cues || []).forEach(function(cue) {
322
+ cue.snapToLines = false;
323
+ cue.line = 100 - subtitlesOffset;
324
+ });
325
+ });
326
+ }
327
+ function onVideoError() {
328
+ if (destroyed) {
329
+ return;
330
+ }
331
+
332
+ var error;
333
+ switch (videoElement.error.code) {
334
+ case 1: {
335
+ error = ERROR.HTML_VIDEO.MEDIA_ERR_ABORTED;
336
+ break;
337
+ }
338
+ case 2: {
339
+ error = ERROR.HTML_VIDEO.MEDIA_ERR_NETWORK;
340
+ break;
341
+ }
342
+ case 3: {
343
+ error = ERROR.HTML_VIDEO.MEDIA_ERR_DECODE;
344
+ break;
345
+ }
346
+ case 4: {
347
+ error = ERROR.HTML_VIDEO.MEDIA_ERR_SRC_NOT_SUPPORTED;
348
+ break;
349
+ }
350
+ default: {
351
+ error = ERROR.UNKNOWN_ERROR;
352
+ }
353
+ }
354
+ onError(Object.assign({}, error, {
355
+ critical: true,
356
+ error: videoElement.error
357
+ }));
358
+ }
359
+ function onError(error) {
360
+ events.emit('error', error);
361
+ if (error.critical) {
362
+ command('unload');
363
+ }
364
+ }
365
+ function onEnded() {
366
+ events.emit('ended');
367
+ }
368
+ function onPropChanged(propName) {
369
+ if (observedProps[propName]) {
370
+ events.emit('propChanged', propName, getProp(propName));
371
+ }
372
+ }
373
+ function observeProp(propName) {
374
+ if (observedProps.hasOwnProperty(propName)) {
375
+ events.emit('propValue', propName, getProp(propName));
376
+ observedProps[propName] = true;
377
+ }
378
+ }
379
+ function setProp(propName, propValue) {
380
+ switch (propName) {
381
+ case 'paused': {
382
+ if (stream !== null) {
383
+ propValue ? videoElement.pause() : videoElement.play();
384
+ onPropChanged('paused');
385
+ }
386
+
387
+ break;
388
+ }
389
+ case 'time': {
390
+ if (stream !== null && propValue !== null && isFinite(propValue)) {
391
+ videoElement.currentTime = parseInt(propValue, 10) / 1000;
392
+ onPropChanged('time');
393
+ }
394
+
395
+ break;
396
+ }
397
+ case 'selectedSubtitlesTrackId': {
398
+ if (stream !== null) {
399
+ Array.from(videoElement.textTracks)
400
+ .forEach(function(track, index) {
401
+ track.mode = 'EMBEDDED_' + String(index) === propValue ? 'showing' : 'disabled';
402
+ });
403
+ var selectedSubtitlesTrack = getProp('subtitlesTracks')
404
+ .find(function(track) {
405
+ return track.id === propValue;
406
+ });
407
+ if (selectedSubtitlesTrack) {
408
+ onPropChanged('selectedSubtitlesTrackId');
409
+ events.emit('subtitlesTrackLoaded', selectedSubtitlesTrack);
410
+ }
411
+ }
412
+
413
+ break;
414
+ }
415
+ case 'subtitlesOffset': {
416
+ if (propValue !== null && isFinite(propValue)) {
417
+ subtitlesOffset = Math.max(0, Math.min(100, parseInt(propValue, 10)));
418
+ onCueChange();
419
+ onPropChanged('subtitlesOffset');
420
+ }
421
+
422
+ break;
423
+ }
424
+ case 'subtitlesSize': {
425
+ if (propValue !== null && isFinite(propValue)) {
426
+ styleElement.sheet.cssRules[0].style.fontSize = Math.floor(Math.max(0, parseInt(propValue, 10)) / 25) + 'vmin';
427
+ onPropChanged('subtitlesSize');
428
+ }
429
+
430
+ break;
431
+ }
432
+ case 'subtitlesTextColor': {
433
+ if (typeof propValue === 'string') {
434
+ try {
435
+ styleElement.sheet.cssRules[0].style.color = Color(propValue).rgb().string();
436
+ } catch (error) {
437
+ // eslint-disable-next-line no-console
438
+ console.error('TitanVideo', error);
439
+ }
440
+
441
+ onPropChanged('subtitlesTextColor');
442
+ }
443
+
444
+ break;
445
+ }
446
+ case 'subtitlesBackgroundColor': {
447
+ if (typeof propValue === 'string') {
448
+ try {
449
+ styleElement.sheet.cssRules[0].style.backgroundColor = Color(propValue).rgb().string();
450
+ } catch (error) {
451
+ // eslint-disable-next-line no-console
452
+ console.error('TitanVideo', error);
453
+ }
454
+
455
+ onPropChanged('subtitlesBackgroundColor');
456
+ }
457
+
458
+ break;
459
+ }
460
+ case 'subtitlesOutlineColor': {
461
+ if (typeof propValue === 'string') {
462
+ try {
463
+ styleElement.sheet.cssRules[0].style.textShadow = Color(propValue).rgb().string() + ' 1px 1px 0.1em';
464
+ } catch (error) {
465
+ // eslint-disable-next-line no-console
466
+ console.error('TitanVideo', error);
467
+ }
468
+
469
+ onPropChanged('subtitlesOutlineColor');
470
+ }
471
+
472
+ break;
473
+ }
474
+ case 'selectedAudioTrackId': {
475
+ if (stream !== null) {
476
+ for (var index = 0; index < videoElement.audioTracks.length; index++) {
477
+ videoElement.audioTracks[index].enabled = !!('EMBEDDED_' + String(index) === propValue);
478
+ }
479
+ }
480
+
481
+ var selectedAudioTrack = getProp('audioTracks')
482
+ .find(function(track) {
483
+ return track.id === propValue;
484
+ });
485
+
486
+ if (selectedAudioTrack) {
487
+ onPropChanged('selectedAudioTrackId');
488
+ events.emit('audioTrackLoaded', selectedAudioTrack);
489
+ }
490
+
491
+ break;
492
+ }
493
+ case 'volume': {
494
+ if (propValue !== null && isFinite(propValue)) {
495
+ videoElement.muted = false;
496
+ videoElement.volume = Math.max(0, Math.min(100, parseInt(propValue, 10))) / 100;
497
+ onPropChanged('muted');
498
+ onPropChanged('volume');
499
+ }
500
+
501
+ break;
502
+ }
503
+ case 'muted': {
504
+ videoElement.muted = !!propValue;
505
+ onPropChanged('muted');
506
+ break;
507
+ }
508
+ case 'playbackSpeed': {
509
+ if (propValue !== null && isFinite(propValue)) {
510
+ videoElement.playbackRate = parseFloat(propValue);
511
+ onPropChanged('playbackSpeed');
512
+ }
513
+
514
+ break;
515
+ }
516
+ }
517
+ }
518
+ function command(commandName, commandArgs) {
519
+ switch (commandName) {
520
+ case 'load': {
521
+ command('unload');
522
+ if (commandArgs && commandArgs.stream && typeof commandArgs.stream.url === 'string') {
523
+ stream = commandArgs.stream;
524
+ onPropChanged('stream');
525
+ onPropChanged('loaded');
526
+ videoElement.autoplay = typeof commandArgs.autoplay === 'boolean' ? commandArgs.autoplay : true;
527
+ videoElement.currentTime = commandArgs.time !== null && isFinite(commandArgs.time) ? parseInt(commandArgs.time, 10) / 1000 : 0;
528
+ onPropChanged('paused');
529
+ onPropChanged('time');
530
+ onPropChanged('duration');
531
+ onPropChanged('buffering');
532
+ onPropChanged('buffered');
533
+ if (videoElement.textTracks) {
534
+ videoElement.textTracks.onaddtrack = function() {
535
+ videoElement.textTracks.onaddtrack = null;
536
+ setTimeout(function() {
537
+ onPropChanged('subtitlesTracks');
538
+ onPropChanged('selectedSubtitlesTrackId');
539
+ });
540
+ };
541
+ }
542
+ if (videoElement.audioTracks) {
543
+ videoElement.audioTracks.onaddtrack = function() {
544
+ videoElement.audioTracks.onaddtrack = null;
545
+ setTimeout(function() {
546
+ onPropChanged('audioTracks');
547
+ onPropChanged('selectedAudioTrackId');
548
+ });
549
+ };
550
+ }
551
+ videoElement.src = stream.url;
552
+ } else {
553
+ onError(Object.assign({}, ERROR.UNSUPPORTED_STREAM, {
554
+ critical: true,
555
+ stream: commandArgs ? commandArgs.stream : null
556
+ }));
557
+ }
558
+ break;
559
+ }
560
+ case 'unload': {
561
+ stream = null;
562
+ Array.from(videoElement.textTracks).forEach(function(track) {
563
+ track.oncuechange = null;
564
+ });
565
+ videoElement.removeAttribute('src');
566
+ videoElement.load();
567
+ videoElement.currentTime = 0;
568
+ onPropChanged('stream');
569
+ onPropChanged('loaded');
570
+ onPropChanged('paused');
571
+ onPropChanged('time');
572
+ onPropChanged('duration');
573
+ onPropChanged('buffering');
574
+ onPropChanged('buffered');
575
+ onPropChanged('subtitlesTracks');
576
+ onPropChanged('selectedSubtitlesTrackId');
577
+ onPropChanged('audioTracks');
578
+ onPropChanged('selectedAudioTrackId');
579
+ break;
580
+ }
581
+ case 'destroy': {
582
+ command('unload');
583
+ destroyed = true;
584
+ onPropChanged('subtitlesOffset');
585
+ onPropChanged('subtitlesSize');
586
+ onPropChanged('subtitlesTextColor');
587
+ onPropChanged('subtitlesBackgroundColor');
588
+ onPropChanged('subtitlesOutlineColor');
589
+ onPropChanged('volume');
590
+ onPropChanged('muted');
591
+ onPropChanged('playbackSpeed');
592
+ events.removeAllListeners();
593
+ videoElement.onerror = null;
594
+ videoElement.onended = null;
595
+ videoElement.onpause = null;
596
+ videoElement.onplay = null;
597
+ videoElement.ontimeupdate = null;
598
+ videoElement.ondurationchange = null;
599
+ videoElement.onwaiting = null;
600
+ videoElement.onseeking = null;
601
+ videoElement.onseeked = null;
602
+ videoElement.onstalled = null;
603
+ videoElement.onplaying = null;
604
+ videoElement.oncanplay = null;
605
+ videoElement.canplaythrough = null;
606
+ videoElement.onloadeddata = null;
607
+ videoElement.onvolumechange = null;
608
+ videoElement.onratechange = null;
609
+ videoElement.textTracks.onchange = null;
610
+ containerElement.removeChild(videoElement);
611
+ containerElement.removeChild(styleElement);
612
+ break;
613
+ }
614
+ }
615
+ }
616
+
617
+ this.on = function(eventName, listener) {
618
+ if (destroyed) {
619
+ throw new Error('Video is destroyed');
620
+ }
621
+
622
+ events.on(eventName, listener);
623
+ };
624
+ this.dispatch = function(action) {
625
+ if (destroyed) {
626
+ throw new Error('Video is destroyed');
627
+ }
628
+
629
+ if (action) {
630
+ action = deepFreeze(cloneDeep(action));
631
+ switch (action.type) {
632
+ case 'observeProp': {
633
+ observeProp(action.propName);
634
+ return;
635
+ }
636
+ case 'setProp': {
637
+ setProp(action.propName, action.propValue);
638
+ return;
639
+ }
640
+ case 'command': {
641
+ command(action.commandName, action.commandArgs);
642
+ return;
643
+ }
644
+ }
645
+ }
646
+
647
+ throw new Error('Invalid action dispatched: ' + JSON.stringify(action));
648
+ };
649
+ }
650
+
651
+ TitanVideo.canPlayStream = function(stream) {
652
+ if (!stream) {
653
+ return Promise.resolve(false);
654
+ }
655
+
656
+ return Promise.resolve(true);
657
+ };
658
+
659
+ TitanVideo.manifest = {
660
+ name: 'TitanVideo',
661
+ external: false,
662
+ props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed'],
663
+ commands: ['load', 'unload', 'destroy'],
664
+ events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded']
665
+ };
666
+
667
+ module.exports = TitanVideo;
@@ -0,0 +1,3 @@
1
+ var TitanVideo = require('./TitanVideo');
2
+
3
+ module.exports = TitanVideo;
@@ -633,9 +633,11 @@ function TizenVideo(options) {
633
633
 
634
634
  var tizenVersion = false;
635
635
 
636
- try {
637
- tizenVersion = parseFloat(global.tizen.systeminfo.getCapability('http://tizen.org/feature/platform.version'));
638
- } catch(e) {}
636
+ var TIZEN_MATCHES = navigator.userAgent.match(/Tizen (\d+\.\d+)/i);
637
+
638
+ if (TIZEN_MATCHES && TIZEN_MATCHES[1]) {
639
+ tizenVersion = parseFloat(TIZEN_MATCHES[1]);
640
+ }
639
641
 
640
642
  if (!tizenVersion || tizenVersion >= 6) {
641
643
  retrieveExtendedTracks();
@@ -0,0 +1,6 @@
1
+ var platform = null;
2
+
3
+ module.exports = {
4
+ set: function(val) { platform = val; },
5
+ get: function() { return platform; }
6
+ };
@@ -1,5 +1,7 @@
1
+ var platform = require('./platform');
2
+
1
3
  function supportsTranscoding() {
2
- if (typeof global.tizen !== 'undefined' || typeof global.webOS !== 'undefined' || typeof window.qt !== 'undefined') {
4
+ if (['Tizen', 'webOS', 'Titan', 'NetTV'].includes(platform.get()) || typeof window.qt !== 'undefined') {
3
5
  return Promise.resolve(false);
4
6
  }
5
7
  return Promise.resolve(true);
@@ -225,40 +225,48 @@ function withHTMLSubtitles(Video) {
225
225
  if (selectedTrack) {
226
226
  selectedTrackId = selectedTrack.id;
227
227
  delay = 0;
228
- fetch(selectedTrack.url)
229
- .then(function(resp) {
230
- if (resp.ok) {
231
- return resp.text();
232
- }
233
-
234
- throw new Error(resp.status + ' (' + resp.statusText + ')');
235
- })
236
- .then(function(text) {
237
- return subtitlesConverter.convert(text);
238
- })
239
- .then(function(text) {
240
- return subtitlesParser.parse(text);
241
- })
242
- .then(function(result) {
243
- if (selectedTrackId !== selectedTrack.id) {
244
- return;
245
- }
246
-
247
- cuesByTime = result;
248
- renderSubtitles();
249
- events.emit('extraSubtitlesTrackLoaded', selectedTrack);
250
- })
251
- .catch(function(error) {
252
- if (selectedTrackId !== selectedTrack.id) {
253
- return;
254
- }
255
-
256
- onError(Object.assign({}, ERROR.WITH_HTML_SUBTITLES.LOAD_FAILED, {
257
- error: error,
258
- track: selectedTrack,
259
- critical: false
260
- }));
261
- });
228
+ function loadSubtitleFromUrl(url, isFallback) {
229
+ fetch(url)
230
+ .then(function(resp) {
231
+ if (resp.ok) {
232
+ return resp.text();
233
+ }
234
+
235
+ throw new Error(resp.status + ' (' + resp.statusText + ')');
236
+ })
237
+ .then(function(text) {
238
+ return subtitlesConverter.convert(text);
239
+ })
240
+ .then(function(text) {
241
+ return subtitlesParser.parse(text);
242
+ })
243
+ .then(function(result) {
244
+ if (selectedTrackId !== selectedTrack.id) {
245
+ return;
246
+ }
247
+
248
+ cuesByTime = result;
249
+ renderSubtitles();
250
+ events.emit('extraSubtitlesTrackLoaded', selectedTrack);
251
+ })
252
+ .catch(function(error) {
253
+ if (selectedTrackId !== selectedTrack.id) {
254
+ return;
255
+ }
256
+
257
+ if (!isFallback && typeof selectedTrack.fallbackUrl === 'string') {
258
+ loadSubtitleFromUrl(selectedTrack.fallbackUrl, true);
259
+ return;
260
+ }
261
+
262
+ onError(Object.assign({}, ERROR.WITH_HTML_SUBTITLES.LOAD_FAILED, {
263
+ error: error,
264
+ track: selectedTrack,
265
+ critical: false
266
+ }));
267
+ });
268
+ }
269
+ loadSubtitleFromUrl(selectedTrack.url);
262
270
  }
263
271
  renderSubtitles();
264
272
  onPropChanged('selectedExtraSubtitlesTrackId');
@@ -259,6 +259,8 @@ function withStreamingServer(Video) {
259
259
  commandArgs: Object.assign({}, commandArgs, {
260
260
  tracks: commandArgs.tracks.map(function(track) {
261
261
  return Object.assign({}, track, {
262
+ // fallback is used in case server conversion fails (if server is offline)
263
+ fallbackUrl: track.url,
262
264
  url: typeof track.url === 'string' ?
263
265
  url.resolve(loadArgs.streamingServerURL, '/subtitles.vtt?' + new URLSearchParams([['from', track.url]]).toString())
264
266
  :