@stremio/stremio-video 0.0.20-rc.7 → 0.0.22
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 +4 -3
- package/src/ChromecastSenderVideo/ChromecastSenderVideo.js +3 -1
- package/src/HTMLVideo/HTMLVideo.js +25 -1
- package/src/IFrameVideo/IFrameVideo.js +3 -10
- package/src/ShellVideo/ShellVideo.js +436 -0
- package/src/ShellVideo/index.js +3 -0
- package/src/StremioVideo/selectVideoImplementation.js +5 -0
- package/src/TizenVideo/TizenVideo.js +36 -4
- package/src/YouTubeVideo/YouTubeVideo.js +11 -1
- package/src/error.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stremio/stremio-video",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "Abstraction layer on top of different media players",
|
|
5
5
|
"author": "Smart Code OOD",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"lint": "eslint src"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"buffer": "6.0.3",
|
|
17
|
+
"color": "4.2.3",
|
|
16
18
|
"deep-freeze": "0.0.1",
|
|
17
19
|
"eventemitter3": "4.0.7",
|
|
18
20
|
"hat": "0.0.3",
|
|
@@ -21,8 +23,7 @@
|
|
|
21
23
|
"magnet-uri": "6.2.0",
|
|
22
24
|
"url": "0.11.0",
|
|
23
25
|
"video-name-parser": "1.4.6",
|
|
24
|
-
"vtt.js": "github:jaruba/vtt.js#e4f5f5603730866bacb174a93f51b734c9f29e6a"
|
|
25
|
-
"color": "4.2.3"
|
|
26
|
+
"vtt.js": "github:jaruba/vtt.js#e4f5f5603730866bacb174a93f51b734c9f29e6a"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"eslint": "7.32.0"
|
|
@@ -45,6 +45,7 @@ function ChromecastSenderVideo(options) {
|
|
|
45
45
|
var destroyed = false;
|
|
46
46
|
var observedProps = {
|
|
47
47
|
stream: false,
|
|
48
|
+
loaded: false,
|
|
48
49
|
paused: false,
|
|
49
50
|
time: false,
|
|
50
51
|
duration: false,
|
|
@@ -107,6 +108,7 @@ function ChromecastSenderVideo(options) {
|
|
|
107
108
|
case 'destroy': {
|
|
108
109
|
destroyed = true;
|
|
109
110
|
onPropChanged('stream', null);
|
|
111
|
+
onPropChanged('loaded', null);
|
|
110
112
|
onPropChanged('paused', null);
|
|
111
113
|
onPropChanged('time', null);
|
|
112
114
|
onPropChanged('duration', null);
|
|
@@ -188,7 +190,7 @@ ChromecastSenderVideo.canPlayStream = function() {
|
|
|
188
190
|
ChromecastSenderVideo.manifest = {
|
|
189
191
|
name: 'ChromecastSenderVideo',
|
|
190
192
|
external: true,
|
|
191
|
-
props: ['stream', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed', 'extraSubtitlesTracks', 'selectedExtraSubtitlesTrackId', 'extraSubtitlesDelay', 'extraSubtitlesSize', 'extraSubtitlesOffset', 'extraSubtitlesTextColor', 'extraSubtitlesBackgroundColor', 'extraSubtitlesOutlineColor'],
|
|
193
|
+
props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed', 'extraSubtitlesTracks', 'selectedExtraSubtitlesTrackId', 'extraSubtitlesDelay', 'extraSubtitlesSize', 'extraSubtitlesOffset', 'extraSubtitlesTextColor', 'extraSubtitlesBackgroundColor', 'extraSubtitlesOutlineColor'],
|
|
192
194
|
commands: ['load', 'unload', 'destroy', 'addExtraSubtitlesTracks'],
|
|
193
195
|
events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded', 'extraSubtitlesTrackLoaded', 'implementationChanged']
|
|
194
196
|
};
|
|
@@ -47,10 +47,12 @@ function HTMLVideo(options) {
|
|
|
47
47
|
onPropChanged('buffered');
|
|
48
48
|
};
|
|
49
49
|
videoElement.onseeking = function() {
|
|
50
|
+
onPropChanged('time');
|
|
50
51
|
onPropChanged('buffering');
|
|
51
52
|
onPropChanged('buffered');
|
|
52
53
|
};
|
|
53
54
|
videoElement.onseeked = function() {
|
|
55
|
+
onPropChanged('time');
|
|
54
56
|
onPropChanged('buffering');
|
|
55
57
|
onPropChanged('buffered');
|
|
56
58
|
};
|
|
@@ -59,6 +61,7 @@ function HTMLVideo(options) {
|
|
|
59
61
|
onPropChanged('buffered');
|
|
60
62
|
};
|
|
61
63
|
videoElement.onplaying = function() {
|
|
64
|
+
onPropChanged('time');
|
|
62
65
|
onPropChanged('buffering');
|
|
63
66
|
onPropChanged('buffered');
|
|
64
67
|
};
|
|
@@ -70,6 +73,9 @@ function HTMLVideo(options) {
|
|
|
70
73
|
onPropChanged('buffering');
|
|
71
74
|
onPropChanged('buffered');
|
|
72
75
|
};
|
|
76
|
+
videoElement.onloadedmetadata = function() {
|
|
77
|
+
onPropChanged('loaded');
|
|
78
|
+
};
|
|
73
79
|
videoElement.onloadeddata = function() {
|
|
74
80
|
onPropChanged('buffering');
|
|
75
81
|
onPropChanged('buffered');
|
|
@@ -98,6 +104,7 @@ function HTMLVideo(options) {
|
|
|
98
104
|
var subtitlesOffset = 0;
|
|
99
105
|
var observedProps = {
|
|
100
106
|
stream: false,
|
|
107
|
+
loaded: false,
|
|
101
108
|
paused: false,
|
|
102
109
|
time: false,
|
|
103
110
|
duration: false,
|
|
@@ -122,6 +129,13 @@ function HTMLVideo(options) {
|
|
|
122
129
|
case 'stream': {
|
|
123
130
|
return stream;
|
|
124
131
|
}
|
|
132
|
+
case 'loaded': {
|
|
133
|
+
if (stream === null) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return videoElement.readyState >= videoElement.HAVE_METADATA;
|
|
138
|
+
}
|
|
125
139
|
case 'paused': {
|
|
126
140
|
if (stream === null) {
|
|
127
141
|
return null;
|
|
@@ -355,6 +369,7 @@ function HTMLVideo(options) {
|
|
|
355
369
|
case 'paused': {
|
|
356
370
|
if (stream !== null) {
|
|
357
371
|
propValue ? videoElement.pause() : videoElement.play();
|
|
372
|
+
onPropChanged('paused');
|
|
358
373
|
}
|
|
359
374
|
|
|
360
375
|
break;
|
|
@@ -362,6 +377,7 @@ function HTMLVideo(options) {
|
|
|
362
377
|
case 'time': {
|
|
363
378
|
if (stream !== null && propValue !== null && isFinite(propValue)) {
|
|
364
379
|
videoElement.currentTime = parseInt(propValue, 10) / 1000;
|
|
380
|
+
onPropChanged('time');
|
|
365
381
|
}
|
|
366
382
|
|
|
367
383
|
break;
|
|
@@ -377,6 +393,7 @@ function HTMLVideo(options) {
|
|
|
377
393
|
return track.id === propValue;
|
|
378
394
|
});
|
|
379
395
|
if (selecterdSubtitlesTrack) {
|
|
396
|
+
onPropChanged('selectedSubtitlesTrackId');
|
|
380
397
|
events.emit('subtitlesTrackLoaded', selecterdSubtitlesTrack);
|
|
381
398
|
}
|
|
382
399
|
}
|
|
@@ -450,6 +467,7 @@ function HTMLVideo(options) {
|
|
|
450
467
|
});
|
|
451
468
|
hls.audioTrack = selecterdAudioTrack ? parseInt(selecterdAudioTrack.id.split('_').pop(), 10) : -1;
|
|
452
469
|
if (selecterdAudioTrack) {
|
|
470
|
+
onPropChanged('selectedAudioTrackId');
|
|
453
471
|
events.emit('audioTrackLoaded', selecterdAudioTrack);
|
|
454
472
|
}
|
|
455
473
|
}
|
|
@@ -460,17 +478,21 @@ function HTMLVideo(options) {
|
|
|
460
478
|
if (propValue !== null && isFinite(propValue)) {
|
|
461
479
|
videoElement.muted = false;
|
|
462
480
|
videoElement.volume = Math.max(0, Math.min(100, parseInt(propValue, 10))) / 100;
|
|
481
|
+
onPropChanged('muted');
|
|
482
|
+
onPropChanged('volume');
|
|
463
483
|
}
|
|
464
484
|
|
|
465
485
|
break;
|
|
466
486
|
}
|
|
467
487
|
case 'muted': {
|
|
468
488
|
videoElement.muted = !!propValue;
|
|
489
|
+
onPropChanged('muted');
|
|
469
490
|
break;
|
|
470
491
|
}
|
|
471
492
|
case 'playbackSpeed': {
|
|
472
493
|
if (propValue !== null && isFinite(propValue)) {
|
|
473
494
|
videoElement.playbackRate = parseFloat(propValue);
|
|
495
|
+
onPropChanged('playbackSpeed');
|
|
474
496
|
}
|
|
475
497
|
|
|
476
498
|
break;
|
|
@@ -484,6 +506,7 @@ function HTMLVideo(options) {
|
|
|
484
506
|
if (commandArgs && commandArgs.stream && typeof commandArgs.stream.url === 'string') {
|
|
485
507
|
stream = commandArgs.stream;
|
|
486
508
|
onPropChanged('stream');
|
|
509
|
+
onPropChanged('loaded');
|
|
487
510
|
videoElement.autoplay = typeof commandArgs.autoplay === 'boolean' ? commandArgs.autoplay : true;
|
|
488
511
|
videoElement.currentTime = commandArgs.time !== null && isFinite(commandArgs.time) ? parseInt(commandArgs.time, 10) / 1000 : 0;
|
|
489
512
|
onPropChanged('paused');
|
|
@@ -547,6 +570,7 @@ function HTMLVideo(options) {
|
|
|
547
570
|
videoElement.load();
|
|
548
571
|
videoElement.currentTime = 0;
|
|
549
572
|
onPropChanged('stream');
|
|
573
|
+
onPropChanged('loaded');
|
|
550
574
|
onPropChanged('paused');
|
|
551
575
|
onPropChanged('time');
|
|
552
576
|
onPropChanged('duration');
|
|
@@ -646,7 +670,7 @@ HTMLVideo.canPlayStream = function(stream) {
|
|
|
646
670
|
HTMLVideo.manifest = {
|
|
647
671
|
name: 'HTMLVideo',
|
|
648
672
|
external: false,
|
|
649
|
-
props: ['stream', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed'],
|
|
673
|
+
props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed'],
|
|
650
674
|
commands: ['load', 'unload', 'destroy'],
|
|
651
675
|
events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded']
|
|
652
676
|
};
|
|
@@ -24,6 +24,7 @@ function IFrameVideo(options) {
|
|
|
24
24
|
var destroyed = false;
|
|
25
25
|
var observedProps = {
|
|
26
26
|
stream: false,
|
|
27
|
+
loaded: false,
|
|
27
28
|
paused: false,
|
|
28
29
|
time: false,
|
|
29
30
|
duration: false,
|
|
@@ -95,6 +96,7 @@ function IFrameVideo(options) {
|
|
|
95
96
|
iframeElement.onload = null;
|
|
96
97
|
iframeElement.removeAttribute('src');
|
|
97
98
|
onPropChanged('stream', null);
|
|
99
|
+
onPropChanged('loaded', null);
|
|
98
100
|
onPropChanged('paused', null);
|
|
99
101
|
onPropChanged('time', null);
|
|
100
102
|
onPropChanged('duration', null);
|
|
@@ -108,15 +110,6 @@ function IFrameVideo(options) {
|
|
|
108
110
|
case 'destroy': {
|
|
109
111
|
command('unload');
|
|
110
112
|
destroyed = true;
|
|
111
|
-
onPropChanged('stream', null);
|
|
112
|
-
onPropChanged('paused', null);
|
|
113
|
-
onPropChanged('time', null);
|
|
114
|
-
onPropChanged('duration', null);
|
|
115
|
-
onPropChanged('buffering', null);
|
|
116
|
-
onPropChanged('buffered', null);
|
|
117
|
-
onPropChanged('volume', null);
|
|
118
|
-
onPropChanged('muted', null);
|
|
119
|
-
onPropChanged('playbackSpeed', null);
|
|
120
113
|
events.removeAllListeners();
|
|
121
114
|
containerElement.removeChild(iframeElement);
|
|
122
115
|
return true;
|
|
@@ -169,7 +162,7 @@ IFrameVideo.canPlayStream = function(stream) {
|
|
|
169
162
|
IFrameVideo.manifest = {
|
|
170
163
|
name: 'IFrameVideo',
|
|
171
164
|
external: true,
|
|
172
|
-
props: ['stream', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed', 'extraSubtitlesTracks', 'selectedExtraSubtitlesTrackId', 'extraSubtitlesDelay', 'extraSubtitlesSize', 'extraSubtitlesOffset', 'extraSubtitlesTextColor', 'extraSubtitlesBackgroundColor', 'extraSubtitlesOutlineColor'],
|
|
165
|
+
props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor', 'volume', 'muted', 'playbackSpeed', 'extraSubtitlesTracks', 'selectedExtraSubtitlesTrackId', 'extraSubtitlesDelay', 'extraSubtitlesSize', 'extraSubtitlesOffset', 'extraSubtitlesTextColor', 'extraSubtitlesBackgroundColor', 'extraSubtitlesOutlineColor'],
|
|
173
166
|
commands: ['load', 'unload', 'destroy', 'addExtraSubtitlesTracks'],
|
|
174
167
|
events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded', 'extraSubtitlesTrackLoaded', 'implementationChanged']
|
|
175
168
|
};
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
var EventEmitter = require('eventemitter3');
|
|
2
|
+
var cloneDeep = require('lodash.clonedeep');
|
|
3
|
+
var deepFreeze = require('deep-freeze');
|
|
4
|
+
var ERROR = require('../error');
|
|
5
|
+
|
|
6
|
+
var SUBS_SCALE_FACTOR = 0.0066;
|
|
7
|
+
|
|
8
|
+
var stremioToMPVProps = {
|
|
9
|
+
'stream': null,
|
|
10
|
+
'paused': 'pause',
|
|
11
|
+
'time': 'time-pos',
|
|
12
|
+
'duration': 'duration',
|
|
13
|
+
'buffering': 'buffering',
|
|
14
|
+
'volume': 'volume',
|
|
15
|
+
'muted': 'mute',
|
|
16
|
+
'playbackSpeed': 'speed',
|
|
17
|
+
'audioTracks': 'audioTracks',
|
|
18
|
+
'selectedAudioTrackId': 'aid',
|
|
19
|
+
'subtitlesTracks': 'subtitlesTracks',
|
|
20
|
+
'selectedSubtitlesTrackId': 'sid',
|
|
21
|
+
'subtitlesSize': 'sub-scale',
|
|
22
|
+
'subtitlesTextColor': 'sub-color',
|
|
23
|
+
'subtitlesBackgroundColor': 'sub-back-color',
|
|
24
|
+
'subtitlesOutlineColor': 'sub-border-color',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function ShellVideo(options) {
|
|
28
|
+
options = options || {};
|
|
29
|
+
|
|
30
|
+
var ipc = options.shellTransport;
|
|
31
|
+
|
|
32
|
+
var stremioProps = {};
|
|
33
|
+
Object.keys(stremioToMPVProps).forEach(function(key) {
|
|
34
|
+
if(stremioToMPVProps[key]) {
|
|
35
|
+
stremioProps[stremioToMPVProps[key]] = key;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
ipc.send('mpv-command', ['stop']);
|
|
40
|
+
ipc.send('mpv-observe-prop', 'path');
|
|
41
|
+
|
|
42
|
+
ipc.send('mpv-observe-prop', 'time-pos');
|
|
43
|
+
ipc.send('mpv-observe-prop', 'volume');
|
|
44
|
+
ipc.send('mpv-observe-prop', 'pause');
|
|
45
|
+
ipc.send('mpv-observe-prop', 'seeking');
|
|
46
|
+
ipc.send('mpv-observe-prop', 'eof-reached');
|
|
47
|
+
|
|
48
|
+
ipc.send('mpv-observe-prop', 'duration');
|
|
49
|
+
ipc.send('mpv-observe-prop', 'metadata');
|
|
50
|
+
ipc.send('mpv-observe-prop', 'video-params'); // video width/height
|
|
51
|
+
ipc.send('mpv-observe-prop', 'track-list');
|
|
52
|
+
|
|
53
|
+
ipc.send('mpv-observe-prop', 'paused-for-cache');
|
|
54
|
+
ipc.send('mpv-observe-prop', 'cache-buffering-state');
|
|
55
|
+
|
|
56
|
+
ipc.send('mpv-observe-prop', 'aid');
|
|
57
|
+
ipc.send('mpv-observe-prop', 'vid');
|
|
58
|
+
ipc.send('mpv-observe-prop', 'sid');
|
|
59
|
+
ipc.send('mpv-observe-prop', 'sub-scale');
|
|
60
|
+
ipc.send('mpv-observe-prop', 'sub-pos');
|
|
61
|
+
ipc.send('mpv-observe-prop', 'speed');
|
|
62
|
+
|
|
63
|
+
ipc.send('mpv-observe-prop', 'mpv-version');
|
|
64
|
+
ipc.send('mpv-observe-prop', 'ffmpeg-version');
|
|
65
|
+
|
|
66
|
+
var events = new EventEmitter();
|
|
67
|
+
var destroyed = false;
|
|
68
|
+
var stream = null;
|
|
69
|
+
// var selectedSubtitlesTrackId = null;
|
|
70
|
+
var observedProps = {};
|
|
71
|
+
var continueFrom = 0;
|
|
72
|
+
|
|
73
|
+
var avgDuration = 0;
|
|
74
|
+
var minClipDuration = 30;
|
|
75
|
+
var props = { };
|
|
76
|
+
|
|
77
|
+
function setBackground(visible) {
|
|
78
|
+
// This is a bit of a hack but there is no better way so far
|
|
79
|
+
var bg = visible ? '' : 'transparent';
|
|
80
|
+
for(var container = options.containerElement; container; container = container.parentElement) {
|
|
81
|
+
container.style.background = bg;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function logProp(args) {
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.log(args.name+': '+args.data);
|
|
87
|
+
}
|
|
88
|
+
function embeddedProp(args) {
|
|
89
|
+
return args.data ? 'EMBEDDED_' + args.data.toString() : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var last_time = 0;
|
|
93
|
+
ipc.on('mpv-prop-change', function(args) {
|
|
94
|
+
switch (args.name) {
|
|
95
|
+
case 'mpv-version':
|
|
96
|
+
case 'ffmpeg-version': {
|
|
97
|
+
props[args.name] = logProp(args);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case 'duration': {
|
|
101
|
+
var intDuration = args.data | 0;
|
|
102
|
+
// Accumulate average duration over time. if it is greater than minClipDuration
|
|
103
|
+
// and equal to the currently reported duration, it is returned as video length.
|
|
104
|
+
// If the reported duration changes over time the average duration is always
|
|
105
|
+
// smaller than the currently reported one so we set the video length to 0 as
|
|
106
|
+
// this is a live stream.
|
|
107
|
+
props[args.name] = args.data >= minClipDuration && (!avgDuration || intDuration === avgDuration) ? Math.round(args.data * 1000) : null;
|
|
108
|
+
// The average duration is calculated using right bit shifting by one of the sum of
|
|
109
|
+
// the previous average and the currently reported value. This method is not very precise
|
|
110
|
+
// as we get integer value but we avoid floating point errors. JS uses 32 bit values
|
|
111
|
+
// for bitwise maths so the maximum supported video duration is 1073741823 (2 ^ 30 - 1)
|
|
112
|
+
// which is around 34 years of playback time.
|
|
113
|
+
avgDuration = avgDuration ? (avgDuration + intDuration) >> 1 : intDuration;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'time-pos': {
|
|
117
|
+
props[args.name] = Math.round(args.data*1000);
|
|
118
|
+
if(continueFrom) {
|
|
119
|
+
ipc.send('mpv-set-prop', ['time-pos', continueFrom]);
|
|
120
|
+
props[args.name] = Math.round(continueFrom);
|
|
121
|
+
continueFrom = 0;
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'sub-scale': {
|
|
126
|
+
props[args.name] = Math.round(args.data / SUBS_SCALE_FACTOR);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'paused-for-cache':
|
|
130
|
+
case 'seeking':
|
|
131
|
+
{
|
|
132
|
+
if(props.buffering !== args.data) {
|
|
133
|
+
props.buffering = args.data;
|
|
134
|
+
onPropChanged('buffering');
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case 'aid':
|
|
139
|
+
case 'sid':
|
|
140
|
+
case 'vid': {
|
|
141
|
+
props[args.name] = embeddedProp(args);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
// In that case onPropChanged() is manually invoked as track-list contains all
|
|
145
|
+
// the tracks but we have different event for each track type
|
|
146
|
+
case 'track-list': {
|
|
147
|
+
props.audioTracks = args.data.filter(function(x) { return x.type === 'audio'; })
|
|
148
|
+
.map(function(x, index) {
|
|
149
|
+
return {
|
|
150
|
+
id: 'EMBEDDED_' + x.id,
|
|
151
|
+
lang: x.lang === undefined ? 'Track' + (index + 1) : x.lang,
|
|
152
|
+
label: x.title === undefined || x.lang === undefined ? '' : x.title || x.lang,
|
|
153
|
+
origin: 'EMBEDDED',
|
|
154
|
+
embedded: true,
|
|
155
|
+
mode: x.id === props.aid ? 'showing' : 'disabled',
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
onPropChanged('audioTracks');
|
|
159
|
+
|
|
160
|
+
props.subtitlesTracks = args.data
|
|
161
|
+
.filter(function(x) { return x.type === 'sub'; })
|
|
162
|
+
.map(function(x, index) {
|
|
163
|
+
return {
|
|
164
|
+
id: 'EMBEDDED_' + x.id,
|
|
165
|
+
lang: x.lang === undefined ? 'Track ' + (index + 1) : x.lang,
|
|
166
|
+
label: x.title === undefined || x.lang === undefined ? '' : x.title || x.lang,
|
|
167
|
+
origin: 'EMBEDDED',
|
|
168
|
+
embedded: true,
|
|
169
|
+
mode: x.id === props.sid ? 'showing' : 'disabled',
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
onPropChanged('subtitlesTracks');
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
default: {
|
|
176
|
+
props[args.name] = args.data;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Cap time update to update only when a second passes
|
|
182
|
+
var current_time = args.name === 'time-pos' ? Math.floor(props['time-pos'] / 1000) : null;
|
|
183
|
+
if((!current_time || last_time !== current_time)&& stremioProps[args.name]) {
|
|
184
|
+
if(current_time) {
|
|
185
|
+
last_time = current_time;
|
|
186
|
+
}
|
|
187
|
+
onPropChanged(stremioProps[args.name]);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
ipc.on('mpv-event-ended', function(args) {
|
|
191
|
+
if (args.error) onError(args.error);
|
|
192
|
+
else onEnded();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
function getProp(propName) {
|
|
196
|
+
if(stremioToMPVProps[propName]) return props[stremioToMPVProps[propName]];
|
|
197
|
+
// eslint-disable-next-line no-console
|
|
198
|
+
console.log('Unsupported prop requested', propName);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function onError(error) {
|
|
202
|
+
events.emit('error', error);
|
|
203
|
+
if (error.critical) {
|
|
204
|
+
command('unload');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function onEnded() {
|
|
208
|
+
events.emit('ended');
|
|
209
|
+
}
|
|
210
|
+
function onPropChanged(propName) {
|
|
211
|
+
if (observedProps[propName]) {
|
|
212
|
+
events.emit('propChanged', propName, getProp(propName));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function observeProp(propName) {
|
|
216
|
+
events.emit('propValue', propName, getProp(propName));
|
|
217
|
+
observedProps[propName] = true;
|
|
218
|
+
}
|
|
219
|
+
function setProp(propName, propValue) {
|
|
220
|
+
switch (propName) {
|
|
221
|
+
case 'paused': {
|
|
222
|
+
if (stream !== null) {
|
|
223
|
+
ipc.send('mpv-set-prop', ['pause', propValue]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case 'time': {
|
|
229
|
+
if (stream !== null && propValue !== null && isFinite(propValue)) {
|
|
230
|
+
ipc.send('mpv-set-prop', ['time-pos', propValue/1000]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case 'playbackSpeed': {
|
|
236
|
+
if (stream !== null && propValue !== null && isFinite(propValue)) {
|
|
237
|
+
ipc.send('mpv-set-prop', ['speed', propValue]);
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case 'volume': {
|
|
242
|
+
if (stream !== null && propValue !== null && isFinite(propValue)) {
|
|
243
|
+
props.mute = false;
|
|
244
|
+
ipc.send('mpv-set-prop', ['mute', 'no']);
|
|
245
|
+
ipc.send('mpv-set-prop', ['volume', propValue]);
|
|
246
|
+
onPropChanged('muted');
|
|
247
|
+
onPropChanged('volume');
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case 'muted': {
|
|
252
|
+
if (stream !== null) {
|
|
253
|
+
ipc.send('mpv-set-prop', ['mute', propValue ? 'yes' : 'no']);
|
|
254
|
+
props.mute = propValue;
|
|
255
|
+
onPropChanged('muted');
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case 'selectedAudioTrackId': {
|
|
260
|
+
if (stream !== null) {
|
|
261
|
+
var actualId = propValue.slice('EMBEDDED_'.length);
|
|
262
|
+
ipc.send('mpv-set-prop', ['aid', actualId]);
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case 'selectedSubtitlesTrackId': {
|
|
267
|
+
if (stream !== null) {
|
|
268
|
+
if(propValue) {
|
|
269
|
+
var actualId = propValue.slice('EMBEDDED_'.length);
|
|
270
|
+
ipc.send('mpv-set-prop', ['sid', actualId]);
|
|
271
|
+
events.emit('subtitlesTrackLoaded', propValue);
|
|
272
|
+
} else {
|
|
273
|
+
// turn off subs
|
|
274
|
+
ipc.send('mpv-set-prop', ['sid', 'no']);
|
|
275
|
+
props.sid = null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
onPropChanged('selectedSubtitlesTrackId');
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case 'subtitlesSize': {
|
|
282
|
+
ipc.send('mpv-set-prop', [stremioToMPVProps[propName], propValue * SUBS_SCALE_FACTOR]);
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case 'subtitlesOffset': {
|
|
286
|
+
ipc.send('mpv-set-prop', [stremioToMPVProps[propName], propValue]);
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
case 'subtitlesTextColor':
|
|
290
|
+
case 'subtitlesBackgroundColor':
|
|
291
|
+
case 'subtitlesOutlineColor':
|
|
292
|
+
{
|
|
293
|
+
// MPV accepts color in #AARRGGBB
|
|
294
|
+
var argb = propValue.replace(/^#(\w{6})(\w{2})$/, '#$2$1');
|
|
295
|
+
ipc.send('mpv-set-prop', [stremioToMPVProps[propName], argb]);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
default: {
|
|
299
|
+
// eslint-disable-next-line no-console
|
|
300
|
+
console.log('Unhandled setProp for', propName);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function command(commandName, commandArgs) {
|
|
305
|
+
switch (commandName) {
|
|
306
|
+
case 'load': {
|
|
307
|
+
command('unload');
|
|
308
|
+
if (commandArgs && commandArgs.stream && typeof commandArgs.stream.url === 'string') {
|
|
309
|
+
stream = commandArgs.stream;
|
|
310
|
+
onPropChanged('stream');
|
|
311
|
+
continueFrom = commandArgs.time !== null && isFinite(commandArgs.time) ? parseInt(commandArgs.time, 10) / 1000 : 0;
|
|
312
|
+
|
|
313
|
+
setBackground(false);
|
|
314
|
+
|
|
315
|
+
ipc.send('mpv-set-prop', ['no-sub-ass']);
|
|
316
|
+
|
|
317
|
+
// opengl-cb is an alias for the new name "libmpv", as shown in mpv's video/out/vo.c aliases
|
|
318
|
+
// opengl is an alias for the new name "gpu"
|
|
319
|
+
// When on Windows we use d3d for the rendering in separate window
|
|
320
|
+
var windowRenderer = navigator.platform === 'Win32' ? 'direct3d' : 'opengl';
|
|
321
|
+
var videoOutput = options.mpvSeparateWindow ? windowRenderer : 'opengl-cb';
|
|
322
|
+
var separateWindow = options.mpvSeparateWindow ? 'yes' : 'no';
|
|
323
|
+
ipc.send('mpv-set-prop', ['vo', videoOutput]);
|
|
324
|
+
ipc.send('mpv-set-prop', ['osc', separateWindow]);
|
|
325
|
+
ipc.send('mpv-set-prop', ['input-defalt-bindings', separateWindow]);
|
|
326
|
+
ipc.send('mpv-set-prop', ['input-vo-keyboard', separateWindow]);
|
|
327
|
+
|
|
328
|
+
ipc.send('mpv-command', ['loadfile', stream.url]);
|
|
329
|
+
ipc.send('mpv-set-prop', ['pause', false]);
|
|
330
|
+
ipc.send('mpv-set-prop', ['speed', props.speed]);
|
|
331
|
+
ipc.send('mpv-set-prop', ['aid', props.aid]);
|
|
332
|
+
ipc.send('mpv-set-prop', ['mute', 'no']);
|
|
333
|
+
|
|
334
|
+
onPropChanged('paused');
|
|
335
|
+
onPropChanged('time');
|
|
336
|
+
onPropChanged('duration');
|
|
337
|
+
onPropChanged('buffering');
|
|
338
|
+
onPropChanged('volume');
|
|
339
|
+
onPropChanged('muted');
|
|
340
|
+
onPropChanged('subtitlesTracks');
|
|
341
|
+
onPropChanged('selectedSubtitlesTrackId');
|
|
342
|
+
} else {
|
|
343
|
+
onError(Object.assign({}, ERROR.UNSUPPORTED_STREAM, {
|
|
344
|
+
critical: true,
|
|
345
|
+
stream: commandArgs ? commandArgs.stream : null
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
case 'unload': {
|
|
352
|
+
props = {
|
|
353
|
+
mute: false,
|
|
354
|
+
speed: 1,
|
|
355
|
+
subtitlesTracks: [],
|
|
356
|
+
buffering: true,
|
|
357
|
+
aid: null,
|
|
358
|
+
sid: null,
|
|
359
|
+
};
|
|
360
|
+
continueFrom = 0;
|
|
361
|
+
avgDuration = 0;
|
|
362
|
+
ipc.send('mpv-command', ['stop']);
|
|
363
|
+
onPropChanged('stream');
|
|
364
|
+
onPropChanged('paused');
|
|
365
|
+
onPropChanged('time');
|
|
366
|
+
onPropChanged('duration');
|
|
367
|
+
onPropChanged('buffering');
|
|
368
|
+
onPropChanged('volume');
|
|
369
|
+
onPropChanged('muted');
|
|
370
|
+
onPropChanged('subtitlesTracks');
|
|
371
|
+
onPropChanged('selectedSubtitlesTrackId');
|
|
372
|
+
setBackground(true);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case 'destroy': {
|
|
376
|
+
command('unload');
|
|
377
|
+
destroyed = true;
|
|
378
|
+
events.removeAllListeners();
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.on = function (eventName, listener) {
|
|
385
|
+
if (destroyed) {
|
|
386
|
+
throw new Error('Video is destroyed');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
events.on(eventName, listener);
|
|
390
|
+
};
|
|
391
|
+
this.dispatch = function (action) {
|
|
392
|
+
if (destroyed) {
|
|
393
|
+
throw new Error('Video is destroyed');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (action) {
|
|
397
|
+
action = deepFreeze(cloneDeep(action));
|
|
398
|
+
switch (action.type) {
|
|
399
|
+
case 'observeProp': {
|
|
400
|
+
observeProp(action.propName);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
case 'setProp': {
|
|
404
|
+
setProp(action.propName, action.propValue);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
case 'command': {
|
|
408
|
+
command(
|
|
409
|
+
action.commandName,
|
|
410
|
+
action.commandArgs
|
|
411
|
+
);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
ShellVideo.canPlayStream = function() {
|
|
419
|
+
return Promise.resolve(true);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
ShellVideo.manifest = {
|
|
423
|
+
name: 'ShellVideo',
|
|
424
|
+
external: false,
|
|
425
|
+
props: Object.keys(stremioToMPVProps),
|
|
426
|
+
commands: ['load', 'unload', 'destroy'],
|
|
427
|
+
events: [
|
|
428
|
+
'propValue',
|
|
429
|
+
'propChanged',
|
|
430
|
+
'ended',
|
|
431
|
+
'error',
|
|
432
|
+
'subtitlesTrackLoaded',
|
|
433
|
+
],
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
module.exports = ShellVideo;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
var ChromecastSenderVideo = require('../ChromecastSenderVideo');
|
|
2
|
+
var ShellVideo = require('../ShellVideo');
|
|
2
3
|
var HTMLVideo = require('../HTMLVideo');
|
|
3
4
|
var TizenVideo = require('../TizenVideo');
|
|
4
5
|
var IFrameVideo = require('../IFrameVideo');
|
|
@@ -23,6 +24,10 @@ function selectVideoImplementation(commandArgs, options) {
|
|
|
23
24
|
return IFrameVideo;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
if (options.shellTransport) {
|
|
28
|
+
return withStreamingServer(withHTMLSubtitles(ShellVideo));
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
if (typeof commandArgs.streamingServerURL === 'string') {
|
|
27
32
|
if (typeof global.tizen !== 'undefined') {
|
|
28
33
|
return withStreamingServer(withHTMLSubtitles(TizenVideo));
|
|
@@ -17,6 +17,8 @@ function TizenVideo(options) {
|
|
|
17
17
|
throw new Error('Container element required to be instance of HTMLElement');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
var promiseAudioTrackChange = false;
|
|
21
|
+
|
|
20
22
|
var size = 100;
|
|
21
23
|
var offset = 0;
|
|
22
24
|
var textColor = 'rgb(255, 255, 255)';
|
|
@@ -151,7 +153,14 @@ function TizenVideo(options) {
|
|
|
151
153
|
return null;
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
var isPaused = !!(window.webapis.avplay.getState() === 'PAUSED');
|
|
157
|
+
|
|
158
|
+
if (!isPaused && promiseAudioTrackChange) {
|
|
159
|
+
window.webapis.avplay.setSelectTrack('AUDIO', parseInt(promiseAudioTrackChange.replace('EMBEDDED_', '')));
|
|
160
|
+
promiseAudioTrackChange = false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return isPaused;
|
|
155
164
|
}
|
|
156
165
|
case 'time': {
|
|
157
166
|
var currentTime = window.webapis.avplay.getCurrentTime();
|
|
@@ -301,8 +310,12 @@ function TizenVideo(options) {
|
|
|
301
310
|
return null;
|
|
302
311
|
}
|
|
303
312
|
|
|
313
|
+
if (promiseAudioTrackChange) {
|
|
314
|
+
return promiseAudioTrackChange;
|
|
315
|
+
}
|
|
316
|
+
|
|
304
317
|
var currentTracks = window.webapis.avplay.getCurrentStreamInfo();
|
|
305
|
-
var currentIndex;
|
|
318
|
+
var currentIndex = false;
|
|
306
319
|
|
|
307
320
|
for (var i = 0; i < currentTracks.length; i++) {
|
|
308
321
|
if (currentTracks[i].type === 'AUDIO') {
|
|
@@ -312,7 +325,7 @@ function TizenVideo(options) {
|
|
|
312
325
|
}
|
|
313
326
|
}
|
|
314
327
|
|
|
315
|
-
return currentIndex ? 'EMBEDDED_' + String(currentIndex) : null;
|
|
328
|
+
return currentIndex !== false ? 'EMBEDDED_' + String(currentIndex) : null;
|
|
316
329
|
}
|
|
317
330
|
case 'playbackSpeed': {
|
|
318
331
|
if (destroyed || videoSpeed === null || !isFinite(videoSpeed)) {
|
|
@@ -375,6 +388,16 @@ function TizenVideo(options) {
|
|
|
375
388
|
|
|
376
389
|
onPropChanged('paused');
|
|
377
390
|
|
|
391
|
+
// the paused state is usually correct, but i have seen it not change on tizen 3
|
|
392
|
+
// which causes all kinds of issues in the UI: (only happens with some videos)
|
|
393
|
+
var lastKnownProp = getProp('paused');
|
|
394
|
+
|
|
395
|
+
setTimeout(function() {
|
|
396
|
+
if (getProp('paused') !== lastKnownProp) {
|
|
397
|
+
onPropChanged('paused');
|
|
398
|
+
}
|
|
399
|
+
}, 1000);
|
|
400
|
+
|
|
378
401
|
break;
|
|
379
402
|
}
|
|
380
403
|
case 'time': {
|
|
@@ -493,8 +516,17 @@ function TizenVideo(options) {
|
|
|
493
516
|
return track.id === propValue;
|
|
494
517
|
});
|
|
495
518
|
|
|
496
|
-
|
|
519
|
+
if (getProp('paused')) {
|
|
520
|
+
// issues before this logic:
|
|
521
|
+
// tizen 3 does not allow changing audio track when paused
|
|
522
|
+
// tizen 5 does, but it will only change getProp('selectedAudioTrackId') after playback starts
|
|
497
523
|
|
|
524
|
+
// will be changed on next play event, until then we will overwrite the result of getProp('selectedAudioTrackId')
|
|
525
|
+
promiseAudioTrackChange = propValue;
|
|
526
|
+
onPropChanged('selectedAudioTrackId');
|
|
527
|
+
} else {
|
|
528
|
+
window.webapis.avplay.setSelectTrack('AUDIO', parseInt(currentAudioTrack.replace('EMBEDDED_', '')));
|
|
529
|
+
}
|
|
498
530
|
if (selectedAudioTrack) {
|
|
499
531
|
events.emit('audioTrackLoaded', selectedAudioTrack);
|
|
500
532
|
onPropChanged('selectedAudioTrackId');
|
|
@@ -39,6 +39,7 @@ function YouTubeVideo(options) {
|
|
|
39
39
|
var selectedSubtitlesTrackId = null;
|
|
40
40
|
var observedProps = {
|
|
41
41
|
stream: false,
|
|
42
|
+
loaded: false,
|
|
42
43
|
paused: false,
|
|
43
44
|
time: false,
|
|
44
45
|
duration: false,
|
|
@@ -190,6 +191,13 @@ function YouTubeVideo(options) {
|
|
|
190
191
|
case 'stream': {
|
|
191
192
|
return stream;
|
|
192
193
|
}
|
|
194
|
+
case 'loaded': {
|
|
195
|
+
if (stream === null) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
193
201
|
case 'paused': {
|
|
194
202
|
if (stream === null || typeof video.getPlayerState !== 'function') {
|
|
195
203
|
return null;
|
|
@@ -360,6 +368,7 @@ function YouTubeVideo(options) {
|
|
|
360
368
|
if (ready) {
|
|
361
369
|
stream = commandArgs.stream;
|
|
362
370
|
onPropChanged('stream');
|
|
371
|
+
onPropChanged('loaded');
|
|
363
372
|
var autoplay = typeof commandArgs.autoplay === 'boolean' ? commandArgs.autoplay : true;
|
|
364
373
|
var time = commandArgs.time !== null && isFinite(commandArgs.time) ? parseInt(commandArgs.time, 10) / 1000 : 0;
|
|
365
374
|
if (autoplay && typeof video.loadVideoById === 'function') {
|
|
@@ -397,6 +406,7 @@ function YouTubeVideo(options) {
|
|
|
397
406
|
pendingLoadArgs = null;
|
|
398
407
|
stream = null;
|
|
399
408
|
onPropChanged('stream');
|
|
409
|
+
onPropChanged('loaded');
|
|
400
410
|
selectedSubtitlesTrackId = null;
|
|
401
411
|
if (ready && typeof video.stopVideo === 'function') {
|
|
402
412
|
video.stopVideo();
|
|
@@ -467,7 +477,7 @@ YouTubeVideo.canPlayStream = function(stream) {
|
|
|
467
477
|
YouTubeVideo.manifest = {
|
|
468
478
|
name: 'YouTubeVideo',
|
|
469
479
|
external: false,
|
|
470
|
-
props: ['stream', 'paused', 'time', 'duration', 'buffering', 'volume', 'muted', 'subtitlesTracks', 'selectedSubtitlesTrackId'],
|
|
480
|
+
props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'volume', 'muted', 'subtitlesTracks', 'selectedSubtitlesTrackId'],
|
|
471
481
|
commands: ['load', 'unload', 'destroy'],
|
|
472
482
|
events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded']
|
|
473
483
|
};
|