Harden RTSP startup recovery and frame settings

This commit is contained in:
seo-amugae committed 2026-10-08 10:41:16 +09:00
1 parent 9513ca6e15
commit 5241194b9a
3 files changed
+187 -9

No files matched your search

+2 -1
View File
@@ -143,6 +143,7 @@ SignalThreshold=0.0005
| `RTSP` | `HealthCheckIntervalSec` | `10` | 모든 RTSP 채널의 실제 프레임 검사 주기 | | `RTSP` | `HealthCheckIntervalSec` | `10` | 모든 RTSP 채널의 실제 프레임 검사 주기 |
| `RTSP` | `HealthTimeoutSec` | `5` | 새 수신기가 영상 1프레임을 디코딩할 제한 시간 | | `RTSP` | `HealthTimeoutSec` | `5` | 새 수신기가 영상 1프레임을 디코딩할 제한 시간 |
| `RTSP` | `HealthFailureThreshold` | `2` | 전체 AV 재시작 전 허용할 연속 검사 실패 횟수 | | `RTSP` | `HealthFailureThreshold` | `2` | 전체 AV 재시작 전 허용할 연속 검사 실패 횟수 |
| `RTSP` | `StartupGraceSec` | `45` | FFmpeg 시작 후 최초 프레임 검사까지의 준비 유예시간 |
| `Dependencies` | `AutoInstall` | `1` | 실행 파일 부재 시 자동 설치 여부 | | `Dependencies` | `AutoInstall` | `1` | 실행 파일 부재 시 자동 설치 여부 |
| `Dependencies` | `FFmpegUrl` | Gyan essentials ZIP | FFmpeg Windows ZIP 다운로드 주소 | | `Dependencies` | `FFmpegUrl` | Gyan essentials ZIP | FFmpeg Windows ZIP 다운로드 주소 |
| `Dependencies` | `FFmpegSha256Url` | Gyan SHA-256 | FFmpeg 게시 해시 주소 | | `Dependencies` | `FFmpegSha256Url` | Gyan SHA-256 | FFmpeg 게시 해시 주소 |
@@ -252,7 +253,7 @@ Start-ScheduledTask -TaskName "Screen RTSP Watchdog"
Watchdog 내부에서는 Windows `PowerModeChanged/Resume` 이벤트를 감시하고, 이벤트 누락에 대비해 polling 시간 공백도 함께 확인합니다. 절전 해제가 감지되면 단일 FFmpeg AV 파이프라인을 종료하고 모니터·NIC·오디오·GPU 구성을 다시 읽은 뒤 안정화 시간 이후 재시작합니다. 모니터 분리 시에는 채널을 종료하고 계속 대기하며, 필요한 수만큼 다시 연결되면 동일하게 자동 복구합니다. Watchdog 내부에서는 Windows `PowerModeChanged/Resume` 이벤트를 감시하고, 이벤트 누락에 대비해 polling 시간 공백도 함께 확인합니다. 절전 해제가 감지되면 단일 FFmpeg AV 파이프라인을 종료하고 모니터·NIC·오디오·GPU 구성을 다시 읽은 뒤 안정화 시간 이후 재시작합니다. 모니터 분리 시에는 채널을 종료하고 계속 대기하며, 필요한 수만큼 다시 연결되면 동일하게 자동 복구합니다.
프로세스 생존 여부와 실제 송출 상태는 별도로 검사합니다. `HealthCheckIntervalSec`마다 모든 `monitorN` URL에 숨김 로컬 FFmpeg 수신기를 동시에 연결하고, `HealthTimeoutSec` 안에 각 채널에서 영상 1프레임을 디코딩해야 정상으로 판정합니다. `HealthFailureThreshold`회 연속 실패하면 FFmpeg PID가 남아 있어도 전체 AV 파이프라인을 강제로 정리하고 3초 안정화 후 다시 만듭니다. 재시작 횟수 제한은 없습니다. 프로세스 생존 여부와 실제 송출 상태는 별도로 검사합니다. 새 FFmpeg를 시작하면 장치와 RTSP Publisher가 준비되도록 `StartupGraceSec` 동안 검사를 유예합니다. 이후 `HealthCheckIntervalSec`마다 모든 `monitorN` URL에 숨김 로컬 FFmpeg 수신기를 동시에 연결하고, `HealthTimeoutSec` 안에 각 채널에서 영상 1프레임을 디코딩해야 정상으로 판정합니다. `HealthFailureThreshold`회 연속 실패하면 FFmpeg PID가 남아 있어도 프로세스별 상태와 로그 끝부분을 기록하고 전체 AV 파이프라인을 강제로 정리한 뒤 다시 만듭니다. 재시작 횟수 제한은 없습니다.
등록된 작업은 `config.ini` 경로만 전달하므로 설정 파일을 수정한 뒤 작업을 다시 시작하면 변경값이 적용됩니다. 기존 작업이 등록돼 있다면 업데이트된 복구 설정을 적용하기 위해 `-InstallStartup`을 다시 실행해야 합니다. 등록된 작업은 `config.ini` 경로만 전달하므로 설정 파일을 수정한 뒤 작업을 다시 시작하면 변경값이 적용됩니다. 기존 작업이 등록돼 있다면 업데이트된 복구 설정을 적용하기 위해 `-InstallStartup`을 다시 실행해야 합니다.
+5
View File
@@ -90,6 +90,7 @@ StableTimeSec=3
PollIntervalSec=1 PollIntervalSec=1
[Video] [Video]
# 화면 캡처 및 RTSP 영상 프레임률입니다. 1~240 범위에서 설정합니다.
FrameRate=30 FrameRate=30
Bitrate=8000k Bitrate=8000k
@@ -128,6 +129,10 @@ HealthTimeoutSec=5
# 연속 실패가 이 횟수에 도달하면 전체 AV 파이프라인을 재시작합니다. # 연속 실패가 이 횟수에 도달하면 전체 AV 파이프라인을 재시작합니다.
HealthFailureThreshold=2 HealthFailureThreshold=2
# FFmpeg를 시작한 직후 장치와 RTSP Publisher가 준비될 때까지 검사를 유예합니다.
# 장치 초기화가 느린 PC에서 너무 짧게 설정하면 정상 시작 중 재시작 루프가 발생할 수 있습니다.
StartupGraceSec=45
[Dependencies] [Dependencies]
# ffmpeg.exe 또는 mediamtx.exe가 없을 때 자동 다운로드: 1=사용, 0=오류 후 종료 # ffmpeg.exe 또는 mediamtx.exe가 없을 때 자동 다운로드: 1=사용, 0=오류 후 종료
AutoInstall=1 AutoInstall=1
+180 -8
View File
@@ -50,6 +50,9 @@ param(
[ValidateRange(1, 10)] [ValidateRange(1, 10)]
[int]$RtspHealthFailureThreshold = 2, [int]$RtspHealthFailureThreshold = 2,
[ValidateRange(0, 600)]
[int]$RtspHealthStartupGraceSec = 45,
[ValidateSet(0, 1)] [ValidateSet(0, 1)]
[int]$AutoInstallDependencies = 1, [int]$AutoInstallDependencies = 1,
@@ -714,7 +717,8 @@ function Assert-IniSettings {
"Port", "Port",
"HealthCheckIntervalSec", "HealthCheckIntervalSec",
"HealthTimeoutSec", "HealthTimeoutSec",
"HealthFailureThreshold" "HealthFailureThreshold",
"StartupGraceSec"
) )
Dependencies = @( Dependencies = @(
"AutoInstall", "AutoInstall",
@@ -831,6 +835,10 @@ if ($ConfigWasLoaded) {
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("RtspHealthFailureThreshold")) { if ($setting.Found -and -not $ExplicitParameters.ContainsKey("RtspHealthFailureThreshold")) {
$RtspHealthFailureThreshold = [int]$setting.Value $RtspHealthFailureThreshold = [int]$setting.Value
} }
$setting = Get-IniSetting $ini "RTSP" "StartupGraceSec"
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("RtspHealthStartupGraceSec")) {
$RtspHealthStartupGraceSec = [int]$setting.Value
}
$setting = Get-IniSetting $ini "Dependencies" "AutoInstall" $setting = Get-IniSetting $ini "Dependencies" "AutoInstall"
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("AutoInstallDependencies")) { if ($setting.Found -and -not $ExplicitParameters.ContainsKey("AutoInstallDependencies")) {
@@ -862,6 +870,39 @@ if ($ConfigWasLoaded) {
} }
} }
function Assert-IntegerRange {
param(
[string]$Name,
[int]$Value,
[int]$Minimum,
[int]$Maximum
)
if ($Value -lt $Minimum -or $Value -gt $Maximum) {
throw "$Name 값은 $Minimum 이상 $Maximum 이하여야 합니다: $Value"
}
}
# Parameter validation runs before config.ini is loaded. Validate the final
# effective values again so an invalid INI value cannot bypass ValidateRange.
Assert-IntegerRange "General.ChannelCount" $ChannelCount 1 16
Assert-IntegerRange "General.StableTimeSec" $StableTimeSec 1 60
Assert-IntegerRange "General.PollIntervalSec" $PollIntervalSec 1 60
Assert-IntegerRange "Video.FrameRate" $FrameRate 1 240
Assert-IntegerRange "RTSP.Port" $RtspPort 1 65535
Assert-IntegerRange "RTSP.HealthCheckIntervalSec" $RtspHealthCheckIntervalSec 1 3600
Assert-IntegerRange "RTSP.HealthTimeoutSec" $RtspHealthTimeoutSec 1 60
Assert-IntegerRange "RTSP.HealthFailureThreshold" $RtspHealthFailureThreshold 1 10
Assert-IntegerRange "RTSP.StartupGraceSec" $RtspHealthStartupGraceSec 0 600
if ($VideoBitrate -notmatch '^[0-9]+[kKmM]$') {
throw "Video.Bitrate 형식이 잘못되었습니다: $VideoBitrate"
}
if ($AudioBitrate -notmatch '^[0-9]+[kKmM]$') {
throw "Audio.Bitrate 형식이 잘못되었습니다: $AudioBitrate"
}
if ($AudioSignalThreshold -lt 0.0 -or $AudioSignalThreshold -gt 1.0) {
throw "Audio.SignalThreshold 값은 0.0 이상 1.0 이하여야 합니다: $AudioSignalThreshold"
}
if (-not [string]::IsNullOrWhiteSpace($EncoderPriorityCsv)) { if (-not [string]::IsNullOrWhiteSpace($EncoderPriorityCsv)) {
$EncoderPriority = @($EncoderPriorityCsv -split ',' | ForEach-Object { $_.Trim() }) $EncoderPriority = @($EncoderPriorityCsv -split ',' | ForEach-Object { $_.Trim() })
} }
@@ -1033,6 +1074,7 @@ function Get-HiddenWorkerArgumentList {
"-RtspHealthCheckIntervalSec", [string]$RtspHealthCheckIntervalSec, "-RtspHealthCheckIntervalSec", [string]$RtspHealthCheckIntervalSec,
"-RtspHealthTimeoutSec", [string]$RtspHealthTimeoutSec, "-RtspHealthTimeoutSec", [string]$RtspHealthTimeoutSec,
"-RtspHealthFailureThreshold", [string]$RtspHealthFailureThreshold, "-RtspHealthFailureThreshold", [string]$RtspHealthFailureThreshold,
"-RtspHealthStartupGraceSec", [string]$RtspHealthStartupGraceSec,
"-AutoInstallDependencies", [string]$AutoInstallDependencies, "-AutoInstallDependencies", [string]$AutoInstallDependencies,
"-FFmpegDownloadUrl", $FFmpegDownloadUrl, "-FFmpegDownloadUrl", $FFmpegDownloadUrl,
"-FFmpegSha256Url", $FFmpegSha256Url, "-FFmpegSha256Url", $FFmpegSha256Url,
@@ -1123,6 +1165,7 @@ if ($ShowConfiguration) {
RtspHealthCheckIntervalSec = $RtspHealthCheckIntervalSec RtspHealthCheckIntervalSec = $RtspHealthCheckIntervalSec
RtspHealthTimeoutSec = $RtspHealthTimeoutSec RtspHealthTimeoutSec = $RtspHealthTimeoutSec
RtspHealthFailureThreshold = $RtspHealthFailureThreshold RtspHealthFailureThreshold = $RtspHealthFailureThreshold
RtspHealthStartupGraceSec = $RtspHealthStartupGraceSec
AutoInstallDependencies = $AutoInstallDependencies AutoInstallDependencies = $AutoInstallDependencies
FFmpegDownloadUrl = $FFmpegDownloadUrl FFmpegDownloadUrl = $FFmpegDownloadUrl
FFmpegSha256Url = $FFmpegSha256Url FFmpegSha256Url = $FFmpegSha256Url
@@ -2733,6 +2776,7 @@ $script:StreamProcesses = @{}
$script:AudioProcesses = @{} $script:AudioProcesses = @{}
$script:LastRtspHealthCheck = [DateTime]::MinValue $script:LastRtspHealthCheck = [DateTime]::MinValue
$script:RtspHealthFailures = 0 $script:RtspHealthFailures = 0
$script:StreamStartedAt = [DateTime]::MinValue
function Test-ProcessRunning { function Test-ProcessRunning {
param($Process) param($Process)
@@ -2786,6 +2830,79 @@ function Stop-Streams {
$script:AudioProcesses = @{} $script:AudioProcesses = @{}
$script:LastRtspHealthCheck = [DateTime]::MinValue $script:LastRtspHealthCheck = [DateTime]::MinValue
$script:RtspHealthFailures = 0 $script:RtspHealthFailures = 0
$script:StreamStartedAt = [DateTime]::MinValue
}
function Get-StreamProcessDiagnosticLines {
$lines = New-Object System.Collections.Generic.List[string]
foreach ($group in @(
[PSCustomObject]@{Name="FFmpeg"; Processes=$script:StreamProcesses},
[PSCustomObject]@{Name="Audio"; Processes=$script:AudioProcesses}
)) {
foreach ($entry in $group.Processes.GetEnumerator()) {
$process = $entry.Value
try {
$process.Refresh()
if ($process.HasExited) {
$lines.Add("$($group.Name)/$($entry.Key): PID=$($process.Id), 종료, ExitCode=$($process.ExitCode)")
}
else {
$lines.Add("$($group.Name)/$($entry.Key): PID=$($process.Id), 실행 중")
}
}
catch {
$lines.Add("$($group.Name)/$($entry.Key): 상태 확인 실패 - $($_.Exception.Message)")
}
}
}
return @($lines)
}
function Write-StreamFailureDiagnostics {
param([string]$Reason)
Write-Log "AV 파이프라인 진단: $Reason"
foreach ($line in @(Get-StreamProcessDiagnosticLines)) {
Write-Log "프로세스: $line"
}
foreach ($path in @(
(Join-Path $LogDir "ffmpeg.log"),
(Join-Path $LogDir "audio-loopback.log")
)) {
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { continue }
$tail = @(Get-Content -LiteralPath $path -Tail 8 -ErrorAction SilentlyContinue)
foreach ($line in $tail) {
if (-not [string]::IsNullOrWhiteSpace($line)) {
Write-Log "$([System.IO.Path]::GetFileName($path)): $line"
}
}
}
}
function Stop-OrphanedPipelineProcesses {
$scriptPath = [System.IO.Path]::GetFullPath($PSCommandPath)
$candidates = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object { $_.Name -in @("ffmpeg.exe", "powershell.exe") })
foreach ($candidate in $candidates) {
$owned = $false
if ($candidate.Name -eq "ffmpeg.exe" -and
-not [string]::IsNullOrWhiteSpace($candidate.ExecutablePath)) {
$owned = [string]::Equals(
[System.IO.Path]::GetFullPath($candidate.ExecutablePath),
[System.IO.Path]::GetFullPath($FFmpegPath),
[System.StringComparison]::OrdinalIgnoreCase
)
}
elseif ($candidate.Name -eq "powershell.exe" -and $candidate.CommandLine) {
$owned = ($candidate.CommandLine.IndexOf(
$scriptPath,
[System.StringComparison]::OrdinalIgnoreCase
) -ge 0 -and $candidate.CommandLine -match '(?i)-AudioWorker(?:\s|$)')
}
if (-not $owned -or $candidate.ProcessId -eq $PID) { continue }
Write-Log "이전 Watchdog의 잔여 프로세스 정리: $($candidate.Name) PID=$($candidate.ProcessId)"
Stop-Process -Id $candidate.ProcessId -Force -ErrorAction SilentlyContinue
}
} }
function Get-RotationFilter { function Get-RotationFilter {
@@ -2988,6 +3105,7 @@ function Start-Streams {
$script:LastRtspHealthCheck = Get-Date $script:LastRtspHealthCheck = Get-Date
$script:RtspHealthFailures = 0 $script:RtspHealthFailures = 0
$script:StreamStartedAt = Get-Date
} }
function Test-StreamsRunning { function Test-StreamsRunning {
@@ -3002,6 +3120,18 @@ function Test-StreamsRunning {
return $true return $true
} }
function Test-RtspHealthCheckDue {
param([datetime]$Now = (Get-Date))
if (-not (Test-StreamsRunning)) { return $false }
if ($script:StreamStartedAt -eq [DateTime]::MinValue) { return $false }
if (($Now - $script:StreamStartedAt).TotalSeconds -lt $RtspHealthStartupGraceSec) {
return $false
}
return (($Now - $script:LastRtspHealthCheck).TotalSeconds -ge
$RtspHealthCheckIntervalSec)
}
function New-RtspHealthProbeArguments { function New-RtspHealthProbeArguments {
param([int]$Channel) param([int]$Channel)
@@ -3217,14 +3347,16 @@ function Invoke-SimulationTests {
Invoke-SimulationCase "RTSP 실제 프레임 검사 설정 파싱" { Invoke-SimulationCase "RTSP 실제 프레임 검사 설정 파싱" {
$path = New-SimulationIni ( $path = New-SimulationIni (
"[RTSP]`r`nPort=8554`r`nHealthCheckIntervalSec=10`r`n" + "[RTSP]`r`nPort=8554`r`nHealthCheckIntervalSec=10`r`n" +
"HealthTimeoutSec=5`r`nHealthFailureThreshold=2`r`n" "HealthTimeoutSec=5`r`nHealthFailureThreshold=2`r`n" +
"StartupGraceSec=45`r`n"
) )
try { try {
$parsed = Read-IniConfiguration $path $parsed = Read-IniConfiguration $path
Assert-IniSettings $parsed Assert-IniSettings $parsed
return ($parsed.RTSP.HealthCheckIntervalSec -eq "10" -and return ($parsed.RTSP.HealthCheckIntervalSec -eq "10" -and
$parsed.RTSP.HealthTimeoutSec -eq "5" -and $parsed.RTSP.HealthTimeoutSec -eq "5" -and
$parsed.RTSP.HealthFailureThreshold -eq "2") $parsed.RTSP.HealthFailureThreshold -eq "2" -and
$parsed.RTSP.StartupGraceSec -eq "45")
} }
finally { [System.IO.File]::Delete($path) } finally { [System.IO.File]::Delete($path) }
} }
@@ -3538,6 +3670,7 @@ function Invoke-SimulationTests {
$arguments -match '-RtspHealthCheckIntervalSec\s+10' -and $arguments -match '-RtspHealthCheckIntervalSec\s+10' -and
$arguments -match '-RtspHealthTimeoutSec\s+5' -and $arguments -match '-RtspHealthTimeoutSec\s+5' -and
$arguments -match '-RtspHealthFailureThreshold\s+2' -and $arguments -match '-RtspHealthFailureThreshold\s+2' -and
$arguments -match '-RtspHealthStartupGraceSec\s+45' -and
$arguments -match '-WatchdogWorker') $arguments -match '-WatchdogWorker')
} }
@@ -3637,6 +3770,42 @@ function Invoke-SimulationTests {
$arguments -contains "$RTSPPublishBase/monitor2") $arguments -contains "$RTSPPublishBase/monitor2")
} }
Invoke-SimulationCase "RTSP 시작 유예 중에는 프레임 검사를 실행하지 않음" {
$savedAudioEnabled = $script:AudioEnabled
$savedStreamProcesses = $script:StreamProcesses
$savedAudioProcesses = $script:AudioProcesses
$savedStartedAt = $script:StreamStartedAt
$savedLastCheck = $script:LastRtspHealthCheck
try {
$now = Get-Date
$alive = [PSCustomObject]@{HasExited=$false}
$script:AudioEnabled = 0
$script:StreamProcesses = @{pipeline=$alive}
$script:AudioProcesses = @{}
$script:LastRtspHealthCheck = $now.AddHours(-1)
$script:StreamStartedAt = $now
$blockedDuringGrace = -not (Test-RtspHealthCheckDue -Now $now)
$script:StreamStartedAt = $now.AddSeconds(-($RtspHealthStartupGraceSec + 1))
$allowedAfterGrace = Test-RtspHealthCheckDue -Now $now
return ($blockedDuringGrace -and $allowedAfterGrace)
}
finally {
$script:AudioEnabled = $savedAudioEnabled
$script:StreamProcesses = $savedStreamProcesses
$script:AudioProcesses = $savedAudioProcesses
$script:StreamStartedAt = $savedStartedAt
$script:LastRtspHealthCheck = $savedLastCheck
}
}
Invoke-SimulationCase "INI FPS 허용 범위 밖 값 거부" {
try {
Assert-IntegerRange "Video.FrameRate" 0 1 240
return $false
}
catch { return $true }
}
Invoke-SimulationCase "비 HTTPS 의존성 URL 거부" { Invoke-SimulationCase "비 HTTPS 의존성 URL 거부" {
try { Assert-HttpsDependencyUrl "http://example.com/file.zip" "test"; return $false } try { Assert-HttpsDependencyUrl "http://example.com/file.zip" "test"; return $false }
catch { return $true } catch { return $true }
@@ -3751,6 +3920,8 @@ try {
$hasMutex = $mutex.WaitOne(0, $false) $hasMutex = $mutex.WaitOne(0, $false)
if (-not $hasMutex) { throw "Screen RTSP Watchdog가 이미 실행 중입니다." } if (-not $hasMutex) { throw "Screen RTSP Watchdog가 이미 실행 중입니다." }
Stop-OrphanedPipelineProcesses
Register-ObjectEvent -InputObject ([Microsoft.Win32.SystemEvents]) ` Register-ObjectEvent -InputObject ([Microsoft.Win32.SystemEvents]) `
-EventName PowerModeChanged -SourceIdentifier $powerEventSourceIdentifier | Out-Null -EventName PowerModeChanged -SourceIdentifier $powerEventSourceIdentifier | Out-Null
$powerEventRegistered = $true $powerEventRegistered = $true
@@ -3791,10 +3962,11 @@ try {
else { Write-Log "NIC 선택: IPv4 기본 경로 자동 감지" } else { Write-Log "NIC 선택: IPv4 기본 경로 자동 감지" }
if ($HealthTarget) { Write-Log "통신 확인 대상: $HealthTarget" } if ($HealthTarget) { Write-Log "통신 확인 대상: $HealthTarget" }
Write-Log ( Write-Log (
"RTSP 프레임 검사: ${RtspHealthCheckIntervalSec}초 주기 / " + "RTSP 프레임 검사: 시작 후 ${RtspHealthStartupGraceSec}초 유예 / " +
"${RtspHealthTimeoutSec}초 제한 / " + "${RtspHealthCheckIntervalSec}초 주기 / ${RtspHealthTimeoutSec}초 제한 / " +
"${RtspHealthFailureThreshold}회 연속 실패 시 재시작" "${RtspHealthFailureThreshold}회 연속 실패 시 재시작"
) )
Write-Log "영상 설정: ${FrameRate}fps / 채널당 $VideoBitrate"
$lastSystemSignature = "" $lastSystemSignature = ""
$healthySince = $null $healthySince = $null
@@ -3905,6 +4077,7 @@ try {
if (($StreamProcesses.Count -gt 0 -or $AudioProcesses.Count -gt 0) -and if (($StreamProcesses.Count -gt 0 -or $AudioProcesses.Count -gt 0) -and
-not (Test-StreamsRunning)) { -not (Test-StreamsRunning)) {
Write-Log "FFmpeg 또는 보조 프로세스 비정상 종료 감지" Write-Log "FFmpeg 또는 보조 프로세스 비정상 종료 감지"
Write-StreamFailureDiagnostics "프로세스 종료 감지"
Stop-Streams Stop-Streams
$healthySince = $null $healthySince = $null
} }
@@ -3912,9 +4085,7 @@ try {
# A live FFmpeg PID does not prove that DDA and both RTSP outputs # A live FFmpeg PID does not prove that DDA and both RTSP outputs
# are progressing. A new local receiver must decode one frame from # are progressing. A new local receiver must decode one frame from
# every channel within the configured deadline. # every channel within the configured deadline.
if ((Test-StreamsRunning) -and if (Test-RtspHealthCheckDue) {
((Get-Date) - $script:LastRtspHealthCheck).TotalSeconds -ge
$RtspHealthCheckIntervalSec) {
$script:LastRtspHealthCheck = Get-Date $script:LastRtspHealthCheck = Get-Date
$rtspHealth = Test-RtspStreamFrames $rtspHealth = Test-RtspStreamFrames
@@ -3934,6 +4105,7 @@ try {
if ($script:RtspHealthFailures -ge $RtspHealthFailureThreshold) { if ($script:RtspHealthFailures -ge $RtspHealthFailureThreshold) {
Write-Log "RTSP 송출 정지 감지 - 전체 AV 파이프라인 재시작" Write-Log "RTSP 송출 정지 감지 - 전체 AV 파이프라인 재시작"
Write-StreamFailureDiagnostics $rtspHealth.Reason
Stop-Streams Stop-Streams
$healthySince = $null $healthySince = $null
} }