@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 +1 -1
- package/src/HTMLVideo/HTMLVideo.js +38 -2
- package/src/TizenVideo/AVPlay.js +98 -0
- package/src/TizenVideo/TizenVideo.js +25 -24
- package/src/withHTMLSubtitles/withHTMLSubtitles.js +84 -1
- package/src/withStreamingServer/convertStream.js +22 -2
- package/src/withStreamingServer/fetchVideoParams.js +47 -5
- package/src/withStreamingServer/withStreamingServer.js +8 -1
package/package.json
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
53
|
+
resolved = { url: buildProxyUrl(streamingServerURL, stream.url, requestHeaders, responseHeaders) };
|
|
53
54
|
} else {
|
|
54
|
-
|
|
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
|
-
|
|
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(
|
|
58
|
-
if (!
|
|
59
|
-
throw new Error('Could not retrieve
|
|
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
|
-
|
|
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
|
-
|
|
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
|