Mimir/scripts/sender-start.ps1
2026-05-18 12:38:30 +01:00

151 lines
4.7 KiB
PowerShell

param(
[string]$Config = ".\config\localcast.json",
[string]$FfmpegPath = "ffmpeg",
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent (Split-Path -Parent $PSCommandPath)
$LogRoot = Join-Path $Root "logs"
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 Format-ArgumentList {
param([string[]]$Arguments)
$parts = @()
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 $LogRoot | Out-Null
$stdout = Join-Path $LogRoot "$Name.out.log"
$stderr = Join-Path $LogRoot "$Name.err.log"
$argumentLine = Format-ArgumentList -Arguments $Arguments
$process = Start-Process -FilePath $FfmpegPath -ArgumentList $argumentLine -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",
"-nostdin",
"-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",
"-nostdin",
"-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."
}