WiFi Audio Streaming Android - v1.2 🎉
Last release I said I'd stop rewriting everything, then immediately designed a new
protocol version and wrote a C implementation for it.
So this time I set a rule: do not touch the protocol.
And I actually kept it. WFAS is still v2, byte for byte, so Desktop 1.1 and Android
1.2 talk to each other perfectly fine. For the first time in this project's
history, you don't have to update both sides in the same afternoon.
Instead I spent the release teaching the app to talk to devices that have never
heard of WFAS and never will. Your TV. Your AV receiver. That Raspberry Pi in the
closet running Snapcast.
Also I broke your automations. Sorry.
The next section explains why, and the one after that tells you what to do about it.
🛡️ Security
This release includes fixes to the authentication and local control paths,
identified during a deeper security review of the code.
I'm not going into detail here, because v1.1 installations may remain in the wild
for months before updating.
If you're running v1.1, please update to v1.2.
⚠️ Breaking Changes
The protocol is untouched, so no "update both apps or nothing works" this time. I
broke something else instead.
If you use NFC, Tasker, MacroDroid, ADB or wifiaudio:// automation, this update
requires action. External commands are now off by default and require a token.
After updating:
- Open Automation & Scripting and turn on External commands.
- Recopy the URIs and ADB commands from that screen — they already include your
token.
The token is stored encrypted on the device and is never transmitted to another
device. You can regenerate it at any time, which invalidates every URI and NFC tag
you've already written. Tiles, widgets and shortcuts are unaffected and need nothing.
The Automation & Scripting screen is now the only supported way in. If you'd
found some other way of driving the app from outside, generate the equivalent there
instead — anything not listed on that screen should be considered unsupported.
Auto-connect only connects to servers you've starred, which is what the name
always implied. Star the ones you want and it does the rest.
📺 DLNA: your TV can play this now
The big one: the other device doesn't need this app.
Audio now goes straight to DLNA/UPnP renderers — smart TVs, AV receivers,
soundbars, network speakers. Nothing to install on them. The phone finds them over
SSDP, asks each one what it can actually play, negotiates a format and drives it.
Pick a renderer, it starts playing. Your choices are remembered for next time, and
a watchdog re-issues playback when one silently drops the stream, which they do a
lot.
DLNA is a standard, and I use that word loosely
Every manufacturer implements it slightly differently and each is convinced their
way is the correct one. Some refuse LPCM and want MP3. Some want a Content-Length
on an infinite stream, which is a delightful thing to ask for. Some need a Stop
before a new URI, some need 600 ms of silence before Play or they just sit there.
So there's a quirk table now: Denon/Marantz/HEOS get longer delays, Sonos a
different codec order, Samsung its Content-Length, LG and webOS a MIME override,
Yamaha and MusicCast get WAV first. Everything else gets sensible defaults and a
retry. If yours does something creative that isn't in there yet, the Copy
diagnostics button produces exactly what I need to add it.
One honest warning: DLNA receivers buffer heavily, so expect a few seconds of
delay. Great for music, useless for video or gaming. Use WFAS for those.
🔊 Snapcast: actual multiroom
The phone can now be a Snapcast server. Every snapclient on your network joins
in and plays in sync — Raspberry Pi, ESP32, Home Assistant, the official Snapcast
apps — room after room, actually aligned rather than roughly at the same time-ish.
- Audio on TCP 1704, JSON-RPC control on 1705, both advertised over mDNS so
clients find it with zero config - PCM, FLAC and Opus (Opus needs 48 kHz stereo; if you're elsewhere it says so
and falls back to PCM instead of failing silently) - Chunks of 10/20/40/60 ms, client buffer 200–5000 ms, configurable stream name
- The home screen shows connected clients, their volume and latency, the active
codec, and whether mDNS actually worked. A busy port is reported with its number
instead of just not starting.
📷 Pair with a QR code
Typing a 40-character key on a phone keyboard is not a pleasant experience, so now
you don't. Point the camera at the code the other device shows: address, port and
key arrive together, and the streaming connection is encrypted from the first
packet.
The part I like is that a QR-paired key is 256 random bits, not a passphrase a
human picked — a strong key without having to invent, type or remember one. On the
wire it's still ordinary Key mode, so nothing else changes.
- Invites expire after two minutes, so a code left up on a screen doesn't stay
useful all afternoon - The QR is an HTTPS link, so scanning it with a phone that doesn't have the app
lands on the download page instead of a dead custom scheme - Encryption is mandatory for QR and multicast pairing
- If a code won't scan, tap it for a plain black-and-white version; the key is
printed underneath in blocks of five if you have to read it out loud
And if you scan a QR that isn't ours, the app has opinions about that.
🔌 Streaming over USB
Plug the phone into the computer, turn on USB tethering, and the audio takes the
cable instead of the air: lower latency, no Wi-Fi congestion, and it works in
places where the network simply doesn't.
- The app tells you exactly where you are — no cable, cable but tethering off, or
connected — with a shortcut straight to the tethering settings, because that
toggle is buried three menus deep on most phones - USB gets its own jitter buffer (5–120 ms, default 20). Some phones deliver USB
packets in bursts and need more buffer over the cable, not less. - Devices found over the cable are badged Connected via USB
- New WFAS availability setting — Always, Not on USB (default) or Never —
decides whether Wi-Fi keeps being offered while the cable is up - There's a
usbscripting action too, and with no address it finds the peer on the
cable by itself, since that address changes every session
On the computer, just leave the desktop app on automatic interface selection.
🎧 Playback that doesn't click
- Latency correction is smooth. The playback rate gets nudged by a fraction of a
percent until the buffer lines up, instead of throwing packets away. Dropping only
happens if the backlog stays high, and at most once every 250 ms. - Lost packets get patched. The app finds the best point to continue from and
crossfades back in when the real stream returns. Where you used to hear a click,
you now mostly hear nothing. - Streams that drift too far get realigned instead of accumulating lag forever.
🩺 "It says Connecting and then nothing"
- A device that never answers now produces a dialog naming it, saying the other
end is probably running an older version, and offering the download page - Protocol mismatches name the guilty party — sender, receiver or this device —
with both version numbers. Two people on a call both reading "update required" and
both assuming it's the other one was a real support thread. - A server that doesn't accept a microphone says so, instead of ignoring your
client mic and leaving you to wonder
🎨 The spectrogram (I got distracted again)
Remember --groove in the desktop CLI, the terminal visualiser that serves
absolutely zero practical purpose? It's on Android now, behind the whole interface.
Turn on Background Spectrogram and pick Bars & Peaks or Fluid Waves. And
yes, Groove came with it: it subtracts the constant parts of the mix so the
visualiser follows the melody instead of headbanging to the kick drum.
There's a "Show only in Dark Screen" option, which is where I actually use it —
dark screen plus a spectrogram on an OLED is a screensaver I didn't mean to build.
🔔 Notifications got rebuilt
- Volume up and down straight from the notification, with percentage and a
proper muted state - Four separate channels — transmission, reception, auto-connect, automation —
so you can silence exactly the one that annoys you - Live status chips, dedicated icons, accent colours, and updates that are throttled
instead of firing once per status string - It actually goes away when the stream stops. It used to linger. It doesn't.
🔧 Other improvements
- Mute this device while streaming (on by default), so you don't hear everything
twice. Volume comes back when the server stops. - Keep server running after disconnect: in unicast a client leaving now ends the
session, not the server — the port stays open and the next device walks right in - Key strength meter under the manually typed key. Guidance, not a gate, and the
same maths runs on the desktop so both ends agree on what "strong" means. - Protocol summary in the UI — "Streaming with WFAS, RTP and DLNA" — instead of
making you remember what you enabled - Onboarding gained cards for DLNA and Snapcast. It's five ways to send the same
audio now. - Bouncy Castle updated to 1.78.1, and the two modules shipped without ever
being used are gone - ProfileInstaller added, so startup is quicker
- README rewritten with real screenshots and the new protocols. It also no longer
contains the sentence "Fred lives in the title!" in the middle of the setup
instructions... Yk, the settings...
I hope Fred is doing well.
💖 Support & Ko-fi
Same as always: if this is useful to you there's a Ko-fi link. :)
Support on Ko-fi
Enjoy v1.2!