@stremio/stremio-video 0.0.52 → 0.0.54

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.52",
3
+ "version": "0.0.54",
4
4
  "description": "Abstraction layer on top of different media players",
5
5
  "author": "Smart Code OOD",
6
6
  "main": "src/index.js",
@@ -351,6 +351,10 @@ function ShellVideo(options) {
351
351
 
352
352
  ipc.send('mpv-set-prop', ['no-sub-ass']);
353
353
 
354
+ // Hardware decoding
355
+ var hwdecValue = commandArgs.hardwareDecoding ? 'auto-copy' : 'no';
356
+ ipc.send('mpv-set-prop', ['hwdec', hwdecValue]);
357
+
354
358
  // opengl-cb is an alias for the new name "libmpv", as shown in mpv's video/out/vo.c aliases
355
359
  // opengl is an alias for the new name "gpu"
356
360
  // When on Windows we use d3d for the rendering in separate window
@@ -66,6 +66,9 @@ function StremioVideo() {
66
66
  video.on('extraSubtitlesTrackLoaded', function(track) {
67
67
  events.emit('extraSubtitlesTrackLoaded', track);
68
68
  });
69
+ video.on('extraSubtitlesTrackAdded', function(track) {
70
+ events.emit('extraSubtitlesTrackAdded', track);
71
+ });
69
72
  if (Video.manifest.external) {
70
73
  video.on('implementationChanged', function(manifest) {
71
74
  events.emit('implementationChanged', manifest);
@@ -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,7 +454,13 @@ 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) {
@@ -414,8 +476,8 @@ function TitanVideo(options) {
414
476
  }
415
477
  case 'subtitlesOffset': {
416
478
  if (propValue !== null && isFinite(propValue)) {
417
- subtitlesOffset = Math.max(0, Math.min(100, parseInt(propValue, 10)));
418
- onCueChange();
479
+ offset = Math.max(0, Math.min(100, parseInt(propValue, 10)));
480
+ refreshSubtitle();
419
481
  onPropChanged('subtitlesOffset');
420
482
  }
421
483
 
@@ -423,7 +485,8 @@ function TitanVideo(options) {
423
485
  }
424
486
  case 'subtitlesSize': {
425
487
  if (propValue !== null && isFinite(propValue)) {
426
- styleElement.sheet.cssRules[0].style.fontSize = Math.floor(Math.max(0, parseInt(propValue, 10)) / 25) + 'vmin';
488
+ size = Math.max(0, parseInt(propValue, 10));
489
+ refreshSubtitle();
427
490
  onPropChanged('subtitlesSize');
428
491
  }
429
492
 
@@ -432,12 +495,13 @@ function TitanVideo(options) {
432
495
  case 'subtitlesTextColor': {
433
496
  if (typeof propValue === 'string') {
434
497
  try {
435
- styleElement.sheet.cssRules[0].style.color = Color(propValue).rgb().string();
498
+ textColor = Color(propValue).rgb().string();
436
499
  } catch (error) {
437
500
  // eslint-disable-next-line no-console
438
- console.error('TitanVideo', error);
501
+ console.error('Tizen player with HTML Subtitles', error);
439
502
  }
440
503
 
504
+ refreshSubtitle();
441
505
  onPropChanged('subtitlesTextColor');
442
506
  }
443
507
 
@@ -446,12 +510,14 @@ function TitanVideo(options) {
446
510
  case 'subtitlesBackgroundColor': {
447
511
  if (typeof propValue === 'string') {
448
512
  try {
449
- styleElement.sheet.cssRules[0].style.backgroundColor = Color(propValue).rgb().string();
513
+ backgroundColor = Color(propValue).rgb().string();
450
514
  } catch (error) {
451
515
  // eslint-disable-next-line no-console
452
- console.error('TitanVideo', error);
516
+ console.error('Tizen player with HTML Subtitles', error);
453
517
  }
454
518
 
519
+ refreshSubtitle();
520
+
455
521
  onPropChanged('subtitlesBackgroundColor');
456
522
  }
457
523
 
@@ -460,17 +526,35 @@ function TitanVideo(options) {
460
526
  case 'subtitlesOutlineColor': {
461
527
  if (typeof propValue === 'string') {
462
528
  try {
463
- styleElement.sheet.cssRules[0].style.textShadow = Color(propValue).rgb().string() + ' 1px 1px 0.1em';
529
+ outlineColor = Color(propValue).rgb().string();
464
530
  } catch (error) {
465
531
  // eslint-disable-next-line no-console
466
- console.error('TitanVideo', error);
532
+ console.error('Tizen player with HTML Subtitles', error);
467
533
  }
468
534
 
535
+ refreshSubtitle();
536
+
469
537
  onPropChanged('subtitlesOutlineColor');
470
538
  }
471
539
 
472
540
  break;
473
541
  }
542
+ case 'subtitlesOpacity': {
543
+ if (typeof propValue === 'number') {
544
+ try {
545
+ subtitlesOpacity = Math.min(Math.max(propValue / 100, 0), 1);
546
+ } catch (error) {
547
+ // eslint-disable-next-line no-console
548
+ console.error('Tizen player with HTML Subtitles', error);
549
+ }
550
+
551
+ refreshSubtitle();
552
+
553
+ onPropChanged('subtitlesOpacity');
554
+ }
555
+
556
+ break;
557
+ }
474
558
  case 'selectedAudioTrackId': {
475
559
  if (stream !== null) {
476
560
  for (var index = 0; index < videoElement.audioTracks.length; index++) {
@@ -529,7 +613,6 @@ function TitanVideo(options) {
529
613
  onPropChanged('time');
530
614
  onPropChanged('duration');
531
615
  onPropChanged('buffering');
532
- onPropChanged('buffered');
533
616
  if (videoElement.textTracks) {
534
617
  videoElement.textTracks.onaddtrack = function() {
535
618
  videoElement.textTracks.onaddtrack = null;
@@ -571,7 +654,6 @@ function TitanVideo(options) {
571
654
  onPropChanged('time');
572
655
  onPropChanged('duration');
573
656
  onPropChanged('buffering');
574
- onPropChanged('buffered');
575
657
  onPropChanged('subtitlesTracks');
576
658
  onPropChanged('selectedSubtitlesTrackId');
577
659
  onPropChanged('audioTracks');
@@ -608,7 +690,6 @@ function TitanVideo(options) {
608
690
  videoElement.onratechange = null;
609
691
  videoElement.textTracks.onchange = null;
610
692
  containerElement.removeChild(videoElement);
611
- containerElement.removeChild(styleElement);
612
693
  break;
613
694
  }
614
695
  }
@@ -659,7 +740,7 @@ TitanVideo.canPlayStream = function(stream) {
659
740
  TitanVideo.manifest = {
660
741
  name: 'TitanVideo',
661
742
  external: false,
662
- props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed'],
743
+ props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'subtitlesOpacity', 'volume', 'muted', 'playbackSpeed'],
663
744
  commands: ['load', 'unload', 'destroy'],
664
745
  events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded']
665
746
  };
@@ -225,15 +225,36 @@ function withHTMLSubtitles(Video) {
225
225
  if (selectedTrack) {
226
226
  selectedTrackId = selectedTrack.id;
227
227
  delay = 0;
228
- function loadSubtitleFromUrl(url, isFallback) {
229
- fetch(url)
230
- .then(function(resp) {
231
- if (resp.ok) {
232
- return resp.text();
233
- }
234
228
 
235
- throw new Error(resp.status + ' (' + resp.statusText + ')');
236
- })
229
+ function getSubtitlesData(track, isFallback) {
230
+ var url = isFallback ? track.fallbackUrl : track.url;
231
+
232
+ if (typeof url === 'string') {
233
+ return fetch(url)
234
+ .then(function(resp) {
235
+ if (resp.ok) {
236
+ return resp.text();
237
+ }
238
+
239
+ throw new Error(resp.status + ' (' + resp.statusText + ')');
240
+ });
241
+ }
242
+
243
+ if (track.buffer instanceof ArrayBuffer) {
244
+ try {
245
+ const uInt8Array = new Uint8Array(track.buffer);
246
+ const text = new TextDecoder().decode(uInt8Array);
247
+ return Promise.resolve(text);
248
+ } catch(e) {
249
+ return Promise.reject(e);
250
+ }
251
+ }
252
+
253
+ return Promise.reject('No `url` or `buffer` field available for this track');
254
+ }
255
+
256
+ function loadSubtitles(track, isFallback) {
257
+ getSubtitlesData(track, isFallback)
237
258
  .then(function(text) {
238
259
  return subtitlesConverter.convert(text);
239
260
  })
@@ -255,7 +276,7 @@ function withHTMLSubtitles(Video) {
255
276
  }
256
277
 
257
278
  if (!isFallback && typeof selectedTrack.fallbackUrl === 'string') {
258
- loadSubtitleFromUrl(selectedTrack.fallbackUrl, true);
279
+ loadSubtitles(selectedTrack, true);
259
280
  return;
260
281
  }
261
282
 
@@ -266,7 +287,7 @@ function withHTMLSubtitles(Video) {
266
287
  }));
267
288
  });
268
289
  }
269
- loadSubtitleFromUrl(selectedTrack.url);
290
+ loadSubtitles(selectedTrack);
270
291
  }
271
292
  renderSubtitles();
272
293
  onPropChanged('selectedExtraSubtitlesTrackId');
@@ -374,7 +395,6 @@ function withHTMLSubtitles(Video) {
374
395
  .filter(function(track, index, tracks) {
375
396
  return track &&
376
397
  typeof track.id === 'string' &&
377
- typeof track.url === 'string' &&
378
398
  typeof track.lang === 'string' &&
379
399
  typeof track.label === 'string' &&
380
400
  typeof track.origin === 'string' &&
@@ -386,6 +406,31 @@ function withHTMLSubtitles(Video) {
386
406
 
387
407
  return true;
388
408
  }
409
+ case 'addLocalSubtitles': {
410
+ if (commandArgs && typeof commandArgs.filename === 'string' && commandArgs.buffer instanceof ArrayBuffer) {
411
+ var id = 'LOCAL_' + tracks
412
+ .filter(function(track) { return track.local; })
413
+ .length;
414
+
415
+ var track = {
416
+ id: id,
417
+ url: null,
418
+ buffer: commandArgs.buffer,
419
+ lang: 'local',
420
+ label: commandArgs.filename,
421
+ origin: 'LOCAL',
422
+ local: true,
423
+ embedded: false,
424
+ };
425
+
426
+ tracks.push(track);
427
+
428
+ onPropChanged('extraSubtitlesTracks');
429
+ events.emit('extraSubtitlesTrackAdded', track);
430
+ }
431
+
432
+ return true;
433
+ }
389
434
  case 'load': {
390
435
  command('unload');
391
436
  if (commandArgs.stream && Array.isArray(commandArgs.stream.subtitles)) {
@@ -485,9 +530,9 @@ function withHTMLSubtitles(Video) {
485
530
  external: Video.manifest.external,
486
531
  props: Video.manifest.props.concat(['extraSubtitlesTracks', 'selectedExtraSubtitlesTrackId', 'extraSubtitlesDelay', 'extraSubtitlesSize', 'extraSubtitlesOffset', 'extraSubtitlesTextColor', 'extraSubtitlesBackgroundColor', 'extraSubtitlesOutlineColor', 'extraSubtitlesOpacity'])
487
532
  .filter(function(value, index, array) { return array.indexOf(value) === index; }),
488
- commands: Video.manifest.commands.concat(['load', 'unload', 'destroy', 'addExtraSubtitlesTracks'])
533
+ commands: Video.manifest.commands.concat(['load', 'unload', 'destroy', 'addExtraSubtitlesTracks', 'addLocalSubtitles'])
489
534
  .filter(function(value, index, array) { return array.indexOf(value) === index; }),
490
- events: Video.manifest.events.concat(['propValue', 'propChanged', 'error', 'extraSubtitlesTrackLoaded'])
535
+ events: Video.manifest.events.concat(['propValue', 'propChanged', 'error', 'extraSubtitlesTrackLoaded', 'extraSubtitlesTrackAdded'])
491
536
  .filter(function(value, index, array) { return array.indexOf(value) === index; })
492
537
  };
493
538