mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-15 06:43:27 -06:00
* feat(group-sync): synchronized playback per group (server + Android) [stage 1]
Play a group's shared playlist in lockstep across its displays — start/end items
together — by reusing the video-wall sync primitive with the spatial transform
removed and keyed on group_id instead of wall_id.
Server:
- device_groups += sync_enabled + leader_device_id (optional pin).
- buildPlaylistPayload emits a group_sync:{group_id, is_leader} block ONLY for a
member whose playlist matches the group's shared playlist (playlist-match guard —
a mismatched member is ignored, never synced). Membership via device_group_members.
- Leader auto-election: pinned-if-online -> first online matching member -> stable
fallback; computed (never persisted, so the operator's pin is preserved).
- group:sync / group:sync-request relay among eligible members (mirrors wall:sync,
guarded on membership + playlist match).
- Self-heal: re-push payloads to group members on (re)connect so is_leader refreshes.
- PUT /api/device-groups/:id accepts sync_enabled + leader_device_id and re-pushes.
Android:
- WallController is now mode-aware. WALL = transform + object-fit:fill + forced
follower-mute (UNCHANGED — every wall branch is byte-equivalent when isGroup=false).
GROUP = same leader/follower timing incl. the full video drift controller, but
full-screen (no transform), normal fit, and per-item mute honored (no forced mute).
- WebSocketService: emitGroupSync/emitGroupSyncRequest + onGroupSync/onGroupSyncRequest.
- MainActivity: dispatch emit by mode, parseGroupConfig, onPlaylistUpdate group hook.
Wall path is provably unchanged (additive mode, defaults to WALL). Kotlin compiles;
server suite 407/407.
Stage 2 (follow-up): web + Tizen parity, dashboard sync toggle + leader picker,
offline-sweep leader promotion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(group-sync): web + Tizen player parity [stage 2]
Mirror the Android group-sync generalization in the two web-based players so a group
with a shared playlist syncs across mixed player types, not just Android.
Web player (server/player/index.html):
- group:sync / group:sync-request handlers (index jump + latency-compensated video
drift, same maths as wall:sync) — no audio policy here, per-item mute honored.
- emitGroupSync() + applyGroupSync() (4Hz leader broadcast; follower sync-request).
NO CSS transform, NO forced mute (unlike applyWallMode).
- isFollower now also true for a group follower (suppresses self-advance).
- handlePlaylistUpdate handles data.group_sync (mutually exclusive with wall).
Tizen (tizen/js/player.js WallController + app.js):
- WallController is mode-aware: WALL styles the slice + forced follower semantics
(UNCHANGED when mode !== 'group'); GROUP clears any wall styling (full-screen) and
drives leader/follower timing only. syncId()/clearStageStyle() helpers; emitSync/
onSync/onSyncRequest key the event + id off the mode.
- app.js: group:sync/request socket handlers; onPlaylist enters group sync on a
group_sync block, else exits — content renders through the normal single-zone path.
Realigned the v4-exit-signal-phase3 TIZEN slice (682->696) shifted by the app.js edits.
Wall path is provably unchanged in both (additive group mode). Server suite 407/407;
both players' JS parse.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(group-sync): dashboard UI — sync toggle + leader picker [stage 3]
On each group's header row:
- "Sync" checkbox -> PUT /api/groups/:id { sync_enabled } enables synchronized
playback; server re-pushes to members so they enter/exit sync mode. A hint notes
it needs a group playlist and that a display on a different playlist is ignored.
- When on, a "Leader: Auto / <display>" picker -> { leader_device_id } (null = auto-
elect, which self-heals; or pin a specific member to always lead when online).
Adds api.updateGroup(id, data) and the en i18n strings (en-only, mirroring the
existing per-group UI keys; the i18n parity test is apitoken-scoped).
Frontend parses (ESM); server suite 407/407.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(group-sync): rework to clock/schedule sync + double-buffer + polish
Replace the leader/follower relay model with clock/schedule sync. Every
same-playlist member derives the identical (index, position) locally from a
server-disciplined clock + the deterministic playlist schedule, so sync:
- needs no server at play-time (offline-native), and
- has no leader role to double-elect (kills the split-brain class the leaked
WallController tick produced).
Server
- heartbeat-ack now carries server_ms + echoes client_ms for NTP-style clock
discipline; the client caches the offset (survives an outage).
- POST /groups/:id/resync -> group:resync (manual "Resync now").
- (kept: group_sync payload; leader machinery is now vestigial/ignored.)
Clients (web / Tizen / Android)
- Clock offset disciplined over the heartbeat, cached (localStorage / prefs).
- Schedule engine: pos = (syncedNow mod Sum(duration_sec)) with a CANONICAL
slot formula identical across platforms so mixed-platform groups can't drift.
- Snap-on-load: a fresh clip hard-seeks ONCE to the exact position (was ~5s of
gentle nudge to eat a ~0.3s load offset); steady-state keeps the gentle nudge.
- Double buffer: warm the next clip a few s before the boundary -> instant
switch, no black hold. Android pre-decodes on a throwaway surface so the swap
doesn't flash one wrong-aspect (landscape-stretched) frame.
- In-place duration edits: duration_sec dropped from the change signature and
applied in place, so a duration edit re-anchors the schedule WITHOUT a restart.
- Live-log shows discrete corrections (jump/align/seek) immediately; only the
steady-state line is throttled.
Android
- Fix leaked WallController leader tick: onDestroy() now stops it (Handler on the
main looper outlived the Activity -> zombie broadcaster / split-brain).
Dashboard
- Group leader picker -> "Resync now" button.
Tests: heartbeat-ack clock fields + resync route; exit-signal .wgt slice realigned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| 148-eviction-storm.test.js | ||
| 148-half-open.test.js | ||
| 156-schedule-read-path.test.js | ||
| admin-users.test.js | ||
| agency-digest.test.js | ||
| agency-gate.test.js | ||
| agency-layouts.test.js | ||
| agency-list.test.js | ||
| agency-scope.test.js | ||
| agency.test.js | ||
| ai-design.test.js | ||
| api.test.js | ||
| apitoken-unit.test.js | ||
| billing-authz.test.js | ||
| billing-endpoint.test.js | ||
| billing-token-mint.test.js | ||
| billing-unit.test.js | ||
| block-authz.test.js | ||
| boot-health.test.js | ||
| branding.test.js | ||
| config-paths.test.js | ||
| content-ack-dedup.test.js | ||
| content-ack-flood.test.js | ||
| content-ack-limiter.test.js | ||
| content-ack-valve.test.js | ||
| debug-throughput.test.js | ||
| device-block-and-auth.test.js | ||
| device-pairing-notify.test.js | ||
| device-zone-contract.test.js | ||
| evicted-socket-rearm.test.js | ||
| fingerprint-reclaim.test.js | ||
| flap-limiter.test.js | ||
| flap-paired-exemption.test.js | ||
| flap-quarantine.test.js | ||
| ghcr-check.test.js | ||
| group-sync-clock.test.js | ||
| i18n-tokens.test.js | ||
| identity-resolver.test.js | ||
| kill-switches.test.js | ||
| log-selfprotect.test.js | ||
| loop-lag-integration.test.js | ||
| loop-lag.test.js | ||
| mute.test.js | ||
| observability-units.test.js | ||
| openapi-contract.test.js | ||
| operator-block.test.js | ||
| operator-permissions.test.js | ||
| ota-breaker-patchn.test.js | ||
| ota-breaker.test.js | ||
| ota-check.test.js | ||
| ota-hardening.test.js | ||
| pair-lockout.test.js | ||
| pip-overlay.test.js | ||
| player-media-health.test.js | ||
| playlist-build-cost.test.js | ||
| provisioning-cleanup.test.js | ||
| provisioning.test.js | ||
| reconnect-storm-load.test.js | ||
| reconnect-throttle-integration.test.js | ||
| reconnect-throttle.test.js | ||
| register-insert-crash.test.js | ||
| schedule-eval.test.js | ||
| schema-check.test.js | ||
| security-fixes.test.js | ||
| session-settle.test.js | ||
| socket-handler-isolation.test.js | ||
| status-debug-toggle.test.js | ||
| status-log-prune-hardening.test.js | ||
| status-log-prune.test.js | ||
| storm-harness.test.js | ||
| tcp-keepalive.test.js | ||
| tenant-cascade-migration.test.js | ||
| thumbnail-proxy.test.js | ||
| tizen-eval-drift.test.js | ||
| totp-keyrotation.test.js | ||
| totp-unit.test.js | ||
| totp.test.js | ||
| user-deletion.test.js | ||
| v4-core-liveness.test.js | ||
| v4-core-refresh-gate.test.js | ||
| v4-exit-signal-phase3.test.js | ||
| v4-exit-signal.test.js | ||
| widget-render-xss.test.js | ||