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-music-streaming-...

13 KiB

Music Streaming — Design Spec

Overview

Add internet-based music streaming to the Music app. A host serves its MP3 library as HLS streams over HTTPS. A client downloads the host's database for local browsing and plays audio by streaming from the host. Audio plays on the client, not the host.

This is distinct from the existing remote-mode design (LAN, audio on host). Here the client is an independent player that happens to source its audio and library from a remote host.

Architecture

┌──────────────────────────┐              ┌──────────────────────────┐
│       HOST (Mac)         │              │       CLIENT (Mac/iOS)   │
│                          │              │                          │
│  Existing App            │              │  Existing App            │
│  ┌────────────────────┐  │              │  ┌────────────────────┐  │
│  │ DatabaseService     │──┼── GET /db ──►  │ Local DB copy      │  │
│  │ (source of truth)  │  │              │  │ (read-only browse) │  │
│  └────────────────────┘  │              │  └────────────────────┘  │
│  ┌────────────────────┐  │              │  ┌────────────────────┐  │
│  │ StreamingServer     │  │◄── HLS ─────│  │ AVPlayer           │  │
│  │  - Hummingbird HTTP │  │  requests   │  │ (buffered playback)│  │
│  │  - HLS segmenter   │  │              │  └────────────────────┘  │
│  │  - WebSocket        │  │              │  ┌────────────────────┐  │
│  └────────────────────┘  │              │  │ WebSocket client   │  │
│  ┌────────────────────┐  │◄── cmds ────│  │ (RemoteCommand /   │  │
│  │ Cloudflare Tunnel  │  │── events ──►│  │  HostEvent)        │  │
│  │ (cloudflared)      │  │              │  └────────────────────┘  │
│  └────────────────────┘  │              │                          │
└──────────────────────────┘              └──────────────────────────┘
         │
         ▼
   https://music.yourdomain.com
   (Cloudflare edge)

Key Difference from Remote Mode

In remote mode, the client is a remote control — audio plays on the host. In streaming mode, the client is an independent player — it streams audio from the host and plays it locally. The host doesn't play anything when serving a streaming client.

MusicShared Swift Package

A local Swift package inside the repo, holding code shared between host and client (and later, an iOS target).

Contents:

  • RemoteProtocol.swift — moved from Music/Remote/. Contains RemoteCommand, HostEvent, PlaybackStatePayload, HandshakeMessage, RemoteProtocolVersion.
  • HLSManifestGenerator.swift — pure function: given track duration and segment size, produces .m3u8 playlist text. No I/O.
  • APIModels.swift — shared DTOs: AuthResponse (host info, protocol version), DBMetadata (version, checksum for conditional re-download).
  • Routes.swift — route path constants so host and client stay in sync.
  • StreamingConstants.swift — segment duration (6s), default port (8420), protocol version.

HTTP Endpoints

All endpoints require the Authorization: Bearer <api-key> header. Served by Hummingbird on the host.

Method Path Purpose
GET /auth Validate API key, return host name + protocol version
GET /db Download the full SQLite database file
GET /tracks/:id/stream.m3u8 HLS manifest for a track
GET /tracks/:id/segments/:index.mp3 Individual MP3 audio segment
GET /ws WebSocket upgrade for real-time command/event channel

No REST API for browsing — the client downloads the full database and queries it locally using existing DatabaseService code.

HLS Streaming

On-the-Fly Segmentation

When a client requests a track's manifest:

  1. Look up the track's file path in the database.
  2. Read MP3 duration from file metadata (cached after first read).
  3. Generate a .m3u8 playlist with N segments of 6 seconds each.

When a client requests a segment:

  1. Use AVAssetReader with a time range (CMTimeRange) for the requested segment (e.g., segment 2 = 12s–18s).
  2. AVAssetReader handles frame-boundary alignment and VBR files correctly.
  3. Return the extracted audio bytes.

This avoids raw byte slicing, which breaks on VBR files and frame-boundary misalignment.

Manifest Format

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:6.0,
segments/0.mp3
#EXTINF:6.0,
segments/1.mp3
#EXTINF:4.2,
segments/2.mp3
#EXT-X-ENDLIST

Design Decisions

  • MP3 byte-range slicing instead of transcoding to AAC. Avoids CPU overhead; AVPlayer handles MP3 segments without issues.
  • 6-second segments: HLS standard. Short enough for responsive seeking, long enough to avoid excessive HTTP requests for a single listener.
  • No adaptive bitrate: the source files are fixed-bitrate MP3s. No need for multiple quality renditions.

Cloudflare Tunnel

The host uses cloudflared to expose its local Hummingbird server to the internet.

Quick Tunnel (Development)

cloudflared tunnel --url http://localhost:8420
  • Zero config, no account required.
  • Generates a random https://xxx-yyy-zzz.trycloudflare.com URL.
  • URL changes on every restart — must be copied to the client each time.

One-time setup with a Cloudflare account and domain:

cloudflared tunnel create music
cloudflared tunnel route dns music music.yourdomain.com

Then the app launches:

cloudflared tunnel run --url http://localhost:8420 music
  • Stable URL: https://music.yourdomain.com.
  • Configure the client once, never touch it again.

App Integration

  • The host manages the cloudflared process as a child process (Process in Swift).
  • On host start: launch cloudflared, parse the tunnel URL from stdout.
  • On host stop: terminate the cloudflared process.
  • The host UI displays the current tunnel URL for the user to share with the client.
  • The app supports both modes: a toggle or setting to choose quick vs named tunnel.

Prerequisite

cloudflared must be installed separately (brew install cloudflared). The app checks for its presence on host startup and shows a clear error with install instructions if missing.

Authentication

Static API key for personal use.

  • The host generates a random API key on first setup (or the user sets one manually).
  • The key is stored in the host's Keychain.
  • The client stores the host URL + API key in its Keychain.
  • Every HTTP request and WebSocket upgrade includes Authorization: Bearer <api-key>.
  • Invalid keys receive HTTP 401. No retry, no session tokens, no expiry — a static secret over HTTPS is sufficient for single-user.

Client-Side Playback

Connection Flow

  1. User enters host URL + API key in the connection settings (one-time).
  2. Client calls GET /auth to validate credentials and check protocol version.
  3. Client calls GET /db to download the SQLite database, saved to Application Support/Music/streaming_db.sqlite.
  4. Client opens the DB with DatabaseService in read-only mode.
  5. Client establishes WebSocket connection to /ws.
  6. App transitions to streaming mode — existing views reload from the downloaded DB.

Playback

When the user picks a track:

  1. Client constructs the HLS URL: https://<host>/tracks/<id>/stream.m3u8.
  2. Creates an AVPlayer with an AVURLAsset, injecting the API key via a custom AVAssetResourceLoaderDelegate or URL request headers.
  3. AVPlayer fetches the manifest, then segments on demand. Buffering, seeking, and playback are handled natively.
  4. Client sends RemoteCommand over WebSocket to keep the host aware of what's playing (for state sync if multiple clients in the future).

AudioService Abstraction

The existing AudioService plays local files. For streaming, a parallel StreamingAudioService wraps AVPlayer with HLS URLs. Both conform to a shared protocol so PlayerViewModel works with either.

Database Refresh

Same as remote mode: send RemoteCommand.refreshDB over WebSocket → host signals HostEvent.dbReady → client re-downloads the DB and reloads views.

WebSocket Channel

Reuses the existing RemoteCommand / HostEvent protocol (JSON over WebSocket).

Client → Host

Command Purpose
play(trackId, queueIds) Inform host what the client is playing
pause Client paused
resume Client resumed
next / previous Client changed track
seek(position) Client seeked
setVolume(level) Client volume changed
toggleShuffle Client toggled shuffle
refreshDB Request fresh database

In streaming mode, these commands are informational (the client controls its own playback). They keep the host aware of client state for logging and potential future multi-client coordination.

Host → Client

Event Purpose
dbReady New database available for download
error(message) Server-side error (track file missing, etc.)

playbackState events are less critical in streaming mode since the client drives its own playback, but can be used for sync verification.

Handshake & Keep-Alive

  • On WebSocket connect: exchange HandshakeMessage with protocol version and app version.
  • Ping/pong every 5 seconds. Connection declared lost after 3 missed pings (15s).

UI Changes

Host Mode

  • "Start Streaming Server" menu toggle — starts Hummingbird + cloudflared.
  • Status indicator: "Streaming server running · https://music.yourdomain.com".
  • Settings panel: API key display/regenerate, tunnel mode (quick/named), named tunnel config.

Client Mode

  • Connection settings: host URL + API key fields, "Connect" / "Disconnect" button.
  • Status indicator: "Connected to [host]" or "Disconnected".
  • "Refresh Library" action to re-download the database.
  • All existing views (HomeView, TrackTableView, playlists, search, player controls) work unchanged against the local DB copy.
  • Playlist creation/editing disabled (read-only snapshot).

Mode Selection

A setting to choose the app's role: Local (default, current behavior), Host (serves library), or Client (streams from host). Persisted in UserDefaults.

Testing

Unit Tests

  • HLSManifestGenerator: correct .m3u8 output for various track durations, edge cases (very short tracks, exact multiples of segment duration).
  • RemoteCommand / HostEvent Codable round-trip (already partially covered in RemoteProtocolTests.swift).
  • API key validation logic.
  • Segment extraction: AVAssetReader produces valid audio for each segment time range, including edge cases (VBR files, last segment shorter than 6s).

Integration Tests

  • Loopback streaming: start Hummingbird server in-process, request manifest + segments over localhost, verify valid HLS output.
  • Database download: verify downloaded DB matches source schema and row counts.
  • Auth rejection: requests without or with wrong API key receive 401.
  • WebSocket handshake: version mismatch is caught and reported.

Manual Test Scenarios

  • Happy path: start host → connect client → browse library → play track → audio streams and plays on client.
  • Seek mid-track → playback resumes from correct position.
  • Network interruption → client buffers, resumes when connection returns.
  • Kill host mid-playback → client shows error cleanly.
  • Add tracks on host → client refreshes DB → new tracks appear.
  • Wrong API key → client shows auth error.
  • cloudflared not installed → host shows clear install instructions.

Scope & Constraints

  • Single client for v1. No concurrent listener handling.
  • Read-only client: no playlist or library modifications from the client.
  • MP3 only: HLS segmentation assumes MP3 source files (matches current library).
  • cloudflared required: not bundled, must be installed separately.
  • No offline mode: client requires active connection to stream. Downloaded DB enables browsing but not playback without the host.
  • No transcoding: segments served as raw MP3 byte ranges.
  • Hummingbird dependency: added via Swift Package Manager for the embedded HTTP server.