@krx3d/tizentube2 1.16.30 → 1.30.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/.github/FUNDING.yml +2 -1
- package/.github/assets/profiles.xml +18 -0
- package/.github/scripts/build-release-notes.sh +113 -0
- package/.github/workflows/build-publish-cleanup.yml +674 -0
- package/.github/workflows/build-standalone-release.yaml +305 -0
- package/.github/workflows/claude.yml +58 -0
- package/.github/workflows/codeql.yml +44 -0
- package/.github/workflows/upstream-sync.yml +165 -0
- package/AGENTS.md +898 -0
- package/README.md +202 -12
- package/dist/service.js +15267 -15966
- package/dist/userScript.js +3 -13
- package/package.json +4 -5
- package/scripts/log-receiver/receiver.ps1 +116 -0
- package/scripts/tampermonkey/tizentube-loader.user.js +18 -0
- package/scripts/tampermonkey/tizentube-log-button.user.js +54 -0
- package/standalone/config.xml +41 -0
- package/standalone/icon.png +0 -0
- package/standalone/icon_16b9.png +0 -0
- package/standalone/index.html +240 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
# AGENTS.md — TizenTube (KrX3D fork)
|
|
2
|
+
|
|
3
|
+
This file is for AI coding agents (and humans) working in this repository. It
|
|
4
|
+
describes the actual current state of the codebase — keep it in sync when
|
|
5
|
+
you make structural changes. A stale version of this file is worse than no
|
|
6
|
+
file at all: update it in the same PR as the change that makes it stale.
|
|
7
|
+
|
|
8
|
+
## What this project is
|
|
9
|
+
|
|
10
|
+
TizenTube is an ad-blocking / SponsorBlock-enabled mod for the YouTube TV app
|
|
11
|
+
on Samsung Tizen TVs. It ships as a userscript (`dist/userScript.js`) that
|
|
12
|
+
gets injected into YouTube TV's page context, plus a small DIAL service
|
|
13
|
+
(`dist/service.js`) that lets the app be cast/launched.
|
|
14
|
+
|
|
15
|
+
This is `KrX3D/TizenTube`, a fork of `reisxd/TizenTube`. It has diverged
|
|
16
|
+
substantially — do not assume upstream's code, conventions, or file layout
|
|
17
|
+
still apply; verify against this repo directly.
|
|
18
|
+
|
|
19
|
+
There are **two independent ways this mod actually reaches a TV**, and
|
|
20
|
+
they matter for understanding "what code path am I changing":
|
|
21
|
+
|
|
22
|
+
1. **TizenBrew-injected (the original, still primary path).** A separate
|
|
23
|
+
host app, [TizenBrew](https://github.com/KrX3D/TizenBrew), uses Chrome
|
|
24
|
+
DevTools Protocol to inject `dist/userScript.js` directly into Cobalt's
|
|
25
|
+
(YouTube TV's browser engine) JS context. TizenBrew is not part of this
|
|
26
|
+
repo. The published npm package (`@krx3d/tizentube2`) is what TizenBrew
|
|
27
|
+
fetches — see "npm publish" below.
|
|
28
|
+
2. **Standalone mode (`standalone/`).** A self-contained, separately
|
|
29
|
+
installable Tizen app that doesn't need TizenBrew at all. See
|
|
30
|
+
"Standalone mode" section below — it's non-trivial and has its own
|
|
31
|
+
quirks, including an unresolved packaging/signing issue (see "Known
|
|
32
|
+
unresolved issues").
|
|
33
|
+
|
|
34
|
+
Both paths ultimately load the *same* `mods/userScript.js` bundle from the
|
|
35
|
+
npm CDN at runtime — standalone mode does not bundle a copy of the mod
|
|
36
|
+
into its `.wgt`. So most feature work in `mods/` automatically applies to
|
|
37
|
+
both paths without any standalone-specific changes.
|
|
38
|
+
|
|
39
|
+
## Repo layout
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
package.json # root — published to npm as @krx3d/tizentube2
|
|
43
|
+
README.md
|
|
44
|
+
AGENTS.md # this file
|
|
45
|
+
dist/ # build output, partially committed (see CI section)
|
|
46
|
+
userScript.js # built mod bundle (mods/ → rollup)
|
|
47
|
+
service.js # built DIAL service (service/ → rollup)
|
|
48
|
+
|
|
49
|
+
mods/ # the userscript — THIS IS WHERE MOST FEATURE WORK HAPPENS
|
|
50
|
+
package.json (package name: @tizentube/mods, not published itself)
|
|
51
|
+
userScript.js # entry point: imports every feature/ui module
|
|
52
|
+
config.js # config schema + configRead/configWrite/configChangeEmitter
|
|
53
|
+
resolveCommand.js # central dispatcher: opens modals, changes client settings, etc.
|
|
54
|
+
rollup.config.js
|
|
55
|
+
tiny-sha256.js, domrect-polyfill.js, spatial-navigation-polyfill.js
|
|
56
|
+
features/ # one file per feature (see "Feature map" below)
|
|
57
|
+
ui/ # settings menu, themes, custom player UI, etc.
|
|
58
|
+
translations/
|
|
59
|
+
index.js # i18next init
|
|
60
|
+
i18nResources.js # registers every resources/*.json file
|
|
61
|
+
language-names.js
|
|
62
|
+
resources/*.json # 27 language files — see "Translations" section
|
|
63
|
+
utils/
|
|
64
|
+
ASTParser.js # esprima/estraverse-based pattern finder, used to
|
|
65
|
+
# locate YouTube's internal functions/classes that
|
|
66
|
+
# aren't exposed by name (YouTube's TV app code is
|
|
67
|
+
# minified/obfuscated and changes over time)
|
|
68
|
+
|
|
69
|
+
service/ # DIAL service (cast/launch support) — separate from mods/
|
|
70
|
+
service.js
|
|
71
|
+
rollup.config.js
|
|
72
|
+
package.json
|
|
73
|
+
|
|
74
|
+
standalone/ # standalone installable app — see "Standalone mode" below
|
|
75
|
+
config.xml # Tizen app manifest — app id: krx3dTtSt1.TizenTubeStandalone
|
|
76
|
+
index.html # loading screen, decides injector vs proxy path
|
|
77
|
+
icon.png, icon_16b9.png
|
|
78
|
+
service/ # standalone's own local proxy + CDP injector (NOT mods/'s service/)
|
|
79
|
+
index.js # Express proxy: rewrites youtube.com/tv, injects userscript <script> tag
|
|
80
|
+
injector.js # CDP-based injector (same technique TizenBrew itself uses)
|
|
81
|
+
bootstrap.js # plain ES5 (no ncc/bundling) — copied through as-is to dist/index.js,
|
|
82
|
+
# the actual tizen:service entry point. require()s dist/bundle.js
|
|
83
|
+
# (the real ncc bundle of index.js/injector.js/express/etc, output
|
|
84
|
+
# to a *different* filename on purpose) inside a try/catch, so a
|
|
85
|
+
# SyntaxError while Node parses that huge bundle — which can't be
|
|
86
|
+
# caught from inside the bundle itself, since parse errors happen
|
|
87
|
+
# before any of that file's own code runs — is at least catchable
|
|
88
|
+
# and loggable here. See "Known unresolved issues" re: Tizen 5.5.
|
|
89
|
+
build-service.js # ncc-bundles index.js, regex-patches known bundled-dep bugs, Babel-
|
|
90
|
+
# transpiles the bundle for Node 4.4.3, → dist/bundle.js; copies
|
|
91
|
+
# bootstrap.js → dist/index.js unmodified
|
|
92
|
+
package.json
|
|
93
|
+
|
|
94
|
+
.github/
|
|
95
|
+
workflows/
|
|
96
|
+
build-publish-cleanup.yml # push to main: bump version, build, npm publish, cleanup old npm versions
|
|
97
|
+
build-standalone-release.yaml # builds/signs standalone/ into a .wgt via Tizen Studio, GitHub Release
|
|
98
|
+
codeql.yml # security scanning
|
|
99
|
+
claude.yml
|
|
100
|
+
assets/
|
|
101
|
+
profiles.xml # Tizen Studio signing profile for standalone (see Known issues)
|
|
102
|
+
|
|
103
|
+
scripts/tampermonkey/ # local Chrome-based dev/test loader — see README.md
|
|
104
|
+
scripts/log-receiver/ # PC-side PS1 receiver for logServer.js's remote logging (see Feature map)
|
|
105
|
+
scripts/pc-installer/ # PC-side install/update script (SDB from the PC, no on-device installer app
|
|
106
|
+
# needed) — workaround for the Host PC IP 127.0.0.1 conflict with standalone's
|
|
107
|
+
# proxy path; in progress, see AGENTS.md "Known unresolved issues"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Configuration system (`mods/config.js`)
|
|
111
|
+
|
|
112
|
+
- Storage: `localStorage['ytaf-configuration']`, JSON-serialized.
|
|
113
|
+
- API: `configRead(key)`, `configWrite(key, value)`,
|
|
114
|
+
`configChangeEmitter.addEventListener('configChange', cb)`.
|
|
115
|
+
- `configRead` auto-populates missing keys from `defaultConfig` and warns
|
|
116
|
+
once per key — don't add a config key without adding its default here.
|
|
117
|
+
- Current default keys (check `mods/config.js` directly for the live list —
|
|
118
|
+
this repo adds new ones regularly, e.g. `enableAIAskButton`, `spoofViewport`,
|
|
119
|
+
`disabledSidebarContents`, `hiddenLibraryTabIds`, `launchToOnStartup`,
|
|
120
|
+
clock/dimming/debug-console settings). **Do not trust a cached list of
|
|
121
|
+
config keys from memory or an old doc — read the file.**
|
|
122
|
+
|
|
123
|
+
## Feature map (`mods/features/*.js`)
|
|
124
|
+
|
|
125
|
+
| File | What it does |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `adblock.js` | Patches `JSON.parse` to strip ad placements/slots from YouTube's data before the app renders it. Also runs DeArrow, hqify, tile processing, library-tab hiding, watch-progress caching, and most per-page-type UI patches from the same hook. **Everything in the patch body is wrapped in try/catch with `parse.error` logging (`appendFileOnlyLog`) and always returns the original parsed object on failure** — a thrown error here previously broke `JSON.parse` for the entire page; do not remove that wrapper. |
|
|
128
|
+
| `sponsorblock.js` | SponsorBlock integration. Uses a *scheduled* `setTimeout` (`scheduleSkip()`) timed to the exact next segment boundary, adjusted for `video.playbackRate`, rather than polling — don't reintroduce a polling/`timeupdate`-based approach without a specific reason. `buildOverlay()` retries every 100ms if the slider DOM element isn't ready yet instead of giving up. |
|
|
129
|
+
| `standaloneUserscript.js` | Only active when `window.location.hostname === 'localhost'` (i.e. running inside standalone mode's local proxy). Redirects `fetch`/`XHR`/`sendBeacon`/img/script `src` for YouTube/Google hosts through the local CORS-bypass proxy. Has its own hostname allowlist — keep it in sync with `standalone/service/index.js`'s server-side allowlist if you touch either. |
|
|
130
|
+
| `viewportSpoofing.js` | Overrides `window.screen` + `matchMedia` width/height queries to report a different resolution to YouTube (`spoofViewport` config: disabled/2160p/1440p/1080p). For TVs that under-report their real decode capability. |
|
|
131
|
+
| `pictureInPicture.js` | PiP mode. |
|
|
132
|
+
| `updater.js` | Checks for and applies TizenTube updates. |
|
|
133
|
+
| `moreSubtitles.js` | Adds extra subtitle language options. |
|
|
134
|
+
| `preferredVideoQuality.js` | Applies `preferredVideoQuality`/`videoPreferredCodec` on playback start. |
|
|
135
|
+
| `autoFrameRate.js` | Matches TV frame rate to video (Tizen-specific `h5vcc.tizentube.SetFrameRate` API when available). |
|
|
136
|
+
| `userAgentSpoofing.js` | UA string overrides. |
|
|
137
|
+
| `enableFeatures.js` | Force-enables YouTube features normally gated because YT treats the TV as a low-end device. |
|
|
138
|
+
| `hideWatched.js` | Watched-video hiding/threshold logic; also hosts shared logging helpers (`appendFileOnlyLog`) and playlist button injection used by `playlistContinue.js`. |
|
|
139
|
+
| `playlistContinue.js` | "Continue playlist" / queued-videos behavior, built on `hideWatched.js`'s helpers. |
|
|
140
|
+
| `libraryTabHider.js` | Hides library tabs per `hiddenLibraryTabIds`. |
|
|
141
|
+
| `specialPlaylistHider.js` | Hides Watch Later / Liked Videos shelves/tiles per `hiddenSpecialPlaylist*` config. |
|
|
142
|
+
| `shorts.js` | Shorts-related behavior (`enableShorts`). |
|
|
143
|
+
| `videoQueuing.js` | Manual video queue feature. |
|
|
144
|
+
| `playlistBatchCollect.js` | Batch-collects playlist items (`enablePlaylistBatchCollect`). |
|
|
145
|
+
| `logServer.js` | Optional remote log server (`logServerEnabled`) for on-device debugging. In TizenBrew mode, relays via TizenBrew's own `127.0.0.1:8081` service + CDP-queue fallback. In standalone mode (`window.location.hostname === 'localhost'`), instead POSTs to the standalone service's own `POST /tizentube/log` (see `standalone/service/index.js`), which relays to the PC receiver at `logServerHost`:`logServerPort` (same `/tv-log` path/JSON shape as TizenBrew's `remoteLogger.js`, so the receiver script at `scripts/log-receiver/receiver.ps1` works for both). Host/port are set via the RED-key theme overlay (`mods/ui/ui.js`), standalone-only fields — there's no free-text entry in the native TV settings menu. The standalone *service itself* also self-logs its own lifecycle (startup, uncaught exceptions, DIAL service load, proxy errors) unconditionally to a hardcoded `DEFAULT_LOG_HOST`/`DEFAULT_LOG_PORT` in `standalone/service/index.js`, independent of any page config — added specifically because the service can crash before the page/userscript ever loads, at which point page-driven config never gets read. `relayLog` (the single choke point both the page-side proxy fetch and the CDP-queue drain ultimately funnel through) tracks per-`host:port` availability: after the first failed attempt (fast `ECONNREFUSED`, or a 3s timeout for a genuinely unreachable host that would otherwise hang far longer at the OS TCP-connect level) it skips all further attempts to that target for the rest of the process, so an unreachable/not-running PC receiver can't add repeated delay to every subsequent log call. |
|
|
146
|
+
| `visualConsole.js` | On-screen debug console (`enableDebugConsole`) — shows version via `../../package.json`, executes commands via `resolveCommand`. |
|
|
147
|
+
|
|
148
|
+
## UI map (`mods/ui/*.js`)
|
|
149
|
+
|
|
150
|
+
`settings.js` is the main settings menu tree — every entry is
|
|
151
|
+
`{ name: t('...'), icon: 'ICON_NAME', ... }`. **Icon names must already be
|
|
152
|
+
used elsewhere in `settings.js` (verified with a grep) before you use them
|
|
153
|
+
— YouTube TV's icon set is closed and many plausible Material icon names
|
|
154
|
+
(e.g. `THUMB_UP`) silently render no icon at all if they don't exist in the
|
|
155
|
+
app.** Don't guess an icon name; check first.
|
|
156
|
+
|
|
157
|
+
Other files: `ui.js` (top-level UI patches), `ytUI.js` (toast/button
|
|
158
|
+
helpers shared across features), `theme.js` (color customization),
|
|
159
|
+
`speedUI.js`, `chapters.js`, `customUI.js` (player transport-controls
|
|
160
|
+
button filtering — e.g. `enableAIAskButton`/`enableSuperThanksButton`),
|
|
161
|
+
`customGuideAction.js`, `customCommandExecution.js`, `customYTSettings.js`,
|
|
162
|
+
`disableWhosWatching.js`, `clock.js`.
|
|
163
|
+
|
|
164
|
+
## Translations (`mods/translations/resources/*.json`)
|
|
165
|
+
|
|
166
|
+
27 language files. **`en.json` and `de.json` are the only two kept fully in
|
|
167
|
+
sync with every new feature** — when you add a user-facing string, update
|
|
168
|
+
both. Other languages are community-contributed snapshots; don't assume
|
|
169
|
+
they have a given nested block (e.g. the `nav.*` sidebar-icon-name block
|
|
170
|
+
under `uiSettings.options` only exists in `en`/`de` — other languages
|
|
171
|
+
correctly fall back to English for it via i18next, which is fine).
|
|
172
|
+
`i18nResources.js` registers every file in `resources/` — a new language
|
|
173
|
+
file needs a matching import/registration there too.
|
|
174
|
+
|
|
175
|
+
## Standalone mode (`standalone/`)
|
|
176
|
+
|
|
177
|
+
A fully separate installable Tizen app (own `config.xml`, own Tizen app
|
|
178
|
+
identity `krx3dTtSt1.TizenTubeStandalone` / package `krx3dTtSt1` — chosen
|
|
179
|
+
deliberately distinct from upstream's `xvvl3S1TT1`, which several other
|
|
180
|
+
forks reuse unchanged, causing them to collide with each other's installs
|
|
181
|
+
on the same device).
|
|
182
|
+
|
|
183
|
+
On launch (`standalone/index.html`) it asks its own local service
|
|
184
|
+
(`standalone/service/index.js`, an Express app on `localhost:8099`)
|
|
185
|
+
whether Tizen debugging (SDB) is reachable:
|
|
186
|
+
|
|
187
|
+
- **If yes:** uses `standalone/service/injector.js` — the *same* CDP
|
|
188
|
+
technique TizenBrew itself uses (hook `Runtime.executionContextCreated`,
|
|
189
|
+
evaluate the userscript before the page's own scripts run, then
|
|
190
|
+
navigate). The standalone app exits once the injected session takes over.
|
|
191
|
+
- **If no:** falls back to navigating its own webview to
|
|
192
|
+
`http://localhost:8099/tv`. The local Express proxy fetches the real
|
|
193
|
+
`youtube.com/tv`, rewrites resource URLs so everything routes back
|
|
194
|
+
through `localhost:8099` (avoiding CORS), and injects the userscript
|
|
195
|
+
`<script>` tag right after `<body>` opens (must run before YouTube's own
|
|
196
|
+
scripts, since the ad blocker's `JSON.parse` patch needs to be active
|
|
197
|
+
before YouTube parses its initial player data). This proxy also
|
|
198
|
+
enforces a hostname allowlist before forwarding any `/cors-bypass/`
|
|
199
|
+
request — never remove that; it's what stops the proxy being an open
|
|
200
|
+
relay to arbitrary hosts.
|
|
201
|
+
|
|
202
|
+
Both paths fetch the userscript live from
|
|
203
|
+
`https://cdn.jsdelivr.net/npm/@krx3d/tizentube2/dist/userScript.js` (with
|
|
204
|
+
an `unpkg.com` fallback) — **never hardcode `@foxreis/tizentube`** (that's
|
|
205
|
+
upstream's package; several forks that copy-paste from upstream get this
|
|
206
|
+
wrong and end up running unmodified upstream code inside "their" fork).
|
|
207
|
+
|
|
208
|
+
`standalone/service/`'s own source files (`index.js`, `injector.js`,
|
|
209
|
+
`build-service.js`, `package.json`) are **not** the
|
|
210
|
+
same thing as top-level `service/` (the DIAL service) — the standalone
|
|
211
|
+
build step builds top-level `service/` first because
|
|
212
|
+
`standalone/service/index.js` wraps and `require()`s its output
|
|
213
|
+
(`dist/service.js`).
|
|
214
|
+
|
|
215
|
+
## CI / build (`.github/workflows/`)
|
|
216
|
+
|
|
217
|
+
- **`build-publish-cleanup.yml`** — on push to `main`: bumps
|
|
218
|
+
`package.json`'s version (+10 patch, rolling over to minor at 1000),
|
|
219
|
+
builds `mods/` and `service/`, commits the bump + built `dist/*` files
|
|
220
|
+
back to `main` with `[skip ci]`, then publishes to npm and removes old
|
|
221
|
+
npm versions. This is why `dist/userScript.js` is partially committed
|
|
222
|
+
despite being build output — CI keeps it in sync, don't hand-edit it.
|
|
223
|
+
- **`build-standalone-release.yaml`** — triggered automatically via
|
|
224
|
+
`workflow_run` right after the above finishes successfully (so
|
|
225
|
+
standalone's version always matches the just-published userscript
|
|
226
|
+
version — it reads `package.json` and writes the same number into
|
|
227
|
+
`standalone/config.xml` before packaging), or by a `v*.*.*` tag push, or
|
|
228
|
+
manually. Builds `mods/`, `service/`, and `standalone/service/`, then
|
|
229
|
+
signs `standalone/` into a `.wgt` using **Tizen Studio's own CLI**
|
|
230
|
+
(`tizen build-web` + `tizen package`, via `.github/assets/profiles.xml`)
|
|
231
|
+
— see "Known unresolved issues", this is mid-troubleshooting.
|
|
232
|
+
- **`codeql.yml`** — security scanning on PRs; check its findings before
|
|
233
|
+
merging anything touching `standalone/service/` (the proxy) or auth-ish
|
|
234
|
+
code — false positives happen (e.g. SSRF flags on the proxy's
|
|
235
|
+
intentional catch-all forwarding) but real findings do too (an actual
|
|
236
|
+
open-proxy gap was found and fixed this way once).
|
|
237
|
+
|
|
238
|
+
Local verification without waiting on CI:
|
|
239
|
+
```
|
|
240
|
+
cd mods && npm ci && npx rollup -c # builds ../dist/userScript.js
|
|
241
|
+
cd service && npm ci && npx rollup -c # builds ../dist/service.js
|
|
242
|
+
cd standalone/service && npm install && npm run build # builds dist/index.js
|
|
243
|
+
```
|
|
244
|
+
Revert `dist/*` afterward if you're not intentionally changing it —
|
|
245
|
+
CI owns that file's committed state.
|
|
246
|
+
|
|
247
|
+
## Known unresolved issues (as of writing)
|
|
248
|
+
|
|
249
|
+
**Standalone `.wgt` install failure — RESOLVED (2026-08-02).** Kept here for
|
|
250
|
+
the history, since it took several disproven theories to get there:
|
|
251
|
+
|
|
252
|
+
1. Signing with `tizenjs` (unofficial community packaging tool) + its
|
|
253
|
+
auto-downloaded distributor cert → installed fine as an *update* but
|
|
254
|
+
was rejected on a fresh install (`invalid certificate chain`). Root
|
|
255
|
+
cause was assumed to be the cert's expiration (Samsung's public sample
|
|
256
|
+
cert, expired 2022) — **disproven**: a sibling project in this same
|
|
257
|
+
author's ecosystem (`TizenYouTube`) installs fine fresh using that
|
|
258
|
+
*exact same expired cert*, just packaged with official Tizen Studio
|
|
259
|
+
instead of `tizenjs`.
|
|
260
|
+
2. Switched to Samsung's official Tizen Studio CLI (`tizen build-web` +
|
|
261
|
+
`tizen package`), matching the working sibling projects
|
|
262
|
+
(`TizenBrew`, `TizenBrewInstaller`, `TizenYouTube` all use it) — the
|
|
263
|
+
structurally correct approach. Packaging then hit
|
|
264
|
+
`CertificationException: Invaild password` while loading the author
|
|
265
|
+
certificate, even though the same password/cert worked fine via
|
|
266
|
+
`tizenjs`.
|
|
267
|
+
3. Attempted fix: re-encrypt the author `.p12` to an older,
|
|
268
|
+
Java-compatible PKCS#12 cipher (`PBE-SHA1-3DES`) before Tizen Studio
|
|
269
|
+
reads it, since Tizen Studio's bundled Java crypto can't read modern
|
|
270
|
+
OpenSSL 3.x's default AES-256 PKCS#12 encryption. Didn't resolve it on
|
|
271
|
+
its own — the `-legacy` flag needed for that re-encrypt/export step
|
|
272
|
+
was missing from the *earlier* decrypt-to-PEM step too, so the
|
|
273
|
+
pipeline still failed reading the incoming cert before it ever got to
|
|
274
|
+
re-encrypting it.
|
|
275
|
+
4. **Actual fix, two parts:**
|
|
276
|
+
- Build-side: add `-legacy` to *both* `openssl pkcs12` invocations
|
|
277
|
+
(decrypt-to-PEM and re-encrypt-to-export), not just the export one —
|
|
278
|
+
some freshly-generated certs' PKCS7 "Encrypted data" bag uses
|
|
279
|
+
`RC2-40-CBC`, which OpenSSL 3.x's default provider can't even read
|
|
280
|
+
without `-legacy`, independent of the AES-256/3DES issue above.
|
|
281
|
+
- Cert-side: the specific `.p12` reused from the `TizenYouTube` sibling
|
|
282
|
+
project (see point 1) never actually worked *as an install*, even
|
|
283
|
+
after the build-side fix — a **fresh, dedicated** author certificate
|
|
284
|
+
(never used to install a different app on the same TV) was required.
|
|
285
|
+
Reusing a cert across different installed apps on one TV appears to
|
|
286
|
+
cause on-device install problems distinct from any build/signing
|
|
287
|
+
error — consistent with this repo's existing convention of never
|
|
288
|
+
reusing app identities/certs across projects (see "Conventions"
|
|
289
|
+
below). Generate a new cert per app, don't reuse one from another
|
|
290
|
+
project even if it builds/signs without error.
|
|
291
|
+
- Also worth knowing: GitHub's **Re-run failed jobs** stays pinned to
|
|
292
|
+
the workflow file as it existed at that run's original commit — it
|
|
293
|
+
will *not* pick up a workflow fix merged afterward. Use **Run
|
|
294
|
+
workflow** (`workflow_dispatch`) or a new tag to actually test a fix.
|
|
295
|
+
|
|
296
|
+
**Standalone runtime issues on-device — open, one fix attempted.**
|
|
297
|
+
Reported 2026-08-02 after the install issue above was fixed:
|
|
298
|
+
- **Tizen 5.5:** app shows the TizenTube splash + loading bar, then hangs
|
|
299
|
+
indefinitely — never finishes loading. Also reproduces with upstream's
|
|
300
|
+
own published build, so this isn't a regression from anything in this
|
|
301
|
+
fork; likely an old-WebKit-engine incompatibility somewhere in the
|
|
302
|
+
proxy/injection path. Not yet root-caused.
|
|
303
|
+
- **Tizen 6.5:** regression specific to this fork's standalone build —
|
|
304
|
+
upstream's `.wgt` works fine on the same TV, but this fork's build
|
|
305
|
+
crash-loops: after a TV reboot the app opens then immediately closes;
|
|
306
|
+
on subsequent launches it loads then restarts, repeating; sometimes it
|
|
307
|
+
fails to open at all.
|
|
308
|
+
|
|
309
|
+
**Babel transpilation ruled out.** This fork's `standalone/service/` had
|
|
310
|
+
a Babel transpile step (PR #607) that ran over the *entire*
|
|
311
|
+
`standalone/service/` directory including `node_modules` (minus a
|
|
312
|
+
handful of excluded packages), transpiling `express` and its ~30
|
|
313
|
+
transitive dependencies — code never written or tested with that in
|
|
314
|
+
mind, and it never actually fixed the Tizen 5.5 hang it was added for.
|
|
315
|
+
Reverted back to upstream's approach (`ncc`-bundle `index.js` directly,
|
|
316
|
+
no Babel) and retested on-device — **no change on either TV**, so Babel
|
|
317
|
+
was not the (or not the only) cause of either issue. Both problems are
|
|
318
|
+
still fully open.
|
|
319
|
+
|
|
320
|
+
**Next step: remote logging added for standalone mode** (2026-08-02),
|
|
321
|
+
specifically to stop guessing blind. `standalone/service/index.js` now
|
|
322
|
+
self-logs its own lifecycle (process start, `app.listen`
|
|
323
|
+
success/error, DIAL service `require()` success/error, `uncaughtException`/
|
|
324
|
+
`unhandledRejection`, proxy errors, first `/tv` request received) to a
|
|
325
|
+
hardcoded `DEFAULT_LOG_HOST`/`DEFAULT_LOG_PORT` (currently
|
|
326
|
+
`192.168.50.57:3030`) — unconditional, no reliance on the page/userscript
|
|
327
|
+
ever loading, since the 6.5 crash happens before that point. The
|
|
328
|
+
userscript's existing `logServer.js` also gained a standalone-mode path
|
|
329
|
+
(`POST /tizentube/log` on the local service, which relays to
|
|
330
|
+
`logServerHost`:`logServerPort`, configurable via the RED-key theme
|
|
331
|
+
overlay) for once the page *does* load. Same `/tv-log` payload shape as
|
|
332
|
+
TizenBrew's `remoteLogger.js`, so the receiver script at
|
|
333
|
+
`scripts/log-receiver/receiver.ps1` works unmodified for both.
|
|
334
|
+
|
|
335
|
+
**Tizen 6.5: DIAL service crash fixed, but that wasn't the whole story.**
|
|
336
|
+
`dist/service.js`'s DIALServer constructor was throwing
|
|
337
|
+
`crypto.getRandomValues() not supported` (uuid's browser-targeted rng,
|
|
338
|
+
no Web Crypto on Tizen's old Node service runtime) — fixed by
|
|
339
|
+
polyfilling `global.crypto.getRandomValues` with Node's own
|
|
340
|
+
`crypto.randomBytes`. Confirmed via a real device log: `dist/service.js`
|
|
341
|
+
now loads cleanly. But the user then clarified the actual observed
|
|
342
|
+
pattern more precisely: **both TVs fail to send any logs (or reach the
|
|
343
|
+
service) on the very first launch, and only work after the app
|
|
344
|
+
auto-relaunches once** — 6.5 reliably gets through that one retry, 5.5
|
|
345
|
+
never does (matches the original bug report: the splash/progress bar
|
|
346
|
+
just hangs forever on 5.5). So this looks like a **first-launch
|
|
347
|
+
service-startup timing race**, not (only) the DIAL crash.
|
|
348
|
+
|
|
349
|
+
Comparing `standalone/config.xml` against `TizenBrew`'s own config.xml
|
|
350
|
+
— TizenBrew reliably starts its service on *both* TVs, no retry needed —
|
|
351
|
+
found it has a `<tizen:app-control>` block with Samsung's
|
|
352
|
+
`eden_resume` operation and `reload="disable"` that ours was completely
|
|
353
|
+
missing, plus `recorder`/`mediacapture`/`unlimitedstorage` privileges
|
|
354
|
+
that PR #608 had removed as "unused" (present in both TizenBrew's and
|
|
355
|
+
upstream's TizenTube's config.xml — removing them may have affected
|
|
356
|
+
more than privilege gating). Added the `app-control`/`eden_resume` block
|
|
357
|
+
and restored those three privileges to match. Also restructured
|
|
358
|
+
`standalone/service/index.js` so logging infrastructure (which only
|
|
359
|
+
needs Node's core `http` module) is set up *before* requiring
|
|
360
|
+
`express`/`node-fetch`/`./injector.js`, with each of those requires
|
|
361
|
+
wrapped individually (`safeRequire`) — previously, if any of those threw
|
|
362
|
+
synchronously (plausible on 5.5's much older Node, since these packages
|
|
363
|
+
likely assume newer Node APIs no amount of transpilation can add), it
|
|
364
|
+
would happen before any logging existed to catch it, which is
|
|
365
|
+
consistent with 5.5's total blackout. **None of this is confirmed
|
|
366
|
+
on-device yet** — next session needs a real retest on both TVs to see
|
|
367
|
+
whether first-launch startup is now reliable, and if 5.5 still produces
|
|
368
|
+
zero logs, whether `safeRequire`'s per-require logging finally shows
|
|
369
|
+
which one fails there.
|
|
370
|
+
|
|
371
|
+
Also found, still unresolved/unconfirmed either way: `app.onRequest is
|
|
372
|
+
not a function` fires repeatedly every launch (from Tizen's own service
|
|
373
|
+
runner) but doesn't crash the process — likely benign, since neither
|
|
374
|
+
our nor upstream's `standalone/service/index.js` implements that
|
|
375
|
+
handler and upstream still works; and one `Cannot find context with
|
|
376
|
+
specified id` from the CDP injector, a normal context-invalidation race
|
|
377
|
+
that seems to resolve itself once a fresh execution context appears.
|
|
378
|
+
Neither is proven unrelated to the retry-need — worth re-reading the
|
|
379
|
+
log after the startup-timing fixes above land, not assumed irrelevant.
|
|
380
|
+
|
|
381
|
+
**Standalone-mode detection was also wrong for the CDP-injection path.**
|
|
382
|
+
`window.location.hostname === 'localhost'` (used by `logServer.js` and
|
|
383
|
+
the RED-key theme overlay in `mods/ui/ui.js` to decide whether to show/
|
|
384
|
+
use the standalone log relay) only covers the *proxy* path. The
|
|
385
|
+
CDP-injection path (`injector.js`) navigates Cobalt directly to real
|
|
386
|
+
`https://youtube.com/tv` — this TV has debug mode reachable, so it
|
|
387
|
+
always uses that path — meaning `hostname` is never `'localhost'`
|
|
388
|
+
there, so the host/port fields never showed and page-level logs never
|
|
389
|
+
sent (they fell through to the TizenBrew-only branch, which has nothing
|
|
390
|
+
listening on `127.0.0.1:8081` in pure standalone, and silently
|
|
391
|
+
queued/dropped). Fixed by having `injector.js` set `window.__ttStandalone
|
|
392
|
+
= true` before evaluating the userscript, and checking that in addition
|
|
393
|
+
to the hostname in both places.
|
|
394
|
+
|
|
395
|
+
**Confirmed on-device (2026-08-02): the mixed-content risk was real.**
|
|
396
|
+
Host/port showed correctly and a manual test log appeared in
|
|
397
|
+
TizenTube's own on-screen debug console (unrelated to network delivery),
|
|
398
|
+
but nothing reached the PC receiver — the direct fetch from
|
|
399
|
+
`https://youtube.com` (page origin) to `http://localhost:8099` (target)
|
|
400
|
+
is cross-origin *and* HTTPS-page-to-HTTP-target, which Cobalt blocks
|
|
401
|
+
silently. Fixed by not attempting that fetch on this path at all:
|
|
402
|
+
`logServer.js` now pushes into `window.__ttLogQueue` instead (tagged
|
|
403
|
+
with `__ttLogHost`/`__ttLogPort`), and `injector.js` drains it once a
|
|
404
|
+
second over the same CDP connection already open for injection
|
|
405
|
+
(`pollLogQueue`, threaded through `startDebugger`/`connectToDebugger`) —
|
|
406
|
+
same technique TizenBrew's own service uses for the equivalent problem.
|
|
407
|
+
The proxy path (`hostname === 'localhost'`) is unaffected — same-origin
|
|
408
|
+
plain HTTP, no restrictions — and still uses the direct fetch.
|
|
409
|
+
|
|
410
|
+
**Found a bug in `pollLogQueue` itself (2026-08-03), via a #631 retest
|
|
411
|
+
on Tizen 6.5 that still hung after "Taking CDP-injection path."** The
|
|
412
|
+
`setInterval` had no lifecycle tied to the CDP connection it depends
|
|
413
|
+
on — once that connection closed (page navigation, app exit, etc.),
|
|
414
|
+
every subsequent tick threw an unhandled `WebSocket.send... not
|
|
415
|
+
opened` rejection on the *same* `Chrome.send`/`enqueueCommand` path
|
|
416
|
+
the real userscript-injection `evaluate()` call uses. Timing lined up
|
|
417
|
+
exactly with the poll's first tick, not the injection call itself.
|
|
418
|
+
Fixed two ways: the interval now clears itself on the client's
|
|
419
|
+
`'disconnect'` event, and as a defensive fallback (in case that event
|
|
420
|
+
doesn't fire reliably) after 3 consecutive failures; it also no longer
|
|
421
|
+
starts immediately alongside `Page.navigate()` — only after the first
|
|
422
|
+
successful injection `evaluate()`, so it can't interfere with that
|
|
423
|
+
critical early window at all, by construction rather than just cleanup.
|
|
424
|
+
**Not yet retested on-device — for either this fix or the CDP delivery
|
|
425
|
+
mechanism itself.** Note: the pre-existing `Cannot find context with
|
|
426
|
+
specified id` / `not opened` timing races in the injection handshake
|
|
427
|
+
itself (separate from this polling bug) were already known and
|
|
428
|
+
unresolved before any of this logging work existed — this fix removes
|
|
429
|
+
one source of instability but may not be the whole story for why 6.5's
|
|
430
|
+
CDP handoff still isn't fully reliable.
|
|
431
|
+
|
|
432
|
+
**Separately found, on Tizen 6.5 (2026-08-03): a real dead-end bug in
|
|
433
|
+
`useInjectorOrProxy()`, unrelated to anything above.** A device log
|
|
434
|
+
showed `getState` returning `{"canConnectToDaemon":true,"isConnecting":
|
|
435
|
+
true}` — a state `standalone/index.html`'s `if (canConnectToDaemon &&
|
|
436
|
+
!isConnecting) {...} else if (!canConnectToDaemon) {...}` had no branch
|
|
437
|
+
for at all, so it silently did nothing. `isConnecting` is a
|
|
438
|
+
module-level variable in `injector.js`, only ever reset to `false`
|
|
439
|
+
inside a *successful* CDP connection callback — if an attempt stalls
|
|
440
|
+
(the ADB shell command never produces a `debug` line, or
|
|
441
|
+
`connectToDebugger`'s own connection retry loop never succeeds), it
|
|
442
|
+
stays `true` forever, and since the service is a long-running
|
|
443
|
+
background process that survives the foreground app closing/reopening,
|
|
444
|
+
this stuck state persisted across every subsequent launch until a full
|
|
445
|
+
TV reboot killed the service process — matching exactly what was
|
|
446
|
+
reported (hangs after `getState`, only clears on TV reboot). Fixed two
|
|
447
|
+
ways: `index.html` now retries (`setTimeout(useInjectorOrProxy, 1000)`)
|
|
448
|
+
instead of silently doing nothing on `canConnectToDaemon && isConnecting`;
|
|
449
|
+
`injector.js` now sets a 20s safety timeout when `isConnecting` is set
|
|
450
|
+
`true` that force-resets it, bounding the worst case instead of a
|
|
451
|
+
permanent hang.
|
|
452
|
+
|
|
453
|
+
**Confirmed on-device (2026-08-03) that this retry logic works** — a
|
|
454
|
+
6.5 log showed the retry loop firing every second and correctly
|
|
455
|
+
recovering once the 20s timeout reset `isConnecting`. But the *next*
|
|
456
|
+
debug-launch attempt then hit `Uncaught exception: ReferenceError:
|
|
457
|
+
packet is not defined` in `AdbHostClient._onPacket`, followed by
|
|
458
|
+
`ECONNRESET`, and the app never actually opened — a **regression from
|
|
459
|
+
the blanket `"use strict"` prepend above**. `adbhost`'s own bundled
|
|
460
|
+
code does `packet = this._packet;` with no declaration at
|
|
461
|
+
`_onPacket`'s first line — a genuine pre-existing bug in that package,
|
|
462
|
+
harmless in sloppy mode (silently creates an implicit global) but a
|
|
463
|
+
`ReferenceError` in strict mode. Patched via a build-time regex in
|
|
464
|
+
`build-service.js` (`packet = this._packet;` → `var packet = ...`),
|
|
465
|
+
the same pattern already used there for two other bundle post-processing
|
|
466
|
+
fixups. **Not yet retested on-device — for either the isConnecting fix
|
|
467
|
+
or this one.**
|
|
468
|
+
|
|
469
|
+
**Also added: earlier diagnostics for Tizen 5.5's still-total
|
|
470
|
+
blackout.** All service-level self-logging lives inside
|
|
471
|
+
`standalone/service/index.js`, which only runs once the service
|
|
472
|
+
actually starts — if the *service* itself never starts on 5.5 (or
|
|
473
|
+
`launchAppControl`'s callback never fires at all), none of that logging
|
|
474
|
+
ever gets a chance to run. `standalone/index.html` now has its own
|
|
475
|
+
minimal `sendLog()` (plain `fetch`, hardcoded to the same default
|
|
476
|
+
receiver, no dependency on the service) logging at the earliest
|
|
477
|
+
possible points: script start, right before `launchAppControl`, inside
|
|
478
|
+
both its success and error callbacks, and at each branch of
|
|
479
|
+
`useInjectorOrProxy()`'s `getState` resolution.
|
|
480
|
+
|
|
481
|
+
**This diagnostic paid off (2026-08-02): on Tizen 5.5, all of
|
|
482
|
+
`index.html`'s own log lines arrive fine — script start,
|
|
483
|
+
`launchAppControl` success, `useInjectorOrProxy()` called — but
|
|
484
|
+
`getState` fails every single time with `Failed to fetch`, across many
|
|
485
|
+
rapid retries (the "progress bar keeps resetting" the user described
|
|
486
|
+
is `index.html`'s own `window.location.reload()` firing every ~1.5s).
|
|
487
|
+
Critically, not one single `[StandaloneService]` log line ever
|
|
488
|
+
appeared — not even the very first one, which is the first statement
|
|
489
|
+
after `require('http')`. Since that's wrapped in nothing but plain
|
|
490
|
+
top-level code, the only way for it to never fire is if
|
|
491
|
+
`service/dist/index.js` never got that far — most plausibly, Node
|
|
492
|
+
failed to *parse* the file at all. A parse-time `SyntaxError` happens
|
|
493
|
+
before any code in that file runs, including its own try/catch, so it
|
|
494
|
+
can't self-report — and the huge `ncc` bundle includes not just this
|
|
495
|
+
app's own code but all of `express`/`node-fetch`/`adbhost`/
|
|
496
|
+
`chrome-remote-interface`'s code too, none of which was written with
|
|
497
|
+
Tizen 5.5's older Node in mind.**
|
|
498
|
+
|
|
499
|
+
Fix attempt: split the service entry into two files.
|
|
500
|
+
`standalone/service/bootstrap.js` is deliberately plain ES5 (`var`/
|
|
501
|
+
`function`, no arrow functions/template literals/const/destructuring)
|
|
502
|
+
and is copied through to `dist/index.js` *unmodified* — no `ncc`
|
|
503
|
+
processing, so it can't itself be the thing that fails to parse. It
|
|
504
|
+
`require()`s the real bundle (now built to `dist/bundle.js` instead of
|
|
505
|
+
`dist/index.js`) inside a try/catch. A `SyntaxError` while parsing a
|
|
506
|
+
*required* file **is** catchable by the requiring file, unlike a parse
|
|
507
|
+
error in the top-level file being executed — so if the bundle still
|
|
508
|
+
fails to parse on 5.5, this should now at least produce one loggable
|
|
509
|
+
line (`require('./bundle.js') FAILED: ...`) instead of total silence.
|
|
510
|
+
**Confirmed on-device (2026-08-02) — the bootstrap paid off immediately.**
|
|
511
|
+
`bootstrap.js starting, node v4.4.3` logged successfully, then:
|
|
512
|
+
`require('./bundle.js') FAILED: SyntaxError: Block-scoped declarations
|
|
513
|
+
(let, const, function, class) not yet supported outside strict mode`.
|
|
514
|
+
A well-known Node 4.x V8 limitation: block-scoped `let`/`const`/
|
|
515
|
+
`function`/`class` only work inside strict-mode code on that engine.
|
|
516
|
+
`ncc` wraps each bundled module in its own function, so one module's
|
|
517
|
+
own `"use strict"` (e.g. this app's `index.js`) doesn't cover sibling
|
|
518
|
+
modules like `express`/`node-fetch`/`adbhost`/`chrome-remote-interface`,
|
|
519
|
+
most of which don't declare it themselves. Fixed by prepending
|
|
520
|
+
`"use strict";` as the literal first line of the *entire* bundled
|
|
521
|
+
output in `build-service.js` — nested functions lexically inherit
|
|
522
|
+
strict mode from their enclosing scope, so one directive at the true
|
|
523
|
+
top of the file covers every bundled module.
|
|
524
|
+
|
|
525
|
+
**Retested (2026-08-03): fixed that specific error, but not the whole
|
|
526
|
+
problem — a *different* parse error surfaced next:**
|
|
527
|
+
`SyntaxError: Unexpected token {`, no line number available (V8
|
|
528
|
+
doesn't attach one to a parse-time error thrown this way). Forcing
|
|
529
|
+
strict mode only fixed the one class of syntax it specifically
|
|
530
|
+
targets; there's evidently at least one more ES6+ construct somewhere
|
|
531
|
+
across the ~30 bundled dependencies that Node 4.4.3's parser rejects
|
|
532
|
+
outright, strict mode or not. Also caused a real regression on Tizen
|
|
533
|
+
6.5 — see the `adbhost` `packet` bug fix above; forcing strict mode
|
|
534
|
+
turns other packages' latent sloppy-mode-only bugs into hard failures.
|
|
535
|
+
|
|
536
|
+
**Resolved (2026-08-03): user chose to reintroduce Babel, deliberately
|
|
537
|
+
differently from the previous attempt.** The original Babel step
|
|
538
|
+
(reverted earlier — see above) ran Babel over the *whole*
|
|
539
|
+
`standalone/service/` source tree, including `node_modules`, *before*
|
|
540
|
+
bundling — that transpiled `ncc`'s own huge internals (~25 minute
|
|
541
|
+
builds) and crash-parsed unrelated test fixtures elsewhere in
|
|
542
|
+
`node_modules`. This time, `build-service.js` runs Babel on the
|
|
543
|
+
*already-bundled* single output file instead, targeting `node: '4.4.3'`
|
|
544
|
+
specifically via `@babel/preset-env`. The final bundle contains
|
|
545
|
+
neither `ncc`'s own tooling nor any test fixtures — only the runtime
|
|
546
|
+
code paths `ncc` already tree-shook down to — so neither previous
|
|
547
|
+
problem applies. Verified locally: build completes in seconds, output
|
|
548
|
+
has zero `regeneratorRuntime` references (no reachable async/await
|
|
549
|
+
needing it), and manual inspection confirmed no real `let`/`const`/
|
|
550
|
+
arrow-function syntax survives (only false-positive substring matches
|
|
551
|
+
inside comments/JSDoc/embedded JSON documentation strings, e.g.
|
|
552
|
+
`chrome-remote-interface`'s bundled protocol definitions). The earlier
|
|
553
|
+
"use strict" prepend and `adbhost` patch are both kept (order:
|
|
554
|
+
regex fixups → Babel → "use strict" prepend on Babel's output) — Babel
|
|
555
|
+
should make the strict-mode prepend largely redundant now (its
|
|
556
|
+
Node-4-targeted output uses `var`, not `let`/`const`), but it's
|
|
557
|
+
harmless to keep, and it already caught one real latent bug
|
|
558
|
+
(`adbhost`'s own undeclared `packet` assignment) by turning a silent
|
|
559
|
+
sloppy-mode footgun into a loud, fixable error.
|
|
560
|
+
|
|
561
|
+
**Confirmed on-device (2026-08-03): this fully solved Tizen 5.5's
|
|
562
|
+
parsing problem.** `bootstrap.js starting, node v4.4.3` →
|
|
563
|
+
`bundle.js required successfully` → `Standalone service listening on
|
|
564
|
+
127.0.0.1:8099` — no more SyntaxErrors of any kind. 5.5's standalone
|
|
565
|
+
service now starts reliably.
|
|
566
|
+
|
|
567
|
+
It surfaced the *next* problem in the chain, though, common to both
|
|
568
|
+
TVs: once `getState` resolves and the CDP-injection handoff begins,
|
|
569
|
+
a device log showed `Unhandled rejection: Error: 'Page.setBypassCSP'
|
|
570
|
+
wasn't found` — Cobalt's CDP implementation on this device doesn't
|
|
571
|
+
support that protocol method at all — immediately followed by the
|
|
572
|
+
same `not opened` / `ECONNRESET` pattern seen on 6.5 in every log so
|
|
573
|
+
far, even ones from before any of this session's logging/fixes
|
|
574
|
+
existed. Compared against TizenBrew's own `debugger.js` (reliably
|
|
575
|
+
works on both TVs): it **never calls `Page.navigate()` or
|
|
576
|
+
`Page.setBypassCSP()` at all** — it attaches to a YouTube TV instance
|
|
577
|
+
already launched through normal means and injects via a script tag
|
|
578
|
+
(with an eval-based fallback for Trusted Types), whereas
|
|
579
|
+
`injector.js` spawns a *fresh* debug-mode instance via ADB and
|
|
580
|
+
immediately drives its navigation via CDP — a structurally different,
|
|
581
|
+
more timing-sensitive sequence TizenBrew's architecture sidesteps
|
|
582
|
+
entirely. Fixed the immediate bug (both `Page.navigate()` and
|
|
583
|
+
`Page.setBypassCSP()` had no error handling, so either failing
|
|
584
|
+
produced an unhandled rejection) and made `setBypassCSP` explicitly
|
|
585
|
+
non-fatal — injection here is `Runtime.evaluate()` of the userscript
|
|
586
|
+
text directly, not a page-loaded `<script src>` that CSP would
|
|
587
|
+
actually block, so that call was likely never load-bearing for this
|
|
588
|
+
approach in the first place.
|
|
589
|
+
|
|
590
|
+
**Real breakthrough (2026-08-03), from a `Clear Cache`/`Clear Data`
|
|
591
|
+
experiment the user ran on-device.** Both TVs showed the exact same
|
|
592
|
+
pattern: standalone works *exactly once* after any cache clear, then
|
|
593
|
+
breaks on every subsequent launch until the cache is cleared again —
|
|
594
|
+
reproduced repeatedly on both. Critically, a full TV **reboot alone
|
|
595
|
+
did not fix it** (rules out anything in-memory — `isConnecting`, the
|
|
596
|
+
CDP connection, any Node process state — none of that survives a
|
|
597
|
+
reboot anyway), only clearing the app's cache did. This happens on
|
|
598
|
+
the pure CDP-injection path (real `youtube.com`, no proxy/URL-
|
|
599
|
+
rewriting involved at all), so it's not this app's own rewriting
|
|
600
|
+
logic either.
|
|
601
|
+
|
|
602
|
+
This explains *why TizenBrew doesn't hit this*: its `debugger.js`
|
|
603
|
+
never calls `Page.navigate()` — it attaches to the actual native
|
|
604
|
+
YouTube TV app, launched fresh each time through Tizen's own normal
|
|
605
|
+
app-launch mechanism, a completely separate Tizen application with
|
|
606
|
+
its own isolated WebView profile that the platform tears down and
|
|
607
|
+
recreates properly on each launch. This app's approach is
|
|
608
|
+
structurally different: `Page.navigate()` redirects the *same*
|
|
609
|
+
WebView instance belonging to this app's own package
|
|
610
|
+
(`krx3dTtSt1.TizenTubeStandalone`) to `youtube.com/tv` rather than
|
|
611
|
+
launching a genuinely separate app — every relaunch reuses the same
|
|
612
|
+
on-disk cache/profile tied to this app's package ID. First navigation
|
|
613
|
+
is a clean cold load (works); the second reuses a now-warm cache from
|
|
614
|
+
the previous run, and something about that warm cache breaks YouTube
|
|
615
|
+
TV's own initialization.
|
|
616
|
+
|
|
617
|
+
First fix attempt: `client.Network.setCacheDisabled({ cacheDisabled:
|
|
618
|
+
true })` before `Page.navigate()`. **Confirmed on-device: did not
|
|
619
|
+
resolve it** — a genuinely fresh app update ("worked once, broke
|
|
620
|
+
again on reopen, cache-clear fixes it") reproduced the exact same
|
|
621
|
+
pattern. Root cause of why: `setCacheDisabled` only stops *new*
|
|
622
|
+
caching for the current session going forward — it does nothing about
|
|
623
|
+
what's already cached/stored from the previous run, which is exactly
|
|
624
|
+
what a stale second load would be reading. Also, the user separately
|
|
625
|
+
found that on Tizen 5.5, clearing cache alone sometimes wasn't
|
|
626
|
+
enough — clearing *data* (not just cache) was sometimes required —
|
|
627
|
+
meaning this may not be HTTP-cache-only, and could involve
|
|
628
|
+
localStorage/cookies/IndexedDB/service workers too, none of which
|
|
629
|
+
`Network.setCacheDisabled` touches.
|
|
630
|
+
|
|
631
|
+
Second fix attempt (2026-08-03): actively **clear** cache/cookies/
|
|
632
|
+
storage before navigating, rather than just disabling new caching —
|
|
633
|
+
replicating what the user's manual Clear Cache/Clear Data TV action
|
|
634
|
+
does, programmatically, on every single launch. Chain (each step
|
|
635
|
+
independently non-fatal, all proceed to `Page.navigate()` regardless
|
|
636
|
+
of outcome — `Page.setBypassCSP` already turned out not to exist on
|
|
637
|
+
this Cobalt CDP implementation, other domains may be similarly
|
|
638
|
+
incomplete): `Network.enable()` → `Network.setCacheDisabled()` →
|
|
639
|
+
`Network.clearBrowserCache()` → `Network.clearBrowserCookies()` →
|
|
640
|
+
`Storage.clearDataForOrigin({ origin: 'https://www.youtube.com',
|
|
641
|
+
storageTypes: 'cookies,local_storage,indexeddb,cache_storage,
|
|
642
|
+
service_workers,websql' })` (only attempted if the `Storage` domain
|
|
643
|
+
exists on this client at all) → then `Page.navigate()`, which now
|
|
644
|
+
waits for this whole chain rather than firing immediately alongside
|
|
645
|
+
it. **Confirmed on-device: did not resolve it either** — same
|
|
646
|
+
"works once, breaks on reopen" pattern reproduced again on retest.
|
|
647
|
+
|
|
648
|
+
**Reframing (2026-08-03), from a genuinely new piece of evidence: the
|
|
649
|
+
user reported TizenBrew — a completely separate app/service, sharing
|
|
650
|
+
no code with this one — hits the identical "closes itself, needs
|
|
651
|
+
several relaunches before it works" pattern when this is happening.**
|
|
652
|
+
That rules out anything specific to *this app's own code* as the sole
|
|
653
|
+
cause of the underlying flakiness; it points at something systemic in
|
|
654
|
+
how the SDB debug daemon / Cobalt's CDP session handling behaves
|
|
655
|
+
across successive debug-session requests, possibly a limited/shared
|
|
656
|
+
per-device resource (the user's own hypothesis, and a plausible one:
|
|
657
|
+
a debug session or port this app fails to release could starve
|
|
658
|
+
*any* app's next attempt, not just its own — clearing cache/data
|
|
659
|
+
likely force-kills this app's service process, releasing whatever it
|
|
660
|
+
was holding).
|
|
661
|
+
|
|
662
|
+
The architectural difference that actually matters, revisited:
|
|
663
|
+
TizenBrew's `debugger.js` retries internally (up to 15 attempts, with
|
|
664
|
+
a session-id-supersession scheme so old retry loops abort cleanly
|
|
665
|
+
once a newer one starts) entirely on its own — the user doesn't do
|
|
666
|
+
anything except keep the app open. This app's `index.html` instead
|
|
667
|
+
calls `tizen.application.getCurrentApplication().exit()` immediately
|
|
668
|
+
after *triggering* the debugger, with no confirmation it actually
|
|
669
|
+
succeeded — if that attempt then fails, there's nothing left alive to
|
|
670
|
+
retry from, so the user has to manually relaunch the whole app every
|
|
671
|
+
single time (the "reopen ~5 times" they were doing).
|
|
672
|
+
|
|
673
|
+
Fix: ported the same pattern into `injector.js`.
|
|
674
|
+
- `_activeSessionId` + a `sessionId` threaded through `startDebugger`/
|
|
675
|
+
`connectToDebugger`: a fresh top-level call supersedes any retry
|
|
676
|
+
loop still in flight from a previous one; superseded attempts abort
|
|
677
|
+
silently (checked before acting at every async boundary: after
|
|
678
|
+
`canConnectToDaemon()`, on ADB stream connect, on CDP connect, in
|
|
679
|
+
the connectivity-retry catch).
|
|
680
|
+
- Up to `MAX_RETRY_ATTEMPTS` (10) automatic retries at
|
|
681
|
+
`RETRY_DELAY_MS` (750ms) apart, triggered by `retryOrGiveUp()` from:
|
|
682
|
+
CDP disconnecting before injection succeeded, the injection
|
|
683
|
+
`evaluate()` call failing, or `Page.navigate()` failing. All of this
|
|
684
|
+
happens entirely service-side — the foreground app has already
|
|
685
|
+
exited by the time these fire, so no user action is needed for a
|
|
686
|
+
retry to happen at all.
|
|
687
|
+
- `isConnecting`'s 20s safety-timeout is now re-armed on every retry
|
|
688
|
+
attempt (not just the first), so the timeout window scales with the
|
|
689
|
+
actual retry budget instead of assuming one attempt is enough.
|
|
690
|
+
- Explicit cleanup on every failure path, not just relying on the
|
|
691
|
+
natural `'disconnect'` event (which doesn't fire if an `evaluate()`
|
|
692
|
+
call rejects without the connection itself dropping): the CDP
|
|
693
|
+
`client.close()`s before retrying, and the ADB stream now always
|
|
694
|
+
gets ended (a 5s fallback timeout in addition to the existing
|
|
695
|
+
success-path `.end()`) — previously it only ended inside the
|
|
696
|
+
`dataString.includes('debug')` branch, leaving it open indefinitely
|
|
697
|
+
on any attempt where that never matched. This is the part directly
|
|
698
|
+
responding to the user's leaked-resource hypothesis — if true, this
|
|
699
|
+
should also reduce how often TizenBrew needs multiple relaunches
|
|
700
|
+
during the same broken window, not just this app.
|
|
701
|
+
|
|
702
|
+
**Not resolved — reframed (2026-08-03), twice.** First finding: tested
|
|
703
|
+
on-device, with Host PC IP set to anything other than `127.0.0.1`, the
|
|
704
|
+
proxy path connects reliably on both Tizen 5.5 and 6.5 — clean
|
|
705
|
+
`getState` → `Taking proxy path` → `Received GET /tv` every time, no
|
|
706
|
+
connection errors, no retries needed. This looked like a strictly
|
|
707
|
+
better result than any CDP-path fix achieved and was initially
|
|
708
|
+
documented as "the fix" (prefer proxy, avoid `127.0.0.1`).
|
|
709
|
+
|
|
710
|
+
**That guidance was wrong, or at least incomplete — corrected the same
|
|
711
|
+
day.** The user then hit video playback stopping entirely on the
|
|
712
|
+
proxy path, with a YouTube verification QR code matching
|
|
713
|
+
upstream reisxd/TizenTube#561 ("Standalone: unable to play any
|
|
714
|
+
video" — worked the first day, broke the next, TizenBrew's own
|
|
715
|
+
unproxied path unaffected throughout). Checked the actual upstream
|
|
716
|
+
commit that's supposed to have addressed this
|
|
717
|
+
(`c497c0590800e0474b198bbfcde77dcfc37f8ad0`) — it turns out to be the
|
|
718
|
+
commit that *introduced* `injector.js` in the first place (added
|
|
719
|
+
fresh, 70 lines, nothing in the proxy code touched at all), with the
|
|
720
|
+
commit message: *"Since fixing #555 and #561 would be starting a cat
|
|
721
|
+
and mouse game between me and YT, I think it's better to do it the
|
|
722
|
+
old way, aka the TizenBrew way."* In other words: upstream didn't fix
|
|
723
|
+
the proxy path's YouTube-detection problem, they built an entirely
|
|
724
|
+
separate path (CDP injection, real direct connection, no rewritten
|
|
725
|
+
traffic) specifically to avoid needing to. The proxy path's
|
|
726
|
+
reliability finding above is real (connection-level), but it doesn't
|
|
727
|
+
mean the proxy path is actually the better choice — it has a
|
|
728
|
+
separate, YouTube-server-side, likely structurally unfixable failure
|
|
729
|
+
mode (breaks *already-working* video playback, not just launch) that
|
|
730
|
+
the CDP path doesn't have at all.
|
|
731
|
+
|
|
732
|
+
**Net effect: neither path is simply "the answer."** CDP is
|
|
733
|
+
connection-unreliable (this session's whole investigation, several
|
|
734
|
+
genuine fixes landed, not fully solved). Proxy connects reliably but
|
|
735
|
+
can silently stop video playback via YouTube's own bot detection,
|
|
736
|
+
which isn't something fixable by patching our own code — it's why
|
|
737
|
+
upstream moved away from it. README.md now documents both failure
|
|
738
|
+
modes explicitly instead of recommending one path over the other.
|
|
739
|
+
Continuing to harden the CDP path (matching upstream's own direction)
|
|
740
|
+
is probably the more sustainable direction than trying to out-guess
|
|
741
|
+
YouTube's detection on the proxy path — but that's not yet a settled
|
|
742
|
+
decision, just the more defensible one given upstream's own
|
|
743
|
+
experience.
|
|
744
|
+
|
|
745
|
+
Trade-off surfaced along the way, independent of which path "wins":
|
|
746
|
+
TizenBrew and TizenBrewInstaller (separate repos, separate apps)
|
|
747
|
+
apparently *also* require Host PC IP `127.0.0.1` for their own local
|
|
748
|
+
SDB-based mechanisms (TizenBrewInstaller needs it to install/update
|
|
749
|
+
packages) — so a TV configured for standalone's proxy path can't use
|
|
750
|
+
those without switching Host PC IP back and forth. A PC-side
|
|
751
|
+
installer script (`scripts/pc-installer/`, this repo) is being built
|
|
752
|
+
as a workaround specifically for the install/update case, since SDB
|
|
753
|
+
installs work the same over network as USB and don't need
|
|
754
|
+
`127.0.0.1` — see that script's own docs. TizenBrew's own CDP
|
|
755
|
+
injection has no equivalent workaround; whether it needs one is a
|
|
756
|
+
separate, cross-repo question, not resolved here.
|
|
757
|
+
|
|
758
|
+
Also fixed while investigating: `standalone/index.html`'s
|
|
759
|
+
`launchAppControl` error callback previously only showed an `alert()`
|
|
760
|
+
and stopped — confirmed on-device, right after a TV reboot,
|
|
761
|
+
`launchAppControl` can genuinely fail once (Tizen's own service-launch
|
|
762
|
+
subsystem still warming up; took ~5s to succeed vs. the usual <1s in
|
|
763
|
+
every other observed launch) and require a manual close/reopen to get
|
|
764
|
+
past. Now retries automatically after 1s, matching the pattern the
|
|
765
|
+
`getState`-fetch-failure path already used.
|
|
766
|
+
|
|
767
|
+
Also added, per user request: a read-only `Receiver: {{host}}:{{port}}`
|
|
768
|
+
subtitle on the native "Remote Log Server" settings menu item (`mods/ui/
|
|
769
|
+
settings.js`, `en.json`/`de.json`), since the native TV settings menu has
|
|
770
|
+
no free-text entry — editing the actual host/port still only happens via
|
|
771
|
+
the RED-key theme overlay.
|
|
772
|
+
|
|
773
|
+
**Deferred feature: Q-Symphony 5.1 audio.** `pilvepank/TizenTube`'s fork
|
|
774
|
+
(compare: `reisxd/TizenTube...pilvepank:TizenTube:main`) has a well-built
|
|
775
|
+
feature that rewrites the standalone proxy's outgoing HTTP User-Agent to a
|
|
776
|
+
Cobalt 20+ living-room-client string, which unlocks YouTube's multichannel
|
|
777
|
+
audio streams (E-AC-3/AC-3/AAC 5.1 — itags 328/380/258/327) that it
|
|
778
|
+
otherwise only ever serves stereo to Tizen's web engine. It's standalone-
|
|
779
|
+
mode-only structurally: the injected-userscript path can only override
|
|
780
|
+
`navigator.userAgent` (what JS reads), not the actual HTTP header YouTube's
|
|
781
|
+
server keys off — only the proxy can rewrite that. Needs a
|
|
782
|
+
`mods/features/surroundAudio.js` codec-support module (`MediaSource.
|
|
783
|
+
isTypeSupported`/`canPlayType` for AC-3/E-AC-3/AC-4, careful never to claim
|
|
784
|
+
a codec the platform doesn't actually support), a settings toggle with 3
|
|
785
|
+
UA profiles, and a diagnostics panel (~600 lines total). Not yet ported —
|
|
786
|
+
deferred until the standalone signing issue above is fixed (can't test an
|
|
787
|
+
install-blocked mode), and the user's hardware is an unconfirmed fit (has
|
|
788
|
+
a Samsung soundbar, not confirmed Q-Symphony-compatible). Ask before
|
|
789
|
+
implementing if this comes back up.
|
|
790
|
+
|
|
791
|
+
## Hard-won lessons (read before touching `mods/features/adblock.js`)
|
|
792
|
+
|
|
793
|
+
These cost real debugging time and are not obvious from the code. They are
|
|
794
|
+
recorded here rather than in any one contributor's notes so a fork inherits
|
|
795
|
+
them.
|
|
796
|
+
|
|
797
|
+
### There are two response-handling paths, and they are not shared code
|
|
798
|
+
|
|
799
|
+
YouTube delivers responses in two shapes, and `adblock.js` has a separate
|
|
800
|
+
handler for each:
|
|
801
|
+
|
|
802
|
+
- **array-root** → `processResponsePayload(payload, detectedPage)`
|
|
803
|
+
- **object-root** → the branch inside the patched `JSON.parse`
|
|
804
|
+
|
|
805
|
+
A filter added to one path and not the other works on some surfaces and
|
|
806
|
+
silently does nothing on others. This has bitten repeatedly: the array-root
|
|
807
|
+
grid handler was missing `filterShortsFromItems` and
|
|
808
|
+
`filterMembersOnlyFromItems`; `noteContinuationBatch` had to be hooked at
|
|
809
|
+
*both* continuation sites; `hideRelatedVideos` is called from both watch-next
|
|
810
|
+
sites.
|
|
811
|
+
|
|
812
|
+
When adding anything per-item or per-shelf, put the logic in its own module and
|
|
813
|
+
call it from both sites — `mods/features/sidebarChannelButton.js` is the shape
|
|
814
|
+
to copy — rather than duplicating the body. Grep for the sibling call site
|
|
815
|
+
before assuming one hook is enough.
|
|
816
|
+
|
|
817
|
+
### `JSON.parse` is wrapped by several modules, and order matters
|
|
818
|
+
|
|
819
|
+
`playlistContinue.js`, `playlistBatchCollect.js`, `adblock.js` and
|
|
820
|
+
`customGuideAction.js` each wrap `JSON.parse`, in that order (set by the import
|
|
821
|
+
order in `userScript.js`). Every wrapper calls through to whatever it captured
|
|
822
|
+
as the original, so they compose — but an early `return` added to any of them
|
|
823
|
+
silently swallows every wrapper installed before it.
|
|
824
|
+
|
|
825
|
+
`captionStylePersistence.js` independently wraps `resolveCommand` alongside
|
|
826
|
+
`resolveCommand.js`'s own patch. Same rule applies.
|
|
827
|
+
|
|
828
|
+
### A playlist continuation response must contain at least one item
|
|
829
|
+
|
|
830
|
+
YouTube's refill loop stops dead if a continuation comes back empty — loading
|
|
831
|
+
never resumes, even on scroll. `hideVideo` therefore deliberately keeps one
|
|
832
|
+
"helper" tile per continuation batch (`__ttKeepOneForContinuation`).
|
|
833
|
+
|
|
834
|
+
This was learned the hard way: PR #678 capped helpers at one per visit and
|
|
835
|
+
returned `[]` for the rest, and playlist loading stalled after two batches on
|
|
836
|
+
device. It was reverted in #679. If you change the helper logic, test a long,
|
|
837
|
+
mostly-watched playlist and confirm it still loads past batch 2.
|
|
838
|
+
|
|
839
|
+
### Tizen 5.0 has no usable Polymer element APIs
|
|
840
|
+
|
|
841
|
+
`yt-virtual-list` positions its rows by transform from its own data model, and
|
|
842
|
+
the Polymer APIs you would reach for to force a re-render are `undefined` on
|
|
843
|
+
Tizen 5.0. Every DOM-level attempt to collapse the blank slot left behind by a
|
|
844
|
+
removed helper tile failed for this reason — rows are recycled and carry their
|
|
845
|
+
previous inline styles, and helper tiles carry no video-id attribute to target
|
|
846
|
+
(the id exists only inside a thumbnail's `background-image` URL).
|
|
847
|
+
|
|
848
|
+
Blank helper slots are an accepted limitation. Don't re-litigate it at the DOM
|
|
849
|
+
layer without new evidence.
|
|
850
|
+
|
|
851
|
+
### Verifying that a change actually reached the bundle
|
|
852
|
+
|
|
853
|
+
`dist/userScript.js` is minified and Babel-transpiled: identifier names are
|
|
854
|
+
mangled, string literals survive. Grep for a distinctive **string literal**,
|
|
855
|
+
never a function name, and use `grep -a` — the bundle is detected as binary.
|
|
856
|
+
|
|
857
|
+
## Conventions this repo has established (follow these)
|
|
858
|
+
|
|
859
|
+
- New app identities (Tizen `package`/app id) must be unique, not reused
|
|
860
|
+
from upstream or another project — collisions cause cross-app "wrong
|
|
861
|
+
version shown" bugs and install conflicts (this has actually happened —
|
|
862
|
+
see standalone's `krx3dTtSt1` choice above).
|
|
863
|
+
- Never hardcode another fork's npm package name or GitHub repo as a
|
|
864
|
+
fallback/CDN source — always this fork's own (`@krx3d/tizentube2`,
|
|
865
|
+
`KrX3D/TizenTube`).
|
|
866
|
+
- New features go in their own file under `mods/features/`, not inline in
|
|
867
|
+
an existing file like `adblock.js`.
|
|
868
|
+
- Verify icon names against existing `settings.js` usage before adding a
|
|
869
|
+
new settings entry.
|
|
870
|
+
- Add new user-facing strings to **both** `en.json` and `de.json`, minimum.
|
|
871
|
+
- Don't add error handling for scenarios that can't happen; do keep the
|
|
872
|
+
defensive try/catch patterns already established in `adblock.js`'s
|
|
873
|
+
`JSON.parse` patch and `sponsorblock.js`'s scheduled-skip handler — both
|
|
874
|
+
exist because an uncaught error there previously broke playback/parsing
|
|
875
|
+
page-wide, not out of general caution.
|
|
876
|
+
- In `standalone/service/` (bundled by `ncc`, no `node_modules` deployed
|
|
877
|
+
on-device — everything must end up inlined into the single
|
|
878
|
+
`dist/index.js`): every `require(...)` call must keep a literal string
|
|
879
|
+
argument, never a variable. `require(path)` with `path` as a variable
|
|
880
|
+
can't be statically analyzed/inlined by `ncc`, so it falls through to
|
|
881
|
+
real Node module resolution at runtime and fails with "Cannot find
|
|
882
|
+
module" — this actually broke every launch on Tizen 6.5 once already
|
|
883
|
+
(a `safeRequire(name, path)` diagnostic helper introduced this exact
|
|
884
|
+
mistake). If you need a per-dependency try/catch wrapper for
|
|
885
|
+
diagnostics, wrap each literal `require('x')` call individually rather
|
|
886
|
+
than passing the module name through a shared helper function.
|
|
887
|
+
- Never run the build by hand and commit the generated output (`dist/*`,
|
|
888
|
+
`standalone/service/dist/*`) alongside a source change. CI rebuilds and
|
|
889
|
+
commits it on version bump; doing it manually produces merge conflicts on
|
|
890
|
+
generated files for everyone branching off `main`. Build locally to check a
|
|
891
|
+
change compiles, then `git checkout -- dist/` before committing.
|
|
892
|
+
- Work on a branch and open a PR; don't push to `main`. Don't push further
|
|
893
|
+
commits onto a branch whose PR is already merged — the commits end up
|
|
894
|
+
orphaned. Check merge state before pushing to an existing branch.
|
|
895
|
+
- When porting an upstream commit, read it rather than applying it: upstream
|
|
896
|
+
code has shipped with operator-precedence bugs, dead branches behind early
|
|
897
|
+
returns, and renames that would reset this fork's stored settings. Fix them
|
|
898
|
+
in the port and say so in the PR.
|