feat: share a panel to LAN devices — 📡 LAN, persistent room key, /ws/sync relay (#21)#22
Conversation
Where ⇱ Pop opens a follower window on this machine, 📡 LAN lets a browser on another machine watch a panel live-synced to this machine's playback (splitscreen#21), consuming the core session-sync relay (feedBack#1030/#1032). The follower architecture already crossed machines everywhere except the sync channel: followers boot from a config, chart data streams per-panel over /ws/highway, and the follower clock interpolates between time messages. This adds the missing transport plus the join flow: - Persistent 6-char room key (unambiguous alphabet, case-insensitive), generated once, shown in settings with Regenerate. Viewers join at /?ss=<key> — nothing else in the URL, so it's typeable and bookmarkable across sessions. - hello/config handshake over the relay: the host answers every hello with the shared panel cfg + filename + playhead, which gives late join, refresh, and reconnect for free on a stateless relay. - _followerBusHandler extracted as the shared dispatch for both transports; FOLLOWER became `let` so the remote boot can assign it (remote: true) and run the unchanged bootFollowerMode(). - Mirrors, not moves: the shared panel stays in the main layout; remote viewers hide Dock, never post docked/closed, and note-detect is stripped at both capture and boot. - Crash recovery: shares re-arm on load, viewers reconnect with backoff and re-hello; main-closed is a recoverable waiting state for remote viewers — only an explicit share-ended (Stop sharing) is terminal. - time frames throttled to ~20 Hz on the network leg; the local BroadcastChannel leg is unchanged (≤60 Hz). Unit tests 15 → 25; live protocol smoke (host + viewer + late joiner against a real server's relay) passes 8/8. Plugin 1.11.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Kris Anderson <topkoa@gmail.com>
|
Warning Review limit reached
Next review available in: 51 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughSplit-screen adds LAN panel sharing through persistent six-character room keys and a WebSocket relay. Hosts relay configuration and playback events, while remote viewers bootstrap as passive followers with reconnect handling, lifecycle overlays, and dedicated sharing controls. ChangesLAN sharing
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Host
participant Relay
participant Viewer
Host->>Relay: Open room and send config
Viewer->>Relay: Send hello
Relay->>Viewer: Return config
Host->>Relay: Send time, playstate, and song changes
Relay->>Viewer: Forward synchronization messages
Host->>Relay: Send share-ended or main-closed
Relay->>Viewer: Forward lifecycle state
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds LAN-based “remote follower” support to the Split Screen plugin by mirroring the existing popup sync protocol over the core /ws/sync/{session_id} relay, backed by a persistent 6-character room key and a new 📡 LAN UI flow.
Changes:
- Implement LAN share host + remote viewer join path in
screen.js(room key generation/persistence, relay connection, hello/config handshake, share modal, crash-resume). - Add settings UI for displaying/regenerating the persistent room key (
settings.html) and document the feature (README.md,CLAUDE.md). - Expand node unit tests for the new pure helpers and bump plugin version (
tests/screen.test.js,plugin.json).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| screen.js | Implements LAN share + remote viewer boot over /ws/sync, adds persistent room key helpers, and unifies follower message handling. |
| settings.html | Adds a “LAN room key” display and Regenerate button in Split Screen settings. |
| tests/screen.test.js | Adds unit coverage for room key helpers, sync URL building, and remote follower config shaping. |
| README.md | Documents how to use 📡 LAN sharing and key caveats/requirements. |
| plugin.json | Version bump and description update to include LAN sharing. |
| CLAUDE.md | Documents new localStorage keys and LAN share architecture details. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…misc - Gate both `config` consumers (pre-boot + recovery) on our own popupId: the relay room broadcasts every viewer's config reply to everyone. - The main-closed waiting state now re-hellos (shared hello-poll helper, self-stopping) and treats any live host traffic (time / playstate / song-changed) as proof the host is back — previously a relaunched host could leave the overlay stuck over already-synced panels if no fresh hello ever fetched a config. - settings.html: Regenerate button gets an explicit type="button". - tests: remote-config fixture uses the real _captureMode() encoding (`viz:<pluginId>`), not the saved-prefs `__viz__:` sentinel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Kris Anderson <topkoa@gmail.com>
_onFollowerOrphaned now stops the hello-poll and closes the relay WebSocket. Orphaned is terminal, but previously the socket stayed open in the room (inert thanks to the _followerOrphaned guards, yet a live network connection) and the poll could fire one more tick. _remoteWs is nulled before close so its onclose is a no-op — no reconnect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Kris Anderson <topkoa@gmail.com>
- _showLanShareModal: invocation nonce so a second open during the getLanAccess() await can't stack a second (orphaned) overlay — only the latest invocation renders. - Copy URL: navigator.clipboard doesn't exist in non-secure contexts, which is exactly where LAN hosts live (http://192.168.x.x Docker sessions). Fall back to hidden-textarea + execCommand('copy'), and flash a "Copy failed — select it above" notice instead of silently doing nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Kris Anderson <topkoa@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/screen.test.js (1)
194-200: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover an existing valid persisted key.
This only verifies a key generated during the test. Seed
splitscreenRoomKeywith a valid lowercase/trimmed value and assertensureRoomKey()returns its normalized form, so persisted-key normalization is covered.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/screen.test.js` around lines 194 - 200, Update the ensureRoomKey test to seed localStorage with a valid persisted splitscreenRoomKey before calling mod.ensureRoomKey(). Assert that the method returns the key’s normalized lowercase/trimmed form and preserves that normalized value in storage, while retaining the existing persistence and repeat-call assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Line 84: Update the README compatibility requirement near the /ws/sync relay
description to state an evaluable requirement: specify the feedBack release that
includes the `#1030` relay, or explicitly require a feedBack version containing
`#1030`. Preserve the existing behavior note for older servers.
---
Nitpick comments:
In `@tests/screen.test.js`:
- Around line 194-200: Update the ensureRoomKey test to seed localStorage with a
valid persisted splitscreenRoomKey before calling mod.ensureRoomKey(). Assert
that the method returns the key’s normalized lowercase/trimmed form and
preserves that normalized value in storage, while retaining the existing
persistence and repeat-call assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: be41b754-faea-4947-9e7d-b0188348d526
📒 Files selected for processing (6)
CLAUDE.mdREADME.mdplugin.jsonscreen.jssettings.htmltests/screen.test.js
_ensureMainBroadcasterAndListener guarded BOTH its halves with one flag, so a crash-recovery _maybeResumeLanShare() running before #audio exists would mark the whole function done and the play/pause listeners never attached — silencing every `playstate` message, which followers rely on exactly when `time` ticks stop (paused). Split the guards: the channel handler stays once-ever; the audio listeners re-attempt per call, keyed to the element they're bound to (also covers a replaced <audio>). Also refresh a stale test comment: modern node ships a global WebSocket; the ?ss= load test pins the _nodeTestEnv gate, not a missing API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Kris Anderson <topkoa@gmail.com>
Implements #21 — "pop out to LAN": share a panel to browsers on other devices, live-synced to this machine's playback. Consumes the core
/ws/sync/{session_id}relay (got-feedBack/feedBack#1030, merged in feedback#1032). Closes #21.What you get
http://<host>:<port>/?ss=K7TR4M— nothing else in the URL — and gets a full-window synced follower view (which can re-split itself 1/2/2/4, exactly like a local popup).splitscreenLanShareActive), viewers reconnect with backoff and re-helloon every open — kill the host mid-song, relaunch, and every viewer resumes with zero interaction.main-closedputs remote viewers in a recoverable "waiting" overlay; only an explicit Stop sharing (share-ended) is terminal.Design (matches the issue spec)
{type:'hello', popupId}; the host answers each with{type:'config', filename, cfg, t, playing}(only once a song is loaded — viewers hello-poll every 3 s until then). Re-answering every hello is what makes late join, refresh, and reconnect all work with a stateless relay._followerBusHandler) for both transports — the existing BroadcastChannel handler body, extracted. Popups behave exactly as before; remote viewers feed relay messages into the same function.FOLLOWERbecameletso the remote boot can assign it (withremote: true) before running the unchangedbootFollowerMode().docked/closed, and have the note-detect fields stripped both in the shared cfg and again inmakeRemoteFollowerCfg(viewers must never inherit mic/device bindings)./ws/highwaystream against the server, as followers always have. Only clock/session messages do, withtimethrottled to ~20 Hz on the network leg (_lanSend); the local BroadcastChannel stays ≤60 Hz. The follower interp makes 20 Hz visually indistinguishable.ABCDEFGHJKMNPQRSTVWXYZ23456789(no 0/O/1/I/L/U — read-aloud-safe), case-insensitive entry,crypto.getRandomValues. Regenerating rotates the key and stops any live share (its viewers would otherwise wait forever on an abandoned room).network.getLanAccess().urlswhen present (the host renderer itself runs on 127.0.0.1); falls back tolocation.origin(Docker/standalone, where the user is already on the LAN address). If desktop LAN access is off, the dialog says so instead of handing out a dead URL.Compatibility
/ws/sync): the share socket just never connects (retry loop is silent); local pop-out and everything else unchanged.ch.onmessagewiring, same dock semantics.splitscreenRoomKey,splitscreenLanShareActive,splitscreenLanShareCfg(documented in CLAUDE.md).Testing
node tests/screen.test.js, 15 → 25): room-key alphabet excludes every lookalike glyph; generate/normalize/ensure round-trips (case-insensitivity, trim, junk rejection, invalid-stored-key replacement);buildShareUrl/getSyncUrlshapes;makeRemoteFollowerCfg(detect stripped, remote flag, sane defaults);?ss=load in a WebSocket-less env neither boots nor throws. Note: modern node ships a globalWebSocket, so the end-of-IIFE boot is additionally gated out of the test env — a capability check alone would open real sockets under CI.main, with the merged relay): host + viewer + late joiner overws://…/ws/sync/K7TR4Mexercising the exact frames screen.js sends — hello→config handshake, no self-echo, 20 Hz time frames, song-changed, playstate, late-join config, share-ended fan-out. 8/8 pass.http://<lan-ip>:<port>/?ss=<key>.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Bug Fixes