Building a WebSocket Media Controller: Remote Control Your Mac from Android
How I built a real-time media controller using WebSockets to control my Mac's music playback from my Android phone. A story of engineering fun and practical problem-solving.
Building a WebSocket Media Controller: Remote Control Your Mac from Android 🎵
There’s something deeply satisfying about solving a problem that’s been nagging at you for weeks. For me, that problem was this: I’m listening to FLAC music on my Mac M1 MacBook streaming to Bluetooth earphones, but every time I want to skip a song or adjust the volume, I have to walk over to my laptop. Not anymore!
The Annoying Problem
Picture this: You’re comfortably settled in your favorite spot, music playing perfectly through your Bluetooth headphones from your Mac. But then… the current track is just not doing it for you. Do you:
- Get up and walk to your laptop to skip the song?
- Leave your comfortable spot and interrupt your flow?
I chose neither. Instead, I decided to build a solution that would let me control my Mac’s media playback directly from my Android phone using WebSockets.
Why WebSockets? (The Sexy Choice)
When brainstorming solutions, I considered several approaches:
- HTTP REST API: Simple but clunky for real-time control
- Bluetooth: Complex and potentially conflicting with audio streaming
- WebSockets: Real-time, bidirectional communication - the sexiest option!
WebSockets offered exactly what I needed: instant, bidirectional communication between my Android phone and Mac. No polling, no request/response delays - just pure real-time control.
The Tech Stack
For maximum fun and learning, I went with:
- Go for the Mac server (fast, simple, battle-tested)
- Kotlin for the Android client (modern Android development)
- AppleScript for media control integration (the bridge to system functions)
- Gorilla WebSocket library for Go (excellent WebSocket support)
The Mac Server Implementation
The heart of the solution is a Go server that listens for WebSocket connections and translates commands to AppleScript calls:
func handleMediaCommand(msg Message) map[string]interface{} {
var result = make(map[string]interface{})
result["action"] = msg.Action
switch msg.Action {
case "play", "pause":
// Both play and pause use the same key code
output, err := exec.Command("osascript", "-e", "tell application \"System Events\" to key code 49").CombinedOutput()
result["success"] = err == nil
result["output"] = string(output)
case "next":
output, err := exec.Command("osascript", "-e", "tell application \"System Events\" to key code 124 using {command down}").CombinedOutput()
result["success"] = err == nil
result["output"] = string(output)
case "previous":
output, err := exec.Command("osascript", "-e", "tell application \"System Events\" to key code 123 using {command down}").CombinedOutput()
result["success"] = err == nil
result["output"] = string(output)
case "volume_up":
output, err := exec.Command("osascript", "-e", "set volume output volume ((output volume of (get volume settings)) + 10)").CombinedOutput()
result["success"] = err == nil
result["output"] = string(output)
case "volume_down":
output, err := exec.Command("osascript", "-e", "set volume output volume ((output volume of (get volume settings)) - 10)").CombinedOutput()
result["success"] = err == nil
result["output"] = string(output)
default:
result["success"] = false
result["error"] = "Unknown action"
}
return result
}
The server handles WebSocket connections and routes commands to the appropriate AppleScript execution, providing real-time feedback to the client.
The Android Client
The Kotlin Android app provides a clean interface with intuitive media controls:
private fun sendCommand(action: String) {
if (!isConnected) {
Toast.makeText(this, "Not connected to server", Toast.LENGTH_SHORT).show()
return
}
val json = JSONObject()
json.put("action", action)
webSocket.send(json.toString())
}
The UI features large, touch-friendly buttons for play/pause, next/previous track, and volume control - perfect for quick interactions without looking at your screen.
Challenges and Solutions
Network Discovery
Problem: How does the Android app find the Mac on the network? Solution: Manual IP entry for now, but I’m planning to implement mDNS discovery for automatic detection.
Media Player Compatibility
Problem: Different media players might respond differently to AppleScript commands. Solution: The current implementation works with most standard media players that respond to system-level play/pause commands.
Connection Reliability
Problem: WiFi drops and sleep modes could interrupt control. Solution: Implemented proper connection state handling and reconnection logic in the Android client.
Performance and User Experience
The WebSocket approach delivers exceptional performance:
- Response times: Nearly instant (<50ms) command execution
- Battery impact: Minimal on both devices
- Reliability: Stable connections with proper error handling
Using the controller feels natural and responsive - exactly what I was looking for when I started this project.
Future Enhancements
This is just the beginning! I’m planning to add:
- Playlist control and track listing
- Current track display with album art
- Multiple device support
- Cross-platform clients (iOS, web)
- mDNS network discovery for automatic server detection
- Enhanced security with authentication
Lessons Learned
Building this project taught me several valuable lessons:
- WebSockets are perfect for real-time control - The instant response makes for a much better user experience than HTTP polling
- AppleScript integration is powerful - Being able to control system functions through simple commands opens up many possibilities
- Go’s simplicity is a strength - The server code is clean, fast, and easy to understand
- Mobile development requires attention to UX details - Large touch targets and clear feedback are essential for a good remote control experience
Try It Yourself
The code is available on my GitHub, and setting it up is straightforward:
-
Run the Go server on your Mac:
cd media-controller go run main.go
-
Build and install the Android app on your phone
-
Connect using your Mac’s IP address
-
Start controlling your music!
Conclusion
What started as a minor annoyance turned into an exciting engineering project that perfectly demonstrates the joy of building solutions to problems that matter to you. The WebSocket media controller is now an essential part of my daily workflow, and I’m excited to continue improving it.
Sometimes the best projects are the ones that solve your own problems - they come with built-in motivation and clear success criteria. This one definitely falls into that category!
Have you ever built something just because it annoyed you? I’d love to hear about your “scratch your own itch” projects in the comments below!