@stremio/stremio-video 0.0.22 → 0.0.24

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.22",
3
+ "version": "0.0.24",
4
4
  "description": "Abstraction layer on top of different media players",
5
5
  "author": "Smart Code OOD",
6
6
  "main": "src/index.js",
@@ -2,6 +2,7 @@ var ChromecastSenderVideo = require('../ChromecastSenderVideo');
2
2
  var ShellVideo = require('../ShellVideo');
3
3
  var HTMLVideo = require('../HTMLVideo');
4
4
  var TizenVideo = require('../TizenVideo');
5
+ var WebOsVideo = require('../WebOsVideo');
5
6
  var IFrameVideo = require('../IFrameVideo');
6
7
  var YouTubeVideo = require('../YouTubeVideo');
7
8
  var withStreamingServer = require('../withStreamingServer');
@@ -32,10 +33,16 @@ function selectVideoImplementation(commandArgs, options) {
32
33
  if (typeof global.tizen !== 'undefined') {
33
34
  return withStreamingServer(withHTMLSubtitles(TizenVideo));
34
35
  }
36
+ if (typeof global.webOS !== 'undefined') {
37
+ return withStreamingServer(withHTMLSubtitles(WebOsVideo));
38
+ }
35
39
  return withStreamingServer(withHTMLSubtitles(HTMLVideo));
36
40
  }
37
41
 
38
42
  if (typeof commandArgs.stream.url === 'string') {
43
+ if (typeof global.webOS !== 'undefined') {
44
+ return withHTMLSubtitles(WebOsVideo);
45
+ }
39
46
  if (typeof global.tizen !== 'undefined') {
40
47
  return withHTMLSubtitles(TizenVideo);
41
48
  }
@@ -0,0 +1,1089 @@
1
+ var EventEmitter = require('eventemitter3');
2
+ var cloneDeep = require('lodash.clonedeep');
3
+ var deepFreeze = require('deep-freeze');
4
+ var ERROR = require('../error');
5
+
6
+ function luna(params, call, fail, method) {
7
+ if (call) params.onSuccess = call || function() {};
8
+
9
+ params.onFailure = function () { // function(result)
10
+ // console.log('WebOS',(params.method || method) + ' [fail][' + result.errorCode + '] ' + result.errorText );
11
+
12
+ if (fail) fail();
13
+ };
14
+
15
+ window.webOS.service.request(method || 'luna://com.webos.media', params);
16
+ }
17
+
18
+ function runWebOS(params, failed) {
19
+ // console.log('run web os', params);
20
+ window.webOS.service.request('luna://com.webos.applicationManager', {
21
+ method: 'launch',
22
+ parameters: {
23
+ 'id': params.need,
24
+ 'params': {
25
+ 'payload':[
26
+ {
27
+ 'fullPath': params.url,
28
+ 'artist':'',
29
+ 'subtitle':'',
30
+ 'dlnaInfo':{
31
+ 'flagVal':4096,
32
+ 'cleartextSize':'-1',
33
+ 'contentLength':'-1',
34
+ 'opVal':1,
35
+ 'protocolInfo':'http-get:*:video/x-matroska:DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000',
36
+ 'duration':0
37
+ },
38
+ 'mediaType':'VIDEO',
39
+ 'thumbnail':'',
40
+ 'deviceType':'DMR',
41
+ 'album':'',
42
+ 'fileName': params.name,
43
+ 'lastPlayPosition': params.position
44
+ }
45
+ ]
46
+ }
47
+ },
48
+ onSuccess: function () {
49
+ // console.log('The app is launched');
50
+ },
51
+ onFailure: function () { // function(inError)
52
+ // console.log('Player', 'Failed to launch the app ('+params.need+'): ', '[' + inError.errorCode + ']: ' + inError.errorText);
53
+
54
+ if (params.need === 'com.webos.app.photovideo') {
55
+ params.need = 'com.webos.app.smartshare';
56
+ runWebOS(params);
57
+ } else if(params.need === 'com.webos.app.smartshare') {
58
+ params.need = 'com.webos.app.mediadiscovery';
59
+ runWebOS(params);
60
+ } else if (params.need === 'com.webos.app.mediadiscovery') {
61
+ failed();
62
+ }
63
+ }
64
+ });
65
+ }
66
+
67
+ var webOsColors = ['black', 'white', 'yellow', 'red', 'green', 'blue'];
68
+ var stremioColors = {
69
+ // rgba
70
+ 'rgba(0, 0, 0, 255)': 'black',
71
+ 'rgba(255, 255, 255, 255)': 'white',
72
+ 'rgba(255, 255, 0, 255)': 'yellow',
73
+ 'rgba(255, 0, 0, 255)': 'red',
74
+ 'rgba(0, 255, 0, 255)': 'green',
75
+ 'rgba(0, 0, 255, 255)': 'blue',
76
+ // rgba case 2
77
+ 'rgba(0, 0, 0, 1)': 'black',
78
+ 'rgba(255, 255, 255, 1)': 'white',
79
+ 'rgba(255, 255, 0, 1)': 'yellow',
80
+ 'rgba(255, 0, 0, 1)': 'red',
81
+ 'rgba(0, 255, 0, 1)': 'green',
82
+ 'rgba(0, 0, 255, 1)': 'blue',
83
+ // rgb
84
+ 'rgba(0, 0, 0)': 'black',
85
+ 'rgba(255, 255, 255)': 'white',
86
+ 'rgba(255, 255, 0)': 'yellow',
87
+ 'rgba(255, 0, 0)': 'red',
88
+ 'rgba(0, 255, 0)': 'green',
89
+ 'rgba(0, 0, 255)': 'blue',
90
+ // 8-digit hex
91
+ '#000000FF': 'black',
92
+ '#FFFFFFFF': 'white',
93
+ '#FFFF00FF': 'yellow',
94
+ '#FF0000FF': 'red',
95
+ '#00FF00FF': 'green',
96
+ '#0000FFFF': 'blue',
97
+ // 6-digit hex
98
+ '#000000': 'black',
99
+ '#FFFFFF': 'white',
100
+ '#FFFF00': 'yellow',
101
+ '#FF0000': 'red',
102
+ '#00FF00': 'green',
103
+ '#0000FF': 'blue'
104
+ };
105
+
106
+ function stremioSubOffsets(offset) {
107
+ if (offset === 0) {
108
+ return -3;
109
+ } else if (offset <= 2) {
110
+ return -2;
111
+ } else if (offset <= 3) {
112
+ return -1;
113
+ } else if (offset <= 5) {
114
+ return 0;
115
+ } else if (offset <= 10) {
116
+ return 1;
117
+ } else if (offset <= 25) {
118
+ return 2;
119
+ } else if (offset <= 50) {
120
+ return 3;
121
+ } else if (offset <= 100) {
122
+ return 4;
123
+ }
124
+ return false;
125
+ }
126
+
127
+ function stremioSubSizes(size) {
128
+ // there is also: 0 (tiny)
129
+ // adding zero will break the logic
130
+ if (size <= 75) {
131
+ return 1;
132
+ } else if (size <= 100) {
133
+ return 2;
134
+ } else if (size <= 150) {
135
+ return 3;
136
+ } else if (size <= 250) {
137
+ return 4;
138
+ }
139
+ return false;
140
+ }
141
+
142
+ function WebOsVideo(options) {
143
+
144
+ options = options || {};
145
+
146
+ var containerElement = options.containerElement;
147
+ if (!(containerElement instanceof HTMLElement)) {
148
+ throw new Error('Container element required to be instance of HTMLElement');
149
+ }
150
+
151
+ var knownMediaId = false;
152
+
153
+ var subSize = 75;
154
+
155
+ var disabledSubs = true;
156
+
157
+ var subscribed = false;
158
+
159
+ var currentSubTrack = false;
160
+
161
+ var currentAudioTrack = false;
162
+
163
+ var textTracks = [];
164
+
165
+ var audioTracks = [];
166
+
167
+ var count_message = 0;
168
+
169
+ var subtitleOffset = 5;
170
+
171
+ var setSubs = function (info) {
172
+ textTracks = [];
173
+ // console.log('sub tracks 1, nr of sub tracks: ', info.numSubtitleTracks);
174
+ if (info.numSubtitleTracks) {
175
+
176
+ // console.log('sub tracks 2');
177
+
178
+ // try {
179
+ // console.log('got sub info', JSON.stringify(info.subtitleTrackInfo));
180
+ // } catch(e) {};
181
+ for (var i = 0; i < info.subtitleTrackInfo.length; i++) {
182
+ var textTrack = info.subtitleTrackInfo[i];
183
+ textTrack.index = i;
184
+ var textTrackLang = textTrack.language === '(null)' ? '' : textTrack.language;
185
+
186
+ var textTrackId = 'EMBEDDED_' + textTrack.index;
187
+
188
+ if (!currentSubTrack && !textTracks.length) {
189
+ currentSubTrack = textTrackId;
190
+ }
191
+
192
+ textTracks.push({
193
+ id: textTrackId,
194
+ lang: textTrackLang,
195
+ label: textTrackLang,
196
+ origin: 'EMBEDDED',
197
+ embedded: true,
198
+ mode: textTrackId === currentSubTrack ? 'showing' : 'disabled',
199
+ });
200
+
201
+ }
202
+
203
+ // console.log('sub tracks all', textTracks);
204
+
205
+ onPropChanged('subtitlesTracks');
206
+ onPropChanged('selectedSubtitlesTrackId');
207
+
208
+ }
209
+ };
210
+
211
+ var setTracks = function (info) {
212
+ audioTracks = [];
213
+ // console.log('audio tracks 1, nr of audio tracks: ', info.numAudioTracks);
214
+ if (info.numAudioTracks) {
215
+
216
+ //console.log('audio tracks 2');
217
+
218
+ // try {
219
+ // console.log('got audio info', JSON.stringify(info.audioTrackInfo));
220
+ // } catch(e) {};
221
+ for (var i = 0; i < info.audioTrackInfo.length; i++) {
222
+ var audioTrack = info.audioTrackInfo[i];
223
+ audioTrack.index = i;
224
+ var audioTrackId = 'EMBEDDED_' + audioTrack.index;
225
+ if (!currentAudioTrack && !audioTracks.length) {
226
+ currentAudioTrack = audioTrackId;
227
+ }
228
+ var audioTrackLang = audioTrack.language === '(null)' ? '' : audioTrack.language;
229
+ audioTracks.push({
230
+ id: audioTrackId,
231
+ lang: audioTrackLang,
232
+ label: audioTrackLang,
233
+ origin: 'EMBEDDED',
234
+ embedded: true,
235
+ mode: audioTrackId === currentAudioTrack ? 'showing' : 'disabled',
236
+ });
237
+ }
238
+ // console.log('audio tracks all', audioTracks);
239
+ onPropChanged('audioTracks');
240
+ onPropChanged('selectedAudioTrackId');
241
+
242
+ }
243
+ };
244
+
245
+ var subscribe = function (cb) {
246
+ if (subscribed) return;
247
+ subscribed = true;
248
+ var answered = false;
249
+ // console.log('subscribing');
250
+ luna({
251
+ method: 'subscribe',
252
+ parameters: {
253
+ 'mediaId': knownMediaId,
254
+ 'subscribe': true
255
+ }
256
+ }, function (result) {
257
+ if (result.sourceInfo && !answered) {
258
+ answered = true;
259
+ // try {
260
+ // console.log('got source info', JSON.stringify(result.sourceInfo.programInfo[0]));
261
+ // } catch(e) {};
262
+ var info = result.sourceInfo.programInfo[0];
263
+
264
+ setSubs(info);
265
+
266
+ setTracks(info);
267
+
268
+ unsubscribe(cb);
269
+ }
270
+
271
+ if ((result.error || {}).errorCode) {
272
+ answered = true;
273
+ // console.error('luna playback error', result.error);
274
+ unsubscribe(cb);
275
+ // unsubscribe();
276
+ // onVideoError();
277
+ return;
278
+ }
279
+
280
+ if ((result.unloadCompleted || {}).mediaId === knownMediaId && (result.unloadCompleted || {}).state) {
281
+ // strange case where it just.. ends? without ever getting result.sourceInfo
282
+ // onEnded();
283
+ // console.log('strange case of end');
284
+ // unsubscribe(cb);
285
+ return;
286
+ }
287
+
288
+ // console.log('WebOS', 'subscribe', JSON.stringify(result));
289
+ count_message++;
290
+
291
+ if (count_message === 30 && !answered) {
292
+ // cb();
293
+ unsubscribe(cb);
294
+ }
295
+ }, function() { // function(err)
296
+ // console.log('luna error log 2');
297
+ // console.error(err);
298
+ });
299
+ };
300
+
301
+ var unsubscribe = function (cb) {
302
+ if (!subscribed) return;
303
+ subscribed = false;
304
+ luna({
305
+ method: 'unsubscribe',
306
+ parameters: {
307
+ 'mediaId': knownMediaId
308
+ }
309
+ }, function () { // function(result)
310
+ // console.log('unsubscribe result', JSON.stringify(result));
311
+ cb();
312
+ }, function () { // function(err)
313
+ // console.log('unsubscribe error', JSON.stringify(err));
314
+ cb();
315
+ });
316
+ cb();
317
+ };
318
+
319
+ // var unload = function (cb) {
320
+ // luna({
321
+ // method: 'unload',
322
+ // parameters: {
323
+ // 'mediaId': knownMediaId
324
+ // }
325
+ // }, cb, cb);
326
+ // };
327
+
328
+ var toggleSubtitles = function (status) {
329
+ if (!knownMediaId) return;
330
+
331
+ disabledSubs = !status;
332
+
333
+ // console.log('enable subs: ' + status);
334
+
335
+ luna({
336
+ method: 'setSubtitleEnable',
337
+ parameters: {
338
+ 'mediaId': knownMediaId,
339
+ 'enable': status
340
+ }
341
+ });
342
+ };
343
+
344
+ var styleElement = document.createElement('style');
345
+ containerElement.appendChild(styleElement);
346
+ styleElement.sheet.insertRule('video::cue { font-size: 4vmin; color: rgb(255, 255, 255); background-color: rgba(0, 0, 0, 0); text-shadow: rgb(34, 34, 34) 1px 1px 0.1em; }');
347
+ var videoElement = document.createElement('video');
348
+ videoElement.style.width = '100%';
349
+ videoElement.style.height = '100%';
350
+ videoElement.style.backgroundColor = 'black';
351
+ // videoElement.crossOrigin = 'anonymous';
352
+ videoElement.controls = false;
353
+ videoElement.onerror = function() {
354
+ onVideoError();
355
+ };
356
+ videoElement.onended = function() {
357
+ onEnded();
358
+ };
359
+ videoElement.onpause = function() {
360
+ onPropChanged('paused');
361
+ };
362
+ videoElement.onplay = function() {
363
+ onPropChanged('paused');
364
+ };
365
+ videoElement.ontimeupdate = function() {
366
+ onPropChanged('time');
367
+ onPropChanged('buffered');
368
+ };
369
+ videoElement.ondurationchange = function() {
370
+ onPropChanged('duration');
371
+ };
372
+ videoElement.onwaiting = function() {
373
+ onPropChanged('buffering');
374
+ onPropChanged('buffered');
375
+ };
376
+ videoElement.onseeking = function() {
377
+ onPropChanged('buffering');
378
+ onPropChanged('buffered');
379
+ };
380
+ videoElement.onseeked = function() {
381
+ onPropChanged('buffering');
382
+ onPropChanged('buffered');
383
+ };
384
+ videoElement.onstalled = function() {
385
+ onPropChanged('buffering');
386
+ onPropChanged('buffered');
387
+ };
388
+ videoElement.onplaying = function() {
389
+ onPropChanged('buffering');
390
+ onPropChanged('buffered');
391
+ };
392
+ videoElement.oncanplay = function() {
393
+ onPropChanged('buffering');
394
+ onPropChanged('buffered');
395
+ };
396
+ videoElement.canplaythrough = function() {
397
+ onPropChanged('buffering');
398
+ onPropChanged('buffered');
399
+ };
400
+ videoElement.onloadeddata = function() {
401
+ onPropChanged('buffering');
402
+ onPropChanged('buffered');
403
+ };
404
+ videoElement.onloadedmetadata = function() {
405
+ onPropChanged('buffering');
406
+ onPropChanged('buffered');
407
+ setProp('time', startTime);
408
+ };
409
+ videoElement.onvolumechange = function() {
410
+ onPropChanged('volume');
411
+ onPropChanged('muted');
412
+ };
413
+ videoElement.onratechange = function() {
414
+ onPropChanged('playbackSpeed');
415
+ };
416
+ videoElement.textTracks.onchange = function() {
417
+ onPropChanged('subtitlesTracks');
418
+ onPropChanged('selectedSubtitlesTrackId');
419
+ onCueChange();
420
+ Array.from(videoElement.textTracks).forEach(function(track) {
421
+ track.oncuechange = onCueChange;
422
+ });
423
+ };
424
+ containerElement.appendChild(videoElement);
425
+
426
+ var lastSubColor = null;
427
+ var lastSubBgColor = null;
428
+ var lastSubBgOpacity = 0;
429
+ var lastPlaybackSpeed = 1;
430
+
431
+ var events = new EventEmitter();
432
+ var destroyed = false;
433
+ var stream = null;
434
+ var startTime = null;
435
+ var subtitlesOffset = 0;
436
+ var observedProps = {
437
+ stream: false,
438
+ paused: false,
439
+ time: false,
440
+ duration: false,
441
+ buffering: false,
442
+ buffered: false,
443
+ subtitlesTracks: false,
444
+ selectedSubtitlesTrackId: false,
445
+ subtitlesOffset: false,
446
+ subtitlesSize: false,
447
+ subtitlesTextColor: false,
448
+ subtitlesBackgroundColor: false,
449
+ audioTracks: false,
450
+ selectedAudioTrackId: false,
451
+ volume: false,
452
+ muted: false,
453
+ playbackSpeed: false
454
+ };
455
+
456
+ function getProp(propName) {
457
+ switch (propName) {
458
+ case 'stream': {
459
+ return stream;
460
+ }
461
+ case 'paused': {
462
+ if (stream === null) {
463
+ return null;
464
+ }
465
+
466
+ return !!videoElement.paused;
467
+ }
468
+ case 'time': {
469
+ if (stream === null || videoElement.currentTime === null || !isFinite(videoElement.currentTime)) {
470
+ return null;
471
+ }
472
+
473
+ return Math.floor(videoElement.currentTime * 1000);
474
+ }
475
+ case 'duration': {
476
+ if (stream === null || videoElement.duration === null || !isFinite(videoElement.duration)) {
477
+ return null;
478
+ }
479
+
480
+ return Math.floor(videoElement.duration * 1000);
481
+ }
482
+ case 'buffering': {
483
+ if (stream === null) {
484
+ return null;
485
+ }
486
+
487
+ return videoElement.readyState < videoElement.HAVE_FUTURE_DATA;
488
+ }
489
+ case 'buffered': {
490
+ if (stream === null) {
491
+ return null;
492
+ }
493
+
494
+ var time = videoElement.currentTime !== null && isFinite(videoElement.currentTime) ? videoElement.currentTime : 0;
495
+ for (var i = 0; i < videoElement.buffered.length; i++) {
496
+ if (videoElement.buffered.start(i) <= time && time <= videoElement.buffered.end(i)) {
497
+ return Math.floor(videoElement.buffered.end(i) * 1000);
498
+ }
499
+ }
500
+
501
+ return Math.floor(time * 1000);
502
+ }
503
+ case 'subtitlesTracks': {
504
+ if (stream === null) {
505
+ return [];
506
+ }
507
+
508
+ return textTracks;
509
+ }
510
+ case 'selectedSubtitlesTrackId': {
511
+ if (stream === null || disabledSubs) {
512
+ return null;
513
+ }
514
+
515
+ return currentSubTrack;
516
+ }
517
+ case 'subtitlesOffset': {
518
+ if (destroyed) {
519
+ return null;
520
+ }
521
+
522
+ return subtitlesOffset;
523
+ }
524
+ case 'subtitlesSize': {
525
+ if (destroyed) {
526
+ return null;
527
+ }
528
+
529
+ return subSize;
530
+ }
531
+ case 'subtitlesTextColor': {
532
+ if (destroyed) {
533
+ return null;
534
+ }
535
+
536
+ return lastSubColor || 'rgba(255, 255, 255, 255)';
537
+ }
538
+ case 'subtitlesBackgroundColor': {
539
+ if (destroyed) {
540
+ return null;
541
+ }
542
+
543
+ return lastSubBgColor || 'rgba(255, 255, 255, 0)';
544
+ }
545
+ case 'audioTracks': {
546
+ return audioTracks;
547
+ }
548
+ case 'selectedAudioTrackId': {
549
+ return currentAudioTrack;
550
+ }
551
+ case 'volume': {
552
+ if (destroyed || videoElement.volume === null || !isFinite(videoElement.volume)) {
553
+ return null;
554
+ }
555
+
556
+ return Math.floor(videoElement.volume * 100);
557
+ }
558
+ case 'muted': {
559
+ if (destroyed) {
560
+ return null;
561
+ }
562
+
563
+ return !!videoElement.muted;
564
+ }
565
+ case 'playbackSpeed': {
566
+ if (destroyed || lastPlaybackSpeed === null || !isFinite(lastPlaybackSpeed)) {
567
+ return null;
568
+ }
569
+
570
+ return lastPlaybackSpeed;
571
+ }
572
+ default: {
573
+ return null;
574
+ }
575
+ }
576
+ }
577
+ function onCueChange() {
578
+ Array.from(videoElement.textTracks).forEach(function(track) {
579
+ Array.from(track.cues || []).forEach(function(cue) {
580
+ cue.snapToLines = false;
581
+ cue.line = 100 - subtitlesOffset;
582
+ });
583
+ });
584
+ }
585
+ function onVideoError() {
586
+ if (destroyed) {
587
+ return;
588
+ }
589
+
590
+ var error;
591
+ switch ((videoElement.error || {}).code) {
592
+ case 1: {
593
+ error = ERROR.HTML_VIDEO.MEDIA_ERR_ABORTED;
594
+ break;
595
+ }
596
+ case 2: {
597
+ error = ERROR.HTML_VIDEO.MEDIA_ERR_NETWORK;
598
+ break;
599
+ }
600
+ case 3: {
601
+ error = ERROR.HTML_VIDEO.MEDIA_ERR_DECODE;
602
+ runWebOS({
603
+ need: 'com.webos.app.photovideo',
604
+ url: stream.url,
605
+ name: 'Stremio',
606
+ position: -1,
607
+ });
608
+ break;
609
+ }
610
+ case 4: {
611
+ error = ERROR.HTML_VIDEO.MEDIA_ERR_SRC_NOT_SUPPORTED;
612
+ runWebOS({
613
+ need: 'com.webos.app.photovideo',
614
+ url: stream.url,
615
+ name: 'Stremio',
616
+ position: -1,
617
+ });
618
+ break;
619
+ }
620
+ default: {
621
+ error = ERROR.UNKNOWN_ERROR;
622
+ }
623
+ }
624
+ onError(Object.assign({}, error, {
625
+ critical: true,
626
+ error: videoElement.error
627
+ }));
628
+ }
629
+ function onError(error) {
630
+ events.emit('error', error);
631
+ if (error.critical) {
632
+ command('unload');
633
+ }
634
+ }
635
+ function onEnded() {
636
+ events.emit('ended');
637
+ }
638
+ function onPropChanged(propName) {
639
+ if (observedProps[propName]) {
640
+ events.emit('propChanged', propName, getProp(propName));
641
+ }
642
+ }
643
+ function observeProp(propName) {
644
+ if (observedProps.hasOwnProperty(propName)) {
645
+ events.emit('propValue', propName, getProp(propName));
646
+ observedProps[propName] = true;
647
+ }
648
+ }
649
+ function setProp(propName, propValue) {
650
+ switch (propName) {
651
+ case 'paused': {
652
+ if (stream !== null) {
653
+ propValue ? videoElement.pause() : videoElement.play();
654
+ }
655
+
656
+ break;
657
+ }
658
+ case 'time': {
659
+ if (stream !== null && videoElement.readyState >= videoElement.HAVE_METADATA && propValue !== null && isFinite(propValue)) {
660
+ try {
661
+ videoElement.currentTime = parseInt(propValue, 10) / 1000;
662
+ } catch(e) {
663
+ // console.log('webos video change time error');
664
+ // console.error(e);
665
+ }
666
+ }
667
+
668
+ break;
669
+ }
670
+ case 'selectedSubtitlesTrackId': {
671
+ if (stream !== null) {
672
+ if ((propValue || '').indexOf('EMBEDDED_') === 0) {
673
+ if (disabledSubs) {
674
+ toggleSubtitles(true);
675
+ }
676
+
677
+ // console.log('WebOS', 'change subtitles for id: ', knownMediaId, ' index:', propValue);
678
+
679
+ currentSubTrack = propValue;
680
+ var trackIndex = parseInt(propValue.replace('EMBEDDED_', ''));
681
+ // console.log('set subs to track idx: ' + trackIndex);
682
+ luna({
683
+ method: 'selectTrack',
684
+ parameters: {
685
+ 'type': 'text',
686
+ 'mediaId': knownMediaId,
687
+ 'index': trackIndex
688
+ }
689
+ }, function() {
690
+ // console.log('changed subs track successfully');
691
+ var selectedSubtitlesTrack = getProp('subtitlesTracks')
692
+ .find(function(track) {
693
+ return track.id === propValue;
694
+ });
695
+ textTracks = textTracks.map(function(track) {
696
+ track.mode = track.id === currentSubTrack ? 'showing' : 'disabled';
697
+ return track;
698
+ });
699
+ if (selectedSubtitlesTrack) {
700
+ events.emit('subtitlesTrackLoaded', selectedSubtitlesTrack);
701
+ onPropChanged('selectedSubtitlesTrackId');
702
+ }
703
+ });
704
+ } else if (!propValue) {
705
+ toggleSubtitles(false);
706
+ }
707
+ }
708
+
709
+ break;
710
+ }
711
+ case 'subtitlesOffset': {
712
+ if (propValue !== null && isFinite(propValue)) {
713
+ subtitlesOffset = Math.max(0, Math.min(100, parseInt(propValue, 10)));
714
+ var nextOffset = stremioSubOffsets(subtitleOffset);
715
+ if (nextOffset === false) { // use default
716
+ nextOffset = 0;
717
+ }
718
+ luna({
719
+ method: 'setSubtitlePosition',
720
+ parameters: {
721
+ 'mediaId': knownMediaId,
722
+ 'position': nextOffset,
723
+ }
724
+ }, function() {
725
+ // console.log('successfully changed sub offset to: ' + nextOffset);
726
+ });
727
+
728
+ onPropChanged('subtitlesOffset');
729
+ }
730
+
731
+ break;
732
+ }
733
+ case 'subtitlesSize': {
734
+ if (propValue !== null && isFinite(propValue)) {
735
+ subSize = Math.max(0, parseInt(propValue, 10));
736
+ var nextSubSize = stremioSubSizes(subSize);
737
+ if (nextSubSize === false) { // use default
738
+ nextSubSize = 2;
739
+ }
740
+ luna({
741
+ method: 'setSubtitleFontSize',
742
+ parameters: {
743
+ 'mediaId': knownMediaId,
744
+ 'fontSize': nextSubSize,
745
+ }
746
+ }, function() {
747
+ // console.log('successfully changed sub size to: ' + nextSubSize);
748
+ });
749
+
750
+ onPropChanged('subtitlesSize');
751
+ }
752
+
753
+ break;
754
+ }
755
+ case 'subtitlesTextColor': {
756
+ if (typeof propValue === 'string') {
757
+ // we use setSubtitleCharacterColor instead of setSubtitleColor
758
+ // because it has the same color options as the sub background
759
+ var nextColor = 'white';
760
+ if (stremioColors[propValue] && webOsColors.indexOf(stremioColors[propValue]) > -1) {
761
+ nextColor = stremioColors[propValue];
762
+ }
763
+ luna({
764
+ method: 'setSubtitleCharacterColor',
765
+ parameters: {
766
+ 'mediaId': knownMediaId,
767
+ 'charColor': nextColor,
768
+ }
769
+ }, function() {
770
+ // console.log('changed subtitle color successfully to: ' + nextColor);
771
+ });
772
+ lastSubColor = propValue;
773
+ onPropChanged('subtitlesTextColor');
774
+ }
775
+
776
+ break;
777
+ }
778
+ case 'subtitlesBackgroundColor': {
779
+ if (typeof propValue === 'string') {
780
+ if (stremioColors[propValue] && webOsColors.indexOf(stremioColors[propValue]) > -1) {
781
+ luna({
782
+ method: 'setSubtitleBackgroundColor',
783
+ parameters: {
784
+ 'mediaId': knownMediaId,
785
+ 'color': stremioColors[propValue],
786
+ }
787
+ }, function() {
788
+ // console.log('changed subtitle background color successfully to: ' + stremioColors[propValue]);
789
+ if (!lastSubBgOpacity) {
790
+ luna({
791
+ method: 'setSubtitleBackgroundOpacity',
792
+ parameters: {
793
+ 'mediaId': knownMediaId,
794
+ 'bgOpacity': 255,
795
+ }
796
+ }, function() {
797
+ // console.log('changed subtitle background opacity successfully to: ' + 255);
798
+ lastSubBgOpacity = 255;
799
+ });
800
+ }
801
+ });
802
+ } else {
803
+ // we don't know this color, set sub background opacity to 0
804
+ luna({
805
+ method: 'setSubtitleBackgroundOpacity',
806
+ parameters: {
807
+ 'mediaId': knownMediaId,
808
+ 'bgOpacity': 0,
809
+ }
810
+ }, function() {
811
+ // console.log('changed subtitle background opacity successfully to: ' + 0);
812
+ lastSubBgOpacity = 0;
813
+ });
814
+ }
815
+ lastSubBgColor = propValue;
816
+ onPropChanged('subtitlesBackgroundColor');
817
+ }
818
+
819
+ break;
820
+ }
821
+ case 'selectedAudioTrackId': {
822
+ // console.log('WebOS', 'change audio track for id: ', knownMediaId, ' index:', propValue);
823
+
824
+ if ((propValue || '').indexOf('EMBEDDED_') === 0) {
825
+ currentAudioTrack = propValue;
826
+ var trackIndex = parseInt(propValue.replace('EMBEDDED_', ''));
827
+ luna({
828
+ method: 'selectTrack',
829
+ parameters: {
830
+ 'type': 'audio',
831
+ 'mediaId': knownMediaId,
832
+ 'index': trackIndex
833
+ }
834
+ }, function() {
835
+ // console.log('changed audio track successfully');
836
+ var selectedAudioTrack = getProp('audioTracks')
837
+ .find(function(track) {
838
+ return track.id === propValue;
839
+ });
840
+
841
+ audioTracks = audioTracks.map(function(track) {
842
+ track.mode = track.id === currentAudioTrack ? 'showing' : 'disabled';
843
+ return track;
844
+ });
845
+
846
+ if (selectedAudioTrack) {
847
+ events.emit('audioTrackLoaded', selectedAudioTrack);
848
+ onPropChanged('selectedAudioTrackId');
849
+ }
850
+ });
851
+ if (videoElement.audioTracks) {
852
+ for (var i = 0; i < videoElement.audioTracks.length; i++) {
853
+ videoElement.audioTracks[i].enabled = false;
854
+ }
855
+
856
+ if(videoElement.audioTracks[trackIndex]) {
857
+ videoElement.audioTracks[trackIndex].enabled = true;
858
+
859
+ // console.log('WebOS', 'change audio two method:', trackIndex);
860
+ }
861
+ }
862
+
863
+ }
864
+
865
+ break;
866
+ }
867
+ case 'volume': {
868
+ if (propValue !== null && isFinite(propValue)) {
869
+ videoElement.muted = false;
870
+ videoElement.volume = Math.max(0, Math.min(100, parseInt(propValue, 10))) / 100;
871
+ }
872
+
873
+ break;
874
+ }
875
+ case 'muted': {
876
+ videoElement.muted = !!propValue;
877
+ break;
878
+ }
879
+ case 'playbackSpeed': {
880
+ // console.log('start change play rate to: ' + propValue);
881
+ // console.log(typeof propValue);
882
+ if (propValue !== null && isFinite(propValue)) {
883
+ lastPlaybackSpeed = parseFloat(propValue);
884
+ luna({
885
+ method: 'setPlayRate',
886
+ parameters: {
887
+ 'mediaId': knownMediaId,
888
+ 'playRate': lastPlaybackSpeed,
889
+ 'audioOutput': true,
890
+ }
891
+ }, function() {
892
+ // console.log('set playback rate success: ', lastPlaybackSpeed);
893
+ }, function() {
894
+ // console.log('failed setting playback rate success: ', lastPlaybackSpeed);
895
+ });
896
+ onPropChanged('playbackSpeed');
897
+ }
898
+
899
+ break;
900
+ }
901
+ }
902
+ }
903
+ function command(commandName, commandArgs) {
904
+ switch (commandName) {
905
+ case 'load': {
906
+ // not sure about this
907
+ // command('unload');
908
+ if (commandArgs && commandArgs.stream && typeof commandArgs.stream.url === 'string') {
909
+ stream = commandArgs.stream;
910
+ startTime = commandArgs.time;
911
+
912
+ onPropChanged('stream');
913
+ videoElement.autoplay = typeof commandArgs.autoplay === 'boolean' ? commandArgs.autoplay : true;
914
+
915
+ onPropChanged('paused');
916
+ onPropChanged('time');
917
+ onPropChanged('duration');
918
+ onPropChanged('buffering');
919
+ onPropChanged('buffered');
920
+ onPropChanged('subtitlesTracks');
921
+ onPropChanged('selectedSubtitlesTrackId');
922
+ onPropChanged('audioTracks');
923
+ onPropChanged('selectedAudioTrackId');
924
+
925
+ var count = 0;
926
+
927
+ var initMediaId = function (cb) {
928
+ function retrieveMediaId() {
929
+ if (videoElement.mediaId) {
930
+ knownMediaId = videoElement.mediaId;
931
+ // console.log('got media id: ', videoElement.mediaId);
932
+ clearInterval(timer);
933
+ subscribe(cb);
934
+ return;
935
+ }
936
+ count++;
937
+ if (count > 4) {
938
+ // console.log('failed to get media id');
939
+ clearInterval(timer);
940
+ cb();
941
+ }
942
+ }
943
+ var timer = setInterval(retrieveMediaId, 300);
944
+ };
945
+
946
+ var startVideo = function () {
947
+ // console.log('startVideo');
948
+ // not needed?
949
+ // videoElement.src = stream.url;
950
+
951
+ try {
952
+ videoElement.load();
953
+ } catch(e) {
954
+ // console.log('can\'t load video');
955
+ // console.error(e);
956
+ }
957
+
958
+ try {
959
+ // console.log('try play');
960
+ videoElement.play();
961
+ } catch(e) {
962
+ // console.log('can\'t start video');
963
+ // console.error(e);
964
+ }
965
+ };
966
+
967
+ videoElement.src = stream.url;
968
+
969
+ initMediaId(startVideo);
970
+ } else {
971
+ onError(Object.assign({}, ERROR.UNSUPPORTED_STREAM, {
972
+ critical: true,
973
+ stream: commandArgs ? commandArgs.stream : null
974
+ }));
975
+ }
976
+ break;
977
+ }
978
+ case 'unload': {
979
+ stream = null;
980
+ startTime = null;
981
+ Array.from(videoElement.textTracks).forEach(function(track) {
982
+ track.oncuechange = null;
983
+ });
984
+ videoElement.removeAttribute('src');
985
+ videoElement.load();
986
+ // not sure about this:
987
+ // try {
988
+ // videoElement.currentTime = 0;
989
+ // } catch(e) {
990
+ // console.log('webos video unload error');
991
+ // console.error(e);
992
+ // }
993
+ onPropChanged('stream');
994
+ onPropChanged('paused');
995
+ onPropChanged('time');
996
+ onPropChanged('duration');
997
+ onPropChanged('buffering');
998
+ onPropChanged('buffered');
999
+ onPropChanged('subtitlesTracks');
1000
+ onPropChanged('selectedSubtitlesTrackId');
1001
+ onPropChanged('audioTracks');
1002
+ onPropChanged('selectedAudioTrackId');
1003
+ // not sure about this:
1004
+ // unload(function() {});
1005
+ break;
1006
+ }
1007
+ case 'destroy': {
1008
+ command('unload');
1009
+ destroyed = true;
1010
+ onPropChanged('subtitlesOffset');
1011
+ onPropChanged('subtitlesSize');
1012
+ onPropChanged('subtitlesTextColor');
1013
+ onPropChanged('subtitlesBackgroundColor');
1014
+ onPropChanged('volume');
1015
+ onPropChanged('muted');
1016
+ onPropChanged('playbackSpeed');
1017
+ events.removeAllListeners();
1018
+ videoElement.onerror = null;
1019
+ videoElement.onended = null;
1020
+ videoElement.onpause = null;
1021
+ videoElement.onplay = null;
1022
+ videoElement.ontimeupdate = null;
1023
+ videoElement.ondurationchange = null;
1024
+ videoElement.onwaiting = null;
1025
+ videoElement.onseeking = null;
1026
+ videoElement.onseeked = null;
1027
+ videoElement.onstalled = null;
1028
+ videoElement.onplaying = null;
1029
+ videoElement.oncanplay = null;
1030
+ videoElement.canplaythrough = null;
1031
+ videoElement.onloadeddata = null;
1032
+ videoElement.onloadedmetadata = null;
1033
+ videoElement.onvolumechange = null;
1034
+ videoElement.onratechange = null;
1035
+ videoElement.textTracks.onchange = null;
1036
+ containerElement.removeChild(videoElement);
1037
+ containerElement.removeChild(styleElement);
1038
+ break;
1039
+ }
1040
+ }
1041
+ }
1042
+
1043
+ this.on = function(eventName, listener) {
1044
+ if (destroyed) {
1045
+ throw new Error('Video is destroyed');
1046
+ }
1047
+
1048
+ events.on(eventName, listener);
1049
+ };
1050
+ this.dispatch = function(action) {
1051
+ if (destroyed) {
1052
+ throw new Error('Video is destroyed');
1053
+ }
1054
+
1055
+ if (action) {
1056
+ action = deepFreeze(cloneDeep(action));
1057
+ switch (action.type) {
1058
+ case 'observeProp': {
1059
+ observeProp(action.propName);
1060
+ return;
1061
+ }
1062
+ case 'setProp': {
1063
+ setProp(action.propName, action.propValue);
1064
+ return;
1065
+ }
1066
+ case 'command': {
1067
+ command(action.commandName, action.commandArgs);
1068
+ return;
1069
+ }
1070
+ }
1071
+ }
1072
+
1073
+ throw new Error('Invalid action dispatched: ' + JSON.stringify(action));
1074
+ };
1075
+ }
1076
+
1077
+ WebOsVideo.canPlayStream = function() { // function(stream)
1078
+ return Promise.resolve(true);
1079
+ };
1080
+
1081
+ WebOsVideo.manifest = {
1082
+ name: 'WebOsVideo',
1083
+ external: false,
1084
+ props: ['stream', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'volume', 'muted', 'playbackSpeed'],
1085
+ commands: ['load', 'unload', 'destroy'],
1086
+ events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded']
1087
+ };
1088
+
1089
+ module.exports = WebOsVideo;
@@ -0,0 +1,3 @@
1
+ var WebOsVideo = require('./WebOsVideo');
2
+
3
+ module.exports = WebOsVideo;
@@ -0,0 +1,94 @@
1
+ var VIDEO_CODEC_CONFIGS = [
2
+ {
3
+ codec: 'h264',
4
+ mime: 'video/mp4; codecs="avc1.42E01E"',
5
+ },
6
+ {
7
+ codec: 'h265',
8
+ mime: 'video/mp4; codecs="hev1.1.6.L150.B0"',
9
+ aliases: ['hevc']
10
+ },
11
+ {
12
+ codec: 'vp8',
13
+ mime: 'video/mp4; codecs="vp8"'
14
+ },
15
+ {
16
+ codec: 'vp9',
17
+ mime: 'video/mp4; codecs="vp9"'
18
+ }
19
+ ];
20
+
21
+ var AUDIO_CODEC_CONFIGS = [
22
+ {
23
+ codec: 'aac',
24
+ mime: 'audio/mp4; codecs="mp4a.40.2"'
25
+ },
26
+ {
27
+ codec: 'mp3',
28
+ mime: 'audio/mp4; codecs="mp3"'
29
+ },
30
+ {
31
+ codec: 'ac3',
32
+ mime: 'audio/mp4; codecs="ac-3"'
33
+ },
34
+ {
35
+ codec: 'eac3',
36
+ mime: 'audio/mp4; codecs="ec-3"'
37
+ },
38
+ {
39
+ codec: 'vorbis',
40
+ mime: 'audio/mp4; codecs="vorbis"'
41
+ },
42
+ {
43
+ codec: 'opus',
44
+ mime: 'audio/mp4; codecs="opus"'
45
+ }
46
+ ];
47
+
48
+ function canPlay(config, options) {
49
+ return options.mediaElement.canPlayType(config.mime) ?
50
+ [config.codec].concat(config.aliases || [])
51
+ :
52
+ [];
53
+ }
54
+
55
+ function getMaxAudioChannels() {
56
+ if (/firefox/i.test(window.navigator.userAgent)) {
57
+ return 6;
58
+ }
59
+
60
+ if (!window.AudioContext) {
61
+ return 2;
62
+ }
63
+
64
+ var maxChannelCount = new AudioContext().destination.maxChannelCount;
65
+ return maxChannelCount > 0 ? maxChannelCount : 2;
66
+ }
67
+
68
+ function getMediaCapabilities() {
69
+ var mediaElement = document.createElement('video');
70
+ var formats = ['mp4'];
71
+ var videoCodecs = VIDEO_CODEC_CONFIGS
72
+ .map(function(config) {
73
+ return canPlay(config, { mediaElement: mediaElement });
74
+ })
75
+ .reduce(function(result, value) {
76
+ return result.concat(value);
77
+ }, []);
78
+ var audioCodecs = AUDIO_CODEC_CONFIGS
79
+ .map(function(config) {
80
+ return canPlay(config, { mediaElement: mediaElement });
81
+ })
82
+ .reduce(function(result, value) {
83
+ return result.concat(value);
84
+ }, []);
85
+ var maxAudioChannels = getMaxAudioChannels();
86
+ return {
87
+ formats: formats,
88
+ videoCodecs: videoCodecs,
89
+ audioCodecs: audioCodecs,
90
+ maxAudioChannels: maxAudioChannels
91
+ };
92
+ }
93
+
94
+ module.exports = getMediaCapabilities();
@@ -3,6 +3,7 @@ var url = require('url');
3
3
  var hat = require('hat');
4
4
  var cloneDeep = require('lodash.clonedeep');
5
5
  var deepFreeze = require('deep-freeze');
6
+ var mediaCapabilities = require('../mediaCapabilities');
6
7
  var convertStream = require('./convertStream');
7
8
  var ERROR = require('../error');
8
9
 
@@ -96,7 +97,29 @@ function withStreamingServer(Video) {
96
97
  onPropChanged('stream');
97
98
  convertStream(commandArgs.streamingServerURL, commandArgs.stream, commandArgs.seriesInfo)
98
99
  .then(function(mediaURL) {
99
- return (commandArgs.forceTranscoding ? Promise.resolve(false) : Video.canPlayStream({ url: mediaURL }))
100
+ var formats = Array.isArray(commandArgs.formats) ?
101
+ commandArgs.formats
102
+ :
103
+ mediaCapabilities.formats;
104
+ var videoCodecs = Array.isArray(commandArgs.videoCodecs) ?
105
+ commandArgs.videoCodecs
106
+ :
107
+ mediaCapabilities.videoCodecs;
108
+ var audioCodecs = Array.isArray(commandArgs.audioCodecs) ?
109
+ commandArgs.audioCodecs
110
+ :
111
+ mediaCapabilities.audioCodecs;
112
+ var maxAudioChannels = commandArgs.maxAudioChannels !== null && isFinite(commandArgs.maxAudioChannels) ?
113
+ commandArgs.maxAudioChannels
114
+ :
115
+ mediaCapabilities.maxAudioChannels;
116
+ var canPlayStreamOptions = Object.assign({}, commandArgs, {
117
+ formats: formats,
118
+ videoCodecs: videoCodecs,
119
+ audioCodecs: audioCodecs,
120
+ maxAudioChannels: maxAudioChannels
121
+ });
122
+ return (commandArgs.forceTranscoding ? Promise.resolve(false) : VideoWithStreamingServer.canPlayStream({ url: mediaURL }, canPlayStreamOptions))
100
123
  .catch(function(error) {
101
124
  console.warn('Media probe error', error);
102
125
  return false;
@@ -113,9 +136,16 @@ function withStreamingServer(Video) {
113
136
  if (commandArgs.forceTranscoding) {
114
137
  queryParams.set('forceTranscoding', '1');
115
138
  }
116
- if (commandArgs.maxAudioChannels !== null && isFinite(commandArgs.maxAudioChannels)) {
117
- queryParams.set('maxAudioChannels', commandArgs.maxAudioChannels);
118
- }
139
+
140
+ videoCodecs.forEach(function(videoCodec) {
141
+ queryParams.append('videoCodecs', videoCodec);
142
+ });
143
+
144
+ audioCodecs.forEach(function(audioCodec) {
145
+ queryParams.append('audioCodecs', audioCodec);
146
+ });
147
+
148
+ queryParams.set('maxAudioChannels', maxAudioChannels);
119
149
 
120
150
  return {
121
151
  url: url.resolve(commandArgs.streamingServerURL, '/hlsv2/' + id + '/master.m3u8?' + queryParams.toString()),
@@ -269,8 +299,38 @@ function withStreamingServer(Video) {
269
299
  };
270
300
  }
271
301
 
272
- VideoWithStreamingServer.canPlayStream = function(stream) {
273
- return Video.canPlayStream(stream);
302
+ VideoWithStreamingServer.canPlayStream = function(stream, options) {
303
+ return Video.canPlayStream(stream)
304
+ .then(function(canPlay) {
305
+ if (!canPlay) {
306
+ throw new Error('Fallback using /hlsv2/probe');
307
+ }
308
+
309
+ return canPlay;
310
+ })
311
+ .catch(function() {
312
+ var queryParams = new URLSearchParams([['mediaURL', stream.url]]);
313
+ return fetch(url.resolve(options.streamingServerURL, '/hlsv2/probe?' + queryParams.toString()))
314
+ .then(function(resp) {
315
+ return resp.json();
316
+ })
317
+ .then(function(probe) {
318
+ var isFormatSupported = options.formats.some(function(format) {
319
+ return probe.format.name.indexOf(format) !== -1;
320
+ });
321
+ var areStreamsSupported = probe.streams.every(function(stream) {
322
+ if (stream.track === 'audio') {
323
+ return stream.channels <= options.maxAudioChannels &&
324
+ options.audioCodecs.indexOf(stream.codec) !== -1;
325
+ } else if (stream.track === 'video') {
326
+ return options.videoCodecs.indexOf(stream.codec) !== -1;
327
+ }
328
+
329
+ return true;
330
+ });
331
+ return isFormatSupported && areStreamsSupported;
332
+ });
333
+ });
274
334
  };
275
335
 
276
336
  VideoWithStreamingServer.manifest = {