10 KiB
Direct device media transfers
Existing paths (audit before implementation)
| Path | Existing media-byte route | Relevant code |
|---|---|---|
| Paired remote | Controls only; the host plays its own source. No existing file-send action. | frontend/app.js Remote; server/remote.js |
| Presenter and OBS | Lyrics, timing and metadata only; neither currently plays host media. | Presenter and overlay modules; remote relay |
| Watch Party playback | Each guest loads independently through normal server/YouTube playback, unless already saved locally. | Party applyState, Player.loadVideo; server/party.js |
| Watch Party voice | Direct WebRTC audio; server forwards SDP/ICE only. | Party voice mesh, party rtc relay |
| On other devices / failed playback | Existing verified-file DataChannel download, 64 KiB frames, SHA-256 validation, worker writes OPFS. Only offered after server failure in playback. | p2p-transfer.js, p2p-recv-worker.js, getFromPeers |
| Save / preload | Server download first, then OPFS. | preload, API.cacheDownload, download endpoints |
| Export to Files / Photos | Local OPFS first, server URL next, peer fallback last. Native share is an OS export, not paired-device transfer. | exportToDevice, export.js |
| Profile / playlist share and send | Metadata only. Sending a playlist does not transfer its media files. | Profile/playlist sync APIs |
| Verify & share | Explicit device upload to server intake; hash and media validation admit it into the server cache. | P2PClient.contribute, p2p-intake.js |
| Automatic rehydration | Server requests a holder to upload its full file to intake. This is an implicit media-byte server path. | createRehydrator, client upload-request handler |
| Admin uploads / original YouTube downloads | Intentional server ingestion of original media; separate from direct device transfer. | Upload and media-cache endpoints |
| VLC / external player URLs | HTTP server media URLs; these players cannot consume this application's DataChannel. | External player actions |
The current file protocol has no resume: its receiver truncates partial files and removes them on failure. Connection timeout is 20 seconds per holder. Device signalling authenticates a device, but does not constrain its destination to a verified profile or paired room. Profile names supplied at device registration are not proof of profile membership. Existing server-verified holdings also exclude files whose bytes the server has never admitted.
Implementation design
Reuse ordered RTCDataChannel transfers with public STUN only. The WebSocket server carries bounded, validated signalling and metadata, never file chunks. Pair authorization must come from an authenticated remote/party room or a verified profile session, not an arbitrary client-supplied profile name.
A receiver explicitly accepts an invitation before saving. A cryptographically random, expiring, one-use invitation binds sender, receiver and file identity; subsequent signalling remains bound to that pair. Reject replay, mismatched identities, binary WebSocket messages and unrecognized protocol fields.
Send 64 KiB chunks with bufferedAmount backpressure. Persist a partial OPFS file and resume at its exact committed byte offset. Rehash the retained prefix before continuing and verify total size and SHA-256 before promoting the file into the saved store; only then record metadata. A changed hash/size cannot reuse a partial.
Use a 10-second connection deadline and an explicit, clearly labelled server fallback offer. Direct mode must not silently rehydrate server media from a device. Register “Direct device transfer (P2P)” in Settings, default on.
Party playback should request a host's saved file when announced, then play the receiver's OPFS copy. Remote controls and presenter lyrics retain their existing behaviour; add explicit receive/play actions rather than automatically starting media in a control-only screen. A manual send action targets authenticated peers.
Ordinary faststart MP4 cannot safely be appended as arbitrary MediaSource chunks. Progressive playback requires supported fragmented media and codec detection; otherwise use the completed, verified file (including iOS). Do not claim that raw MP4 chunks provide progressive playback.
Verification required before release
Pure tests cover protocol parsing, chunk bounds, offsets, invitation replay and pair authorization. A two-page localhost browser test must transfer real bytes into OPFS through WebRTC and assert that server HTTP/WS logs contain only signalling and metadata. Exercise interruption/resume and explicit fallback. Review real iPhone foreground/background behaviour and restrictive NAT failure.
Implemented behaviour
direct-protocol.jsstrips unexpected fields and validates file claims and bounded signalling;direct-relay.jsbinds invitations to authenticated room members. Accepting an invitation is one-use, and only the bound pair can signal.- Remote, presenter and party relays carry the new protocol. Presenter and OBS still display lyrics/control metadata; they do not automatically play media. A presenter can explicitly accept a file sent by its paired host.
- Settings → Direct device transfer provides the default-on toggle and send buttons for currently connected paired/party/profile peers. Send the currently playing saved song; the receiving device confirms every copy.
- The paired remote has “Play here from paired screen” when its host has saved media. Party guests and ordinary playback resolve an advertised direct source before loading media. Saves and exports do the same; the existing OS export sheet is preserved.
/ws/p2pnow has a profile-scoped, ephemeral file-id directory. Protected profiles require password/key proof, checked against the server profile hash. Unprotected profiles retain their existing name-based access policy. Claims are not admitted as server-verified media. Each device advertises at most 1,000 local ids per directory update; use paired-room sending for larger libraries.- The legacy unconfirmed global signalling path is refused. Older clients need to reload the updated application to transfer files. This closes a bypass of receiver confirmation and room/profile membership.
direct-media.jsuses public STUN, ordered 64 KiB DataChannel chunks and bufferedAmount backpressure.direct-recv-worker.jswrites partials directly to OPFS, rehashes their retained prefix and verifies exact size plus SHA-256 before promotion. File extensions are preserved. A disconnected partial can be resumed by sending/requesting the same file again, including non-aligned offsets.- The 10-second deadline covers connection establishment. Prefix rehashing and final verification do not consume it. Idle transfer timeout is 30 seconds. Failures retain partial data and offer an explicit Use server copy action. Automatic holder-to-server rehydration is blocked while direct mode is on; Verify & share remains an intentional, explicit server upload.
direct-stream.jssupports progressive preview for compatible fragmented H.264 MP4 through MediaSource. Ordinary faststart MP4, unsupported codecs, unavailable MediaSource and preview buffer exhaustion use the complete file. iPhone playback should be reviewed using that completed-file path. A preview is interactive and independent of the main player's host clock; main party playback follows the host after the verified copy is ready.
Paths that still use server media
Original YouTube acquisition, server uploads, intentional Verify & share intake, external VLC/M3U HTTP URLs and explicitly accepted server fallbacks still use the server. If no connected device advertises a local copy, normal original-source playback/download is preserved; this is not a transfer of another device's file. Profile/playlist shares, chapters, lyrics, presenter state and OBS state remain metadata/control only. Direct discovery requires an online paired room or an existing profile, and the browser must remain available to serve its OPFS file. No TURN service, deployment change or secret configuration is required.
Checks and reviewer follow-up
Run:
node --test frontend/*.test.js
node --check frontend/app.js
bun build --no-bundle server/server.js
bun test server/direct-relay.test.js server/p2p-hub.test.js server/remote.test.js server/party.test.js
npx playwright test -c playwright.direct.config.js
npx playwright test -c playwright.glass-navigation.config.js
The two-page localhost test uses the production relay and transport, separate browser stores, a real RTCDataChannel and actual OPFS writes. It resumes a 2 MB file at byte 12,345, verifies the resulting SHA-256 and asserts that the server log contains only invitation/acceptance/signalling/completion. A second case adds 12 seconds of resume preparation to prove it is outside the 10-second connection deadline. This is a local transport proof, not proof that arbitrary NATs will connect.
Check real iPhone foreground/background suspension, large-file quota and final promotion, iOS completed-file playback, restrictive/symmetric NAT fallback, paired screen playback, party host changes during a copy, protected-profile credential changes/reconnect, OS export gestures, and an actual fragmented MP4 MediaSource preview on desktop. Public STUN cannot overcome every NAT; declined invitations do not start media acquisition. Peer directories update when the presence socket connects and when local saves complete, rather than promising availability of offline/backgrounded devices.
Final verification on 2026-10-03: 123 frontend unit tests, 22 touched server tests (including remote and party), app syntax and server build, 8 Glass/Classic navigation browser tests at 390/1440 px, and both direct-transfer browser cases passed. The first navigation run lost its reused localhost server; a fresh-server rerun passed all eight cases without a code change. Classic screenshot comparisons remain within the test's rasterization tolerance. No real iPhone or restrictive NAT test was performed here, and no code was pushed or deployed.