A show-owner developer tool for Falcon Player (FPP). Runs on the master Pi and gives you full control: start/stop sequences and playlists, and hear synchronized show audio on your phone — all from one page.
Tested on FPP 9.4 (Raspberry Pi OS Bookworm). Should work on any FPP version that uses
/opt/fpp/www/as its web root (FPP 6+).
This is NOT the visitor-facing listener. For an open WiFi hotspot that lets your audience listen along while keeping connected deviices isoolated and blocking FPP from intrusion, see fpp-listener-sync (runs on a remote). Eavesdrop runs on the master with direct API access.
- Serves a control page where the show owner can start/stop playlists and sequences
- Syncs show audio to the owner's phone using an adaptive PLL with WebSocket transport
- Registers as an FPP plugin with a header icon and footer button for quick access
- Auto-detects when sequences start from any source (FPP web UI, scheduler, API) and begins playing
- Optional WiFi access point (wlan1) — I developed this to run a couple of WLED bulbs without needing a separate AP. I don't recommend it for shows that transmit a lot of pixel data or use remotes
- Accessible from your show network — just click the navigate to
http://YOUR_FPP_IP/listen/or the new button/icon.
If this tool saved you time or made your show better, consider buying me a coffee or donate for me to get more tokkens:
This must be installed on your master FPP (the one in player mode), not on a remote. It needs:
- Direct access to the music files in
/home/fpp/media/music/ - The FPP API at
127.0.0.1to read playback status and start/stop playlists - Remotes don't have to store media locally — they only receive channel data from the master. Typically, there is no media to play on a remote.
- No seperate USB wifi adapter is needed
If your master controls the show, that's where this goes.
Before you start, make sure you have:
- A Raspberry Pi running Falcon Player (FPP) 9.x in player (master) mode
- Your FPP already has sequences (.fseq files) and matching audio files (.mp3) loaded
- A computer on the same network as your FPP to run the install commands
You need to get a command line on your FPP. Pick one of these methods:
Option A — SSH from your computer:
-
Windows: Open PowerShell or Command Prompt and type:
ssh fpp@YOUR_FPP_IPReplace
YOUR_FPP_IPwith your FPP's IP address (for example10.1.66.204). When it asks for a password, typefalconand press Enter. -
Mac/Linux: Open Terminal and type the same command above.
Option B — Use the FPP web interface:
- Open your browser and go to
http://YOUR_FPP_IP/ - Click on Content Setup in the menu
- Click File Manager
- (This method only works for uploading files — you'll still need SSH for the install command)
Once you're logged in via SSH, type these commands one at a time (press Enter after each one):
cd /home/fppgit clone https://github.com/UndocEng/fpp-eavesdrop.gitcd fpp-eavesdropsudo ./install.shYou should see output like this:
=========================================
FPP Eavesdrop - v3.4
=========================================
[install] Web root: /opt/fpp/www
[install] Deploying web files...
[install] Web files deployed
[install] Created /music symlink
[install] Default AP config created (IP: 192.168.50.1)
[install] Default WiFi password: Listen123
[install] Sudoers configured
[install] listener-ap service installed
[install] python3-websockets already installed
[install] ws-sync-server.py deployed
[install] ws-sync service installed and started
[install] Apache listener config deployed
[install] Apache restarted
[install] Plugin registered
[install] Footer button added
[install] Running self-tests...
[test] ws-sync service: OK
[test] ws-sync port 8080: OK (HTTP 426 = WebSocket expected)
=========================================
Install complete!
=========================================
Page: http://YOUR_FPP_IP/listen/
Sync: WebSocket (ws://YOUR_FPP_IP/ws)
WiFi: SHOW_AUDIO (WPA2)
AP IP: 192.168.50.1
Pass: Listen123 (change via web UI)
=========================================
- On your phone (connected to the same network as your FPP), open the browser
- Go to
http://YOUR_FPP_IP/listen/ - You should see the Show Audio page with a Playback section at the top
- Select a sequence from the dropdown and tap Start — your lights and audio should begin
- Tap anywhere on the page to unlock audio (required by mobile browsers on first visit)
That's it! You're done.
- Open the listen page at
http://YOUR_FPP_IP/listen/ - Pick a playlist or sequence from the dropdown
- Tap Start
- Audio will play through the phone speaker and lights will run on your display
The page automatically detects when a sequence is playing — even if started from FPP's web UI, the scheduler, or any other source. Just keep the page open and it will start syncing as soon as something plays.
Tap the Stop button.
Tap the Debug checkbox at the bottom of the sync card to see live diagnostics: transport type, clock offset, PLL state, error history, and playback rate. The Client Log checkbox shows a running log of sync events.
- On iPhone, check that the ringer switch (on the side of the phone) is not on silent
- Turn up the volume on the phone
- Tap anywhere on the page — mobile browsers require a user gesture before they'll play audio
- Check that you have
.mp3files in/home/fpp/media/music/with the same name as your.fseqfiles (e.g.Elvis.fseqneedsElvis.mp3)
This is an FPP output configuration issue, not a listener issue. Check:
- Go to your FPP web interface (
http://YOUR_FPP_IP/) - Click Input/Output Setup > Channel Outputs
- Make sure your output universes are active (checkbox enabled)
- Make sure the output IP addresses are correct (not your FPP's own IP)
- Enable the Debug checkbox and watch the PLL converge — it takes ~12-14 seconds after a track starts
- If error stays large, check that your Pi has the correct time (FPP usually handles this automatically)
- The Avg Error (2s) field should hover near 0 once locked — typical steady-state is 5-25ms
- Check the ws-sync service:
sudo systemctl status ws-sync - View logs:
journalctl -u ws-sync -f - Test the port:
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/— should return426 - The page will automatically fall back to HTTP polling if WebSocket is unavailable
To get the latest version:
cd /home/fpp/fpp-eavesdrop
git pull
sudo ./install.shTo completely remove everything this project installed:
cd /home/fpp/fpp-eavesdrop
sudo ./uninstall.shThis removes:
- All web files from
/opt/fpp/www/listen/ - The
/musicsymlink - The
ws-syncWebSocket service (stopped and disabled) - The
listener-apWiFi AP service (stopped, hostapd/dnsmasq killed) - The Apache WebSocket proxy configuration
- The sudoers entry for WiFi management
- The config directory at
/home/fpp/listen-sync/(AP config, hostapd config, scripts) - The FPP plugin registration and header icon
- The Eavesdrop footer button from
custom.js - Network routing rules (nftables, policy routes) added by the AP
After uninstalling, your FPP is exactly as it was before. You can then delete the project folder:
rm -rf /home/fpp/fpp-eavesdrop| File | What it does |
|---|---|
www/listen/listen.html |
Main page — playback controls, audio sync, WiFi AP settings, debug UI |
www/listen/index.html |
Redirects to listen.html |
www/listen/status.php |
Returns current FPP playback status as JSON |
www/listen/admin.php |
Handles start/stop commands, WiFi AP config (SSID, password, IP), connected clients |
www/listen/version.php |
Returns version info |
www/listen/logo.png |
Undocumented Engineer logo |
server/ws-sync-server.py |
Python WebSocket server — bridges FPP status to clients at 100ms |
server/listener-ap.sh |
Brings up WPA2 access point on wlan1 with hostapd, dnsmasq, and nftables routing |
server/listener-ap.service |
Systemd service for the WiFi access point |
config/ws-sync.service |
Systemd service for the WebSocket server |
config/apache-listener.conf |
Apache config — proxies /ws to the WebSocket server |
config/ap.conf |
Default AP IP/netmask template (deployed if not present on Pi) |
api.php |
FPP plugin API — header indicator icon linking to the listen page |
pluginInfo.json |
FPP plugin metadata for plugin manager registration |
install.sh |
Installs everything (web files, services, AP, plugin, sudoers) |
uninstall.sh |
Removes everything (restores FPP to original state) |
Eavesdrop uses an adaptive Phase-Locked Loop (PLL) to keep the phone's audio in sync with FPP's sequence playback. Instead of repeatedly jumping to the correct position (which causes audible pops), it smoothly adjusts the playback speed to converge on FPP's position and stay locked.
The WebSocket server (ws-sync-server.py) polls FPP's API every 100ms and broadcasts state to all connected clients. The browser connects via WebSocket (proxied through Apache on port 80 at /ws), with automatic HTTP polling fallback if WebSocket is unavailable.
NTP-style clock offset estimation uses ping/pong round-trips through the WebSocket, with a median filter + EWMA for stable offset calculation.
The sync engine runs through three phases:
1. Start (first message after a track begins)
- Preloads the audio file and waits for metadata
- Seeks to FPP's current position
- Starts playback, enters 1.5-second settle period
2. Calibrate (~800ms minimum, 6+ samples)
- Collects
{local_time, fpp_position}pairs - Computes a least-squares linear regression to find the rate ratio between FPP's clock and the phone's clock
- Clamps the base rate to +/-1% (rejects garbage calibration)
3. Locked (ongoing)
- Computes phase error:
fpp_position - audio.currentTime - Maintains a 2-second rolling average (avg2s) as PLL input — prevents oscillation from instantaneous noise
- Adaptive gain:
Kp = 0.01 * (1 + 4 * min(|avg2s|/200, 1))— gentle when close (0.01), aggressive when far (0.05) - Log-compressed correction:
rate = baseRate + sign(avg2s) * Kp * log1p((|avg2s| - deadZone) / 100) - Dead zone: no correction when error < 5ms
- Rate learning: EMA (alpha=0.05) from 2-second observation windows, so corrections shrink as the true clock relationship is learned
- Hard seek fallback: if error exceeds 2 seconds, seeks directly (with 2-second cooldown)
After ~12-14 seconds (settle + calibration), the phone stays locked to FPP's position with 5-25ms steady-state error. The debug display shows live PLL state, error history, and playback rate.
- WebSocket transport: Python asyncio server polls FPP every 100ms, broadcasts
{state, base, pos_ms, mp3_url, server_ms}to all clients - HTTP fallback: 250ms polling of
status.phpwhen WebSocket is unavailable - Clock offset: NTP-style estimation via WebSocket ping/pong, median filter + EWMA (alpha=0.3)
- PLL calibration: least-squares linear regression, 800ms minimum window, 6+ samples, base rate clamped to +/-1%
- Locked correction:
rate = baseRate + sign(avg2s) * Kp * log1p((|avg2s| - 5) / 100), Kp adaptive 0.01-0.05 - Error averaging: 2-second rolling window (avg2s) as PLL input, all-time average for diagnostics
- Hard seek: >2 seconds error, 2-second cooldown between seeks
- Rate learning: EMA alpha=0.05 from 2-second windows
- Position data:
milliseconds_elapsedfrom FPP API (notseconds_played, which is whole-seconds only) - Server timestamp:
server_mscaptured at midpoint of API call, used for clock offset calculation;round()notintval()to avoid 32-bit overflow on Pi 3B - Apache proxy:
mod_proxy_wstunnelproxies/wson port 80 to Python server on port 8080 - systemd service: runs as
fppuser with 64MB RAM / 25% CPU limits for Pi safety - Playback control:
POST /api/commandwith "Start Playlist" / "Stop Now" commands - Audio unlock: browser autoplay policy requires a user gesture — first click/touch on the page silently plays and pauses to unlock the audio context