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/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
- function liveKeep(url, fix) {
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 (!known || !known.fix) { startAV(ctx.url, settle); return; }
1065
- ctx._liveChecked = true;
1066
- YTPlay.liveOpen(ctx.url, ctx._headers, true).then(function (r) {
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
- if (r && typeof r.url === 'string' && /^http:\/\/localhost:\d+\/live\//.test(r.url)) { ctx._relay = r.url; ctx._relayFor = ctx.url; }
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
- if (liveKnown(ctx.url)) { return; }
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
- liveKeep(url, !!r.fix);
1081
- if (!r.fix || typeof r.url !== 'string' || !/^http:\/\/localhost:\d+\/live\//.test(r.url) || S.ctx !== ctx || !S.active || ctx.url !== url) { return; }
1082
- ctx._relay = r.url; ctx._relayFor = url;
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);
@@ -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' }; // a second subtitle at the top (player-subs; Off by default)
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 (&amp; &gt; &#8212; …) 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
- var txt = skip(charCodes(lines.slice(ti + 1).join('\n').replace(/<[^>]+>/g, '').replace(/\{[^}]+\}/g, '')).replace(/\s+$/, ''));
250
+ // (tags out, a stray < with them, in one pass; then entities read: one written as &lt; 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').replace(/<[^>]+>/g, '').replace(/\{[^}]+\}/g, '').trim();
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(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim().slice(0, 70) + '”'; }
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
- function langMenu() {
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(langKey(textLang(tr)), { emb: tr, n: i }); });
572
- _subTracks.forEach(function (tr) { add(langKey(tr.lang), { track: tr }); });
573
- var curKey = _subOn ? langKey(_subLabel) : (_embSub != null ? langKey(_embLang) : null);
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 ? ', in the file' : ', ' + emb + ' in the file') : ''),
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);