Samsung Smart TV client application for Phlix Media Server, built with Tizen SDK.
- Overview
- Features
- Tech Stack
- Prerequisites
- Installation
- Configuration
- Building the App
- Testing
- Deployment to TV
- Remote Control
- Supported Codecs
- Project Structure
- Troubleshooting
- License
Phlix Tizen is a native Samsung Smart TV application that connects to a Phlix Media Server, letting users browse their media library and play content on their television. It is a thin Vue 3 consumer of @phlix/ui — the entire UI (browse, detail, player, settings, auth) is rendered by @phlix/ui's createPhlixApp(), and this repository contributes the boot glue, the Tizen remote-control bridge, and TV spatial-navigation gating. The build pipeline is Vite + TypeScript, packaged as a signed .wgt widget for Tizen. HLS streaming is provided by @phlix/ui's player and tuned for Samsung TV memory limits.
- Library Browsing: Browse movies, TV shows, music, and other media from Phlix
- Video Playback: Direct play and HLS streaming with quality selection, tuned for TV memory
- Remote Control: Samsung remote support — spatial D-pad navigation plus transport keys (play/pause/stop/seek/back/home)
- User Authentication: Secure login with Phlix account credentials
- Progress Tracking: Automatic resume from last playback position
- Subtitle & Audio Tracks: Multiple subtitle and audio track selection
- Search: Search across your media library
- Favorites & Watch History: Mark favorites and track watched items
- Player Overlays: TV-specific chapter/marker ticks, Skip Intro & Skip Outro, sleep timer, picture-in-picture, and an end-of-video "Up next" card
- Parental Controls: Per-profile viewing schedules, tag restrictions, and stream limits
- Theming: Ships the
nocturnetheme by default
Provided by
@phlix/ui. To add or change a feature/screen, editphlix-ui.
- Vue 3 + Pinia + vue-router (peer dependencies)
@phlix/uiv1.0.0— the entire application UI viacreatePhlixApp(), incl. the player'sQualityMenustream-quality picker@phlix/contractsv0.5.8—buildPhlixHeaders(device headers)@phlix/syncplayv0.1.8— SyncPlay frame client used bysrc/stores/useSyncPlayStore.ts
Pin source of truth:
package.json— the three libraries above are declared as exact GitHub tag pins (github:detain/phlix-*#v...), and the trackedpackage-lock.jsonfreezes each tag to its exact commit, so CI'snpm ci --allow-git=allinstalls reproducible bytes. The numbers in this README (badge + list) are copies, kept honest bytests/unit/docsPinConsistency.test.ts: a re-pin must update all five doc surfaces (README, DEVELOPER, docs/i18n.md, AGENTS.md, CLAUDE.md) in the same commit or the test suite reddens. The widget's OWN version (what the TV and store read) is single-sourced frompackage.json"version"and stamped byscripts/version-sync.mjs— see DEVELOPER.md "Versioning" andtests/unit/versionStamping.test.ts.
- Vite +
@vitejs/plugin-vue(build targetchrome100— an esbuild syntax ceiling, not the engine floor; the runtime contract is Chromium ≥ 85, enforced bytests/unit/distApiFloor.test.ts, canonical indocs/runtime-floor.md;base: './') - Vitest + jsdom +
@vue/test-utils(tests) - TypeScript +
vue-tsc(typecheck) - Flat ESLint (
eslint.config.mjs) - Tizen
.wgtpackaging viaapp/config.xml+scripts/package.js
- Node.js: CI runs the 24.x line (every workflow leg).
engines.node>=22.12.0is the declared install floor — 22.x installs are best-effort: no workflow leg exercises them - npm: Included with Node.js
- Git: For version control
- Tizen Studio: current generation (6.1-era, or Tizen SDK 10 + the VS Code
Tizen extension) from https://samsungtizenos.com/tools-download/ — for
.wgtsigning + deployment; seedocs/signing.mdfor the verified install steps (Package Manager → TV Extensions + Samsung Certificate Extension ≥ 2.0.73) - Samsung Developer account: required to create TV author certificates
- Samsung Smart TV: Tizen OS device (
config.xmlrequired_versionis6.5), in Developer Mode for sideloading
- Phlix Media Server: A running Phlix server reachable from the TV
- Network: TV and server must be on the same network
git clone https://github.com/detain/phlix-tizen-client.git
cd phlix-tizen-clientnpm ci --allow-git=all(
.npmrcalready setsallow-git=all, so a barenpm cibehaves the same.)CI installs with
npm ci --allow-git=allfrom the TRACKEDpackage-lock.json(audit W105 T-01 — every git-dep resolution is pinned to an exact commit SHA). Whenever apackage.jsonpin changes, regenerate the lock withnpm installand commit it; a stale lock makes CI'snpm cifail loudly.
npm run devThe app is served at http://localhost:8080. Point it at a server via the
VITE_PHLIX_SERVER_URL environment variable, or set phlix.serverUrl in the
app's localStorage at runtime.
TV-era motion posture. The sealed @phlix/ui stylesheet ships
52 backdrop-blur layers tuned for desktop compositors; on a TV webview they cost
dropped frames during D-pad shelf scrolls. The client therefore boots in a
lite posture — a host stylesheet (src/styles/tv-motion.css) neutralizes
all backdrop blur under a body class, while gradients and the nocturne
atmosphere stay. Opt a device back into full blur by setting
localStorage['phlix.tvMotion'] = 'full' (any other value, including garbage,
keeps the TV-era default; the rule is src/tvMotion.ts).
The client resolves which server to talk to in this order (see src/resolveConfig.ts):
localStorage['phlix.serverUrl'](set at runtime in the app)import.meta.env.VITE_PHLIX_SERVER_URL(build/dev-time env)- empty — with
requireConnection: true,@phlix/uishows its first-run Connect screen instead of guessinglocalhost; the URL you enter is mirrored back tolocalStorage['phlix.serverUrl']and re-seeds the next launch
The hub-relay consumer (src/api/hubRelay.ts, the "Alexa, play X" path) resolves
its own context from localStorage['phlix.hubUrl'], ['phlix.hubServerId'] and
['phlix.hubAccessToken'], with the build-time overrides VITE_PHLIX_HUB_URL
and VITE_PHLIX_HUB_SERVER_ID (all env vars are declared in vite-env.d.ts).
Without a hub context nothing opens.
Token custody is device-class by design, not a deferral (estate cookie-auth
design, COOKIE-DESIGN §2.1/§4.6, owner ruling D6c, 2026-11-05). This widget runs
cross-origin to the server it talks to, so a SameSite=Lax session cookie is never
sent to it — the bearer token that @phlix/ui's LocalStorageTokenStore keeps in
localStorage and rides on the Authorization header is the permanent custody
shape, and phlix.hubAccessToken above follows the same law. These rows never
migrate to cookie custody; the estate's browser-session cookie path is for
same-origin browser clients, a different device class.
Operator endpoint overrides. A deployment whose
SyncPlay endpoints do not answer the baked defaults can set three runtime rows
in localStorage (read live at each dial — no rebuild, mirrors the hub's
HUB_WS_URL/HUB_WS_PATH seam and the server's ListenEndpoints config):
| Row | Default | Moves |
|---|---|---|
phlix.syncplayPort |
8097 |
the direct room-socket port (host follows the configured server origin — the port row alone) |
phlix.relayPort |
8804 |
the hub-relay port (host stays the phlix.hubUrl host) |
phlix.relayPath |
/syncplay |
the hub-relay mount path |
Every row is validated at the trust boundary (src/endpointOverrides.ts:
plain-decimal 1–65535; absolute single-slash path with no query/fragment/
whitespace) — a poison value falls back to the default rather than dialing
somewhere surprising, and none of the three can move the HOST.
Relay transport is TLS-mandatory. The relay socket presents a
bearer credential in its opening handshake (Sec-WebSocket-Protocol: bearer, …),
so the dial requires an https:// hub base (wss://). A plaintext hub base
is refused at connect — loudly, with an actionable console error — before any
credential (including the relay-token mint) touches the wire; nothing else in
the app is affected and the socket simply stays closed. For LOCAL TESTING
ONLY (a dev hub on plain http), the build-time escape hatch
VITE_PHLIX_DEV_ALLOW_INSECURE_RELAY=1 npm run buildpermits the plaintext ws:// dial and warns on the console with every attempt.
It ships the relay bearer token unencrypted to anyone on your network and is
never release-safe — do not build a .wgt for distribution with it set.
SyncPlay transport is TLS-mandatory. The direct watch-party
socket (:8097) presents the bearer JWT in its opening handshake — and the
group-join frame that follows carries the replayable SHA-256 group password
hash — so that dial requires an https:// server base (wss://): the
The relay law transposed to the surviving lane. A plaintext base is
refused at connect with a loud, actionable console error and a short
"needs an https:// server URL" notice on the SyncPlay error channel; the
socket is never constructed, while browsing and REST on the persisted LAN
address stay untouched (the boundary law keeps http: persistable).
For LOCAL TESTING ONLY (a dev server on plain http), the build-time escape
hatch
VITE_PHLIX_DEV_ALLOW_INSECURE_SYNCPLAY=1 npm run buildpermits the plaintext ws:// dial and warns on the console with every attempt.
It ships the session credential unencrypted to anyone on your network and is
never release-safe — do not build a .wgt for distribution with it set. The
two hatches are deliberately SEPARATE env vars: enabling one never unlocks the
other bearer transport.
The plaintext refusal also lives at the resolve boundary.
resolveConfig.resolveCredentialBase is the scheme gate HUB/CREDENTIAL traffic
resolves through: a persisted (or build-env) http://-literal server URL is
REFUSED there with a named, honest error — the e4d8516 idiom refuses, it never
silently upgrades — and the hub-relay server-origin fallback is never fed a
plaintext base (a refused fallback is disclosed once at boot; the consumer stays
closed exactly as the dial-side law already decided). Legacy-poisoned entries
(javascript:/data:/raw-scheme storage) are sanitized on read at the same
edge. This is defense-in-depth, not a replacement: http: remains persistable
for LAN browsing (unchanged law), and both dial builders keep their
own named refusals. Each consumer's documented VITE_PHLIX_DEV_ALLOW_INSECURE_*
hatch threads through the boundary too — still exactly one flag per transport.
The client sends X-Phlix-Device-Type: samsung-tizen and a stable
X-Phlix-Device-ID (persisted as phlix.deviceId). The server maps the
device-type header to the appropriate streaming/transcode quality profile — the
client does not post a device profile.
The remaining knobs vite-env.d.ts (repo root) declares:
VITE_PHLIX_LOCALE— build-time locale override for the client i18n seam. Wins overnavigator.language, loses to an explicit runtime value; unset/enkeeps the byte-identical English defaults. The full locale ladder and catalog law live indocs/i18n.md.VITE_PHLIX_TV_ADMIN— release-variant switch: set to1to ship the admin console (its routes + nav entry) inside the.wgt; ANY other value (including unset) omits it — default-off (buildMenu()/buildExtraRoutes()insrc/main.ts).
The VITE_PHLIX_DEV_ALLOW_INSECURE_RELAY / VITE_PHLIX_DEV_ALLOW_INSECURE_SYNCPLAY
hatches belong to the TLS-mandatory sections above — never set them when
building a release .wgt. npm run package enforces this: it exits 1 if
either hatch is set in the build environment or compiled into the bundle.
app/config.xml is the Tizen widget (.wgt) manifest:
- App ID (
phlix.app.phlixtizen) and version — the version attribute is machine-stamped frompackage.json"version"byscripts/version-sync.mjson everynpm run package(single source; see DEVELOPER.md "Versioning") required_version(6.5)- Network access (
<access origin="*">) and TV privileges (internet,tv.inputdevice— only what the code actually exercises is granted; a privilege folds in with its feature, never pre-added) <tizen:profile name="tv-samsung"/>andhwkey-event="enable"(without the hwkey flag the webview never delivers remote key events to the app)- Landscape / maximized viewmode
TIZEN_HLS_CONFIG in src/main.ts is forwarded to @phlix/ui's player via
playerHlsConfig to keep HLS buffers bounded on RAM-constrained Samsung
webviews (maxBufferLength, capLevelToPlayerSize, enableSoftwareAES, etc.).
npm run typecheck # vue-tsc --noEmit (src/)
npm run typecheck:tests # vue-tsc -p tsconfig.tests.json (src/, tests/, scripts/ —
# same strict base; shrink-only ledger excludes two
# vitest-v5-era files, see testsTypecheckLedger.test.ts)npm run build # vue-tsc --noEmit && vite buildOutput is placed in the dist/ directory (index.html + assets/).
npm run package # node scripts/version-sync.mjs, npm run build, then node scripts/package.js — the sync runs FIRST so the bundle itself carries the stamped version (pinned by tests/unit/versionStamping.test.ts)This assembles the package/ directory (the Vite dist/ plus app/config.xml
at the widget root). Tizen Studio (or the tizen CLI) then signs package/
into a .wgt. See docs/signing.md for the certificate +
signing flow.
base: './'invite.config.tsis required — a.wgtruns from afile://origin on the TV, so absolute asset paths would 404.
npm test # vitest run (jsdom)npm run test:watchnpx vitest run tests/unit/tizenBridge.test.ts
npx vitest run tests/unit/QuickConnectPanel.test.ts
npx vitest run tests/unit/routeManifest.gate.test.ts
npx vitest run -t "BACK"npm run lint # eslint .
npm run lint:fix # eslint . --fixOne fact governs both paths below: the repository directory is not a Tizen
project. Tizen Studio's web-project import expects config.xml at the
project root, and this repo keeps its widget manifest in app/config.xml
(a version-stamped source file) — the repo root has none. The real Tizen web
project is package/: npm run package assembles config.xml,
icon.png, index.html and assets/ at its root, and the directory is
committed and kept byte-fresh by CI's "Assert committed package/ is fresh"
gate. Import or sign that — and never a directory called phlix-tizen:
no such directory exists (the clone directory is phlix-tizen-client, and
phlix-tizen is only the npm package name inside package.json).
-
Open Tizen Studio — launch the Studio GUI from the installation (launcher under
<install>/ide/bin/; there is notizen studiocommand) -
Prepare an importable project directory (from the repo root):
npm ci --allow-git=all npm run package # dist/ + app/config.xml + icon → package/ cp -r package ../phlix-tv-import # import a COPY, never the tracked dir
The copy is deliberate: Tizen Studio writes project metadata into whatever directory you import, and the tracked
package/must stay clean or CI's freshness gate reddens the build. -
Import the project
- File → Import → Tizen → Tizen Project → select
../phlix-tv-import(the copy — the repo root imports nowhere, having no rootconfig.xml) - Platform TV; the profile is dictated by the manifest (
tv-samsung) - If Import rejects the copy, the fallback is create-then-copy: File →
New → Tizen Project → TV → Web → template, then replace the new
project's
config.xml,index.html,icon.pngandassets/with the copy's files. Same final tree either way; import-from-existing is the supported path.
- File → Import → Tizen → Tizen Project → select
-
Create the signing profile — Certificate Manager: author certificate + distributor (development) certificate + a profile name. The full walk, the Samsung Certificate Extension prerequisite, and every failure mode live in
docs/signing.md("Creating the Certificate Profile") — Studio signs with this profile when you build. -
Connect the TV (2026 flow — the retired Eclipse "Window → Preferences → Tizen TV → Devices" path no longer exists)
- On the TV: enable Developer Mode — Smart Hub → Apps → App
Settings → type
12345on the remote → toggle Developer mode ON → enter your development computer's IP → full power cycle (standby is not enough on "Instant On" models) - Studio → Tools → Device Manager → Remote Device Manager → + → the TV's IP, port 26101 (not the legacy 26159, which is for phone/wearables and fails against TVs)
- The TV confirms the pairing, and on first install shows a "Permit to install applications" dialog — accept it (or right-click the device → Permit to install applications beforehand)
- On the TV: enable Developer Mode — Smart Hub → Apps → App
Settings → type
-
Build signed package and run
- Right-click the project → Tizen → Build Signed Package (produces the
signed
.wgt— the GUI equivalent of thetizen packageCLI below) - Right-click the project → Run As → Tizen TV Application
- Install refused with "invalid author certificate"? The TV is out of
Developer Mode, the profile's development distributor certificate does
not list the TV's DUID, or the permit dialog was never accepted —
docs/signing.md("Known Issues") names every mode.
- Right-click the project → Tizen → Build Signed Package (produces the
signed
# Build and package (assembles the widget root: package/ with config.xml on top)
npm ci --allow-git=all
npm run package
# Sign a COPY into a .wgt with the tizen CLI (needs a Certificate Manager
# profile; `-s` is the profile NAME — there is no --password flag, and the
# signed .wgt is written INTO the directory given after `--`, so the tracked,
# CI-freshness-gated package/ root itself is not the output dir):
cp -r package ./build-widget
<install>/tools/ide/bin/tizen package -t wgt -s <profile-name> -- ./build-widget
# Connect the TV — it must be in Developer Mode (walk-through in
# docs/signing.md); on first install the TV shows a "Permit to install
# applications" dialog to accept. `sdb devices` prints [serialNumber]
# [state] [targetName]; `-t` takes the target name (or use `-s <serial>`):
sdb connect <TV_IP>:26101
<install>/tools/ide/bin/tizen install -n <signed>.wgt -t <target-name> -- ./build-widget
<install>/tools/ide/bin/tizen run -p phlix.app.phlixtizen -t <target-name>The app id phlix.app.phlixtizen comes from app/config.xml (authoritative).
See docs/signing.md for signing details, the TV-as-device
walk, and install failure modes.
There is no Samsung Apps store listing — the Phlix widget is self-distributed, so an update travels the same sideload path as the first install:
- Build and sign the new
.wgtfrom a checkout of the new version, exactly as in Option 2 above, with the same author certificate as the installed one (a different author certificate installs a second, unrelated app — see “Update installs as a second app” indocs/signing.mdKnown Issues). - Re-run the Option 2 install step above against the newly signed
.wgt— same app id + same author certificate makes the TV replace the existing widget in place. - What survives: an update install preserves the widget's private storage, so
everything under
localStorage— the server address (phlix.serverUrl), the install-stablephlix.deviceId, session tokens, preferences — carries over; the updated app starts paired. An uninstall before installing does not: the next boot is a fresh pairing with a new device id. One shipped exception is one-time: the first update that applies the webdata-encryption flag (encryption="enable"inapp/config.xml, added 2026-10) may fail to decrypt previously-written webdata — the effect is a single re-login. - The
.wgtpublished on the GitHublatestrelease is an unsigned inspection snapshot, not an update source — install paths must sign from a checkout (see the CI signing section indocs/signing.md).
D-pad navigation is handled by @phlix/ui's spatial navigation
(useSpatialNav); transport keys are routed by the Tizen bridge
(src/tizenBridge.ts) to the player and router. The engine moves focus between
its registered candidates; because the ui bundle ships no v-focusable call
sites at the pinned version, the Tizen host keeps that candidate pool populated
itself — src/spatial/focusCandidates.ts registers every currently-interactive
element under document.body (links, buttons, inputs, selects, textareas,
tabindex stops) live as they appear, leave, or become disabled.
| Button | Action |
|---|---|
| Arrow Up/Down/Left/Right | Spatial navigation between focusable items |
| OK / Enter | Activate the focused item (native focus) |
| Back | Walks the four-rung BACK ladder (src/remote/backPolicy.ts): snap a panned shelf row back to its start, else close the topmost open layer (quick-connect, rating modal, @phlix/ui modal, quality picker), else navigate back (on the player this closes playback first), else at the home route exit the app. A navigate-back that leaves the screen unchanged is a dead end: the next BACK exits the app instead of repeating a press that does nothing |
| Home | Return to the home route (/app) |
| Play / Pause | Toggle playback |
| Stop | Close the player; when the player route is what's on screen, also returns to where you came from (off the player route it is a plain player teardown, no navigation) |
| Fast Forward | Seek forward 10s (30s when held) |
| Rewind | Seek backward 10s (30s when held) |
| Volume Up / Down | Step the player volume by 5% |
| Mute | Toggle mute |
| Yellow | On the player route, open/close the on-screen stream-quality picker (only when the active stream has ≥2 ABR rungs to choose from); while open, Left/Right/Up/Down/Enter drive the picker instead of seek/volume |
On the player route, spatial navigation is disabled so the player's own Arrow seek/volume shortcuts take over. While the quality picker is open, the Arrow keys are instead routed to the picker (the player's seek/volume shortcuts are briefly suppressed) so the D-pad can select a rung.
This client does not decode media by itself: @phlix/ui's player feeds
streams to the Samsung webview's native decoder, and the server maps the
X-Phlix-Device-Type: samsung-tizen header to a transcode profile targeting
what those webviews handle (see Troubleshooting → Playback Issues). The list
below is therefore the server-assisted working set — what the profile
aims at — not a guarantee of native playback on every model or region.
- H.264 (AVC) — natively decoded on Tizen TVs
- H.265 (HEVC) — hardware-dependent per model/region; the server transcodes where the profile says so
- AAC — the default HLS audio track
- AC3 (Dolby Digital) / EAC3 (Dolby Digital Plus) — hardware-dependent; transcode fallback applies
- MP3
- HLS (HTTP Live Streaming) via
@phlix/ui's hls.js player, RAM-bounded byTIZEN_HLS_CONFIG - Progressive HTTP download of server-chosen direct-play streams
Container/codec combinations outside the device's native support are the server's transcode decision; nothing in this repository adds or removes a decodable format.
phlix-tizen-client/
├── index.html # Vite entry (repo root); mounts #phlix-app, #phlix-spatial-host + 4 overlay hosts
├── vite-env.d.ts # ALL build-time env knobs (7 declared — see Configuration)
├── src/
│ ├── main.ts # boot(): createPhlixApp + bridge + spatial-nav host + 4 overlay apps; TIZEN_HLS_CONFIG, buildMenu, buildExtraRoutes
│ ├── polyfills.ts # structuredClone fallback (imported first)
│ ├── resolveConfig.ts # pure resolveAppConfig({serverUrl, envUrl})
│ ├── deviceId.ts # pure resolveDeviceId(storage) → persisted phlix.deviceId
│ ├── syncplayDispatch.ts # pending_command → resolve media id → @phlix/ui player store
│ ├── SpatialNavHost.vue # renderless useSpatialNav gate (off on player route)
│ ├── tizenBridge.ts # RemoteManager 'action' → usePlayerStore + router; Yellow/Back drive the QualityMenu
│ ├── api/
│ │ └── hubRelay.ts # hub-relay pending_command WebSocket consumer (wss://<hub>:8804; plaintext only behind a build-time dev hatch)
│ ├── components/ # TV overlays (ActionToastOverlay, TelemetryConsent, ScreenSaverOverlay) + D-pad lists/cards
│ ├── pages/ # ChaptersPage, AudioTracksPage, SubtitleTracksPage, MusicPage, ParentalControlsPage
│ ├── screens/ # RecommendationsScreen
│ ├── stores/ # useMusicStore, useSyncPlayStore (Pinia)
│ └── remote/
│ ├── RemoteManager.ts # TV-remote event singleton (keydown/keyup/action)
│ └── KeyMapping.ts # Samsung key codes → actions (arrows/ENTER not handled)
├── app/
│ └── config.xml # Tizen .wgt manifest (privileges, app id, version)
├── scripts/
│ ├── package.js # assembles package/ from dist/ + config.xml (ESM)
│ ├── audit-gate.mjs # npm-audit allow-list gate (CI Test final step)
│ ├── audit-gate.d.mts # type declarations for the gate's pure core (tests import it)
│ ├── version-sync.mjs # stamps package.json version into app/config.xml + telemetry carrier BEFORE build (chain pinned by versionStamping.test.ts)
│ ├── version-sync.d.mts # type declarations for the sync CLI's pure core
│ ├── sync-ui-locale-bundles.mjs # re-vendors @phlix/ui locale bundles + PIN manifest (docs/i18n.md)
│ ├── make-icon.js # deterministic app/icon.png generator (npm run icon:gen)
│ └── add-copyright.mjs # idempotent @copyright header injector (npm run copyright:headers)
├── tests/
│ ├── test-setup.ts # in-memory localStorage mock
│ ├── fixtures/
│ │ └── server-route-manifest.json # vendored @phlix/contracts route manifest (route gate)
│ └── unit/ # suites — a selection below; `ls tests/unit` for the full set
│ ├── resolveConfig.test.ts
│ ├── deviceId.test.ts
│ ├── tizenBridge.test.ts
│ ├── SpatialNavHost.test.ts
│ ├── RemoteManager.test.ts
│ ├── ParentalControlsTimeStepper.test.ts
│ ├── hubRelay.test.ts
│ ├── syncplayDispatch.test.ts
│ ├── routeManifest.gate.test.ts
│ └── main.test.ts
├── docs/
│ ├── signing.md # certificate + .wgt signing guide
│ ├── runtime-floor.md # engine-floor contract (syntax target vs runtime)
│ ├── i18n.md # locale catalogs + vendored ui-locale story
│ └── ui-port-divergence.md # local ports of sealed @phlix/ui surfaces
├── .github/workflows/
│ ├── test.yml # three legs: vitest + S501 coverage floor, tests/scripts typecheck, npm-audit gate last
│ ├── lint.yml # npm run lint + npm run build
│ └── build.yml # npm run package + package/ freshness gate + rolling 'latest' .wgt release
├── vite.config.ts # base: './', target chrome100
├── vitest.config.ts
├── eslint.config.mjs # flat config
├── tsconfig.json
├── package.json
├── LICENSE # MIT
└── README.md
- Ensure your TV runs Tizen OS (
config.xmlrequired_version6.5) - Check that the TV is on the same network as the server
- Verify the Phlix Media Server is running and the resolved server URL is reachable
- Confirm the build used
base: './'(absolute asset paths 404 from thefile://.wgtorigin)
- For buffering, check network bandwidth and the
TIZEN_HLS_CONFIGbuffer sizes insrc/main.ts - Quality/transcode decisions are made server-side from the
X-Phlix-Device-Type: samsung-tizenheader - Check server logs for codec/transcode warnings
- Each Tizen-hosted overlay is its own mounted app — confirm its host
<div>exists inindex.html(#phlix-action-toast-overlay,#phlix-quick-connect,#phlix-telemetry-consent,#phlix-screensaver) and the matchingcreateApp(...).mount(...)call is present insrc/main.ts - Chapters, skip-intro and picture-in-picture are not Tizen-hosted since
RT-100 / RT-101 (w13-L4): the sealed
@phlix/uiPlayerPage bundle ships the operable surfaces for all three (its marker timeline is on by default), and the host-mounted twins that used to duplicate them could never receive the remote. If one looks "missing" on screen, the surface to inspect is the vendored Player, not this tree.
Sleep timer and the end-of-video "Up next" card are not Tizen-hosted. They
render from @phlix/ui's PlayerPage bundle — the former
SleepTimerOverlay.vue / UpNextOverlay.vue mount apps were unreachable
duplicates and were deleted (their index.html host divs are gone, so a
host-div check for them can never succeed). If either is missing on the player
screen, suspect the pinned @phlix/ui bundle's player route, not this repo's
mount list.
- D-pad navigation comes from
@phlix/ui's spatial navigation — verify the TV layout is active and you are not on the player route - Transport keys (play/stop/seek) flow through
src/tizenBridge.ts— confirminstallTizenBridgeran after mount - Ensure no other device is controlling the TV; check the remote batteries
MIT License — see LICENSE.
Copyright (c) 2026 Phlix contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.