@stremio/stremio-video 0.0.78 → 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.78",
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) {
@@ -323,6 +331,12 @@ function HTMLVideo(options) {
323
331
  case 'videoScale': {
324
332
  return videoElement.style.objectFit || 'contain';
325
333
  }
334
+ case 'fullscreen': {
335
+ if (stream === null) {
336
+ return null;
337
+ }
338
+ return videoElement.webkitDisplayingFullscreen === true || document.fullscreenElement === videoElement;
339
+ }
326
340
  default: {
327
341
  return null;
328
342
  }
@@ -550,6 +564,23 @@ function HTMLVideo(options) {
550
564
 
551
565
  break;
552
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
+ }
553
584
  }
554
585
  }
555
586
  function command(commandName, commandArgs) {
@@ -571,6 +602,7 @@ function HTMLVideo(options) {
571
602
  onPropChanged('selectedSubtitlesTrackId');
572
603
  onPropChanged('audioTracks');
573
604
  onPropChanged('selectedAudioTrackId');
605
+ onPropChanged('fullscreen');
574
606
  getContentType(stream)
575
607
  .then(function(contentType) {
576
608
  if (stream !== commandArgs.stream) {
@@ -636,6 +668,7 @@ function HTMLVideo(options) {
636
668
  onPropChanged('selectedSubtitlesTrackId');
637
669
  onPropChanged('audioTracks');
638
670
  onPropChanged('selectedAudioTrackId');
671
+ onPropChanged('fullscreen');
639
672
  break;
640
673
  }
641
674
  case 'destroy': {
@@ -668,6 +701,9 @@ function HTMLVideo(options) {
668
701
  videoElement.onvolumechange = null;
669
702
  videoElement.onratechange = null;
670
703
  videoElement.textTracks.onchange = null;
704
+ videoElement.removeEventListener('webkitbeginfullscreen', onFullscreenChanged);
705
+ videoElement.removeEventListener('webkitendfullscreen', onFullscreenChanged);
706
+ videoElement.removeEventListener('fullscreenchange', onFullscreenChanged);
671
707
  containerElement.removeChild(videoElement);
672
708
  containerElement.removeChild(styleElement);
673
709
  break;
@@ -727,7 +763,7 @@ HTMLVideo.canPlayStream = function(stream) {
727
763
  HTMLVideo.manifest = {
728
764
  name: 'HTMLVideo',
729
765
  external: false,
730
- 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'],
731
767
  commands: ['load', 'unload', 'destroy'],
732
768
  events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded']
733
769
  };
@@ -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,
@@ -190,7 +247,7 @@ function withHTMLSubtitles(Video) {
190
247
 
191
248
  events.emit(eventName, propName, getProp(propName, propValue));
192
249
 
193
- if (propName === 'selectedSubtitlesTrackId' && propValue !== null && selectedTrackId !== null) {
250
+ if (propName === 'selectedSubtitlesTrackId' && propValue !== null && selectedTrackId !== null && nativeTextTrack === null) {
194
251
  setProp('selectedExtraSubtitlesTrackId', null);
195
252
  }
196
253
  }
@@ -276,6 +333,24 @@ function withHTMLSubtitles(Video) {
276
333
 
277
334
  return opacity;
278
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
+ }
279
354
  default: {
280
355
  return videoPropValue;
281
356
  }
@@ -369,6 +444,9 @@ function withHTMLSubtitles(Video) {
369
444
 
370
445
  cuesByTime = result;
371
446
  startRenderLoop();
447
+ if (isWebkitDisplayingFullscreen() && nativeTextTrack === null) {
448
+ createNativeTrack();
449
+ }
372
450
  events.emit('extraSubtitlesTrackLoaded', selectedTrack);
373
451
  })
374
452
  .catch(function(error) {
@@ -556,6 +634,7 @@ function withHTMLSubtitles(Video) {
556
634
  return false;
557
635
  }
558
636
  case 'unload': {
637
+ removeNativeTrack();
559
638
  stopRenderLoop();
560
639
  lastTimeIndex = null;
561
640
  cuesByTime = null;
@@ -571,6 +650,10 @@ function withHTMLSubtitles(Video) {
571
650
  case 'destroy': {
572
651
  command('unload');
573
652
  destroyed = true;
653
+ if (videoElement) {
654
+ videoElement.removeEventListener('webkitbeginfullscreen', onWebkitBeginFullscreen);
655
+ videoElement.removeEventListener('webkitendfullscreen', onWebkitEndFullscreen);
656
+ }
574
657
  onPropChanged('extraSubtitlesSize');
575
658
  onPropChanged('extraSubtitlesOffset');
576
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 &&
@@ -378,7 +379,13 @@ function withStreamingServer(Video) {
378
379
  var hasEmbeddedSubtitles = probe.streams.some(function(stream) {
379
380
  return stream.track === 'subtitle';
380
381
  });
381
- return isFormatSupported && areStreamsSupported && !hasEmbeddedSubtitles;
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;
382
389
  })
383
390
  .catch(function() {
384
391
  // this uses content-type header in HTMLVideo which