A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
README.md

Rocksky Cast receiver #

A custom Google Cast TV receiver built with Vite, React and Tailwind, using the Android app's Rockford Sans fonts. The Android sender uses application ID 833D8703 (override with GOOGLE_CAST_RECEIVER_APP_ID at build time).

Preview #

From the repository root, install dependencies with bun install, then:

cd apps/cast
bun run storybook

Open http://localhost:6008. Stories cover uploaded/local playback, pause, buffering, idle, missing/broken artwork, long metadata, empty queue, errors and 720p screens. Playback controls in stories update the mock state; they do not play audio.

For the standalone preview:

bun run dev

Open http://localhost:5178/?preview (or ?preview=idle). Without the preview query the page starts the Google Cast receiver runtime and expects a Chromecast.

Production setup #

  1. Run bun run build in this directory. Host the contents of dist/ on an HTTPS URL accessible to your Chromecast. Keep the Google-hosted Cast framework script in index.html.
  2. In the Google Cast SDK Developer Console, set application 833D8703 to use this receiver URL. Register test Chromecast devices while the application is unpublished; publish the receiver registration for general use.
  3. Build a new Android native app (expo prebuild / EAS); Expo Go and previously installed binaries do not contain the Cast SDK or local-file server.
  4. Select This Device in Rocksky's source picker. Only this selection exposes the Chromecast option. Connect to your receiver; the phone's current mixed local/uploaded queue transfers to Cast. Existing playback and notification controls then control that session. Stop casting returns to the phone with playback paused.

Local files are served from the phone's Wi-Fi/Ethernet IPv4 address using unguessable per-file URLs, range requests and CORS. Keep the phone and Chromecast on the same network without client isolation. A foreground service holds wake/Wi-Fi locks while local files are shared and releases them when the Cast session ends. Force-stopping the app or removing network connectivity interrupts local streaming. No directory is exposed.

Uploaded files use scoped opaque stream tokens, never the user's sign-in JWT. Deploy the accompanying API change to enable the purpose=cast 24-hour token lifetime (older APIs retain their one-hour lifetime). URLs for other sources must also be directly accessible from the Chromecast.

Queue startup resolves and loads only the selected track. The sender adds the next track first, then fills the remaining queue in batches of at most five, with a 250 ms pause between batches. Earlier tracks are inserted before the selected item to preserve queue order. Replacing, skipping, stopping, or disconnecting cancels stale preparation; transport controls do not wait for background metadata requests. Token and missing-MIME HTTP requests share a single request lane, spaced at least one second apart, and respect Retry-After or the API's X-RateLimit-Reset TTL on HTTP 429. Known MIME types and cached tokens need no per-track HTTP lookup.

The receiver does not transcode. Playback depends on the target device's supported audio codecs. Unsupported formats may need conversion. Receiver hosting/registration and playback on real Cast hardware still require verification; the repository checks cover the native server, sender queue and receiver build/state parsing.

Web sender #

In the web app, play an uploaded/library track on This Device, then choose Chromecast from the player's source picker. The option appears when the browser's Cast SDK discovers a receiver and This Device is selected. Existing miniplayer, fullscreen, keyboard, queue, and media-session controls target the selected TV. Choose Stop casting to return to paused local playback.

The web sender uses receiver 833D8703 by default (VITE_GOOGLE_CAST_RECEIVER_APP_ID overrides it). Serve the app over HTTPS, or localhost for development, in a browser supported by the Google Cast Web Sender SDK. It loads the selected track first and fills the remaining queue in small background batches. Its token and MIME requests share the Android sender's request-pacing implementation in apps/shared/castRequests.ts. Uploaded stream URLs carry scoped streaming tokens; they never fall back to the account sign-in JWT.

Sender regression tests: bun test ./src/lib/audio/cast-player.test.ts from apps/web.

Checks #

bun run test
bun run build
bun run build-storybook

The Android module has JUnit HTTP/range tests (:rocksky-cast:testDebugUnitTest). The app's sender and request-pacing tests run with bun test ./src/lib/castPlayback.test.ts ./src/lib/castRequests.test.ts from apps/app.

Preview artwork #

Storybook uses real metadata and album covers from the Apple Music catalog, stored locally so previews work without external image requests. Artwork belongs to its respective rights holders; these fixtures are for UI previews, not bundled audio:

No-artwork and failed-image states use a CSS gradient/vinyl illustration.

Rocksky remote player #

Signed-in web and Android senders authorize the TV over urn:x-cast:app.rocksky.remote. The receiver registers its own RemotePlayer connection to wss://api.rocksky.app/ws, using the Chromecast friendly name (for example, Salon TV). Other Rocksky clients can select that device and control play/pause, next/previous, seek, volume, repeat, queue jump/remove/reorder, and enqueue uploaded tracks. Uploaded enqueue starts the selected track first and prepares the remainder with paced requests in the background. Local files already in the Cast queue can be controlled remotely; adding new local files still requires the phone's file server. Navidrome-only IDs cannot be enqueued by the receiver without upload IDs.

The account token is held only in receiver memory. It is never included in media metadata, stream URLs, published queue data, or browser storage. Signing out in the authorizing sender disconnects the remote registration while that sender is connected. Closing a sender leaves the TV's remote connection alive until the receiver exits; local streaming still requires the phone to remain reachable. Stream URLs use the existing scoped upload-stream token. The SDK provides heartbeat and reconnection.

After deploying receiver and web changes, restart the Cast session. Web must be built using bun run build:prod. Android needs a build containing the new sender handshake; once the receiver acknowledges registration, the phone stops advertising that same playback under its own device name. Receiver build/test scripts build the SDK workspace dependency first so a clean checkout works.