Publish Muninn OBS stream catalog

This commit is contained in:
Meta Vens 2026-06-08 21:01:33 +01:00
commit 66a3df8ad2
5 changed files with 86 additions and 6 deletions

View file

@ -37,6 +37,7 @@ struct Options {
ffmpeg_path: String,
loopback_script: PathBuf,
log_root: PathBuf,
obs_catalog_path: PathBuf,
interval_seconds: Option<u64>,
}
@ -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")?

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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