Casting YouTube Music to KEF LSX via DLNA: Solving the Missing Cast Target
The first-generation KEF LSX speakers sound fantastic, but their network streaming software is showing its age. They support Spotify Connect, AirPlay 2, and DLNA/UPnP, but there is no native Google Cast (Chromecast built-in) receiver.
If you use YouTube Music as your primary streaming service on Android or desktop Chrome, you are stuck: YouTube Music has no option to output to raw DLNA renderers. You either settle for lossy Bluetooth compression or juggle third-party casting utilities that constantly drop connections.
I decided to solve this properly by building kef-yt-streamer, a lightweight bridge service that runs on my local server (or a Raspberry Pi) and acts as an official Cast receiver target.
The Core Problem
When you hit the "Cast" button in YouTube Music, the app advertises discovery via mDNS / SSDP and expects to talk to a Cast V2 device. Once connected, YouTube Music spins up a web receiver session on the target device, passing stream tokens and media metadata over TLS.
The KEF LSX, meanwhile, exposes a standard UPnP/AVTransport endpoint on port 8080:
SetAVTransportURIto pass the media stream URL.Play,Pause,Stop,Seek.RenderingControlfor hardware volume adjustment.
How the Bridge Works
- Cast V2 Receiver Emulation: The bridge runs an mDNS advertiser declaring itself as a Cast-compatible audio receiver named "KEF LSX".
- Session Handshake: When you tap "KEF LSX" in YouTube Music, the bridge handles the initial TLS handshake and receiver launch request.
- Stream Resolution: When a track starts playing, the bridge intercepts the video/audio ID, resolves the direct audio stream URL (via internal stream extraction), and initializes the audio pipeline.
- DLNA Pipe & Metadata: The resolved audio stream is immediately pushed to the KEF LSX over UPnP AVTransport using raw PCM or high-bitrate AAC, accompanied by DIDL-Lite XML metadata (track title, artist, album art).
- Two-Way Control Synchronization: Volume changes made inside the YouTube Music app map directly to the speaker hardware volume, and physical remote commands (pause/play) map back to the cast session.
// Simplified AVTransport URI assignment
async function playTrackOnKef(speakerIp, streamUrl, meta) {
const didlXml = buildDidlMetadata(meta);
const action = 'SetAVTransportURI';
const body = `
<u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
<CurrentURI>${streamUrl}</CurrentURI>
<CurrentURIMetaData>${escapeXml(didlXml)}</CurrentURIMetaData>
</u:SetAVTransportURI>
`;
await sendSoapRequest(speakerIp, 8080, 'AVTransport', action, body);
await sendSoapRequest(speakerIp, 8080, 'AVTransport', 'Play', '<u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><Speed>1</Speed></u:Play>');
}
Running as a Persistent Service
The entire bridge runs in Docker on my home server with host networking enabled so mDNS packets flow freely:
services:
kef-yt-streamer:
image: ghcr.io/kennycoder/kef-yt-streamer:latest
network_mode: host
environment:
- SPEAKER_IP=192.168.1.140
- DEVICE_NAME=KEF LSX
restart: unless-stopped
Now, whenever anyone on my home Wi-Fi opens YouTube Music, "KEF LSX" shows up instantly in the cast list, latency is under 200ms, and audio streams bit-perfect without Bluetooth compression.