stremiok 2.0.0 → 2.1.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/README.md CHANGED
@@ -75,8 +75,10 @@ a direct HTTP(S)/HLS/DASH URL, and this app embeds no torrent or debrid engine.
75
75
 
76
76
  The **IPTV** feature likewise hosts no content: you supply your own M3U playlist URLs. For
77
77
  YouTube entries, the bundled service finds the video's stream; the video itself comes
78
- straight from YouTube. Nothing is saved anywhere unless you download a source to your own USB
79
- drive.
78
+ straight from YouTube. No video or audio is kept: everything plays straight from its source,
79
+ unless you download it to your own USB drive. What StremioK does keep, on the TV, is your
80
+ progress, settings and copies that let it start faster (see
81
+ [Privacy and data](docs/PRIVACY.md#what-stays-on-the-tv)).
80
82
 
81
83
  ## Known limitations
82
84
 
package/config.xml CHANGED
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets"
3
- id="http://stremiok.tizen/StremioKTizen" version="2.0.0" viewmodes="maximized">
3
+ id="http://stremiok.tizen/StremioKTizen" version="2.1.0" viewmodes="maximized">
4
4
  <tizen:application id="StremioK01.StremioK" package="StremioK01" required_version="3.0"/>
5
5
  <content src="index.html"/>
6
6
  <feature name="http://tizen.org/feature/screen.size.all"/>
package/css/style.css CHANGED
@@ -40,7 +40,7 @@ html, body {
40
40
  .header .spacer { flex: 1; }
41
41
  .header-title { font-size: 36px; font-weight: bold; }
42
42
  /* Search and Settings: pills like the tabs (the top menu fits every tab, Local included, at 1920 px) */
43
- .header .hchip { display: inline-flex; align-items: center; font-size: 22px; padding: 8px 13px; margin-right: 6px; border-radius: 28px; background: #1a1f2b; color: #aeb6c4; border: 4px solid transparent; white-space: nowrap; }
43
+ .header .hchip { display: inline-flex; align-items: center; font-size: 22px; padding: 8px 9px; margin-right: 5px; border-radius: 28px; background: #1a1f2b; color: #aeb6c4; border: 4px solid transparent; white-space: nowrap; }
44
44
  .header .hchip.focusable.focused { background: #818cf8; color: #0e1117; border-color: #fff; opacity: 1; }
45
45
 
46
46
  /* --- Rows --- */
@@ -335,6 +335,13 @@ html, body {
335
335
  .playlist-row .playlist-item { flex: 1; margin-bottom: 0; }
336
336
  .feed-off .playlist-item { opacity: 0.5; }
337
337
  .playlist-item .addon-said:empty { display: none; }
338
+ /* an addon's logo (Settings › Content › Addons): its initial until the logo has loaded, then the logo over it */
339
+ .addon-item.has-logo { position: relative; padding-left: 92px; min-height: 56px; }
340
+ .addon-logo { position: absolute; left: 18px; top: 16px; width: 56px; height: 56px; border-radius: 8px; overflow: hidden; background: #0d1119; color: #8b93a5; font-size: 26px; font-weight: bold; line-height: 56px; text-align: center; word-break: normal; }
341
+ .addon-logo img { position: absolute; left: 0; top: 0; width: 100%; height: 100%; object-fit: contain; visibility: hidden; }
342
+ .addon-logo:before { content: attr(data-initial); }
343
+ .addon-logo.loaded:before { visibility: hidden; }
344
+ .addon-logo.loaded img { visibility: visible; }
338
345
  /* list picker (single choice) */
339
346
  .pick-box { width: 720px; text-align: left; }
340
347
  .pick-box .modal-msg { text-align: left; margin-bottom: 20px; }
@@ -367,12 +374,13 @@ html, body {
367
374
 
368
375
  /* Home top row: tab pills + Search + Settings on the left, StremioK logo on the right */
369
376
  .home-header .tabbar-inline { display: flex; align-items: center; }
370
- .tab-pill { display: inline-flex; align-items: center; font-size: 22px; padding: 8px 13px; margin-right: 6px; border-radius: 28px; background: #1a1f2b; color: #aeb6c4; border: 4px solid transparent; white-space: nowrap; }
377
+ .tab-pill { display: inline-flex; align-items: center; font-size: 22px; padding: 8px 9px; margin-right: 5px; border-radius: 28px; background: #1a1f2b; color: #aeb6c4; border: 4px solid transparent; white-space: nowrap; }
371
378
  .tab-ic { margin-left: 6px; font-size: 22px; }
372
379
  .tab-pill.active { background: #2a3346; color: #fff; }
373
380
  .tab-pill.focusable.focused { background: #818cf8; color: #0e1117; border-color: #fff; }
374
381
  .home-header .hchip.active { background: #2a3346; color: #fff; opacity: 1; }
375
382
  .row-empty { margin: 50px 60px; font-size: 26px; color: #8b93a3; }
383
+ .row-loading .row-empty { margin: 10px 60px 26px 60px; font-size: 22px; }
376
384
 
377
385
  /* --- Subtitle fonts (bundled; SIL Open Font License, see css/fonts/OFL-*.txt) ---
378
386
  Latin, Latin-extended and Cyrillic subsets; unicode-range makes the webview
package/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=1920, height=1080, user-scalable=no">
6
6
  <title>StremioK</title>
7
- <link rel="stylesheet" href="css/style.css?v=2.0.0">
7
+ <link rel="stylesheet" href="css/style.css?v=2.1.0">
8
8
  </head>
9
9
  <body>
10
10
  <!-- CSS-applied sentinel (kept off-screen); app.js waits for its width before rendering -->
@@ -52,35 +52,35 @@
52
52
 
53
53
  <!-- Load order matters: polyfills -> util -> iptv -> data -> input -> tvapps -> player parts -> player -> ytplay -> view parts -> views -> app -->
54
54
  <!-- ?v= busts the TV webview cache on every release (keep in sync with package.json) -->
55
- <script src="js/polyfills.js?v=2.0.0"></script>
56
- <script src="js/util.js?v=2.0.0"></script>
57
- <script src="js/iptv.js?v=2.0.0"></script>
58
- <script src="js/youtube.js?v=2.0.0"></script>
59
- <script src="js/stremio.js?v=2.0.0"></script>
60
- <script src="js/trakt.js?v=2.0.0"></script>
61
- <script src="js/keys.js?v=2.0.0"></script>
62
- <script src="js/focus.js?v=2.0.0"></script>
63
- <script src="js/tvapps.js?v=2.0.0"></script>
64
- <script src="js/player-core.js?v=2.0.0"></script>
65
- <script src="js/player-store.js?v=2.0.0"></script>
66
- <script src="js/player-lang.js?v=2.0.0"></script>
67
- <script src="js/player-quality.js?v=2.0.0"></script>
68
- <script src="js/player-subs.js?v=2.0.0"></script>
69
- <script src="js/player-audio.js?v=2.0.0"></script>
70
- <script src="js/player-controls.js?v=2.0.0"></script>
71
- <script src="js/player.js?v=2.0.0"></script>
72
- <script src="js/ytplay.js?v=2.0.0"></script>
73
- <script src="js/local.js?v=2.0.0"></script>
74
- <script src="js/downloads.js?v=2.0.0"></script>
75
- <script src="js/stremio-account.js?v=2.0.0"></script>
76
- <script src="js/view-core.js?v=2.0.0"></script>
77
- <script src="js/view-home.js?v=2.0.0"></script>
78
- <script src="js/view-detail.js?v=2.0.0"></script>
79
- <script src="js/view-search.js?v=2.0.0"></script>
80
- <script src="js/view-settings.js?v=2.0.0"></script>
81
- <script src="js/view-discover.js?v=2.0.0"></script>
82
- <script src="js/views.js?v=2.0.0"></script>
83
- <script src="js/app.js?v=2.0.0"></script>
84
- <script src="js/hubpreview.js?v=2.0.0"></script>
55
+ <script src="js/polyfills.js?v=2.1.0"></script>
56
+ <script src="js/util.js?v=2.1.0"></script>
57
+ <script src="js/iptv.js?v=2.1.0"></script>
58
+ <script src="js/youtube.js?v=2.1.0"></script>
59
+ <script src="js/stremio.js?v=2.1.0"></script>
60
+ <script src="js/trakt.js?v=2.1.0"></script>
61
+ <script src="js/keys.js?v=2.1.0"></script>
62
+ <script src="js/focus.js?v=2.1.0"></script>
63
+ <script src="js/tvapps.js?v=2.1.0"></script>
64
+ <script src="js/player-core.js?v=2.1.0"></script>
65
+ <script src="js/player-store.js?v=2.1.0"></script>
66
+ <script src="js/player-lang.js?v=2.1.0"></script>
67
+ <script src="js/player-quality.js?v=2.1.0"></script>
68
+ <script src="js/player-subs.js?v=2.1.0"></script>
69
+ <script src="js/player-audio.js?v=2.1.0"></script>
70
+ <script src="js/player-controls.js?v=2.1.0"></script>
71
+ <script src="js/player.js?v=2.1.0"></script>
72
+ <script src="js/ytplay.js?v=2.1.0"></script>
73
+ <script src="js/local.js?v=2.1.0"></script>
74
+ <script src="js/downloads.js?v=2.1.0"></script>
75
+ <script src="js/stremio-account.js?v=2.1.0"></script>
76
+ <script src="js/view-core.js?v=2.1.0"></script>
77
+ <script src="js/view-home.js?v=2.1.0"></script>
78
+ <script src="js/view-detail.js?v=2.1.0"></script>
79
+ <script src="js/view-search.js?v=2.1.0"></script>
80
+ <script src="js/view-settings.js?v=2.1.0"></script>
81
+ <script src="js/view-discover.js?v=2.1.0"></script>
82
+ <script src="js/views.js?v=2.1.0"></script>
83
+ <script src="js/app.js?v=2.1.0"></script>
84
+ <script src="js/hubpreview.js?v=2.1.0"></script>
85
85
  </body>
86
86
  </html>
package/js/downloads.js CHANGED
@@ -13,7 +13,8 @@ var Downloads = (function () {
13
13
  return typeof YTPlay !== 'undefined' && YTPlay.installMode() === 'wgt' && typeof LocalMedia !== 'undefined' && LocalMedia.present && LocalMedia.present();
14
14
  }
15
15
  function kept() { var l = U.store.get(DL_KEY, []); return Array.isArray(l) ? l : []; }
16
- function keep(l) { U.store.set(DL_KEY, l.slice(-MAX)); }
16
+ var _keptJson = null; // (what was last written: the list polled every few seconds is written only when it changed)
17
+ function keep(l) { var v = l.slice(-MAX), j = JSON.stringify(v); if (j === _keptJson) { return; } _keptJson = j; U.store.set(DL_KEY, v); }
17
18
  function keyOf(x) { return x.dir + '/' + x.name; }
18
19
  // The same file, renamed or not ("Film.mkv", "Film (k3x9).mkv"): its folder and its name's stem.
19
20
  function sameFile(y, k) {
@@ -55,11 +56,13 @@ var Downloads = (function () {
55
56
  });
56
57
  _synced = true;
57
58
  emit();
58
- return U.settleAll(again.map(function (k) { return function () { return send(k); }; }), 1).then(function () { return _items; });
59
+ return U.settleAll(again.map(function (k) { return function () { return send(k, true); }; }), 1).then(function () { return _items; });
59
60
  }, function () { return _items; });
60
61
  }
61
- function send(k) {
62
- return ask('/fsdl/add?' + qs({ dir: k.dir, name: k.name, url: k.url, ua: k.ua, ref: k.ref, size: k.size, title: k.title }), 20000).then(function (x) {
62
+ // again: the service no longer knows it (it restarted): it checks the drive first, so one it had
63
+ // finished isn't downloaded a second time.
64
+ function send(k, again) {
65
+ return ask('/fsdl/add?' + qs({ dir: k.dir, name: k.name, url: k.url, ua: k.ua, ref: k.ref, size: k.size, title: k.title, again: again ? '1' : '' }), 20000).then(function (x) {
63
66
  var mine = kept();
64
67
  mine.forEach(function (y) { if (keyOf(y) === keyOf(k)) { y.id = x.id; y.state = x.state; y.err = ''; } });
65
68
  keep(mine);
@@ -129,6 +132,14 @@ var Downloads = (function () {
129
132
 
130
133
  // ---- starting one, from a title's source (view-detail's source menu) ----
131
134
  var EXT = /\.(mkv|mp4|m4v|avi|ts|webm|mov|wmv|mpe?g)$/i;
135
+ // Only a file can be downloaded: a link (http/https) that isn't a streamed playlist (HLS
136
+ // .m3u8, DASH .mpd), which would save just the list, not the video. (One whose link doesn't
137
+ // say is checked by StremioK's service, by what the server answers.)
138
+ function isFileLink(stream) {
139
+ var u = '' + ((stream && stream.url) || ''), path = u.split(/[?#]/)[0], query = u.split('#')[0].split('?')[1] || '';
140
+ if (!/^https?:\/\//i.test(u)) { return false; }
141
+ return !/\.(m3u8?|mpd)$/i.test(path) && !/(^|&)(type|format|ext|output)=(m3u8|hls|mpd|dash)(&|$)/i.test(query);
142
+ }
132
143
  function fileNameFor(stream, label) {
133
144
  var bh = stream.behaviorHints || {}, path = ('' + (stream.url || '')).split(/[?#]/)[0];
134
145
  var named = typeof bh.filename === 'string' && EXT.test(bh.filename) ? bh.filename : '';
@@ -150,6 +161,7 @@ var Downloads = (function () {
150
161
  // ui: { pick(title, options, onPick), confirm(msg, ok, fn) } (the views' popups)
151
162
  function start(stream, t, ui) {
152
163
  if (!available()) { U.toast('Plug a USB drive into the TV first'); return; }
164
+ if (!isFileLink(stream)) { U.toast('This source is a streamed playlist (HLS or DASH), not a file: it can’t be downloaded. Pick another source.', 6000); return; }
153
165
  var name = fileNameFor(stream, t.label), bytes = +((stream.behaviorHints || {}).videoSize) || 0;
154
166
  spaces().then(function (list) {
155
167
  if (!list.length) { U.toast('No USB drive'); return; }
@@ -202,6 +214,6 @@ var Downloads = (function () {
202
214
  }, function () {});
203
215
  }
204
216
 
205
- return { available: available, items: items, sync: sync, watch: watch, onChange: onChange, add: add, cancel: cancel, clear: clear, retry: retry,
217
+ return { available: available, isFileLink: isFileLink, items: items, sync: sync, watch: watch, onChange: onChange, add: add, cancel: cancel, clear: clear, retry: retry,
206
218
  start: start, fileNameFor: fileNameFor, busy: busy, synced: function () { return _synced; } };
207
219
  })();
package/js/hubpreview.js CHANGED
@@ -8,6 +8,9 @@
8
8
  var HubPreview = (function () {
9
9
  'use strict';
10
10
  var SERVICE = 'StremioK01.service', EVERY_MS = 10 * 60 * 1000, SENT_KEY = 'stremiok.homepreview';
11
+ // Sent counts only once the preview service says the TV took them (SENT_KEY); a try is kept
12
+ // apart (_tried), for the 10 minutes between sends; a failure is tried again later (RETRY_MS).
13
+ var RETRY_MS = [60000, 300000, 900000], _tried = null, _fails = 0;
11
14
  var _timer = null;
12
15
  // Tizen 4 and later: the 2017 TV (Tizen 3) shows no preview for it, so there it's left out
13
16
  // (no setting, nothing sent).
@@ -28,15 +31,22 @@ var HubPreview = (function () {
28
31
  function send(now) {
29
32
  if (!available()) { return; }
30
33
  var json = JSON.stringify(payload()), last = U.store.get(SENT_KEY, null) || {}, t = Date.now();
31
- if (!now && json === last.json) { return; }
32
- if (!now && t - (last.at || 0) < EVERY_MS) { later(last.at + EVERY_MS - t); return; }
33
- U.store.set(SENT_KEY, { json: json, at: t });
34
+ if (!now && json === last.json) { return; } // (the TV has these already)
35
+ var lastTry = Math.max(last.at || 0, (_tried && _tried.at) || 0);
36
+ if (!now && t - lastTry < EVERY_MS) { later(lastTry + EVERY_MS - t); return; }
37
+ _tried = { json: json, at: t };
38
+ function failed(why) {
39
+ U.log('preview', 'Home bar preview not sent: ' + why);
40
+ if (_fails < RETRY_MS.length) { var wait = RETRY_MS[_fails++]; _tried = null; later(wait); } // (then as usual: a change, 10 minutes apart)
41
+ }
34
42
  try {
35
43
  tizen.application.launchAppControl(
36
44
  new tizen.ApplicationControl('http://tizen.org/appcontrol/operation/pick', null, 'image/jpeg', null,
37
45
  [new tizen.ApplicationControlData('caller', ['ForegroundApp']), new tizen.ApplicationControlData('tiles', [json])]),
38
- SERVICE, function () {}, function (e) { U.log('preview', 'Home bar preview not sent: ' + ((e && e.message) || e)); });
39
- } catch (e) { U.log('preview', 'Home bar preview not sent: ' + (e && e.message)); }
46
+ SERVICE, function () {}, function (e) { failed((e && e.message) || e); },
47
+ { onsuccess: function () { U.store.set(SENT_KEY, { json: json, at: t }); _fails = 0; },
48
+ onfailure: function () { failed('the TV didn’t take them'); } });
49
+ } catch (e) { failed(e && e.message); }
40
50
  }
41
51
  function later(ms) {
42
52
  if (_timer) { clearTimeout(_timer); }
package/js/iptv.js CHANGED
@@ -75,9 +75,8 @@ var IPTV = (function () {
75
75
  // Up (-1) or down (1): a channel in two playlists comes from the first, and rows are
76
76
  // in playlist order.
77
77
  function movePlaylist(id, dir) {
78
- var list = getPlaylists(), i = find(list, id), j = i + dir;
79
- if (i === -1 || j < 0 || j >= list.length) { return false; }
80
- var t = list[i]; list[i] = list[j]; list[j] = t;
78
+ var list = getPlaylists();
79
+ if (!U.moveIn(list, find(list, id), dir)) { return false; } // (one place, or to the top / bottom)
81
80
  savePlaylists(list);
82
81
  return true;
83
82
  }
@@ -368,6 +367,7 @@ var IPTV = (function () {
368
367
  _progressFns.forEach(function (fn) { try { fn(total); } catch (e) {} });
369
368
  }
370
369
  function onProgress(fn) { _progressFns.push(fn); }
370
+ function isReady() { return !!_cache; } // (the playlists read: synthAddon has their channels)
371
371
 
372
372
  // Each playlist is fetched once and saved (the big store). Passing a playlist id
373
373
  // forces only that entry to refresh; true refreshes every configured playlist.
@@ -478,7 +478,7 @@ var IPTV = (function () {
478
478
  getPlaylists: getPlaylists, addPlaylist: addPlaylist, updatePlaylist: updatePlaylist,
479
479
  removePlaylist: removePlaylist, renamePlaylist: renamePlaylist, setPlaylistOff: setPlaylistOff,
480
480
  movePlaylist: movePlaylist, label: labelOf, isEnabled: isEnabled, reset: reset, status: status,
481
- load: load, reload: reload, catalog: catalog, meta: meta, streams: streams, synthAddon: synthAddon,
481
+ load: load, reload: reload, catalog: catalog, meta: meta, streams: streams, synthAddon: synthAddon, isReady: isReady,
482
482
  onProgress: onProgress
483
483
  };
484
484
  })();
@@ -22,6 +22,7 @@
22
22
  function infoLang() { return P.infoLang.apply(null, arguments); } // lang
23
23
  function trackInfo() { return P.trackInfo.apply(null, arguments); } // lang
24
24
  function hasQualities() { return P.hasQualities.apply(null, arguments); } // quality
25
+ function qualityUnread() { return P.qualityUnread ? P.qualityUnread() : false; } // quality
25
26
  function qualityCount() { return P.qualityCount.apply(null, arguments); } // quality
26
27
  function qualityLabel() { return P.qualityLabel.apply(null, arguments); } // quality
27
28
  function qualityMenu() { return P.qualityMenu.apply(null, arguments); } // quality
@@ -627,6 +628,8 @@
627
628
  }
628
629
  if (hasQualities()) {
629
630
  items.push({ label: 'Video quality', detail: qualityLabel(), run: function () { pushMenu(qualityMenu()); } });
631
+ } else if (qualityUnread()) {
632
+ items.push({ label: 'Video quality', detail: 'Auto: this stream’s qualities couldn’t be read', run: function () { U.toast('This stream’s qualities couldn’t be read: it plays on Auto', 4000); } });
630
633
  }
631
634
  if (chapters().length) {
632
635
  var ci = chapterAt(S.cur);
@@ -683,6 +686,7 @@
683
686
  function canDownload() {
684
687
  var c = S.ctx, st = c && c.stream;
685
688
  return !!(st && c.downloadAs && !S.isLive && !c.trailer && !c.yt && !c.localFile && !st._localFile && !st._yt && /^https?:\/\//i.test(st.url || '') &&
689
+ (typeof Downloads === 'undefined' || !Downloads.isFileLink || Downloads.isFileLink(st)) &&
686
690
  typeof Downloads !== 'undefined' && Downloads.available());
687
691
  }
688
692
  function downloadPlaying() {
@@ -807,6 +811,7 @@
807
811
  }
808
812
  lines.push(['Subtitles', subtitleSummary()]);
809
813
  if (hasQualities()) { lines.push(['Quality', qualityLabel() + ' (' + qualityCount() + ' available)']); }
814
+ else if (qualityUnread()) { lines.push(['Quality', 'Auto (this stream’s qualities couldn’t be read)']); }
810
815
  if (useAV) {
811
816
  try {
812
817
  var bw = +avplay.getStreamingProperty('CURRENT_BANDWIDTH');
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 reached playback (vs. failed early)
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
- function saveProgress() {
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; }
@@ -376,7 +383,8 @@ var PlayerParts = {};
376
383
  resetQuality(ctx);
377
384
  resetControls();
378
385
  _stall = { t: -1, since: 0, bufSince: 0, recoveries: [] };
379
- _prefetchedNext = false; S.playStarted = false;
386
+ recoReset();
387
+ _prefetchedNext = false; S.playStarted = false; S.playing = false;
380
388
  _segments = null; _creditsSec = null; _creditsHandled = false; _segmentsFor = null;
381
389
  S.buffering = false; S.pendingSeek = null; S.lastSeekAt = 0; S.settleFrom = 0; _seekRecovering = false; _jumpUndone = null; _pausedOnce = false;
382
390
  updateHint();
@@ -509,7 +517,7 @@ var PlayerParts = {};
509
517
  url = (S.ctx && S.ctx._relay && S.ctx._relayFor === url) ? S.ctx._relay : opened(url); // (a live channel's sound repacked: see liveCheck)
510
518
  var token = ++_playToken;
511
519
  objEl.className = 'active';
512
- S.playStarted = false;
520
+ S.playStarted = false; S.playing = false;
513
521
  _lastTickMs = null; _playedMs = 0;
514
522
  _tick = null; _pausedOnce = false; // (a new open: the jump watch starts over, see checkJump)
515
523
  subsBeforeOpen(); // a built-in subtitle track is selected again once this attempt plays
@@ -564,6 +572,7 @@ var PlayerParts = {};
564
572
  // Played, not just opened: a file can open and then fail (or show nothing)
565
573
  if (_lastTickMs !== null && ms > _lastTickMs && ms - _lastTickMs < 1500) { _playedMs += ms - _lastTickMs; }
566
574
  _lastTickMs = ms;
575
+ if (!S.playing && _playedMs >= 1000) { S.playing = true; _reco.tries = 0; } // (started: playing, not just opened; a reconnect's tries start over)
567
576
  if (_playedMs >= 2000 && !isNotice()) { reportStart(true); } // (a notice clip proves nothing about the format)
568
577
  audioOnTick();
569
578
  subsOnTick();
@@ -586,11 +595,15 @@ var PlayerParts = {};
586
595
  setTimeout(function () { _seekRecovering = false; }, 4000);
587
596
  return;
588
597
  }
589
- // Failed before playback ever started → retry / step down / Auto.
590
- if (!S.playStarted && (relayFailed() || tryFallback())) { return; }
598
+ // A live channel that played: its connection dropped → opened again (liveDropped).
599
+ if (S.playing && liveReconnects()) { U.log('player', 'Playback error: ' + err + ' (' + where(S.ctx.url) + ')'); liveDropped('Connection lost'); return; }
600
+ // A reconnect's try that didn't play → the next try.
601
+ if (!S.playing && _reco.tries) { if (!relayFailed()) { liveDropped('Couldn’t reconnect'); } return; }
602
+ // Failed before it ever played (opened, maybe, but nothing shown) → retry / step down / Auto.
603
+ if (!S.playing && (relayFailed() || tryFallback())) { return; }
591
604
  reportStart(false, err); // (unless it had already played)
592
605
  U.log('player', 'Playback error: ' + err + ' (' + where(S.ctx && S.ctx.url) + ')');
593
- if (!S.playStarted) { failedStart('Playback error: ' + err); return; }
606
+ if (!S.playing) { failedStart('Playback error: ' + err); return; }
594
607
  U.toast('Playback error: ' + err);
595
608
  exitLater();
596
609
  }
@@ -625,6 +638,7 @@ var PlayerParts = {};
625
638
  if (!S.active || token !== _playToken) { return; }
626
639
  U.spinner(false);
627
640
  if (relayFailed()) { return; }
641
+ if (_reco.tries) { liveDropped('Couldn’t reconnect'); return; } // (a reconnect's try: the next one)
628
642
  // Prepare failed → retry / step down / Auto.
629
643
  if (tryFallback()) { return; }
630
644
  U.log('player', 'Could not open: ' + (err && (err.name || err.message || err)) + ' (' + where(url) + ')');
@@ -710,7 +724,7 @@ var PlayerParts = {};
710
724
  _ticker = setInterval(function () {
711
725
  if (inBackground()) { return; } // (saved on the way there; nothing to watch until it's back)
712
726
  if (useAV) { try { S.cur = avplay.getCurrentTime() / 1000; } catch (e) {} onProgress(); checkStall(); }
713
- saveProgress();
727
+ saveProgress(true);
714
728
  }, 5000);
715
729
  }
716
730
 
@@ -723,8 +737,8 @@ var PlayerParts = {};
723
737
  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
738
  }
725
739
  function onEnded() {
726
- // a live channel whose stream ended (its server stopped it): opened again, as a stall is
727
- if (S.isLive && S.ctx && !S.ctx.trailer && !S.ctx.yt) { recoverStall('The stream ended — opening the channel again…'); return; }
740
+ // a live channel whose stream ended (its server stopped it): opened again (liveDropped)
741
+ if (liveReconnects()) { liveDropped('The stream ended'); return; }
728
742
  if (isNotice()) { // (back to its sources: another one, or this one later)
729
743
  U.toast('That source isn\'t ready yet (the debrid service is still getting it): try another, or this one later', 6000);
730
744
  exit();
@@ -834,6 +848,13 @@ var PlayerParts = {};
834
848
  if (_creditsHandled || !S.ctx || S.ctx.trailer || isNotice()) { return; }
835
849
  _creditsHandled = true;
836
850
  if (S.ctx.private) { return; } // (private viewing: not marked watched, nothing queued)
851
+ // With Trakt: sent as a finish (Trakt's own way, which also clears a paused place it
852
+ // kept for it), not as a separate "watched"
853
+ if (S.dur > 0 && typeof Trakt !== 'undefined' && Trakt.onStopped && Trakt.isConnected && Trakt.isConnected()) {
854
+ markFinished(true);
855
+ Trakt.onStopped(S.ctx.type, S.ctx.id, S.cur, S.dur, S.ctx.name);
856
+ return;
857
+ }
837
858
  markFinished(false);
838
859
  }
839
860
  // Watched: off Continue Watching, the next episode queued. quiet: Trakt's been told another
@@ -937,7 +958,7 @@ var PlayerParts = {};
937
958
  var NEXT_TRIES = 3, AV_START_MS = 45000, _openedAt = 0;
938
959
  function nextSourceOnFail(notice) {
939
960
  var c = S.ctx;
940
- if (!c || !S.active || S.playStarted || S.isLive || c.trailer || c.yt || c.localFile || !c.stream || typeof c.nextSource !== 'function') { return false; }
961
+ if (!c || !S.active || S.playing || S.isLive || c.trailer || c.yt || c.localFile || !c.stream || typeof c.nextSource !== 'function') { return false; }
941
962
  if (typeof Stremio === 'undefined' || !Stremio.getPrefs || !Stremio.getPrefs().nextOnFail) { return false; }
942
963
  var tried = (c._tried || []).concat([Stremio.sourceId(c.stream)]);
943
964
  if (tried.length > NEXT_TRIES) { return false; }
@@ -1000,11 +1021,17 @@ var PlayerParts = {};
1000
1021
  else { n.text = svc + ' couldn’t give this file (' + addon + ': “' + kind.replace(/_/g, ' ') + '”)'; }
1001
1022
  return n;
1002
1023
  }
1003
- // Not started after AV_START_MS (no answer from the server): as a refusal, when there's a
1004
- // next source to try (otherwise it's left to go on waiting).
1024
+ // Not playing after AV_START_MS (no answer from the server, or opened and nothing shown): the
1025
+ // next source when there's one; else it stops and says so (not while you've paused it).
1005
1026
  function startCheck() {
1006
- if (!S.active || S.playStarted || !useAV || !_openedAt || Date.now() - _openedAt < AV_START_MS) { return; }
1007
- nextSourceOnFail();
1027
+ if (!S.active || S.playing || !useAV || !_openedAt || Date.now() - _openedAt < AV_START_MS) { return; }
1028
+ if (_reco.tries) { liveDropped('Couldn’t reconnect'); return; } // (a reconnect's try: the next one)
1029
+ if (nextSourceOnFail()) { return; }
1030
+ if (S.playStarted && isPaused()) { return; }
1031
+ U.log('player', 'Didn’t start: nothing played in ' + (AV_START_MS / 1000) + ' s (' + where(S.ctx && S.ctx.url) + ')');
1032
+ U.spinner(false);
1033
+ U.toast('Didn’t start: nothing played in ' + (AV_START_MS / 1000) + ' seconds. Try another source.', 6000);
1034
+ exitLater();
1008
1035
  }
1009
1036
  P.startCheck = startCheck;
1010
1037
 
@@ -1077,14 +1104,15 @@ var PlayerParts = {};
1077
1104
  var now = Date.now();
1078
1105
  if (S.buffering) {
1079
1106
  if (!_stall.bufSince) { _stall.bufSince = now; }
1080
- else if (now - _stall.bufSince > BUFFER_STALL_SECS * 1000) { recoverStall('Stream stopped loading — reloading…'); }
1107
+ else if (now - _stall.bufSince > BUFFER_STALL_SECS * 1000) { recoverStall('Stream stopped loading'); }
1081
1108
  return;
1082
1109
  }
1083
1110
  _stall.bufSince = 0;
1084
1111
  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 — reloading…'); }
1112
+ if (_stall.since && now - _stall.since > STALL_SECS * 1000) { recoverStall('Playback stalled'); }
1086
1113
  }
1087
- function recoverStall(message) {
1114
+ function recoverStall(why) {
1115
+ if (liveReconnects()) { liveDropped(why); return; }
1088
1116
  var now = Date.now();
1089
1117
  _stall.recoveries = _stall.recoveries.filter(function (t) { return now - t < STALL_WINDOW_MS; });
1090
1118
  if (_stall.recoveries.length >= STALL_MAX) {
@@ -1094,7 +1122,51 @@ var PlayerParts = {};
1094
1122
  return;
1095
1123
  }
1096
1124
  _stall.recoveries.push(now);
1097
- reopen(message);
1125
+ reopen(why + ' — reloading…');
1126
+ }
1127
+
1128
+ // ---------- a live channel that drops ----------
1129
+ // A live channel that stops (an error once it played, its connection lost; frozen, stuck
1130
+ // loading, or ended by its server) is opened again at the live edge, as Settings › Playback ›
1131
+ // Seeking & buffering › Reconnect live channels says: Off (the player closes), A few times
1132
+ // (STALL_MAX within STALL_WINDOW_MS, as any stall) or Keep trying (no limit). A try that
1133
+ // doesn't play is followed by another, each a little later (RECO_WAIT_S); BACK stops it.
1134
+ // Not for a trailer or YouTube (a stall there reloads as a video's does).
1135
+ var RECO_WAIT_S = [0, 5, 10, 20, 40, 60], _reco = { tries: 0, timer: null };
1136
+ function liveReconnects() { return S.isLive && !!S.ctx && !S.ctx.trailer && !S.ctx.yt; }
1137
+ function recoReset() {
1138
+ if (_reco.timer) { clearTimeout(_reco.timer); }
1139
+ _reco = { tries: 0, timer: null };
1140
+ }
1141
+ function liveDropped(why) {
1142
+ if (_reco.timer) { return; } // (a try already waiting)
1143
+ var mode = P.playerCfg().reconnect, now = Date.now();
1144
+ if (mode === 'off') {
1145
+ U.log('player', 'Live channel: ' + why + '; reconnecting is off');
1146
+ U.spinner(false); U.toast(why + ': the channel stopped (reconnecting is off)', 5000);
1147
+ exitLater();
1148
+ return;
1149
+ }
1150
+ _stall.recoveries = _stall.recoveries.filter(function (t) { return now - t < STALL_WINDOW_MS; });
1151
+ if (mode !== 'always' && _stall.recoveries.length >= STALL_MAX) {
1152
+ U.log('player', 'Live channel: ' + why + '; dropped ' + STALL_MAX + ' times in ' + (STALL_WINDOW_MS / 60000) + ' minutes, given up');
1153
+ U.spinner(false); U.toast('This channel keeps dropping — try it again later', 5000);
1154
+ exit();
1155
+ return;
1156
+ }
1157
+ _stall.recoveries.push(now);
1158
+ var n = ++_reco.tries, wait = RECO_WAIT_S[Math.min(n - 1, RECO_WAIT_S.length - 1)] * 1000, ctx = S.ctx;
1159
+ var msg = why + ' — reconnecting' + (n > 1 ? ' (try ' + n + ')' : '') + '…';
1160
+ U.log('player', 'Live channel: ' + why + ', reconnecting (try ' + n + (wait ? ', in ' + (wait / 1000) + ' s' : '') + ')');
1161
+ if (!wait) { reopen(msg); return; }
1162
+ U.spinner(true, msg);
1163
+ function due() {
1164
+ _reco.timer = null;
1165
+ if (!S.active || S.ctx !== ctx) { return; }
1166
+ if (inBackground()) { _reco.timer = setTimeout(due, 5000); return; } // (back first)
1167
+ reopen(msg);
1168
+ }
1169
+ _reco.timer = setTimeout(due, wait);
1098
1170
  }
1099
1171
  // The stream opened again in place: VOD at the same position, live at the edge.
1100
1172
  function reopen(message) {
@@ -1126,6 +1198,7 @@ var PlayerParts = {};
1126
1198
  closeControls();
1127
1199
  screenSaver(true);
1128
1200
  _playToken++; // invalidate AVPlay/HTML5 callbacks before stop/close emits any
1201
+ recoReset();
1129
1202
  if (_ticker) { clearInterval(_ticker); _ticker = null; }
1130
1203
  if (_osdTimer) { clearTimeout(_osdTimer); _osdTimer = null; }
1131
1204
  hideOSD();
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 = {}, re = /["']?([A-Za-z_]+)["']?\s*[:=]\s*["']?([^"',}]*)/g, m;
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
  }
@@ -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
- function getHlsMaster(url) {
191
- return U.getText(url, 8000, HLS_MASTER_MAX).then(function (text) {
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 () { if (tok === _variantsToken) { _variants = []; _sepAudio = false; } });
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;