A web-based remote control for Roku devices with private listening (audio streaming) support. Control your Roku from any browser — phone, tablet, or desktop.
Live Demo: roku.bluefin605.com
Browser (Angular) → Proxy (C++ or ESP32) → Roku (ECP / RTP)
The Angular app can't talk to the Roku directly (no CORS), so a proxy sits in between. The proxy also handles SSDP discovery and private listening audio. You can run the proxy on your desktop or flash it onto an ESP32 for a dedicated always-on device.
RokuRemote/
├── app/ ← Angular web app
├── proxy/ ← C++ desktop proxy
├── esp32/ ← ESP32 firmware (same proxy for microcontroller)
├── mock/ ← Mock Roku server for testing
├── infra/ ← AWS CDK (S3 + CloudFront deployment)
├── aspire/ ← Aspire AppHost (local development orchestration)
└── docs/ ← Design docs and notes
Aspire runs the mock Roku, proxy, and Angular app in one command with a unified dashboard.
./setup-aspire.ps1 # first time: install deps, build proxy
./start-aspire.ps1 # start everythingOpen http://localhost:4200 and enter localhost as the Roku IP.
See local-development.md for the full setup guide including manual start, testing scenarios, and ESP32 details.
# Terminal 1: Mock Roku
node mock/roku-mock.mjs
# Terminal 2: Proxy
cd proxy && cmake -B build && cmake --build build && ./build/roku-proxy --port 8080
# Terminal 3: Angular app
cd app && npm ci && ng serveRequires CMake 3.20+ and a C++17 compiler.
cd proxy
cmake -B build
cmake --build build
./build/roku-proxy --port 8080The proxy exposes these endpoints:
| Endpoint | Method | Description |
|---|---|---|
/roku/<path>?ip=<roku-ip> |
GET/POST | Forward to Roku ECP with CORS |
/discover |
GET | SSDP discovery of Roku devices |
/start?roku=<ip> |
POST | Start private listening |
/stop |
POST | Stop private listening |
/status |
GET | Session state |
/audio |
GET | Streaming Opus audio (chunked) |
The ESP32 firmware runs the same proxy on a microcontroller — plug it in, connect to Wi-Fi, and it's always available. It advertises itself as roku-proxy.local via mDNS and can also be reached as roku-proxy on routers that register DHCP hostnames.
Option A: Pre-built firmware (no toolchain needed)
Download the roku-proxy-esp32 artifact from the latest Actions build, then flash:
pip install esptool # if not already installed
esptool.py --chip esp32s3 -p /dev/tty.usbserial-0001 write_flash \
0x0 bootloader.bin \
0x8000 partition-table.bin \
0x10000 roku-proxy-esp32.binOption B: Build locally with ESP-IDF
Requires ESP-IDF v5.x.
cd esp32
idf.py set-target esp32s3
idf.py build
idf.py -p /dev/tty.usbserial-0001 flashOn first boot the ESP32 prompts for Wi-Fi credentials over the USB serial connection. Open a serial monitor:
screen /dev/tty.usbserial-0001 115200You'll see:
=================================
Roku Proxy — Wi-Fi Setup
=================================
Wi-Fi SSID: MyNetwork
Wi-Fi Password: MyPassword
Type your SSID and password. They're saved to flash and persist across reboots — you only need to do this once.
To re-enter credentials, erase the saved config and reboot:
esptool.py --chip esp32s3 -p /dev/tty.usbserial-0001 erase_region 0x9000 0x6000Then open the serial monitor again and the ESP32 will re-prompt.
Once connected, the serial output shows:
Local IP: 192.168.1.x
mDNS hostname: roku-proxy.local
Roku proxy ready on http://roku-proxy.local:80
Open the hosted web app or http://localhost:4200 on your phone.
Recommended proxy URL order:
http://roku-proxy/http://roku-proxy.local/http://<device-ip>/
If you have a pre-built firmware (e.g., from GitHub Actions), you can flash it directly with esptool without needing ESP-IDF:
esptool --chip esp32s3 --port COM3 --baud 460800 write_flash -z \
0x0 bootloader/bootloader.bin \
0x8000 partition_table/partition-table.bin \
0x10000 roku-proxy-esp32.binReplace COM3 with your serial port (/dev/ttyUSB0 on Linux, /dev/tty.usbserial-* on macOS).
You can host firmware files on your existing S3 + CloudFront deployment and flash directly from a URL.
- Upload firmware files (same folder structure) to S3:
aws s3 cp bootloader/bootloader.bin s3://<bucket>/firmware/latest/bootloader/bootloader.bin
aws s3 cp partition_table/partition-table.bin s3://<bucket>/firmware/latest/partition_table/partition-table.bin
aws s3 cp roku-proxy-esp32.bin s3://<bucket>/firmware/latest/roku-proxy-esp32.bin- Invalidate CloudFront so clients get the latest files:
aws cloudfront create-invalidation --distribution-id <distribution-id> --paths "/firmware/latest/*"- Flash directly from your domain URL:
./esp32-tool.ps1 flash-url -Port COM4 -FirmwareUrlBase https://roku.yourdomain.com/firmware/latestOr flash and then open serial monitor:
./esp32-tool.ps1 flash-monitor-url -Port COM4 -FirmwareUrlBase https://roku.yourdomain.com/firmware/latestThis avoids sharing local files and gives everyone a single stable download URL.
Note: You can find published ESP32 firmware images and version listings at https://roku.bluefin605.com/versions.
The helper script supports both ESP32 and ESP32-S3 style layouts:
# List currently available COM ports
./esp32-tool.ps1 ports
# Flash classic ESP32 boards
./esp32-tool.ps1 flash -Chip esp32 -Port COM3
# Flash ESP32-S3 boards
./esp32-tool.ps1 flash -Chip esp32s3 -Port COM4If you host separate board builds in subfolders (for example firmware/latest/esp32 and firmware/latest/esp32s3), use:
./esp32-tool.ps1 flash-url -Chip esp32 -FirmwareFlavor esp32 -Port COM3 -FirmwareUrlBase https://roku.yourdomain.com/firmware/latest
./esp32-tool.ps1 flash-url -Chip esp32s3 -FirmwareFlavor esp32s3 -Port COM4 -FirmwareUrlBase https://roku.yourdomain.com/firmware/latestFor uncommon board layouts, you can override image names and offsets directly:
./esp32-tool.ps1 flash -Chip esp32 -Port COM3 `
-BootloaderRelativePath "myboot/bootloader.bin" `
-PartitionRelativePath "myboot/partitions.bin" `
-AppRelativePath "myboot/app.bin" `
-BootloaderOffset 0x1000 -PartitionOffset 0x8000 -AppOffset 0x10000Use these when Wi-Fi credentials are wrong or when you want to start from a clean device.
# Erase full flash (firmware + credentials + settings)
./esp32-tool.ps1 full-reset -Port COM5
# Non-interactive full erase
./esp32-tool.ps1 full-reset -Port COM5 -ForceNotes:
full-reseterases everything. Reflash firmware afterwards.- If
-Chipis not specified and the default chip does not match, the script auto-fallbacks to the other chip (esp32<->esp32s3). - For Wi-Fi only reset (without erasing firmware), hold the BOOT button during startup for ~2 seconds to clear saved credentials from NVS, then reboot.
| Problem | Fix |
|---|---|
| No serial prompt after flashing | Press the EN/Reset button on the board |
| Board not detected | Check USB cable (some are charge-only) and install serial driver |
| Wi-Fi won't connect | ESP32 only supports 2.4 GHz Wi-Fi, not 5 GHz |
roku-proxy not resolving |
Try http://roku-proxy.local/, then the IP address shown in serial output |
The web app is hosted on AWS (S3 + CloudFront). Infrastructure is managed with CDK (C#).
Copy config.example.json to config.json and fill in your values:
{
"rokuremote": {
"prefix": "rokuremote",
"region": "ap-southeast-2",
"environment": "production",
"domain": "roku.yourdomain.com",
"certificateArnUsEast1": "arn:aws:acm:us-east-1:..."
}
}domain and certificateArnUsEast1 are optional — without them, CloudFront serves on its default *.cloudfront.net domain.
From the infra/ directory:
cdk bootstrap # first time only
cdk diff --context configFile=../config.json # preview changes
cdk deploy --context configFile=../config.json # deploy to AWSSee infra/README.md for full CDK details.