154 lines
10 KiB
Markdown
154 lines
10 KiB
Markdown
# 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.js` strips unexpected fields and validates file claims and
|
|
bounded signalling; `direct-relay.js` binds 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/p2p` now 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.js` uses public STUN, ordered 64 KiB DataChannel chunks and
|
|
bufferedAmount backpressure. `direct-recv-worker.js` writes 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.js` supports 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:
|
|
|
|
```sh
|
|
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.
|