Both players carried calls that compile, read correctly, and are documented to
do something else. Verified line by line against docs.brightsign.biz and
Samsung's Smart TV Filesystem reference; every fix below cites the doc that
proves it, and the linter has been extended so each one fails here next time.
TIZEN
The offline media cache could never have worked on a panel. Its adapter used
the deprecated Filesystem API in three ways the IDL rules out:
`tizen.filesystem.resolve()` is declared `void`, so `var dir = resolve(...)`
was always undefined and MediaCache.create() returned null on every panel in
the fleet; `openStream()` is asynchronous, so appendPart read `written` before
any callback could run and returned 0 forever; and `moveTo()` is asynchronous,
belongs on the parent directory, and takes (origin, destination) — it was
called on a file handle with the arguments transposed. Rewritten against the
5.0 synchronous FileSystemManager, which is genuinely synchronous and is what
the decision layer needs. A Tizen 4.0 panel now reports available() false
instead of being handed a cache that silently writes nothing.
Writes are now POSITIONED rather than appended at EOF. Power cut between a
write and the index save — the exact event this feature exists for — replayed
the last chunk, and an append landed it twice: a silently corrupt video that
promoted as complete. A positioned write makes the replay idempotent.
Three decision-layer bugs alongside it: a 206 with no readable Content-Range
fell back to Content-Length, which is the CHUNK length, so the first megabyte
of a 50MB video promoted as a complete 1MB asset; a 200 whose body was short of
its own Content-Length returned 'done'; and a server with no ETag or
Last-Modified was re-fetched from zero on every sweep, forever, on precisely
the marginal link this feature exists to be gentle on.
The volume slider was dead. The dashboard sends `{level: 0..1}`; this handler
read `value`/`volume` as a 0..100 percentage, so it matched nothing and logged
"no usable value in payload" on every slider move while the panel declared
audio.volume as working. Both halves had to move together — taking `level` as a
percentage turns 50% into 0.5%, which is inaudible and looks like a fix.
Verified by driving the real handler in headless Chrome, before and after.
BRIGHTSIGN
FindMemberFunction is documented as available only when
roDeviceInfo.HasFeature("FindMemberFunction") is true. It was called
unguarded from the capability probe and from host telemetry — both on the event
loop — so a player without the feature would have died within a minute of boot
and taken the display with it. The guard needed guarding.
The boot report never arrived. The host flushed its buffer straight after
Show(), before the page had been fetched, while the player correctly waits for
its socket before subscribing. Between two correct decisions every boot line
fell on the floor. The host now waits for the page's `probe`, and the bridge
buffers until a consumer registers.
offline.cache was claimed on `navigator.serviceWorker` being present. It is
present on a BrightSign widget and will not run a worker — our XT245 passes the
check and never fetches sw.js. Now requires a controller, matching the web
player. Removed from the brightsign baseline for the same reason.
display.resolution was claimed on @brightsign/videooutput, which has no
setMode at all; mode setting lives on @brightsign/videomodeconfiguration.
roStorageHotplug.GetStorages() answers "USB1:/" while GetStorageStatus() is
documented as unreliable for "USBn:" — feeding one to the other re-created the
bug the static fallback list exists to avoid, and only on the OS versions that
have the enumerator.
dual/clone output mode put two full-screen widgets on output ONE, on top of
each other, while output two stayed dark: roHtmlWidget has no output selector,
and a second output is addressed by its display_x/display_y within the
SetScreenModes canvas. Now positioned properly, or refused with a reason.
Also: a manifest missing sha256/size passed `invalid` into typed parameters, a
runtime error at the call the comment already described and did not prevent;
storage_quota was a string where the docs say use a double; and the comment
crediting brightsign_js_objects_enabled with gating require("@brightsign/*")
named the wrong flag — it is nodejs_enabled.
TESTS
The two suites that mattered most were the ones that passed while the code was
broken, because they asserted on source text or against a fake more correct
than the platform. The host-diagnostics regexes now execute the bridge; the
media-cache suite now drives the shipped adapter against a fake tizen.filesystem
written from Samsung's IDL. Ten new rules in the BrightScript linter, each
verified to fail against the source it was written to reject.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
|
||
|---|---|---|
| .. | ||
| css | ||
| js | ||
| .gitignore | ||
| build-wgt.sh | ||
| config.xml | ||
| icon.png | ||
| index.html | ||
| README.md | ||
ScreenTinker — Tizen TV Player (.wgt)
A Samsung Tizen TV / signage web port of the ScreenTinker player. It speaks the
exact same /device socket.io protocol as the Android player, so a Tizen
display pairs and plays from the same dashboard with no server changes.
What it does
- Enter a server URL → connects to
{server}/device(socket.io v4). - Registers, shows a 6-digit pairing code; you claim it in the dashboard
(Devices → Pair a display). On
device:pairedit switches to playback. - Reconnects automatically with a stored
device_id+device_token. - Renders multi-zone layouts (matching the Android player) when a layout is assigned —
each zone has its own percent geometry,
z_index,fit_mode, background, and rotates its own assignments independently — and falls back to fullscreen single-zone when no layout is set, looping:- image → shown for
duration_sec(min 3s) - video (
/api/content/{id}/fileorremote_url) → plays to end, then next; single item loops - YouTube (
mime video/youtube) → muted autoplay<iframe>embed - widget →
<iframe>of{server}/api/widgets/{id}/render
- image → shown for
- Sends
device:heartbeatevery 15s (with best-effort Tizen telemetry). - Keeps the screen awake (
tizen.power/ Samsungappcommonscreensaver-off). - Video walls (mirrors the web player): when the device is a wall member the payload
carries
wall_config; the stage is positioned (in vw/vh) as this screen's slice of the wall, the leader broadcastswall:syncand followers align index + drift-correct their video to the leader's clock. Per-tilerotationis not applied yet (matches the web player); video walls have no Android equivalent.
Files
config.xml Tizen TV web-app manifest (privileges, profile, icon)
index.html setup / pairing / stage screens
css/style.css
js/app.js device protocol client (register, pair, heartbeat, state)
js/device-control.js Samsung B2B/system fleet control (device:command) — #125
js/player.js fullscreen playlist renderer
js/socket.io.min.js socket.io-client v4.7.5 (bundled)
icon.png
build-wgt.sh package (signed if Tizen CLI present, else unsigned)
Build
./build-wgt.sh # -> ScreenTinker.wgt
Without the Tizen CLI this is an unsigned .wgt.
Why the released
.wgtis unsigned: Samsung distributor certificates are locked to the DUID of the signer's own TVs, so a.wgtwe signed would not install on your TV anyway. Releases therefore ship it unsigned (for inspection only). To actually run it, use path A (no signing) or sign it yourself with your own certificate (path B).
Deploy — two paths
A) URL Launcher / TV browser (easiest, no signing)
No package, no Tizen Studio. Point the TV's URL Launcher (or just its web
browser) at your server's built-in web player: https://<your-instance>/player.
The TV runs it as a web app on boot, pairs with a 6-digit code, and plays - best
for Samsung B2B signage (SSSP). (You can instead self-host this tizen/ folder
and point the URL Launcher at …/index.html for the Tizen-specific build.)
B) Signed .wgt (installed app)
A signing profile is already set up on the build box (Tizen Studio CLI 6.1):
- Profile
ScreenTinker= a self-signed author cert (~/tizen-studio-data/keystore/author/st_author.p12) + the default Tizen distributor cert../build-wgt.shauto-detects the CLI and signs with it, producing a.wgtwithauthor-signature.xml+signature1.xml. - This installs on developer-mode Samsung TVs and the Tizen emulator —
the right path for a self-hosted fleet you control (enable Developer Mode
on each TV once: Apps → enter
12345→ set the host IP).
Install onto a dev-mode TV:
sdb connect <tv-ip>
tizen install -n ScreenTinker.wgt -t <tv-device>
Production / retail (no developer mode): re-sign with a Samsung Partner
or Public distributor certificate from the Tizen Certificate Manager
(free Samsung account; distributor cert tied to each TV's DUID), then
./build-wgt.sh <thatProfile>. The self-signed author cert is not committed (it
lives in ~/tizen-studio-data, password screentinker).
C) SSSP URL-Launcher native install (one URL, like Fusion fus.app/tizen)
The slick "add the screen to Wi-Fi → URL Launcher / Custom App → type a URL → you're in"
flow. The panel fetches <url>/sssp_config.xml, reads the version + byte size, downloads
ScreenTinker.wgt from the same folder, and installs it as a native app — auto-reinstalling
whenever <ver> bumps on the next release.
The ScreenTinker server hosts this for you. Drop a signed build at /data/ScreenTinker.wgt
(same convention as /data/ScreenTinker.apk) and it's served at:
GET /tizen/sssp_config.xml— manifest, generated dynamically so<size>always matches the exact.wgtbytes (a mismatch fails the install).GET /tizen/ScreenTinker.wgt— the package.GET /tizen— a human landing page with the install steps.
On the panel, under URL Launcher / Custom App, enter: https://<your-instance>/tizen
To host on a CDN/bucket instead (not the ST server), ./build-wgt.sh also drops a static
sssp_config.xml next to the .wgt; upload both to one folder and point the panel at that folder.
Env TIZEN_WGT_VER overrides the reported version; the default is the app version.
Signing (the one real prerequisite): the URL-Launcher /
sssp_config.xmlpath always requires a Samsung Partner distributor certificate — a public/self-signed build fails here witherror -3: invalid certificate chain,error -4: invalid signature, orinstall failed[118012], and a normal distributor cert is DUID-locked. Get the Partner cert via TV Seller Office (developer side; your Samsung Ascend channel/B2B contact is the fastest route to approval), re-signbuild-wgt.shwith that profile, and one URL installs on every panel.Developer Mode does NOT bypass this. Dev Mode only enables the SDB install path (
sdb connect+tizen install), which does accept a self-signed build on the panel's DUID — a separate, manual path, not the URL flow. So without the Partner cert you can (a) SDB-install the self-signed.wgton a dev-mode panel to prove the app runs on real Tizen hardware, or (b) use URL-Launcher web mode (Path A →/player) for the type-a-URL feel without a native install. Thesssp_config.xmlnative install specifically waits on the Partner cert.
Validated (2026-06-09)
- Protocol: headless test against the live server passed end-to-end —
register(pairing_code) → device:registered → pair → reconnect(device_id+token) → device:playlist-update(2 items) → GET /api/content/{id}/file = 200. - Runtime: loads + renders in Chromium with no JS errors (setup screen verified).
- Not yet on real Tizen hardware — needs signing + a TV (or URL Launcher).
Remote control & preview (#120 / #121 / #125)
The Tizen player listens for the same dashboard events as the web/Android player.
device:command is handled by js/device-control.js, which drives the real Samsung
fleet-control surface (webapis.systemcontrol on Tizen 6.5/7, else b2bapis.b2bcontrol
on SSSP/Tizen 4) and reports each outcome back via device:log (tag command, shown
live on the device-detail screen) plus a structured device:command-result:
Command (device:command type) |
Tizen behaviour |
|---|---|
refresh / reload |
location.reload() |
launch / screen_on |
clears the screen-off overlay + re-asserts wake; setPanelMute("OFF") when the B2B surface is present |
screen_off |
setPanelMute("ON") (backlight off) on a B2B panel; black overlay fallback otherwise |
update |
reload to re-pull URL-Launcher content (no in-app OTA — see Updates) |
reboot |
rebootDevice() on a B2B panel; unsupported otherwise |
shutdown |
setPanelMute("ON") + note (SSSP web API has no true power-off) |
| unknown | reported as unsupported |
device:screenshot-request |
best-effort capture (see note) |
device:remote-start / -stop |
start/stop ~1 fps preview streaming |
Partner-signing caveat (#125): the
b2bcontrol/systemcontrolprivileges inconfig.xmlonly take effect on a partner-signed.wgton a real SSSP panel. On the dev/URL-Launcher/web build (or a consumer TV) those surfaces are absent, so reboot returnsunsupported,screen_offuses the black overlay, and the startup capability log reportsbackend=none. Only a partner-signed build on real hardware fully validates reboot / panel power.
Screenshot/preview note: the TV decodes
<video>onto a hardware overlay plane and plays YouTube in a cross-origin<iframe>, neither of which can be read back into a<canvas>. So images capture for real; video/YouTube fall back to a status card (device + timestamp). The dashboard preview shows a truthful frame rather than a dead button. Full-fidelity video preview isn't feasible on the sideloaded Tizen runtime.
Updates (#122)
There is no in-app OTA for a sideloaded, signed .wgt. Updating a screen means
re-building and re-sideloading the .wgt (path B above), or — on Samsung B2B
signage — pushing it through the URL Launcher refresh / MDM (MagicINFO / SSSP)
channel. The dashboard update command therefore just tells the screen an update is
pending; it cannot self-apply. If you run the URL Launcher path (A), a plain
TV reboot re-fetches …/player and you're current with the server with no .wgt step.
Auto-launch on boot (#122)
Boot auto-start for a sideloaded consumer TV web app is a display setting, not an
app setting — there's no config.xml autostart for the TV profile. Configure it on
the panel:
- URL Launcher path (A): set the URL Launcher as the boot app (it relaunches on power-up automatically) — the recommended signage setup.
- Signed-app path (B): use the TV's kiosk / auto-start app setting (B2B/SSSP firmware) to launch ScreenTinker on boot; on dev-mode consumer TVs there's no guaranteed boot-launch, so the URL Launcher path is preferred for unattended screens.
Version reporting (#119)
app_version is sourced from config.xml's version="" — read at runtime via the
Tizen application API, with a build-stamped constant fallback (build-wgt.sh stamps it
from config.xml). The dashboard always shows the version actually installed.