CEC (HDMI) to Sonos Adapter
Building an HDMI to Sonos Adapter to fix my TV
Here’s a dumb problem to have in 2024: I need to use a separate remote to control the volume of my TV. Worse than that, I need to aim it with laser precision at a tiny IR receiver. The solution involves a Raspi, some python code, and the mysterious inner workings of the HDMI CEC protocol.
Home theater woes
I use a Sonos Playbar sound bar and a Roku powered smart TV for my living room.
Roku remotes come in two major flavors: Infrared and Wifi. They have the same overall design and functionality, but the IR remote I have is the worst. Each button takes so much force to actuate it feels like it was expressly designed to cause early-onset arthritis. And I need to press those awful buttons over and over again as I send half of the ill-fated radiation to the nether-realm just a tiny bit too far left of the receiver.
So I prefer the Wifi remote. Under the hood the wifi remote uses “Wifi Direct” to directly connect to a hidden Wifi network between the TV and the remote. It’s a very fancy and more secure way of doing “RF”. The build quality is also 1000% better and it doesn’t matter if I aim it at the moon. The problem is volume requests need to first pass through the TV which then translates those requests to HDMI CEC packets to the “audio system” connected over HDMI. If there is no such system, it simply doesn’t work.
My sound bar only supports optical input from the TV rather than eARC HDMI. Both standards are perfectly capable of transmitting digital audio. But optical only transmits audio, “requested volume” is not part of that standard. It’s only a part of the HDMI standard. The modern “just works” magic is all within HDMI, in particular within a feature called CEC. CEC allows for devices to actually have names, for volume to just work, to switch to the right input source auto-magically when played, etc.
The only upside of the IR remote is that it is reprogrammable like any universal remote. So I programmed it to control the IR reciever on the Sonos Playbar, and it “just works”. I just aim at the TV IR reciever for navigation and at the Playbar for volume control. It’s just that it “just works” in the most painful way possible, sapping my very life force with every press of a button.
Smart stuff that smarts
I have mixed feelings about both the Sonos and Roku software stacks. They are both technically interesting and offer ways control them over a local network. But they can cause just as much joy as dire frustration when actually attempting to integrate with them.
The entire line of Sonos products are primarily marketed as smart wifi speakers first, TV and other inputs secondarily so. You are meant to install their app (which for no good reason requires a recent Android or Windows release), then browse music services within the app to play on your speakers. Under the hood the app is similar to DLNA or “casting” in that you are really just telling the speaker where the audio source is on the internet rather than providing it one directly. You can’t normally even connect with them over bluetooth. But you can point it to a media source, pause/play, control the volume, and group it with other Sonos speakers that all sync perfectly (a big selling point). Most importantly all of that has been reverse engineered well enough to do it in Python, even if it is far from being officially supported in any capacity. So that is one piece of the puzzle. I can’t control the volume over HDMI, but I can control the volume over either IR or a special network API.
Roku also has a lot of programmatic network control, including the ability to emulate pressing remote buttons and launch specific applications. But at least as far as I am aware, none of it it is helpful to this problem. You can’t listen to and intercept the normal remote events through any of the standard API. I was hoping at one point I could make the remote connect to my regular wifi network instead of wifi direct and intercept its requests, but I found no way to make that happen.
Raspi to the rescue
What if you could join that HDMI CEC network as a fake audio reciever and translate the volume requests you get to volume changes on the Sonos network API? Can you do that? Yes, you can!
I happen to have a PC on one of the HDMI ports for couch gaming, so my first thought was there must be a way to use that connection. There is and there isn’t. It’s just not a standard thing for graphics cards to expose any way to talk on the CEC bus that is right there on the HDMI cable attached to it. But what you can do is get a Pulse-Eight adapter like this one and just talk to the CEC bus over a USB connection instead. I was about to shell out for that adapter before I learned that some single board computers have the same adapter built into them. And that includes every iteration of the famous Raspberry Pi boards.
And hey, I have one of those! In fact I have a “Raspberry Pi Model B Rev 1” kicking around. As in the first one. From 2012. Might as well put it to good use.
I just needed to enable CEC and ARC on my TV and plug the Raspi in. Thankfully my TV doesn’t try to be too smart and turn off the optical output simply because HDMI eARC is turned on. If it did that would be a disaster.
The setup looks like this:
┌────────┐ │ │HDMI┌─────┐ │ TV ├────►Raspi│ │ │ └──┬──┘ └───┬────┘ │ │Optical │Network ┌───────▼────────┐ │ │ Sonos ◄───┘ └────────────────┘
Funnily enough I did need to plug it into the special “eARC” port to avoid a special warning message popping up occasionally. For my setup it would be perfectly fine to just be anywhere on the CEC bus since I only care about volume requests. But normally it would be a mistake to plug your reciever into an HDMI port that isn’t ARC (Audio Return Channel) since it wouldn’t get digital audio back from the TV. Apparently the Roku detects that situation and tries to warn you about it.
I pushed the code up to Github.
The most obvious issue is the Roku likes to display a big fat “100” as a default on-screen volume status before it gets corrected by my code on button release. I believe the issue is I can’t properly “reply” to a request from the TV asking about the volume status, and it asks about the status a lot. I am relying on the Python bindings to libcec, and the ability to indicate a transmission is a “reply” got lost in the binding translation. The best I can do is tell the TV what the status is outside of that request, which leads to weird on-screen behavior. But thankfully it doesn’t affect the functionality, it just looks broken.
It’s also just kind of slow, as you might expect for an old Raspi
trying to run Python code that is partially network dependent. I put in
a lot of tricky timing logic to get it feeling as responsive as
possible, mostly by avoiding overdoing either audio status reports or
Sonos interactions. SoCo
provides a nice abstraction where setting a volume is literally as
simple as sonos.volume = 15
. But that hides a relatively
slow network request. I found I needed to avoid what looks like a simple
assignment as much as possible by doing that operation in a separate
thread and only when strictly necessary.
In general libcec is designed for home theater PC use cases. It’s geared more as a way to control the volume rather than emulating the thing that gets volume controlled. So I needed to build a few “raw” CEC frames to directly transmit on the line. CEC-O-MATIC was incredibly useful for that. It’s a nice web app that supports decoding and encoding raw bytes to and from human descriptions. There isn’t any other resource that makes it more clear how CEC actually works and how to hack around in that protocol.
If I had to do it over I would explore either using libcec from C code, or better yet look at using the kernel cec API. But it all works far better than the IR remote, so I’m happy.
- Published
- 2024-12-01