Pairing and Handoff between Devices #50

Closed
opened 2026-10-12 22:19:01 +02:00 by SBDeveloper · 1 comment
Owner

Part of #49

Phase 1 of Handoff. Decisions in ADR 0009; terms in GLOSSARY.md (Device, Pairing, Handoff, Output).

  • Device identity (id, name, key pair) persisted in the data dir
  • Discovery of other Devices on the local network via mDNS (Android: acquire a multicast lock from Kotlin)
  • Pairing: one Device shows a QR code that the phone scans with its camera; a short code to type as fallback. List of Paired Devices with unpair
  • Connection between Paired Devices encrypted and authenticated with the paired keys; live status of the other Device ("Playing on Phone")
  • Handoff in both directions ("Play on …" from the handing Device, "Continue here" from the receiving one): the receiving Device resolves and buffers the current Track, reports ready; the handing Device then pauses and sends its final position; the receiving Device starts there. No ready within a timeout: abandon, handing Device keeps playing
  • Track resolution on the receiving Device: same Source + Track id (Source installed and logged in) → same ISRC → similar title/artist/duration (used automatically, listed in a notice afterwards). Unresolvable entries are skipped. Independent of the Track matching setting; nothing is grouped into a Recording
  • The receiving Device's Queue is replaced (shuffle, repeat and context carry over)
  • The handing Device closes its audio stream (not just pauses), so a multipoint headset can switch
  • Integration tests through Core, with two Cores talking in one test
Part of #49 Phase 1 of Handoff. Decisions in ADR 0009; terms in `GLOSSARY.md` (Device, Pairing, Handoff, Output). - Device identity (id, name, key pair) persisted in the data dir - Discovery of other Devices on the local network via mDNS (Android: acquire a multicast lock from Kotlin) - Pairing: one Device shows a QR code that the phone scans with its camera; a short code to type as fallback. List of Paired Devices with unpair - Connection between Paired Devices encrypted and authenticated with the paired keys; live status of the other Device ("Playing on Phone") - Handoff in both directions ("Play on …" from the handing Device, "Continue here" from the receiving one): the receiving Device resolves and buffers the current Track, reports ready; the handing Device then pauses and sends its final position; the receiving Device starts there. No ready within a timeout: abandon, handing Device keeps playing - Track resolution on the receiving Device: same Source + Track id (Source installed and logged in) → same ISRC → similar title/artist/duration (used automatically, listed in a notice afterwards). Unresolvable entries are skipped. Independent of the Track matching setting; nothing is grouped into a Recording - The receiving Device's Queue is replaced (shuffle, repeat and context carry over) - The handing Device closes its audio stream (not just pauses), so a multipoint headset can switch - Integration tests through `Core`, with two Cores talking in one test
Author
Owner

Done (uncommitted at time of writing; the maintainer commits). Verified on a Linux PC and a Samsung phone on Android 17.

Built

  • Device identity (id, editable name: hostname / phone model) in device.json, private key readable by the user only.
  • Pairing: QR invite (one-time token, 5 min) and a 6-digit code (SPAKE2, proofs bound to both TLS certificates, 3 attempts). TLS 1.3 with self-signed certificates pinned by fingerprint.
  • Discovery over mDNS (_medley._tcp), only on the local network's interface; fixed port 47530 with fallback.
  • Live connections between Paired Devices with status push; unpair closes them and later ones are refused.
  • Handoff both ways (hand_off_to, take_over_from): offer → ready → commit → started, abandoned on timeout. Queue (order, shuffle, repeat, context) and position carry over; receiving Queue replaced. Resolution: same Source + id (catalog Sources: one login check per Source), ISRC, similar title/artist/duration (listed in a notice), else skipped.
  • Output lets go of the device after 5 s idle, so a multipoint headset can switch.
  • UI: Devices button in the player bar, Settings → Devices (name, paired list, QR + code, nearby devices), notice after a Handoff.
  • Android: multicast lock, ACCESS_LOCAL_NETWORK (Android 17 blocks the LAN without it, asked when Devices is first opened), QR scanning via Google's code scanner, phone model as name.

Tests: crates/core/tests/devices.rs (27 + 1 manual real-mDNS test), crates/core/tests/output.rs, ui/src/lib/devices.test.ts.

Left out / follow-ups

  • Finding a handed-off Track in a catalog by ISRC (a local file the phone lacks is skipped even if Deezer has it): new issue.
  • Deezer resolution assumes Track ids are the same on every Device.
  • Not yet confirmed: the headset switching back to the transmitter by itself after handing back to the PC.

Decisions in ADR 0009; terms in GLOSSARY.md (Device, Pairing, Handoff, Output).

Done (uncommitted at time of writing; the maintainer commits). Verified on a Linux PC and a Samsung phone on Android 17. **Built** - Device identity (id, editable name: hostname / phone model) in `device.json`, private key readable by the user only. - Pairing: QR invite (one-time token, 5 min) and a 6-digit code (SPAKE2, proofs bound to both TLS certificates, 3 attempts). TLS 1.3 with self-signed certificates pinned by fingerprint. - Discovery over mDNS (`_medley._tcp`), only on the local network's interface; fixed port 47530 with fallback. - Live connections between Paired Devices with status push; unpair closes them and later ones are refused. - Handoff both ways (`hand_off_to`, `take_over_from`): offer → ready → commit → started, abandoned on timeout. Queue (order, shuffle, repeat, context) and position carry over; receiving Queue replaced. Resolution: same Source + id (catalog Sources: one login check per Source), ISRC, similar title/artist/duration (listed in a notice), else skipped. - Output lets go of the device after 5 s idle, so a multipoint headset can switch. - UI: Devices button in the player bar, Settings → Devices (name, paired list, QR + code, nearby devices), notice after a Handoff. - Android: multicast lock, `ACCESS_LOCAL_NETWORK` (Android 17 blocks the LAN without it, asked when Devices is first opened), QR scanning via Google's code scanner, phone model as name. **Tests**: `crates/core/tests/devices.rs` (27 + 1 manual real-mDNS test), `crates/core/tests/output.rs`, `ui/src/lib/devices.test.ts`. **Left out / follow-ups** - Finding a handed-off Track in a catalog by ISRC (a local file the phone lacks is skipped even if Deezer has it): new issue. - Deezer resolution assumes Track ids are the same on every Device. - Not yet confirmed: the headset switching back to the transmitter by itself after handing back to the PC. Decisions in ADR 0009; terms in GLOSSARY.md (Device, Pairing, Handoff, Output).
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
SBDeveloper/medley#50
No description provided.