stremiok 2.1.0 → 2.2.1
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 +97 -0
- package/README.md +10 -9
- package/config.xml +1 -1
- package/css/style.css +27 -0
- package/index.html +34 -31
- package/js/app.js +25 -10
- package/js/epg.js +320 -0
- package/js/iptv.js +198 -25
- package/js/keys.js +2 -0
- package/js/player-controls.js +50 -8
- package/js/player-core.js +112 -12
- package/js/player-lang.js +66 -5
- package/js/player-store.js +23 -2
- package/js/player-subs.js +57 -13
- package/js/stremio.js +70 -6
- package/js/trakt.js +1 -0
- package/js/util.js +24 -13
- package/js/view-core.js +128 -55
- package/js/view-detail.js +1 -1
- package/js/view-guide.js +371 -0
- package/js/view-home.js +148 -33
- package/js/view-settings.js +241 -90
- package/js/views.js +4 -3
- package/js/ytplay.js +53 -5
- package/package.json +2 -1
- package/service/index.js +863 -46
package/js/player-core.js
CHANGED
|
@@ -299,15 +299,29 @@ var PlayerParts = {};
|
|
|
299
299
|
} catch (er2) {}
|
|
300
300
|
return out;
|
|
301
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(); } }); }
|
|
302
306
|
function renderOSD() {
|
|
303
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); }
|
|
304
310
|
renderTags();
|
|
305
311
|
renderChapters();
|
|
306
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
|
+
}
|
|
307
323
|
U.byId('osd-bar').style.display = 'none';
|
|
308
|
-
var cur = U.byId('osd-cur');
|
|
309
324
|
cur.textContent = '● LIVE';
|
|
310
|
-
cur.className = 'live';
|
|
311
325
|
U.byId('osd-dur').textContent = '';
|
|
312
326
|
return;
|
|
313
327
|
}
|
|
@@ -331,6 +345,41 @@ var PlayerParts = {};
|
|
|
331
345
|
yt: page, ytLang: lang, type: 'YouTube', id: 'yt_id:' + link.id, parent: 'yt_id:' + link.id, name: link.title || 'YouTube',
|
|
332
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)
|
|
333
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
|
+
|
|
334
383
|
// Another source of what's playing (the player's source list): played in its place, at
|
|
335
384
|
// the same point, as a playback of its own — its quality ladder, YouTube details,
|
|
336
385
|
// captions, SponsorBlock sections, headers and file details are the new source's, and so
|
|
@@ -375,6 +424,7 @@ var PlayerParts = {};
|
|
|
375
424
|
S.active = true; S.cur = 0; S.dur = 0; _bg = null; S.actual = null;
|
|
376
425
|
// Stremio "tv" and "radio" types are strong live hints; refined after prepare.
|
|
377
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);
|
|
378
428
|
S.ctx.resumeSec = (ctx.trailer || ctx.fromStart) ? 0 : (ctx.resumeSec || getResume(ctx.type, ctx.id)); // (Play from start: 0:00)
|
|
379
429
|
|
|
380
430
|
// reset subtitle + audio + quality + on-screen controls state
|
|
@@ -1041,6 +1091,11 @@ var PlayerParts = {};
|
|
|
1041
1091
|
// cut that way, the channel restarts through the service's relay, which repacks the sound.
|
|
1042
1092
|
// The answer is kept per link (LIVE_KEY, LIVE_DAYS): a channel known to need it starts
|
|
1043
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.
|
|
1044
1099
|
var LIVE_KEY = 'stremiok.livefix', LIVE_KEEP = 300, LIVE_DAYS = 14;
|
|
1045
1100
|
function liveFixable(ctx) {
|
|
1046
1101
|
return useAV && S.isLive && !!ctx && !ctx.yt && !ctx.trailer && /^https?:\/\//i.test(ctx.url || '') &&
|
|
@@ -1050,22 +1105,45 @@ var PlayerParts = {};
|
|
|
1050
1105
|
var k = (U.store.get(LIVE_KEY, {}) || {})[url];
|
|
1051
1106
|
return (k && Date.now() - k.at < LIVE_DAYS * 86400000) ? k : null;
|
|
1052
1107
|
}
|
|
1053
|
-
|
|
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) {
|
|
1054
1114
|
var all = U.store.get(LIVE_KEY, {}) || {};
|
|
1055
1115
|
all[url] = { fix: fix ? 1 : 0, at: Date.now() };
|
|
1116
|
+
if (cc) { all[url].cc = cc; }
|
|
1056
1117
|
var keys = Object.keys(all);
|
|
1057
1118
|
if (keys.length > LIVE_KEEP) {
|
|
1058
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]; });
|
|
1059
1120
|
}
|
|
1060
1121
|
U.store.set(LIVE_KEY, all);
|
|
1061
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
|
+
}
|
|
1062
1138
|
function startLive(ctx, settle) {
|
|
1063
|
-
var known = liveFixable(ctx) ? liveKnown(ctx.url) : null;
|
|
1064
|
-
if (
|
|
1065
|
-
|
|
1066
|
-
|
|
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) {
|
|
1067
1145
|
if (S.ctx !== ctx || !S.active) { return; }
|
|
1068
|
-
|
|
1146
|
+
relayed(ctx, r, withCc);
|
|
1069
1147
|
startAV(ctx.url, settle);
|
|
1070
1148
|
}, function () { if (S.ctx === ctx && S.active) { startAV(ctx.url, settle); } });
|
|
1071
1149
|
}
|
|
@@ -1073,17 +1151,37 @@ var PlayerParts = {};
|
|
|
1073
1151
|
var ctx = S.ctx;
|
|
1074
1152
|
if (!ctx || ctx._liveChecked || !liveFixable(ctx)) { return; }
|
|
1075
1153
|
ctx._liveChecked = true;
|
|
1076
|
-
|
|
1154
|
+
var known = liveKnown(ctx.url);
|
|
1155
|
+
if (known && ccOf(known)) { return; }
|
|
1077
1156
|
var url = ctx.url;
|
|
1078
1157
|
YTPlay.liveOpen(url, ctx._headers, false).then(function (r) {
|
|
1079
1158
|
if (!r || !r.sure) { return; } // (couldn't tell: asked again next time)
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
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)
|
|
1083
1165
|
U.log('player', 'Live sound repacked by StremioK\'s service (' + (r.why || '') + ')');
|
|
1084
1166
|
reopen('Fixing this channel’s sound…');
|
|
1085
1167
|
}, function () {});
|
|
1086
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); };
|
|
1087
1185
|
// The relay didn't start (StremioK's service gone): the channel as it is.
|
|
1088
1186
|
function relayFailed() {
|
|
1089
1187
|
if (S.playStarted || !S.ctx || !S.ctx._relay) { return false; }
|
|
@@ -1181,6 +1279,7 @@ var PlayerParts = {};
|
|
|
1181
1279
|
function exit() {
|
|
1182
1280
|
if (!S.active) { return; }
|
|
1183
1281
|
if (U.crumb) { U.crumb('player closed'); }
|
|
1282
|
+
if (P.sleepClear) { P.sleepClear(); } // (the sleep timer: this playback's)
|
|
1184
1283
|
saveProgress();
|
|
1185
1284
|
// Record partial progress to Trakt (unfinished VOD) so it shows in Continue Watching. Past
|
|
1186
1285
|
// 85% (Trakt connected): finished, Trakt's way (it marks it watched), and here as at the credits.
|
|
@@ -1227,4 +1326,5 @@ var PlayerParts = {};
|
|
|
1227
1326
|
P.sponsorHighlight = sponsorHighlight;
|
|
1228
1327
|
P.chapters = chapters; P.chapterAt = chapterAt; P.chapterLabel = chapterLabel;
|
|
1229
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)
|
|
1230
1330
|
})(PlayerParts);
|
package/js/player-lang.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
fre: ['fre', 'fra', 'fr', 'french', 'français', 'francais'],
|
|
35
35
|
ger: ['ger', 'deu', 'de', 'german', 'deutsch'],
|
|
36
36
|
ita: ['ita', 'it', 'italian', 'italiano'],
|
|
37
|
-
por: ['por', 'pob', 'pt', 'portuguese', 'português', 'portugues'],
|
|
37
|
+
por: ['por', 'pob', 'pom', 'pt', 'portuguese', 'português', 'portugues'],
|
|
38
38
|
dut: ['dut', 'nld', 'nl', 'dutch', 'nederlands'],
|
|
39
39
|
rus: ['rus', 'ru', 'russian'],
|
|
40
40
|
ara: ['ara', 'ar', 'arabic'],
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
pol: ['pol', 'pl', 'polish', 'polski'],
|
|
43
43
|
jpn: ['jpn', 'ja', 'japanese', '日本語'],
|
|
44
44
|
kor: ['kor', 'ko', 'korean', '한국어'],
|
|
45
|
-
chi: ['chi', 'zho', 'zh', 'cmn', 'chinese', 'mandarin', '中文'],
|
|
45
|
+
chi: ['chi', 'zho', 'zh', 'cmn', 'zht', 'zhe', 'chinese', 'mandarin', '中文'], // (zht / zhe: OpenSubtitles' traditional and bilingual)
|
|
46
46
|
hin: ['hin', 'hi', 'hindi'],
|
|
47
47
|
swe: ['swe', 'sv', 'swedish', 'svenska'],
|
|
48
48
|
nor: ['nor', 'nob', 'nno', 'nb', 'nn', 'norwegian', 'norsk'],
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
ukr: ['ukr', 'uk', 'ukrainian'],
|
|
59
59
|
tha: ['tha', 'th', 'thai'],
|
|
60
60
|
vie: ['vie', 'vi', 'vietnamese'],
|
|
61
|
-
ind: ['ind', 'id', 'indonesian'],
|
|
61
|
+
ind: ['ind', 'id', 'indonesian', 'indonesia'], // ("Bahasa Indonesia": its last word)
|
|
62
62
|
may: ['may', 'msa', 'ms', 'malay'],
|
|
63
63
|
fil: ['fil', 'tgl', 'tl', 'filipino', 'tagalog'],
|
|
64
64
|
cat: ['cat', 'ca', 'catalan', 'català'],
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
glg: ['glg', 'gl', 'galician', 'galego'],
|
|
67
67
|
bul: ['bul', 'bg', 'bulgarian'],
|
|
68
68
|
hrv: ['hrv', 'hr', 'croatian', 'hrvatski'],
|
|
69
|
-
srp: ['srp', 'sr', 'serbian'],
|
|
69
|
+
srp: ['srp', 'scc', 'sr', 'serbian'],
|
|
70
70
|
per: ['per', 'fas', 'fa', 'persian', 'farsi'],
|
|
71
71
|
est: ['est', 'et', 'estonian'],
|
|
72
72
|
lav: ['lav', 'lv', 'latvian'],
|
|
@@ -74,7 +74,57 @@
|
|
|
74
74
|
ice: ['ice', 'isl', 'is', 'icelandic'],
|
|
75
75
|
tam: ['tam', 'ta', 'tamil'],
|
|
76
76
|
tel: ['tel', 'te', 'telugu'],
|
|
77
|
-
ben: ['ben', 'bn', 'bengali']
|
|
77
|
+
ben: ['ben', 'bn', 'bengali'],
|
|
78
|
+
// (2.2.0.2) the rest of OpenSubtitles' languages; two-letter codes that are also everyday
|
|
79
|
+
// words or labels (my, be, or, so, an, se, si, ne, mr, SD, BR…) left out
|
|
80
|
+
afr: ['afr', 'af', 'afrikaans'],
|
|
81
|
+
alb: ['alb', 'sqi', 'sq', 'albanian', 'shqip'],
|
|
82
|
+
arg: ['arg', 'aragonese'],
|
|
83
|
+
arm: ['arm', 'hye', 'hy', 'armenian'],
|
|
84
|
+
ast: ['ast', 'asturian'],
|
|
85
|
+
aze: ['aze', 'az', 'azerbaijani'],
|
|
86
|
+
bel: ['bel', 'belarusian'],
|
|
87
|
+
bos: ['bos', 'bs', 'bosnian'],
|
|
88
|
+
bre: ['bre', 'breton'],
|
|
89
|
+
bur: ['bur', 'mya', 'burmese'],
|
|
90
|
+
prs: ['prs', 'dari'],
|
|
91
|
+
epo: ['epo', 'eo', 'esperanto'],
|
|
92
|
+
ext: ['ext', 'extremaduran'],
|
|
93
|
+
gla: ['gla', 'gd', 'gaelic'],
|
|
94
|
+
geo: ['geo', 'kat', 'ka', 'georgian'],
|
|
95
|
+
ibo: ['ibo', 'ig', 'igbo'],
|
|
96
|
+
ina: ['ina', 'interlingua'],
|
|
97
|
+
gle: ['gle', 'ga', 'irish'],
|
|
98
|
+
kan: ['kan', 'kn', 'kannada'],
|
|
99
|
+
kaz: ['kaz', 'kk', 'kazakh'],
|
|
100
|
+
khm: ['khm', 'km', 'khmer'],
|
|
101
|
+
kur: ['kur', 'ku', 'kurdish'],
|
|
102
|
+
ltz: ['ltz', 'lb', 'luxembourgish'],
|
|
103
|
+
mac: ['mac', 'mkd', 'mk', 'macedonian'],
|
|
104
|
+
mal: ['mal', 'ml', 'malayalam'],
|
|
105
|
+
mni: ['mni', 'manipuri'],
|
|
106
|
+
mar: ['mar', 'marathi'],
|
|
107
|
+
mon: ['mon', 'mn', 'mongolian'],
|
|
108
|
+
mne: ['mne', 'montenegrin'],
|
|
109
|
+
nav: ['nav', 'nv', 'navajo'],
|
|
110
|
+
nep: ['nep', 'nepali'],
|
|
111
|
+
sme: ['sme', 'sami'],
|
|
112
|
+
oci: ['oci', 'oc', 'occitan'],
|
|
113
|
+
ori: ['ori', 'odia', 'oriya'],
|
|
114
|
+
pan: ['pan', 'pa', 'punjabi', 'panjabi'],
|
|
115
|
+
pus: ['pus', 'pashto'],
|
|
116
|
+
sat: ['sat', 'santali'],
|
|
117
|
+
snd: ['snd', 'sindhi'],
|
|
118
|
+
sin: ['sin', 'sinhala', 'sinhalese'],
|
|
119
|
+
som: ['som', 'somali'],
|
|
120
|
+
swa: ['swa', 'sw', 'swahili'],
|
|
121
|
+
syr: ['syr', 'syriac'],
|
|
122
|
+
tat: ['tat', 'tt', 'tatar'],
|
|
123
|
+
tok: ['tok', 'toki'],
|
|
124
|
+
tuk: ['tuk', 'tk', 'turkmen'],
|
|
125
|
+
urd: ['urd', 'ur', 'urdu'],
|
|
126
|
+
uzb: ['uzb', 'uz', 'uzbek'],
|
|
127
|
+
wel: ['wel', 'cym', 'cy', 'welsh']
|
|
78
128
|
};
|
|
79
129
|
function langMatches(pref, lang) {
|
|
80
130
|
var names = LANG_ALIASES[pref] || [pref];
|
|
@@ -97,6 +147,14 @@
|
|
|
97
147
|
per: 'Persian', fas: 'Persian', fa: 'Persian', slo: 'Slovak', slv: 'Slovenian', may: 'Malay', fil: 'Filipino',
|
|
98
148
|
baq: 'Basque', glg: 'Galician', est: 'Estonian', lav: 'Latvian', lit: 'Lithuanian', ice: 'Icelandic',
|
|
99
149
|
tam: 'Tamil', tel: 'Telugu', ben: 'Bengali', mul: 'Multiple languages', zxx: 'No language',
|
|
150
|
+
afr: 'Afrikaans', alb: 'Albanian', arg: 'Aragonese', arm: 'Armenian', ast: 'Asturian', aze: 'Azerbaijani',
|
|
151
|
+
bel: 'Belarusian', bos: 'Bosnian', bre: 'Breton', bur: 'Burmese', prs: 'Dari', epo: 'Esperanto', ext:
|
|
152
|
+
'Extremaduran', gla: 'Scottish Gaelic', geo: 'Georgian', ibo: 'Igbo', ina: 'Interlingua', gle: 'Irish', kan:
|
|
153
|
+
'Kannada', kaz: 'Kazakh', khm: 'Khmer', kur: 'Kurdish', ltz: 'Luxembourgish', mac: 'Macedonian', mal:
|
|
154
|
+
'Malayalam', mni: 'Manipuri', mar: 'Marathi', mon: 'Mongolian', mne: 'Montenegrin', nav: 'Navajo', nep:
|
|
155
|
+
'Nepali', sme: 'Northern Sami', oci: 'Occitan', ori: 'Odia', pan: 'Punjabi', pus: 'Pashto', sat: 'Santali',
|
|
156
|
+
snd: 'Sindhi', sin: 'Sinhala', som: 'Somali', swa: 'Swahili', syr: 'Syriac', tat: 'Tatar', tok: 'Toki Pona',
|
|
157
|
+
tuk: 'Turkmen', urd: 'Urdu', uzb: 'Uzbek', wel: 'Welsh',
|
|
100
158
|
und: 'Unknown language', '': 'Unknown language' };
|
|
101
159
|
function langName(lang) {
|
|
102
160
|
var k = langKey(lang);
|
|
@@ -122,6 +180,9 @@
|
|
|
122
180
|
// ----- for the other parts (and js/player.js) -----
|
|
123
181
|
P.trackInfo = trackInfo; P.infoLang = infoLang; P.trackLang = trackLang; P.langMatches = langMatches;
|
|
124
182
|
P.langName = langName; P.langRank = langRank; P.langKey = langKey;
|
|
183
|
+
// A track that doesn't say its language (by its key): none, "und", "unknown"…, or no letters.
|
|
184
|
+
// (A language StremioK has no name for is still a language.)
|
|
185
|
+
P.langUnstated = function (k) { k = ('' + (k || '')).trim(); return !/[a-zà-ÿ]/i.test(k) || /^(und|unk|unknown|unknown language|mul|zxx|n\/a|none)$/i.test(k); };
|
|
125
186
|
// A language code's two-letter form (ISO 639-1: "spa" -> "es"), or '' (TMDB asks by it).
|
|
126
187
|
P.lang2 = function (key) { var a = LANG_ALIASES[key] || []; for (var i = 0; i < a.length; i++) { if (/^[a-z]{2}$/.test(a[i])) { return a[i]; } } return ''; };
|
|
127
188
|
})(PlayerParts);
|
package/js/player-store.js
CHANGED
|
@@ -85,7 +85,8 @@
|
|
|
85
85
|
edge: pick3(SUB_EDGES, d.edge)[0], pos: pick3(SUB_POSITIONS, d.pos || 'bottom')[0], // (Bottom unless chosen: Lower comes first in the list)
|
|
86
86
|
font: subFontFamily(d.font) ? d.font : 'default', // an unknown font -> TV default
|
|
87
87
|
skip: skipWords(d.skip), // lines with one of these aren't shown (none by default: see player-subs' skipLines)
|
|
88
|
-
second: typeof d.second === 'string' && /^[a-z]{2,3}$/.test(d.second) ? d.second : 'off'
|
|
88
|
+
second: typeof d.second === 'string' && /^[a-z]{2,3}$/.test(d.second) ? d.second : 'off', // a second subtitle at the top (player-subs; Off by default)
|
|
89
|
+
only: d.only === 'on' ? 'on' : 'off' }; // the subtitle menu: only your languages (player-subs' langMenu; Off by default)
|
|
89
90
|
}
|
|
90
91
|
// Settings › Subtitles › Skip lines with: words (or bits, ".srt"), capitals or not; at most 20.
|
|
91
92
|
function skipWords(x) {
|
|
@@ -255,6 +256,26 @@
|
|
|
255
256
|
U.store.set(PLAYERCFG_KEY, d);
|
|
256
257
|
}
|
|
257
258
|
function aspectDef(id) { for (var i = 0; i < ASPECTS.length; i++) { if (ASPECTS[i][0] === id) { return ASPECTS[i]; } } return ASPECTS[0]; }
|
|
259
|
+
// A live channel's own picture mode (picked while it played): { channel id: mode }, the last
|
|
260
|
+
// ASPECTS_KEEP; one set back to the usual mode isn't kept apart. Anything else: the usual one.
|
|
261
|
+
var ASPECTS_KEY = 'stremiok.aspects', ASPECTS_KEEP = 300;
|
|
262
|
+
function aspectChannel() { var S = P.S; return (S && S.isLive && S.ctx && S.ctx.id) ? S.ctx.id : ''; }
|
|
263
|
+
function aspectNow() {
|
|
264
|
+
var ch = aspectChannel(), own = ch ? (U.store.get(ASPECTS_KEY, {}) || {})[ch] : '';
|
|
265
|
+
return own && aspectDef(own)[0] === own ? own : playerCfg().aspect;
|
|
266
|
+
}
|
|
267
|
+
function setAspectNow(mode) {
|
|
268
|
+
var ch = aspectChannel();
|
|
269
|
+
if (!ch) { setPlayerCfg({ aspect: mode }); return; }
|
|
270
|
+
var all = U.store.get(ASPECTS_KEY, {}) || {};
|
|
271
|
+
delete all[ch];
|
|
272
|
+
if (mode !== playerCfg().aspect) {
|
|
273
|
+
all[ch] = mode;
|
|
274
|
+
var keys = Object.keys(all);
|
|
275
|
+
keys.slice(0, Math.max(0, keys.length - ASPECTS_KEEP)).forEach(function (k) { delete all[k]; }); // (the oldest picked)
|
|
276
|
+
}
|
|
277
|
+
U.store.set(ASPECTS_KEY, all);
|
|
278
|
+
}
|
|
258
279
|
|
|
259
280
|
// ----- for the other parts (and js/player.js) -----
|
|
260
281
|
P.watchedState = watchedState; P.isWatched = isWatched; P.watchedAt = watchedAt; P.watchedSince = watchedSince;
|
|
@@ -265,7 +286,7 @@
|
|
|
265
286
|
P.getResume = getResume; P.removeProgress = removeProgress; P.continueList = continueList;
|
|
266
287
|
P.rememberSub = rememberSub; P.recallSub = recallSub; P.rememberAudio = rememberAudio;
|
|
267
288
|
P.recallAudio = recallAudio; P.playerCfg = playerCfg; P.setPlayerCfg = setPlayerCfg;
|
|
268
|
-
P.aspectDef = aspectDef; P.SUB_FONTS = SUB_FONTS; P.SUB_COLORS = SUB_COLORS; P.SUB_BGS = SUB_BGS;
|
|
289
|
+
P.aspectDef = aspectDef; P.aspectNow = aspectNow; P.setAspectNow = setAspectNow; P.SUB_FONTS = SUB_FONTS; P.SUB_COLORS = SUB_COLORS; P.SUB_BGS = SUB_BGS;
|
|
269
290
|
P.SUB_EDGES = SUB_EDGES; P.SUB_POSITIONS = SUB_POSITIONS; P.ASPECTS = ASPECTS;
|
|
270
291
|
P.isPrivate = isPrivate; P.setPrivate = setPrivate; P.privateNow = privateNow; P.subCfgVersion = subCfgVersion;
|
|
271
292
|
})(PlayerParts);
|
package/js/player-subs.js
CHANGED
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
if (mem.off) { return true; }
|
|
114
114
|
if (mem.embedded) {
|
|
115
115
|
if (partial) { return false; }
|
|
116
|
-
wantEmbedded({ index: mem.index, lang: mem.lang }); _embDelay = Math.max(0, +mem.delay || 0); return true;
|
|
116
|
+
wantEmbedded({ index: mem.index, lang: mem.lang, nth: mem.nth }); _embDelay = Math.max(0, +mem.delay || 0); return true;
|
|
117
117
|
}
|
|
118
118
|
var exact =
|
|
119
119
|
(mem.trackId != null ? _subTracks.find(function (t) { return t.id === mem.trackId; }) : null) ||
|
|
@@ -202,6 +202,21 @@
|
|
|
202
202
|
var WEBVTT_ASIDE = /^(NOTE|STYLE|REGION)([ \t]|$)/;
|
|
203
203
|
// Character codes (& > — …) as the characters they stand for.
|
|
204
204
|
var CHAR_CODES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: '\u00a0', lrm: '\u200e', rlm: '\u200f' };
|
|
205
|
+
// A subtitle's tags taken out, a stray < with them: one pass, in time with the text's length
|
|
206
|
+
// however it's made (a run of < with no > after it used to hold the remote for seconds).
|
|
207
|
+
// Its text is only ever shown as text (textContent): this is for how it reads.
|
|
208
|
+
function noTags(t) {
|
|
209
|
+
var out = '', i = 0, n = t.length;
|
|
210
|
+
while (i < n) {
|
|
211
|
+
var lt = t.indexOf('<', i);
|
|
212
|
+
if (lt === -1) { out += t.slice(i); break; }
|
|
213
|
+
out += t.slice(i, lt);
|
|
214
|
+
var gt = t.indexOf('>', lt + 1);
|
|
215
|
+
if (gt === -1) { out += t.slice(lt + 1).split('<').join(''); break; } // (no tag ends after it: the rest, its < gone)
|
|
216
|
+
i = gt + 1;
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
205
220
|
function charCodes(t) {
|
|
206
221
|
if (t.indexOf('&') === -1) { return t; }
|
|
207
222
|
return t.replace(/&(#[xX][0-9a-fA-F]{1,6}|#[0-9]{1,7}|[a-zA-Z]+);/g, function (all, c) {
|
|
@@ -213,13 +228,16 @@
|
|
|
213
228
|
return CHAR_CODES.hasOwnProperty(c) ? CHAR_CODES[c] : all;
|
|
214
229
|
});
|
|
215
230
|
}
|
|
231
|
+
// A file's cues: CUES_MAX at most, CUE_TEXT_MAX characters of text each (a broken or hostile
|
|
232
|
+
// file can't hold the remote: the whole parse stays in time with the file's length).
|
|
233
|
+
var CUES_MAX = 20000, CUE_TEXT_MAX = 1000;
|
|
216
234
|
function parseCues(text) {
|
|
217
235
|
text = ('' + text).replace(/\r/g, '');
|
|
218
236
|
var blocks = text.split(/\n\n+/);
|
|
219
237
|
var cues = [], skip = skipper(); // (Skip lines with: your words)
|
|
220
238
|
var re = /(?:(\d{1,3}):)?(\d{2}):(\d{2})[,.](\d{1,3})\s*-->\s*(?:(\d{1,3}):)?(\d{2}):(\d{2})[,.](\d{1,3})/;
|
|
221
239
|
function ms(x) { return +((x + '00').slice(0, 3)) / 1000; } // (".5" is half a second)
|
|
222
|
-
for (var i = 0; i < blocks.length; i++) {
|
|
240
|
+
for (var i = 0; i < blocks.length && cues.length < CUES_MAX; i++) {
|
|
223
241
|
var lines = blocks[i].replace(/^\n+/, '').split('\n');
|
|
224
242
|
if (WEBVTT_ASIDE.test(lines[0])) { continue; } // (a WebVTT comment, style or region: not shown)
|
|
225
243
|
var ti = -1;
|
|
@@ -229,7 +247,9 @@
|
|
|
229
247
|
if (!m) { continue; }
|
|
230
248
|
var start = (+(m[1] || 0)) * 3600 + (+m[2]) * 60 + (+m[3]) + ms(m[4]);
|
|
231
249
|
var end = (+(m[5] || 0)) * 3600 + (+m[6]) * 60 + (+m[7]) + ms(m[8]);
|
|
232
|
-
|
|
250
|
+
// (tags out, a stray < with them, in one pass; then entities read: one written as < is the file's own text,
|
|
251
|
+
// "<b>" shown as written. Never a tag: subtitles reach the screen as text only, textContent)
|
|
252
|
+
var txt = skip(charCodes(noTags(lines.slice(ti + 1).join('\n').slice(0, CUE_TEXT_MAX)).replace(/\{[^}]+\}/g, '')).replace(/\s+$/, ''));
|
|
233
253
|
if (txt) { cues.push({ start: start, end: end, text: txt }); }
|
|
234
254
|
}
|
|
235
255
|
cues.sort(function (a, b) { return a.start - b.start; });
|
|
@@ -384,6 +404,7 @@
|
|
|
384
404
|
pick = tracks.find(function (t) { return t.index === w.index && (!key || langKey(textLang(t)) === key); }) || null;
|
|
385
405
|
}
|
|
386
406
|
if (!pick && key) { pick = tracks.find(function (t) { return langMatches(key, textLang(t)); }) || null; }
|
|
407
|
+
if (!pick && w.nth != null && S.ctx && S.ctx._relayCc) { pick = tracks[w.nth] || null; } // (a live channel's closed captions: in the order asked for)
|
|
387
408
|
if (!pick && w.lang2) { // nothing in the default language anywhere: the second choice, a track to download first
|
|
388
409
|
var key2 = langKey(w.lang2);
|
|
389
410
|
var dl = _subTracks.find(function (t) { return langMatches(key2, t.lang); });
|
|
@@ -398,7 +419,7 @@
|
|
|
398
419
|
try { avplay.setSelectTrack('TEXT', tr.index); }
|
|
399
420
|
catch (e2) { try { avplay.setSelectTrack('TEXT', '' + tr.index); } catch (e3) {} }
|
|
400
421
|
_embSub = tr.index; _embLang = textLang(tr);
|
|
401
|
-
U.toast('Subtitles: ' + (_embLang || 'on') + ' (built-in)', 1800);
|
|
422
|
+
U.toast('Subtitles: ' + (_embLang || 'on') + (S.ctx && S.ctx._relayCc ? ' (closed captions)' : ' (built-in)'), 1800);
|
|
402
423
|
}
|
|
403
424
|
function embOff() {
|
|
404
425
|
if (_embSub != null) { try { avplay.setSilentSubtitle(true); } catch (e) {} }
|
|
@@ -446,7 +467,7 @@
|
|
|
446
467
|
function drawEmbedded(text, duration) {
|
|
447
468
|
if (_embSub == null || _subOn) { return; }
|
|
448
469
|
if (_embTimer) { clearTimeout(_embTimer); _embTimer = null; }
|
|
449
|
-
var t = ('' + (text || '')).replace(/<br\s*\/?>/gi, '\n')
|
|
470
|
+
var t = noTags(('' + (text || '')).replace(/<br\s*\/?>/gi, '\n')).replace(/\{[^}]+\}/g, '').trim();
|
|
450
471
|
if (!t) { S.subsEl.className = 'hidden'; return; }
|
|
451
472
|
S.subsEl.className = '';
|
|
452
473
|
var span = S.subsEl.firstChild;
|
|
@@ -515,7 +536,7 @@
|
|
|
515
536
|
replaceTop(timingMenu(topMenu().idx));
|
|
516
537
|
U.toast('Subtitles: ' + fmtDelay(_subDelay) + (_subDelay > 0 ? ' (later)' : _subDelay < 0 ? ' (earlier)' : ' (as in the file)'), 1800);
|
|
517
538
|
}
|
|
518
|
-
function lineText(c) { return '“' + ('' + c.text).replace(
|
|
539
|
+
function lineText(c) { return '“' + noTags('' + c.text).replace(/\s+/g, ' ').trim().slice(0, 70) + '”'; }
|
|
519
540
|
// The next line coming up (as timed now): OK the moment it's said.
|
|
520
541
|
function earMenu() {
|
|
521
542
|
var t = S.cur - _subDelay, next = null;
|
|
@@ -562,17 +583,32 @@
|
|
|
562
583
|
// Level 1: one entry per language, spellings merged ("en", "eng" and "English" are one
|
|
563
584
|
// group), named in full, with the file's own tracks in the same groups as the addons'.
|
|
564
585
|
// Order: the language showing now, the preferred language, then by name.
|
|
565
|
-
|
|
586
|
+
// Only my languages (Settings, Off by default): just your default, second-choice and
|
|
587
|
+
// second-subtitle languages, the one showing, and tracks that don't say their language;
|
|
588
|
+
// Other languages, after them, lists the rest (all: that list).
|
|
589
|
+
function langMenu(all) {
|
|
566
590
|
var groups = Object.create(null), order = []; // (no inherited names: a language an addon calls "__proto__" is just a key)
|
|
591
|
+
function key(l) { var k = langKey(l); return P.langUnstated(k) ? '' : k; } // (no language, "und"…: one Unknown language)
|
|
567
592
|
function add(k, item) {
|
|
568
593
|
if (!groups[k]) { groups[k] = []; order.push(k); }
|
|
569
594
|
groups[k].push(item);
|
|
570
595
|
}
|
|
571
|
-
getTextTracks().forEach(function (tr, i) { add(
|
|
572
|
-
|
|
573
|
-
|
|
596
|
+
getTextTracks().forEach(function (tr, i) { add(key(textLang(tr)), { emb: tr, n: i }); });
|
|
597
|
+
var offer = S.ctx && !S.ctx._relayCc ? S.ctx._ccOffer : null; // (a live channel's closed captions, not on yet)
|
|
598
|
+
(offer || []).forEach(function (c, i) { add(key(c.lang || ''), { cc: c, n: i, emb: true }); });
|
|
599
|
+
_subTracks.forEach(function (tr) { add(key(tr.lang), { track: tr }); });
|
|
600
|
+
var curKey = _subOn ? key(_subLabel) : (_embSub != null ? key(_embLang) : null);
|
|
574
601
|
var pref = getSubCfg().lang;
|
|
575
602
|
var prefs = [curKey, pref].filter(function (x) { return x && x !== 'off'; });
|
|
603
|
+
var cfg = getSubCfg(), hidden = 0;
|
|
604
|
+
var mine = [cfg.lang, cfg.lang2, cfg.second].filter(function (l) { return l && l !== 'off' && l !== 'none'; });
|
|
605
|
+
if (cfg.only === 'on' && !all && mine.length) {
|
|
606
|
+
order = order.filter(function (k) {
|
|
607
|
+
var keep = k === curKey || k === '' || mine.some(function (l) { return langKey(l) === k || langMatches(l, k); });
|
|
608
|
+
if (!keep) { hidden++; }
|
|
609
|
+
return keep;
|
|
610
|
+
});
|
|
611
|
+
}
|
|
576
612
|
order.sort(function (a, b) {
|
|
577
613
|
var ra = langRank(a, prefs), rb = langRank(b, prefs);
|
|
578
614
|
if (ra !== rb) { return ra - rb; }
|
|
@@ -583,17 +619,19 @@
|
|
|
583
619
|
order.forEach(function (k) {
|
|
584
620
|
var list = groups[k], n = list.length, emb = list.filter(function (x) { return x.emb; }).length;
|
|
585
621
|
items.push({ label: langName(k),
|
|
586
|
-
detail: n + ' option' + (n > 1 ? 's' : '') + (emb ? (emb === n ? ',
|
|
622
|
+
detail: n + ' option' + (n > 1 ? 's' : '') + (emb ? (emb === n ? ', ' + inFile() : ', ' + emb + ' ' + inFile()) : ''),
|
|
587
623
|
sel: (k === curKey),
|
|
588
624
|
run: function () { pushMenu(langTracksMenu(k, list)); } });
|
|
589
625
|
});
|
|
626
|
+
if (hidden) { items.push({ label: 'Other languages', detail: hidden + ' more', run: function () { pushMenu(langMenu(true)); } }); }
|
|
590
627
|
var sec = getSubCfg().second;
|
|
591
628
|
if (_subTracks.length) { items.push({ label: 'Second subtitle: ' + (sec === 'off' ? 'Off' : langName(sec)), detail: 'At the top, at the same time', run: function () { pushMenu(secondMenu()); } }); }
|
|
592
629
|
var idx = 0;
|
|
593
630
|
for (var i = 0; i < items.length; i++) { if (items[i].sel) { idx = i; break; } }
|
|
594
|
-
return { title: 'Subtitle language', items: items, idx: idx, rebuild: langMenu };
|
|
631
|
+
return { title: all ? 'All subtitle languages' : 'Subtitle language', items: items, idx: idx, rebuild: function () { return langMenu(all); } };
|
|
595
632
|
}
|
|
596
633
|
|
|
634
|
+
function inFile() { return S.ctx && (S.ctx._relayCc || S.ctx._ccOffer) ? 'closed captions' : 'in the file'; }
|
|
597
635
|
// Level 2: the tracks in one language: the file's own first, then each addon's.
|
|
598
636
|
// Picking a track keeps the menu open, the checkmark on it (an addon's once it loads), so
|
|
599
637
|
// several can be tried in a row: the file's own the same as an addon's.
|
|
@@ -601,10 +639,15 @@
|
|
|
601
639
|
var idx = 0, nAddon = 0;
|
|
602
640
|
list = list.filter(function (x) { return x.emb; }).concat(list.filter(function (x) { return !x.emb; }));
|
|
603
641
|
var items = list.map(function (x, i) {
|
|
642
|
+
if (x.cc) { // (turning a live channel's closed captions on: it restarts with them)
|
|
643
|
+
var c = x.cc;
|
|
644
|
+
return { label: 'Closed captions (' + c.ch + ')', detail: 'The channel restarts with them',
|
|
645
|
+
run: function () { closeMenu(); rememberSub(S.ctx.id, { embedded: true, nth: x.n, lang: c.lang }); P.ccOn(c); } };
|
|
646
|
+
}
|
|
604
647
|
if (x.emb) {
|
|
605
648
|
var tr = x.emb, l = textLang(tr);
|
|
606
649
|
if (_embSub === tr.index) { idx = i; }
|
|
607
|
-
return { label: 'Built into the file' + (l ? ' (' + l + ')' : ' · track ' + (x.n + 1)), sel: _embSub === tr.index,
|
|
650
|
+
return { label: (S.ctx && S.ctx._relayCc ? 'Closed captions' : 'Built into the file') + (l ? ' (' + l + ')' : ' · track ' + (x.n + 1)), sel: _embSub === tr.index,
|
|
608
651
|
run: function () {
|
|
609
652
|
_embDelay = 0; _embQueue = []; _embNudge = null; selectEmbeddedSub(tr); rememberSub(S.ctx.id, { embedded: true, index: tr.index, lang: l });
|
|
610
653
|
if (menuOpen()) { replaceTop(langTracksMenu(key, list)); }
|
|
@@ -690,5 +733,6 @@
|
|
|
690
733
|
P.fileHints = fileHints; P.parseCues = parseCues; P.loadTrack = loadTrack; P.subsOff = subsOff;
|
|
691
734
|
P.resetSubs = resetSubs; P.closeSubs = closeSubs; P.startSubtitles = startSubtitles;
|
|
692
735
|
P.subsBeforeOpen = subsBeforeOpen; P.subsOnTick = subsOnTick; P.subtitleSummary = subtitleSummary;
|
|
736
|
+
P.wantEmbedded = wantEmbedded;
|
|
693
737
|
P.renderSubs = renderSubs; P.showEmbeddedText = showEmbeddedText; P.subConfigMenu = subConfigMenu; P.langMenu = langMenu;
|
|
694
738
|
})(PlayerParts);
|