nuvio-tizen 1.9.5 → 1.9.7

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/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://nuvio.tizen/NuvioTizen" version="1.9.5" viewmodes="maximized">
3
+ id="http://nuvio.tizen/NuvioTizen" version="1.9.7" viewmodes="maximized">
4
4
  <tizen:application id="Nuvio00001.Nuvio" package="Nuvio00001" required_version="3.0"/>
5
5
  <content src="index.html"/>
6
6
  <feature name="http://tizen.org/feature/screen.size.all"/>
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>Nuvio</title>
7
- <link rel="stylesheet" href="css/style.css?v=1.9.5">
7
+ <link rel="stylesheet" href="css/style.css?v=1.9.7">
8
8
  </head>
9
9
  <body>
10
10
  <!-- CSS-applied sentinel (kept off-screen); app.js waits for its width before rendering -->
@@ -50,15 +50,15 @@
50
50
 
51
51
  <!-- Load order matters: polyfills -> util -> iptv -> data -> input -> player -> ytplay -> views -> app -->
52
52
  <!-- ?v= busts the TV webview cache on every release (keep in sync with package.json) -->
53
- <script src="js/polyfills.js?v=1.9.5"></script>
54
- <script src="js/util.js?v=1.9.5"></script>
55
- <script src="js/iptv.js?v=1.9.5"></script>
56
- <script src="js/stremio.js?v=1.9.5"></script>
57
- <script src="js/keys.js?v=1.9.5"></script>
58
- <script src="js/focus.js?v=1.9.5"></script>
59
- <script src="js/player.js?v=1.9.5"></script>
60
- <script src="js/ytplay.js?v=1.9.5"></script>
61
- <script src="js/views.js?v=1.9.5"></script>
62
- <script src="js/app.js?v=1.9.5"></script>
53
+ <script src="js/polyfills.js?v=1.9.7"></script>
54
+ <script src="js/util.js?v=1.9.7"></script>
55
+ <script src="js/iptv.js?v=1.9.7"></script>
56
+ <script src="js/stremio.js?v=1.9.7"></script>
57
+ <script src="js/keys.js?v=1.9.7"></script>
58
+ <script src="js/focus.js?v=1.9.7"></script>
59
+ <script src="js/player.js?v=1.9.7"></script>
60
+ <script src="js/ytplay.js?v=1.9.7"></script>
61
+ <script src="js/views.js?v=1.9.7"></script>
62
+ <script src="js/app.js?v=1.9.7"></script>
63
63
  </body>
64
64
  </html>
package/js/app.js CHANGED
@@ -4,6 +4,7 @@ var App = (function () {
4
4
 
5
5
  var _current = 'home';
6
6
  var _stack = []; // frames: { screen, focus }
7
+ var _lastBackAt = 0; // debounce BACK across keydown + tizenhwkey
7
8
 
8
9
  function screenEl(name) { return U.byId('screen-' + name); }
9
10
 
@@ -58,6 +59,7 @@ var App = (function () {
58
59
  }
59
60
 
60
61
  function back() {
62
+ if (typeof YTPlay !== 'undefined' && YTPlay.isOpen && YTPlay.isOpen()) { YTPlay.close(); return; }
61
63
  if (Player.isActive()) { Player.exit(); return; }
62
64
  if (Views.isModalOpen()) { Views.closeModal(); return; }
63
65
  if (_current === 'detail' && Views.backInDetail()) { return; }
@@ -102,7 +104,14 @@ var App = (function () {
102
104
  else { Focus.clickCurrent(); }
103
105
  return true;
104
106
  }
105
- case 'BACK': back(); return true;
107
+ case 'BACK': {
108
+ // keydown(10009) and tizenhwkey can both fire for one press — debounce.
109
+ var now = Date.now();
110
+ if (now - _lastBackAt < 400) { return true; }
111
+ _lastBackAt = now;
112
+ back();
113
+ return true;
114
+ }
106
115
  default: return true;
107
116
  }
108
117
  }
package/js/keys.js CHANGED
@@ -67,10 +67,19 @@ var Keys = (function () {
67
67
  dispatch(held >= LONG_MS ? 'OK_LONG' : 'OK', ev);
68
68
  }
69
69
 
70
+ // Tizen delivers the hardware RETURN/BACK as a 'tizenhwkey' event to the app
71
+ // document even when a cross-origin iframe (e.g. the YouTube leanback overlay)
72
+ // holds focus — where a normal keydown never reaches us. Route it through the same
73
+ // handler and preventDefault so the platform doesn't exit the app behind our back.
74
+ function onHwKey(ev) {
75
+ if (ev && ev.keyName === 'back') { dispatch('BACK', ev); }
76
+ }
77
+
70
78
  function init() {
71
79
  register();
72
80
  document.addEventListener('keydown', onKeyDown, false);
73
81
  document.addEventListener('keyup', onKeyUp, false);
82
+ document.addEventListener('tizenhwkey', onHwKey, false);
74
83
  }
75
84
 
76
85
  return { init: init, setHandler: setHandler, CODE: CODE };
package/js/views.js CHANGED
@@ -526,6 +526,29 @@ var Views = (function () {
526
526
  }
527
527
 
528
528
  // ----- streams -----
529
+ // "Where to watch" / subscription links (Paramount+, Netflix, rent/buy, etc.) are
530
+ // pushed to the bottom, below directly-playable sources.
531
+ function isSubscriptionStream(stream) {
532
+ if (Stremio.isPlayable(stream)) {
533
+ var t = ('' + (stream.name || '') + ' ' + (stream.title || '') + ' ' + (stream.description || ''));
534
+ return /\b(subscription|subscribe|rent|buy|purchase)\b|paramount|netflix|disney\+|disney plus|hbo|max\b|hulu|peacock|prime video|apple tv|starz|showtime/i.test(t);
535
+ }
536
+ return true; // non-playable (external "where to watch") always sinks to the bottom
537
+ }
538
+ function streamRank(stream) {
539
+ if (Stremio.isPlayable(stream) && !isSubscriptionStream(stream)) { return 0; }
540
+ if (Stremio.isPlayable(stream)) { return 1; }
541
+ return 2;
542
+ }
543
+ function sortStreams(list) {
544
+ return list.map(function (s, i) { return { s: s, i: i }; })
545
+ .sort(function (a, b) {
546
+ var ra = streamRank(a.s), rb = streamRank(b.s);
547
+ return ra !== rb ? ra - rb : a.i - b.i; // stable within a rank
548
+ })
549
+ .map(function (x) { return x.s; });
550
+ }
551
+
529
552
  function openStreams(videoId, label) {
530
553
  _d.mode = 'streams';
531
554
  _d.streamId = videoId;
@@ -534,7 +557,7 @@ var Views = (function () {
534
557
  drawDetail();
535
558
  U.spinner(true);
536
559
  Stremio.getStreams(_d.type, videoId).then(function (list) {
537
- _d.streams = list;
560
+ _d.streams = sortStreams(list);
538
561
  U.spinner(false);
539
562
  if (_d.mode === 'streams') { drawDetail(); }
540
563
  }, function () {
@@ -544,6 +567,11 @@ var Views = (function () {
544
567
  });
545
568
  }
546
569
 
570
+ // Re-fetch the current title's sources and land focus on the first result.
571
+ function rescanStreams() {
572
+ if (_d && _d.streamId) { openStreams(_d.streamId, _d.streamLabel); }
573
+ }
574
+
547
575
  function streamActionsBar() {
548
576
  var row = U.el('div', 'btn-row');
549
577
  row.appendChild(U.el('div', 'detail-meta', 'Sources for ' + (_d.streamLabel || '')));
@@ -588,6 +616,14 @@ var Views = (function () {
588
616
  };
589
617
  listScroll.appendChild(node);
590
618
  });
619
+
620
+ // Re-scan: refresh the source list (and jump back to the first result).
621
+ var rescan = U.el('div', 'stream focusable rescan-row');
622
+ rescan.appendChild(document.createTextNode('↻ Re-scan for sources'));
623
+ rescan.appendChild(U.el('span', 's-sub', 'Refresh the list from all stream addons'));
624
+ rescan.__onselect = function () { rescanStreams(); };
625
+ listScroll.appendChild(rescan);
626
+
591
627
  panel.appendChild(listScroll);
592
628
  return panel;
593
629
  }
package/js/ytplay.js CHANGED
@@ -1,25 +1,23 @@
1
1
  /* YouTube leanback playback. Global namespace: YTPlay
2
- YouTube-live IPTV channels can't be extracted to a direct URL on-device and
3
- youtube.com/tv can't be framed (X-Frame-Options). So we launch a bundled
4
- localhost proxy (service/index.js) that serves youtube.com/tv with those
5
- headers stripped, and load it in a fullscreen overlay iframe — the same
6
- leanback approach TizenTube uses. See service/index.js for the proxy. */
2
+ YouTube-live IPTV channels can't be extracted to a direct URL on-device. We play
3
+ them through YouTube's own leanback TV app (youtube.com/tv) behind a bundled
4
+ localhost proxy (service/index.js) that strips X-Frame-Options/CSP and injects an
5
+ ad-block userscript the same approach TizenTube uses.
6
+
7
+ IMPORTANT: leanback must run in the app's MAIN FRAME. On Tizen the platform media
8
+ APIs (webapis/AVPlay) that leanback needs to actually decode video are injected only
9
+ into the main frame, never a cross-origin iframe — in an iframe the UI works but the
10
+ picture stays black and Play does nothing. So we navigate the whole webview to the
11
+ proxy (like TizenTube standalone) and pass a return URL; the proxy injects a Back
12
+ handler that navigates back to Nuvio. */
7
13
  var YTPlay = (function () {
8
14
  'use strict';
9
15
 
10
16
  var PROXY = 'http://127.0.0.1:8099'; // 127.0.0.1 (not "localhost") resolves reliably on Tizen
11
- var _open = false;
12
- var _onExit = null;
13
17
  var _idCache = {}; // ytPage -> { id, ts }
14
18
  var ID_TTL = 5 * 60 * 1000; // live video ids rotate; keep short
15
19
 
16
- function overlay() { return U.byId('yt-leanback'); }
17
-
18
- // Resolve once the proxy answers /health. Order:
19
- // 1. Probe /health first — covers a warm service (TV) and a dev run of the proxy
20
- // under Node in a desktop browser (`node service/index.js`).
21
- // 2. On Tizen, if it's not up, launch the <tizen:service> and poll.
22
- // 3. Otherwise (browser, no proxy running) reject with a dev hint.
20
+ // Resolve once the proxy answers /health; launch the bundled <tizen:service> if needed.
23
21
  function ensureService() {
24
22
  return new Promise(function (resolve, reject) {
25
23
  function poll(left) {
@@ -28,9 +26,7 @@ var YTPlay = (function () {
28
26
  setTimeout(function () { poll(left - 1); }, 500);
29
27
  });
30
28
  }
31
- // Already running? (warm TV service, or a locally-run proxy in dev)
32
29
  U.getText(PROXY + '/health', 1500).then(function () { resolve(); }, function () {
33
- // Best-effort launch of the bundled <tizen:service>, then keep polling.
34
30
  var launched = false;
35
31
  try {
36
32
  if (window.tizen && tizen.application && tizen.application.launchAppControl) {
@@ -49,8 +45,7 @@ var YTPlay = (function () {
49
45
  }
50
46
 
51
47
  // A youtube.com/<channel>/live (or /watch?v=) URL -> the current live video id.
52
- // The page is fetched THROUGH the proxy's /cors-bypass so it works both on the TV
53
- // and in a desktop dev browser (youtube.com sends no CORS header of its own).
48
+ // Fetched THROUGH the proxy's /cors-bypass so it works on the TV and in dev.
54
49
  function resolveVideoId(page) {
55
50
  var direct = ('' + page).match(/[?&]v=([A-Za-z0-9_-]{11})/);
56
51
  if (direct) { return Promise.resolve(direct[1]); }
@@ -64,49 +59,7 @@ var YTPlay = (function () {
64
59
  }, function () { return ''; });
65
60
  }
66
61
 
67
- function onHwKey(e) {
68
- if (e && e.keyName === 'back') { if (e.preventDefault) { e.preventDefault(); } close(); }
69
- }
70
- function onKeyDown(e) {
71
- if (e && (e.keyCode === 10009 || e.keyCode === 461)) {
72
- if (e.preventDefault) { e.preventDefault(); }
73
- if (e.stopPropagation) { e.stopPropagation(); }
74
- close();
75
- }
76
- }
77
-
78
- function show(id) {
79
- var o = overlay();
80
- if (!o) { return; }
81
- U.clear(o);
82
- var iframe = document.createElement('iframe');
83
- iframe.setAttribute('allow', 'autoplay; encrypted-media');
84
- iframe.src = PROXY + '/tv#/watch?v=' + encodeURIComponent(id);
85
- iframe.onload = function () { try { iframe.focus(); } catch (e) {} };
86
- o.appendChild(iframe);
87
- o.className = '';
88
- U.byId('app').className = 'hidden';
89
- _open = true;
90
- // Hardware Back is delivered app-wide (tizenhwkey) even when the iframe has
91
- // focus; the keydown capture covers the moment before the iframe grabs focus.
92
- document.addEventListener('tizenhwkey', onHwKey, true);
93
- document.addEventListener('keydown', onKeyDown, true);
94
- }
95
-
96
- function close() {
97
- if (!_open) { return; }
98
- _open = false;
99
- document.removeEventListener('tizenhwkey', onHwKey, true);
100
- document.removeEventListener('keydown', onKeyDown, true);
101
- var o = overlay();
102
- if (o) { o.className = 'hidden'; U.clear(o); }
103
- U.byId('app').className = '';
104
- var cb = _onExit; _onExit = null;
105
- if (cb) { try { cb(); } catch (e) {} } // redraw detail -> restores focus scope
106
- }
107
-
108
- function open(stream, onExit) {
109
- _onExit = onExit || null;
62
+ function open(stream /*, onExit */) {
110
63
  var page = stream.ytPage || stream.url;
111
64
  U.spinner(true);
112
65
  ensureService().then(function () {
@@ -114,13 +67,22 @@ var YTPlay = (function () {
114
67
  }).then(function (id) {
115
68
  U.spinner(false);
116
69
  if (!id) { U.toast('Could not find the live stream for this channel', 3500); return; }
117
- show(id);
118
- }, function (err) {
70
+ // Navigate the MAIN FRAME to leanback (needed for real playback on Tizen).
71
+ // nvret = where to return on Back; the hash carries the video so youtube.com/tv
72
+ // doesn't 303-redirect and drop our query param.
73
+ var ret = location.href;
74
+ window.location.href = PROXY + '/tv?nvret=' + encodeURIComponent(ret) +
75
+ '#/watch?v=' + encodeURIComponent(id);
76
+ }, function () {
119
77
  U.spinner(false);
120
78
  U.toast('Couldn\'t reach the YouTube proxy (port 8099). Use the sideloaded Nuvio ' +
121
79
  'build so its background service is installed. (Desktop dev: run node service/index.js.)', 5500);
122
80
  });
123
81
  }
124
82
 
125
- return { open: open, close: close, isOpen: function () { return _open; } };
83
+ // Kept for API compatibility (playback now navigates away rather than overlaying).
84
+ function close() {}
85
+ function isOpen() { return false; }
86
+
87
+ return { open: open, close: close, isOpen: isOpen };
126
88
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuvio-tizen",
3
- "version": "1.9.5",
3
+ "version": "1.9.7",
4
4
  "description": "Nuvio-style Stremio-addon streaming client for Tizen 3.0+ Samsung TVs (TizenBrew app module).",
5
5
  "packageType": "app",
6
6
  "appName": "Nuvio",
package/service/index.js CHANGED
@@ -42,10 +42,24 @@ function hostAllowed(h) {
42
42
 
43
43
  function proxyPrefix() { return 'http://localhost:' + PORT + '/cors-bypass/'; }
44
44
 
45
- function rewriteText(text, isTvHtml) {
45
+ // A Back-key handler injected into the leanback page so pressing Return goes back to
46
+ // Nuvio (nvret = the app URL Nuvio navigated from). Runs in leanback's top frame, where
47
+ // tizenhwkey fires. Capture phase + stopImmediatePropagation so leanback doesn't also
48
+ // handle it. Falls back to history.back() if no return URL was supplied.
49
+ function backHandlerScript(nvret) {
50
+ return '<script>(function(){var r=' + JSON.stringify(nvret || '') + ';' +
51
+ 'function go(e){try{e.preventDefault();e.stopImmediatePropagation();}catch(x){}' +
52
+ 'if(r){try{window.location.href=r;return;}catch(x2){}}try{history.back();}catch(x3){}}' +
53
+ 'window.addEventListener("tizenhwkey",function(e){if(e&&e.keyName==="back")go(e);},true);' +
54
+ 'window.addEventListener("keydown",function(e){if(e&&(e.keyCode===10009||e.keyCode===461))go(e);},true);' +
55
+ '})();</script>';
56
+ }
57
+
58
+ function rewriteText(text, isTvHtml, nvret) {
46
59
  var p = proxyPrefix();
47
60
  if (isTvHtml) {
48
- var inject = '<script>window.__tizenTubeStandaloneVersion="nuvio";</script>' +
61
+ var inject = (nvret ? backHandlerScript(nvret) : '') +
62
+ '<script>window.__tizenTubeStandaloneVersion="nuvio";</script>' +
49
63
  '<script src="' + USERSCRIPT_CDN + '?ver=' + Date.now() +
50
64
  '" onerror="this.onerror=null;this.src=\'' + USERSCRIPT_FALLBACK + '\'"></script>';
51
65
  if (/<body[^>]*>/i.test(text)) {
@@ -143,6 +157,20 @@ function handle(req, res) {
143
157
  if (!hostAllowed(pu.hostname)) { res.writeHead(403); res.end('host not allowed'); return; }
144
158
 
145
159
  var isTvHtml = (!isCors && pathOnly === '/tv');
160
+ // Nuvio passes its return URL as ?nvret= on the /tv request so we can inject a
161
+ // Back handler that navigates back to the app.
162
+ var nvret = '';
163
+ if (isTvHtml) {
164
+ var qmatch = req.url.split('?')[1];
165
+ if (qmatch) {
166
+ var parts = qmatch.split('&');
167
+ for (var qi = 0; qi < parts.length; qi++) {
168
+ if (parts[qi].indexOf('nvret=') === 0) {
169
+ try { nvret = decodeURIComponent(parts[qi].slice('nvret='.length)); } catch (e) {}
170
+ }
171
+ }
172
+ }
173
+ }
146
174
 
147
175
  var headers = {};
148
176
  for (var k in req.headers) {
@@ -196,7 +224,7 @@ function handle(req, res) {
196
224
  pres.on('end', function () {
197
225
  decompress(Buffer.concat(chunks), enc, function (err, buf) {
198
226
  if (err) { res.writeHead(502); res.end('decompress error'); return; }
199
- var text = rewriteText(buf.toString('utf8'), isTvHtml);
227
+ var text = rewriteText(buf.toString('utf8'), isTvHtml, nvret);
200
228
  // new Buffer(): Buffer.from() doesn't exist on the TV's old Node (< 4.5).
201
229
  var body = new Buffer(text, 'utf8');
202
230
  outHeaders['Content-Length'] = body.length;