@hatfiller/tizenpstream 1.0.0 → 1.0.2
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 +12 -1
- package/dist/userScript.js +175 -32
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@ TizenBrew **site modification** module for [P-Stream](https://pstream.mov) on Sa
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
1. Install [TizenBrew](https://github.com/reisxd/TizenBrew) on your Samsung Tizen TV.
|
|
8
|
-
2. In
|
|
8
|
+
2. **App (launcher):** In the module manager add **`tizenpstream-app`** (no @). This gives you a **PStream** tile; opening it launches pstream.mov in the app view so you can navigate with the remote like a normal Tizen app.
|
|
9
|
+
3. **Mod (navigation + keys):** Add **`@hatfiller/tizenpstream`** so D-pad, Enter, Back, and media keys work on pstream.mov. Enter exactly **`@hatfiller/tizenpstream`** (no `npm/` in front). If you see “unknown module”, try **`hatfiller/tizenpstream`** (without `@`).
|
|
9
10
|
|
|
10
11
|
## What it does
|
|
11
12
|
|
|
@@ -16,10 +17,20 @@ TizenBrew **site modification** module for [P-Stream](https://pstream.mov) on Sa
|
|
|
16
17
|
|
|
17
18
|
The script is written in ES5-style JS for compatibility with older Tizen WebKit.
|
|
18
19
|
|
|
20
|
+
### See your TV’s Tizen / device info (debug)
|
|
21
|
+
|
|
22
|
+
- **Once on load:** A small debug overlay appears for about 6 seconds with your TV’s user agent and Tizen build info (if available).
|
|
23
|
+
- **Anytime:** Press the **0** key on the remote to show or hide the same overlay (UA, Tizen API, build). Press **0** or **Back** again to close it.
|
|
24
|
+
|
|
19
25
|
## Module type
|
|
20
26
|
|
|
21
27
|
This is a **site modification** module (`packageType: "mods"`). It injects `mods/userScript.js` (or `dist/userScript.js`) into `https://pstream.mov` when you open it via TizenBrew.
|
|
22
28
|
|
|
29
|
+
## Repo layout
|
|
30
|
+
|
|
31
|
+
- **Mod (this package):** `@hatfiller/tizenpstream` — injects into pstream.mov for remote navigation and debug overlay.
|
|
32
|
+
- **App launcher:** `tizen-pstream-app/` — package **`tizenpstream-app`** (unscoped); add it in TizenBrew for a PStream tile. Publish from that folder with `npm publish --access public`.
|
|
33
|
+
|
|
23
34
|
## Build
|
|
24
35
|
|
|
25
36
|
```bash
|
package/dist/userScript.js
CHANGED
|
@@ -16,8 +16,40 @@
|
|
|
16
16
|
/** @type {HTMLVideoElement | null} */
|
|
17
17
|
var lastKnownVideo = null;
|
|
18
18
|
|
|
19
|
+
var FOCUS_CLASS = 'tizenpstream-focus';
|
|
20
|
+
var lastFocused = null;
|
|
21
|
+
|
|
22
|
+
function injectFocusStyles() {
|
|
23
|
+
if (document.getElementById('tizenpstream-focus-styles')) return;
|
|
24
|
+
var style = document.createElement('style');
|
|
25
|
+
style.id = 'tizenpstream-focus-styles';
|
|
26
|
+
style.textContent = [
|
|
27
|
+
'*:focus { outline: 4px solid #00d4ff !important; outline-offset: 2px !important; box-shadow: 0 0 0 6px rgba(0,212,255,0.4) !important; }',
|
|
28
|
+
'.' + FOCUS_CLASS + ' { outline: 4px solid #00d4ff !important; outline-offset: 2px !important; box-shadow: 0 0 0 6px rgba(0,212,255,0.4) !important; }',
|
|
29
|
+
'video.' + FOCUS_CLASS + ' { outline: 4px solid #00d4ff !important; }'
|
|
30
|
+
].join('\n');
|
|
31
|
+
(document.head || document.documentElement).appendChild(style);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeFocusable(el) {
|
|
35
|
+
if (el && !el.disabled && el.getAttribute('tabindex') === null) {
|
|
36
|
+
try { el.setAttribute('tabindex', '0'); } catch (err) {}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ensureClickablesFocusable() {
|
|
41
|
+
var sel = '[onclick], [role="button"], [role="link"], [role="option"], [data-href], [data-link], [data-action], a, button, [tabindex]';
|
|
42
|
+
var nodes = document.querySelectorAll(sel);
|
|
43
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
44
|
+
var n = nodes[i];
|
|
45
|
+
if (n.offsetParent !== null && (n.getAttribute('tabindex') === null || n.getAttribute('tabindex') === '')) {
|
|
46
|
+
makeFocusable(n);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
19
51
|
function getFocusableSelector() {
|
|
20
|
-
return 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select, textarea, [onclick], video';
|
|
52
|
+
return 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select, textarea, [onclick], [role="button"], [role="link"], [role="option"], [data-href], video';
|
|
21
53
|
}
|
|
22
54
|
|
|
23
55
|
function getFocusables(container) {
|
|
@@ -26,13 +58,29 @@
|
|
|
26
58
|
var list = [];
|
|
27
59
|
for (var i = 0; i < nodes.length; i++) {
|
|
28
60
|
var n = nodes[i];
|
|
29
|
-
if (n.offsetParent !== null && !n.disabled) {
|
|
61
|
+
if (n.offsetParent !== null && !n.disabled && n.offsetWidth > 0 && n.offsetHeight > 0) {
|
|
30
62
|
list.push(n);
|
|
31
63
|
}
|
|
32
64
|
}
|
|
33
65
|
return list;
|
|
34
66
|
}
|
|
35
67
|
|
|
68
|
+
function setFocusRing(el) {
|
|
69
|
+
if (lastFocused && lastFocused !== el) {
|
|
70
|
+
lastFocused.classList.remove(FOCUS_CLASS);
|
|
71
|
+
}
|
|
72
|
+
lastFocused = el;
|
|
73
|
+
if (el) {
|
|
74
|
+
el.classList.add(FOCUS_CLASS);
|
|
75
|
+
el.focus();
|
|
76
|
+
try {
|
|
77
|
+
el.scrollIntoView({ block: 'nearest', behavior: 'auto', inline: 'nearest' });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
el.scrollIntoView(true);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
36
84
|
function getVisibleCenter(el) {
|
|
37
85
|
var r = el.getBoundingClientRect();
|
|
38
86
|
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
@@ -62,10 +110,98 @@
|
|
|
62
110
|
best = cand;
|
|
63
111
|
}
|
|
64
112
|
}
|
|
65
|
-
return best;
|
|
113
|
+
if (best) return best;
|
|
114
|
+
var idx = focusables.indexOf(current);
|
|
115
|
+
if (idx === -1) return focusables[0] || null;
|
|
116
|
+
if (dir === 'right' || dir === 'down') return focusables[idx + 1] || focusables[0] || null;
|
|
117
|
+
if (dir === 'left' || dir === 'up') return focusables[idx - 1] || focusables[focusables.length - 1] || null;
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function registerTVKeys() {
|
|
122
|
+
try {
|
|
123
|
+
if (typeof tizen !== 'undefined' && tizen.tvinputdevice) {
|
|
124
|
+
var keys = [
|
|
125
|
+
'MediaPlayPause', 'MediaPlay', 'MediaPause', 'MediaStop',
|
|
126
|
+
'MediaFastForward', 'MediaRewind', 'MediaTrackNext', 'MediaTrackPrevious'
|
|
127
|
+
];
|
|
128
|
+
for (var i = 0; i < keys.length; i++) {
|
|
129
|
+
try {
|
|
130
|
+
tizen.tvinputdevice.registerKey(keys[i]);
|
|
131
|
+
} catch (err) {}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function trackVideo() {
|
|
138
|
+
var v = document.querySelector('video');
|
|
139
|
+
if (v && v !== lastKnownVideo) {
|
|
140
|
+
lastKnownVideo = v;
|
|
141
|
+
v.addEventListener('play', function () {
|
|
142
|
+
lastKnownVideo = v;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
var KEY_0 = 48;
|
|
148
|
+
var debugOverlay = null;
|
|
149
|
+
|
|
150
|
+
function getTizenDebugInfo(cb) {
|
|
151
|
+
var out = { userAgent: navigator.userAgent, tizen: 'no' };
|
|
152
|
+
try {
|
|
153
|
+
if (typeof tizen !== 'undefined') {
|
|
154
|
+
out.tizen = 'yes';
|
|
155
|
+
if (tizen.systeminfo && tizen.systeminfo.getPropertyValue) {
|
|
156
|
+
tizen.systeminfo.getPropertyValue('BUILD', function (build) {
|
|
157
|
+
out.build = typeof build === 'object' ? JSON.stringify(build) : String(build);
|
|
158
|
+
cb(out);
|
|
159
|
+
}, function () { cb(out); });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {
|
|
164
|
+
out.error = String(e.message || e);
|
|
165
|
+
}
|
|
166
|
+
cb(out);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function showDebugOverlay() {
|
|
170
|
+
getTizenDebugInfo(function (info) {
|
|
171
|
+
if (debugOverlay && debugOverlay.parentNode) {
|
|
172
|
+
debugOverlay.parentNode.removeChild(debugOverlay);
|
|
173
|
+
debugOverlay = null;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
var lines = [
|
|
177
|
+
'PStream mod debug',
|
|
178
|
+
'UA: ' + (info.userAgent || '').substring(0, 80),
|
|
179
|
+
'Tizen: ' + info.tizen,
|
|
180
|
+
info.build ? 'Build: ' + info.build : '',
|
|
181
|
+
info.error ? 'Err: ' + info.error : ''
|
|
182
|
+
].filter(Boolean);
|
|
183
|
+
var div = document.createElement('div');
|
|
184
|
+
div.id = 'tizenpstream-debug-overlay';
|
|
185
|
+
div.style.cssText = 'position:fixed;top:20px;left:20px;right:20px;max-width:90%;background:rgba(0,0,0,0.9);color:#0f0;font:14px monospace;padding:12px;z-index:999999;border:2px solid #0f0;white-space:pre-wrap;word-break:break-all;';
|
|
186
|
+
div.textContent = lines.join('\n');
|
|
187
|
+
div.addEventListener('keydown', function (e) {
|
|
188
|
+
if (e.keyCode === KEY_0 || e.keyCode === KEY_BACK) {
|
|
189
|
+
if (div.parentNode) div.parentNode.removeChild(div);
|
|
190
|
+
debugOverlay = null;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
document.body.appendChild(div);
|
|
194
|
+
debugOverlay = div;
|
|
195
|
+
div.focus();
|
|
196
|
+
});
|
|
66
197
|
}
|
|
67
198
|
|
|
68
199
|
function handleKeydown(e) {
|
|
200
|
+
if (e.keyCode === KEY_0) {
|
|
201
|
+
showDebugOverlay();
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
69
205
|
var key = e.keyCode;
|
|
70
206
|
var dir = null;
|
|
71
207
|
if (key === KEY_LEFT) dir = 'left';
|
|
@@ -76,12 +212,12 @@
|
|
|
76
212
|
if (dir) {
|
|
77
213
|
var focusables = getFocusables();
|
|
78
214
|
var current = document.activeElement;
|
|
79
|
-
if (!current || current === document.body) {
|
|
215
|
+
if (!current || current === document.body || focusables.indexOf(current) === -1) {
|
|
80
216
|
current = focusables[0] || null;
|
|
81
217
|
}
|
|
82
218
|
var next = findNeighbor(current, focusables, dir);
|
|
83
219
|
if (next) {
|
|
84
|
-
next
|
|
220
|
+
setFocusRing(next);
|
|
85
221
|
e.preventDefault();
|
|
86
222
|
e.stopPropagation();
|
|
87
223
|
return false;
|
|
@@ -90,12 +226,25 @@
|
|
|
90
226
|
|
|
91
227
|
if (key === KEY_ENTER) {
|
|
92
228
|
var target = document.activeElement;
|
|
93
|
-
if (target
|
|
229
|
+
if (!target || target === document.body) {
|
|
230
|
+
var focusables = getFocusables();
|
|
231
|
+
if (focusables[0]) setFocusRing(focusables[0]);
|
|
232
|
+
e.preventDefault();
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
if (target.tagName === 'VIDEO') {
|
|
94
236
|
if (target.paused) target.play();
|
|
95
237
|
else target.pause();
|
|
96
238
|
e.preventDefault();
|
|
97
239
|
return false;
|
|
98
240
|
}
|
|
241
|
+
if (target.tagName === 'A' || target.getAttribute('role') === 'button' || target.getAttribute('role') === 'link' || target.onclick || target.getAttribute('onclick')) {
|
|
242
|
+
try {
|
|
243
|
+
target.click();
|
|
244
|
+
} catch (err) {}
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
99
248
|
}
|
|
100
249
|
|
|
101
250
|
if (key === KEY_BACK) {
|
|
@@ -109,37 +258,31 @@
|
|
|
109
258
|
return true;
|
|
110
259
|
}
|
|
111
260
|
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
if (typeof tizen !== 'undefined' && tizen.tvinputdevice) {
|
|
115
|
-
var keys = [
|
|
116
|
-
'MediaPlayPause', 'MediaPlay', 'MediaPause', 'MediaStop',
|
|
117
|
-
'MediaFastForward', 'MediaRewind', 'MediaTrackNext', 'MediaTrackPrevious'
|
|
118
|
-
];
|
|
119
|
-
for (var i = 0; i < keys.length; i++) {
|
|
120
|
-
try {
|
|
121
|
-
tizen.tvinputdevice.registerKey(keys[i]);
|
|
122
|
-
} catch (err) {}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
} catch (err) {}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function trackVideo() {
|
|
129
|
-
var v = document.querySelector('video');
|
|
130
|
-
if (v && v !== lastKnownVideo) {
|
|
131
|
-
lastKnownVideo = v;
|
|
132
|
-
v.addEventListener('play', function () {
|
|
133
|
-
lastKnownVideo = v;
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
261
|
+
var debugShownOnce = false;
|
|
138
262
|
function init() {
|
|
263
|
+
injectFocusStyles();
|
|
264
|
+
ensureClickablesFocusable();
|
|
139
265
|
registerTVKeys();
|
|
140
266
|
document.addEventListener('keydown', handleKeydown, true);
|
|
141
267
|
trackVideo();
|
|
142
268
|
setInterval(trackVideo, 2000);
|
|
269
|
+
setInterval(ensureClickablesFocusable, 3000);
|
|
270
|
+
setTimeout(function () {
|
|
271
|
+
var focusables = getFocusables();
|
|
272
|
+
if (focusables[0]) setFocusRing(focusables[0]);
|
|
273
|
+
}, 500);
|
|
274
|
+
if (!debugShownOnce) {
|
|
275
|
+
debugShownOnce = true;
|
|
276
|
+
setTimeout(function () {
|
|
277
|
+
showDebugOverlay();
|
|
278
|
+
setTimeout(function () {
|
|
279
|
+
if (debugOverlay && debugOverlay.parentNode) {
|
|
280
|
+
debugOverlay.parentNode.removeChild(debugOverlay);
|
|
281
|
+
debugOverlay = null;
|
|
282
|
+
}
|
|
283
|
+
}, 6000);
|
|
284
|
+
}, 1500);
|
|
285
|
+
}
|
|
143
286
|
}
|
|
144
287
|
|
|
145
288
|
if (document.readyState === 'loading') {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hatfiller/tizenpstream",
|
|
3
3
|
"appName": "PStream",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"description": "TizenBrew module for P-Stream (pstream.mov) on Samsung Tizen TVs — TV remote support and focus handling for pre-2022 and older Tizen.",
|
|
6
6
|
"packageType": "mods",
|
|
7
7
|
"websiteURL": "https://pstream.mov",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"type": "git",
|
|
16
16
|
"url": "git+https://github.com/perlytiara/TizenPStream.git"
|
|
17
17
|
},
|
|
18
|
+
"keywords": ["tizenbrew", "tizen", "pstream", "samsung-tv"],
|
|
18
19
|
"keys": [
|
|
19
20
|
"MediaPlayPause",
|
|
20
21
|
"MediaPlay",
|