# ============================================================ # SDI Bridge Orchestrator # - Checks FFmpeg build (must be with --enable-decklink) # - Reads Blackmagic driver version from registry # - Detects DeckLink channels # - Reads streams.txt # - Stops any previously running workers # - Launches one worker.cmd per stream in a separate window # ============================================================ # --- Config --- $FFmpeg = 'C:\sdi-bridge\ffmpeg.exe' $StreamsFile = 'C:\sdi-bridge\streams.txt' $WorkerScript = 'C:\sdi-bridge\worker.cmd' $StopFlag = 'C:\sdi-bridge\STOP.flag' # --- Helper: run native command and capture stdout+stderr --- function Invoke-Native { param([string] $Exe, [string[]] $Arguments) $prev = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { $out = & $Exe @Arguments 2>&1 | Out-String } finally { $ErrorActionPreference = $prev } return $out } # ============================================================ # 0. Sanity checks: ffmpeg exists, supports DeckLink # ============================================================ if (-not (Test-Path $FFmpeg)) { Write-Host "ERROR: ffmpeg.exe not found: $FFmpeg" -ForegroundColor Red exit 1 } Write-Host 'Checking FFmpeg build...' -ForegroundColor Cyan $versionOut = Invoke-Native -Exe $FFmpeg -Arguments @('-hide_banner', '-version') if ($versionOut -notmatch '--enable-decklink') { Write-Host 'ERROR: this FFmpeg build was compiled WITHOUT DeckLink support.' -ForegroundColor Red Write-Host ' Look for a build with --enable-decklink in its configuration.' -ForegroundColor Red Write-Host ' Current build configuration:' -ForegroundColor DarkGray Write-Host $versionOut exit 1 } Write-Host 'FFmpeg build OK (DeckLink enabled).' -ForegroundColor Green # ============================================================ # 0b. Blackmagic driver version (from registry) # ============================================================ function Get-BlackmagicDriverVersion { $candidates = @( @{ Path = 'HKLM:\SOFTWARE\Blackmagic Design\Desktop Video'; Name = 'Version' }, @{ Path = 'HKLM:\SOFTWARE\Blackmagic Design\Desktop Video'; Name = 'DriverVersion' }, @{ Path = 'HKLM:\SOFTWARE\WOW6432Node\Blackmagic Design\Desktop Video'; Name = 'Version' }, @{ Path = 'HKLM:\SOFTWARE\WOW6432Node\Blackmagic Design\Desktop Video'; Name = 'DriverVersion' } ) foreach ($c in $candidates) { try { $val = Get-ItemProperty -Path $c.Path -Name $c.Name -ErrorAction Stop if ($val.$($c.Name)) { return $val.$($c.Name) } } catch { } } # Fallback: look for the "Decklink" driver under Display adapters / Sound try { $drv = Get-CimInstance Win32_PnPSignedDriver -ErrorAction Stop | Where-Object { $_.DeviceName -match 'Blackmagic|DeckLink' } | Select-Object -First 1 if ($drv -and $drv.DriverVersion) { return $drv.DriverVersion } } catch { } return $null } $bmVersion = Get-BlackmagicDriverVersion if ($bmVersion) { Write-Host ("Blackmagic driver: {0}" -f $bmVersion) -ForegroundColor Green } else { Write-Host 'Blackmagic driver: version not detected (registry key missing)' -ForegroundColor DarkYellow } # ============================================================ # 1. Detect DeckLink channels # ============================================================ Write-Host 'Detecting DeckLink channels...' -ForegroundColor Cyan $sinks = Invoke-Native -Exe $FFmpeg -Arguments @('-hide_banner', '-sinks', 'decklink') $matches = [regex]::Matches($sinks, '\[([^\]]+?)\s*\((\d+)\)\]') $found = @() foreach ($m in $matches) { $found += [PSCustomObject]@{ Number = [int]$m.Groups[2].Value Name = $m.Groups[1].Value.Trim() } } if ($found.Count -eq 0) { Write-Host 'ERROR: No DeckLink channels detected.' -ForegroundColor Red Write-Host '--- Raw output from ffmpeg -sinks decklink ---' -ForegroundColor DarkGray Write-Host $sinks exit 1 } # Deduplicate + force sort by number, store as plain int array $channelNumbers = @($found | Sort-Object Number -Unique | ForEach-Object { $_.Number }) $cardName = ($found | Select-Object -First 1).Name -replace '\s*\(\d+\)$','' Write-Host ("Card: {0}" -f $cardName) -ForegroundColor Green Write-Host ("Channels: {0}" -f ($channelNumbers -join ', ')) -ForegroundColor Green # ============================================================ # 2. Read streams file # ============================================================ if (-not (Test-Path $StreamsFile)) { Write-Host "ERROR: streams file not found: $StreamsFile" -ForegroundColor Red exit 1 } $lines = Get-Content $StreamsFile | ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') } if ($lines.Count -eq 0) { Write-Host "ERROR: no streams in $StreamsFile" -ForegroundColor Red exit 1 } Write-Host ("Loaded {0} stream line(s), {1} channel(s) available" -f $lines.Count, $channelNumbers.Count) -ForegroundColor Green if ($lines.Count -gt $channelNumbers.Count) { Write-Host ("WARNING: {0} stream(s) will be skipped (not enough channels)" -f ($lines.Count - $channelNumbers.Count)) -ForegroundColor Yellow } # ============================================================ # 3. Build assignment: channel -> url # ============================================================ $usedChannels = @{} $assignments = @() $skipped = @() foreach ($line in $lines) { if ($line -match '^(\d+)\s*:\s*(.+)$') { # Explicit channel: "3:rtsp://..." $ch = [int]$matches[1] $url = $matches[2].Trim() if (-not ($channelNumbers -contains $ch)) { Write-Host " SKIP: channel $ch not present -> $url" -ForegroundColor Yellow $skipped += $line continue } if ($usedChannels.ContainsKey($ch)) { Write-Host " SKIP: channel $ch already taken -> $url" -ForegroundColor Yellow $skipped += $line continue } } else { # Auto-assign next free channel $ch = $null $url = $line foreach ($c in $channelNumbers) { if (-not $usedChannels.ContainsKey($c)) { $ch = $c; break } } if ($null -eq $ch) { Write-Host " SKIP: no free channels left -> $url" -ForegroundColor Yellow $skipped += $line continue } } $assignments += [PSCustomObject]@{ Channel = $ch; Url = $url } $usedChannels[$ch] = $true } if ($assignments.Count -eq 0) { Write-Host 'ERROR: no valid assignments could be made.' -ForegroundColor Red exit 1 } # ============================================================ # 4. Cleanup: stop any previously running workers # ============================================================ Write-Host '' Write-Host 'Cleaning up previous workers...' -ForegroundColor Cyan # 4a. Signal all workers to stop New-Item -Path $StopFlag -ItemType File -Force | Out-Null # 4b. Kill ffmpeg so workers unblock and see the flag Get-Process ffmpeg -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 # 4c. Kill any leftover worker cmd windows by their title Get-Process cmd -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowTitle -like 'SDI-*' } | Stop-Process -Force -ErrorAction SilentlyContinue Start-Sleep -Milliseconds 500 # 4d. Remove the flag so new workers can start normally Remove-Item $StopFlag -ErrorAction SilentlyContinue Write-Host 'Cleanup complete.' -ForegroundColor DarkGray # ============================================================ # 5. Launch workers in separate cmd windows # ============================================================ Write-Host '' Write-Host 'Launching workers:' -ForegroundColor Cyan $table = @() foreach ($a in ($assignments | Sort-Object Channel)) { $argLine = "/k `"`"$WorkerScript`" $($a.Channel) `"$($a.Url)`" `"$cardName`"`"" Start-Process -FilePath 'cmd.exe' -ArgumentList $argLine | Out-Null $table += [PSCustomObject]@{ SDI = "SDI-$($a.Channel)" Stream = $a.Url Status = 'launched' } } # ============================================================ # 6. Summary # ============================================================ Write-Host '' $table | Format-Table -AutoSize | Out-String | Write-Host Write-Host ("Launched: {0}" -f $assignments.Count) -ForegroundColor Green if ($skipped.Count -gt 0) { Write-Host ("Skipped: {0}" -f $skipped.Count) -ForegroundColor Yellow foreach ($s in $skipped) { Write-Host " $s" -ForegroundColor DarkYellow } } Write-Host '' Write-Host 'To stop everything, run: powershell -ExecutionPolicy Bypass -File C:\sdi-bridge\stop-all.ps1' -ForegroundColor DarkGray