Turned telep-mainframe into an AirPlay receiver named telep-tv (uxplay 1.71) so an iPhone/Mac can screen-mirror to the HDMI TV, with the camera wall auto-pausing during a cast and returning when it ends. The enabling change was splitting the Xorg :0 server out of camwall.service into its own persistent systemd service so stopping the camera grid no longer kills X (which uxplay needs alive).

For Agents

Cast target name: telep-tv (advertised over avahi as _airplay._tcp + _raop._tcp). New service: uxplay.service runs /usr/local/bin/uxplay-cast.sh as root on DISPLAY=:0, Requires+After camwall-x.service + avahi. Enabled + active. X server is now standalone: camwall-x.service owns Xorg :0 (persistent, Restart=always). camwall.service is now just a client of it (the 2×2 grid). Stopping/starting camwall.service toggles ONLY the grid — Xorg :0 persists. No firewall change needed — nftables INPUT policy is ACCEPT (only tailscale + docker chains); AirPlay on the LAN works as-is. Decode is SOFTWARE avdec_h264 — this box has NO GStreamer HW H.264 decode (no nvcodec, no VAAPI). Acceptable because AirPlay streams aren’t true 4K.

Why the X server had to be split (the big gotcha)

The original camwall.service ran xinit directly:

ExecStart=/usr/local/bin/camwall-x.sh   # -> xinit camwall-session.sh -- :0 vt1

So camwall.service OWNED the Xorg :0 server. Stopping camwall.service killed Xorg entirely (verified empirically). That made the intended “stop the camera wall while casting, restore it after” handoff impossible — uxplay needs :0 alive to render into.

Discrepancy with the older telep-mainframe doc

TV wall (HDMI) still describes the wall as mpv straight to DRM/KMS with no X server. That is now stale — as found during this work the camera wall runs under an Xorg :0 server (matchbox WM + a pane runner). Trust the X-based architecture below for anything touching the display/casting; the DRM/KMS description predates the go2rtc/X wall rebuild.

The fix: three-service X split

ComponentRole
/usr/local/bin/camwall-xserver.shexec xinit /usr/local/bin/camwall-xsession.sh -- :0 vt1 -nolisten tcp -keeptty
/usr/local/bin/camwall-xsession.shxset dpms/screensaver off, then exec sleep infinity — owns X, runs no WM
/etc/systemd/system/camwall-x.serviceNEW persistent Xorg :0, Restart=always, enabled
/etc/systemd/system/camwall.serviceMODIFIED — now a client: Environment=DISPLAY=:0, ExecStart=/usr/local/bin/camwall-session.sh (matchbox + pane-runner.sh grid). Requires + BindsTo + After camwall-x.service. Keeps its Frigate-wait ExecStartPre (curl 127.0.0.1:5000/api/version)

Result: stopping/starting camwall.service toggles only the camera grid; Xorg :0 persists so uxplay keeps rendering.

Backups

  • /etc/systemd/system/camwall.service.bak.<ts>
  • /usr/local/bin/camwall-x.sh.bak.<ts>

uxplay service + handoff wrapper

/etc/systemd/system/uxplay.service runs /usr/local/bin/uxplay-cast.sh as root, with:

Environment=DISPLAY=:0
Environment=XDG_RUNTIME_DIR=/run/uxplay
Requires=camwall-x.service avahi-daemon.service
After=camwall-x.service avahi-daemon.service

Enabled --now.

/usr/local/bin/uxplay-cast.sh runs uxplay line-buffered and parses its stdout to toggle the camera grid via a /run/uxplay/casting state file:

stdbuf -oL -eL uxplay -n telep-tv -vs xvimagesink -as "alsasink device=plughw:1,3"
  • Parses stdout for the real 1.71 log markers (below) → stops the grid on connect, restarts it on disconnect.
  • Has an EXIT trap that restores camwall if uxplay crashes mid-cast (verified working).

Real uxplay 1.71 log markers (verified from a live macOS mirror)

The initial guessed markers were WRONG — camwall failed to return

The first wrapper guessed at Open connections: N / TEARDOWN for the disconnect. uxplay 1.71 does NOT print those on a macOS screen-mirror stop at default verbosity, so the grid never came back. These are the markers observed from a real macOS (“András’s MacBook Pro”) session:

EventActionLog line (substring match)
CONNECTstop the gridAccepted <ip> client on socket
STREAM STARTfullscreen the windowBegin streaming to GStreamer video pipeline
DISCONNECT / ENDrestart the gridConnection closed for socket OR raop_rtp_mirror->running is no longer true

Trigger strings may still need an iPhone-specific tweak

All markers above were verified against a macOS connect/disconnect. An iPhone cast may print slightly different disconnect strings — flag a one-time re-check of the trigger against a live iOS session.

Hardware / sink decisions

GPU & display

  • Display driven by the NVIDIA RTX 3080 (driver 550.163.01), output HDMI-1 @ 3840x2160@60. Intel iGPU also present.

No hardware H.264 decode — software avdec_h264

GStreamer has NO HW H.264 decode on this box

  • Debian’s gstreamer1.0-plugins-bad ships no nvcodec (no nvh264dec/nvdec).
  • No VAAPI (vainfo empty; gstreamer1.0-vaapi not installed).

So decode is software avdec_h264 (from gstreamer1.0-libav). Rendering/scaling is still GPU via the sink. Acceptable because AirPlay streams aren’t true 4K, so the CPU cost is tolerable. (Note this is a different constraint from Frigate, which decodes via NVDEC — see venv) and the GPU topic.)

Audio — ALSA-direct to the TV over HDMI

  • HDMI audio to the TV is ALSA card 1 device 3 = “LG TV SSCR2” (ELD valid) → alsasink device=plughw:1,3.
  • A test tone plays (EOS, no busy error) even though PipeWire runs as user levander — ALSA-direct works because camwall has no audio and PipeWire wasn’t actively holding that PCM.
  • (This is the same NVIDIA-HDMI ALSA path the 2026-07-17-intruder-alarm siren uses: plughw:1,3.)

Video fullscreen — xvimagesink + xdotool resize, NOT -fs

uxplay -fs did NOT fill the 4K panel

With the matchbox WM the outer window became 3840x2160 but glimagesink rendered the video ~1:1 in the top-left (the viewport was never rescaled).

Solution:

  • Use xvimagesink (Xv rescales to the drawable every frame) + no WM.
  • Have the wrapper force-resize uxplay’s window (titled telep-tv@telep-mainframe) to 3840x2160+0+0 via xdotool when the Begin streaming marker appears.

Installed xdotool for this. The resize mechanism is proven; full edge-to-edge 4K fill is pending a final live-device confirmation.

Packages installed

uxplay, gstreamer1.0-tools, gstreamer1.0-alsa, xdotool (plus the libavahi-compat-libdnssd1 dependency). uxplay is 1.71 from Debian apt.

Firewall

nftables INPUT policy is ACCEPT (only tailscale + docker chains, no LAN block); no ufw. AirPlay on the LAN works with no firewall change. (Contrast with the tailnet-only posture of the Frigate UI — the AirPlay service is LAN-facing by design.)

Verified vs still to confirm

Verified working

  • uxplay.service active + enabled, running as root on :0.
  • avahi advertises telep-tv as _airplay._tcp and _raop._tcp.
  • The camwall X split works — the 2×2 camera grid renders (screenshot confirmed); grid stop/start keeps Xorg alive.
  • A real macOS connect (“András’s MacBook Pro”) triggered the handoff and stopped the grid.
  • The corrected wrapper’s EXIT trap restored camwall after uxplay exit.

Still to confirm with a live device

  • That xvimagesink + xdotool resize fills the 4K panel edge-to-edge.
  • That stopping the cast now restores camwall (the corrected-trigger-string fix, verified only in the crash/EXIT path so far).
  • Whether the disconnect trigger strings need an iPhone-specific tweak (all verified markers were from macOS).

Fix / troubleshooting — 2026-08-08: AirPlay dead after UPS switch + reboot

Root cause found + fixed, verified working (real cast connected + streamed at 14:55)

After the 2026-08-08 UPS switch + reboot, telep-tv stopped working — the target either didn’t appear on the iPhone/Mac, or appeared but casting did nothing. Two compounding issues; both now fixed durably.

Root cause (two compounding issues)

1. avahi was publishing telep-tv on ALL interfaces. allow-interfaces was commented out in /etc/avahi/avahi-daemon.conf → avahi’s default is to announce on every interface, including docker0, the incus/veth bridges, and loopback. So telep-tv.local resolved to a set of bogus addresses alongside the real LAN IP:

AddressInterfaceReachable by an Apple client?
192.168.1.123enp5s0 (LAN)✅ the only real one
172.17.0.1docker0
172.18.0.1incus/veth bridge
172.19.0.1incus/veth bridge
127.0.0.1loopback

Apple clients could latch onto an unreachable docker/loopback record → “appears but casting does nothing.”

2. DHCP IP flap on boot. enp5s0’s 192.168.1.123 is a dynamic DHCP lease (noprefixroute), not static. On this boot the address was briefly withdrawn and re-added; avahi logged Leaving mDNS multicast group ... Interface no longer relevant at 14:47:22 and only rejoined at 14:47:58. During that ~35 s window telep-tv wasn’t announced at all → “doesn’t appear.”

Fix (durable, survives reboot)

Set the interface allow-list in /etc/avahi/avahi-daemon.conf under [server] (was commented out → default all-interfaces):

[server]
allow-interfaces=enp5s0
  • Backup saved as avahi-daemon.conf.bak-<ts>.
  • systemctl restart avahi-daemon uxplay.
  • Verify: avahi-browse -rtp _airplay._tcp now shows ONLY the enp5s0 / 192.168.1.123 record — the docker/veth/loopback junk records are gone.

Verified working

Cast connected (Accepted IPv6 clientBegin streaming to GStreamer video pipeline) and the camwall handoff cycled correctly (grid stopped during cast, restored after). A real cast connected + streamed at 14:55.

Gotchas worth recording

tcpdump was NOT installed on telep-mainframe — the go-to network-debugging gotcha

mDNS/network debugging silently produced empty output until tcpdump was apt-get installed. It is now installed. For any future network debugging on this host, verify tcpdump exists first before trusting a “no packets” result.

casting state file

The camwall handoff state file lives at /run/uxplay/casting (0 = idle). /run/uxplay/ is root-only.

Open follow-up — DHCP reservation still needed

Not yet done: add a DHCP reservation for telep-mainframe

The boot-time IP flap will recur on future reboots unless 192.168.1.123 gets a DHCP reservation on the router. The router was factory-reset on 2026-08-03 (see 2026-08-03-telep-router-factory-reset-recovery), which likely dropped any prior reservation. Recommendation: add a static DHCP lease for the box’s MAC d8:5e:d3:a7:05:d6 so avahi doesn’t drop the mDNS group on future reboots.