nuvio-tizen 1.9.6 → 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 +1 -1
- package/index.html +11 -11
- package/js/ytplay.js +26 -49
- package/package.json +1 -1
- package/service/index.js +31 -3
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.
|
|
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.
|
|
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.
|
|
54
|
-
<script src="js/util.js?v=1.9.
|
|
55
|
-
<script src="js/iptv.js?v=1.9.
|
|
56
|
-
<script src="js/stremio.js?v=1.9.
|
|
57
|
-
<script src="js/keys.js?v=1.9.
|
|
58
|
-
<script src="js/focus.js?v=1.9.
|
|
59
|
-
<script src="js/player.js?v=1.9.
|
|
60
|
-
<script src="js/ytplay.js?v=1.9.
|
|
61
|
-
<script src="js/views.js?v=1.9.
|
|
62
|
-
<script src="js/app.js?v=1.9.
|
|
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/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
|
|
3
|
-
|
|
4
|
-
localhost proxy (service/index.js) that
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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,34 +59,7 @@ var YTPlay = (function () {
|
|
|
64
59
|
}, function () { return ''; });
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
function
|
|
68
|
-
var o = overlay();
|
|
69
|
-
if (!o) { return; }
|
|
70
|
-
U.clear(o);
|
|
71
|
-
var iframe = document.createElement('iframe');
|
|
72
|
-
iframe.setAttribute('allow', 'autoplay; encrypted-media');
|
|
73
|
-
iframe.src = PROXY + '/tv#/watch?v=' + encodeURIComponent(id);
|
|
74
|
-
iframe.onload = function () { try { iframe.focus(); } catch (e) {} };
|
|
75
|
-
o.appendChild(iframe);
|
|
76
|
-
o.className = '';
|
|
77
|
-
U.byId('app').className = 'hidden';
|
|
78
|
-
_open = true;
|
|
79
|
-
// Back is handled app-wide by keys.js (tizenhwkey) → App.back() → YTPlay.close(),
|
|
80
|
-
// which works even while this cross-origin iframe holds focus.
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function close() {
|
|
84
|
-
if (!_open) { return; }
|
|
85
|
-
_open = false;
|
|
86
|
-
var o = overlay();
|
|
87
|
-
if (o) { o.className = 'hidden'; U.clear(o); }
|
|
88
|
-
U.byId('app').className = '';
|
|
89
|
-
var cb = _onExit; _onExit = null;
|
|
90
|
-
if (cb) { try { cb(); } catch (e) {} } // redraw detail -> restores focus scope
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function open(stream, onExit) {
|
|
94
|
-
_onExit = onExit || null;
|
|
62
|
+
function open(stream /*, onExit */) {
|
|
95
63
|
var page = stream.ytPage || stream.url;
|
|
96
64
|
U.spinner(true);
|
|
97
65
|
ensureService().then(function () {
|
|
@@ -99,13 +67,22 @@ var YTPlay = (function () {
|
|
|
99
67
|
}).then(function (id) {
|
|
100
68
|
U.spinner(false);
|
|
101
69
|
if (!id) { U.toast('Could not find the live stream for this channel', 3500); return; }
|
|
102
|
-
|
|
103
|
-
|
|
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 () {
|
|
104
77
|
U.spinner(false);
|
|
105
78
|
U.toast('Couldn\'t reach the YouTube proxy (port 8099). Use the sideloaded Nuvio ' +
|
|
106
79
|
'build so its background service is installed. (Desktop dev: run node service/index.js.)', 5500);
|
|
107
80
|
});
|
|
108
81
|
}
|
|
109
82
|
|
|
110
|
-
|
|
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 };
|
|
111
88
|
})();
|
package/package.json
CHANGED
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
|
-
|
|
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 = '
|
|
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;
|