Push audio from any headless server to your phone. One HTTP POST, instant playback.
You're SSH'd into a server running TTS, generating music, or processing audio. You can't hear it -- it plays on the remote machine, or worse, there's no audio device at all.
ShellCast fixes that. curl -X POST your audio bytes to the relay, and they play on your phone instantly. No accounts, no cloud, no config files. One relay, one WebSocket, done.
Any Server ShellCast Relay Your Phone
┌────────────┐ POST /push ┌──────────────┐ WebSocket ┌──────────┐
│ TTS engine │───(audio bytes)─│ 111 lines │──broadcast──│ iOS app │
│ Music gen │ │ of Node.js │ │ plays it │
│ AI pipeline│ └──────────────┘ └──────────┘
└────────────┘
- Zero config --
npm install && node server.js. That's the setup. - Any audio format -- MP3, WAV, AAC, FLAC, OGG. Whatever the server generates, the phone plays.
- Files up to 50MB -- Full songs, not just notification clips.
- No cloud -- Runs on your LAN or Tailscale mesh. Audio never leaves your network.
- Metadata passthrough -- Voice name, text content, and IDs travel with the audio as HTTP headers.
git clone https://github.com/scrappylabsai/shellcast.git
cd shellcast/relay
npm install
node server.js
# ShellCast relay listening on :9876Requires XcodeGen and Xcode 16+.
cd ios
xcodegen generate
open ShellCast.xcodeproj
# Build and run on your deviceSet your relay URL in the app settings. Default: ws://localhost:9876/ws
# Push any audio file
curl -X POST http://localhost:9876/push \
-H "X-Voice: narrator" \
-H "X-Text: Hello from the server" \
--data-binary @speech.mp3
# Response: {"ok": true, "clients": 1, "bytes": 51672}From a script, TTS pipeline, or MCP tool:
# Generate TTS and push in one line
your-tts-engine "Hello world" -o /tmp/out.wav && \
curl -X POST http://localhost:9876/push --data-binary @/tmp/out.wav| Endpoint | Method | Description |
|---|---|---|
/push |
POST |
Push audio to all connected clients. Body = raw audio bytes. |
/status |
GET |
Returns {"clients": N, "uptime": seconds} |
/ws |
WebSocket | Client connection endpoint |
| Header | Description |
|---|---|
X-Voice |
Voice/speaker name (forwarded to clients as metadata) |
X-Text |
Text content that was spoken (for display) |
X-Id |
Unique ID for this audio clip |
Clients receive two messages per push:
- JSON metadata:
{"type": "meta", "voice": "narrator", "text": "Hello", "id": "..."} - Binary audio data: Raw bytes, same format as the POST body
The relay also sends periodic pings ({"type": "ping", "ts": ...}) every 15 seconds for keepalive.
- Auto-reconnect with exponential backoff -- survives network changes, server restarts
- Audio queue with sequential playback -- pushes stack up, play in order
- Background audio -- keeps playing when the app is backgrounded
- Tap-to-replay history of last 50 items
- Debug panel -- tap the bug icon for connection state and message log
- Configurable relay URL -- set once, persists across launches
- Any format -- if AVAudioPlayer can decode it, ShellCast plays it
cp relay/shellcast-relay.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now shellcast-relaySHELLCAST_PORT=8080 node server.jsThe relay is 111 lines of Node.js. One file, one dependency (ws), no framework.
relay/
├── server.js # HTTP server + WebSocket relay (111 lines)
├── package.json # One dependency: ws
└── shellcast-relay.service # systemd unit file
ios/
├── ShellCastApp.swift # App entry point
├── Services/ # WebSocket client, audio player
├── Views/ # SwiftUI views
├── Models/ # Data models
└── project.yml # XcodeGen spec
- No auth -- designed for private networks (LAN, Tailscale, VPN). If you need auth, put nginx in front.
- No persistence -- audio is forwarded and forgotten. The relay holds zero state between pushes.
- No transcoding -- whatever bytes go in, those bytes go out. The client handles decoding.
- No framework -- raw
http.createServerandws. Nothing to configure, nothing to break.
| Use Case | How |
|---|---|
| TTS playback | AI agent generates speech, pushes to your phone so you hear it anywhere |
| Music generation | Pipe output from AI music models for instant preview |
| Build notifications | Push a chime when your CI build finishes |
| Audio monitoring | Stream processed audio clips for review |
| Podcast editing | Push segments to your phone for on-the-go review |
| Fleet alerts | Server generates voice alerts, you hear them on your phone |
| Network | Relay URL |
|---|---|
| Same machine | ws://localhost:9876/ws |
| LAN | ws://10.0.0.5:9876/ws |
| Tailscale | ws://your-node.tailnet.ts.net:9876/ws |
Note: ShellCast uses ws:// (unencrypted WebSocket), not wss://. This is intentional -- it's designed for private networks. For Tailscale with MagicDNS hostnames on iOS, add an ATS exception domain in project.yml.
| Component | Description |
|---|---|
| ShellCast | This repo. Audio relay. |
| BrainJack Agent | WebSocket daemon for keystroke injection on Linux/macOS. |
| BrainJack HID | ESP32-S3 USB dongle. WiFi to USB HID. |
| ShellDrop FAP | Flipper Zero voice-to-keystroke app. |
| ShellDrop Bridge | ESP32-S2 WiFi bridge for Flipper Zero. |
PRs welcome. The relay should stay under 200 lines. The iOS app should stay simple. If a feature requires a database, it doesn't belong here.
- Fork the repo
- Make your changes
- Test with a real iOS device (simulators can't do background audio)
- Submit a PR
Built by ScrappyLabs | brainjack.ai