@stremio/stremio-video 0.0.53 → 0.0.55

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.53",
3
+ "version": "0.0.55",
4
4
  "description": "Abstraction layer on top of different media players",
5
5
  "author": "Smart Code OOD",
6
6
  "main": "src/index.js",
@@ -4,17 +4,23 @@ var deepFreeze = require('deep-freeze');
4
4
  var Color = require('color');
5
5
  var ERROR = require('../error');
6
6
 
7
+ var SSA_DESCRIPTORS_REGEX = /^\{(\\an[1-8])+\}/i;
8
+
7
9
  function TitanVideo(options) {
8
10
  options = options || {};
9
11
 
12
+ var size = 100;
13
+ var offset = 0;
14
+ var textColor = 'rgb(255, 255, 255)';
15
+ var backgroundColor = 'rgba(0, 0, 0, 0)';
16
+ var outlineColor = 'rgb(34, 34, 34)';
17
+ var subtitlesOpacity = 1;
18
+
10
19
  var containerElement = options.containerElement;
11
20
  if (!(containerElement instanceof HTMLElement)) {
12
21
  throw new Error('Container element required to be instance of HTMLElement');
13
22
  }
14
23
 
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
24
  var videoElement = document.createElement('video');
19
25
  videoElement.style.width = '100%';
20
26
  videoElement.style.height = '100%';
@@ -35,48 +41,39 @@ function TitanVideo(options) {
35
41
  };
36
42
  videoElement.ontimeupdate = function() {
37
43
  onPropChanged('time');
38
- onPropChanged('buffered');
39
44
  };
40
45
  videoElement.ondurationchange = function() {
41
46
  onPropChanged('duration');
42
47
  };
43
48
  videoElement.onwaiting = function() {
44
49
  onPropChanged('buffering');
45
- onPropChanged('buffered');
46
50
  };
47
51
  videoElement.onseeking = function() {
48
52
  onPropChanged('time');
49
53
  onPropChanged('buffering');
50
- onPropChanged('buffered');
51
54
  };
52
55
  videoElement.onseeked = function() {
53
56
  onPropChanged('time');
54
57
  onPropChanged('buffering');
55
- onPropChanged('buffered');
56
58
  };
57
59
  videoElement.onstalled = function() {
58
60
  onPropChanged('buffering');
59
- onPropChanged('buffered');
60
61
  };
61
62
  videoElement.onplaying = function() {
62
63
  onPropChanged('time');
63
64
  onPropChanged('buffering');
64
- onPropChanged('buffered');
65
65
  };
66
66
  videoElement.oncanplay = function() {
67
67
  onPropChanged('buffering');
68
- onPropChanged('buffered');
69
68
  };
70
69
  videoElement.canplaythrough = function() {
71
70
  onPropChanged('buffering');
72
- onPropChanged('buffered');
73
71
  };
74
72
  videoElement.onloadedmetadata = function() {
75
73
  onPropChanged('loaded');
76
74
  };
77
75
  videoElement.onloadeddata = function() {
78
76
  onPropChanged('buffering');
79
- onPropChanged('buffered');
80
77
  };
81
78
  videoElement.onvolumechange = function() {
82
79
  onPropChanged('volume');
@@ -88,17 +85,23 @@ function TitanVideo(options) {
88
85
  videoElement.textTracks.onchange = function() {
89
86
  onPropChanged('subtitlesTracks');
90
87
  onPropChanged('selectedSubtitlesTrackId');
91
- onCueChange();
92
- Array.from(videoElement.textTracks).forEach(function(track) {
93
- track.oncuechange = onCueChange;
94
- });
95
88
  };
96
89
  containerElement.appendChild(videoElement);
97
90
 
91
+ var subtitlesElement = document.createElement('div');
92
+ subtitlesElement.style.position = 'absolute';
93
+ subtitlesElement.style.right = '0';
94
+ subtitlesElement.style.bottom = '0';
95
+ subtitlesElement.style.left = '0';
96
+ subtitlesElement.style.zIndex = '1';
97
+ subtitlesElement.style.textAlign = 'center';
98
+ containerElement.style.position = 'relative';
99
+ containerElement.style.zIndex = '0';
100
+ containerElement.appendChild(subtitlesElement);
101
+
98
102
  var events = new EventEmitter();
99
103
  var destroyed = false;
100
104
  var stream = null;
101
- var subtitlesOffset = 0;
102
105
  var observedProps = {
103
106
  stream: false,
104
107
  loaded: false,
@@ -106,7 +109,6 @@ function TitanVideo(options) {
106
109
  time: false,
107
110
  duration: false,
108
111
  buffering: false,
109
- buffered: false,
110
112
  subtitlesTracks: false,
111
113
  selectedSubtitlesTrackId: false,
112
114
  subtitlesOffset: false,
@@ -121,6 +123,74 @@ function TitanVideo(options) {
121
123
  playbackSpeed: false
122
124
  };
123
125
 
126
+ var lastSub;
127
+ var disabledSubs = false;
128
+
129
+ async function refreshSubtitle() {
130
+ if (lastSub) {
131
+ renderSubtitle(lastSub.text, 'show');
132
+ }
133
+ }
134
+
135
+ async function renderSubtitle(text, visibility) {
136
+ if (disabledSubs) return;
137
+ if (visibility === 'hide') {
138
+ while (subtitlesElement.hasChildNodes()) {
139
+ subtitlesElement.removeChild(subtitlesElement.lastChild);
140
+ }
141
+ lastSub = null;
142
+ return;
143
+ }
144
+
145
+ lastSub = {
146
+ text: text,
147
+ };
148
+
149
+ while (subtitlesElement.hasChildNodes()) {
150
+ subtitlesElement.removeChild(subtitlesElement.lastChild);
151
+ }
152
+
153
+ subtitlesElement.style.bottom = offset + '%';
154
+ subtitlesElement.style.opacity = subtitlesOpacity;
155
+
156
+ var cueNode = document.createElement('span');
157
+ cueNode.innerHTML = text;
158
+ cueNode.style.display = 'inline-block';
159
+ cueNode.style.padding = '0.2em';
160
+ cueNode.style.fontSize = Math.floor(size / 25) + 'vmin';
161
+ cueNode.style.color = textColor;
162
+ cueNode.style.backgroundColor = backgroundColor;
163
+ cueNode.style.textShadow = '1px 1px 0.1em ' + outlineColor;
164
+ cueNode.style.whiteSpace = 'pre-wrap';
165
+
166
+ subtitlesElement.appendChild(cueNode);
167
+ subtitlesElement.appendChild(document.createElement('br'));
168
+
169
+ }
170
+
171
+ function renderCue(ev) {
172
+ var cues = (ev.target || {}).activeCues;
173
+ if (!cues.length) {
174
+ renderSubtitle('', 'hide');
175
+ } else {
176
+ if (cues.length > 3) {
177
+ // most probably SSA/ASS subs glitch
178
+ ev.target.removeEventListener('cuechange', renderCue);
179
+ renderSubtitle('', 'hide');
180
+ return;
181
+ }
182
+ var text = '';
183
+ for (var i in cues) {
184
+ var cue = cues[i];
185
+ if (cue.text) {
186
+ var cleanedText = cue.text.replace(SSA_DESCRIPTORS_REGEX, '');
187
+ text += (text ? '\n' : '') + cleanedText;
188
+ }
189
+ }
190
+ renderSubtitle(text, 'show');
191
+ }
192
+ }
193
+
124
194
  function getProp(propName) {
125
195
  switch (propName) {
126
196
  case 'stream': {
@@ -161,20 +231,6 @@ function TitanVideo(options) {
161
231
 
162
232
  return videoElement.readyState < videoElement.HAVE_FUTURE_DATA;
163
233
  }
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
234
  case 'subtitlesTracks': {
179
235
  if (stream === null) {
180
236
  return [];
@@ -209,7 +265,7 @@ function TitanVideo(options) {
209
265
 
210
266
  return Array.from(videoElement.textTracks)
211
267
  .reduce(function(result, track, index) {
212
- if (result === null && track.mode === 'showing') {
268
+ if (result === null && track.mode === 'hidden') {
213
269
  return 'EMBEDDED_' + String(index);
214
270
  }
215
271
 
@@ -221,35 +277,42 @@ function TitanVideo(options) {
221
277
  return null;
222
278
  }
223
279
 
224
- return subtitlesOffset;
280
+ return offset;
225
281
  }
226
282
  case 'subtitlesSize': {
227
283
  if (destroyed) {
228
284
  return null;
229
285
  }
230
286
 
231
- return parseInt(styleElement.sheet.cssRules[0].style.fontSize, 10) * 25;
287
+ return size;
232
288
  }
233
289
  case 'subtitlesTextColor': {
234
290
  if (destroyed) {
235
291
  return null;
236
292
  }
237
293
 
238
- return styleElement.sheet.cssRules[0].style.color;
294
+ return textColor;
239
295
  }
240
296
  case 'subtitlesBackgroundColor': {
241
297
  if (destroyed) {
242
298
  return null;
243
299
  }
244
300
 
245
- return styleElement.sheet.cssRules[0].style.backgroundColor;
301
+ return backgroundColor;
246
302
  }
247
303
  case 'subtitlesOutlineColor': {
248
304
  if (destroyed) {
249
305
  return null;
250
306
  }
251
307
 
252
- return styleElement.sheet.cssRules[0].style.textShadow.slice(0, styleElement.sheet.cssRules[0].style.textShadow.indexOf(')') + 1);
308
+ return outlineColor;
309
+ }
310
+ case 'subtitlesOpacity': {
311
+ if (destroyed) {
312
+ return null;
313
+ }
314
+
315
+ return subtitlesOpacity;
253
316
  }
254
317
  case 'audioTracks': {
255
318
  if (stream === null) {
@@ -316,14 +379,6 @@ function TitanVideo(options) {
316
379
  }
317
380
  }
318
381
  }
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
382
  function onVideoError() {
328
383
  if (destroyed) {
329
384
  return;
@@ -388,6 +443,7 @@ function TitanVideo(options) {
388
443
  }
389
444
  case 'time': {
390
445
  if (stream !== null && propValue !== null && isFinite(propValue)) {
446
+ renderSubtitle('', 'hide');
391
447
  videoElement.currentTime = parseInt(propValue, 10) / 1000;
392
448
  onPropChanged('time');
393
449
  }
@@ -398,12 +454,21 @@ function TitanVideo(options) {
398
454
  if (stream !== null) {
399
455
  Array.from(videoElement.textTracks)
400
456
  .forEach(function(track, index) {
401
- track.mode = 'EMBEDDED_' + String(index) === propValue ? 'showing' : 'disabled';
457
+ if (track.mode === 'hidden') {
458
+ track.removeEventListener('cuechange', renderCue);
459
+ }
460
+ track.mode = 'EMBEDDED_' + String(index) === propValue ? 'hidden' : 'disabled';
461
+ if (track.mode === 'hidden') {
462
+ track.addEventListener('cuechange', renderCue);
463
+ }
402
464
  });
403
465
  var selectedSubtitlesTrack = getProp('subtitlesTracks')
404
466
  .find(function(track) {
405
467
  return track.id === propValue;
406
468
  });
469
+
470
+ renderSubtitle('', 'hide');
471
+
407
472
  if (selectedSubtitlesTrack) {
408
473
  onPropChanged('selectedSubtitlesTrackId');
409
474
  events.emit('subtitlesTrackLoaded', selectedSubtitlesTrack);
@@ -414,8 +479,8 @@ function TitanVideo(options) {
414
479
  }
415
480
  case 'subtitlesOffset': {
416
481
  if (propValue !== null && isFinite(propValue)) {
417
- subtitlesOffset = Math.max(0, Math.min(100, parseInt(propValue, 10)));
418
- onCueChange();
482
+ offset = Math.max(0, Math.min(100, parseInt(propValue, 10)));
483
+ refreshSubtitle();
419
484
  onPropChanged('subtitlesOffset');
420
485
  }
421
486
 
@@ -423,7 +488,8 @@ function TitanVideo(options) {
423
488
  }
424
489
  case 'subtitlesSize': {
425
490
  if (propValue !== null && isFinite(propValue)) {
426
- styleElement.sheet.cssRules[0].style.fontSize = Math.floor(Math.max(0, parseInt(propValue, 10)) / 25) + 'vmin';
491
+ size = Math.max(0, parseInt(propValue, 10));
492
+ refreshSubtitle();
427
493
  onPropChanged('subtitlesSize');
428
494
  }
429
495
 
@@ -432,12 +498,13 @@ function TitanVideo(options) {
432
498
  case 'subtitlesTextColor': {
433
499
  if (typeof propValue === 'string') {
434
500
  try {
435
- styleElement.sheet.cssRules[0].style.color = Color(propValue).rgb().string();
501
+ textColor = Color(propValue).rgb().string();
436
502
  } catch (error) {
437
503
  // eslint-disable-next-line no-console
438
- console.error('TitanVideo', error);
504
+ console.error('Tizen player with HTML Subtitles', error);
439
505
  }
440
506
 
507
+ refreshSubtitle();
441
508
  onPropChanged('subtitlesTextColor');
442
509
  }
443
510
 
@@ -446,12 +513,14 @@ function TitanVideo(options) {
446
513
  case 'subtitlesBackgroundColor': {
447
514
  if (typeof propValue === 'string') {
448
515
  try {
449
- styleElement.sheet.cssRules[0].style.backgroundColor = Color(propValue).rgb().string();
516
+ backgroundColor = Color(propValue).rgb().string();
450
517
  } catch (error) {
451
518
  // eslint-disable-next-line no-console
452
- console.error('TitanVideo', error);
519
+ console.error('Tizen player with HTML Subtitles', error);
453
520
  }
454
521
 
522
+ refreshSubtitle();
523
+
455
524
  onPropChanged('subtitlesBackgroundColor');
456
525
  }
457
526
 
@@ -460,17 +529,35 @@ function TitanVideo(options) {
460
529
  case 'subtitlesOutlineColor': {
461
530
  if (typeof propValue === 'string') {
462
531
  try {
463
- styleElement.sheet.cssRules[0].style.textShadow = Color(propValue).rgb().string() + ' 1px 1px 0.1em';
532
+ outlineColor = Color(propValue).rgb().string();
464
533
  } catch (error) {
465
534
  // eslint-disable-next-line no-console
466
- console.error('TitanVideo', error);
535
+ console.error('Tizen player with HTML Subtitles', error);
467
536
  }
468
537
 
538
+ refreshSubtitle();
539
+
469
540
  onPropChanged('subtitlesOutlineColor');
470
541
  }
471
542
 
472
543
  break;
473
544
  }
545
+ case 'subtitlesOpacity': {
546
+ if (typeof propValue === 'number') {
547
+ try {
548
+ subtitlesOpacity = Math.min(Math.max(propValue / 100, 0), 1);
549
+ } catch (error) {
550
+ // eslint-disable-next-line no-console
551
+ console.error('Tizen player with HTML Subtitles', error);
552
+ }
553
+
554
+ refreshSubtitle();
555
+
556
+ onPropChanged('subtitlesOpacity');
557
+ }
558
+
559
+ break;
560
+ }
474
561
  case 'selectedAudioTrackId': {
475
562
  if (stream !== null) {
476
563
  for (var index = 0; index < videoElement.audioTracks.length; index++) {
@@ -529,7 +616,6 @@ function TitanVideo(options) {
529
616
  onPropChanged('time');
530
617
  onPropChanged('duration');
531
618
  onPropChanged('buffering');
532
- onPropChanged('buffered');
533
619
  if (videoElement.textTracks) {
534
620
  videoElement.textTracks.onaddtrack = function() {
535
621
  videoElement.textTracks.onaddtrack = null;
@@ -571,7 +657,6 @@ function TitanVideo(options) {
571
657
  onPropChanged('time');
572
658
  onPropChanged('duration');
573
659
  onPropChanged('buffering');
574
- onPropChanged('buffered');
575
660
  onPropChanged('subtitlesTracks');
576
661
  onPropChanged('selectedSubtitlesTrackId');
577
662
  onPropChanged('audioTracks');
@@ -608,7 +693,6 @@ function TitanVideo(options) {
608
693
  videoElement.onratechange = null;
609
694
  videoElement.textTracks.onchange = null;
610
695
  containerElement.removeChild(videoElement);
611
- containerElement.removeChild(styleElement);
612
696
  break;
613
697
  }
614
698
  }
@@ -659,7 +743,7 @@ TitanVideo.canPlayStream = function(stream) {
659
743
  TitanVideo.manifest = {
660
744
  name: 'TitanVideo',
661
745
  external: false,
662
- props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed'],
746
+ props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'subtitlesOpacity', 'volume', 'muted', 'playbackSpeed'],
663
747
  commands: ['load', 'unload', 'destroy'],
664
748
  events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded']
665
749
  };
@@ -226,9 +226,11 @@ function withHTMLSubtitles(Video) {
226
226
  selectedTrackId = selectedTrack.id;
227
227
  delay = 0;
228
228
 
229
- function getSubtitlesData(track) {
230
- if (typeof track.url === 'string') {
231
- return fetch(track.url)
229
+ function getSubtitlesData(track, isFallback) {
230
+ var url = isFallback ? track.fallbackUrl : track.url;
231
+
232
+ if (typeof url === 'string') {
233
+ return fetch(url)
232
234
  .then(function(resp) {
233
235
  if (resp.ok) {
234
236
  return resp.text();
@@ -252,7 +254,7 @@ function withHTMLSubtitles(Video) {
252
254
  }
253
255
 
254
256
  function loadSubtitles(track, isFallback) {
255
- getSubtitlesData(track)
257
+ getSubtitlesData(track, isFallback)
256
258
  .then(function(text) {
257
259
  return subtitlesConverter.convert(text);
258
260
  })