@stremio/stremio-video 0.0.78 → 0.0.79-tizen02

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-tizen02",
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
  };
@@ -0,0 +1,98 @@
1
+ const SCOPE = 'AVPlay';
2
+
3
+ const createAVPlay = (transport) => {
4
+ const getState = () => {
5
+ return transport.request(SCOPE, 'getState');
6
+ };
7
+
8
+ const getCurrentTime = () => {
9
+ return transport.request(SCOPE, 'getCurrentTime');
10
+ };
11
+
12
+ const getDuration = () => {
13
+ return transport.request(SCOPE, 'getDuration');
14
+ };
15
+
16
+ const getTotalTrackInfo = () => {
17
+ return transport.request(SCOPE, 'getTotalTrackInfo');
18
+ };
19
+
20
+ const getCurrentStreamInfo = () => {
21
+ return transport.request(SCOPE, 'getCurrentStreamInfo');
22
+ };
23
+
24
+ const open = (path) => {
25
+ return transport.request(SCOPE, 'open', path);
26
+ };
27
+
28
+ const prepareAsync = async (successHandler, errorHandler) => {
29
+ const [handler, handlerResult] = await transport.request(SCOPE, 'prepareAsync', 'handler:success', 'handler:error');
30
+ if (handler === 'handler:success') successHandler();
31
+ if (handler === 'handler:error') errorHandler(...handlerResult);
32
+ };
33
+
34
+ const pause = () => {
35
+ return transport.request(SCOPE, 'pause');
36
+ };
37
+
38
+ const play = () => {
39
+ return transport.request(SCOPE, 'play');
40
+ };
41
+
42
+ const stop = () => {
43
+ return transport.request(SCOPE, 'stop');
44
+ };
45
+
46
+ const seekTo = (time) => {
47
+ return transport.request(SCOPE, 'seekTo', time);
48
+ };
49
+
50
+ const setSpeed = (rate) => {
51
+ return transport.request(SCOPE, 'setSpeed', rate);
52
+ };
53
+
54
+ const setSelectTrack = (type, id) => {
55
+ return transport.request(SCOPE, 'setSelectTrack', type, id);
56
+ };
57
+
58
+ const setDisplayRect = (x, y, width, height) => {
59
+ return transport.request(SCOPE, 'setDisplayRect', x, y, width, height);
60
+ };
61
+
62
+ const setDisplayMethod = (method) => {
63
+ return transport.request(SCOPE, 'setDisplayMethod', method);
64
+ };
65
+
66
+ const setListener = (listener) => {
67
+ const handlers = Object.keys(listener).map((name) => `handler:${name}`);
68
+ const onHandlerResponse = (handler, handlerResult) => {
69
+ const name = handler.replace('handler:', '');
70
+ if (listener[name]) {
71
+ handlerResult ? listener[name](...handlerResult) : listener[name]();
72
+ }
73
+ };
74
+
75
+ transport.listen(SCOPE, 'setListener', onHandlerResponse, ...handlers);
76
+ };
77
+
78
+ return {
79
+ getState,
80
+ getCurrentTime,
81
+ getDuration,
82
+ getTotalTrackInfo,
83
+ getCurrentStreamInfo,
84
+ open,
85
+ prepareAsync,
86
+ pause,
87
+ play,
88
+ stop,
89
+ seekTo,
90
+ setSpeed,
91
+ setSelectTrack,
92
+ setDisplayRect,
93
+ setDisplayMethod,
94
+ setListener,
95
+ };
96
+ };
97
+
98
+ module.exports = createAVPlay;
@@ -4,6 +4,7 @@ var deepFreeze = require('deep-freeze');
4
4
  var Color = require('color');
5
5
  var ERROR = require('../error');
6
6
  var getTracksData = require('../tracksData');
7
+ var createAVPlay = require('./AVPlay');
7
8
 
8
9
  var SSA_DESCRIPTORS_REGEX = /^\{(\\an[1-8])+\}/i;
9
10
 
@@ -20,7 +21,7 @@ function TizenVideo(options) {
20
21
  throw new Error('Container element required to be instance of HTMLElement');
21
22
  }
22
23
 
23
- var AVPlay = window.webapis.avplay;
24
+ var AVPlay = createAVPlay(options.transport);
24
25
 
25
26
  var promiseAudioTrackChange = false;
26
27
 
@@ -40,17 +41,17 @@ function TizenVideo(options) {
40
41
  var lastSub;
41
42
  var disabledSubs = true;
42
43
 
43
- function refreshSubtitle() {
44
+ async function refreshSubtitle() {
44
45
  if (lastSub) {
45
- var currentTime = getProp('time');
46
+ var currentTime = await getProp('time');
46
47
  var lastSubDurationDiff = lastSub.duration - (currentTime - lastSub.now);
47
48
  if (lastSubDurationDiff > 0) renderSubtitle(lastSubDurationDiff, lastSub.text);
48
49
  }
49
50
  }
50
51
 
51
- function renderSubtitle(duration, text) {
52
+ async function renderSubtitle(duration, text) {
52
53
  if (disabledSubs) return;
53
- var now = getProp('time');
54
+ var now = await getProp('time');
54
55
  var cleanedText = text.replace(SSA_DESCRIPTORS_REGEX, '');
55
56
 
56
57
  // we ignore custom delay here, it's not needed for embedded subs
@@ -177,7 +178,7 @@ function TizenVideo(options) {
177
178
  }
178
179
  }
179
180
 
180
- function getProp(propName) {
181
+ async function getProp(propName) {
181
182
  switch (propName) {
182
183
  case 'stream': {
183
184
  return stream;
@@ -190,7 +191,7 @@ function TizenVideo(options) {
190
191
  return null;
191
192
  }
192
193
 
193
- var state = AVPlay.getState();
194
+ var state = await AVPlay.getState();
194
195
  var isPaused = !!(state === 'PAUSED');
195
196
 
196
197
  if (!isPaused && promiseAudioTrackChange) {
@@ -201,7 +202,7 @@ function TizenVideo(options) {
201
202
  return isPaused;
202
203
  }
203
204
  case 'time': {
204
- var currentTime = AVPlay.getCurrentTime();
205
+ var currentTime = await AVPlay.getCurrentTime();
205
206
  if (stream === null || currentTime === null || !isFinite(currentTime)) {
206
207
  return null;
207
208
  }
@@ -209,7 +210,7 @@ function TizenVideo(options) {
209
210
  return Math.floor(currentTime);
210
211
  }
211
212
  case 'duration': {
212
- var duration = AVPlay.getDuration();
213
+ var duration = await AVPlay.getDuration();
213
214
  if (stream === null || duration === null || !isFinite(duration)) {
214
215
  return null;
215
216
  }
@@ -228,7 +229,7 @@ function TizenVideo(options) {
228
229
  return [];
229
230
  }
230
231
 
231
- var totalTrackInfo = AVPlay.getTotalTrackInfo();
232
+ var totalTrackInfo = await AVPlay.getTotalTrackInfo();
232
233
  var textTracks = [];
233
234
 
234
235
  for (var i = 0; i < totalTrackInfo.length; i++) {
@@ -268,7 +269,7 @@ function TizenVideo(options) {
268
269
  return null;
269
270
  }
270
271
 
271
- var currentTracks = AVPlay.getCurrentStreamInfo();
272
+ var currentTracks = await AVPlay.getCurrentStreamInfo();
272
273
  var currentIndex;
273
274
 
274
275
  for (var i = 0; i < currentTracks.length; i++) {
@@ -329,7 +330,7 @@ function TizenVideo(options) {
329
330
  return [];
330
331
  }
331
332
 
332
- var totalTrackInfo = AVPlay.getTotalTrackInfo();
333
+ var totalTrackInfo = await AVPlay.getTotalTrackInfo();
333
334
  var audioTracks = [];
334
335
 
335
336
  for (var i = 0; i < totalTrackInfo.length; i++) {
@@ -376,7 +377,7 @@ function TizenVideo(options) {
376
377
  return promiseAudioTrackChange;
377
378
  }
378
379
 
379
- var currentTracks = AVPlay.getCurrentStreamInfo();
380
+ var currentTracks = await AVPlay.getCurrentStreamInfo();
380
381
  var currentIndex = false;
381
382
 
382
383
  for (var i = 0; i < currentTracks.length; i++) {
@@ -410,20 +411,20 @@ function TizenVideo(options) {
410
411
  function onEnded() {
411
412
  events.emit('ended');
412
413
  }
413
- function onPropChanged(propName) {
414
+ async function onPropChanged(propName) {
414
415
  if (observedProps[propName]) {
415
- var propValue = getProp(propName);
416
+ var propValue = await getProp(propName);
416
417
  events.emit('propChanged', propName, propValue);
417
418
  }
418
419
  }
419
- function observeProp(propName) {
420
+ async function observeProp(propName) {
420
421
  if (observedProps.hasOwnProperty(propName)) {
421
- var propValue = getProp(propName);
422
+ var propValue = await getProp(propName);
422
423
  events.emit('propValue', propName, propValue);
423
424
  observedProps[propName] = true;
424
425
  }
425
426
  }
426
- function setProp(propName, propValue) {
427
+ async function setProp(propName, propValue) {
427
428
  switch (propName) {
428
429
  case 'paused': {
429
430
  if (stream !== null) {
@@ -442,10 +443,10 @@ function TizenVideo(options) {
442
443
 
443
444
  // the paused state is usually correct, but i have seen it not change on tizen 3
444
445
  // which causes all kinds of issues in the UI: (only happens with some videos)
445
- var lastKnownProp = getProp('paused');
446
+ var lastKnownProp = await getProp('paused');
446
447
 
447
- setTimeout(function() {
448
- if (getProp('paused') !== lastKnownProp) {
448
+ setTimeout(async function() {
449
+ if (await getProp('paused') !== lastKnownProp) {
449
450
  onPropChanged('paused');
450
451
  }
451
452
  }, 1000);
@@ -470,7 +471,7 @@ function TizenVideo(options) {
470
471
  return;
471
472
  }
472
473
 
473
- var subtitlesTracks = getProp('subtitlesTracks');
474
+ var subtitlesTracks = await getProp('subtitlesTracks');
474
475
  var selectedSubtitlesTrack = subtitlesTracks
475
476
  .find(function(track) {
476
477
  return track.id === propValue;
@@ -574,13 +575,13 @@ function TizenVideo(options) {
574
575
  if (stream !== null) {
575
576
  currentAudioTrack = propValue;
576
577
 
577
- var audioTracks = getProp('audioTracks');
578
+ var audioTracks = await getProp('audioTracks');
578
579
  var selectedAudioTrack = audioTracks
579
580
  .find(function(track) {
580
581
  return track.id === propValue;
581
582
  });
582
583
 
583
- if (getProp('paused')) {
584
+ if (await getProp('paused')) {
584
585
  // issues before this logic:
585
586
  // tizen 3 does not allow changing audio track when paused
586
587
  // tizen 5 does, but it will only change getProp('selectedAudioTrackId') after playback starts
@@ -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