I’ve got an AppleTV and I’ve got a little computer next to that hooked up to the other HDMI for when I need to put something that’s not supported via AppleTV directly. I’ll occasionally use Airplay to send content to the TV but that usually means some computer somewhere has to have buttons clicked and has to have a good network connection and not run out of batteries.
So I had this wild idea that I might be able to have a computer sitting in my basement that’s remotely accessible enough to start streaming things to the AppleTV but doesn’t need any special configurations or buttons clicks that are too fancy or needing to have specific Apple Hardware. Ultimately it was successful. Here’s the tail.
For the sake of a few examples where it was necessary the user is “stevemartin” the server is “murders.local”
Setting Up the Basics
Perform a mostly normal archinstall. Using EXT4. Create a new user for yourself. Selected the “Xorg” Profile and the Intel Drivers. “Use Network Manager (default backend)” and restart.
Now install ssh server on the device so we can access it remotely and check what IP address your new install has. After this you should be able to remove it from the display and unplug the keyboard.
> sudo pacman -S openssh > sudo systemctl enable --now sshd > ip addr Some more basic setup. Include a text editor (yes, vim is overkill) and install avahi/mDNS so I don’t have to remember the IP address anymore.
[on the client] > ssh stevemartin@57.145.0.1 [on the server] > sudo pacman -S vim > sudo pacman -S avahi nss-mdns > sudo vim /etc/nsswitch.conf You need to update the hosts line. Since I don’t want or need to support IPv6 I’ve said to explicitly use the IPv4 version. Update the hosts line so it looks something like this:
hosts: mymachines mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] files myhostname dns
Then enable the service so we can use it immediately. Then I like to do a reboot and ssh in using the name of the server.
[on the server] > sudo systemctl enable --now avahi-daemon > sudo reboot [on the client] > ssh stevemartin@murders.local Hacking the Kernel to customize the Display
We want to force the display to be 1080p even thought it doesn’t exist. So download a custom EDID, confirm that the file is what it says and then move it into place to be used.
> curl -LO https://github.com/akatrevorjay/edid-generator/raw/refs/heads/master/1920x1080.bin > di-edid-decode 1920x1080.bin > sudo mkdir -p /usr/lib/firmware/edid > sudo mv 1920x1080.bin /usr/lib/firmware/edid/ Get the custom EDID injected into the kernel by editing the config and rebuild.
> sudo vim /etc/mkinitcpio.conf The FILES section should look like this:
# FILES
# This setting is similar to BINARIES above, however, files are added
# as-is and are not parsed in any way. This is useful for config files.
FILES=(/usr/lib/firmware/edid/1920×1080.bin)
Then rebuild and confirm that it was included properly. There shouldn’t be anything using the “1080” text so it’s a pretty straightforward thing to confirm that it exists.
> sudo mkinitcpio -P > sudo lsinitcpio /boot/EFI/Linux/arch-linux.efi | grep 1080 Now lets get that connected up the the unified kernel image. You can take a peek in folder to see the display naming standards. We’ll be using the “HDMI-A-1” display. This needs to stay on one line. The “e” at the end of HDMI-A-1:e is a little interesting because it forces the display to be enabled even when it isn’t plugged in.
> sudo ls /sys/class/drm/ > sudo vim /etc/kernel/cmdln root=PARTUUID=xxx zswap.enabled=0 rw rootfstype=ext4 video=HDMI-A-1:e drm.edid_firmware=HDMI-A-1:edid/1920×1080.bin
Rebuild the UKI again and we should be good to go with the nasty kernel editing.
> sudo mkinitcpio -P Restart the box and check that the display shows as enabled and shows that it is running in 1080p
> cat /sys/class/drm/card0-HDMI-A-1/status > cat /sys/class/drm/card0-HDMI-A-1/modes Setting Up The Display Server
Tweak X11 so that it can be run by the system. The expectation is that X11 is always run by a user so you have to put this funny little override in place so that the system can run an X11 session as well. Otherwise you get errors like: “Only console users are allowed to run the X server.”
> sudo vim /etc/X11/Xwrapper.config The file should be empty so paste these contents
allowed_users = anybody
needs_root_rights = yes
Install Openbox so we can have the bare minimum window manager for our display server. Then create the systemd daemon service file.
> sudo pacman -S openbox > sudo vim /etc/systemd/system/xorg.service [Unit]
Description=Headless Xorg session
After=systemd-user-sessions.service[Service]
User=stevemartin
Environment=DISPLAY=:0
ExecStart=/usr/bin/startx /usr/bin/openbox-session — :0 vt1
Restart=always[Install]
WantedBy=multi-user.target
When the file is in place reload and enable the service.
> sudo systemctl daemon-reload > sudo systemctl enable --now xorg.service > systemctl status xorg.service > sudo journalctl -u xorg.service Check the resolution of X server and that the hardware is setup for acceleration. The glxinfo should tell you that intel graphics are used and the vainfo should tell you a list of supported accelerated encoders and decoders.
> sudo pacman -S xorg-xrandr mesa-utils libva-utils > DISPLAY=:0 sudo xrandr > DISPLAY=:0 glxinfo | grep "OpenGL renderer" > DISPLAY=:0 vainfo Setting Up VNC
> sudo pacman -S tigervnc If you get a prompt for which “jack” dependency to use select the “pipewire-jack” because we’ll be using pipewire in the audio process later so don’t want things to get mixed up.
You’ll need to set a password for the vnc server. I choose to not create a read only one because this device shouldn’t need that ability.
> vncpasswd The VNC server won’t run if it doesn’t have a display to attach it self to so it needs to start after Xorg is done setting things up. Xorg is meant to be run by a user so it doesn’t emit a proper “ready” event because different users have different definition of ready. So we need to pack it into Xorg’s autostart feature.
> vim ~/.config/openbox/autostart x0vncserver -display :0 -rfbauth /home/jlind/.config/tigervnc/passwd -SecurityTypes VncAuth -localhost no &
You should now be able to use the VNC client of your choice and connect to the server. Because we haven’t configured anything at all yet it’ll appear as any empty screen but if you right click you’ll open the menu. However that menu renders as a very odd set of boxes because there aren’t any fonts or styling available on the system yet.
Setting Up Openbox and Building a Basic GUI
The menu is expecting a generic “sans” font. So install a basic font and clear caches and restart and the menu will show up. Note that the menu contains a lot of things that we have not and will not install.
> sudo pacman -S ttf-dejavu > fc-cache -fv > sudo systemctl restart xorg.service Now install a browser and a terminal. Replace the contents of the menu with only the things we actually need. We’ll continue to refine that menu later but this is an extremely purposed build so leaving out things that are unnecessary.
> sudo pacman -S firefox kitty > sudo vim /etc/xdg/openbox/menu.xml <openbox_menu xmlns=”http://openbox.org/3.4/menu”>
<menu id=”root-menu” label=”Openbox 3″>
<item label=”Firefox”>
<action name=”Execute”>
<command>firefox</command>
</action>
</item>
<item label=”Terminal”>
<action name=”Execute”>
<command>kitty</command>
</action>
</item>
</menu>
</openbox_menu>
That menu is a little hard to read since it’s setup for an ultra minimalist approach. So I’ll increase the font size of the menu a bit. I found that 14 seems to fit the nice spot of big enough to read but not so big that navigation is difficult.
> sudo vim /etc/xdg/openbox/rc.xml <font place=”MenuHeader”>
<name>sans</name>
<size>14</size>
<weight>normal</weight>
<slant>normal</slant>
</font>
<font place=”MenuItem”>
<name>sans</name>
<size>14</size>
<weight>normal</weight>
<slant>normal</slant>
</font>
As long as I’m in there for this project I don’t need a virtual desktops so I’m going to limit it to just one. Making my desktops section look like this:
<desktops> <number>1</number> <firstdesk>1</firstdesk> <names> <name>Desktop</name> </names> </desktops> Now force Openbox to refresh the display configuration.
> DISPLAY=:0 openbox --reconfigure At this point your Firefox should be fully functional so add your adblocker of choice and disable the ai features and set the noai duckduckgo as your default search and set the new windows and new tabs starting page to blank page.
I’m not going to put any time into customizing Kitty here because it’ll only be used very briefly.
Enabling Audio
Yes. A headless device probably shouldn’t need audio but the point is to airplay stuff onto another device so we have to have something. The audio service should run as the user stevemartin (note no sudo on that line) because that’s how that service is really designed to be run. Tell the system to allow the user services lingering so that pulse should stay running when you need it. You could run it as a system service but that gets cranky fast.
> sudo pacman -S pulseaudio > systemctl --user enable --now pulseaudio.service > sudo loginctl enable-linger stevemartin Now that you’ve got pulse audio installed and told the service to activate your user you need to put one tweak into place to get Firefox inside the Xorg service running. Add the environment variable for PULSE_SERVER. The 1000 is the hardcoded user id for stevemartin so update that if you need. Then give the service a restart and
> sudo pacman -S openbox > sudo vim /etc/systemd/system/xorg.service [Service]
User=stevemartin
Environment=DISPLAY=:0
Environment=PULSE_SERVER=unix:/run/user/1000/pulse/native
ExecStart=/usr/bin/startx /usr/bin/openbox-session — :0 vt1
Restart=always
> sudo systemctl restart xorg.service Load up Firefox in the browser and play something that has audio. If you run this command in an ssh terminal to the device it should show that the audio is getting picked up by pulse.
> pactl list sink-inputs Running Doubletake for Airplay
> sudo pacman -S --needed base-devel
> sudo pacman -S git go gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav libpulse
Then checkout the code and build the application
> git clone https://github.com/omarroth/doubletake.git > cd doubletake > make Then if building seems to have gone well then you may as well just install it as a system application.
> sudo make install Now if you go back inside your VNC/OpenBox and open a new console and run doubletake you should get the list of Airplay compatible devices on your network and you can select one and watch Firefox/YouTube on your Apple TV.
Setting Up Hardware Acceleration
It seems like things are working fine but CPU usage is a bit high so let’s take a closer look. Install the Intel gpu tools and open up the monitoring.
> sudo pacman -S intel-gpu-tools > sudo intel_gpu_top As I’m using the VNC connection to watch a video in YouTube in Firefox I can see that the CPU is running high on the “gst” and “vnc” processes (around 80-100% each) but the Firefox is sitting in the single digits. When I look at the GPU I can see that Xorg and Firefox have offloaded work to the GPU so that’s perfect. Likely nothing worth trying to do for the VNC server (like replacing it with something that isn’t ancient) because it’s an old technology and I won’t have it running long term sending video over the wire.
Install the bridge for GStreamer hardware for my Intel GPU:
> sudo pacman -S gst-plugin-va Now when I run doubletake inside the OpenBox console I can see that the “gst” process sits consistently around 30% of CPU core usage but I can also see that it gets picked up in the GPU usage list. I don’t have to do anything special. The bridge was picked up automatically by the doubletake software.
Custom Pipe Menu for Doubletake
> mkdir -p ~/.config/openbox/scripts > vim ~/.config/openbox/scripts/doubletake.sh > chmod +x ~/.config/openbox/scripts/doubletake.sh Then open the file again and put this script in place.
#!/bin/bash
echo “<openbox_pipe_menu>”
if pgrep -x “doubletake” > /dev/null; then
echo ” <item label=\”Doubletake: RUNNING (PID: $(pgrep -xo doubletake))\”>”
echo ” <action name=\”Execute\”><command>echo ‘Already running'</command></action>”
echo ” </item>”
echo ” <separator />”
echo ” <item label=\”Kill Doubletake\”>”
echo ” <action name=\”Execute\”><command>pkill doubletake</command></action>”
echo ” </item>”
else
echo ” <item label=\”Launch Doubletake for Apple TV\”>”
echo ” <action name=\”Execute\”><command>doubletake -target=192.168.1.101 -no-cursor</command></action>”
echo ” </item>”
echo ” <item label=\”Launch Doubletake for Roku TV\”>”
echo ” <action name=\”Execute\”><command>doubletake -target=192.168.1.102 -no-cursor</command></action>”
echo ” </item>”
fi
echo “</openbox_pipe_menu>”
Then open up the original Openbox menu again and put the pipe menu inside the root menu to call the script. I can now start and stop Doubletake with specific
> sudo vim /etc/xdg/openbox/menu.xml <menu id=”doubletake” label=”Doubletake” execute=”~/.config/openbox/scripts/doubletake.sh” />
Putting Xpad in Place
I wanted a little sticky note on my desktop so I can leave some notes for myself. This can be done fairly easily through using Xpad. Install it and run it opening a new window. Then configure it as well as you can. I removed the toolbar and the window treatment. Put in the place I wanted and typed what I needed in it. Without the toolbar and window treatment is isn’t impossible to interact with the note but it’s harder and that’s what I wanted.
> sudo pacman -S xpad > xpad --new Next I want to make sure that Xpad is always available on restart so add it to the Openbox autostart
> vim ~/.config/openbox/autostart xpad &
If you want to manually tweak the Xpad you can see the settings and content in plain text by looking in the config directory. The content-* files are the text in a pad and info-* files are the pad configuration. Useful if you want to position the pad very specifically or edit the text of the pad in a different app.
> ls ~/.config/xpad/ If you want to test that everything is working as expected and Xpad is starting and putting your text in the right spot then you can restart the Xorg service.
> sudo systemctl restart xorg.service The End
I’m pretty sure that I’m the only person who has any interest in this document but if I ever need to build something similar in the future I’ve got this hanging around for myself. Otherwise I’m sure all the scrapers will be having a blast trying to mine this content for something useful.