@stremio/stremio-video 0.0.77 → 0.0.79

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.77",
3
+ "version": "0.0.79",
4
4
  "description": "Abstraction layer on top of different media players",
5
5
  "author": "Smart Code OOD",
6
6
  "main": "src/index.js",
@@ -99,6 +99,13 @@ function HTMLVideo(options) {
99
99
  };
100
100
  containerElement.appendChild(videoElement);
101
101
 
102
+ function onFullscreenChanged() {
103
+ onPropChanged('fullscreen');
104
+ }
105
+ videoElement.addEventListener('webkitbeginfullscreen', onFullscreenChanged);
106
+ videoElement.addEventListener('webkitendfullscreen', onFullscreenChanged);
107
+ videoElement.addEventListener('fullscreenchange', onFullscreenChanged);
108
+
102
109
  var hls = null;
103
110
  var events = new EventEmitter();
104
111
  var destroyed = false;
@@ -125,7 +132,8 @@ function HTMLVideo(options) {
125
132
  volume: false,
126
133
  muted: false,
127
134
  playbackSpeed: false,
128
- videoScale: false
135
+ videoScale: false,
136
+ fullscreen: false
129
137
  };
130
138
 
131
139
  function getProp(propName) {
@@ -255,39 +263,49 @@ function HTMLVideo(options) {
255
263
  return Math.round(subtitlesOpacity * 100);
256
264
  }
257
265
  case 'audioTracks': {
258
- if (hls === null || !Array.isArray(hls.audioTracks)) {
266
+ if (hls === null || !Array.isArray(hls.allAudioTracks)) {
259
267
  return [];
260
268
  }
261
269
 
262
- return hls.audioTracks
263
- .map(function(track) {
270
+ return hls.allAudioTracks
271
+ .map(function(track, index) {
264
272
  return Object.freeze({
265
- id: 'EMBEDDED_' + String(track.id),
273
+ id: 'EMBEDDED_' + String(index),
266
274
  lang: typeof track.lang === 'string' && track.lang.length > 0 ?
267
275
  track.lang
268
276
  :
269
277
  typeof track.name === 'string' && track.name.length > 0 ?
270
278
  track.name
271
279
  :
272
- String(track.id),
280
+ String(index),
273
281
  label: typeof track.name === 'string' && track.name.length > 0 ?
274
282
  track.name
275
283
  :
276
284
  typeof track.lang === 'string' && track.lang.length > 0 ?
277
285
  track.lang
278
286
  :
279
- String(track.id),
287
+ String(index),
280
288
  origin: 'EMBEDDED',
281
289
  embedded: true
282
290
  });
283
291
  });
284
292
  }
285
293
  case 'selectedAudioTrackId': {
286
- if (hls === null || hls.audioTrack === null || !isFinite(hls.audioTrack) || hls.audioTrack === -1) {
294
+ if (hls === null || hls.audioTrack === -1) {
287
295
  return null;
288
296
  }
289
297
 
290
- return 'EMBEDDED_' + String(hls.audioTrack);
298
+ var currentGroupTrack = hls.audioTracks[hls.audioTrack];
299
+ if (!currentGroupTrack) {
300
+ return null;
301
+ }
302
+
303
+ var allTracksIndex = hls.allAudioTracks.indexOf(currentGroupTrack);
304
+ if (allTracksIndex === -1) {
305
+ return null;
306
+ }
307
+
308
+ return 'EMBEDDED_' + String(allTracksIndex);
291
309
  }
292
310
  case 'volume': {
293
311
  if (destroyed || videoElement.volume === null || !isFinite(videoElement.volume)) {
@@ -313,6 +331,12 @@ function HTMLVideo(options) {
313
331
  case 'videoScale': {
314
332
  return videoElement.style.objectFit || 'contain';
315
333
  }
334
+ case 'fullscreen': {
335
+ if (stream === null) {
336
+ return null;
337
+ }
338
+ return videoElement.webkitDisplayingFullscreen === true || document.fullscreenElement === videoElement;
339
+ }
316
340
  default: {
317
341
  return null;
318
342
  }
@@ -491,14 +515,18 @@ function HTMLVideo(options) {
491
515
  }
492
516
  case 'selectedAudioTrackId': {
493
517
  if (hls !== null) {
494
- var selecterdAudioTrack = getProp('audioTracks')
518
+ var selectedAudioTrack = getProp('audioTracks')
495
519
  .find(function(track) {
496
520
  return track.id === propValue;
497
521
  });
498
- hls.audioTrack = selecterdAudioTrack ? parseInt(selecterdAudioTrack.id.split('_').pop(), 10) : -1;
499
- if (selecterdAudioTrack) {
522
+ if (selectedAudioTrack) {
523
+ var trackIndex = parseInt(selectedAudioTrack.id.split('_').pop(), 10);
524
+ var allTracks = hls.allAudioTracks;
525
+ if (trackIndex >= 0 && trackIndex < allTracks.length) {
526
+ hls.setAudioOption(allTracks[trackIndex]);
527
+ }
500
528
  onPropChanged('selectedAudioTrackId');
501
- events.emit('audioTrackLoaded', selecterdAudioTrack);
529
+ events.emit('audioTrackLoaded', selectedAudioTrack);
502
530
  }
503
531
  }
504
532
 
@@ -536,6 +564,23 @@ function HTMLVideo(options) {
536
564
 
537
565
  break;
538
566
  }
567
+ case 'fullscreen': {
568
+ if (stream === null) break;
569
+ if (propValue) {
570
+ if (typeof videoElement.webkitEnterFullscreen === 'function') {
571
+ videoElement.webkitEnterFullscreen();
572
+ } else if (typeof videoElement.requestFullscreen === 'function') {
573
+ videoElement.requestFullscreen();
574
+ }
575
+ } else {
576
+ if (typeof videoElement.webkitExitFullscreen === 'function' && videoElement.webkitDisplayingFullscreen) {
577
+ videoElement.webkitExitFullscreen();
578
+ } else if (document.fullscreenElement === videoElement) {
579
+ document.exitFullscreen();
580
+ }
581
+ }
582
+ break;
583
+ }
539
584
  }
540
585
  }
541
586
  function command(commandName, commandArgs) {
@@ -557,6 +602,7 @@ function HTMLVideo(options) {
557
602
  onPropChanged('selectedSubtitlesTrackId');
558
603
  onPropChanged('audioTracks');
559
604
  onPropChanged('selectedAudioTrackId');
605
+ onPropChanged('fullscreen');
560
606
  getContentType(stream)
561
607
  .then(function(contentType) {
562
608
  if (stream !== commandArgs.stream) {
@@ -573,6 +619,9 @@ function HTMLVideo(options) {
573
619
  onPropChanged('audioTracks');
574
620
  onPropChanged('selectedAudioTrackId');
575
621
  });
622
+ hls.on(Hls.Events.MANIFEST_LOADING, function() {
623
+ hls.subtitleTrack = -1;
624
+ });
576
625
  hls.loadSource(stream.url);
577
626
  hls.attachMedia(videoElement);
578
627
  } else {
@@ -619,6 +668,7 @@ function HTMLVideo(options) {
619
668
  onPropChanged('selectedSubtitlesTrackId');
620
669
  onPropChanged('audioTracks');
621
670
  onPropChanged('selectedAudioTrackId');
671
+ onPropChanged('fullscreen');
622
672
  break;
623
673
  }
624
674
  case 'destroy': {
@@ -651,6 +701,9 @@ function HTMLVideo(options) {
651
701
  videoElement.onvolumechange = null;
652
702
  videoElement.onratechange = null;
653
703
  videoElement.textTracks.onchange = null;
704
+ videoElement.removeEventListener('webkitbeginfullscreen', onFullscreenChanged);
705
+ videoElement.removeEventListener('webkitendfullscreen', onFullscreenChanged);
706
+ videoElement.removeEventListener('fullscreenchange', onFullscreenChanged);
654
707
  containerElement.removeChild(videoElement);
655
708
  containerElement.removeChild(styleElement);
656
709
  break;
@@ -710,7 +763,7 @@ HTMLVideo.canPlayStream = function(stream) {
710
763
  HTMLVideo.manifest = {
711
764
  name: 'HTMLVideo',
712
765
  external: false,
713
- props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'subtitlesOpacity', 'volume', 'muted', 'playbackSpeed', 'videoScale'],
766
+ props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'subtitlesOpacity', 'volume', 'muted', 'playbackSpeed', 'videoScale', 'fullscreen'],
714
767
  commands: ['load', 'unload', 'destroy'],
715
768
  events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded']
716
769
  };
@@ -413,14 +413,9 @@ function ShellVideo(options) {
413
413
  var hwdecValue = commandArgs.hardwareDecoding ? 'auto-copy' : 'no';
414
414
  ipc.send('mpv-set-prop', ['hwdec', hwdecValue]);
415
415
 
416
- // On macOS the shell manages vo and HDR/EDR configuration
417
- // directly do not override vo here.
418
- var platformLower = String(commandArgs.platform || '').toLowerCase();
419
- var isMac = platformLower.indexOf('mac') !== -1;
420
- if (!isMac) {
421
- var videoOutput = platformLower === 'windows' ? (commandArgs.videoMode === null ? 'gpu-next' : 'gpu') : 'libmpv';
422
- ipc.send('mpv-set-prop', ['vo', videoOutput]);
423
- }
416
+ // Video output
417
+ var videoOutput = commandArgs.platform === 'windows' ? (commandArgs.videoMode === null ? 'gpu-next' : 'gpu') : 'libmpv';
418
+ ipc.send('mpv-set-prop', ['vo', videoOutput]);
424
419
 
425
420
  var separateWindow = options.mpvSeparateWindow ? 'yes' : 'no';
426
421
  ipc.send('mpv-set-prop', ['osc', separateWindow]);
@@ -352,7 +352,9 @@ function WebOsVideo(options) {
352
352
  mode: audioTrackId === currentAudioTrack ? 'showing' : 'disabled',
353
353
  });
354
354
  });
355
- currentAudioTrack = 'EMBEDDED_0';
355
+ if (!currentAudioTrack) {
356
+ currentAudioTrack = 'EMBEDDED_0';
357
+ }
356
358
  onPropChanged('audioTracks');
357
359
  onPropChanged('selectedAudioTrackId');
358
360
  }
@@ -40,6 +40,63 @@ function withHTMLSubtitles(Video) {
40
40
  containerElement.style.zIndex = '0';
41
41
  containerElement.appendChild(subtitlesElement);
42
42
 
43
+ var videoElement = containerElement.querySelector('video');
44
+ var nativeTextTrack = null;
45
+ var syntheticNativeTextTracks = [];
46
+
47
+ function createNativeTrack() {
48
+ removeNativeTrack();
49
+ if (cuesByTime === null || selectedTrackId === null) return false;
50
+ var selectedTrack = tracks.find(function(track) { return track.id === selectedTrackId; });
51
+ if (!selectedTrack) return false;
52
+ var delayMs = delay || 0;
53
+ nativeTextTrack = videoElement.addTextTrack('subtitles', selectedTrack.label || selectedTrack.lang, selectedTrack.lang || '');
54
+ syntheticNativeTextTracks.push(nativeTextTrack);
55
+ cuesByTime.times.forEach(function(time) {
56
+ cuesByTime[time].forEach(function(cue) {
57
+ if (cue.startTime !== time) return;
58
+ var start = (cue.startTime + delayMs) / 1000;
59
+ var end = (cue.endTime + delayMs) / 1000;
60
+ if (start < 0) start = 0;
61
+ if (end <= start) return;
62
+ nativeTextTrack.addCue(new VTTCue(start, end, cue.text));
63
+ });
64
+ });
65
+ nativeTextTrack.mode = 'showing';
66
+ return true;
67
+ }
68
+ function removeNativeTrack() {
69
+ if (nativeTextTrack !== null) {
70
+ nativeTextTrack.mode = 'disabled';
71
+ nativeTextTrack = null;
72
+ }
73
+ }
74
+ function isNativeTextTrack(track) {
75
+ return syntheticNativeTextTracks.includes(track);
76
+ }
77
+ function getEmbeddedTrackIndex(trackId) {
78
+ if (typeof trackId !== 'string' || !trackId.startsWith('EMBEDDED_')) {
79
+ return null;
80
+ }
81
+ var index = parseInt(trackId.replace('EMBEDDED_', ''), 10);
82
+ return isNaN(index) ? null : index;
83
+ }
84
+ function isWebkitDisplayingFullscreen() {
85
+ return videoElement && videoElement.webkitDisplayingFullscreen === true;
86
+ }
87
+ function onWebkitBeginFullscreen() {
88
+ createNativeTrack();
89
+ subtitlesElement.style.display = 'none';
90
+ }
91
+ function onWebkitEndFullscreen() {
92
+ removeNativeTrack();
93
+ subtitlesElement.style.display = '';
94
+ }
95
+ if (videoElement) {
96
+ videoElement.addEventListener('webkitbeginfullscreen', onWebkitBeginFullscreen);
97
+ videoElement.addEventListener('webkitendfullscreen', onWebkitEndFullscreen);
98
+ }
99
+
43
100
  var videoState = {
44
101
  time: null,
45
102
  paused: false,
@@ -189,6 +246,10 @@ function withHTMLSubtitles(Video) {
189
246
  }
190
247
 
191
248
  events.emit(eventName, propName, getProp(propName, propValue));
249
+
250
+ if (propName === 'selectedSubtitlesTrackId' && propValue !== null && selectedTrackId !== null && nativeTextTrack === null) {
251
+ setProp('selectedExtraSubtitlesTrackId', null);
252
+ }
192
253
  }
193
254
  function onOtherVideoEvent(eventName) {
194
255
  return function() {
@@ -272,6 +333,24 @@ function withHTMLSubtitles(Video) {
272
333
 
273
334
  return opacity;
274
335
  }
336
+ case 'subtitlesTracks': {
337
+ if (Array.isArray(videoPropValue) && videoElement && videoElement.textTracks) {
338
+ return videoPropValue.filter(function(track) {
339
+ var index = getEmbeddedTrackIndex(track.id);
340
+ return index === null || !isNativeTextTrack(videoElement.textTracks[index]);
341
+ });
342
+ }
343
+
344
+ return videoPropValue;
345
+ }
346
+ case 'selectedSubtitlesTrackId': {
347
+ if (typeof videoPropValue === 'string' && videoElement && videoElement.textTracks) {
348
+ var index = getEmbeddedTrackIndex(videoPropValue);
349
+ return index !== null && isNativeTextTrack(videoElement.textTracks[index]) ? null : videoPropValue;
350
+ }
351
+
352
+ return videoPropValue;
353
+ }
275
354
  default: {
276
355
  return videoPropValue;
277
356
  }
@@ -300,6 +379,13 @@ function withHTMLSubtitles(Video) {
300
379
  function setProp(propName, propValue) {
301
380
  switch (propName) {
302
381
  case 'selectedExtraSubtitlesTrackId': {
382
+ if (propValue !== null) {
383
+ video.dispatch({
384
+ type: 'setProp',
385
+ propName: 'selectedSubtitlesTrackId',
386
+ propValue: null,
387
+ });
388
+ }
303
389
  if (propValue !== null && selectedTrackId === propValue) {
304
390
  return true;
305
391
  }
@@ -358,6 +444,9 @@ function withHTMLSubtitles(Video) {
358
444
 
359
445
  cuesByTime = result;
360
446
  startRenderLoop();
447
+ if (isWebkitDisplayingFullscreen() && nativeTextTrack === null) {
448
+ createNativeTrack();
449
+ }
361
450
  events.emit('extraSubtitlesTrackLoaded', selectedTrack);
362
451
  })
363
452
  .catch(function(error) {
@@ -545,6 +634,7 @@ function withHTMLSubtitles(Video) {
545
634
  return false;
546
635
  }
547
636
  case 'unload': {
637
+ removeNativeTrack();
548
638
  stopRenderLoop();
549
639
  lastTimeIndex = null;
550
640
  cuesByTime = null;
@@ -560,6 +650,10 @@ function withHTMLSubtitles(Video) {
560
650
  case 'destroy': {
561
651
  command('unload');
562
652
  destroyed = true;
653
+ if (videoElement) {
654
+ videoElement.removeEventListener('webkitbeginfullscreen', onWebkitBeginFullscreen);
655
+ videoElement.removeEventListener('webkitendfullscreen', onWebkitEndFullscreen);
656
+ }
563
657
  onPropChanged('extraSubtitlesSize');
564
658
  onPropChanged('extraSubtitlesOffset');
565
659
  onPropChanged('extraSubtitlesTextColor');
@@ -46,13 +46,33 @@ function convertStream(streamingServerURL, stream, seriesInfo, streamingServerSe
46
46
  } else {
47
47
  var proxyStreamsEnabled = streamingServerSettings && streamingServerSettings.proxyStreamsEnabled;
48
48
  var proxyHeaders = stream.behaviorHints && stream.behaviorHints.proxyHeaders;
49
+ var resolved;
49
50
  if (proxyStreamsEnabled || proxyHeaders) {
50
51
  var requestHeaders = proxyHeaders && proxyHeaders.request ? proxyHeaders.request : {};
51
52
  var responseHeaders = proxyHeaders && proxyHeaders.response ? proxyHeaders.response : {};
52
- resolve({ url: buildProxyUrl(streamingServerURL, stream.url, requestHeaders, responseHeaders) });
53
+ resolved = { url: buildProxyUrl(streamingServerURL, stream.url, requestHeaders, responseHeaders) };
53
54
  } else {
54
- resolve({ url: stream.url });
55
+ resolved = { url: stream.url };
55
56
  }
57
+ // Propagate infoHash/fileIdx so fetchFilename can hit stats.json
58
+ // instead of leaking the URL fragment as the filename.
59
+ if (typeof stream.infoHash === 'string' && stream.infoHash.length > 0) {
60
+ resolved.infoHash = stream.infoHash.toLowerCase();
61
+ if (stream.fileIdx !== null && stream.fileIdx !== undefined && isFinite(stream.fileIdx)) {
62
+ resolved.fileIdx = stream.fileIdx;
63
+ }
64
+ } else {
65
+ // Fallback for addons shipping pre-computed streaming-server URLs.
66
+ try {
67
+ var parsed = new URL(stream.url);
68
+ var parts = parsed.pathname.split('/').filter(Boolean);
69
+ if (parts.length === 2 && /^[a-f0-9]{40}$/i.test(parts[0]) && /^-?\d+$/.test(parts[1])) {
70
+ resolved.infoHash = parts[0].toLowerCase();
71
+ resolved.fileIdx = parseInt(parts[1], 10);
72
+ }
73
+ } catch (_e) { /* unparsable URL */ }
74
+ }
75
+ resolve(resolved);
56
76
  }
57
77
 
58
78
  return;
@@ -46,7 +46,30 @@ function fetchFilename(streamingServerURL, mediaURL, infoHash, fileIdx, behavior
46
46
  }
47
47
 
48
48
  if (infoHash) {
49
- return fetch(url.resolve(streamingServerURL, '/' + encodeURIComponent(infoHash) + '/' + encodeURIComponent(fileIdx) + '/stats.json'))
49
+ // fileIdx -1 means auto-pick; /-1/stats.json has no streamName,
50
+ // so resolve via engine guessedFileIdx instead.
51
+ var fileIdxNum = typeof fileIdx === 'string' ? parseInt(fileIdx, 10) : fileIdx;
52
+ var hasSpecificFileIdx = fileIdxNum !== null && fileIdxNum !== -1 && isFinite(fileIdxNum);
53
+
54
+ if (hasSpecificFileIdx) {
55
+ return fetch(url.resolve(streamingServerURL, '/' + encodeURIComponent(infoHash) + '/' + encodeURIComponent(fileIdx) + '/stats.json'))
56
+ .then(function(resp) {
57
+ if (resp.ok) {
58
+ return resp.json();
59
+ }
60
+
61
+ throw new Error(resp.status + ' (' + resp.statusText + ')');
62
+ })
63
+ .then(function(resp) {
64
+ if (!resp || typeof resp.streamName !== 'string') {
65
+ throw new Error('Could not retrieve filename from torrent');
66
+ }
67
+
68
+ return resp.streamName;
69
+ });
70
+ }
71
+
72
+ return fetch(url.resolve(streamingServerURL, '/' + encodeURIComponent(infoHash) + '/stats.json'))
50
73
  .then(function(resp) {
51
74
  if (resp.ok) {
52
75
  return resp.json();
@@ -54,12 +77,31 @@ function fetchFilename(streamingServerURL, mediaURL, infoHash, fileIdx, behavior
54
77
 
55
78
  throw new Error(resp.status + ' (' + resp.statusText + ')');
56
79
  })
57
- .then(function(resp) {
58
- if (!resp || typeof resp.streamName !== 'string') {
59
- throw new Error('Could not retrieve filename from torrent');
80
+ .then(function(stats) {
81
+ if (!stats || !Array.isArray(stats.files)) {
82
+ throw new Error('Could not retrieve file list from torrent');
83
+ }
84
+
85
+ // Prefer guessedFileIdx — the file the engine is streaming.
86
+ var guessed = typeof stats.guessedFileIdx === 'number' ? stats.files[stats.guessedFileIdx] : null;
87
+ if (guessed && typeof guessed.name === 'string') {
88
+ return guessed.name;
89
+ }
90
+
91
+ // Fallback: largest video file (mirrors server's GuessFileIdx for movies).
92
+ var videoExt = /\.(mp4|mkv|avi|mov|wmv|flv|webm|m4v|mpg|mpeg|ts|m2ts)$/i;
93
+ var pool = stats.files.filter(function(f) { return f && typeof f.name === 'string' && videoExt.test(f.name); });
94
+ if (pool.length === 0) {
95
+ pool = stats.files.filter(function(f) { return f && typeof f.name === 'string'; });
96
+ }
97
+ var largest = pool.reduce(function(best, f) {
98
+ return (!best || (f.length || 0) > (best.length || 0)) ? f : best;
99
+ }, null);
100
+ if (largest && typeof largest.name === 'string') {
101
+ return largest.name;
60
102
  }
61
103
 
62
- return resp.streamName;
104
+ throw new Error('Could not retrieve filename from torrent');
63
105
  });
64
106
  }
65
107
 
@@ -365,6 +365,7 @@ function withStreamingServer(Video) {
365
365
  var isFormatSupported = options.formats.some(function(format) {
366
366
  return probe.format.name.indexOf(format) !== -1;
367
367
  });
368
+
368
369
  var areStreamsSupported = probe.streams.every(function(stream) {
369
370
  if (stream.track === 'audio') {
370
371
  return stream.channels <= options.maxAudioChannels &&
@@ -375,7 +376,16 @@ function withStreamingServer(Video) {
375
376
 
376
377
  return true;
377
378
  });
378
- return isFormatSupported && areStreamsSupported;
379
+ var hasEmbeddedSubtitles = probe.streams.some(function(stream) {
380
+ return stream.track === 'subtitle';
381
+ });
382
+
383
+ // HTML5 video doesn't support multiple audio tracks, so we can't switch languages
384
+ var supportedAudioTracks = probe.streams.filter(function(stream) {
385
+ return stream.track === 'audio' && options.audioCodecs.indexOf(stream.codec) !== -1;
386
+ });
387
+
388
+ return isFormatSupported && areStreamsSupported && !hasEmbeddedSubtitles && supportedAudioTracks.length < 2;
379
389
  })
380
390
  .catch(function() {
381
391
  // this uses content-type header in HTMLVideo which