From 66a3df8ad2cc4c90b5abc0e2ee8d63ab705ac0ff Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 8 Jun 2026 21:01:33 +0100 Subject: [PATCH] Publish Muninn OBS stream catalog --- crates/muninn-daemon/src/main.rs | 49 ++++++++++++++++++++++++ docs/muninn.md | 11 ++++++ scripts/activate-muninn-raven-av-srt.ps1 | 3 +- scripts/health-muninn.ps1 | 15 +++++++- scripts/restart-muninn.ps1 | 14 +++++-- 5 files changed, 86 insertions(+), 6 deletions(-) diff --git a/crates/muninn-daemon/src/main.rs b/crates/muninn-daemon/src/main.rs index 53e63a9..01e0d37 100644 --- a/crates/muninn-daemon/src/main.rs +++ b/crates/muninn-daemon/src/main.rs @@ -37,6 +37,7 @@ struct Options { ffmpeg_path: String, loopback_script: PathBuf, log_root: PathBuf, + obs_catalog_path: PathBuf, interval_seconds: Option, } @@ -162,6 +163,7 @@ fn publish_surface( state: &str, active_streams: &[String], ) -> Result<()> { + write_obs_catalog_idle(options)?; let record = MuninnTelemetrySurfaceRecord { surface_id: options.surface_id.clone(), host_id: options.host_id.clone(), @@ -209,6 +211,7 @@ fn publish_stream( restart_count: u32, detail: &str, ) -> Result<()> { + write_obs_catalog_active(options, plan, state)?; let record = MuninnCaptureStreamRecord { stream_id: options.stream_id.clone(), host_id: options.host_id.clone(), @@ -235,6 +238,47 @@ fn publish_stream( Ok(()) } +fn write_obs_catalog_idle(options: &Options) -> Result<()> { + if let Some(parent) = options.obs_catalog_path.parent() { + fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?; + } + let mut lines = vec![ + "# stream_id\tlabel\turl\tstate".to_string(), + format!( + "{}\t{} screen and loopback A/V\t\tactivation-required", + options.stream_id, options.host_id + ), + ]; + for source in available_sources(options) { + lines.push(format!( + "{}:{}\t{}\t\taffordance", + options.surface_id, source, source + )); + } + fs::write(&options.obs_catalog_path, lines.join("\r\n") + "\r\n") + .with_context(|| format!("writing {}", options.obs_catalog_path.display())) +} + +fn write_obs_catalog_active(options: &Options, plan: &MuxPlan, state: &str) -> Result<()> { + if let Some(parent) = options.obs_catalog_path.parent() { + fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?; + } + let mut lines = vec!["# stream_id\tlabel\turl\tstate".to_string()]; + for (index, target) in plan.targets.iter().enumerate() { + lines.push(format!( + "{}:{}\t{} A/V target {}\t{}\t{}", + options.stream_id, + index, + options.host_id, + index + 1, + target, + state + )); + } + fs::write(&options.obs_catalog_path, lines.join("\r\n") + "\r\n") + .with_context(|| format!("writing {}", options.obs_catalog_path.display())) +} + fn health_check(options: &Options) -> Result<()> { let node = open_node(options, "muninn-health")?; let surface = node @@ -421,6 +465,7 @@ impl Options { ffmpeg_path: "ffmpeg".to_string(), loopback_script: PathBuf::from("scripts/wasapi-loopback-capture.ps1"), log_root: PathBuf::from("C:/Meta/Odin/logs/muninn"), + obs_catalog_path: PathBuf::from("C:/Meta/Odin/state/muninn-obs-streams.tsv"), interval_seconds: None, }; @@ -467,6 +512,10 @@ impl Options { "--log-root" => { options.log_root = PathBuf::from(take_value(&mut args, "--log-root")?) } + "--obs-catalog" => { + options.obs_catalog_path = + PathBuf::from(take_value(&mut args, "--obs-catalog")?) + } "--interval-seconds" => { options.interval_seconds = Some( take_value(&mut args, "--interval-seconds")? diff --git a/docs/muninn.md b/docs/muninn.md index 26b0a6e..4189faf 100644 --- a/docs/muninn.md +++ b/docs/muninn.md @@ -29,6 +29,17 @@ muninn --health --store C:\Meta\Odin\state\muninn.telemetry.cc surface fresh without starting FFmpeg, screen capture, microphone capture, or loopback capture. +Muninn also writes an OBS compatibility catalog by default: + +```text +C:\Meta\Odin\state\muninn-obs-streams.tsv +``` + +That TSV is a lowering of Muninn's typed state for OBS source property lists. It +is not the authority; the `.cc` CultMesh store is. On Starfire, +`health-muninn.ps1` syncs Raven's catalog to the same local path so the OBS +plugin can read it without learning Raven SSH or CultCache internals. + Activation is explicit: ```powershell diff --git a/scripts/activate-muninn-raven-av-srt.ps1 b/scripts/activate-muninn-raven-av-srt.ps1 index 484e6dd..b487ec6 100644 --- a/scripts/activate-muninn-raven-av-srt.ps1 +++ b/scripts/activate-muninn-raven-av-srt.ps1 @@ -2,6 +2,7 @@ param( [string] $RavenHost = "raven", [string] $MuninnExe = "C:\Meta\Odin\Muninn\muninn.exe", [string] $StorePath = "C:\Meta\Odin\state\muninn.telemetry.cc", + [string] $ObsCatalogPath = "C:\Meta\Odin\state\muninn-obs-streams.tsv", [string] $LoopbackScript = "C:\Meta\Odin\Muninn\scripts\wasapi-loopback-capture.ps1", [string] $LogRoot = "C:\Meta\Odin\logs\muninn", [string] $Ffmpeg = "C:\Users\Madman's Lullaby\AppData\Local\Microsoft\WinGet\Links\ffmpeg.exe", @@ -32,7 +33,7 @@ Get-CimInstance Win32_Process | `$lines = @( "@echo off", "cd /d ""`$muninnDir""", - """$MuninnExe"" activate --store ""$StorePath"" --host raven --stream muninn.raven.av.srt --target-host ""$TargetHost"" --port $Port --obs-target-host ""$ObsTargetHost"" --obs-port $ObsPort --audio-device ""$AudioDevice"" --ffmpeg ""$Ffmpeg"" --loopback-script ""$LoopbackScript"" --log-root ""$LogRoot"" 1>>""$LogRoot\muninn-activate.out.log"" 2>>""$LogRoot\muninn-activate.err.log""" + """$MuninnExe"" activate --store ""$StorePath"" --obs-catalog ""$ObsCatalogPath"" --host raven --stream muninn.raven.av.srt --target-host ""$TargetHost"" --port $Port --obs-target-host ""$ObsTargetHost"" --obs-port $ObsPort --audio-device ""$AudioDevice"" --ffmpeg ""$Ffmpeg"" --loopback-script ""$LoopbackScript"" --log-root ""$LogRoot"" 1>>""$LogRoot\muninn-activate.out.log"" 2>>""$LogRoot\muninn-activate.err.log""" ) Set-Content -LiteralPath `$launcher -Value `$lines -Encoding ASCII cmd /c "schtasks /Delete /TN GameCult-Muninn-Activate /F 2>NUL" diff --git a/scripts/health-muninn.ps1 b/scripts/health-muninn.ps1 index c12dfdd..37e9244 100644 --- a/scripts/health-muninn.ps1 +++ b/scripts/health-muninn.ps1 @@ -1,7 +1,9 @@ param( [string] $RavenHost = "raven", [string] $MuninnExe = "C:\Meta\Odin\Muninn\muninn.exe", - [string] $StorePath = "C:\Meta\Odin\state\muninn.telemetry.cc" + [string] $StorePath = "C:\Meta\Odin\state\muninn.telemetry.cc", + [string] $RemoteObsCatalogPath = "C:\Meta\Odin\state\muninn-obs-streams.tsv", + [string] $LocalObsCatalogPath = "C:\Meta\Odin\state\muninn-obs-streams.tsv" ) $ErrorActionPreference = "Stop" @@ -22,4 +24,13 @@ if (`$null -eq `$process) { $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteScript)) & ssh.exe -o BatchMode=yes -o ConnectTimeout=5 $RavenHost "powershell.exe -NoProfile -EncodedCommand $encoded" -exit $LASTEXITCODE +$healthExit = $LASTEXITCODE +if ($healthExit -eq 0) { + $localParent = Split-Path -Parent $LocalObsCatalogPath + if (-not [string]::IsNullOrWhiteSpace($localParent)) { + New-Item -ItemType Directory -Force -Path $localParent | Out-Null + } + & ssh.exe -o BatchMode=yes -o ConnectTimeout=5 $RavenHost "cmd /c type $RemoteObsCatalogPath" | + Set-Content -Encoding UTF8 -LiteralPath $LocalObsCatalogPath +} +exit $healthExit diff --git a/scripts/restart-muninn.ps1 b/scripts/restart-muninn.ps1 index fe4dc75..3a62cea 100644 --- a/scripts/restart-muninn.ps1 +++ b/scripts/restart-muninn.ps1 @@ -2,7 +2,9 @@ param( [string] $RavenHost = "raven", [string] $MuninnExe = "C:\Meta\Odin\Muninn\muninn.exe", [string] $StorePath = "C:\Meta\Odin\state\muninn.telemetry.cc", - [string] $LogRoot = "C:\Meta\Odin\logs\muninn" + [string] $ObsCatalogPath = "C:\Meta\Odin\state\muninn-obs-streams.tsv", + [string] $LogRoot = "C:\Meta\Odin\logs\muninn", + [string] $LocalObsCatalogPath = "C:\Meta\Odin\state\muninn-obs-streams.tsv" ) $ErrorActionPreference = "Stop" @@ -24,7 +26,7 @@ New-Item -ItemType Directory -Force -Path (Split-Path -Parent "$StorePath") | Ou `$lines = @( "@echo off", "cd /d ""`$muninnDir""", - """$MuninnExe"" serve --store ""$StorePath"" --log-root ""$LogRoot"" --host raven --interval-seconds 15 1>>""$LogRoot\muninn-serve.out.log"" 2>>""$LogRoot\muninn-serve.err.log""" + """$MuninnExe"" serve --store ""$StorePath"" --obs-catalog ""$ObsCatalogPath"" --log-root ""$LogRoot"" --host raven --interval-seconds 15 1>>""$LogRoot\muninn-serve.out.log"" 2>>""$LogRoot\muninn-serve.err.log""" ) Set-Content -LiteralPath `$launcher -Value `$lines -Encoding ASCII cmd /c "schtasks /Delete /TN GameCult-Muninn /F 2>NUL" @@ -34,4 +36,10 @@ cmd /c schtasks /Run /TN GameCult-Muninn $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteScript)) & ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $RavenHost "powershell.exe -NoProfile -EncodedCommand $encoded" -exit $LASTEXITCODE +$restartExit = $LASTEXITCODE +if ($restartExit -eq 0) { + Start-Sleep -Seconds 2 + $healthScript = Join-Path $PSScriptRoot "health-muninn.ps1" + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $healthScript -RavenHost $RavenHost -MuninnExe $MuninnExe -StorePath $StorePath -RemoteObsCatalogPath $ObsCatalogPath -LocalObsCatalogPath $LocalObsCatalogPath +} +exit $restartExit