Add dependency bootstrap and simulation tests

This commit is contained in:
seo-amugae committed 2026-10-08 00:46:26 +09:00
1 parent 089c2421c5
commit 4088f2299d
3 files changed
+580 -15

No files matched your search

+526 -13
View File
@@ -38,12 +38,22 @@ param(
[ValidateRange(1, 65535)]
[int]$RtspPort = 8554,
[ValidateSet(0, 1)]
[int]$AutoInstallDependencies = 1,
[string]$FFmpegDownloadUrl = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip",
[string]$FFmpegSha256Url = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip.sha256",
[string]$MediaMTXDownloadUrl = "https://github.com/bluenviron/mediamtx/releases/download/v1.21.1/mediamtx_v1.21.1_windows_amd64.zip",
[string]$MediaMTXSha256 = "faa97974861eb75a68b5aa326c78e7e7a6f670b5ef191bace78e715130381f23",
[string]$ConfigPath = "",
[switch]$InstallStartup,
[switch]$UninstallStartup,
[switch]$StartupStatus,
[switch]$ShowConfiguration,
[switch]$InstallDependenciesOnly,
[switch]$RunSimulationTests,
[Parameter(DontShow = $true)]
[switch]$AudioWorker,
@@ -669,6 +679,13 @@ function Assert-IniSettings {
Audio = @("Enabled", "Bitrate", "SignalThreshold")
Network = @("NicDescription", "HealthTarget")
RTSP = @("Port")
Dependencies = @(
"AutoInstall",
"FFmpegUrl",
"FFmpegSha256Url",
"MediaMTXUrl",
"MediaMTXSha256"
)
}
foreach ($sectionName in $Configuration.Keys) {
if (-not $allowed.ContainsKey($sectionName)) {
@@ -759,6 +776,27 @@ if ($ConfigWasLoaded) {
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("RtspPort")) {
$RtspPort = [int]$setting.Value
}
$setting = Get-IniSetting $ini "Dependencies" "AutoInstall"
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("AutoInstallDependencies")) {
$AutoInstallDependencies = Convert-IniSwitch $setting.Value "Dependencies.AutoInstall"
}
$setting = Get-IniSetting $ini "Dependencies" "FFmpegUrl"
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("FFmpegDownloadUrl")) {
$FFmpegDownloadUrl = [string]$setting.Value
}
$setting = Get-IniSetting $ini "Dependencies" "FFmpegSha256Url"
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("FFmpegSha256Url")) {
$FFmpegSha256Url = [string]$setting.Value
}
$setting = Get-IniSetting $ini "Dependencies" "MediaMTXUrl"
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("MediaMTXDownloadUrl")) {
$MediaMTXDownloadUrl = [string]$setting.Value
}
$setting = Get-IniSetting $ini "Dependencies" "MediaMTXSha256"
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("MediaMTXSha256")) {
$MediaMTXSha256 = [string]$setting.Value
}
}
if (-not [string]::IsNullOrWhiteSpace($EncoderPriorityCsv)) {
@@ -832,6 +870,11 @@ if ($ShowConfiguration) {
NicDescription = $NicDescription
HealthTarget = $HealthTarget
RtspPort = $RtspPort
AutoInstallDependencies = $AutoInstallDependencies
FFmpegDownloadUrl = $FFmpegDownloadUrl
FFmpegSha256Url = $FFmpegSha256Url
MediaMTXDownloadUrl = $MediaMTXDownloadUrl
MediaMTXSha256 = $MediaMTXSha256
} | Format-List
exit 0
}
@@ -877,10 +920,150 @@ function Write-Log {
Add-Content -LiteralPath $WatchdogLog -Value $line -Encoding UTF8
}
function Assert-HttpsDependencyUrl {
param([string]$Url, [string]$Name)
$uri = $null
if (-not [Uri]::TryCreate($Url, [UriKind]::Absolute, [ref]$uri) -or
$uri.Scheme -ne "https") {
throw "$Name 다운로드 URL은 유효한 HTTPS 주소여야 합니다: $Url"
}
}
function New-DependencyTempDirectory {
$tempRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
$path = Join-Path $tempRoot ("screen-rtsp-dependencies-" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $path -Force | Out-Null
return $path
}
function Remove-DependencyTempDirectory {
param([string]$Path)
if ([string]::IsNullOrWhiteSpace($Path)) { return }
$tempRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()).TrimEnd('\') + '\'
$resolvedPath = [System.IO.Path]::GetFullPath($Path)
$leaf = Split-Path -Leaf $resolvedPath
if (-not $resolvedPath.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase) -or
$leaf -notlike "screen-rtsp-dependencies-*") {
throw "안전하지 않은 임시 폴더 삭제 요청을 거부했습니다: $resolvedPath"
}
if (Test-Path -LiteralPath $resolvedPath) {
Remove-Item -LiteralPath $resolvedPath -Recurse -Force
}
}
function Save-DependencyFile {
param([string]$Url, [string]$Destination, [string]$Name)
Assert-HttpsDependencyUrl -Url $Url -Name $Name
Write-Log "$Name 다운로드 시작: $Url"
$previousProgressPreference = $ProgressPreference
$ProgressPreference = "SilentlyContinue"
try {
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -UseBasicParsing -Uri $Url -OutFile $Destination -TimeoutSec 900
}
finally {
$ProgressPreference = $previousProgressPreference
}
if (-not (Test-Path -LiteralPath $Destination -PathType Leaf) -or
(Get-Item -LiteralPath $Destination).Length -le 0) {
throw "$Name 다운로드 결과가 비어 있습니다."
}
}
function Get-RemoteSha256 {
param([string]$Url, [string]$Name)
Assert-HttpsDependencyUrl -Url $Url -Name "$Name SHA-256"
$previousProgressPreference = $ProgressPreference
$ProgressPreference = "SilentlyContinue"
try {
$response = Invoke-WebRequest -UseBasicParsing -Uri $Url -TimeoutSec 60
}
finally {
$ProgressPreference = $previousProgressPreference
}
$match = [regex]::Match([string]$response.Content, '(?i)\b[0-9a-f]{64}\b')
if (-not $match.Success) {
throw "$Name SHA-256 응답에서 해시를 찾을 수 없습니다: $Url"
}
return $match.Value.ToLowerInvariant()
}
function Install-ZipDependency {
param(
[string]$Name,
[string]$DownloadUrl,
[string]$ExpectedSha256,
[string]$ExpectedSha256Url,
[string]$ExecutableName,
[string]$DestinationPath
)
$tempDirectory = New-DependencyTempDirectory
try {
$archivePath = Join-Path $tempDirectory "$Name.zip"
$extractPath = Join-Path $tempDirectory "extracted"
Save-DependencyFile -Url $DownloadUrl -Destination $archivePath -Name $Name
if (-not [string]::IsNullOrWhiteSpace($ExpectedSha256Url)) {
$ExpectedSha256 = Get-RemoteSha256 -Url $ExpectedSha256Url -Name $Name
}
if ($ExpectedSha256 -notmatch '(?i)^[0-9a-f]{64}$') {
throw "$Name SHA-256 값이 올바르지 않습니다."
}
$actualSha256 = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualSha256 -ne $ExpectedSha256.ToLowerInvariant()) {
throw "$Name SHA-256 검증 실패: expected=$ExpectedSha256 actual=$actualSha256"
}
Write-Log "$Name SHA-256 검증 성공"
Expand-Archive -LiteralPath $archivePath -DestinationPath $extractPath -Force
$candidate = Get-ChildItem -LiteralPath $extractPath -Recurse -File |
Where-Object Name -eq $ExecutableName |
Select-Object -First 1
if (-not $candidate) {
throw "$Name 압축 파일에서 $ExecutableName 파일을 찾을 수 없습니다."
}
Copy-Item -LiteralPath $candidate.FullName -Destination $DestinationPath -Force
Unblock-File -LiteralPath $DestinationPath -ErrorAction SilentlyContinue
if (-not (Test-Path -LiteralPath $DestinationPath -PathType Leaf)) {
throw "$Name 설치에 실패했습니다: $DestinationPath"
}
Write-Log "$Name 설치 완료: $DestinationPath"
}
finally {
Remove-DependencyTempDirectory -Path $tempDirectory
}
}
function Assert-RequiredFiles {
$missing = @()
if (-not (Test-Path -LiteralPath $FFmpegPath -PathType Leaf)) { $missing += "ffmpeg.exe" }
if (-not (Test-Path -LiteralPath $MediaMTXPath -PathType Leaf)) { $missing += "mediamtx.exe" }
if ($missing.Count -eq 0) { return }
if ($AutoInstallDependencies -ne 1) {
throw "필수 파일이 없고 자동 설치가 꺼져 있습니다: $($missing -join ', ')"
}
Write-Log "필수 프로그램 자동 설치 시작: $($missing -join ', ')"
if ($missing -contains "ffmpeg.exe") {
Install-ZipDependency -Name "FFmpeg" -DownloadUrl $FFmpegDownloadUrl `
-ExpectedSha256 "" -ExpectedSha256Url $FFmpegSha256Url `
-ExecutableName "ffmpeg.exe" -DestinationPath $FFmpegPath
}
if ($missing -contains "mediamtx.exe") {
Install-ZipDependency -Name "MediaMTX" -DownloadUrl $MediaMTXDownloadUrl `
-ExpectedSha256 $MediaMTXSha256 -ExpectedSha256Url "" `
-ExecutableName "mediamtx.exe" -DestinationPath $MediaMTXPath
}
foreach ($path in @($FFmpegPath, $MediaMTXPath)) {
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
throw "필수 파일을 찾을 수 없습니다: $path"
throw "자동 설치 후에도 필수 파일을 찾을 수 없습니다: $path"
}
}
}
@@ -937,6 +1120,20 @@ function Test-FFmpegCapabilities {
return $encoders
}
if ($InstallDependenciesOnly) {
Assert-RequiredFiles
$null = Test-FFmpegCapabilities
$ffmpegVersion = (& $FFmpegPath -hide_banner -version 2>&1 | Select-Object -First 1)
$mediaMTXVersion = (& $MediaMTXPath --version 2>&1 | Select-Object -First 1)
[PSCustomObject]@{
FFmpegPath = $FFmpegPath
FFmpegVersion = $ffmpegVersion
MediaMTXPath = $MediaMTXPath
MediaMTXVersion = $mediaMTXVersion
} | Format-List
exit 0
}
function Invoke-ProcessWithTimeout {
param(
[string]$FilePath,
@@ -1543,6 +1740,332 @@ function Test-StreamsRunning {
return $true
}
function Get-StreamingReadinessReason {
param($Screens, $Network, $Endpoint)
$capturableScreenCount = @($Screens | Where-Object { $_.OutputIndex -ge 0 }).Count
if ($Screens.Count -lt $ChannelCount) {
return "활성 모니터 $ChannelCount 개 미만"
}
if ($capturableScreenCount -lt $ChannelCount) {
return "기본 D3D11 adapter에서 캡처 가능한 모니터 $ChannelCount 개 미만"
}
$networkReason = Test-NetworkHealth $Network
if ($networkReason) { return $networkReason }
if ($AudioEnabled -eq 1 -and -not $Endpoint) {
return "활성 오디오 출력 장치 없음"
}
return $null
}
function Invoke-SimulationTests {
$results = New-Object 'System.Collections.Generic.List[object]'
function Invoke-SimulationCase {
param([string]$Name, [scriptblock]$Body)
try {
$passed = [bool](& $Body)
$detail = if ($passed) { "PASS" } else { "조건 결과가 False입니다." }
}
catch {
$passed = $false
$detail = $_.Exception.Message
}
$results.Add([PSCustomObject]@{
Number = $results.Count + 1
Name = $Name
Passed = $passed
Detail = $detail
})
}
function New-SimulationIni {
param([string]$Content)
$path = Join-Path ([System.IO.Path]::GetTempPath()) `
("screen-rtsp-simulation-" + [Guid]::NewGuid().ToString("N") + ".ini")
[System.IO.File]::WriteAllText($path, $Content, (New-Object System.Text.UTF8Encoding($false)))
return $path
}
$originalChannelCount = $script:ChannelCount
$originalSelectedAudioEndpointId = $script:SelectedAudioEndpointId
$originalHealthTarget = $script:HealthTarget
$originalAudioEnabled = $script:AudioEnabled
$originalStreamProcesses = $script:StreamProcesses
$originalAudioProcesses = $script:AudioProcesses
try {
$script:ChannelCount = 2
$script:HealthTarget = ""
Invoke-SimulationCase "INI 주석(#, ;, //)과 따옴표 값 파싱" {
$path = New-SimulationIni "# one`r`n; two`r`n// three`r`n[Audio]`r`nEnabled=0`r`nBitrate=`"128k`"`r`nSignalThreshold=0.1"
try {
$parsed = Read-IniConfiguration $path
return ($parsed.Audio.Enabled -eq "0" -and $parsed.Audio.Bitrate -eq "128k")
}
finally { [System.IO.File]::Delete($path) }
}
Invoke-SimulationCase "알 수 없는 INI 설정 거부" {
$path = New-SimulationIni "[Audio]`r`nUnknownOption=1"
try {
$parsed = Read-IniConfiguration $path
try { Assert-IniSettings $parsed; return $false } catch { return $true }
}
finally { [System.IO.File]::Delete($path) }
}
Invoke-SimulationCase "기능 스위치 1/0 허용" {
return ((Convert-IniSwitch "1" "test") -eq 1 -and
(Convert-IniSwitch "0" "test") -eq 0)
}
Invoke-SimulationCase "기능 스위치 2 거부" {
try { $null = Convert-IniSwitch "2" "test"; return $false } catch { return $true }
}
$screenPrimary = [PSCustomObject]@{
DeviceName="DISPLAY1"; Primary=$true; X=0; Y=0; Width=2560; Height=1440
Orientation=0; OutputIndex=0; DxgiRotation=1
}
$screenSecond = [PSCustomObject]@{
DeviceName="DISPLAY2"; Primary=$false; X=2560; Y=0; Width=1080; Height=1920
Orientation=3; OutputIndex=1; DxgiRotation=4
}
$screenThird = [PSCustomObject]@{
DeviceName="DISPLAY3"; Primary=$false; X=-1920; Y=0; Width=1920; Height=1080
Orientation=0; OutputIndex=2; DxgiRotation=1
}
Invoke-SimulationCase "모니터 1대이면 2채널 시작 보류" {
return (@(Select-StreamingScreens @($screenPrimary)).Count -eq 0)
}
Invoke-SimulationCase "모니터 2대이면 Primary가 monitor1" {
$selected = @(Select-StreamingScreens @($screenSecond,$screenPrimary))
return ($selected.Count -eq 2 -and $selected[0].DeviceName -eq "DISPLAY1")
}
Invoke-SimulationCase "모니터 3대여도 설정된 2대만 선택" {
$selected = @(Select-StreamingScreens @($screenPrimary,$screenSecond,$screenThird))
return ($selected.Count -eq 2 -and $selected[0].Primary -and
$selected[1].DeviceName -eq "DISPLAY3")
}
Invoke-SimulationCase "D3D11에 매핑되지 않은 모니터가 있으면 시작 보류" {
$unmapped = $screenSecond.PSObject.Copy()
$unmapped.OutputIndex = -1
$network = [PSCustomObject]@{
Adapter=[PSCustomObject]@{Status="Up"}
IPv4=[PSCustomObject]@{IPAddress="192.168.1.10"}
}
$script:AudioEnabled = 0
return ((Get-StreamingReadinessReason @($screenPrimary,$unmapped) $network $null) -like
"기본 D3D11 adapter*")
}
Invoke-SimulationCase "해상도 변경 signature 감지" {
$changed = $screenPrimary.PSObject.Copy()
$changed.Width = 1920
return ((Get-ScreenSignature @($screenPrimary)) -ne (Get-ScreenSignature @($changed)))
}
Invoke-SimulationCase "위치/Primary/회전 변경 signature 감지" {
$changed = $screenSecond.PSObject.Copy()
$changed.X = -1080
$changed.Primary = $true
$changed.Orientation = 1
return ((Get-ScreenSignature @($screenSecond)) -ne (Get-ScreenSignature @($changed)))
}
Invoke-SimulationCase "세로 모니터 회전 필터 선택" {
return ((Get-RotationFilter 1) -eq "transpose=clock" -and
(Get-RotationFilter 2) -eq "hflip,vflip" -and
(Get-RotationFilter 3) -eq "transpose=cclock" -and
$null -eq (Get-RotationFilter 0))
}
$defaultAudio = [PSCustomObject]@{
Id="default"; Name="Default"; IsDefault=$true; Peak=0.0
SampleRate=48000; Channels=2; BitsPerSample=32; SampleFormat="f32le"
}
$audibleAudio = [PSCustomObject]@{
Id="audible"; Name="HDMI"; IsDefault=$false; Peak=0.2
SampleRate=48000; Channels=2; BitsPerSample=32; SampleFormat="f32le"
}
Invoke-SimulationCase "실제 peak가 있는 오디오 장치 우선" {
$script:SelectedAudioEndpointId = $null
return ((Select-AudioEndpoint @($defaultAudio,$audibleAudio)).Id -eq "audible")
}
Invoke-SimulationCase "모든 장치 무음이면 기본 출력 선택" {
$script:SelectedAudioEndpointId = $null
$audibleAudio.Peak = 0.0
return ((Select-AudioEndpoint @($audibleAudio,$defaultAudio)).Id -eq "default")
}
Invoke-SimulationCase "현재 출력 장치에 신호가 있으면 선택 유지" {
$audibleAudio.Peak = 0.1
$defaultAudio.Peak = 0.3
$script:SelectedAudioEndpointId = "audible"
return ((Select-AudioEndpoint @($defaultAudio,$audibleAudio)).Id -eq "audible")
}
Invoke-SimulationCase "오디오 mix format 변경 signature 감지" {
$changedAudio = $defaultAudio.PSObject.Copy()
$changedAudio.SampleRate = 44100
return ((Get-AudioSignature $defaultAudio) -ne (Get-AudioSignature $changedAudio))
}
Invoke-SimulationCase "NIC 부재 signature" {
$network = [PSCustomObject]@{ Adapter=$null; IPv4=$null }
return ((Get-NetworkSignature $network) -eq "NIC_MISSING")
}
Invoke-SimulationCase "NIC IPv4 변경 signature 감지" {
$adapter = [PSCustomObject]@{
InterfaceGuid="guid"; ifIndex=8; Status="Up"; LinkSpeed="1 Gbps"
}
$network1 = [PSCustomObject]@{
Adapter=$adapter; IPv4=[PSCustomObject]@{IPAddress="192.168.50.34"}
}
$network2 = [PSCustomObject]@{
Adapter=$adapter; IPv4=[PSCustomObject]@{IPAddress="192.168.50.35"}
}
return ((Get-NetworkSignature $network1) -ne (Get-NetworkSignature $network2))
}
Invoke-SimulationCase "NIC가 없으면 송출 시작 보류" {
$script:AudioEnabled = 0
$network = [PSCustomObject]@{Adapter=$null; IPv4=$null}
return ((Get-StreamingReadinessReason @($screenPrimary,$screenSecond) $network $null) -eq
"활성 네트워크 어댑터 없음")
}
Invoke-SimulationCase "NIC Link Down이면 송출 시작 보류" {
$script:AudioEnabled = 0
$network = [PSCustomObject]@{
Adapter=[PSCustomObject]@{Status="Disconnected"};
IPv4=[PSCustomObject]@{IPAddress="192.168.1.10"}
}
return ((Get-StreamingReadinessReason @($screenPrimary,$screenSecond) $network $null) -eq
"네트워크 Link Down")
}
Invoke-SimulationCase "NIC IPv4가 없으면 송출 시작 보류" {
$script:AudioEnabled = 0
$network = [PSCustomObject]@{Adapter=[PSCustomObject]@{Status="Up"}; IPv4=$null}
return ((Get-StreamingReadinessReason @($screenPrimary,$screenSecond) $network $null) -eq
"네트워크 IPv4 없음")
}
Invoke-SimulationCase "오디오 사용 중 endpoint가 없으면 시작 보류" {
$script:AudioEnabled = 1
$network = [PSCustomObject]@{
Adapter=[PSCustomObject]@{Status="Up"};
IPv4=[PSCustomObject]@{IPAddress="192.168.1.10"}
}
return ((Get-StreamingReadinessReason @($screenPrimary,$screenSecond) $network $null) -eq
"활성 오디오 출력 장치 없음")
}
Invoke-SimulationCase "모니터/NIC/오디오가 정상이면 시작 가능" {
$script:AudioEnabled = 1
$network = [PSCustomObject]@{
Adapter=[PSCustomObject]@{Status="Up"};
IPv4=[PSCustomObject]@{IPAddress="192.168.1.10"}
}
return ($null -eq (Get-StreamingReadinessReason `
@($screenPrimary,$screenSecond) $network $defaultAudio))
}
Invoke-SimulationCase "GPU별 H.264 인코더 매핑" {
return ((Get-EncoderDefinition "NVIDIA").Name -eq "h264_nvenc" -and
(Get-EncoderDefinition "AMD").Name -eq "h264_amf" -and
(Get-EncoderDefinition "Intel").Name -eq "h264_qsv")
}
Invoke-SimulationCase "자동 실행은 config.ini 경로만 전달" {
$arguments = Get-StartupArgumentList
return ($arguments -match '-ConfigPath' -and $arguments -notmatch '-VideoBitrate')
}
Invoke-SimulationCase "2채널 FFmpeg/오디오 worker가 모두 생존" {
$alive = [PSCustomObject]@{HasExited=$false}
$script:AudioEnabled = 1
$script:StreamProcesses = @{monitor1=$alive; monitor2=$alive}
$script:AudioProcesses = @{monitor1=$alive; monitor2=$alive}
return (Test-StreamsRunning)
}
Invoke-SimulationCase "FFmpeg 하나가 종료되면 전체 재시작 필요" {
$alive = [PSCustomObject]@{HasExited=$false}
$dead = [PSCustomObject]@{HasExited=$true}
$script:AudioEnabled = 1
$script:StreamProcesses = @{monitor1=$alive; monitor2=$dead}
$script:AudioProcesses = @{monitor1=$alive; monitor2=$alive}
return (-not (Test-StreamsRunning))
}
Invoke-SimulationCase "오디오 OFF이면 worker 없이 2채널 정상" {
$alive = [PSCustomObject]@{HasExited=$false}
$script:AudioEnabled = 0
$script:StreamProcesses = @{monitor1=$alive; monitor2=$alive}
$script:AudioProcesses = @{}
return (Test-StreamsRunning)
}
Invoke-SimulationCase "비 HTTPS 의존성 URL 거부" {
try { Assert-HttpsDependencyUrl "http://example.com/file.zip" "test"; return $false }
catch { return $true }
}
Invoke-SimulationCase "자동 설치 OFF에서 파일 부재 오류" {
$savedFFmpegPath = $script:FFmpegPath
$savedMediaMTXPath = $script:MediaMTXPath
$savedAutoInstall = $script:AutoInstallDependencies
try {
$script:FFmpegPath = Join-Path $BaseDir "definitely-missing-ffmpeg.exe"
$script:MediaMTXPath = Join-Path $BaseDir "definitely-missing-mediamtx.exe"
$script:AutoInstallDependencies = 0
try { Assert-RequiredFiles; return $false } catch { return $true }
}
finally {
$script:FFmpegPath = $savedFFmpegPath
$script:MediaMTXPath = $savedMediaMTXPath
$script:AutoInstallDependencies = $savedAutoInstall
}
}
Invoke-SimulationCase "안전 범위 밖 임시 폴더 삭제 거부" {
try { Remove-DependencyTempDirectory ([System.IO.Path]::GetTempPath()); return $false }
catch { return $true }
}
}
finally {
$script:ChannelCount = $originalChannelCount
$script:SelectedAudioEndpointId = $originalSelectedAudioEndpointId
$script:HealthTarget = $originalHealthTarget
$script:AudioEnabled = $originalAudioEnabled
$script:StreamProcesses = $originalStreamProcesses
$script:AudioProcesses = $originalAudioProcesses
}
return $results.ToArray()
}
if ($RunSimulationTests) {
$simulationResults = @(Invoke-SimulationTests)
$simulationResults | Format-Table Number,Passed,Name,Detail -AutoSize
$passedCount = @($simulationResults | Where-Object Passed).Count
$failedCount = $simulationResults.Count - $passedCount
Write-Host "Simulation summary: total=$($simulationResults.Count), passed=$passedCount, failed=$failedCount"
if ($failedCount -gt 0) { exit 1 }
exit 0
}
$mutex = New-Object System.Threading.Mutex($false, "Local\ScreenRtspWatchdog")
$hasMutex = $false
@@ -1618,18 +2141,8 @@ try {
$healthySince = $null
}
$reason = $null
$capturableScreenCount = @($screens | Where-Object { $_.OutputIndex -ge 0 }).Count
if ($screens.Count -lt $ChannelCount) {
$reason = "활성 모니터 $ChannelCount 개 미만"
}
elseif ($capturableScreenCount -lt $ChannelCount) {
$reason = "기본 D3D11 adapter에서 캡처 가능한 모니터 $ChannelCount 개 미만"
}
if (-not $reason) { $reason = Test-NetworkHealth $network }
if (-not $reason -and $AudioEnabled -eq 1 -and -not $endpoint) {
$reason = "활성 오디오 출력 장치 없음"
}
$reason = Get-StreamingReadinessReason -Screens $screens -Network $network `
-Endpoint $endpoint
if ($reason) {
if ($reason -ne $lastReason) { Write-Log "송출 조건 불충족: $reason" }