You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
Music/docs/superpowers/specs/2026-05-26-remote-mode-desi...

11 KiB

Remote Mode — Design Spec

Overview

Add a Host/Remote mode to the Music app so a MacBook can control playback on a Mac Mini over the local network. The remote sees the full library and controls playback, but audio plays on the host. The remote is read-only — no playlist or library modifications for v1.

Architecture

Two roles the app can operate in, one at a time:

  • Host: Runs a network server, advertises via Bonjour, serves its database, accepts playback commands, streams playback state.
  • Remote: Discovers hosts via Bonjour, downloads the host's database, sends playback commands, displays synced playback state.
┌──────────────────────┐         ┌──────────────────────┐
│      MAC MINI        │         │      MACBOOK         │
│      (Host)          │         │      (Remote)        │
│                      │         │                      │
│  Existing App        │         │  Existing App        │
│  ┌────────────────┐  │         │  ┌────────────────┐  │
│  │ AudioService   │  │◄────────│  │ RemoteClient   │  │
│  │ PlayerViewModel│  │ WebSocket│  │                │  │
│  │ DatabaseService│──┼────────►│  │ Local DB copy  │  │
│  └────────────────┘  │  HTTP   │  └────────────────┘  │
│  ┌────────────────┐  │         │                      │
│  │ HostServer     │  │         │  Reuses all existing │
│  │  - HTTP (DB)   │  │         │  ViewModels & Views  │
│  │  - WebSocket   │  │         │  for browsing        │
│  │  - Bonjour     │  │         │                      │
│  └────────────────┘  │         │                      │
└──────────────────────┘         └──────────────────────┘

Playback Abstraction

A PlaybackController protocol abstracts where playback happens:

  • LocalPlaybackController — wraps the existing AudioService + PlayerViewModel logic. This is what the app uses today.
  • RemotePlaybackController — sends commands over WebSocket to the host, receives state updates, and updates the PlayerViewModel accordingly.

Views and ViewModels call the same methods (play, pause, next, seek, etc.) regardless of which controller is active. The active controller decides whether that's local audio or a network command.

Host Server

New service: HostServer.

Bonjour Advertisement

  • Uses NWListener with service type _musicremote._tcp.
  • Service name is the computer's local name.
  • Automatically discoverable on the local network when hosting is enabled.

HTTP — Database Download

  • When a remote connects, its first request is GET /db.
  • The host reads db.sqlite from its Application Support directory and streams it as a binary response.
  • Typically a few MB, under a second on WiFi.

WebSocket — Command & State Channel

After the DB download, the remote establishes a WebSocket connection for bidirectional communication.

Remote → Host (commands):

Command Payload
play trackId, queueIds (array of track IDs)
pause
resume
next
previous
seek position (seconds)
setVolume level (0.0–1.0)
toggleShuffle
refreshDB

Host → Remote (events):

Event Payload
playbackState trackId, isPlaying, currentTime, duration, volume, isShuffled
dbReady — (sent after refreshDB, signals new DB is available for download)
error message (human-readable)

State Update Frequency

  • Immediate on discrete events: play, pause, track change, volume change, shuffle toggle.
  • Every ~1 second while playing for progress bar position.
  • The remote interpolates locally between updates for smooth scrubber movement.

Connection Limits

Single remote connection at a time for v1. A second connection attempt is rejected with a clear error.

Remote Client

New service: RemoteClient.

Discovery

  • Uses NWBrowser to scan for _musicremote._tcp services.
  • Presents discovered hosts by computer name in the connection sheet.
  • Resolves the selected endpoint to get IP/port.

Connection Flow

  1. Connect to the host's HTTP endpoint.
  2. Download db.sqlite, save to Application Support/Music/remote_db.sqlite.
  3. Open the downloaded DB with DatabaseService in read-only mode.
  4. Establish the WebSocket connection.
  5. App transitions to remote mode — existing ViewModels reload from the downloaded DB.

Command Forwarding

In remote mode, the RemotePlaybackController intercepts all playback calls and sends them as WebSocket commands instead of calling the local AudioService.

State Sync

The remote listens for playbackState messages and updates the PlayerViewModel:

  • Current track is looked up by ID from the local DB copy.
  • isPlaying, currentTime, duration, volume, isShuffled are set directly.
  • SwiftUI observation triggers UI updates automatically.

DB Refresh

A "Refresh Library" action sends refreshDB, the host signals dbReady, the remote re-downloads the DB and reloads the ViewModels.

Disconnection

On disconnect (user-initiated or connection drop), the app returns to local mode. The temporary remote DB file is deleted.

Message Protocol

JSON over WebSocket. Swift Codable enums for type safety.

// Remote → Host
{"type": "play", "payload": {"trackId": 42, "queueIds": [42, 43, 44, 45]}}
{"type": "pause"}
{"type": "resume"}
{"type": "next"}
{"type": "previous"}
{"type": "seek", "payload": {"position": 65.3}}
{"type": "setVolume", "payload": {"level": 0.75}}
{"type": "toggleShuffle"}
{"type": "refreshDB"}

// Host → Remote
{"type": "playbackState", "payload": {
  "trackId": 42,
  "isPlaying": true,
  "currentTime": 65.3,
  "duration": 210.0,
  "volume": 0.75,
  "isShuffled": false
}}
{"type": "dbReady"}
{"type": "error", "payload": {"message": "Track file not found"}}

Handshake

On WebSocket connect, host and client exchange a handshake message with app version. Version mismatches are caught early and logged.

Keep-Alive

WebSocket ping/pong at 5-second intervals. If 3 consecutive pings go unanswered, the connection is declared lost.

UI Changes

Menu Bar

  • "Enable Host Mode" — toggle menu item. Starts/stops the HostServer.
  • "Connect to Remote..." — opens the connection sheet.

Connection Sheet (Remote Side)

Modal sheet showing:

  • List of discovered Bonjour hosts (computer name + connectivity indicator).
  • "Connect" button for the selected host.
  • Progress indicator during DB download.
  • Error state with retry if connection fails.

Remote Mode Indicators

When connected as a remote:

  • A persistent banner/badge showing "Connected to [host name]" with a disconnect button.
  • Playlist creation/editing UI disabled (greyed out context menus, hidden "New Playlist").
  • "Open Music Folder..." menu item disabled.
  • "Refresh Library" action available (triggers DB re-download).

Host Mode Indicators

When hosting:

  • Status indicator showing "Hosting" or "Hosting · [remote name] connected".

Unchanged

Track table, player controls, search bar, home view, playlist bar — all work as-is against the local DB copy and the PlaybackController abstraction.

Observability

Structured Logging

os.Logger with subsystem com.music.remote and two categories: host and client. Logs are filterable in Console.app.

Level Examples
Info "Host started on port 8432", "Remote connected: Laurent's MacBook", "DB download complete (2.4 MB, 340ms)"
Debug Command send/receive, state update cycle, Bonjour browse events, connection lifecycle transitions
Error Connection refused, DB read failure, WebSocket decode failure, unexpected disconnect with reason

Connection State Machine

Every state transition is logged and drives user-visible status:

Disconnected → Discovering → Found Host → Downloading DB → Connecting WebSocket → Connected
     ↑                                                                                 │
     └──── Connection Lost ◄───────────────────────────────────────────────────────────┘

User-visible status messages: "Searching for hosts...", "Connecting to [name]...", "Downloading library...", "Connected to [name]", "Connection lost — Reconnect?"

Error Messages

Every error includes:

  • A clean, human-readable summary for the user (shown in UI).
  • The underlying NWError description in the log for debugging.

Examples: "Host refused connection", "Download timed out after 10s", "Network changed", "Host stopped hosting".

Diagnostics

  • Version handshake on connect catches protocol mismatches early.
  • WebSocket keep-alive detects stale connections within 15 seconds.
  • All incoming commands logged at debug level on the host for traceability.

Testing

Unit Tests

  • RemoteCommand / HostEvent Codable round-trip for every message type.
  • RemotePlaybackController sends correct WebSocket messages for each action.
  • Connection state machine: valid transitions succeed, invalid transitions are rejected.
  • HostServer command dispatch: incoming commands map to correct PlayerViewModel calls.

Integration Tests

  • Loopback connection: HostServer + RemoteClient in the same process over localhost — full flow from DB download through command/response round-trip.
  • DB download integrity: downloaded DB matches source schema and row counts.
  • State sync accuracy: play a track on host, verify remote receives correct playbackState values.

Real network connections in integration tests — no mocks for the networking layer.

Manual Test Scenarios

  • Happy path: enable host → connect remote → browse → play → verify audio on host, UI synced on remote.
  • Kill host app mid-playback → remote shows "Connection lost" cleanly.
  • Disconnect WiFi on remote → reconnect flow works.
  • Scan new folder on host while remote connected → remote can refresh and see new tracks.
  • Attempt playlist creation on remote → properly disabled.

Scope & Constraints

  • v1 only: Single remote, read-only, no authentication, local network only.
  • No changes to existing playback logic: The HostServer wraps PlayerViewModel and AudioService, it doesn't modify them.
  • No dependencies added: All networking uses Apple's Network.framework.
  • Existing UI untouched: Only additions are menu items, connection sheet, and status indicators.
  • Play counts track on the host: Since the host is playing the audio, play count increments happen on the host's database. The remote's local DB copy is a read-only snapshot and is not written to.