diff --git a/brightsign/README.md b/brightsign/README.md index 2e49099..1ab28c7 100644 --- a/brightsign/README.md +++ b/brightsign/README.md @@ -147,6 +147,30 @@ because the BrightSigns would look perfectly synchronised while the odd panel dr A player paired before this port is still recognised, by its BrightSign user agent. +### How the choice reaches a screen + +`device_groups.sync_backend` (`auto` | `screentinker` | `brightsign`) is the operator's **request**. +The server resolves it per push through `resolveSyncBackend()` and sends the answer — plus the +reason and a `downgraded` flag — in the `group_sync` payload, so the players, the dashboard and the +stored setting can never disagree about which protocol is running. + +Three things force a fallback to our protocol, and each is reported rather than applied silently: + +| condition | why native sync cannot run | +|---|---| +| any non-BrightSign member | BrightWall cannot include a foreign screen | +| members on different subnets | it is multicast; it does not cross networks | +| the elected leader is offline | it is leader/follower — nobody would broadcast | + +That last one has no equivalent in our protocol, which is leaderless and carries on regardless. +Leadership uses the existing election (`resolveGroupLeader`): the pinned leader if it is an online +member on the shared playlist, else the first online member, else the first member by id. + +**Item selection stays clock-derived under both backends.** Native sync only replaces the +seek/nudge drift correction, because `setSyncParams` has the video element hold its own alignment — +and correcting it ourselves would fight the platform. That also keeps images and widgets, which have +no `setSyncParams`, advancing with the videos instead of drifting off on their own. + ## Command parity The web player handles four of the ~20 fleet commands — `launch`, `refresh`, `screen_on`, @@ -194,13 +218,14 @@ have no BrightSign equivalent — a signage player has no per-window brightness 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. +- **Nothing consumes the `bs_model` / `bs_serial` / `bs_screen` fields** the player reports. Device + telemetry (temperature, storage) also has no schema to land in yet. +- **Native sync is wired but UNPROVEN on hardware.** The player drives it end to end — the leader + announces on each advance, every member (leader included) binds via `attachVideo()` on a new id, + and the resolved backend is chosen per group and pushed down. It cannot be verified with one + player: a single unit is trivially "in sync with itself". **Two BrightSigns on one subnet are + needed** to confirm frame alignment, that the leader does not run ahead, and that the 1Hz repeat + causes no visible reload. ```js const SyncManager = require('@brightsign/syncmanager'); // BrightSignOS 8.2.10+ diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js index 2c3ad53..1d9f852 100644 --- a/frontend/js/i18n/en.js +++ b/frontend/js/i18n/en.js @@ -207,6 +207,12 @@ export default { 'dashboard.group_sync.toast_on': 'Synchronized playback enabled', 'dashboard.group_sync.toast_off': 'Synchronized playback disabled', 'dashboard.group_sync.toast_resync': 'Resync sent to group', + 'dashboard.group_sync.backend_auto': 'Sync: Auto', + 'dashboard.group_sync.backend_screentinker': 'Sync: Standard', + 'dashboard.group_sync.backend_brightsign': 'Sync: BrightSign', + 'dashboard.group_sync.backend_hint': "Which synchronisation protocol this group uses. Standard works across every player type and keeps displays aligned to the second, with no leader and no internet needed. BrightSign is frame-accurate but only works when every display in the group is a BrightSign on the same network, and it synchronises video only. Auto picks BrightSign when the group can actually run it, and Standard otherwise.", + 'dashboard.group_sync.toast_backend': 'Sync protocol updated', + 'dashboard.group_sync.toast_downgraded': 'Saved, but this group cannot run that protocol:', 'dashboard.manage_tooltip': 'Add/remove devices', 'dashboard.delete_group_tooltip': 'Delete group', 'dashboard.no_devices_in_group': 'No devices in this group. Click Manage to add some.', diff --git a/frontend/js/views/dashboard.js b/frontend/js/views/dashboard.js index 60232dd..1369532 100644 --- a/frontend/js/views/dashboard.js +++ b/frontend/js/views/dashboard.js @@ -234,6 +234,14 @@ function renderGroupSection(group, devices, playlists) { ${t('dashboard.group_sync.label')} ${group.sync_enabled ? ` + + ${group.sync_effective ? ` + ${group.sync_downgraded ? '⚠ ' : ''}${esc(group.sync_effective)}${group.sync_reason ? ' — ' + esc(group.sync_reason) : ''}` : ''} ` : ''} ` : ''} @@ -862,6 +870,31 @@ function attachGroupHandlers(groupsWithDevices, allDevices) { }); }); + // Choose the sync protocol. The server may refuse the choice (native sync needs every member to + // be a BrightSign on one L2 network), so re-render from its answer rather than assuming the + // request took — showing a setting that isn't in force is exactly what makes a drifting wall + // impossible to diagnose. + document.querySelectorAll('.group-backend-select').forEach(sel => { + sel.addEventListener('change', async (e) => { + const groupId = e.target.dataset.groupId; + const previous = sel.dataset.previous || 'auto'; + const chosen = e.target.value; + try { + const updated = await api.updateGroup(groupId, { sync_backend: chosen }); + if (updated?.sync_downgraded && updated?.sync_reason) { + showToast(t('dashboard.group_sync.toast_downgraded') + ' ' + updated.sync_reason, 'warning'); + } else { + showToast(t('dashboard.group_sync.toast_backend'), 'success'); + } + loadDashboard(); + } catch (err) { + showToast(err.message, 'error'); + e.target.value = previous; + } + }); + sel.dataset.previous = sel.value; + }); + // #group-sync: manual "Resync now" — nudge all members to re-snap to the shared schedule. document.querySelectorAll('.group-resync-btn').forEach(btn => { btn.addEventListener('click', async (e) => { diff --git a/server/db/database.js b/server/db/database.js index 7bc4305..5656c2b 100644 --- a/server/db/database.js +++ b/server/db/database.js @@ -167,6 +167,12 @@ const migrations = [ // or offline the server auto-elects the first online member on the matching playlist. "ALTER TABLE device_groups ADD COLUMN sync_enabled INTEGER NOT NULL DEFAULT 0", "ALTER TABLE device_groups ADD COLUMN leader_device_id TEXT REFERENCES devices(id) ON DELETE SET NULL", + // Which synchronisation protocol the group runs: 'auto' | 'screentinker' | 'brightsign'. + // BrightSign's native SyncManager is frame-accurate but exists only between BrightSign players + // on one L2 network, so it cannot be the default — 'auto' picks it only when the group can + // actually run it. See server/lib/sync-backend.js; the resolver is the single source of that + // decision and this column is only the operator's request. + "ALTER TABLE device_groups ADD COLUMN sync_backend TEXT NOT NULL DEFAULT 'auto'", // Wall-level playlist: video walls now play a playlist (not just one content). "ALTER TABLE video_walls ADD COLUMN playlist_id TEXT REFERENCES playlists(id) ON DELETE SET NULL", // Free-form canvas layout: walls store a player rect; member devices store diff --git a/server/player/index.html b/server/player/index.html index 35d01d8..6f66ffe 100644 --- a/server/player/index.html +++ b/server/player/index.html @@ -597,6 +597,15 @@ let groupAlignPending = true; let groupLastAlignedIndex = -1; let groupLastSeekAt = 0; // seek cooldown — don't hard-seek every tick (decoder-thrash guard) + // BrightSign native sync (SyncManager). An ALTERNATIVE to the clock-derived correction above, + // never an addition: when it is running, the seek/nudge maths is skipped entirely because the + // video element aligns itself once setSyncParams has been applied. Item SELECTION stays + // clock-derived either way — that is what keeps images and widgets, which have no + // setSyncParams, advancing together with the videos. + let nativeSync = null; // the ScreenTinkerBSSync instance while active + let nativeSyncEvent = null; // latest sync event awaiting a video to bind + let nativeSyncBoundId = null; // sync id already bound, so we attach once per session + let nativeSyncAnnounced = -1; // last index the LEADER announced, to announce once per advance // Double buffer: a hidden