stremiok 2.0.0 → 2.2.0
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/CHANGELOG.md +82 -0
- package/README.md +10 -8
- package/config.xml +1 -1
- package/css/style.css +37 -2
- package/index.html +34 -31
- package/js/app.js +19 -7
- package/js/downloads.js +17 -5
- package/js/epg.js +320 -0
- package/js/hubpreview.js +15 -5
- package/js/iptv.js +202 -29
- package/js/keys.js +2 -0
- package/js/player-controls.js +55 -8
- package/js/player-core.js +204 -31
- package/js/player-lang.js +1 -1
- package/js/player-quality.js +15 -7
- package/js/player-store.js +28 -4
- package/js/player-subs.js +45 -11
- package/js/player.js +1 -1
- package/js/stremio-account.js +3 -0
- package/js/stremio.js +157 -28
- package/js/trakt.js +61 -9
- package/js/util.js +36 -10
- package/js/view-core.js +139 -55
- package/js/view-detail.js +5 -4
- package/js/view-guide.js +332 -0
- package/js/view-home.js +168 -36
- package/js/view-settings.js +170 -80
- package/js/views.js +3 -2
- package/js/youtube.js +1 -3
- package/js/ytplay.js +62 -5
- package/package.json +2 -1
- package/service/index.js +1131 -108
- package/service/preview.js +14 -4
package/js/player-core.js
CHANGED
|
@@ -58,7 +58,8 @@ var PlayerParts = {};
|
|
|
58
58
|
dur: 0, // seconds
|
|
59
59
|
cur: 0, // seconds
|
|
60
60
|
isLive: false, // live stream (sports/IPTV): no fixed duration
|
|
61
|
-
playStarted: false, // this AVPlay attempt
|
|
61
|
+
playStarted: false, // this AVPlay attempt was prepared (opened, ready to play)
|
|
62
|
+
playing: false, // ...and actually played: its position moved for a second (see oncurrentplaytime)
|
|
62
63
|
// seek robustness (AVPlay can error if you seek mid-buffer)
|
|
63
64
|
buffering: false,
|
|
64
65
|
pendingSeek: null,
|
|
@@ -156,8 +157,14 @@ var PlayerParts = {};
|
|
|
156
157
|
|
|
157
158
|
function isActive() { return S.active; }
|
|
158
159
|
|
|
159
|
-
|
|
160
|
+
// fromTicker: the 5-second save while it plays: skipped when nothing moved since the last one
|
|
161
|
+
// (paused); a stop, a leave or a jump always saves.
|
|
162
|
+
var _savedSig = '';
|
|
163
|
+
function saveProgress(fromTicker) {
|
|
160
164
|
if (!S.ctx || !S.dur || S.isLive || S.ctx.trailer || S.ctx.private || isNotice()) { return; } // (a notice clip, private viewing: nothing saved, nothing removed)
|
|
165
|
+
var sig = progressKey(S.ctx) + '|' + Math.floor(S.cur) + '|' + Math.floor(S.dur);
|
|
166
|
+
if (fromTicker && sig === _savedSig) { return; }
|
|
167
|
+
_savedSig = sig;
|
|
161
168
|
// Past the credits the episode is watched: its entry was removed and the next one
|
|
162
169
|
// queued (handleCredits). Saving again would put it back next to the next episode.
|
|
163
170
|
if (_creditsHandled) { return; }
|
|
@@ -292,15 +299,29 @@ var PlayerParts = {};
|
|
|
292
299
|
} catch (er2) {}
|
|
293
300
|
return out;
|
|
294
301
|
}
|
|
302
|
+
// An IPTV channel's TV guide (js/epg.js): { now, next } or null.
|
|
303
|
+
function guideNow() { return (typeof EPG !== 'undefined' && S.ctx && /^iptv:/.test(S.ctx.id || '')) ? EPG.now(S.ctx.id) : null; }
|
|
304
|
+
function clock(sec) { var d = new Date(sec * 1000); return (d.getHours() < 10 ? '0' : '') + d.getHours() + ':' + (d.getMinutes() < 10 ? '0' : '') + d.getMinutes(); }
|
|
305
|
+
if (typeof EPG !== 'undefined' && EPG.onChange) { EPG.onChange(function () { if (S.active && S.isLive) { renderOSD(); } }); }
|
|
295
306
|
function renderOSD() {
|
|
296
307
|
U.byId('osd-title').textContent = S.ctx ? S.ctx.name : '';
|
|
308
|
+
var ck = U.byId('osd-clock'); // (the time of day, top right while the bar is on screen)
|
|
309
|
+
if (ck) { ck.textContent = clock(Date.now() / 1000); }
|
|
297
310
|
renderTags();
|
|
298
311
|
renderChapters();
|
|
299
312
|
if (S.isLive) {
|
|
313
|
+
var cur = U.byId('osd-cur'), g = guideNow();
|
|
314
|
+
cur.className = 'live';
|
|
315
|
+
if (g && g.now) { // an IPTV channel with a TV guide: what's on, how far along, and next
|
|
316
|
+
var t = Date.now() / 1000;
|
|
317
|
+
U.byId('osd-bar').style.display = '';
|
|
318
|
+
U.byId('osd-progress').style.width = Math.max(0, Math.min(100, Math.round((t - g.now.start) / (g.now.stop - g.now.start) * 100))) + '%';
|
|
319
|
+
cur.textContent = '● LIVE ' + clock(g.now.start) + '–' + clock(g.now.stop) + ' ' + g.now.title;
|
|
320
|
+
U.byId('osd-dur').textContent = g.next ? 'Next ' + clock(g.next.start) + ' ' + g.next.title : '';
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
300
323
|
U.byId('osd-bar').style.display = 'none';
|
|
301
|
-
var cur = U.byId('osd-cur');
|
|
302
324
|
cur.textContent = '● LIVE';
|
|
303
|
-
cur.className = 'live';
|
|
304
325
|
U.byId('osd-dur').textContent = '';
|
|
305
326
|
return;
|
|
306
327
|
}
|
|
@@ -324,6 +345,41 @@ var PlayerParts = {};
|
|
|
324
345
|
yt: page, ytLang: lang, type: 'YouTube', id: 'yt_id:' + link.id, parent: 'yt_id:' + link.id, name: link.title || 'YouTube',
|
|
325
346
|
poster: 'https://i.ytimg.com/vi/' + link.id + '/hqdefault.jpg', streams: [], private: !!(S.ctx && S.ctx.private) || undefined }, _onExit); // (from a private one: private too)
|
|
326
347
|
}
|
|
348
|
+
// ---------- a live Xtream channel's format ----------
|
|
349
|
+
// An Xtream server sends a channel as MPEG-TS (…/live/USER/PASS/123.ts) or as HLS (….m3u8).
|
|
350
|
+
// The menu's Format plays it the other way and keeps that for the channel (FORMAT_KEY, by its
|
|
351
|
+
// id, the last FORMAT_KEEP), so it starts that way next time; its playlist's own way isn't
|
|
352
|
+
// kept. ctx._ownUrl: the link as its playlist gives it.
|
|
353
|
+
var FORMAT_KEY = 'stremiok.liveformat', FORMAT_KEEP = 300;
|
|
354
|
+
var XT_LIVE = /^(https?:\/\/[^?#]+\/live\/[^\/?#]+\/[^\/?#]+\/[^\/?#.]+)\.(ts|m3u8)([?#].*)?$/i;
|
|
355
|
+
function liveFormat(url) { var m = XT_LIVE.exec(url || ''); return m ? (m[2].toLowerCase() === 'm3u8' ? 'hls' : 'ts') : ''; }
|
|
356
|
+
function withFormat(url, fmt) { var m = XT_LIVE.exec(url || ''); return m ? m[1] + '.' + (fmt === 'hls' ? 'm3u8' : 'ts') + (m[3] || '') : url; }
|
|
357
|
+
function formatStart(ctx) {
|
|
358
|
+
if (!S.isLive || !ctx.id || !liveFormat(ctx._ownUrl || ctx.url)) { return; }
|
|
359
|
+
if (!ctx._ownUrl) { ctx._ownUrl = ctx.url; }
|
|
360
|
+
var want = (U.store.get(FORMAT_KEY, {}) || {})[ctx.id];
|
|
361
|
+
ctx.url = (want === 'ts' || want === 'hls') ? withFormat(ctx._ownUrl, want) : ctx._ownUrl;
|
|
362
|
+
}
|
|
363
|
+
function setLiveFormat(fmt) {
|
|
364
|
+
var old = S.ctx;
|
|
365
|
+
if (!S.active || !old || !old._ownUrl || !liveFormat(old.url) || liveFormat(old.url) === fmt) { return; }
|
|
366
|
+
var all = U.store.get(FORMAT_KEY, {}) || {};
|
|
367
|
+
delete all[old.id];
|
|
368
|
+
if (fmt !== liveFormat(old._ownUrl)) {
|
|
369
|
+
all[old.id] = fmt;
|
|
370
|
+
var keys = Object.keys(all);
|
|
371
|
+
keys.slice(0, Math.max(0, keys.length - FORMAT_KEEP)).forEach(function (k) { delete all[k]; }); // (the oldest picked)
|
|
372
|
+
}
|
|
373
|
+
U.store.set(FORMAT_KEY, all);
|
|
374
|
+
var ctx = {}; // (the channel again, without what was learned from the other link: its relay, its look)
|
|
375
|
+
Object.keys(old).forEach(function (k) { if (!/^_(relay|relayFor|relayCc|ccOffer|liveChecked)$/.test(k)) { ctx[k] = old[k]; } });
|
|
376
|
+
ctx.switching = true;
|
|
377
|
+
play(ctx, _onExit);
|
|
378
|
+
U.spinner(true, 'Opening it as ' + (fmt === 'hls' ? 'HLS' : 'MPEG-TS') + '…');
|
|
379
|
+
}
|
|
380
|
+
P.liveFormat = function () { return S.active && S.ctx && S.isLive && S.ctx._ownUrl ? liveFormat(S.ctx.url) : ''; };
|
|
381
|
+
P.setLiveFormat = setLiveFormat;
|
|
382
|
+
|
|
327
383
|
// Another source of what's playing (the player's source list): played in its place, at
|
|
328
384
|
// the same point, as a playback of its own — its quality ladder, YouTube details,
|
|
329
385
|
// captions, SponsorBlock sections, headers and file details are the new source's, and so
|
|
@@ -368,6 +424,7 @@ var PlayerParts = {};
|
|
|
368
424
|
S.active = true; S.cur = 0; S.dur = 0; _bg = null; S.actual = null;
|
|
369
425
|
// Stremio "tv" and "radio" types are strong live hints; refined after prepare.
|
|
370
426
|
S.isLive = (ctx.type === 'tv' || ctx.type === 'radio') || /^iptv:/.test(ctx.id || '') || !!ctx.isLive; // (an IPTV channel: live, a sports one too)
|
|
427
|
+
formatStart(ctx);
|
|
371
428
|
S.ctx.resumeSec = (ctx.trailer || ctx.fromStart) ? 0 : (ctx.resumeSec || getResume(ctx.type, ctx.id)); // (Play from start: 0:00)
|
|
372
429
|
|
|
373
430
|
// reset subtitle + audio + quality + on-screen controls state
|
|
@@ -376,7 +433,8 @@ var PlayerParts = {};
|
|
|
376
433
|
resetQuality(ctx);
|
|
377
434
|
resetControls();
|
|
378
435
|
_stall = { t: -1, since: 0, bufSince: 0, recoveries: [] };
|
|
379
|
-
|
|
436
|
+
recoReset();
|
|
437
|
+
_prefetchedNext = false; S.playStarted = false; S.playing = false;
|
|
380
438
|
_segments = null; _creditsSec = null; _creditsHandled = false; _segmentsFor = null;
|
|
381
439
|
S.buffering = false; S.pendingSeek = null; S.lastSeekAt = 0; S.settleFrom = 0; _seekRecovering = false; _jumpUndone = null; _pausedOnce = false;
|
|
382
440
|
updateHint();
|
|
@@ -509,7 +567,7 @@ var PlayerParts = {};
|
|
|
509
567
|
url = (S.ctx && S.ctx._relay && S.ctx._relayFor === url) ? S.ctx._relay : opened(url); // (a live channel's sound repacked: see liveCheck)
|
|
510
568
|
var token = ++_playToken;
|
|
511
569
|
objEl.className = 'active';
|
|
512
|
-
S.playStarted = false;
|
|
570
|
+
S.playStarted = false; S.playing = false;
|
|
513
571
|
_lastTickMs = null; _playedMs = 0;
|
|
514
572
|
_tick = null; _pausedOnce = false; // (a new open: the jump watch starts over, see checkJump)
|
|
515
573
|
subsBeforeOpen(); // a built-in subtitle track is selected again once this attempt plays
|
|
@@ -564,6 +622,7 @@ var PlayerParts = {};
|
|
|
564
622
|
// Played, not just opened: a file can open and then fail (or show nothing)
|
|
565
623
|
if (_lastTickMs !== null && ms > _lastTickMs && ms - _lastTickMs < 1500) { _playedMs += ms - _lastTickMs; }
|
|
566
624
|
_lastTickMs = ms;
|
|
625
|
+
if (!S.playing && _playedMs >= 1000) { S.playing = true; _reco.tries = 0; } // (started: playing, not just opened; a reconnect's tries start over)
|
|
567
626
|
if (_playedMs >= 2000 && !isNotice()) { reportStart(true); } // (a notice clip proves nothing about the format)
|
|
568
627
|
audioOnTick();
|
|
569
628
|
subsOnTick();
|
|
@@ -586,11 +645,15 @@ var PlayerParts = {};
|
|
|
586
645
|
setTimeout(function () { _seekRecovering = false; }, 4000);
|
|
587
646
|
return;
|
|
588
647
|
}
|
|
589
|
-
//
|
|
590
|
-
if (
|
|
648
|
+
// A live channel that played: its connection dropped → opened again (liveDropped).
|
|
649
|
+
if (S.playing && liveReconnects()) { U.log('player', 'Playback error: ' + err + ' (' + where(S.ctx.url) + ')'); liveDropped('Connection lost'); return; }
|
|
650
|
+
// A reconnect's try that didn't play → the next try.
|
|
651
|
+
if (!S.playing && _reco.tries) { if (!relayFailed()) { liveDropped('Couldn’t reconnect'); } return; }
|
|
652
|
+
// Failed before it ever played (opened, maybe, but nothing shown) → retry / step down / Auto.
|
|
653
|
+
if (!S.playing && (relayFailed() || tryFallback())) { return; }
|
|
591
654
|
reportStart(false, err); // (unless it had already played)
|
|
592
655
|
U.log('player', 'Playback error: ' + err + ' (' + where(S.ctx && S.ctx.url) + ')');
|
|
593
|
-
if (!S.
|
|
656
|
+
if (!S.playing) { failedStart('Playback error: ' + err); return; }
|
|
594
657
|
U.toast('Playback error: ' + err);
|
|
595
658
|
exitLater();
|
|
596
659
|
}
|
|
@@ -625,6 +688,7 @@ var PlayerParts = {};
|
|
|
625
688
|
if (!S.active || token !== _playToken) { return; }
|
|
626
689
|
U.spinner(false);
|
|
627
690
|
if (relayFailed()) { return; }
|
|
691
|
+
if (_reco.tries) { liveDropped('Couldn’t reconnect'); return; } // (a reconnect's try: the next one)
|
|
628
692
|
// Prepare failed → retry / step down / Auto.
|
|
629
693
|
if (tryFallback()) { return; }
|
|
630
694
|
U.log('player', 'Could not open: ' + (err && (err.name || err.message || err)) + ' (' + where(url) + ')');
|
|
@@ -710,7 +774,7 @@ var PlayerParts = {};
|
|
|
710
774
|
_ticker = setInterval(function () {
|
|
711
775
|
if (inBackground()) { return; } // (saved on the way there; nothing to watch until it's back)
|
|
712
776
|
if (useAV) { try { S.cur = avplay.getCurrentTime() / 1000; } catch (e) {} onProgress(); checkStall(); }
|
|
713
|
-
saveProgress();
|
|
777
|
+
saveProgress(true);
|
|
714
778
|
}, 5000);
|
|
715
779
|
}
|
|
716
780
|
|
|
@@ -723,8 +787,8 @@ var PlayerParts = {};
|
|
|
723
787
|
return !!S.ctx && !S.ctx.yt && !S.ctx.trailer && !S.isLive && (S.ctx.type === 'movie' || S.ctx.type === 'series') && S.dur > 0 && S.dur < NOTICE_MAX_S;
|
|
724
788
|
}
|
|
725
789
|
function onEnded() {
|
|
726
|
-
// a live channel whose stream ended (its server stopped it): opened again
|
|
727
|
-
if (
|
|
790
|
+
// a live channel whose stream ended (its server stopped it): opened again (liveDropped)
|
|
791
|
+
if (liveReconnects()) { liveDropped('The stream ended'); return; }
|
|
728
792
|
if (isNotice()) { // (back to its sources: another one, or this one later)
|
|
729
793
|
U.toast('That source isn\'t ready yet (the debrid service is still getting it): try another, or this one later', 6000);
|
|
730
794
|
exit();
|
|
@@ -834,6 +898,13 @@ var PlayerParts = {};
|
|
|
834
898
|
if (_creditsHandled || !S.ctx || S.ctx.trailer || isNotice()) { return; }
|
|
835
899
|
_creditsHandled = true;
|
|
836
900
|
if (S.ctx.private) { return; } // (private viewing: not marked watched, nothing queued)
|
|
901
|
+
// With Trakt: sent as a finish (Trakt's own way, which also clears a paused place it
|
|
902
|
+
// kept for it), not as a separate "watched"
|
|
903
|
+
if (S.dur > 0 && typeof Trakt !== 'undefined' && Trakt.onStopped && Trakt.isConnected && Trakt.isConnected()) {
|
|
904
|
+
markFinished(true);
|
|
905
|
+
Trakt.onStopped(S.ctx.type, S.ctx.id, S.cur, S.dur, S.ctx.name);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
837
908
|
markFinished(false);
|
|
838
909
|
}
|
|
839
910
|
// Watched: off Continue Watching, the next episode queued. quiet: Trakt's been told another
|
|
@@ -937,7 +1008,7 @@ var PlayerParts = {};
|
|
|
937
1008
|
var NEXT_TRIES = 3, AV_START_MS = 45000, _openedAt = 0;
|
|
938
1009
|
function nextSourceOnFail(notice) {
|
|
939
1010
|
var c = S.ctx;
|
|
940
|
-
if (!c || !S.active || S.
|
|
1011
|
+
if (!c || !S.active || S.playing || S.isLive || c.trailer || c.yt || c.localFile || !c.stream || typeof c.nextSource !== 'function') { return false; }
|
|
941
1012
|
if (typeof Stremio === 'undefined' || !Stremio.getPrefs || !Stremio.getPrefs().nextOnFail) { return false; }
|
|
942
1013
|
var tried = (c._tried || []).concat([Stremio.sourceId(c.stream)]);
|
|
943
1014
|
if (tried.length > NEXT_TRIES) { return false; }
|
|
@@ -1000,11 +1071,17 @@ var PlayerParts = {};
|
|
|
1000
1071
|
else { n.text = svc + ' couldn’t give this file (' + addon + ': “' + kind.replace(/_/g, ' ') + '”)'; }
|
|
1001
1072
|
return n;
|
|
1002
1073
|
}
|
|
1003
|
-
// Not
|
|
1004
|
-
// next source
|
|
1074
|
+
// Not playing after AV_START_MS (no answer from the server, or opened and nothing shown): the
|
|
1075
|
+
// next source when there's one; else it stops and says so (not while you've paused it).
|
|
1005
1076
|
function startCheck() {
|
|
1006
|
-
if (!S.active || S.
|
|
1007
|
-
|
|
1077
|
+
if (!S.active || S.playing || !useAV || !_openedAt || Date.now() - _openedAt < AV_START_MS) { return; }
|
|
1078
|
+
if (_reco.tries) { liveDropped('Couldn’t reconnect'); return; } // (a reconnect's try: the next one)
|
|
1079
|
+
if (nextSourceOnFail()) { return; }
|
|
1080
|
+
if (S.playStarted && isPaused()) { return; }
|
|
1081
|
+
U.log('player', 'Didn’t start: nothing played in ' + (AV_START_MS / 1000) + ' s (' + where(S.ctx && S.ctx.url) + ')');
|
|
1082
|
+
U.spinner(false);
|
|
1083
|
+
U.toast('Didn’t start: nothing played in ' + (AV_START_MS / 1000) + ' seconds. Try another source.', 6000);
|
|
1084
|
+
exitLater();
|
|
1008
1085
|
}
|
|
1009
1086
|
P.startCheck = startCheck;
|
|
1010
1087
|
|
|
@@ -1014,6 +1091,11 @@ var PlayerParts = {};
|
|
|
1014
1091
|
// cut that way, the channel restarts through the service's relay, which repacks the sound.
|
|
1015
1092
|
// The answer is kept per link (LIVE_KEY, LIVE_DAYS): a channel known to need it starts
|
|
1016
1093
|
// through the relay (startLive), one known not to isn't asked about again.
|
|
1094
|
+
// The same look says which closed-caption channels (CC1–CC4) are in its picture (cc, kept
|
|
1095
|
+
// with it once sure). They're offered in the subtitle menu (ctx._ccOffer); turned on (picked
|
|
1096
|
+
// there, remembered for the channel, or in your default subtitle language), the channel plays
|
|
1097
|
+
// through the relay with them as subtitle tracks (ctx._relayCc), the sound as it is unless it
|
|
1098
|
+
// needs repacking too.
|
|
1017
1099
|
var LIVE_KEY = 'stremiok.livefix', LIVE_KEEP = 300, LIVE_DAYS = 14;
|
|
1018
1100
|
function liveFixable(ctx) {
|
|
1019
1101
|
return useAV && S.isLive && !!ctx && !ctx.yt && !ctx.trailer && /^https?:\/\//i.test(ctx.url || '') &&
|
|
@@ -1023,22 +1105,45 @@ var PlayerParts = {};
|
|
|
1023
1105
|
var k = (U.store.get(LIVE_KEY, {}) || {})[url];
|
|
1024
1106
|
return (k && Date.now() - k.at < LIVE_DAYS * 86400000) ? k : null;
|
|
1025
1107
|
}
|
|
1026
|
-
|
|
1108
|
+
// The caption channels kept for a link: [{ ch: 'CC1', lang: 'en' }], or null (not known yet).
|
|
1109
|
+
function ccOf(k) {
|
|
1110
|
+
if (!k || !Array.isArray(k.cc)) { return null; }
|
|
1111
|
+
return k.cc.filter(function (c) { return c && /^CC[1-4]$/.test(c.ch) && (!c.lang || /^[a-z]{2,3}$/.test(c.lang)); }).slice(0, 4);
|
|
1112
|
+
}
|
|
1113
|
+
function liveKeep(url, fix, cc) {
|
|
1027
1114
|
var all = U.store.get(LIVE_KEY, {}) || {};
|
|
1028
1115
|
all[url] = { fix: fix ? 1 : 0, at: Date.now() };
|
|
1116
|
+
if (cc) { all[url].cc = cc; }
|
|
1029
1117
|
var keys = Object.keys(all);
|
|
1030
1118
|
if (keys.length > LIVE_KEEP) {
|
|
1031
1119
|
keys.sort(function (a, b) { return all[a].at - all[b].at; }).slice(0, keys.length - LIVE_KEEP).forEach(function (k) { delete all[k]; });
|
|
1032
1120
|
}
|
|
1033
1121
|
U.store.set(LIVE_KEY, all);
|
|
1034
1122
|
}
|
|
1123
|
+
// Captions on from the start: your choice for this channel (a closed-caption track picked
|
|
1124
|
+
// there last), else one in your default subtitle language (or second choice).
|
|
1125
|
+
function ccWanted(ctx, cc) {
|
|
1126
|
+
if (!cc || !cc.length) { return false; }
|
|
1127
|
+
var mem = P.recallSub(ctx.id);
|
|
1128
|
+
if (mem) { return !!mem.embedded; }
|
|
1129
|
+
var cfg = P.getSubCfg(), langs = [cfg.lang, cfg.lang2].filter(function (l) { return l && l !== 'off' && l !== 'none'; });
|
|
1130
|
+
return cc.some(function (c) { return c.lang && langs.some(function (l) { return P.langMatches(l, c.lang); }); });
|
|
1131
|
+
}
|
|
1132
|
+
function ccIndex(cc, w) { for (var i = 0; i < cc.length; i++) { if (cc[i].ch === w.ch) { return i; } } return 0; }
|
|
1133
|
+
function relayed(ctx, r, cc) {
|
|
1134
|
+
if (!r || typeof r.url !== 'string' || !/^http:\/\/localhost:\d+\/live\//.test(r.url)) { return false; }
|
|
1135
|
+
ctx._relay = r.url; ctx._relayFor = ctx.url; ctx._relayCc = cc || null; ctx._ccOffer = cc ? null : ctx._ccOffer;
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1035
1138
|
function startLive(ctx, settle) {
|
|
1036
|
-
var known = liveFixable(ctx) ? liveKnown(ctx.url) : null;
|
|
1037
|
-
if (
|
|
1038
|
-
|
|
1039
|
-
|
|
1139
|
+
var known = liveFixable(ctx) ? liveKnown(ctx.url) : null, cc = ccOf(known);
|
|
1140
|
+
if (cc && cc.length) { ctx._ccOffer = cc; }
|
|
1141
|
+
var withCc = ccWanted(ctx, cc) ? cc : null;
|
|
1142
|
+
if (cc) { ctx._liveChecked = true; } // (both answers in: not asked again)
|
|
1143
|
+
if (!known || (!known.fix && !withCc)) { startAV(ctx.url, settle); return; }
|
|
1144
|
+
YTPlay.liveOpen(ctx.url, ctx._headers, true, { fix: !!known.fix, cc: withCc }).then(function (r) {
|
|
1040
1145
|
if (S.ctx !== ctx || !S.active) { return; }
|
|
1041
|
-
|
|
1146
|
+
relayed(ctx, r, withCc);
|
|
1042
1147
|
startAV(ctx.url, settle);
|
|
1043
1148
|
}, function () { if (S.ctx === ctx && S.active) { startAV(ctx.url, settle); } });
|
|
1044
1149
|
}
|
|
@@ -1046,17 +1151,37 @@ var PlayerParts = {};
|
|
|
1046
1151
|
var ctx = S.ctx;
|
|
1047
1152
|
if (!ctx || ctx._liveChecked || !liveFixable(ctx)) { return; }
|
|
1048
1153
|
ctx._liveChecked = true;
|
|
1049
|
-
|
|
1154
|
+
var known = liveKnown(ctx.url);
|
|
1155
|
+
if (known && ccOf(known)) { return; }
|
|
1050
1156
|
var url = ctx.url;
|
|
1051
1157
|
YTPlay.liveOpen(url, ctx._headers, false).then(function (r) {
|
|
1052
1158
|
if (!r || !r.sure) { return; } // (couldn't tell: asked again next time)
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
ctx
|
|
1159
|
+
var cc = (r.ccSure || r.cc === undefined) ? (ccOf(r) || []) : null, fix = known ? !!known.fix : !!r.fix; // (captions with no text yet: asked again next time; the sound's answer: as first found)
|
|
1160
|
+
liveKeep(url, fix, cc);
|
|
1161
|
+
if (S.ctx !== ctx || !S.active || ctx.url !== url) { return; }
|
|
1162
|
+
if (cc && cc.length && !ctx._relayCc) { ctx._ccOffer = cc; }
|
|
1163
|
+
if (cc && cc.length && ccWanted(ctx, cc)) { ccOn(ctx, 'Adding this channel’s captions…'); return; }
|
|
1164
|
+
if (known || !fix || ctx._relay || !relayed(ctx, r, null)) { return; } // (the sound: restarted for on its first look only)
|
|
1056
1165
|
U.log('player', 'Live sound repacked by StremioK\'s service (' + (r.why || '') + ')');
|
|
1057
1166
|
reopen('Fixing this channel’s sound…');
|
|
1058
1167
|
}, function () {});
|
|
1059
1168
|
}
|
|
1169
|
+
// The channel again through the relay with its closed captions as subtitle tracks (the sound
|
|
1170
|
+
// repacked too when it needs it); then: true once it's reopening. want: a caption track to
|
|
1171
|
+
// select once it plays ({ ch, lang }).
|
|
1172
|
+
function ccOn(ctx, message, want) {
|
|
1173
|
+
var known = liveKnown(ctx.url), cc = ccOf(known) || ctx._ccOffer;
|
|
1174
|
+
if (!cc || !cc.length || !liveFixable(ctx)) { return Promise.resolve(false); }
|
|
1175
|
+
return YTPlay.liveOpen(ctx.url, ctx._headers, true, { fix: !!(known && known.fix), cc: cc }).then(function (r) {
|
|
1176
|
+
if (S.ctx !== ctx || !S.active || !relayed(ctx, r, cc)) { return false; }
|
|
1177
|
+
var w = want || cc.filter(function (c) { return ccWanted(ctx, [c]); })[0] || null;
|
|
1178
|
+
U.log('player', 'Live captions through StremioK\'s service (' + cc.map(function (c) { return c.ch; }).join(', ') + ')');
|
|
1179
|
+
reopen(message);
|
|
1180
|
+
if (w && P.wantEmbedded) { P.wantEmbedded({ nth: ccIndex(cc, w), lang: w.lang }); } // (after the reopen: on its tracks)
|
|
1181
|
+
return true;
|
|
1182
|
+
}, function () { return false; });
|
|
1183
|
+
}
|
|
1184
|
+
P.ccOn = function (want) { return S.ctx ? ccOn(S.ctx, 'Adding this channel’s captions…', want) : Promise.resolve(false); };
|
|
1060
1185
|
// The relay didn't start (StremioK's service gone): the channel as it is.
|
|
1061
1186
|
function relayFailed() {
|
|
1062
1187
|
if (S.playStarted || !S.ctx || !S.ctx._relay) { return false; }
|
|
@@ -1077,14 +1202,15 @@ var PlayerParts = {};
|
|
|
1077
1202
|
var now = Date.now();
|
|
1078
1203
|
if (S.buffering) {
|
|
1079
1204
|
if (!_stall.bufSince) { _stall.bufSince = now; }
|
|
1080
|
-
else if (now - _stall.bufSince > BUFFER_STALL_SECS * 1000) { recoverStall('Stream stopped loading
|
|
1205
|
+
else if (now - _stall.bufSince > BUFFER_STALL_SECS * 1000) { recoverStall('Stream stopped loading'); }
|
|
1081
1206
|
return;
|
|
1082
1207
|
}
|
|
1083
1208
|
_stall.bufSince = 0;
|
|
1084
1209
|
if (isPaused() || S.cur !== _stall.t) { _stall.t = S.cur; _stall.since = now; return; }
|
|
1085
|
-
if (_stall.since && now - _stall.since > STALL_SECS * 1000) { recoverStall('Playback stalled
|
|
1210
|
+
if (_stall.since && now - _stall.since > STALL_SECS * 1000) { recoverStall('Playback stalled'); }
|
|
1086
1211
|
}
|
|
1087
|
-
function recoverStall(
|
|
1212
|
+
function recoverStall(why) {
|
|
1213
|
+
if (liveReconnects()) { liveDropped(why); return; }
|
|
1088
1214
|
var now = Date.now();
|
|
1089
1215
|
_stall.recoveries = _stall.recoveries.filter(function (t) { return now - t < STALL_WINDOW_MS; });
|
|
1090
1216
|
if (_stall.recoveries.length >= STALL_MAX) {
|
|
@@ -1094,7 +1220,51 @@ var PlayerParts = {};
|
|
|
1094
1220
|
return;
|
|
1095
1221
|
}
|
|
1096
1222
|
_stall.recoveries.push(now);
|
|
1097
|
-
reopen(
|
|
1223
|
+
reopen(why + ' — reloading…');
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// ---------- a live channel that drops ----------
|
|
1227
|
+
// A live channel that stops (an error once it played, its connection lost; frozen, stuck
|
|
1228
|
+
// loading, or ended by its server) is opened again at the live edge, as Settings › Playback ›
|
|
1229
|
+
// Seeking & buffering › Reconnect live channels says: Off (the player closes), A few times
|
|
1230
|
+
// (STALL_MAX within STALL_WINDOW_MS, as any stall) or Keep trying (no limit). A try that
|
|
1231
|
+
// doesn't play is followed by another, each a little later (RECO_WAIT_S); BACK stops it.
|
|
1232
|
+
// Not for a trailer or YouTube (a stall there reloads as a video's does).
|
|
1233
|
+
var RECO_WAIT_S = [0, 5, 10, 20, 40, 60], _reco = { tries: 0, timer: null };
|
|
1234
|
+
function liveReconnects() { return S.isLive && !!S.ctx && !S.ctx.trailer && !S.ctx.yt; }
|
|
1235
|
+
function recoReset() {
|
|
1236
|
+
if (_reco.timer) { clearTimeout(_reco.timer); }
|
|
1237
|
+
_reco = { tries: 0, timer: null };
|
|
1238
|
+
}
|
|
1239
|
+
function liveDropped(why) {
|
|
1240
|
+
if (_reco.timer) { return; } // (a try already waiting)
|
|
1241
|
+
var mode = P.playerCfg().reconnect, now = Date.now();
|
|
1242
|
+
if (mode === 'off') {
|
|
1243
|
+
U.log('player', 'Live channel: ' + why + '; reconnecting is off');
|
|
1244
|
+
U.spinner(false); U.toast(why + ': the channel stopped (reconnecting is off)', 5000);
|
|
1245
|
+
exitLater();
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
_stall.recoveries = _stall.recoveries.filter(function (t) { return now - t < STALL_WINDOW_MS; });
|
|
1249
|
+
if (mode !== 'always' && _stall.recoveries.length >= STALL_MAX) {
|
|
1250
|
+
U.log('player', 'Live channel: ' + why + '; dropped ' + STALL_MAX + ' times in ' + (STALL_WINDOW_MS / 60000) + ' minutes, given up');
|
|
1251
|
+
U.spinner(false); U.toast('This channel keeps dropping — try it again later', 5000);
|
|
1252
|
+
exit();
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
_stall.recoveries.push(now);
|
|
1256
|
+
var n = ++_reco.tries, wait = RECO_WAIT_S[Math.min(n - 1, RECO_WAIT_S.length - 1)] * 1000, ctx = S.ctx;
|
|
1257
|
+
var msg = why + ' — reconnecting' + (n > 1 ? ' (try ' + n + ')' : '') + '…';
|
|
1258
|
+
U.log('player', 'Live channel: ' + why + ', reconnecting (try ' + n + (wait ? ', in ' + (wait / 1000) + ' s' : '') + ')');
|
|
1259
|
+
if (!wait) { reopen(msg); return; }
|
|
1260
|
+
U.spinner(true, msg);
|
|
1261
|
+
function due() {
|
|
1262
|
+
_reco.timer = null;
|
|
1263
|
+
if (!S.active || S.ctx !== ctx) { return; }
|
|
1264
|
+
if (inBackground()) { _reco.timer = setTimeout(due, 5000); return; } // (back first)
|
|
1265
|
+
reopen(msg);
|
|
1266
|
+
}
|
|
1267
|
+
_reco.timer = setTimeout(due, wait);
|
|
1098
1268
|
}
|
|
1099
1269
|
// The stream opened again in place: VOD at the same position, live at the edge.
|
|
1100
1270
|
function reopen(message) {
|
|
@@ -1109,6 +1279,7 @@ var PlayerParts = {};
|
|
|
1109
1279
|
function exit() {
|
|
1110
1280
|
if (!S.active) { return; }
|
|
1111
1281
|
if (U.crumb) { U.crumb('player closed'); }
|
|
1282
|
+
if (P.sleepClear) { P.sleepClear(); } // (the sleep timer: this playback's)
|
|
1112
1283
|
saveProgress();
|
|
1113
1284
|
// Record partial progress to Trakt (unfinished VOD) so it shows in Continue Watching. Past
|
|
1114
1285
|
// 85% (Trakt connected): finished, Trakt's way (it marks it watched), and here as at the credits.
|
|
@@ -1126,6 +1297,7 @@ var PlayerParts = {};
|
|
|
1126
1297
|
closeControls();
|
|
1127
1298
|
screenSaver(true);
|
|
1128
1299
|
_playToken++; // invalidate AVPlay/HTML5 callbacks before stop/close emits any
|
|
1300
|
+
recoReset();
|
|
1129
1301
|
if (_ticker) { clearInterval(_ticker); _ticker = null; }
|
|
1130
1302
|
if (_osdTimer) { clearTimeout(_osdTimer); _osdTimer = null; }
|
|
1131
1303
|
hideOSD();
|
|
@@ -1154,4 +1326,5 @@ var PlayerParts = {};
|
|
|
1154
1326
|
P.sponsorHighlight = sponsorHighlight;
|
|
1155
1327
|
P.chapters = chapters; P.chapterAt = chapterAt; P.chapterLabel = chapterLabel;
|
|
1156
1328
|
P.seekTo = seekTo; P.doAVSeek = doAVSeek; P.seekBy = seekBy; P.exit = exit;
|
|
1329
|
+
P.reload = function () { if (S.active && S.ctx) { reopen(S.isLive ? 'Reloading the channel…' : 'Reloading…'); } }; // (the menu's Reload)
|
|
1157
1330
|
})(PlayerParts);
|
package/js/player-lang.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
var info = tr && tr.extra_info;
|
|
11
11
|
if (typeof info === 'string') {
|
|
12
12
|
try { info = JSON.parse(info); } catch (e) {
|
|
13
|
-
var out =
|
|
13
|
+
var out = Object.create(null), re = /["']?([A-Za-z_]+)["']?\s*[:=]\s*["']?([^"',}]*)/g, m; // (no inherited names)
|
|
14
14
|
while ((m = re.exec(info))) { out[m[1]] = m[2].trim(); }
|
|
15
15
|
info = out;
|
|
16
16
|
}
|
package/js/player-quality.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
function resetQuality(ctx) {
|
|
27
27
|
_variants = []; _qualityBw = null; _ytRetried = false; _switchNote = null;
|
|
28
28
|
_userPicked = false; _autoRetried = false; _startNote = '';
|
|
29
|
-
loadVariants(ctx.variantUrl || ctx.url, ctx.initialQualityHeight || 0, !!ctx.variantUrl);
|
|
29
|
+
loadVariants(ctx.variantUrl || ctx.url, ctx.initialQualityHeight || 0, !!ctx.variantUrl, ctx._headers);
|
|
30
30
|
}
|
|
31
31
|
// Playback started: a switch is done; say where a fallback landed.
|
|
32
32
|
function qualityOnStart() {
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
function qualityHeight() { var v = variantByBw(_qualityBw); return v ? v.h : 0; }
|
|
39
39
|
function qualityCount() { return _variants.length; }
|
|
40
40
|
function hasQualities() { return _variants.length > 1; }
|
|
41
|
+
var _unread = false; // an HLS stream whose quality list couldn't be read
|
|
42
|
+
function qualityUnread() { return _unread && !_variants.length; }
|
|
41
43
|
|
|
42
44
|
// Auto lets AVPlay adapt to bandwidth; an explicit pick caps the ladder via ADAPTIVE_INFO.
|
|
43
45
|
// Two cap strategies, because YouTube-style HLS breaks the usual one:
|
|
@@ -187,8 +189,11 @@
|
|
|
187
189
|
var u = '' + (url || ''), path = u.split('#')[0].split('?')[0], query = u.split('#')[0].split('?')[1] || '';
|
|
188
190
|
return /\.m3u8$/i.test(path) || /(^|&)(type|format|ext|output)=m3u8(&|$)/i.test(query);
|
|
189
191
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
+
// h: the stream's own User-Agent / Referer ({ ua, ref }): the list is then read through
|
|
193
|
+
// StremioK's service, which can send them (as the TV's player does); a web page can't.
|
|
194
|
+
function getHlsMaster(url, h) {
|
|
195
|
+
var viaService = !!(h && (h.ua || h.ref) && typeof YTPlay !== 'undefined' && YTPlay.hlsMaster && YTPlay.installMode && YTPlay.installMode() !== 'browser');
|
|
196
|
+
return (viaService ? YTPlay.hlsMaster(url, h) : U.getText(url, 8000, HLS_MASTER_MAX)).then(function (text) {
|
|
192
197
|
var t = ('' + (text || '')).replace(/^\uFEFF/, '').replace(/^\s+/, '');
|
|
193
198
|
if (t.indexOf('#EXTM3U') !== 0) { throw new Error('not an HLS playlist'); }
|
|
194
199
|
return t;
|
|
@@ -205,12 +210,13 @@
|
|
|
205
210
|
CLASSES.forEach(function (c) { if (Math.abs(c - t) <= c * 0.06) { best = c; } });
|
|
206
211
|
return best;
|
|
207
212
|
}
|
|
208
|
-
function loadVariants(url, initialHeight, isMaster) {
|
|
213
|
+
function loadVariants(url, initialHeight, isMaster, headers) {
|
|
209
214
|
var tok = ++_variantsToken; // a list arriving after you've moved on is for the old stream
|
|
210
215
|
_variants = [];
|
|
211
216
|
_sepAudio = false;
|
|
217
|
+
_unread = false;
|
|
212
218
|
if (!url || !(isMaster || isHlsUrl(url))) { return; }
|
|
213
|
-
getHlsMaster(url).then(function (text) {
|
|
219
|
+
getHlsMaster(url, headers).then(function (text) {
|
|
214
220
|
if (tok !== _variantsToken) { return; }
|
|
215
221
|
var t = '' + text;
|
|
216
222
|
_sepAudio = t.indexOf('#EXT-X-MEDIA:TYPE=AUDIO') !== -1 || /#EXT-X-STREAM-INF[^\r\n]*[,:]AUDIO=/.test(t);
|
|
@@ -239,7 +245,9 @@
|
|
|
239
245
|
}
|
|
240
246
|
}
|
|
241
247
|
}
|
|
242
|
-
}, function () {
|
|
248
|
+
}, function (e) { // (couldn't be read: said so, not "one quality"; an answer that isn't a playlist at all: no list, as before)
|
|
249
|
+
if (tok === _variantsToken) { _variants = []; _sepAudio = false; _unread = !/not an HLS playlist/.test((e && e.message) || ''); }
|
|
250
|
+
});
|
|
243
251
|
}
|
|
244
252
|
|
|
245
253
|
// Switch to a different source mid-playback, keeping position (VOD): a playback of its
|
|
@@ -312,7 +320,7 @@
|
|
|
312
320
|
|
|
313
321
|
// ----- for the other parts (and js/player.js) -----
|
|
314
322
|
P.resetQuality = resetQuality; P.qualityOnStart = qualityOnStart; P.clearStartNote = clearStartNote;
|
|
315
|
-
P.qualityHeight = qualityHeight; P.qualityCount = qualityCount; P.hasQualities = hasQualities;
|
|
323
|
+
P.qualityHeight = qualityHeight; P.qualityCount = qualityCount; P.hasQualities = hasQualities; P.qualityUnread = qualityUnread;
|
|
316
324
|
P.adaptiveInfo = adaptiveInfo; P.tryFallback = tryFallback; P.switchSource = switchSource;
|
|
317
325
|
P.qualityLabel = qualityLabel; P.qualityMenu = qualityMenu;
|
|
318
326
|
P.hasYtLanguages = hasYtLanguages; P.ytLanguageLabel = ytLanguageLabel; P.ytLanguageMenu = ytLanguageMenu;
|
package/js/player-store.js
CHANGED
|
@@ -107,7 +107,9 @@
|
|
|
107
107
|
for (var i = 0; i < SUB_FONTS.length; i++) { if (SUB_FONTS[i][0] === id) { return SUB_FONTS[i][2]; } }
|
|
108
108
|
return '';
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
var _subCfgVer = 0; // bumped on every change: a styled line knows it's up to date
|
|
111
|
+
function setSubCfg(cfg) { U.store.set(SUBCFG_KEY, cfg); _subCfgVer++; }
|
|
112
|
+
function subCfgVersion() { return _subCfgVer; }
|
|
111
113
|
|
|
112
114
|
// Preferred audio language ('default' = keep the stream's first audio track) and a
|
|
113
115
|
// second choice used when the first isn't on the stream ('none' = no second choice).
|
|
@@ -236,13 +238,15 @@
|
|
|
236
238
|
['zoom', 'Zoom (fill the screen, trims edges)', 'PLAYER_DISPLAY_MODE_CROPPED_FULL', 'cover'],
|
|
237
239
|
['stretch', 'Stretch', 'PLAYER_DISPLAY_MODE_FULL_SCREEN', 'fill']
|
|
238
240
|
];
|
|
239
|
-
// + seek: how far LEFT / RIGHT jump (s); buffer: 'normal' (the TV's own) | 'more' | 'most'
|
|
241
|
+
// + seek: how far LEFT / RIGHT jump (s); buffer: 'normal' (the TV's own) | 'more' | 'most';
|
|
242
|
+
// reconnect: a live channel that drops, 'off' | 'few' | 'always' (see player-core's liveDropped)
|
|
240
243
|
// (Settings › Playback › Seeking & buffering)
|
|
241
244
|
var SEEK_STEPS = [5, 10, 15, 30];
|
|
242
245
|
function playerCfg() {
|
|
243
246
|
var d = U.store.get(PLAYERCFG_KEY, {}) || {};
|
|
244
247
|
return { aspect: d.aspect || 'fit', seek: SEEK_STEPS.indexOf(+d.seek) !== -1 ? +d.seek : 10,
|
|
245
248
|
buffer: d.buffer === 'more' || d.buffer === 'most' ? d.buffer : 'normal',
|
|
249
|
+
reconnect: d.reconnect === 'off' || d.reconnect === 'always' ? d.reconnect : 'few',
|
|
246
250
|
phone: d.phone === 'infuse' || d.phone === 'link' ? d.phone : 'vlc' }; // (Continue on phone: what the QR opens)
|
|
247
251
|
}
|
|
248
252
|
function setPlayerCfg(cfg) { // (what's given changes; the rest stays)
|
|
@@ -251,6 +255,26 @@
|
|
|
251
255
|
U.store.set(PLAYERCFG_KEY, d);
|
|
252
256
|
}
|
|
253
257
|
function aspectDef(id) { for (var i = 0; i < ASPECTS.length; i++) { if (ASPECTS[i][0] === id) { return ASPECTS[i]; } } return ASPECTS[0]; }
|
|
258
|
+
// A live channel's own picture mode (picked while it played): { channel id: mode }, the last
|
|
259
|
+
// ASPECTS_KEEP; one set back to the usual mode isn't kept apart. Anything else: the usual one.
|
|
260
|
+
var ASPECTS_KEY = 'stremiok.aspects', ASPECTS_KEEP = 300;
|
|
261
|
+
function aspectChannel() { var S = P.S; return (S && S.isLive && S.ctx && S.ctx.id) ? S.ctx.id : ''; }
|
|
262
|
+
function aspectNow() {
|
|
263
|
+
var ch = aspectChannel(), own = ch ? (U.store.get(ASPECTS_KEY, {}) || {})[ch] : '';
|
|
264
|
+
return own && aspectDef(own)[0] === own ? own : playerCfg().aspect;
|
|
265
|
+
}
|
|
266
|
+
function setAspectNow(mode) {
|
|
267
|
+
var ch = aspectChannel();
|
|
268
|
+
if (!ch) { setPlayerCfg({ aspect: mode }); return; }
|
|
269
|
+
var all = U.store.get(ASPECTS_KEY, {}) || {};
|
|
270
|
+
delete all[ch];
|
|
271
|
+
if (mode !== playerCfg().aspect) {
|
|
272
|
+
all[ch] = mode;
|
|
273
|
+
var keys = Object.keys(all);
|
|
274
|
+
keys.slice(0, Math.max(0, keys.length - ASPECTS_KEEP)).forEach(function (k) { delete all[k]; }); // (the oldest picked)
|
|
275
|
+
}
|
|
276
|
+
U.store.set(ASPECTS_KEY, all);
|
|
277
|
+
}
|
|
254
278
|
|
|
255
279
|
// ----- for the other parts (and js/player.js) -----
|
|
256
280
|
P.watchedState = watchedState; P.isWatched = isWatched; P.watchedAt = watchedAt; P.watchedSince = watchedSince;
|
|
@@ -261,7 +285,7 @@
|
|
|
261
285
|
P.getResume = getResume; P.removeProgress = removeProgress; P.continueList = continueList;
|
|
262
286
|
P.rememberSub = rememberSub; P.recallSub = recallSub; P.rememberAudio = rememberAudio;
|
|
263
287
|
P.recallAudio = recallAudio; P.playerCfg = playerCfg; P.setPlayerCfg = setPlayerCfg;
|
|
264
|
-
P.aspectDef = aspectDef; P.SUB_FONTS = SUB_FONTS; P.SUB_COLORS = SUB_COLORS; P.SUB_BGS = SUB_BGS;
|
|
288
|
+
P.aspectDef = aspectDef; P.aspectNow = aspectNow; P.setAspectNow = setAspectNow; P.SUB_FONTS = SUB_FONTS; P.SUB_COLORS = SUB_COLORS; P.SUB_BGS = SUB_BGS;
|
|
265
289
|
P.SUB_EDGES = SUB_EDGES; P.SUB_POSITIONS = SUB_POSITIONS; P.ASPECTS = ASPECTS;
|
|
266
|
-
P.isPrivate = isPrivate; P.setPrivate = setPrivate;
|
|
290
|
+
P.isPrivate = isPrivate; P.setPrivate = setPrivate; P.privateNow = privateNow; P.subCfgVersion = subCfgVersion;
|
|
267
291
|
})(PlayerParts);
|