@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/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.