mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
st-sync.js wraps SyncManager, the native protocol. Three properties drove the shape of it. It repeats the sync broadcast at 1Hz so a player powered on late still joins, which means acting on every repeat would reload the video once a second forever — on screen that reads as a stutter, not as a sync fault, so the id dedupe is mandatory rather than an optimisation. The leader starts from its OWN broadcast rather than at announce() time, or it runs ahead of the group by the width of the network. And attachVideo refuses an element with no setSyncParams instead of half-syncing it. offline.html is the local fallback the host falls back to after three failed loads. It names the server, keeps probing with capped backoff so a site full of panels cannot storm a server that is coming back, and asks the HOST to restart the player when it answers — never navigating itself, for the same reason the player never reloads itself here. The resolver now models multicast reach. All-BrightSign groups spread across subnets no longer get native sync: each subnet would sync neatly within itself while drifting from the others, and the dashboard would show a healthy group throughout. The IP comparison is a heuristic so it is used in one direction only — differing networks are evidence against, matching ones are never proof for, and unknown addresses block nothing. st-sync.js is served from its single source like the bridge, and the SD card deliberately carries neither: the player pulls both from the server so a stale copy on a card can never skew from the player using it. 948 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
178 lines
10 KiB
Markdown
178 lines
10 KiB
Markdown
# ScreenTinker on BrightSign
|
|
|
|
The player is the ordinary web player (`server/player/index.html`) running in an `roHtmlWidget`.
|
|
It already runs unmodified on real hardware — a Series 5 (HD1026, BOS 9.1, Chromium 120) played
|
|
4,723 items over 12.4h averaging 9.4s against a 10s slot. So the port is not "can it run". It is
|
|
the four things a page cannot do for itself.
|
|
|
|
```
|
|
autorun.brs the host: owns the widget, identity, outputs, recovery
|
|
| @brightsign/messageport (bidirectional)
|
|
st-bridge.js the page's half of the same contract
|
|
|
|
|
server/player/index.html the unmodified player
|
|
```
|
|
|
|
## Files
|
|
|
|
| file | role |
|
|
|---|---|
|
|
| `autorun.brs` | BrightScript host. Builds the widget, supervises it, persists identity, drives a second output, executes what the page cannot. |
|
|
| `st-bridge.js` | Loaded by the player on this platform. Registry identity, restart-instead-of-reload, heartbeat, sync-backend reporting. Degrades to no-ops everywhere else, so it is safe to load unconditionally. |
|
|
| `st-sync.js` | Native SyncManager adapter. Inert without the platform module, so the player falls back to its own group sync. |
|
|
| `probe.html` | The original capability probe. Still useful on a new model/OS build. |
|
|
| `offline.html` | Local fallback page — names the server, keeps probing it, and asks the host to restart the player the moment it answers. |
|
|
|
|
## The four things the host exists for
|
|
|
|
**1. It owns the widget lifecycle.** A page-initiated `location.reload()` does not reliably bring
|
|
an `roHtmlWidget` back. On 2026-07-28 a ScreenTinker deploy reloaded every connected player;
|
|
the BrightSign was the only one that never returned, and a browser on the same deploy reloaded and
|
|
was heartbeating minutes later. So the page never reloads itself here — it posts
|
|
`{type:"restart"}` and the host tears the widget down and builds a new one. Without this, every
|
|
deploy silently darkens every BrightSign panel until someone power-cycles it.
|
|
|
|
**2. It recovers.** `load-error` retries with backoff (5s → 15s → 30s → 60s) and after three
|
|
failures falls back to a local page, so a dead server shows something truthful instead of white.
|
|
On top of that, a watchdog: the page beats every 30s and three missed beats rebuild the widget.
|
|
That covers the case `load-error` never reports — a page that loaded fine and then wedged on a
|
|
dead socket, a JS exception, or a stalled decoder.
|
|
|
|
**3. Identity lives in the registry.** `localStorage` is tied to the page's origin and quota; the
|
|
registry survives reboots, content updates and origin changes. The hardware serial is the stable
|
|
id, so two panels imaged from the same card never collide — which is exactly how the web player's
|
|
hardware-only fingerprint once merged two identical panels into a single device row.
|
|
|
|
**4. It reaches BrightScript-only capabilities** — video mode, a second output, and native
|
|
BrightWall sync — on the page's behalf, over `@brightsign/messageport`.
|
|
|
|
## What goes on the SD card
|
|
|
|
```
|
|
autorun.brs the host
|
|
offline.html local fallback, used after three failed loads
|
|
screentinker.json optional — server URL, sync backend, output mode
|
|
```
|
|
|
|
**`st-bridge.js` and `st-sync.js` do NOT go on the card.** The player pulls them from the server
|
|
(`/player/st-bridge.js`, `/player/st-sync.js`) so they can never skew from the player that uses
|
|
them. A stale copy on a card is precisely the version skew that would leave a panel unable to
|
|
restart itself.
|
|
|
|
## Provisioning
|
|
|
|
Config resolves `screentinker.json` on the card **>** registry **>** built-in default. The JSON
|
|
file is how a batch gets imaged without touching each box:
|
|
|
|
```json
|
|
{ "server_url": "https://screentinker.com", "sync_backend": "auto", "output_mode": "single" }
|
|
```
|
|
|
|
## Dual output
|
|
|
|
`output_mode` is `single` | `dual` | `clone`.
|
|
|
|
- **dual** — a second widget loads the same player with `&screen=2`, so the server can hand it its
|
|
own playlist. Two independent displays from one player.
|
|
- **clone** — the second widget loads `&screen=1`: the same content on both outputs.
|
|
|
|
Multi-output models are **XC2055** (dual HDMI), **XC4055** (quad), and **XT245 / XT1145 / XT2145**
|
|
(dual HDMI, dual 4K60p simultaneous). Every other model is single-output, so the second widget is
|
|
only ever created when the config asks for it — an unsupported model keeps working as a normal
|
|
single-screen player rather than failing to start.
|
|
|
|
## Synchronisation — ours or theirs
|
|
|
|
Both, chosen per group. `server/lib/sync-backend.js` decides and `resolveSyncBackend()` is pure,
|
|
so the decision is tested without a fleet (`server/test/sync-backend.test.js`).
|
|
|
|
| backend | reach | accuracy |
|
|
|---|---|---|
|
|
| `screentinker` | Android, web, Tizen, BrightSign — any mix | to the second; clock-derived, no leader, survives a server outage |
|
|
| `brightsign` | BrightSign only | frame-accurate (BrightWall) |
|
|
|
|
`auto` picks native sync when **every** member is a BrightSign and ours otherwise. Explicit
|
|
settings are honoured, with one refusal: native sync selected for a group containing a
|
|
non-BrightSign display **downgrades and reports why**. A group that half-syncs is worse than one
|
|
that syncs to the second everywhere — and the failure would be invisible from the dashboard,
|
|
because the BrightSigns would look perfectly synchronised while the odd panel drifted alone.
|
|
|
|
A player paired before this port is still recognised, by its BrightSign user agent.
|
|
|
|
## What is NOT done yet
|
|
|
|
Stated plainly so nobody reads this as finished:
|
|
|
|
- **No server-side plumbing**: no `sync_backend` column, no dashboard control, nothing sends
|
|
`set-sync-backend` down, and nothing consumes the `bs_model` / `bs_serial` / `bs_screen` fields
|
|
the player now reports. The resolver is ready for all of it.
|
|
- **Native sync is implemented but not yet driven by the playlist engine.** `st-sync.js` wraps
|
|
SyncManager and is tested (`server/test/brightsign-sync.test.js`), but nothing in the player
|
|
calls `announce()` on item advance or binds `attachVideo()` yet, and no leader is designated.
|
|
That wiring is the next step and wants hardware to validate.
|
|
|
|
```js
|
|
const SyncManager = require('@brightsign/syncmanager'); // BrightSignOS 8.2.10+
|
|
const sync = new SyncManager('', 'ScreenTinkerSync', '224.0.126.10', 1539);
|
|
sync.leader = true; // followers just omit this
|
|
sync.addEventListener('syncevent', (e) => { // BOTH roles listen
|
|
if (e.id === lastId) return; // 1Hz rebroadcast — dedupe!
|
|
lastId = e.id;
|
|
video.setSyncParams(e.domain, e.id, e.iso_timestamp); // extension on <video>
|
|
video.load(); video.play();
|
|
});
|
|
sync.synchronize('item_' + Date.now(), 1000); // leader only; msDelay to prep
|
|
```
|
|
|
|
Three properties that shaped the design: it is **leader/follower** where ours is leaderless (and
|
|
the leader starts from its OWN broadcast, or it runs ahead of the group); it synchronises
|
|
**video only**, so images and widgets get item-boundary alignment at best; and it is
|
|
**multicast**, so the whole group must share one L2 network — the resolver now treats differing
|
|
subnets as evidence against it.
|
|
|
|
Also: MP4/MOV are fine, MPEG-TS needs its presentation timestamp starting at 0, MPEG-PS is
|
|
unsupported. `synchronize()` rebroadcasts at 1Hz so late-powered players still join, which is
|
|
why the dedupe above is mandatory rather than an optimisation — without it every player reloads
|
|
its video once a second, forever.
|
|
- **Addressing a specific HDMI connector from JS is unverified.** `@brightsign/videooutput`
|
|
documents `setMode({width,height,refreshRate})` with no output index. Dual output above assumes
|
|
a second widget maps to the second connector; that needs hardware confirmation.
|
|
- **Registry from a remote origin is still unproven** — the original probe question. If injection
|
|
turns out to be origin-dependent, identity moves to a local shim page that owns the registry and
|
|
passes it to the hosted player in an iframe via `postMessage`.
|
|
- **Nothing here has run on hardware.** It is written against the BrightDeveloper docs and
|
|
checked line-by-line against the `brightsign/dev-cookbook` examples, which corrected four
|
|
config keys, the registry API and a hard SyncManager requirement (see below).
|
|
|
|
## Verified against the dev-cookbook
|
|
|
|
`autorun.brs` and `st-bridge.js` were reviewed against the real examples rather than the prose:
|
|
|
|
- **`brightsign_js_objects_enabled: true` is required** alongside `nodejs_enabled` for
|
|
`require("@brightsign/*")` (`syncmanager-js/autorun.brs`). Without it the bridge degrades to
|
|
no-ops and the player silently loses identity *and* restart delegation — the failure would look
|
|
like "BrightSign just doesn't work" rather than a missing flag.
|
|
- **`storage_path` is a directory name** (`"/cache"`), not a volume, and **`storage_quota` is a
|
|
string** (`indexeddb-caching/autorun.brs`).
|
|
- **`security_params: { websecurity: true }`** and `hwz_default: "on"` are the shapes the examples
|
|
use; local URLs carry the volume (`file:/SD:/index.html`).
|
|
- **The registry API is asynchronous and section-oriented**: `read(section, key)` returns a
|
|
**Promise** and writes take an object — `write(section, {k: v})`. The bridge prefetches into a
|
|
cache and exposes `onReady()`; the player waits for it before its first connect, because
|
|
registering early would pair the panel as a new display and strand its real row.
|
|
- **SyncManager needs `networking/ptp_domain = "0"`, applied by a reboot**
|
|
(`syncmanager-js/autorun.brs`). Done only when this player is configured for native sync, and
|
|
read-before-write so it reboots at most once rather than every boot.
|
|
- Confirmed correct as written: `@brightsign/messageport` (`new`, `addEventListener('bsmessage')`,
|
|
`PostBSMessage`), the `roHtmlWidgetEvent` loop, and `RebootSystem()`.
|
|
- The notes state a widget URL may be **"an externally hosted page"** with the same access to the
|
|
BrightSign JS APIs, which is the answer the original probe was built to get — still worth
|
|
confirming on hardware, but the documented answer is the favourable one.
|
|
|
|
## Model notes
|
|
|
|
Target **Series 5** (Chromium 120) or newer. **Series 4 is pinned to Chromium 87**, and Series 4
|
|
and older have fixed graphics/JS memory splits (XTx43/44: 512MB/512MB; HDx23: 256MB/128MB) where
|
|
Series 5 allocates dynamically. Image size defaults to 2048x1280x32bpp (3840x2160 on XT/4K models)
|
|
and is raised with `roVideoMode.SetImageSizeThreshold()`.
|