Add INI configuration and optional audio capture
This commit is contained in:
1 parent
5b15b06570
commit
089c2421c5
4 files changed
+458
-41
No files matched your search
@@ -6,6 +6,7 @@
|
|||||||
/mediamtx.yml
|
/mediamtx.yml
|
||||||
/logs/
|
/logs/
|
||||||
/*.log
|
/*.log
|
||||||
|
/config.local.ini
|
||||||
|
|
||||||
# Local certificates and secrets.
|
# Local certificates and secrets.
|
||||||
/*.crt
|
/*.crt
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ RTSP는 `ddagrab` 기반 Desktop Duplication API로 화면과 마우스 커서
|
|||||||
- 기존 MediaMTX가 RTSP 포트를 사용 중이면 해당 프로세스 재사용
|
- 기존 MediaMTX가 RTSP 포트를 사용 중이면 해당 프로세스 재사용
|
||||||
- 자동 생성 MediaMTX 설정은 RTSP over TCP만 사용해 UDP RTP 포트 충돌 방지
|
- 자동 생성 MediaMTX 설정은 RTSP over TCP만 사용해 UDP RTP 포트 충돌 방지
|
||||||
- 로그인 시 자동 실행 작업 등록 및 제거
|
- 로그인 시 자동 실행 작업 등록 및 제거
|
||||||
|
- `config.ini` 기반 기능별 설정과 명령줄 override
|
||||||
|
- 오디오 캡처/AAC 트랙을 `1` 또는 `0`으로 활성화·비활성화
|
||||||
|
|
||||||
## RTSP URL
|
## RTSP URL
|
||||||
|
|
||||||
@@ -44,6 +46,7 @@ rtsp://<현재_NIC_IP>:8554/monitor2
|
|||||||
## 구성
|
## 구성
|
||||||
|
|
||||||
- `screen-rtsp.ps1`: 캡처, 오디오 loopback, GPU 선택, MediaMTX/FFmpeg watchdog
|
- `screen-rtsp.ps1`: 캡처, 오디오 loopback, GPU 선택, MediaMTX/FFmpeg watchdog
|
||||||
|
- `config.ini`: 영상, 오디오, 네트워크, RTSP 설정과 진단 명령 예시
|
||||||
- `ffmpeg.exe`: `ddagrab`, `h264_nvenc`, `h264_amf`, `h264_qsv`, `aac` 지원 FFmpeg
|
- `ffmpeg.exe`: `ddagrab`, `h264_nvenc`, `h264_amf`, `h264_qsv`, `aac` 지원 FFmpeg
|
||||||
- `mediamtx.exe`: RTSP 서버. 현재 검증 기준 버전은 v1.21.1
|
- `mediamtx.exe`: RTSP 서버. 현재 검증 기준 버전은 v1.21.1
|
||||||
- `mediamtx.yml`: 없으면 스크립트가 기본 설정을 자동 생성
|
- `mediamtx.yml`: 없으면 스크립트가 기본 설정을 자동 생성
|
||||||
@@ -54,6 +57,51 @@ rtsp://<현재_NIC_IP>:8554/monitor2
|
|||||||
|
|
||||||
실행 파일과 생성 파일은 저장소에 포함하지 않습니다. 배포할 때 `ffmpeg.exe`와 `mediamtx.exe`를 `screen-rtsp.ps1` 옆에 둡니다.
|
실행 파일과 생성 파일은 저장소에 포함하지 않습니다. 배포할 때 `ffmpeg.exe`와 `mediamtx.exe`를 `screen-rtsp.ps1` 옆에 둡니다.
|
||||||
|
|
||||||
|
## 설정 파일
|
||||||
|
|
||||||
|
기본 설정은 `screen-rtsp.ps1`과 같은 폴더의 `config.ini`에서 읽습니다.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Video]
|
||||||
|
FrameRate=30
|
||||||
|
Bitrate=8000k
|
||||||
|
EncoderPriority=NVIDIA,AMD,Intel
|
||||||
|
|
||||||
|
[Audio]
|
||||||
|
Enabled=1
|
||||||
|
Bitrate=192k
|
||||||
|
SignalThreshold=0.0005
|
||||||
|
```
|
||||||
|
|
||||||
|
`Audio.Enabled=1`이면 현재 재생 장치의 시스템 소리를 WASAPI loopback으로 캡처해 AAC 트랙을 추가합니다. `0`이면 오디오 장치 감시, loopback worker, AAC 트랙을 모두 사용하지 않고 영상만 송출합니다.
|
||||||
|
|
||||||
|
줄 앞의 공백을 제외한 첫 문자가 `#`, `;`, `//`이면 설명이나 참고 명령으로 인식해 건너뜁니다. 값 오른쪽의 inline 주석은 지원하지 않으므로 설명은 별도 줄에 작성합니다. 명령줄에서 직접 지정한 값은 INI 값보다 우선합니다.
|
||||||
|
|
||||||
|
알 수 없는 섹션, 설정 이름, 범위를 벗어난 값, `0/1` 이외의 기능 스위치는 시작 전에 오류로 표시합니다.
|
||||||
|
|
||||||
|
| 섹션 | 설정 | 기본값 | 설명 |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `General` | `ChannelCount` | `2` | 송출 채널과 필요한 모니터 수 |
|
||||||
|
| `General` | `StableTimeSec` | `3` | 정상 상태 안정화 시간 |
|
||||||
|
| `General` | `PollIntervalSec` | `1` | 상태 확인 주기 |
|
||||||
|
| `Video` | `FrameRate` | `30` | 화면 프레임률 |
|
||||||
|
| `Video` | `Bitrate` | `8000k` | 채널별 영상 비트레이트 |
|
||||||
|
| `Video` | `EncoderPriority` | `NVIDIA,AMD,Intel` | GPU 선택 순서 |
|
||||||
|
| `Audio` | `Enabled` | `1` | 시스템 재생음과 AAC 트랙 사용 여부 |
|
||||||
|
| `Audio` | `Bitrate` | `192k` | AAC 비트레이트 |
|
||||||
|
| `Audio` | `SignalThreshold` | `0.0005` | 실제 재생 장치 판별 peak 기준 |
|
||||||
|
| `Network` | `NicDescription` | 빈 값 | 비우면 기본 경로 NIC 자동 선택 |
|
||||||
|
| `Network` | `HealthTarget` | 빈 값 | Source IP ping 대상 |
|
||||||
|
| `RTSP` | `Port` | `8554` | MediaMTX RTSP 포트 |
|
||||||
|
|
||||||
|
GPU, 모니터, 오디오, NIC, FFmpeg 지원, RTSP 포트를 확인하는 PowerShell 명령은 `config.ini` 위쪽에 모두 주석 형태로 포함되어 있습니다.
|
||||||
|
|
||||||
|
INI를 해석한 최종 설정값만 확인하고 종료할 수도 있습니다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\screen-rtsp.ps1 -ShowConfiguration
|
||||||
|
```
|
||||||
|
|
||||||
## 처리 흐름
|
## 처리 흐름
|
||||||
|
|
||||||
1. FFmpeg의 `ddagrab`과 H.264 하드웨어 인코더 포함 여부를 확인합니다.
|
1. FFmpeg의 `ddagrab`과 H.264 하드웨어 인코더 포함 여부를 확인합니다.
|
||||||
@@ -80,6 +128,14 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1
|
|||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -InstallStartup
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -InstallStartup
|
||||||
```
|
```
|
||||||
|
|
||||||
|
등록된 작업은 `config.ini` 경로만 전달하므로 설정 파일을 수정한 뒤 작업을 다시 시작하면 변경값이 적용됩니다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -StartupStatus
|
||||||
|
Stop-ScheduledTask -TaskName "Screen RTSP Watchdog"
|
||||||
|
Start-ScheduledTask -TaskName "Screen RTSP Watchdog"
|
||||||
|
```
|
||||||
|
|
||||||
자동 실행 제거:
|
자동 실행 제거:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -88,6 +144,18 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -Unins
|
|||||||
|
|
||||||
## 실행 옵션
|
## 실행 옵션
|
||||||
|
|
||||||
|
다른 설정 파일을 사용하는 예시:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\screen-rtsp.ps1 -ConfigPath C:\RTSP\config.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
명령줄 값은 `config.ini`보다 우선합니다. 다음 실행에서만 오디오를 끄는 예시:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\screen-rtsp.ps1 -AudioEnabled 0
|
||||||
|
```
|
||||||
|
|
||||||
GPU 우선순위를 바꾸는 예시:
|
GPU 우선순위를 바꾸는 예시:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
# ============================================================
|
||||||
|
# RTSP Watchdog 설정
|
||||||
|
# ============================================================
|
||||||
|
#
|
||||||
|
# 주석/설명 규칙
|
||||||
|
# - 줄의 첫 문자가 #, ;, // 중 하나이면 설명으로 처리하고 건너뜁니다.
|
||||||
|
# - 앞쪽 공백 뒤에 #, ;, //가 있어도 설명으로 처리합니다.
|
||||||
|
# - 값 오른쪽에는 주석을 붙이지 마십시오. 설명은 별도 줄에 작성합니다.
|
||||||
|
# - 경로와 장치명은 따옴표 없이 적어도 됩니다.
|
||||||
|
# - 기능 스위치는 1=사용, 0=사용 안 함입니다.
|
||||||
|
# - 명령줄에서 지정한 값은 이 파일보다 우선합니다.
|
||||||
|
; 세미콜론으로 시작하는 이 줄도 설명으로 처리됩니다.
|
||||||
|
// 슬래시 두 개로 시작하는 이 줄도 설명으로 처리됩니다.
|
||||||
|
#
|
||||||
|
# 실행
|
||||||
|
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1
|
||||||
|
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -ShowConfiguration
|
||||||
|
#
|
||||||
|
# 별도 설정 파일 사용
|
||||||
|
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -ConfigPath .\config.ini
|
||||||
|
#
|
||||||
|
# 자동 실행 등록 / 상태 확인 / 즉시 실행 / 제거
|
||||||
|
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -InstallStartup
|
||||||
|
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -StartupStatus
|
||||||
|
# Start-ScheduledTask -TaskName "Screen RTSP Watchdog"
|
||||||
|
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\screen-rtsp.ps1 -UninstallStartup
|
||||||
|
#
|
||||||
|
# GPU와 FFmpeg 지원 확인
|
||||||
|
# Get-CimInstance Win32_VideoController | Select-Object Name,DriverVersion,Status
|
||||||
|
# .\ffmpeg.exe -hide_banner -filters | Select-String ddagrab
|
||||||
|
# .\ffmpeg.exe -hide_banner -encoders | Select-String 'h264_(nvenc|amf|qsv)'
|
||||||
|
#
|
||||||
|
# 모니터 확인
|
||||||
|
# Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
# [System.Windows.Forms.Screen]::AllScreens | Select-Object DeviceName,Primary,Bounds
|
||||||
|
#
|
||||||
|
# 오디오 장치 확인
|
||||||
|
# Get-PnpDevice -Class AudioEndpoint | Select-Object Status,FriendlyName,InstanceId
|
||||||
|
# Get-CimInstance Win32_SoundDevice | Select-Object Name,Status,PNPDeviceID
|
||||||
|
#
|
||||||
|
# 네트워크 확인
|
||||||
|
# Get-NetAdapter | Select-Object Name,InterfaceDescription,ifIndex,Status,LinkSpeed
|
||||||
|
# Get-NetIPAddress -AddressFamily IPv4 | Select-Object InterfaceAlias,InterfaceIndex,IPAddress
|
||||||
|
# Get-NetRoute -AddressFamily IPv4 -DestinationPrefix 0.0.0.0/0
|
||||||
|
# ping.exe -S 192.168.50.34 192.168.50.1
|
||||||
|
#
|
||||||
|
# RTSP 포트와 수신 시험
|
||||||
|
# Get-NetTCPConnection -LocalPort 8554 -State Listen
|
||||||
|
# .\ffmpeg.exe -hide_banner -rtsp_transport tcp -i rtsp://127.0.0.1:8554/monitor1 -t 5 -f null NUL
|
||||||
|
|
||||||
|
[General]
|
||||||
|
# 생성할 monitor 채널 수. 이 수만큼 캡처 가능한 활성 모니터가 필요합니다.
|
||||||
|
ChannelCount=2
|
||||||
|
|
||||||
|
# 모든 조건이 정상인 상태로 유지되어야 하는 시간입니다.
|
||||||
|
StableTimeSec=3
|
||||||
|
|
||||||
|
# 모니터, NIC, GPU, 오디오 상태 확인 주기입니다.
|
||||||
|
PollIntervalSec=1
|
||||||
|
|
||||||
|
[Video]
|
||||||
|
FrameRate=30
|
||||||
|
Bitrate=8000k
|
||||||
|
|
||||||
|
# 앞에 있는 GPU부터 실제 1프레임 인코더 초기화를 시험합니다.
|
||||||
|
EncoderPriority=NVIDIA,AMD,Intel
|
||||||
|
|
||||||
|
[Audio]
|
||||||
|
# 시스템 재생음 녹음 및 RTSP AAC 트랙: 1=사용, 0=사용 안 함
|
||||||
|
Enabled=1
|
||||||
|
Bitrate=192k
|
||||||
|
|
||||||
|
# 실제 소리가 출력되는 장치를 판별할 peak 최소값입니다. 범위는 0.0~1.0입니다.
|
||||||
|
SignalThreshold=0.0005
|
||||||
|
|
||||||
|
[Network]
|
||||||
|
# 비워 두면 IPv4 기본 경로의 활성 NIC를 자동 선택합니다.
|
||||||
|
# 특정 장치를 강제하려면 Get-NetAdapter의 InterfaceDescription을 그대로 적습니다.
|
||||||
|
NicDescription=
|
||||||
|
|
||||||
|
# 비워 두면 Source IP ping 검사를 하지 않습니다.
|
||||||
|
# 예: 192.168.50.1
|
||||||
|
HealthTarget=
|
||||||
|
|
||||||
|
[RTSP]
|
||||||
|
Port=8554
|
||||||
+303
-41
@@ -20,6 +20,12 @@ param(
|
|||||||
[ValidatePattern("^[0-9]+[kKmM]$")]
|
[ValidatePattern("^[0-9]+[kKmM]$")]
|
||||||
[string]$AudioBitrate = "192k",
|
[string]$AudioBitrate = "192k",
|
||||||
|
|
||||||
|
[ValidateSet(0, 1)]
|
||||||
|
[int]$AudioEnabled = 1,
|
||||||
|
|
||||||
|
[ValidateRange(0.0, 1.0)]
|
||||||
|
[double]$AudioSignalThreshold = 0.0005,
|
||||||
|
|
||||||
[string]$NicDescription = "",
|
[string]$NicDescription = "",
|
||||||
[string]$HealthTarget = "",
|
[string]$HealthTarget = "",
|
||||||
|
|
||||||
@@ -32,8 +38,12 @@ param(
|
|||||||
[ValidateRange(1, 65535)]
|
[ValidateRange(1, 65535)]
|
||||||
[int]$RtspPort = 8554,
|
[int]$RtspPort = 8554,
|
||||||
|
|
||||||
|
[string]$ConfigPath = "",
|
||||||
|
|
||||||
[switch]$InstallStartup,
|
[switch]$InstallStartup,
|
||||||
[switch]$UninstallStartup,
|
[switch]$UninstallStartup,
|
||||||
|
[switch]$StartupStatus,
|
||||||
|
[switch]$ShowConfiguration,
|
||||||
|
|
||||||
[Parameter(DontShow = $true)]
|
[Parameter(DontShow = $true)]
|
||||||
[switch]$AudioWorker,
|
[switch]$AudioWorker,
|
||||||
@@ -46,13 +56,9 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ExplicitParameters = @{}
|
||||||
if (-not [string]::IsNullOrWhiteSpace($EncoderPriorityCsv)) {
|
foreach ($parameterName in $PSBoundParameters.Keys) {
|
||||||
$EncoderPriority = @($EncoderPriorityCsv -split ',' | ForEach-Object { $_.Trim() })
|
$ExplicitParameters[$parameterName] = $true
|
||||||
$invalidEncoderVendor = $EncoderPriority | Where-Object { $_ -notin @("NVIDIA", "AMD", "Intel") }
|
|
||||||
if ($invalidEncoderVendor) {
|
|
||||||
throw "잘못된 EncoderPriorityCsv 값: $($invalidEncoderVendor -join ', ')"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -485,6 +491,10 @@ namespace ScreenRtsp.Audio
|
|||||||
ThrowIfFailed(client.Start(), "IAudioClient.Start");
|
ThrowIfFailed(client.Start(), "IAudioClient.Start");
|
||||||
|
|
||||||
byte[] buffer = new byte[1024 * 1024];
|
byte[] buffer = new byte[1024 * 1024];
|
||||||
|
int silenceFrames = Math.Max(1, (int)(format.SamplesPerSec / 100));
|
||||||
|
int silenceBytes = checked(silenceFrames * format.BlockAlign);
|
||||||
|
System.Diagnostics.Stopwatch packetClock =
|
||||||
|
System.Diagnostics.Stopwatch.StartNew();
|
||||||
while (pipe.IsConnected)
|
while (pipe.IsConnected)
|
||||||
{
|
{
|
||||||
uint packetFrames;
|
uint packetFrames;
|
||||||
@@ -493,7 +503,17 @@ namespace ScreenRtsp.Audio
|
|||||||
|
|
||||||
if (packetFrames == 0)
|
if (packetFrames == 0)
|
||||||
{
|
{
|
||||||
Thread.Sleep(3);
|
// Some endpoints stop producing loopback packets while
|
||||||
|
// completely silent or power-saving. Keep FFmpeg and
|
||||||
|
// RTSP clocks moving with one 10 ms silent PCM block.
|
||||||
|
if (packetClock.ElapsedMilliseconds >= 10)
|
||||||
|
{
|
||||||
|
if (buffer.Length < silenceBytes) buffer = new byte[silenceBytes];
|
||||||
|
Array.Clear(buffer, 0, silenceBytes);
|
||||||
|
pipe.Write(buffer, 0, silenceBytes);
|
||||||
|
packetClock.Restart();
|
||||||
|
}
|
||||||
|
Thread.Sleep(2);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,6 +537,7 @@ namespace ScreenRtsp.Audio
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
pipe.Write(buffer, 0, bytes);
|
pipe.Write(buffer, 0, bytes);
|
||||||
|
packetClock.Restart();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -577,6 +598,180 @@ if ([string]::IsNullOrWhiteSpace($BaseDir)) {
|
|||||||
$BaseDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
$BaseDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Read-IniConfiguration {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
$result = @{}
|
||||||
|
$sectionName = "General"
|
||||||
|
$result[$sectionName] = @{}
|
||||||
|
$lineNumber = 0
|
||||||
|
|
||||||
|
foreach ($rawLine in (Get-Content -LiteralPath $Path)) {
|
||||||
|
$lineNumber++
|
||||||
|
$line = $rawLine.Trim()
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($line) -or
|
||||||
|
$line.StartsWith("#") -or
|
||||||
|
$line.StartsWith(";") -or
|
||||||
|
$line.StartsWith("//")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($line -match '^\[([^\]]+)\]$') {
|
||||||
|
$sectionName = $Matches[1].Trim()
|
||||||
|
if (-not $result.ContainsKey($sectionName)) {
|
||||||
|
$result[$sectionName] = @{}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($line -notmatch '^([^=]+)=(.*)$') {
|
||||||
|
throw "config.ini $lineNumber 번째 줄을 해석할 수 없습니다: $rawLine"
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $Matches[1].Trim()
|
||||||
|
$value = $Matches[2].Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($key)) {
|
||||||
|
throw "config.ini $lineNumber 번째 줄의 설정 이름이 비어 있습니다."
|
||||||
|
}
|
||||||
|
$hasDoubleQuotes = $value.StartsWith('"') -and $value.EndsWith('"')
|
||||||
|
$hasSingleQuotes = $value.StartsWith("'") -and $value.EndsWith("'")
|
||||||
|
if ($value.Length -ge 2 -and ($hasDoubleQuotes -or $hasSingleQuotes)) {
|
||||||
|
$value = $value.Substring(1, $value.Length - 2)
|
||||||
|
}
|
||||||
|
$result[$sectionName][$key] = $value
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-IniSetting {
|
||||||
|
param($Configuration, [string]$Section, [string]$Key)
|
||||||
|
if ($Configuration.ContainsKey($Section) -and $Configuration[$Section].ContainsKey($Key)) {
|
||||||
|
return [PSCustomObject]@{ Found = $true; Value = $Configuration[$Section][$Key] }
|
||||||
|
}
|
||||||
|
return [PSCustomObject]@{ Found = $false; Value = $null }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-IniSwitch {
|
||||||
|
param([string]$Value, [string]$Name)
|
||||||
|
if ($Value -notin @("0", "1")) {
|
||||||
|
throw "$Name 값은 1 또는 0이어야 합니다: $Value"
|
||||||
|
}
|
||||||
|
return [int]$Value
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-IniSettings {
|
||||||
|
param($Configuration)
|
||||||
|
$allowed = @{
|
||||||
|
General = @("ChannelCount", "StableTimeSec", "PollIntervalSec")
|
||||||
|
Video = @("FrameRate", "Bitrate", "EncoderPriority")
|
||||||
|
Audio = @("Enabled", "Bitrate", "SignalThreshold")
|
||||||
|
Network = @("NicDescription", "HealthTarget")
|
||||||
|
RTSP = @("Port")
|
||||||
|
}
|
||||||
|
foreach ($sectionName in $Configuration.Keys) {
|
||||||
|
if (-not $allowed.ContainsKey($sectionName)) {
|
||||||
|
throw "config.ini에 알 수 없는 섹션이 있습니다: [$sectionName]"
|
||||||
|
}
|
||||||
|
foreach ($key in $Configuration[$sectionName].Keys) {
|
||||||
|
if ($allowed[$sectionName] -notcontains $key) {
|
||||||
|
throw "config.ini에 알 수 없는 설정이 있습니다: [$sectionName] $key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ConfigPath)) {
|
||||||
|
$ConfigPath = Join-Path $BaseDir "config.ini"
|
||||||
|
}
|
||||||
|
elseif (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
|
||||||
|
$ConfigPath = Join-Path $BaseDir $ConfigPath
|
||||||
|
}
|
||||||
|
$ConfigPath = [System.IO.Path]::GetFullPath($ConfigPath)
|
||||||
|
|
||||||
|
$ConfigWasLoaded = $false
|
||||||
|
$ini = @{}
|
||||||
|
if (Test-Path -LiteralPath $ConfigPath -PathType Leaf) {
|
||||||
|
$ini = Read-IniConfiguration -Path $ConfigPath
|
||||||
|
Assert-IniSettings $ini
|
||||||
|
$ConfigWasLoaded = $true
|
||||||
|
}
|
||||||
|
elseif ($ExplicitParameters.ContainsKey("ConfigPath")) {
|
||||||
|
throw "지정한 설정 파일을 찾을 수 없습니다: $ConfigPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ConfigWasLoaded) {
|
||||||
|
$setting = Get-IniSetting $ini "General" "ChannelCount"
|
||||||
|
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("ChannelCount")) {
|
||||||
|
$ChannelCount = [int]$setting.Value
|
||||||
|
}
|
||||||
|
$setting = Get-IniSetting $ini "General" "StableTimeSec"
|
||||||
|
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("StableTimeSec")) {
|
||||||
|
$StableTimeSec = [int]$setting.Value
|
||||||
|
}
|
||||||
|
$setting = Get-IniSetting $ini "General" "PollIntervalSec"
|
||||||
|
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("PollIntervalSec")) {
|
||||||
|
$PollIntervalSec = [int]$setting.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting = Get-IniSetting $ini "Video" "FrameRate"
|
||||||
|
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("FrameRate")) {
|
||||||
|
$FrameRate = [int]$setting.Value
|
||||||
|
}
|
||||||
|
$setting = Get-IniSetting $ini "Video" "Bitrate"
|
||||||
|
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("VideoBitrate")) {
|
||||||
|
$VideoBitrate = [string]$setting.Value
|
||||||
|
}
|
||||||
|
$setting = Get-IniSetting $ini "Video" "EncoderPriority"
|
||||||
|
if ($setting.Found -and
|
||||||
|
-not $ExplicitParameters.ContainsKey("EncoderPriority") -and
|
||||||
|
-not $ExplicitParameters.ContainsKey("EncoderPriorityCsv")) {
|
||||||
|
$EncoderPriority = @($setting.Value -split ',' | ForEach-Object { $_.Trim() })
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting = Get-IniSetting $ini "Audio" "Enabled"
|
||||||
|
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("AudioEnabled")) {
|
||||||
|
$AudioEnabled = Convert-IniSwitch $setting.Value "Audio.Enabled"
|
||||||
|
}
|
||||||
|
$setting = Get-IniSetting $ini "Audio" "Bitrate"
|
||||||
|
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("AudioBitrate")) {
|
||||||
|
$AudioBitrate = [string]$setting.Value
|
||||||
|
}
|
||||||
|
$setting = Get-IniSetting $ini "Audio" "SignalThreshold"
|
||||||
|
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("AudioSignalThreshold")) {
|
||||||
|
$AudioSignalThreshold = [double]::Parse(
|
||||||
|
$setting.Value,
|
||||||
|
[System.Globalization.CultureInfo]::InvariantCulture
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting = Get-IniSetting $ini "Network" "NicDescription"
|
||||||
|
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("NicDescription")) {
|
||||||
|
$NicDescription = [string]$setting.Value
|
||||||
|
}
|
||||||
|
$setting = Get-IniSetting $ini "Network" "HealthTarget"
|
||||||
|
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("HealthTarget")) {
|
||||||
|
$HealthTarget = [string]$setting.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting = Get-IniSetting $ini "RTSP" "Port"
|
||||||
|
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("RtspPort")) {
|
||||||
|
$RtspPort = [int]$setting.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($EncoderPriorityCsv)) {
|
||||||
|
$EncoderPriority = @($EncoderPriorityCsv -split ',' | ForEach-Object { $_.Trim() })
|
||||||
|
}
|
||||||
|
$invalidEncoderVendor = $EncoderPriority | Where-Object { $_ -notin @("NVIDIA", "AMD", "Intel") }
|
||||||
|
if ($EncoderPriority.Count -eq 0) {
|
||||||
|
throw "EncoderPriority에는 하나 이상의 GPU 제조사가 필요합니다."
|
||||||
|
}
|
||||||
|
if ($invalidEncoderVendor) {
|
||||||
|
throw "잘못된 EncoderPriority 값: $($invalidEncoderVendor -join ', ')"
|
||||||
|
}
|
||||||
|
|
||||||
$FFmpegPath = Join-Path $BaseDir "ffmpeg.exe"
|
$FFmpegPath = Join-Path $BaseDir "ffmpeg.exe"
|
||||||
$MediaMTXPath = Join-Path $BaseDir "mediamtx.exe"
|
$MediaMTXPath = Join-Path $BaseDir "mediamtx.exe"
|
||||||
$MediaMTXConfig = Join-Path $BaseDir "mediamtx.yml"
|
$MediaMTXConfig = Join-Path $BaseDir "mediamtx.yml"
|
||||||
@@ -585,7 +780,6 @@ $WatchdogLog = Join-Path $LogDir "watchdog.log"
|
|||||||
$TaskName = "Screen RTSP Watchdog"
|
$TaskName = "Screen RTSP Watchdog"
|
||||||
$PowerShellPath = Join-Path $PSHOME "powershell.exe"
|
$PowerShellPath = Join-Path $PSHOME "powershell.exe"
|
||||||
$RTSPPublishBase = "rtsp://127.0.0.1:$RtspPort"
|
$RTSPPublishBase = "rtsp://127.0.0.1:$RtspPort"
|
||||||
$AudioSignalThreshold = 0.0005
|
|
||||||
|
|
||||||
function Quote-ProcessArgument {
|
function Quote-ProcessArgument {
|
||||||
param([string]$Value)
|
param([string]$Value)
|
||||||
@@ -602,21 +796,8 @@ function Join-ProcessArguments {
|
|||||||
function Get-StartupArgumentList {
|
function Get-StartupArgumentList {
|
||||||
$values = @(
|
$values = @(
|
||||||
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $PSCommandPath,
|
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $PSCommandPath,
|
||||||
"-ChannelCount", $ChannelCount,
|
"-ConfigPath", $ConfigPath
|
||||||
"-FrameRate", $FrameRate,
|
|
||||||
"-VideoBitrate", $VideoBitrate,
|
|
||||||
"-AudioBitrate", $AudioBitrate,
|
|
||||||
"-StableTimeSec", $StableTimeSec,
|
|
||||||
"-PollIntervalSec", $PollIntervalSec,
|
|
||||||
"-RtspPort", $RtspPort,
|
|
||||||
"-EncoderPriorityCsv", ($EncoderPriority -join ",")
|
|
||||||
)
|
)
|
||||||
if (-not [string]::IsNullOrWhiteSpace($NicDescription)) {
|
|
||||||
$values += @("-NicDescription", $NicDescription)
|
|
||||||
}
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($HealthTarget)) {
|
|
||||||
$values += @("-HealthTarget", $HealthTarget)
|
|
||||||
}
|
|
||||||
return Join-ProcessArguments $values
|
return Join-ProcessArguments $values
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,6 +813,27 @@ function Install-StartupTask {
|
|||||||
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
|
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
|
||||||
-Principal $principal -Settings $settings -Force | Out-Null
|
-Principal $principal -Settings $settings -Force | Out-Null
|
||||||
Write-Host "자동 실행 작업을 등록했습니다: $TaskName"
|
Write-Host "자동 실행 작업을 등록했습니다: $TaskName"
|
||||||
|
Write-Host "설정 파일: $ConfigPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ShowConfiguration) {
|
||||||
|
[PSCustomObject]@{
|
||||||
|
ConfigPath = $ConfigPath
|
||||||
|
ConfigLoaded = $ConfigWasLoaded
|
||||||
|
ChannelCount = $ChannelCount
|
||||||
|
StableTimeSec = $StableTimeSec
|
||||||
|
PollIntervalSec = $PollIntervalSec
|
||||||
|
FrameRate = $FrameRate
|
||||||
|
VideoBitrate = $VideoBitrate
|
||||||
|
EncoderPriority = ($EncoderPriority -join " > ")
|
||||||
|
AudioEnabled = $AudioEnabled
|
||||||
|
AudioBitrate = $AudioBitrate
|
||||||
|
AudioSignalThreshold = $AudioSignalThreshold
|
||||||
|
NicDescription = $NicDescription
|
||||||
|
HealthTarget = $HealthTarget
|
||||||
|
RtspPort = $RtspPort
|
||||||
|
} | Format-List
|
||||||
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($InstallStartup) {
|
if ($InstallStartup) {
|
||||||
@@ -645,6 +847,25 @@ if ($UninstallStartup) {
|
|||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($StartupStatus) {
|
||||||
|
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||||
|
if (-not $task) {
|
||||||
|
Write-Host "자동 실행 작업이 등록되어 있지 않습니다: $TaskName"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||||
|
[PSCustomObject]@{
|
||||||
|
TaskName = $TaskName
|
||||||
|
State = $task.State
|
||||||
|
LastRunTime = $taskInfo.LastRunTime
|
||||||
|
LastTaskResult = $taskInfo.LastTaskResult
|
||||||
|
NextRunTime = $taskInfo.NextRunTime
|
||||||
|
Execute = $task.Actions.Execute
|
||||||
|
Arguments = $task.Actions.Arguments
|
||||||
|
} | Format-List
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath $LogDir)) {
|
if (-not (Test-Path -LiteralPath $LogDir)) {
|
||||||
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
@@ -710,6 +931,9 @@ function Test-FFmpegCapabilities {
|
|||||||
if ($encoders -notmatch '(?m)\bh264_(nvenc|amf|qsv)\b') {
|
if ($encoders -notmatch '(?m)\bh264_(nvenc|amf|qsv)\b') {
|
||||||
throw "현재 FFmpeg에 지원 가능한 H.264 하드웨어 인코더가 없습니다."
|
throw "현재 FFmpeg에 지원 가능한 H.264 하드웨어 인코더가 없습니다."
|
||||||
}
|
}
|
||||||
|
if ($AudioEnabled -eq 1 -and $encoders -notmatch '(?m)\baac\b') {
|
||||||
|
throw "오디오 캡처가 켜져 있지만 현재 FFmpeg에 AAC 인코더가 없습니다."
|
||||||
|
}
|
||||||
return $encoders
|
return $encoders
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,7 +1048,9 @@ function Select-HardwareEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Add-Type -AssemblyName System.Windows.Forms
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
if ($AudioEnabled -eq 1) {
|
||||||
Initialize-AudioInterop
|
Initialize-AudioInterop
|
||||||
|
}
|
||||||
|
|
||||||
$DisplayInteropSource = @'
|
$DisplayInteropSource = @'
|
||||||
using System;
|
using System;
|
||||||
@@ -1227,11 +1453,6 @@ function Start-Stream {
|
|||||||
$Endpoint,
|
$Endpoint,
|
||||||
$Encoder
|
$Encoder
|
||||||
)
|
)
|
||||||
$pipeName = "screen-rtsp-{0}-{1}-{2}" -f $PID,$Channel,([Guid]::NewGuid().ToString("N"))
|
|
||||||
$audioProcess = Start-AudioCaptureWorker -Channel $Channel -Endpoint $Endpoint -PipeName $pipeName
|
|
||||||
$script:AudioProcesses["monitor$Channel"] = $audioProcess
|
|
||||||
Wait-AudioPipe -Process $audioProcess -PipeName $pipeName
|
|
||||||
|
|
||||||
$ddaInput = "ddagrab=output_idx=$($Screen.OutputIndex):draw_mouse=1:framerate=${FrameRate}:dup_frames=1"
|
$ddaInput = "ddagrab=output_idx=$($Screen.OutputIndex):draw_mouse=1:framerate=${FrameRate}:dup_frames=1"
|
||||||
$filters = @("hwdownload", "format=bgra")
|
$filters = @("hwdownload", "format=bgra")
|
||||||
$rotationFilter = Get-RotationFilter $Screen.Orientation
|
$rotationFilter = Get-RotationFilter $Screen.Orientation
|
||||||
@@ -1240,27 +1461,48 @@ function Start-Stream {
|
|||||||
$videoFilter = $filters -join ","
|
$videoFilter = $filters -join ","
|
||||||
$rtspUrl = "$RTSPPublishBase/monitor$Channel"
|
$rtspUrl = "$RTSPPublishBase/monitor$Channel"
|
||||||
$logFile = Join-Path $LogDir "monitor$Channel-ffmpeg.log"
|
$logFile = Join-Path $LogDir "monitor$Channel-ffmpeg.log"
|
||||||
$pipePath = "\\.\pipe\$pipeName"
|
|
||||||
|
|
||||||
$arguments = @(
|
$arguments = @(
|
||||||
"-hide_banner", "-loglevel", "warning", "-nostdin",
|
"-hide_banner", "-loglevel", "warning", "-nostdin",
|
||||||
"-f", "lavfi", "-i", $ddaInput,
|
"-f", "lavfi", "-i", $ddaInput
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($AudioEnabled -eq 1) {
|
||||||
|
$pipeName = "screen-rtsp-{0}-{1}-{2}" -f $PID,$Channel,([Guid]::NewGuid().ToString("N"))
|
||||||
|
$audioProcess = Start-AudioCaptureWorker -Channel $Channel -Endpoint $Endpoint -PipeName $pipeName
|
||||||
|
$script:AudioProcesses["monitor$Channel"] = $audioProcess
|
||||||
|
Wait-AudioPipe -Process $audioProcess -PipeName $pipeName
|
||||||
|
$pipePath = "\\.\pipe\$pipeName"
|
||||||
|
$arguments += @(
|
||||||
"-thread_queue_size", "1024", "-f", $Endpoint.SampleFormat,
|
"-thread_queue_size", "1024", "-f", $Endpoint.SampleFormat,
|
||||||
"-ar", $Endpoint.SampleRate, "-ac", $Endpoint.Channels, "-i", $pipePath,
|
"-ar", $Endpoint.SampleRate, "-ac", $Endpoint.Channels, "-i", $pipePath,
|
||||||
"-map", "0:v:0", "-map", "1:a:0",
|
"-map", "0:v:0", "-map", "1:a:0"
|
||||||
"-vf", $videoFilter,
|
)
|
||||||
"-c:v", $Encoder.Name
|
}
|
||||||
) + $Encoder.Arguments + @(
|
else {
|
||||||
|
$arguments += @("-map", "0:v:0")
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments += @("-vf", $videoFilter, "-c:v", $Encoder.Name)
|
||||||
|
$arguments += $Encoder.Arguments
|
||||||
|
$arguments += @(
|
||||||
"-b:v", $VideoBitrate, "-maxrate", $VideoBitrate, "-bufsize", $VideoBitrate,
|
"-b:v", $VideoBitrate, "-maxrate", $VideoBitrate, "-bufsize", $VideoBitrate,
|
||||||
"-g", $FrameRate, "-bf", "0", "-pix_fmt", "nv12",
|
"-g", $FrameRate, "-bf", "0", "-pix_fmt", "nv12"
|
||||||
|
)
|
||||||
|
if ($AudioEnabled -eq 1) {
|
||||||
|
$arguments += @(
|
||||||
"-c:a", "aac", "-b:a", $AudioBitrate, "-ar", "48000", "-ac", "2",
|
"-c:a", "aac", "-b:a", $AudioBitrate, "-ar", "48000", "-ac", "2",
|
||||||
"-af", "aresample=async=1000:first_pts=0",
|
"-af", "aresample=async=1000:first_pts=0"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
$arguments += @(
|
||||||
"-f", "rtsp", "-rtsp_transport", "tcp", $rtspUrl
|
"-f", "rtsp", "-rtsp_transport", "tcp", $rtspUrl
|
||||||
)
|
)
|
||||||
|
|
||||||
|
$audioDescription = if ($AudioEnabled -eq 1) { $Endpoint.Name } else { "사용 안 함" }
|
||||||
Write-Log ("monitor{0} 시작: DDA output_idx={1}, {2}x{3}, 회전={4}, {5}, 오디오={6}" -f `
|
Write-Log ("monitor{0} 시작: DDA output_idx={1}, {2}x{3}, 회전={4}, {5}, 오디오={6}" -f `
|
||||||
$Channel,$Screen.OutputIndex,$Screen.Width,$Screen.Height,$Screen.Orientation,
|
$Channel,$Screen.OutputIndex,$Screen.Width,$Screen.Height,$Screen.Orientation,
|
||||||
$Encoder.Name,$Endpoint.Name)
|
$Encoder.Name,$audioDescription)
|
||||||
|
|
||||||
return Start-Process -FilePath $FFmpegPath -ArgumentList (Join-ProcessArguments $arguments) `
|
return Start-Process -FilePath $FFmpegPath -ArgumentList (Join-ProcessArguments $arguments) `
|
||||||
-WorkingDirectory $BaseDir -WindowStyle Hidden -RedirectStandardError $logFile -PassThru
|
-WorkingDirectory $BaseDir -WindowStyle Hidden -RedirectStandardError $logFile -PassThru
|
||||||
@@ -1271,7 +1513,12 @@ function Start-Streams {
|
|||||||
$selected = @(Select-StreamingScreens $Screens)
|
$selected = @(Select-StreamingScreens $Screens)
|
||||||
if ($selected.Count -lt $ChannelCount) { return }
|
if ($selected.Count -lt $ChannelCount) { return }
|
||||||
Write-Log "RTSP $ChannelCount 채널 시작"
|
Write-Log "RTSP $ChannelCount 채널 시작"
|
||||||
|
if ($AudioEnabled -eq 1) {
|
||||||
Write-Log "오디오 출력 장치: $($Endpoint.Name) (peak=$([Math]::Round($Endpoint.Peak, 4)))"
|
Write-Log "오디오 출력 장치: $($Endpoint.Name) (peak=$([Math]::Round($Endpoint.Peak, 4)))"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "오디오 캡처: 사용 안 함"
|
||||||
|
}
|
||||||
for ($index = 0; $index -lt $ChannelCount; $index++) {
|
for ($index = 0; $index -lt $ChannelCount; $index++) {
|
||||||
$channel = $index + 1
|
$channel = $index + 1
|
||||||
$process = Start-Stream -Channel $channel -Screen $selected[$index] `
|
$process = Start-Stream -Channel $channel -Screen $selected[$index] `
|
||||||
@@ -1285,7 +1532,9 @@ function Start-Streams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Test-StreamsRunning {
|
function Test-StreamsRunning {
|
||||||
if ($StreamProcesses.Count -ne $ChannelCount -or $AudioProcesses.Count -ne $ChannelCount) {
|
$expectedAudioProcessCount = if ($AudioEnabled -eq 1) { $ChannelCount } else { 0 }
|
||||||
|
if ($StreamProcesses.Count -ne $ChannelCount -or
|
||||||
|
$AudioProcesses.Count -ne $expectedAudioProcessCount) {
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
foreach ($process in @($StreamProcesses.Values) + @($AudioProcesses.Values)) {
|
foreach ($process in @($StreamProcesses.Values) + @($AudioProcesses.Values)) {
|
||||||
@@ -1308,7 +1557,10 @@ try {
|
|||||||
$gpuSignature = Get-GpuSignature
|
$gpuSignature = Get-GpuSignature
|
||||||
|
|
||||||
Write-Log "Screen RTSP Watchdog 시작"
|
Write-Log "Screen RTSP Watchdog 시작"
|
||||||
|
if ($ConfigWasLoaded) { Write-Log "설정 파일: $ConfigPath" }
|
||||||
|
else { Write-Log "설정 파일 없음 - 내장 기본값 사용: $ConfigPath" }
|
||||||
Write-Log "인코더 우선순위: $($EncoderPriority -join ' > ')"
|
Write-Log "인코더 우선순위: $($EncoderPriority -join ' > ')"
|
||||||
|
Write-Log "오디오 캡처: $(if ($AudioEnabled -eq 1) { '사용' } else { '사용 안 함' })"
|
||||||
if ($NicDescription) { Write-Log "고정 NIC: $NicDescription" }
|
if ($NicDescription) { Write-Log "고정 NIC: $NicDescription" }
|
||||||
else { Write-Log "NIC 선택: IPv4 기본 경로 자동 감지" }
|
else { Write-Log "NIC 선택: IPv4 기본 경로 자동 감지" }
|
||||||
if ($HealthTarget) { Write-Log "통신 확인 대상: $HealthTarget" }
|
if ($HealthTarget) { Write-Log "통신 확인 대상: $HealthTarget" }
|
||||||
@@ -1329,8 +1581,16 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$screens = @(Get-ActiveScreens)
|
$screens = @(Get-ActiveScreens)
|
||||||
|
if ($AudioEnabled -eq 1) {
|
||||||
$endpoints = @(Get-AudioEndpoints)
|
$endpoints = @(Get-AudioEndpoints)
|
||||||
$endpoint = Select-AudioEndpoint $endpoints
|
$endpoint = Select-AudioEndpoint $endpoints
|
||||||
|
$audioSignature = Get-AudioSignature $endpoint
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$endpoints = @()
|
||||||
|
$endpoint = $null
|
||||||
|
$audioSignature = "AUDIO_DISABLED"
|
||||||
|
}
|
||||||
$network = Get-NetworkState
|
$network = Get-NetworkState
|
||||||
|
|
||||||
if (((Get-Date) - $lastGpuCheck).TotalSeconds -ge 10) {
|
if (((Get-Date) - $lastGpuCheck).TotalSeconds -ge 10) {
|
||||||
@@ -1346,7 +1606,7 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$systemSignature = (Get-ScreenSignature $screens) + "||" +
|
$systemSignature = (Get-ScreenSignature $screens) + "||" +
|
||||||
(Get-NetworkSignature $network) + "||" + (Get-AudioSignature $endpoint) +
|
(Get-NetworkSignature $network) + "||" + $audioSignature +
|
||||||
"||" + $encoder.Name
|
"||" + $encoder.Name
|
||||||
|
|
||||||
if ($systemSignature -ne $lastSystemSignature) {
|
if ($systemSignature -ne $lastSystemSignature) {
|
||||||
@@ -1367,7 +1627,9 @@ try {
|
|||||||
$reason = "기본 D3D11 adapter에서 캡처 가능한 모니터 $ChannelCount 개 미만"
|
$reason = "기본 D3D11 adapter에서 캡처 가능한 모니터 $ChannelCount 개 미만"
|
||||||
}
|
}
|
||||||
if (-not $reason) { $reason = Test-NetworkHealth $network }
|
if (-not $reason) { $reason = Test-NetworkHealth $network }
|
||||||
if (-not $reason -and -not $endpoint) { $reason = "활성 오디오 출력 장치 없음" }
|
if (-not $reason -and $AudioEnabled -eq 1 -and -not $endpoint) {
|
||||||
|
$reason = "활성 오디오 출력 장치 없음"
|
||||||
|
}
|
||||||
|
|
||||||
if ($reason) {
|
if ($reason) {
|
||||||
if ($reason -ne $lastReason) { Write-Log "송출 조건 불충족: $reason" }
|
if ($reason -ne $lastReason) { Write-Log "송출 조건 불충족: $reason" }
|
||||||
@@ -1384,7 +1646,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 또는 오디오 loopback worker 비정상 종료 감지"
|
Write-Log "FFmpeg 또는 보조 프로세스 비정상 종료 감지"
|
||||||
Stop-Streams
|
Stop-Streams
|
||||||
$healthySince = $null
|
$healthySince = $null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user