Scott Conway

Information Security Researcher

A Tour of my Overcomplicated Home Theater Setup

Using a Fire Stick

I bought a TV a few months ago. I hadn’t had one for a while, but I was ready to make a home theater at the time of purchase. I already had a 3.1 audio system ready, with a 4K capable Yamaha receiver. Not to mention, a trusty rooted Amazon Fire Stick 4K that had been collecting dust since the last time I had a TV.

After initial setup, the equipment seemed to work well. It took a bit to set up ARC and HDMI-CEC, but once they were ready, everything seemed to operate fantastically. At first glance, the Fire Stick was a great solution to streaming content from my local Jellyfin server. However, after some time, I found a number of issues with some combination of Android, the device itself, and my heavily customized setup. First, I had a tough time finding a media player that would play well with hardware decoding 4K streams (without dropping frames). When I settled on Kodi, I found that video streams would consistently error after ~40-60 minutes of playback. The Kodi app would need to be restarted to re-launch the media stream. After pulling my hair out for a bit, I decided on an easier course of action than using an Android TV box - long HDMI cable.

You should really just plug your desktop into your TV

I was lucky to find that I only needed a ~35ft cable to reach from my desktop to my TV, almost maxing out the practical length of an HDMI cable. So I got to testing. I had grand plans, but at first I just addressed the TV as another logical display and managed it with a wireless keyboard/trackpad (Logitech K400, courtesy of the local Goodwill). I’d be using the keyboard 99% of the time, as I just move through my media collection to launch videos in mpv.

Initial problems:

Let’s go through these one at a time.

Changing from desktop to TV mode and back take too many steps!

I quickly made some aliases for changing my display modes to “desktop” and “tv” modes, using xrandr. This one mirrors the TV’s output at scale onto my 1080p monitor.

xrandr --output HDMI-A-0 --auto \
--output DisplayPort-1 --scale-from 4096x2160 --same-as HDMI-A-0

Using pacmd, we can set the audio sink to use the TV (which is hooked up to the receiver through HDMI ARC).

pacmd set-default-sink alsa_output.pci-0000_03_00.1.hdmi-stereo

The interface sucks - keyboards (without a backlight) are not great in low-light conditions, and the K400 trackpad is awful to use

After using the keyboard/trackpad for a few weeks, I noticed that my biggest complaint was not having a backlight. I largely use the keyboard to select media to play, and as an oversized pause/play button. If done correctly, I could absolutely swap it out for a properly configured remote or gamepad. The Fire Stick remote is just a Bluetooth HID device, so I paired it with my desktop and started mapping its buttons. xev showed that it used keys, not buttons. That made macroing it a bit more difficult than it needed to be.

If it was using buttons, I’d simply write the actions to take in ~/.xbindkeysrc. Instead, the keys it sent were quite normal - arrow keys, enter, and several XF86 keys. I’d like to macro the buttons, but not affect other input devices’ operation. After reading a few too many StackOverflow posts on the matter, I landed up at evdevremapkeys. This program allows you to remap (not macro) keys from specific input devices. Since you can only remap keys, I ended up creating macros by mapping the voice search button as a series of modifier keys that no sane person would ever use on a real keyboard. This allowed me to create shortcuts in my desktop environment that would effectively only be used by the Fire Stick remote. Additionally, since evdevremapkeys does not support disabling keys entirely, I “disabled” keys by having them send a useless key event - scroll lock.

Here’s the configuration that I ended up using. This is heavily geared towards my exact workflow and the programs I use. It is likely not optimal for you, but it should serve as a nice starting point.

devices:
- input_name: AR Keyboard
  input_phys: 'e8:48:b8:c8:20:00'
  output_name: remap-fire
  remappings:
    # disable buttons that perform IR actions
    KEY_POWER:
    - KEY_SCROLLLOCK
    KEY_VOLUMEUP:
    - KEY_SCROLLLOCK
    KEY_VOLUMEDOWN:
    - KEY_SCROLLLOCK
    KEY_MUTE:
    - KEY_SCROLLLOCK

    # voice search button as modifier key
    KEY_SEARCH:
    - KEY_LEFTMETA
    - KEY_LEFTCTRL
    - KEY_LEFTALT
    - KEY_LEFTSHIFT

    # keep arrow keys and enter button as they are
    # arrow keys scrub, center button selects
    #
    # KEY_UP
    # KEY_DOWN
    # KEY_LEFT
    # KEY_RIGHT
    # KEY_KPENTER

    # caja up directory
    KEY_BACK:
    - KEY_LEFTALT
    - KEY_UP

    # caja movies shortcut
    KEY_HOMEPAGE:
    - KEY_LEFTMETA
    - KEY_LEFTCTRL
    - KEY_LEFTALT
    - KEY_LEFTSHIFT
    - KEY_UP

    # mpv fullscreen
    KEY_MENU:
    - KEY_F

    # mpv prev file
    KEY_REWIND:
    - KEY_LEFTSHIFT
    - KEY_COMMA

    # mpv pause/play
    KEY_PLAYPAUSE:
    - KEY_SPACE

    # mpv next file
    KEY_FASTFORWARD:
    - KEY_LEFTSHIFT
    - KEY_DOT 

Another note - xev had problems detecting the media keys, since my desktop environment hijacked them. Using evdevremapkeys -e to listen to the remote, I was able to identify all of the keys.

With this setup, I can configure 11 macros with the search key and another button, all without having to use buttons that are already mapped to IR functions. I’ve configured macros in my desktop environment to launch applications and run scripts, such as the above xrandr command for toggling my “tv mode” display layout.

I’m still not 100% happy with this configuration, but the basis of how the remote generates macros and keypresses is good.

Audio issues with non-stereo profiles (5.1, 7.1)

It took me far too long to realize this, but using an audio receiver in any mode other than “straight” is incorrect. As stated in this fantastic blog post, HDMI ARC for 3.1+ audio channels can be hit or miss. TVs don’t always support more than 2 PCM channels (even though HDMI 1.2 supports up to 8 channels).

Fortunately, additional audio channels work when compressed input is used. HDMI supports more audio formats, including DTS-HD MA and TrueHD.

Well, not on my TV! My Samsung TV displays PCM, Dolby Digital, DTS, and DTS Neo 2:5 as possible output methods. However, it does not let me select Dolby or (standard) DTS settings. When outputting DTS to the TV with this PulseAudio plugin, my receiver produces a nice buzz, and nothing more.

That’s not to detract from the blog post - if your TV is sane and supports standards such as DTS, by all means, use it and continue to use HDMI ARC! For me, I have to live with connecting my input source to my receiver to have 3.1+ sound. That leads to its own issues, such as HDMI-CEC not turning on my receiver when the TV is powered on.

My desktop only wants to drive the TV at 30Hz for 4K resolutions

This one is likely specific to Samsung TVs, though other OEMs might have similar issues. In order to drive the display above resolutions of 1080p at 60Hz, you need to enable the Full UHD Color setting for your given HDMI port in the TV’s configuration (under General -> External Device Manager). However, this requires that the connected input provides 10-bit color. This wasn’t too difficult to configure in Xorg for my AMD graphics card. Thanks Archwiki!

/etc/X11/xorg.conf.d/20-amdgpu.conf

Section "Screen"
	Identifier "HDMI-A-0"
	DefaultDepth 30
EndSection

Bonus - HDMI-CEC isn’t turning on my audio receiver!

After settling on connecting my input sources to the audio receiver, I noticed that HDMI-CEC would no longer power on the receiver when the TV is turned on. This lead to some interesting behaviors.

I figured that since I use a script to switch to “TV mode” to begin with, including a line that would power on the receiver over the network wouldn’t be so bad. I have a Yamaha RX-V679, which has a pretty capable API. The only downside is that it’s XML-based. I came across this repo to control it. I started a python rewrite, but as most projects go, I didn’t get complete coverage / testing so I’ve yet to publish it. Maybe someday I will!

Anyway, this is all it takes from a NodeJS script to power on the receiver.

var YamahaAPI = require("yamaha-nodejs");
var yamaha = new YamahaAPI("192.168.1.100");
yamaha.powerOn()

Addendum - everything is not great

Nov 10, 2023

Turns out I have a few more issues. First, 10-bit color isn’t really addressable on a per-display basis. By setting a single screen to a color depth of 30 in my Xorg config, now all windows render in 10-bit mode. Although things have improved significantly since this post, namely with 10-bit color support in Vulkan, some applications still incorrectly interpret the color depth, and others fail to launch entirely (eg. steam). Due to this, I’ll be putting the display back on 8-bit color, limiting me to 1080p 60Hz or 4K 30Hz.

And from the looks of it, even if 10-bit color was totally supported by all of the programs I use, I’d still be in a bit of a bind. It appears that my receiver doesn’t want to pass 10-bit color signals (though I’m not 100% confident in this assessment at this time). Even with 10-bit color enabled, I’m not able to drive the TV at 4K 60Hz behind the receiver.

On a slightly brighter note, after futzing with the Yamaha receiver API a bit, I’m happy to be able to replace the above use of NodeJS with curl. It’s off-putting that the request must be of type POST but is referred to as a PUT command in the request content.

curl 'http://192.168.1.100/YamahaRemoteControl/ctrl' -X POST --data-raw '<YAMAHA_AV cmd="PUT"><Main_Zone><Power_Control><Power>On</Power></Power_Control></Main_Zone></YAMAHA_AV>'

Addendum Addendum

Feb 27, 2024

Through the magic of ESPHome, I’ve installed an ESP8266 with an IR LED right next to my TV that can be controlled through Home Assistant. With this, I shouldn’t have a need for the original controller ever again. I’ve modified my “TV mode” scripts to send the power on/off command to the IR LED, so that I can properly sequence turning the TV on, turning the receiver on, and setting my default ALSA sink (and lastly changing my display settings if I want).

Here are the relevant parts of my config (for a Samsung TV). I wasn’t able to find these values anywhere online, so I had to read them myself. For that, I followed this guide.

- platform: template
  name: "On/Off"
  on_press:
    - remote_transmitter.transmit_samsung:
        data: 0xE0E040BF
- platform: template
  name: "Source"
  on_press:
    - remote_transmitter.transmit_samsung:
        data: 0xE0E0807F
- platform: template
  name: "Exit"
  on_press:
    - remote_transmitter.transmit_samsung:
        data: 0xE0E0B44B
- platform: template
  name: "Select"
  on_press:
    - remote_transmitter.transmit_samsung:
        data: 0xE0E016E9

remote_transmitter:
  pin: GPIO4
  carrier_duty_percent: 50%

Although it’s not perfect, I’m decently happy with my setup now. The only thing I’m lacking now is awareness of the TV’s power state, which could be achieved by having the TV’s power draw monitored by the connected ESP board, but I don’t care that much.