mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 06:16:20 -06:00
The player is the unmodified web player in an roHtmlWidget — that already runs
on real hardware. What was missing is everything a page cannot do for itself.
autorun.brs becomes a host rather than a URL wrapper. It owns the widget
lifecycle, because a page-initiated location.reload() does not reliably bring an
roHtmlWidget back: a deploy on 2026-07-28 reloaded every connected player and
the BrightSign was the only one that never returned. The page now posts
{type:"restart"} and the host rebuilds the widget. It also retries load-error
with backoff, falls back to a local page, and runs a heartbeat watchdog that
catches the case load-error never reports — a page that loaded fine and then
wedged on a dead socket or a stalled decoder.
st-bridge.js is the page's half over @brightsign/messageport: registry-backed
identity (localStorage is origin- and quota-bound, the registry is not),
restart-instead-of-reload, heartbeat, and sync-backend reporting. Every method
degrades to a no-op off-platform, so it is safe to load unconditionally.
sync-backend.js decides whose synchronisation a group runs. Ours is
clock-derived and spans any mix of Android, web, Tizen and BrightSign; BrightWall
is frame-accurate and BrightSign-only. auto picks native when every member is a
BrightSign. The refusal that matters: native sync selected for a mixed group
downgrades and says why, because a half-synced group would look perfectly
synchronised on the dashboard while one panel drifted alone.
Dual output via output_mode single|dual|clone — a second widget loads the same
player with &screen=2 so the server can give it its own playlist.
Written against the BrightDeveloper docs; not yet run on hardware. The README
lists what is unimplemented, including the BrightWall runtime API, which that
doc set does not cover.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
83 lines
3.4 KiB
JavaScript
83 lines
3.4 KiB
JavaScript
'use strict';
|
|
|
|
/*
|
|
* Which synchronisation protocol a group runs.
|
|
*
|
|
* ScreenTinker has its own group sync: every member derives its position from a shared clock,
|
|
* so it needs no leader, survives a server outage, and works across Android, web, Tizen and
|
|
* BrightSign alike. BrightSign has its own — BrightWall — which is native, frame-accurate, and
|
|
* only exists between BrightSign players.
|
|
*
|
|
* The choice is therefore not "which is better" but "what is in this group":
|
|
*
|
|
* screentinker works everywhere, mixed fleets included; sync is to the second, not the frame
|
|
* brightsign frame-accurate video walls; requires EVERY member to be a BrightSign
|
|
*
|
|
* `auto` picks the strongest protocol the group can actually run, which is what an operator
|
|
* means when they say "just make the wall work". Explicit settings are honoured, except the one
|
|
* that cannot physically work (native sync with a non-BrightSign member) — that downgrades and
|
|
* says why, rather than silently doing nothing on the screens that can't participate.
|
|
*
|
|
* Kept pure so the decision is testable without a fleet: callers pass plain device rows.
|
|
*/
|
|
|
|
const BACKENDS = ['auto', 'screentinker', 'brightsign'];
|
|
|
|
/*
|
|
* A device is a BrightSign if it said so. The player sends ?platform=brightsign (autorun.brs
|
|
* puts it there), which lands in devices.platform. The UA fallback covers players paired before
|
|
* the port existed — those registered a platform of "Chrome 120" with a BrightSign UA.
|
|
*/
|
|
function isBrightSignDevice(device) {
|
|
if (!device) return false;
|
|
const platform = String(device.platform || '').toLowerCase();
|
|
if (platform.includes('brightsign')) return true;
|
|
const ua = String(device.user_agent || '').toLowerCase();
|
|
return ua.includes('brightsign');
|
|
}
|
|
|
|
/**
|
|
* @param {string} setting 'auto' | 'screentinker' | 'brightsign' (unknown values read as auto)
|
|
* @param {Array} members device rows in the group
|
|
* @returns {{backend: 'screentinker'|'brightsign', reason: string, downgraded: boolean}}
|
|
*/
|
|
function resolveSyncBackend(setting, members) {
|
|
const list = Array.isArray(members) ? members.filter(Boolean) : [];
|
|
const requested = BACKENDS.includes(setting) ? setting : 'auto';
|
|
|
|
const brightsignCount = list.filter(isBrightSignDevice).length;
|
|
const allBrightSign = list.length > 0 && brightsignCount === list.length;
|
|
|
|
if (requested === 'screentinker') {
|
|
return { backend: 'screentinker', reason: 'explicitly selected', downgraded: false };
|
|
}
|
|
|
|
if (requested === 'brightsign') {
|
|
if (allBrightSign) {
|
|
return { backend: 'brightsign', reason: 'explicitly selected', downgraded: false };
|
|
}
|
|
// Refusing to pretend: BrightWall cannot include a non-BrightSign screen, and a group that
|
|
// half-syncs is worse than one that syncs to the second everywhere.
|
|
const others = list.length - brightsignCount;
|
|
return {
|
|
backend: 'screentinker',
|
|
reason: list.length === 0
|
|
? 'group is empty — native sync needs BrightSign members'
|
|
: `group has ${others} non-BrightSign display${others === 1 ? '' : 's'}`,
|
|
downgraded: true
|
|
};
|
|
}
|
|
|
|
// auto
|
|
if (allBrightSign) {
|
|
return { backend: 'brightsign', reason: 'every display is a BrightSign', downgraded: false };
|
|
}
|
|
return {
|
|
backend: 'screentinker',
|
|
reason: list.length === 0 ? 'no displays in the group' : 'mixed fleet',
|
|
downgraded: false
|
|
};
|
|
}
|
|
|
|
module.exports = { resolveSyncBackend, isBrightSignDevice, BACKENDS };
|