BuildWithMatija
Back to Builds
ToolActiveOpen source

StreamSpeaker

Use your Android TV as wireless speakers for your Mac. No AirPlay, no cloud — just UDP and a HAL driver.

  • Swift 6
  • SwiftUI
  • Core Audio (HAL Audio Server Plugin)
  • C (lock-free POSIX shared memory)
  • Kotlin
  • Ktor
  • Android AudioTrack
  • UDP / TCP
  • mDNS / Android NSD
  • SwiftPM
GitHub
Problem
Android TVs have decent speakers but no native way to receive audio from a Mac or phone. AirPlay requires Apple hardware on both ends. Bluetooth adds compression and range problems. There was no open, LAN-first solution that let macOS route system audio directly to a TV across a room.
Thesis
A custom Core Audio HAL Audio Server Plugin can intercept macOS system audio without touching any app, write it into a lock-free POSIX shared memory ring buffer, and a companion SwiftUI app can drain that buffer and send it over UDP fast enough to maintain a 120 ms jitter buffer on the receiver. The same wire protocol works from an Android phone using the AudioPlaybackCapture API.
Validation
The full end-to-end audio path is functional: Mac system audio (Spotify, browsers, VLC) routes through the HAL driver, through shared memory, across UDP, and plays out of the Android TV speakers. All three sender paths — macOS app, macOS CLI, and Android phone — are verified working. The project is open-source on GitHub.
Proof points
  • End-to-end verified: Mac system audio streams in real time to Android TV speakers
  • HAL driver loads as a custom Audio Server Plugin — virtual 'StreamSpeaker' output appears in System Settings → Sound → Output
  • All three sender paths functional: macOS SwiftUI app, macOS CLI, Android phone app
  • Custom ATV1 binary protocol: 32-byte fixed header, sequence numbers, timestamps, telemetry feedback
  • Open-source at github.com/matija2209/streamspeaker
Audience
  • Mac users with an Android TV who want to hear Mac audio through the TV without an Apple TV
  • Developers exploring Core Audio HAL plugin development or lock-free shared memory IPC
  • Anyone wanting system-wide, LAN-based audio routing with no cloud dependency

Executive summary

StreamSpeaker routes macOS system audio to an Android TV over a local network. It ships as three components: a custom Core Audio HAL driver that installs as an Audio Server Plugin, a SwiftUI app that reads from the driver via shared memory and sends over UDP, and a Kotlin Android TV receiver that buffers and plays incoming audio with AudioTrack. An Android phone sender (using AudioPlaybackCapture) is also functional. The whole audio path — from HAL driver callback through shared memory through UDP to TV speakers — is verified end-to-end. The codebase is open-source on GitHub.

The problem

Android TVs sit in living rooms with capable speakers but no input path from a Mac. AirPlay is locked to Apple hardware on both ends. Bluetooth introduces compression, range limits, and codec negotiation. Casting protocols require cloud infrastructure or proprietary receivers. A Mac playing music at a desk has no clean way to push that audio to a TV across the room without buying into a walled-garden ecosystem.

The missing piece was a LAN-first, protocol-level solution: intercept system audio at the OS level, ship it over UDP, buffer it on the TV, and play it.

The thesis

The Core Audio HAL Audio Server Plugin API lets a kernel-level audio device run inside coreaudiod without app entitlements or user-space workarounds. If the driver only writes audio frames to a lock-free POSIX shared memory ring buffer — no network I/O, no malloc, no locking in the real-time callback — it can safely intercept any macOS system audio output. A companion app drains that buffer and sends 20 ms PCM packets over UDP fast enough that a 120 ms jitter buffer on the receiver hides any network jitter on a home Wi-Fi network.

The secondary bet: the same binary wire protocol (ATV1) works from an Android phone using the AudioPlaybackCapture API, so the receiver doesn't need to know or care which sender is upstream.

What I built

Core Audio HAL driver

  • Custom Audio Server Plugin forked from Apple's NullAudio sample, implementing AudioServerPlugIn_Host callbacks.
  • Creates a virtual output device that appears in System Settings → Sound → Output as "StreamSpeaker".
  • Real-time-safe callback: writes Float32LE interleaved stereo to a POSIX shm_open ring buffer using C11 atomics — no malloc, no locks, no I/O.
  • Ring buffer capacity: 32,768 frames (~683 ms at 48 kHz), 266,240 bytes total shared memory.
  • Install/uninstall scripts with sudo and coreaudiod restart; prompts for reboot after install.

macOS SwiftUI app

  • NetServiceBrowser (Bonjour) discovers Android TV receivers advertising _streamspeaker._udp automatically.
  • Manual IP:port entry for when mDNS fails on the router.
  • SharedMemoryAudioSource C library (SharedAudioRing) bridges the POSIX ring buffer to Swift.
  • State machine: idle → connecting → streaming → stopping → error, with reconnect after TV restart.
  • Diagnostics tab: live event log (5,000-event cap), real-time receiver buffer depth, packet stats, ring buffer cursor positions.
  • Settings: transport mode (TCP/UDP), input source (sine wave for testing vs. shared memory for real audio), HAL driver install/uninstall via AppleScript-wrapped admin auth.

Android TV receiver

  • Kotlin foreground service accepting both TCP and UDP connections.
  • TreeMap-based jitter buffer targeting 120 ms depth with sequence-aware insertion and late-packet discard.
  • AudioTrack playback at 48 kHz, S16LE stereo.
  • Android NSD registration as StreamSpeaker: <Manufacturer> <Model>.
  • UDP telemetry back to sender every 250 ms: bufferMs, bufferFrames, underruns, overruns, packetsReceived, packetsDropped, packetsLate.
  • MediaSession for home screen "Now Playing" card; stealth mode (OLED-friendly black screen); optional boot auto-start.

Android phone sender

  • AudioPlaybackCapture (Android 10+, MediaProjection) captures system-wide playback audio.
  • MTU-safe UDP: 7 ms packets, 336 frames, 1,376 bytes per datagram — fits in one Ethernet frame.
  • Local speaker muted automatically during streaming; restored on disconnect.
  • NSD discovery of receivers; manual IP fallback.
  • Clean teardown: disconnect packet ×3, volume restore, resource release.

ATV1 wire protocol

Custom binary protocol with 32-byte fixed header:

FieldSizeNotes
magic4 bytesASCII ATV1
version1 byte1
packet_type1 bytehello / audio / telemetry / keepalive / disconnect / error
sequence8 bytesmonotonic counter
timestamp8 bytescumulative frame count at stream clock rate
payload_frames2 bytesaudio frames in packet
sample_format2 bytesS16LE or Float32LE
payload_size4 bytespayload bytes

Default ports: TCP 49152, UDP audio 49153, UDP telemetry 49154.

Architecture

code
macOS apps (Spotify, browsers, VLC, etc.)
  ↓
AndroidTVSpeaker.driver (Core Audio HAL, runs inside coreaudiod)
  ↓
POSIX shm ring buffer  (/com.macstreamer.audio-ring.v0, 266 KB, Float32LE)
  ↓
StreamSpeaker.app (SwiftUI, reads via SharedAudioRing C library)
  ↓
UDP audio port :49153  /  TCP fallback :49152
  ↓
ReceiverService (Kotlin foreground service, Android TV)
  ↓
TreeMap jitter buffer (120 ms target)
  ↓
AudioTrack (48 kHz, S16LE stereo) → TV speakers

← UDP telemetry :49154 (buffer depth, underruns, packet loss) ←

Android phone path replaces the top three layers with an AudioPlaybackCapture foreground service sending 7 ms UDP packets.

Roadmap

  1. Long-session soak testing — 1+ hour continuous streams with drift correction verified over multiple runs.
  2. Clock drift handling — Mac-side pacing adjustment (±8 ms) based on receiver telemetry to prevent buffer creep or underrun.
  3. Signing and notarization — Developer ID signing for the HAL driver bundle and app, Gatekeeper-compatible distribution.
  4. Installer package — .pkg with privileged helper, coreaudiod restart, and clean uninstaller.
  5. Sleep/wake resilience — HAL driver reload and app reconnect after macOS sleep/wake cycle.

Current status

Functional end-to-end, not yet distributed. The core audio path works: Mac system audio routes through the HAL driver, shared memory, and UDP to play out of Android TV speakers. All three sender paths are verified. Active work is on soak testing, clock drift correction, and the distribution/signing pipeline. No installer yet — building and deploying requires running the build-app.sh script and adb install manually.

Related services

  • Internal tools

Working through something similar?

If your company has a workflow, content system, or internal process that needs to become real software, this is the kind of work I can help with.

Get in touch

Related builds

You might also find these useful

ToolActiveOpen source

DropImg

A self-hosted, Docker-first image hosting tool — drag, drop, or paste to get instant public URLs, with built-in multi-user support, S3-compatible storage, and optional background removal.

  • React 19
  • Vite
  • Hono
  • Node.js
  • TypeScript
  • Tailwind CSS 4
  • SQLite
  • Drizzle ORM
  • Garage S3
  • Docker
  • Better Auth
  • Cloudflare
View buildGitHub
LabActiveClosed source

Sports Stream

A multi-camera live streaming stack that turns Android phones into SRT encoders and routes feeds through MediaMTX into OBS.

  • Kotlin
  • Android Camera2
  • RootEncoder
  • MediaMTX
  • Docker
  • SRT
  • RTSP
  • HLS
  • Node.js
  • Express
  • React
  • Vite
  • TypeScript
  • pnpm
  • GitHub Actions
View buildGitHub
Build with Matija logo

Build with Matija

Modern websites, content systems, and AI workflows built for long-term growth.

Services

  • Headless CMS Websites
  • Next.js & Headless CMS Advisory
  • AI Systems & Automation
  • Website & Content Audit

Resources

  • Case Studies
  • How I Work
  • Blog
  • CMS Hub
  • E-commerce Hub
  • Dashboard

Headless CMS

  • Payload CMS Developer
  • CMS Migration
  • Multi-Tenant CMS
  • Payload vs Sanity
  • Payload vs WordPress
  • Payload vs Contentful

Get in Touch

Ready to modernize your stack? Let's talk about what you're building.

Book a discovery callContact me →
© 2026Build with Matija•All rights reserved•Privacy Policy•Terms of Service
BuildWithMatija
Get In Touch