mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 06:16:20 -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
129 lines
5.4 KiB
JavaScript
129 lines
5.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');
|
|
}
|
|
|
|
/*
|
|
* SyncManager is MULTICAST (224.0.126.10:1539), so every member must share one L2 network. A group
|
|
* spanning sites or VLANs cannot use it — and the failure is silent: each subnet would sync neatly
|
|
* within itself while drifting from the others.
|
|
*
|
|
* The evidence available server-side is the address each device connects from. Comparing the /24
|
|
* is a heuristic, not proof — two VLANs can share a public IP, and one subnet can span a routed
|
|
* boundary that blocks multicast. So it is used in ONE direction only: differing networks are
|
|
* treated as evidence against native sync, while matching ones are never treated as proof for it.
|
|
* Unknown addresses block nothing, because "we cannot see it" must not read as "it is broken".
|
|
*/
|
|
function networkOf(device) {
|
|
const ip = String((device && (device.ip_address || device.last_ip)) || '').trim();
|
|
if (!ip) return null;
|
|
if (ip.includes(':')) { // IPv6: compare the /64
|
|
const parts = ip.split(':');
|
|
return parts.slice(0, 4).join(':').toLowerCase();
|
|
}
|
|
const octets = ip.split('.');
|
|
if (octets.length !== 4) return null;
|
|
return octets.slice(0, 3).join('.');
|
|
}
|
|
|
|
function networksDiffer(members) {
|
|
const nets = members.map(networkOf).filter(Boolean);
|
|
if (nets.length < 2) return false; // nothing to contradict
|
|
return new Set(nets).size > 1;
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
const split = networksDiffer(list);
|
|
|
|
if (requested === 'screentinker') {
|
|
return { backend: 'screentinker', reason: 'explicitly selected', downgraded: false };
|
|
}
|
|
|
|
if (requested === 'brightsign') {
|
|
if (allBrightSign && !split) {
|
|
return { backend: 'brightsign', reason: 'explicitly selected', downgraded: false };
|
|
}
|
|
if (allBrightSign && split) {
|
|
// Every member is a BrightSign, but they are not on one network. Native sync would appear
|
|
// to work inside each subnet while the subnets drifted apart — worse than not using it.
|
|
return {
|
|
backend: 'screentinker',
|
|
reason: 'displays are on different networks — native sync is multicast and cannot cross them',
|
|
downgraded: true
|
|
};
|
|
}
|
|
// 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 && !split) {
|
|
return { backend: 'brightsign', reason: 'every display is a BrightSign', downgraded: false };
|
|
}
|
|
if (allBrightSign && split) {
|
|
return {
|
|
backend: 'screentinker',
|
|
reason: 'displays are on different networks',
|
|
downgraded: false
|
|
};
|
|
}
|
|
return {
|
|
backend: 'screentinker',
|
|
reason: list.length === 0 ? 'no displays in the group' : 'mixed fleet',
|
|
downgraded: false
|
|
};
|
|
}
|
|
|
|
module.exports = { resolveSyncBackend, isBrightSignDevice, networksDiffer, BACKENDS };
|