feat(audio): HDMI audio works out of the box — pipewire in ISO + router daemon + ELD boot-race heal
Root cause of silent HDMI on kiosk TVs (framework-pt, LG TV): 1) fresh ISOs installed NO audio stack even though the kiosk launcher expects PipeWire-Pulse — add pipewire/pipewire-pulse/pipewire-alsa/ wireplumber/alsa-utils to the rootfs and put the archipelago user in 'audio' (linger user manager gets no logind seat ACLs on /dev/snd); 2) the kiosk's boot-time Xorg modeset races the i915→HDA audio-component bind, the ELD notify is lost, and every HDMI profile stays 'available: no' forever. Verified live: one off/on modeset re-delivers the ELD and WirePlumber immediately switches to HDMI. New archipelago-audio-router daemon (same splice-from-configs pattern as the kiosk, ISO + include_str! self-heal): routes the card to the best available HDMI profile, keeps the default sink on it, migrates live streams on hot-plug, unmutes (HDMI forced 100% — the TV owns volume), and re-modesets a connected external output once when no ELD reports a monitor. bootstrap::ensure_audio_stack() installs packages + router on already-deployed kiosk nodes via OTA (apt through systemd-run — the service sandbox keeps /usr and dpkg read-only). main.rs also spawns the pine satellite keeper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
852ffc5b18
commit
8a450bb8f8
@@ -39,6 +39,19 @@ const KIOSK_LAUNCHER: &str =
|
||||
const KIOSK_SERVICE_PATH: &str = "/etc/systemd/system/archipelago-kiosk.service";
|
||||
const KIOSK_LAUNCHER_PATH: &str = "/usr/local/bin/archipelago-kiosk-launcher";
|
||||
|
||||
// HDMI audio (kiosk nodes): ISOs built before 2026-07-23 shipped no audio
|
||||
// stack at all even though the kiosk launcher expects PipeWire-Pulse, and the
|
||||
// kiosk's boot-time modeset can race the i915→HDA audio-component bind so the
|
||||
// HDMI ELD is lost and every HDMI profile stays unavailable (silent failure).
|
||||
// The router daemon handles routing + the ELD re-modeset nudge; this heal
|
||||
// installs the packages, group membership, script and unit on deployed nodes.
|
||||
const AUDIO_ROUTER: &str =
|
||||
include_str!("../../../image-recipe/configs/archipelago-audio-router.sh");
|
||||
const AUDIO_SERVICE: &str =
|
||||
include_str!("../../../image-recipe/configs/archipelago-audio-router.service");
|
||||
const AUDIO_ROUTER_PATH: &str = "/usr/local/bin/archipelago-audio-router";
|
||||
const AUDIO_SERVICE_PATH: &str = "/etc/systemd/system/archipelago-audio-router.service";
|
||||
|
||||
// Journald log-volume policy (size cap + per-service rate limit). Fresh ISOs
|
||||
// write the identical file at build time (image-recipe/_archived/
|
||||
// build-auto-installer-iso.sh); this heals already-deployed nodes via OTA.
|
||||
@@ -794,6 +807,78 @@ pub async fn ensure_kiosk_hardened() {
|
||||
}
|
||||
}
|
||||
|
||||
/// HDMI-audio self-heal for kiosk nodes: install the PipeWire stack (older
|
||||
/// ISOs shipped none), put the archipelago user in `audio` (PipeWire runs
|
||||
/// under the lingering user manager — no logind seat, so no udev ACLs on
|
||||
/// /dev/snd), and keep the audio-router daemon (HDMI routing + ELD boot-race
|
||||
/// nudge) installed and current. No-op on nodes without the kiosk — audio
|
||||
/// only matters where media plays on an attached display.
|
||||
pub async fn ensure_audio_stack() {
|
||||
if fs::metadata(KIOSK_SERVICE_PATH).await.is_err() {
|
||||
return; // no kiosk → no display audio to route
|
||||
}
|
||||
|
||||
// Package install runs via systemd-run (host_sudo), outside the service
|
||||
// sandbox — /usr and the dpkg database are read-only in our namespace.
|
||||
if fs::metadata("/usr/bin/pactl").await.is_err() {
|
||||
info!("audio: PipeWire stack missing — installing packages");
|
||||
let _ = host_sudo(&["apt-get", "update", "-qq"]).await;
|
||||
match host_sudo(&[
|
||||
"apt-get",
|
||||
"install",
|
||||
"-y",
|
||||
"-qq",
|
||||
"--no-install-recommends",
|
||||
"pipewire",
|
||||
"pipewire-pulse",
|
||||
"pipewire-alsa",
|
||||
"wireplumber",
|
||||
"alsa-utils",
|
||||
])
|
||||
.await
|
||||
{
|
||||
Ok(s) if s.success() => info!("audio: PipeWire stack installed"),
|
||||
Ok(s) => {
|
||||
warn!("audio: package install exited with {} — will retry next start", s);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("audio: package install failed: {:#} — will retry next start", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = host_sudo(&["usermod", "-aG", "audio", "archipelago"]).await;
|
||||
|
||||
let unit_was_missing = fs::metadata(AUDIO_SERVICE_PATH).await.is_err();
|
||||
let script_changed = write_root_if_needed(AUDIO_ROUTER_PATH, AUDIO_ROUTER)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if script_changed {
|
||||
let _ = host_sudo(&["chmod", "+x", AUDIO_ROUTER_PATH]).await;
|
||||
}
|
||||
let unit_changed = write_root_if_needed(AUDIO_SERVICE_PATH, AUDIO_SERVICE)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
if script_changed || unit_changed {
|
||||
if let Err(e) = host_sudo(&["systemctl", "daemon-reload"]).await {
|
||||
warn!("audio: daemon-reload failed: {:#}", e);
|
||||
}
|
||||
}
|
||||
if unit_was_missing {
|
||||
// First install on this node — bring it up now and on every boot.
|
||||
let _ = host_sudo(&["systemctl", "enable", "--now", "archipelago-audio-router.service"]).await;
|
||||
info!("audio: router installed and enabled (HDMI routing + ELD heal)");
|
||||
} else if script_changed || unit_changed {
|
||||
// Content update: restart only if it's running — never re-enable a
|
||||
// unit an operator deliberately disabled.
|
||||
let _ = host_sudo(&["systemctl", "try-restart", "archipelago-audio-router.service"]).await;
|
||||
info!("audio: router updated");
|
||||
}
|
||||
}
|
||||
|
||||
/// Patch the nginx site config to add missing backend proxy blocks. Older ISO
|
||||
/// configs shipped individual per-endpoint `location` blocks, so missing
|
||||
/// endpoints silently fell through to the SPA `index.html` and the frontend
|
||||
|
||||
@@ -386,6 +386,15 @@ async fn main() -> Result<()> {
|
||||
// flags) on already-deployed nodes via OTA; no-op if the kiosk isn't installed.
|
||||
tokio::spawn(bootstrap::ensure_kiosk_hardened());
|
||||
|
||||
// HDMI audio: install the PipeWire stack + audio-router daemon on kiosk
|
||||
// nodes (older ISOs shipped no audio stack; the router also heals the
|
||||
// boot-time ELD race that leaves HDMI silently unavailable).
|
||||
tokio::spawn(bootstrap::ensure_audio_stack());
|
||||
|
||||
// Pine voice: re-point IP-pinned Wyoming satellite entries (speakers) when
|
||||
// DHCP renumbering strands them — HA never re-resolves on its own.
|
||||
tokio::spawn(api::rpc::wyoming_satellite_keeper());
|
||||
|
||||
// Spawn periodic container snapshot (for crash recovery)
|
||||
crash_recovery::spawn_snapshot_task(config.data_dir.clone());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user