Scaffold local OBS streaming bridge

This commit is contained in:
Meta Vens 2026-05-18 10:56:01 +01:00
commit beac84ae23
18 changed files with 1159 additions and 0 deletions

View file

@ -0,0 +1,33 @@
param(
[string]$FfmpegPath = "ffmpeg"
)
$ErrorActionPreference = "Stop"
function Invoke-Checked {
param(
[string]$Label,
[string[]]$Arguments
)
Write-Host ""
Write-Host "== $Label =="
& $FfmpegPath @Arguments 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Warning "$Label exited with code $LASTEXITCODE"
}
}
Write-Host "FFmpeg binary: $FfmpegPath"
Invoke-Checked -Label "Version" -Arguments @("-hide_banner", "-version")
Invoke-Checked -Label "SRT protocol check" -Arguments @("-hide_banner", "-protocols")
Invoke-Checked -Label "NVENC encoder check" -Arguments @("-hide_banner", "-encoders")
Invoke-Checked -Label "DirectShow devices" -Arguments @("-hide_banner", "-f", "dshow", "-list_devices", "true", "-i", "dummy")
Write-Host ""
Write-Host "Look for:"
Write-Host "- protocol list contains srt"
Write-Host "- encoder list contains h264_nvenc"
Write-Host "- audio device names you can copy into config/localcast.json"

132
scripts/sender-start.ps1 Normal file
View file

@ -0,0 +1,132 @@
param(
[string]$Config = ".\config\localcast.json",
[string]$FfmpegPath = "ffmpeg",
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
function Resolve-ConfigPath {
param([string]$Path)
$resolved = Resolve-Path -LiteralPath $Path -ErrorAction Stop
return $resolved.Path
}
function Assert-Value {
param(
[object]$Value,
[string]$Message
)
if ($null -eq $Value -or "$Value".Trim().Length -eq 0) {
throw $Message
}
}
function Get-SrtUrl {
param(
[object]$Receiver,
[int]$PortOffset
)
$port = [int]$Receiver.basePort + $PortOffset
$query = "mode=caller&latency=$($Receiver.srtLatencyMicros)"
if ($Receiver.passphrase -and "$($Receiver.passphrase)".Length -gt 0) {
$escaped = [uri]::EscapeDataString("$($Receiver.passphrase)")
$query = "$query&passphrase=$escaped&pbkeylen=16"
}
return "srt://$($Receiver.host):$($port)?$query"
}
function Format-CommandLine {
param(
[string]$Executable,
[string[]]$Arguments
)
$parts = @($Executable)
foreach ($arg in $Arguments) {
if ($arg -match '[\s"&?=]') {
$escaped = $arg.Replace('"', '\"')
$parts += '"' + $escaped + '"'
} else {
$parts += $arg
}
}
return ($parts -join " ")
}
function Start-SourceProcess {
param(
[string]$Name,
[string[]]$Arguments
)
$line = Format-CommandLine -Executable $FfmpegPath -Arguments $Arguments
if ($DryRun) {
Write-Host ""
Write-Host "[$Name]"
Write-Host $line
return
}
New-Item -ItemType Directory -Force ".\logs" | Out-Null
$stdout = Join-Path ".\logs" "$Name.out.log"
$stderr = Join-Path ".\logs" "$Name.err.log"
$process = Start-Process -FilePath $FfmpegPath -ArgumentList $Arguments -RedirectStandardOutput $stdout -RedirectStandardError $stderr -PassThru -WindowStyle Hidden
Write-Host "$Name started. PID=$($process.Id) stdout=$stdout stderr=$stderr"
}
$configPath = Resolve-ConfigPath -Path $Config
$settings = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
Assert-Value -Value $settings.receiver.host -Message "receiver.host is required"
Assert-Value -Value $settings.receiver.basePort -Message "receiver.basePort is required"
if (-not $settings.receiver.srtLatencyMicros) {
$settings.receiver | Add-Member -NotePropertyName srtLatencyMicros -NotePropertyValue 120000
}
if ($settings.video -and $settings.video.enabled) {
$videoUrl = Get-SrtUrl -Receiver $settings.receiver -PortOffset ([int]$settings.video.portOffset)
$videoArgs = @(
"-hide_banner",
"-loglevel", "info",
"-f", "gdigrab",
"-framerate", "$($settings.video.framerate)",
"-video_size", "$($settings.video.width)x$($settings.video.height)",
"-i", "$($settings.video.source)",
"-an",
"-c:v", "h264_nvenc",
"-preset", "$($settings.video.preset)",
"-tune", "$($settings.video.tune)",
"-b:v", "$($settings.video.bitrate)",
"-maxrate", "$($settings.video.maxrate)",
"-bufsize", "$($settings.video.bufsize)",
"-pix_fmt", "yuv420p",
"-g", "$([int]$settings.video.framerate * 2)",
"-f", "mpegts",
$videoUrl
)
Start-SourceProcess -Name "video" -Arguments $videoArgs
}
foreach ($audio in @($settings.audioSources)) {
Assert-Value -Value $audio.name -Message "audioSources[].name is required"
Assert-Value -Value $audio.device -Message "audioSources[].device is required"
$audioUrl = Get-SrtUrl -Receiver $settings.receiver -PortOffset ([int]$audio.portOffset)
$audioArgs = @(
"-hide_banner",
"-loglevel", "info",
"-f", "dshow",
"-i", "audio=$($audio.device)",
"-vn",
"-c:a", "$(if ($audio.codec) { $audio.codec } else { "aac" })",
"-b:a", "$($audio.bitrate)",
"-f", "mpegts",
$audioUrl
)
Start-SourceProcess -Name "audio-$($audio.name)" -Arguments $audioArgs
}
if ($DryRun) {
Write-Host ""
Write-Host "Dry run complete. Start OBS Media Sources in listener mode before running without -DryRun."
}

28
scripts/sender-stop.ps1 Normal file
View file

@ -0,0 +1,28 @@
param(
[string]$LogDirectory = ".\logs"
)
$ErrorActionPreference = "Stop"
$logs = Resolve-Path -LiteralPath $LogDirectory -ErrorAction SilentlyContinue
if (-not $logs) {
Write-Host "No log directory found at $LogDirectory"
return
}
$ffmpegProcesses = Get-CimInstance Win32_Process |
Where-Object {
$_.Name -ieq "ffmpeg.exe" -and
$_.CommandLine -like "*srt://*"
}
if (-not $ffmpegProcesses) {
Write-Host "No LocalCastBridge-looking ffmpeg SRT processes found."
return
}
foreach ($process in $ffmpegProcesses) {
Stop-Process -Id $process.ProcessId -Force
Write-Host "Stopped ffmpeg PID=$($process.ProcessId)"
}