Fix RTSP clock pacing at 20 fps
This commit is contained in:
1 parent
5241194b9a
commit
6c91d4f372
3 files changed
+130
-40
No files matched your search
+108
-35
@@ -12,7 +12,10 @@ param(
|
||||
[int]$ChannelCount = 2,
|
||||
|
||||
[ValidateRange(1, 240)]
|
||||
[int]$FrameRate = 30,
|
||||
[int]$FrameRate = 20,
|
||||
|
||||
[ValidateRange(1, 10)]
|
||||
[int]$GopSeconds = 2,
|
||||
|
||||
[ValidatePattern("^[0-9]+[kKmM]$")]
|
||||
[string]$VideoBitrate = "8000k",
|
||||
@@ -536,9 +539,9 @@ namespace ScreenRtsp.Audio
|
||||
ThrowIfFailed(client.Start(), "IAudioClient.Start");
|
||||
|
||||
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 =
|
||||
int maximumSilenceFrames = Math.Max(1, (int)(format.SamplesPerSec / 20));
|
||||
long framesWritten = 0;
|
||||
System.Diagnostics.Stopwatch streamClock =
|
||||
System.Diagnostics.Stopwatch.StartNew();
|
||||
while (pipe.IsConnected)
|
||||
{
|
||||
@@ -548,17 +551,23 @@ namespace ScreenRtsp.Audio
|
||||
|
||||
if (packetFrames == 0)
|
||||
{
|
||||
// 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)
|
||||
// Sleep(2) can wake at roughly 15.6 ms on Windows. A
|
||||
// fixed 10 ms block per wake therefore makes the audio
|
||||
// clock run slow. Pace silence from elapsed wall time.
|
||||
long targetFrames = (long)(
|
||||
streamClock.Elapsed.TotalSeconds * format.SamplesPerSec);
|
||||
long missingFrames = targetFrames - framesWritten;
|
||||
if (missingFrames > 0)
|
||||
{
|
||||
int silenceFrames = (int)Math.Min(
|
||||
missingFrames, maximumSilenceFrames);
|
||||
int silenceBytes = checked(silenceFrames * format.BlockAlign);
|
||||
if (buffer.Length < silenceBytes) buffer = new byte[silenceBytes];
|
||||
Array.Clear(buffer, 0, silenceBytes);
|
||||
pipe.Write(buffer, 0, silenceBytes);
|
||||
packetClock.Restart();
|
||||
framesWritten += silenceFrames;
|
||||
}
|
||||
Thread.Sleep(2);
|
||||
Thread.Sleep(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -582,7 +591,7 @@ namespace ScreenRtsp.Audio
|
||||
try
|
||||
{
|
||||
pipe.Write(buffer, 0, bytes);
|
||||
packetClock.Restart();
|
||||
framesWritten += frames;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -710,7 +719,7 @@ function Assert-IniSettings {
|
||||
param($Configuration)
|
||||
$allowed = @{
|
||||
General = @("ChannelCount", "StableTimeSec", "PollIntervalSec")
|
||||
Video = @("FrameRate", "Bitrate", "EncoderPriority")
|
||||
Video = @("FrameRate", "GopSeconds", "Bitrate", "EncoderPriority")
|
||||
Audio = @("Enabled", "MicrophoneEnabled", "Bitrate", "SignalThreshold")
|
||||
Network = @("NicDescription", "HealthTarget")
|
||||
RTSP = @(
|
||||
@@ -779,6 +788,10 @@ if ($ConfigWasLoaded) {
|
||||
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("FrameRate")) {
|
||||
$FrameRate = [int]$setting.Value
|
||||
}
|
||||
$setting = Get-IniSetting $ini "Video" "GopSeconds"
|
||||
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("GopSeconds")) {
|
||||
$GopSeconds = [int]$setting.Value
|
||||
}
|
||||
$setting = Get-IniSetting $ini "Video" "Bitrate"
|
||||
if ($setting.Found -and -not $ExplicitParameters.ContainsKey("VideoBitrate")) {
|
||||
$VideoBitrate = [string]$setting.Value
|
||||
@@ -888,6 +901,7 @@ 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 "Video.GopSeconds" $GopSeconds 1 10
|
||||
Assert-IntegerRange "RTSP.Port" $RtspPort 1 65535
|
||||
Assert-IntegerRange "RTSP.HealthCheckIntervalSec" $RtspHealthCheckIntervalSec 1 3600
|
||||
Assert-IntegerRange "RTSP.HealthTimeoutSec" $RtspHealthTimeoutSec 1 60
|
||||
@@ -902,6 +916,7 @@ if ($AudioBitrate -notmatch '^[0-9]+[kKmM]$') {
|
||||
if ($AudioSignalThreshold -lt 0.0 -or $AudioSignalThreshold -gt 1.0) {
|
||||
throw "Audio.SignalThreshold 값은 0.0 이상 1.0 이하여야 합니다: $AudioSignalThreshold"
|
||||
}
|
||||
$GopSize = $FrameRate * $GopSeconds
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($EncoderPriorityCsv)) {
|
||||
$EncoderPriority = @($EncoderPriorityCsv -split ',' | ForEach-Object { $_.Trim() })
|
||||
@@ -1059,6 +1074,7 @@ function Get-HiddenWorkerArgumentList {
|
||||
"-EncoderPriorityCsv", ($EncoderPriority -join ","),
|
||||
"-ChannelCount", [string]$ChannelCount,
|
||||
"-FrameRate", [string]$FrameRate,
|
||||
"-GopSeconds", [string]$GopSeconds,
|
||||
"-VideoBitrate", $VideoBitrate,
|
||||
"-AudioBitrate", $AudioBitrate,
|
||||
"-AudioEnabled", [string]$AudioEnabled,
|
||||
@@ -1153,6 +1169,8 @@ if ($ShowConfiguration) {
|
||||
StableTimeSec = $StableTimeSec
|
||||
PollIntervalSec = $PollIntervalSec
|
||||
FrameRate = $FrameRate
|
||||
GopSeconds = $GopSeconds
|
||||
GopSize = $GopSize
|
||||
VideoBitrate = $VideoBitrate
|
||||
EncoderPriority = ($EncoderPriority -join " > ")
|
||||
AudioEnabled = $AudioEnabled
|
||||
@@ -1662,7 +1680,7 @@ function Get-EncoderDefinition {
|
||||
Vendor = "AMD"; Name = "h264_amf"
|
||||
Arguments = @(
|
||||
"-usage","lowlatency","-quality","speed","-rc","cbr","-latency","1",
|
||||
"-forced_idr","1","-header_spacing",[string]$FrameRate
|
||||
"-forced_idr","1","-header_spacing",[string]$GopSize
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1747,8 +1765,10 @@ function Test-HardwareEncoder {
|
||||
$testDirectory = New-DependencyTempDirectory
|
||||
$bitstreamPath = Join-Path $testDirectory ("{0}.h264" -f $Definition.Name)
|
||||
$testFrameRate = [Math]::Max(1, [int]$FrameRate)
|
||||
$frameCount = $testFrameRate * [Math]::Max(3, $TestSeconds)
|
||||
$expectedIdrFrames = [Math]::Max(3, [int][Math]::Floor(($frameCount - 1) / $testFrameRate) + 1)
|
||||
$testGopSize = [Math]::Max(1, [int]$GopSize)
|
||||
$effectiveTestSeconds = [Math]::Max([Math]::Max(3, $TestSeconds), (($GopSeconds * 2) + 1))
|
||||
$frameCount = $testFrameRate * $effectiveTestSeconds
|
||||
$expectedIdrFrames = [Math]::Max(3, [int][Math]::Floor(($frameCount - 1) / $testGopSize) + 1)
|
||||
$arguments = @(
|
||||
"-hide_banner", "-loglevel", "verbose", "-nostdin",
|
||||
"-f", "lavfi", "-i", "testsrc2=size=1280x720:rate=$testFrameRate",
|
||||
@@ -1756,8 +1776,8 @@ function Test-HardwareEncoder {
|
||||
"-c:v", $Definition.Name
|
||||
) + $Definition.Arguments + @(
|
||||
"-b:v", "4000k", "-maxrate", "4000k", "-bufsize", "4000k",
|
||||
"-g", [string]$testFrameRate, "-bf", "0",
|
||||
"-force_key_frames", "expr:gte(t,n_forced*1)",
|
||||
"-g", [string]$testGopSize, "-bf", "0",
|
||||
"-force_key_frames", "expr:gte(t,n_forced*$GopSeconds)",
|
||||
"-f", "h264", "-y", $bitstreamPath
|
||||
)
|
||||
try {
|
||||
@@ -1799,7 +1819,7 @@ function Test-HardwareEncoder {
|
||||
Encoder = $Definition.Name
|
||||
Passed = $passed
|
||||
ExitCode = $result.ExitCode
|
||||
TestSeconds = [Math]::Max(3, $TestSeconds)
|
||||
TestSeconds = $effectiveTestSeconds
|
||||
Frames = if ($metrics) { $metrics.DecodedFrames } else { 0 }
|
||||
IdrFrames = if ($metrics) { $metrics.IdrFrameCount } else { 0 }
|
||||
NalIdrCount = if ($metrics) { $metrics.NalIdrCount } else { 0 }
|
||||
@@ -2147,7 +2167,7 @@ function Get-EnvironmentFingerprint {
|
||||
}
|
||||
else { "" }
|
||||
$source = (Get-GpuSignature) + "|" + $ffmpegHash + "|" + $mediaHash + "|" + $scriptHash + "|" +
|
||||
($EncoderPriority -join ",") + "|$FrameRate|$VideoBitrate"
|
||||
($EncoderPriority -join ",") + "|$FrameRate|$GopSeconds|$VideoBitrate"
|
||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($source)
|
||||
@@ -2948,7 +2968,14 @@ function Get-VideoFilter {
|
||||
$filters = @("hwdownload", "format=bgra")
|
||||
$rotationFilter = Get-RotationFilter $Screen.Orientation
|
||||
if ($rotationFilter) { $filters += $rotationFilter.Split(',') }
|
||||
$filters += @("setpts=N/(${FrameRate}*TB)", "format=nv12")
|
||||
# ddagrab emits one probe frame before slower inputs (notably DirectShow)
|
||||
# finish opening. Drop that stale frame, then preserve DDA elapsed-time PTS.
|
||||
# Do not synthesize missed frames: doing so creates an encode backlog and
|
||||
# makes media time lag wall time on an overloaded GPU.
|
||||
$filters += @(
|
||||
"trim=start_frame=1", "setpts=PTS-STARTPTS", "settb=AVTB",
|
||||
"format=nv12"
|
||||
)
|
||||
return ($filters -join ",")
|
||||
}
|
||||
|
||||
@@ -2966,37 +2993,38 @@ function New-FFmpegArguments {
|
||||
"-hide_banner", "-loglevel", "warning", "-nostdin"
|
||||
)
|
||||
$filterParts = @()
|
||||
|
||||
for ($index = 0; $index -lt $SelectedScreens.Count; $index++) {
|
||||
$screen = $SelectedScreens[$index]
|
||||
$ddaInput = "ddagrab=output_idx=$($screen.OutputIndex):draw_mouse=1:framerate=${FrameRate}:dup_frames=1"
|
||||
$arguments += @("-f", "lavfi", "-i", $ddaInput)
|
||||
$filterParts += "[$index`:v]$(Get-VideoFilter $screen)[v$index]"
|
||||
}
|
||||
$videoInputOffset = 0
|
||||
|
||||
if ($AudioEnabled -eq 1) {
|
||||
$playbackInputIndex = $SelectedScreens.Count
|
||||
# Open slower audio devices first. ddagrab creates a probe frame as
|
||||
# soon as its input is opened; opening it before DirectShow gives the
|
||||
# first two DDA frames timestamps that are several seconds apart.
|
||||
$playbackInputIndex = 0
|
||||
$arguments += @(
|
||||
"-thread_queue_size", "1024", "-f", $PlaybackEndpoint.SampleFormat,
|
||||
"-ar", $PlaybackEndpoint.SampleRate, "-ac", $PlaybackEndpoint.Channels,
|
||||
"-i", $AudioPipePath
|
||||
)
|
||||
$filterParts += "[$playbackInputIndex`:a]aresample=48000:async=1:first_pts=0," +
|
||||
$filterParts += "[$playbackInputIndex`:a]asetpts=PTS-STARTPTS," +
|
||||
"aresample=48000:async=1:first_pts=0," +
|
||||
"aformat=sample_fmts=fltp:channel_layouts=stereo[sys]"
|
||||
|
||||
if ($MicrophoneEnabled -eq 1) {
|
||||
$microphoneInputIndex = $playbackInputIndex + 1
|
||||
$arguments += @(
|
||||
"-thread_queue_size", "1024", "-f", "dshow",
|
||||
"-thread_queue_size", "1024", "-rtbufsize", "32M", "-f", "dshow",
|
||||
"-i", "audio=$MicrophoneInputName"
|
||||
)
|
||||
$filterParts += "[$microphoneInputIndex`:a]aresample=48000:async=1:first_pts=0," +
|
||||
$filterParts += "[$microphoneInputIndex`:a]asetpts=PTS-STARTPTS," +
|
||||
"aresample=48000:async=1:first_pts=0," +
|
||||
"aformat=sample_fmts=fltp:channel_layouts=stereo[mic]"
|
||||
$filterParts += "[sys][mic]amix=inputs=2:duration=longest:" +
|
||||
"dropout_transition=2:normalize=1[mixed]"
|
||||
$videoInputOffset = 2
|
||||
}
|
||||
else {
|
||||
$filterParts += "[sys]anull[mixed]"
|
||||
$videoInputOffset = 1
|
||||
}
|
||||
|
||||
if ($SelectedScreens.Count -eq 1) {
|
||||
@@ -3008,6 +3036,14 @@ function New-FFmpegArguments {
|
||||
}
|
||||
}
|
||||
|
||||
for ($index = 0; $index -lt $SelectedScreens.Count; $index++) {
|
||||
$screen = $SelectedScreens[$index]
|
||||
$videoInputIndex = $videoInputOffset + $index
|
||||
$ddaInput = "ddagrab=output_idx=$($screen.OutputIndex):draw_mouse=1:framerate=${FrameRate}:dup_frames=1"
|
||||
$arguments += @("-f", "lavfi", "-i", $ddaInput)
|
||||
$filterParts += "[$videoInputIndex`:v]$(Get-VideoFilter $screen)[v$index]"
|
||||
}
|
||||
|
||||
$arguments += @("-filter_complex", ($filterParts -join ";"))
|
||||
|
||||
for ($index = 0; $index -lt $SelectedScreens.Count; $index++) {
|
||||
@@ -3018,8 +3054,10 @@ function New-FFmpegArguments {
|
||||
$arguments += @($Encoder.Arguments)
|
||||
$arguments += @(
|
||||
"-b:v", $VideoBitrate, "-maxrate", $VideoBitrate, "-bufsize", $VideoBitrate,
|
||||
"-g", $FrameRate, "-bf", "0", "-pix_fmt", "nv12",
|
||||
"-force_key_frames", "expr:gte(t,n_forced*1)"
|
||||
"-g", $GopSize, "-bf", "0", "-pix_fmt", "nv12",
|
||||
"-force_key_frames", "expr:gte(t,n_forced*$GopSeconds)",
|
||||
"-enc_time_base", "filter",
|
||||
"-fps_mode", "passthrough"
|
||||
)
|
||||
if ($AudioEnabled -eq 1) {
|
||||
$arguments += @(
|
||||
@@ -3027,7 +3065,8 @@ function New-FFmpegArguments {
|
||||
)
|
||||
}
|
||||
$arguments += @(
|
||||
"-f", "rtsp", "-rtsp_transport", "tcp", "$RTSPPublishBase/monitor$channel"
|
||||
"-f", "rtsp", "-max_interleave_delta", "100000", "-flush_packets", "1",
|
||||
"-rtsp_transport", "tcp", "$RTSPPublishBase/monitor$channel"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3652,6 +3691,13 @@ function Invoke-SimulationTests {
|
||||
$qsv -contains "-forced_idr" -and $qsv -contains "-idr_interval")
|
||||
}
|
||||
|
||||
Invoke-SimulationCase "무음 WASAPI loopback은 경과시간 기준으로 PCM 시계 유지" {
|
||||
return ($AudioInteropSource -match 'streamClock\.Elapsed\.TotalSeconds' -and
|
||||
$AudioInteropSource -match 'targetFrames\s*-\s*framesWritten' -and
|
||||
$AudioInteropSource -match 'framesWritten\s*\+=\s*silenceFrames' -and
|
||||
$AudioInteropSource -notmatch 'packetClock\.Restart')
|
||||
}
|
||||
|
||||
Invoke-SimulationCase "자동 실행은 숨김 worker와 config.ini만 전달" {
|
||||
$arguments = Get-StartupArgumentList
|
||||
return ($arguments -match '-WindowStyle\s+Hidden' -and
|
||||
@@ -3715,6 +3761,7 @@ function Invoke-SimulationTests {
|
||||
$filterIndex = [Array]::IndexOf($arguments, "-filter_complex")
|
||||
$filter = if ($filterIndex -ge 0) { $arguments[$filterIndex + 1] } else { "" }
|
||||
return ($filter -match 'amix=inputs=2' -and $filter -match 'asplit=2' -and
|
||||
@([regex]::Matches($filter, 'asetpts=PTS-STARTPTS')).Count -eq 2 -and
|
||||
$arguments -contains 'dshow' -and
|
||||
$arguments -contains '-force_key_frames' -and
|
||||
@($arguments | Where-Object { $_ -eq 'h264_nvenc' }).Count -eq 2 -and
|
||||
@@ -3722,6 +3769,32 @@ function Invoke-SimulationTests {
|
||||
@($arguments | Where-Object { $_ -eq "$RTSPPublishBase/monitor2" }).Count -eq 1)
|
||||
}
|
||||
|
||||
Invoke-SimulationCase "DDA와 인코더가 20fps/GOP40 및 원본 PTS를 일관되게 사용" {
|
||||
$script:AudioEnabled = 1
|
||||
$script:MicrophoneEnabled = 1
|
||||
$arguments = @(New-FFmpegArguments `
|
||||
-SelectedScreens @($screenPrimary,$screenSecond) `
|
||||
-PlaybackEndpoint $defaultAudio -MicrophoneEndpoint $defaultMicrophone `
|
||||
-MicrophoneInputName $defaultMicrophone.Name `
|
||||
-Encoder (Get-EncoderDefinition "Intel") -AudioPipePath "\\.\pipe\simulation")
|
||||
$filterIndex = [Array]::IndexOf($arguments, "-filter_complex")
|
||||
$filter = if ($filterIndex -ge 0) { $arguments[$filterIndex + 1] } else { "" }
|
||||
$joined = $arguments -join " "
|
||||
$pipePosition = $joined.IndexOf('\\.\pipe\simulation')
|
||||
$ddaPosition = $joined.IndexOf('ddagrab=')
|
||||
return ($FrameRate -eq 20 -and $GopSize -eq 40 -and
|
||||
$pipePosition -ge 0 -and $ddaPosition -gt $pipePosition -and
|
||||
$joined -match 'framerate=20' -and
|
||||
$filter -match 'trim=start_frame=1' -and
|
||||
$filter -match 'setpts=PTS-STARTPTS' -and $filter -match 'settb=AVTB' -and
|
||||
$filter -notmatch 'setpts=N/' -and
|
||||
@($arguments | Where-Object { $_ -eq "40" }).Count -ge 2 -and
|
||||
@($arguments | Where-Object { $_ -eq "filter" }).Count -eq 2 -and
|
||||
@($arguments | Where-Object { $_ -eq "passthrough" }).Count -eq 2 -and
|
||||
@($arguments | Where-Object { $_ -eq "100000" }).Count -eq 2 -and
|
||||
$joined -match 'n_forced\*2')
|
||||
}
|
||||
|
||||
Invoke-SimulationCase "마이크 OFF이면 시스템 재생음만 두 채널에 분배" {
|
||||
$script:AudioEnabled = 1
|
||||
$script:MicrophoneEnabled = 0
|
||||
@@ -3966,7 +4039,7 @@ try {
|
||||
"${RtspHealthCheckIntervalSec}초 주기 / ${RtspHealthTimeoutSec}초 제한 / " +
|
||||
"${RtspHealthFailureThreshold}회 연속 실패 시 재시작"
|
||||
)
|
||||
Write-Log "영상 설정: ${FrameRate}fps / 채널당 $VideoBitrate"
|
||||
Write-Log "영상 설정: ${FrameRate}fps / GOP ${GopSize}프레임(${GopSeconds}초) / DDA PTS 유지 / 채널당 $VideoBitrate"
|
||||
|
||||
$lastSystemSignature = ""
|
||||
$healthySince = $null
|
||||
|
||||
Reference in new issue
Block a user