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.serviceruns/usr/local/bin/uxplay-cast.shas root onDISPLAY=:0,Requires+After camwall-x.service+ avahi. Enabled + active. X server is now standalone:camwall-x.serviceowns Xorg:0(persistent,Restart=always).camwall.serviceis now just a client of it (the 2×2 grid). Stopping/startingcamwall.servicetoggles ONLY the grid — Xorg:0persists. No firewall change needed — nftables INPUT policy is ACCEPT (only tailscale + docker chains); AirPlay on the LAN works as-is. Decode is SOFTWAREavdec_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
:0server (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
| Component | Role |
|---|---|
/usr/local/bin/camwall-xserver.sh | exec xinit /usr/local/bin/camwall-xsession.sh -- :0 vt1 -nolisten tcp -keeptty |
/usr/local/bin/camwall-xsession.sh | xset dpms/screensaver off, then exec sleep infinity — owns X, runs no WM |
/etc/systemd/system/camwall-x.service | NEW persistent Xorg :0, Restart=always, enabled |
/etc/systemd/system/camwall.service | MODIFIED — 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.serviceEnabled --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
EXITtrap 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/TEARDOWNfor 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:
| Event | Action | Log line (substring match) |
|---|---|---|
| CONNECT | stop the grid | Accepted <ip> client on socket |
| STREAM START | fullscreen the window | Begin streaming to GStreamer video pipeline |
| DISCONNECT / END | restart the grid | Connection 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), outputHDMI-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-badships no nvcodec (nonvh264dec/nvdec).- No VAAPI (
vainfoempty;gstreamer1.0-vaapinot installed).So decode is software
avdec_h264(fromgstreamer1.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
-fsdid NOT fill the 4K panelWith 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) to3840x2160+0+0viaxdotoolwhen theBegin streamingmarker appears.Installed
xdotoolfor 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.serviceactive + enabled, running as root on:0.- avahi advertises
telep-tvas_airplay._tcpand_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
EXITtrap restored camwall after uxplay exit.
Still to confirm with a live device
- That
xvimagesink+xdotoolresize 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-tvstopped 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:
| Address | Interface | Reachable by an Apple client? |
|---|---|---|
192.168.1.123 | enp5s0 (LAN) | ✅ the only real one |
172.17.0.1 | docker0 | ❌ |
172.18.0.1 | incus/veth bridge | ❌ |
172.19.0.1 | incus/veth bridge | ❌ |
127.0.0.1 | loopback | ❌ |
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._tcpnow shows ONLY theenp5s0/192.168.1.123record — the docker/veth/loopback junk records are gone.
Verified working
Cast connected (
Accepted IPv6 client→Begin 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
tcpdumpwasapt-get installed. It is now installed. For any future network debugging on this host, verifytcpdumpexists 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.123gets 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 MACd8:5e:d3:a7:05:d6so avahi doesn’t drop the mDNS group on future reboots.
Related
- telep-mainframe — the host; note its TV-wall section still describes the pre-X DRM/KMS mpv wall (stale re: this X split)
- telep-mainframe-handover — host handover / operational context
- 2026-08-03-telep-router-factory-reset-recovery — the router rebuild that likely dropped the DHCP reservation (open follow-up above)
- 2026-07-28-camwall-4-substream-composite — the 4-substream camera-wall grid that this now pauses/restores
- 2026-07-31-camwall-lag-24ghz-wifi-bufferbloat — the wall’s real latency bottleneck (2.4 GHz WiFi), context for the grid it renders
- 2026-08-31-cams-vlan-device-claim-failed-wifi-reload —
camwall.service’s FrigateExecStartPregate (until curl -sf http://127.0.0.1:5000/api/version) makes it hang inactivating (start-pre)whenever the cams VLAN is down on the router — a downstream symptom of a router-side outage, not a camwall fault - 2026-07-17-intruder-alarm — same NVIDIA-HDMI ALSA sink (
plughw:1,3) for TV audio - homelab
- LOG
- TOPICS