From dcd2ee420827ef487fcaae2dc095a86d0fff82d3 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Fri, 27 Mar 2026 09:37:33 -0500 Subject: [PATCH 1/9] Fixing uac issue --- .../Invoke-iKATRecording.ps1 | 140 ++++++++++++------ Tests/iCat/Invoke-iKATRecording.Tests.ps1 | 2 +- Tests/iCat/Remove-ExpiredRecordings.Tests.ps1 | 12 +- 3 files changed, 103 insertions(+), 51 deletions(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index be02d06..d7b2c10 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -18,6 +18,7 @@ if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Forc "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Script started. USERNAME=$($env:USERNAME), TargetUser=$TargetUser" | Add-Content "$logDir\debug.log" if ($env:USERNAME -ne $TargetUser) { + Write-Output "Current user '$($env:USERNAME)' does not match target user '$TargetUser'. Exiting." "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - User mismatch. Exiting." | Add-Content "$logDir\debug.log" Exit } @@ -35,6 +36,7 @@ if ($drive) { $freeSpaceGB = [math]::Round($drive.FreeSpace / 1GB, 2) "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Free space: $freeSpaceGB GB, Required: $MinFreeSpaceGB GB" | Add-Content "$logDir\debug.log" if ($freeSpaceGB -lt $MinFreeSpaceGB) { + Write-Warning "Not enough free space. $freeSpaceGB GB available, $MinFreeSpaceGB GB required." "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Not enough free space. Exiting." | Add-Content "$logDir\debug.log" Exit } @@ -46,50 +48,100 @@ Add-Type -AssemblyName System.Windows.Forms $virtualScreen = [System.Windows.Forms.SystemInformation]::VirtualScreen "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Reported VirtualScreen: $($virtualScreen.Width)x$($virtualScreen.Height) offset $($virtualScreen.X),$($virtualScreen.Y)" | Add-Content "$logDir\debug.log" -# 4. Prepare output file -$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_session_$timestamp.ts" - -# Construct the FFmpeg arguments -# - gdigrab with no -video_size/-offset: auto-detects the full desktop at true physical pixel dimensions, -# avoiding DPI scaling mismatches that caused the resolution to appear cropped on RDS sessions. -# - scale filter: rounds width and height down to even numbers required by libx264 (no resize, just crop 1px if odd). -# - tune zerolatency: disables libx264 lookahead and B-frames so each frame is encoded and released immediately. -# - g 5: 1-second keyframe interval (at 5fps); combined with zerolatency the encoder buffer stays under 1 second. -# - MPEG-TS: self-contained 188-byte packets, fully playable with no trailer needed. -# - flush_packets 1: flush every TS packet to disk immediately after encoding. -$ffmpegArgsStr = "-f gdigrab -framerate 5 -i desktop -c:v libx264 -preset ultrafast -tune zerolatency -crf 30 -pix_fmt yuv420p -g 5 -vf `"scale=trunc(iw/2)*2:trunc(ih/2)*2`" -flush_packets 1 -f mpegts `"$outputFile`"" - -# 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown -$procInfo = New-Object System.Diagnostics.ProcessStartInfo -$procInfo.FileName = $FFmpegPath -$procInfo.Arguments = $ffmpegArgsStr -$procInfo.RedirectStandardInput = $true -$procInfo.UseShellExecute = $false -$procInfo.CreateNoWindow = $true - -"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Launching ffmpeg: $ffmpegArgsStr" | Add-Content "$logDir\debug.log" -$ffmpegProcess = [System.Diagnostics.Process]::Start($procInfo) -"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg PID: $($ffmpegProcess.Id)" | Add-Content "$logDir\debug.log" - -try { - # 6. Block until FFmpeg exits naturally or the script is interrupted (logoff/session end) - while (-not $ffmpegProcess.HasExited) { - Start-Sleep -Seconds 5 - } +# 4. Restart guardrails for unexpected FFmpeg exits (ex: UAC secure desktop transitions) +$restartWindowMinutes = 10 +$maxRestartCount = 5 +$restartDelaySeconds = 5 +$restartCount = 0 +$restartWindowStart = Get-Date + +function New-RecordingFilePath { + param( + [Parameter(Mandatory = $true)] + [string]$DestinationDir, + + [Parameter(Mandatory = $true)] + [string]$UserName + ) + + $timestamp = Get-Date -Format "yyyyMMdd_HHmmss_fff" + return Join-Path -Path $DestinationDir -ChildPath "$($UserName)_session_$timestamp.ts" } -finally { - # 7. Session ended or script was interrupted — terminate FFmpeg gracefully - if ($null -ne $ffmpegProcess -and -not $ffmpegProcess.HasExited) { - # Send 'q' to gracefully stop recording so the file header formatting writes correctly - $ffmpegProcess.StandardInput.WriteLine("q") - - # Wait up to 10 seconds for it to write headers and close - $ffmpegProcess.WaitForExit(10000) | Out-Null - - # Fallback if the process stubbornly hung - if (-not $ffmpegProcess.HasExited) { - $ffmpegProcess.Kill() + +while ($true) { + # 4. Prepare output file + $outputFile = New-RecordingFilePath -DestinationDir $OutputDir -UserName $TargetUser + + # Construct the FFmpeg arguments + # - gdigrab with no -video_size/-offset: auto-detects the full desktop at true physical pixel dimensions, + # avoiding DPI scaling mismatches that caused the resolution to appear cropped on RDS sessions. + # - scale filter: rounds width and height down to even numbers required by libx264 (no resize, just crop 1px if odd). + # - tune zerolatency: disables libx264 lookahead and B-frames so each frame is encoded and released immediately. + # - g 5: 1-second keyframe interval (at 5fps); combined with zerolatency the encoder buffer stays under 1 second. + # - MPEG-TS: self-contained 188-byte packets, fully playable with no trailer needed. + # - flush_packets 1: flush every TS packet to disk immediately after encoding. + $ffmpegArgsStr = "-f gdigrab -framerate 5 -i desktop -c:v libx264 -preset ultrafast -tune zerolatency -crf 30 -pix_fmt yuv420p -g 5 -vf `"scale=trunc(iw/2)*2:trunc(ih/2)*2`" -flush_packets 1 -f mpegts `"$outputFile`"" + + # 5. Start FFmpeg silently but intercept StandardInput for graceful shutdown + $procInfo = New-Object System.Diagnostics.ProcessStartInfo + $procInfo.FileName = $FFmpegPath + $procInfo.Arguments = $ffmpegArgsStr + $procInfo.RedirectStandardInput = $true + $procInfo.UseShellExecute = $false + $procInfo.CreateNoWindow = $true + + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Launching ffmpeg: $ffmpegArgsStr" | Add-Content "$logDir\debug.log" + try { + $ffmpegProcess = [System.Diagnostics.Process]::Start($procInfo) + } + catch { + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Failed to start ffmpeg: $($_.Exception.Message)" | Add-Content "$logDir\debug.log" + throw + } + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg PID: $($ffmpegProcess.Id)" | Add-Content "$logDir\debug.log" + + try { + # 6. Block until FFmpeg exits naturally or the script is interrupted (logoff/session end) + while (-not $ffmpegProcess.HasExited) { + Start-Sleep -Seconds 5 + } + } + finally { + # 7. Session ended or script was interrupted — terminate FFmpeg gracefully + if ($null -ne $ffmpegProcess -and -not $ffmpegProcess.HasExited) { + # Send 'q' to gracefully stop recording so the file header formatting writes correctly + $ffmpegProcess.StandardInput.WriteLine("q") + + # Wait up to 10 seconds for it to write headers and close + $ffmpegProcess.WaitForExit(10000) | Out-Null + + # Fallback if the process stubbornly hung + if (-not $ffmpegProcess.HasExited) { + $ffmpegProcess.Kill() + } } } -} \ No newline at end of file + + $exitCode = $ffmpegProcess.ExitCode + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg exited with code $exitCode" | Add-Content "$logDir\debug.log" + + if ($exitCode -eq 0) { + break + } + + $now = Get-Date + if ($now -gt $restartWindowStart.AddMinutes($restartWindowMinutes)) { + $restartCount = 0 + $restartWindowStart = $now + } + + $restartCount += 1 + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg exited unexpectedly. Restart $restartCount of $maxRestartCount within $restartWindowMinutes minutes." | Add-Content "$logDir\debug.log" + + if ($restartCount -gt $maxRestartCount) { + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg exited repeatedly. Recording stopped." | Add-Content "$logDir\debug.log" + throw "FFmpeg exited repeatedly; aborting recording." + } + + Start-Sleep -Seconds $restartDelaySeconds +} diff --git a/Tests/iCat/Invoke-iKATRecording.Tests.ps1 b/Tests/iCat/Invoke-iKATRecording.Tests.ps1 index 484d692..988f006 100644 --- a/Tests/iCat/Invoke-iKATRecording.Tests.ps1 +++ b/Tests/iCat/Invoke-iKATRecording.Tests.ps1 @@ -1,7 +1,7 @@ Describe 'Invoke-iKATRecording.ps1' { BeforeAll { - $sut = Resolve-Path "$PSScriptRoot\..\..\Scripts\iCat\Invoke-FFmpegCapture\Invoke-iKATRecording.ps1" + $sut = Resolve-Path "$PSScriptRoot\..\..\Scripts\iKAT\Invoke-FFmpegCapture\Invoke-iKATRecording.ps1" } It 'Should exit if current user does not match TargetUser' { diff --git a/Tests/iCat/Remove-ExpiredRecordings.Tests.ps1 b/Tests/iCat/Remove-ExpiredRecordings.Tests.ps1 index f950e2d..1b81e35 100644 --- a/Tests/iCat/Remove-ExpiredRecordings.Tests.ps1 +++ b/Tests/iCat/Remove-ExpiredRecordings.Tests.ps1 @@ -1,6 +1,6 @@ Describe 'Remove-ExpiredRecordings.ps1' { BeforeAll { - $sut = Resolve-Path "$PSScriptRoot\..\..\Scripts\iCat\Invoke-FFmpegCapture\Remove-ExpiredRecordings.ps1" + $sut = Resolve-Path "$PSScriptRoot\..\..\Scripts\iKAT\Invoke-FFmpegCapture\Remove-ExpiredRecordings.ps1" $testDir = Join-Path -Path $env:TEMP -ChildPath "FFmpegTest_$(New-Guid)" New-Item -ItemType Directory -Path $testDir | Out-Null } @@ -21,13 +21,13 @@ Describe 'Remove-ExpiredRecordings.ps1' { Assert-MockCalled Write-Warning -Times 1 -ParameterFilter { $Message -match "Directory '$escapedDir' does not exist" } } - It 'Should delete .mkv files older than DaysToKeep and leave new files intact' { + It 'Should delete .ts files older than DaysToKeep and leave new files intact' { # Create a 6-day-old file - $oldFile = New-Item -Path $testDir -Name "old_video.mkv" -ItemType File + $oldFile = New-Item -Path $testDir -Name "old_video.ts" -ItemType File $oldFile.LastWriteTime = (Get-Date).AddDays(-6) # Create a new file (1 day old) - $newFile = New-Item -Path $testDir -Name "new_video.mkv" -ItemType File + $newFile = New-Item -Path $testDir -Name "new_video.ts" -ItemType File $newFile.LastWriteTime = (Get-Date).AddDays(-1) # Create a 6-day-old file with a different extension (should be ignored) @@ -53,8 +53,8 @@ Describe 'Remove-ExpiredRecordings.ps1' { $logExists | Should -Be $true $logContent = Get-Content $logFile - $logContent | Should -Match 'Successfully deleted: old_video.mkv' - $logContent | Should -Not -Match 'new_video.mkv' + $logContent | Should -Match 'Successfully deleted: old_video.ts' + $logContent | Should -Not -Match 'new_video.ts' $logContent | Should -Not -Match 'old_notes.txt' } } From 85337dac740477560ae0f54c1326140eb24c5522 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Fri, 27 Mar 2026 09:42:31 -0500 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=90=9B=20Stabilize=20iKAT=20recording?= =?UTF-8?q?=20around=20UAC=20exits=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Enhance iKAT recording script with user validation and restart logic; update test paths * ✨ Update Remove-ExpiredRecordings.Tests.ps1 to test .ts file deletion instead of .mkv From 65f690945187e3e6752d61e623d947ea2f020c9a Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Fri, 27 Mar 2026 09:46:44 -0500 Subject: [PATCH 3/9] docs: Add iKAT recording conventions and constraints to Copilot instructions --- .github/copilot-instructions.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2ceca52..bf22981 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -96,3 +96,26 @@ The `/Scripts/SFA/` directory contains the integrated certificate distribution s - Update README.html to reflect workflow changes - Use `refactor:` commits when integrating diagnostic tools (e.g., Test-BranchMappings) - Use `feat:` commits for new connectivity/workflow improvements + +### iKAT Automated Screen Recording +The `/Scripts/iKAT/Invoke-FFmpegCapture/` directory contains the session recording system deployed to `KFWS6IKAT01.kikkoman.com`. + +**Purpose:** Capture `dpuerner`'s full RDS desktop sessions to diagnose intermittent iKAT application errors. + +**Workflow:** +1. **Invoke-iKATRecording.ps1** — Core script: validates user, checks disk space, launches FFmpeg in a restart loop +2. **Start-DinaRecording.ps1** / **Start-JoeyRecording.ps1** — Runner scripts called by Task Scheduler via `.vbs` launchers +3. **Remove-ExpiredRecordings.ps1** — Daily cleanup of `.ts` files older than the retention period + +**Critical Constraints — do not change without understanding the impact:** +- **Output format must stay MPEG-TS (`.ts`)** — MP4/MKV require a trailer; if FFmpeg is force-killed at logoff, the file is unplayable. MPEG-TS packets are self-contained with no trailer needed. +- **`-tune zerolatency` must stay** — disables libx264 lookahead buffer; without it, up to 5 seconds of encoded frames are held in memory and lost on abrupt kill. +- **`-flush_packets 1` must stay** — forces OS-level flush after every TS packet; removing it re-introduces data loss at session end. +- **Task Scheduler tasks must use `LogonType Interactive` + `RunLevel Limited`** — `gdigrab` cannot access the RDS desktop from Session 0 or when running elevated. +- **The restart loop** (`while ($true)` with exit-code check) handles UAC secure desktop transitions that force-kill FFmpeg mid-session. Do not replace with a single-launch pattern. + +**When Making Changes:** +- Always update `log.md` with a dated entry describing what changed and why +- The restart guardrails (`$maxRestartCount`, `$restartWindowMinutes`) protect against infinite crash loops — adjust values carefully +- `Remove-ExpiredRecordings.ps1` filter must match the output extension (currently `*.ts`) — keep in sync if format ever changes +- Test with both a clean logoff (graceful) and an abrupt logoff (hard disconnect) to verify no data loss in either case From 93bf71b1f54b05e5da5a263fd0e3545cbae2df3a Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Fri, 27 Mar 2026 09:47:48 -0500 Subject: [PATCH 4/9] Change test folder iCat to iKAT --- .github/copilot-instructions.md | 20 +++++++++++++++++++ .../DateFormat/Get-UserDateFormats.Tests.ps1 | 0 .../DateFormat/Set-UserDateFormats.Tests.ps1 | 0 .../Invoke-iKATRecording.Tests.ps1 | 0 .../Get-UserRegionFormats.Tests.ps1 | 0 .../Set-UserRegionFormats.Tests.ps1 | 0 .../Remove-ExpiredRecordings.Tests.ps1 | 0 7 files changed, 20 insertions(+) rename Tests/{iCat => iKAT}/DateFormat/Get-UserDateFormats.Tests.ps1 (100%) rename Tests/{iCat => iKAT}/DateFormat/Set-UserDateFormats.Tests.ps1 (100%) rename Tests/{iCat => iKAT}/Invoke-iKATRecording.Tests.ps1 (100%) rename Tests/{iCat => iKAT}/RegionFormat/Get-UserRegionFormats.Tests.ps1 (100%) rename Tests/{iCat => iKAT}/RegionFormat/Set-UserRegionFormats.Tests.ps1 (100%) rename Tests/{iCat => iKAT}/Remove-ExpiredRecordings.Tests.ps1 (100%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index bf22981..4023d09 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -3,7 +3,9 @@ ## Code Style & Formatting Standards ### Commit Messages (Conventional Commits) + Use the following prefixes for all commits: + - `feat:` New features - `fix:` Bug fixes - `docs:` Documentation changes @@ -18,7 +20,9 @@ Use the following prefixes for all commits: Example: `feat: Add ETA automation with timezone awareness` ### PR Titles (Emoji Prefix) + Use emoji prefix followed by brief description: + - `✨ Add new feature` - `🐛 Fix bug or issue` - `📚 Update documentation` @@ -31,25 +35,33 @@ Use emoji prefix followed by brief description: Example: `✨ Add interactive task selection with rich UI` ### PR Body (GitHub-Flavored Markdown) + Structure all PR descriptions with these sections: + ```markdown ### What does this PR do? + Brief explanation of changes and what was implemented. ### Why are we doing this? + Context, motivation, and reason for the changes. ### How should this be tested? + Testing instructions, test cases, and validation steps. ### Any deployment notes? + Environment variables, migrations, breaking changes, or special instructions. ``` Include related issue references: `Closes #71, #77` (at end of description) ### PR Metadata Requirements + Always ensure the following metadata is set on every PR: + - **Labels**: Assign relevant labels (e.g., `enhancement`, `bug`, `documentation`, `refactor`, `testing`) - **Assignees**: Assign to yourself (J-MaFf) - **Issues**: Link all related issues in the PR description and GitHub's linked issues feature @@ -75,14 +87,17 @@ Note: Use `gh issue edit` for both issues and pull requests. Replace ` Date: Fri, 27 Mar 2026 09:59:22 -0500 Subject: [PATCH 5/9] fix: Use explorer.exe check instead of exit code to detect session end after UAC --- .../iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index d7b2c10..5dbc8c7 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -125,10 +125,17 @@ while ($true) { $exitCode = $ffmpegProcess.ExitCode "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg exited with code $exitCode" | Add-Content "$logDir\debug.log" - if ($exitCode -eq 0) { + # Detect session end via explorer.exe — it only runs in an active interactive session. + # If explorer is gone, the user has logged off (regardless of FFmpeg's exit code). + # If explorer is still running, FFmpeg was killed mid-session (e.g., UAC secure desktop + # transition closes the gdigrab handle cleanly with exit code 0) — restart the recording. + if (-not (Get-Process -Name "explorer" -ErrorAction SilentlyContinue)) { + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Explorer not found; user has logged off. Stopping." | Add-Content "$logDir\debug.log" break } + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Explorer still running; FFmpeg exited mid-session. Restarting recording." | Add-Content "$logDir\debug.log" + $now = Get-Date if ($now -gt $restartWindowStart.AddMinutes($restartWindowMinutes)) { $restartCount = 0 From da6ac59447a0fc08c081e58567893abfa1bad536 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Fri, 27 Mar 2026 10:03:04 -0500 Subject: [PATCH 6/9] docs: Log UAC restart loop verification and update copilot instructions with exit code lesson --- .github/copilot-instructions.md | 1 + Scripts/iKAT/Invoke-FFmpegCapture/log.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4023d09..429793b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -132,6 +132,7 @@ The `/Scripts/iKAT/Invoke-FFmpegCapture/` directory contains the session recordi - **`-flush_packets 1` must stay** — forces OS-level flush after every TS packet; removing it re-introduces data loss at session end. - **Task Scheduler tasks must use `LogonType Interactive` + `RunLevel Limited`** — `gdigrab` cannot access the RDS desktop from Session 0 or when running elevated. - **The restart loop** (`while ($true)` with exit-code check) handles UAC secure desktop transitions that force-kill FFmpeg mid-session. Do not replace with a single-launch pattern. +- **Session end must be detected via `explorer.exe`**, not FFmpeg's exit code — UAC secure desktop transitions cause gdigrab to exit with code `0` (clean), not a crash code. Checking exit code `0` to break the loop would stop recording at every UAC prompt. **When Making Changes:** diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/log.md b/Scripts/iKAT/Invoke-FFmpegCapture/log.md index 32ada43..6e0a397 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/log.md +++ b/Scripts/iKAT/Invoke-FFmpegCapture/log.md @@ -28,3 +28,5 @@ Several issues discovered and fixed during server deployment: - Switched output format from MP4 to MPEG-TS (.ts). MP4 requires a trailer written at the end to be playable; if ffmpeg is force-killed at logoff the file is corrupt. MPEG-TS writes self-contained 188-byte packets continuously with no trailer needed, so the file is fully playable up to the last written frame even after an abrupt kill. - Recording was still losing 5-10 seconds at the end due to libx264's encoder lookahead buffer. With a 25-frame GOP, libx264 holds up to 5 seconds of frames in memory before flushing. Fixed by adding -tune zerolatency (disables lookahead/B-frames, each frame encodes and flushes immediately) and reducing GOP to -g 5 (1-second keyframe intervals at 5fps). Also added -flush_packets 1 to force an OS-level flush after every TS packet. Maximum data loss is now under 1 second. - Dina's task (iKAT-Record-Dina) re-registered with RunLevel Limited to match Joey's working configuration. +Thursday, March 27, 2026 @ 10:00 AM: +UAC restart loop implemented and verified. When gdigrab loses the desktop handle during a UAC secure desktop transition, FFmpeg exits with code 0 (clean exit, not a crash). The original fix checked for non-zero exit codes to decide whether to restart — this meant UAC-triggered exits were mistakenly treated as graceful logoffs and the loop broke. Fixed by replacing the exit-code check with an explorer.exe presence check: if explorer is still running, the user session is still active and FFmpeg is restarted regardless of exit code. Confirmed working on server: UAC prompt triggered two .ts segment files as expected. From 087f533e55867ec10a34d48e8e3be985e4072ee0 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Fri, 27 Mar 2026 10:06:06 -0500 Subject: [PATCH 7/9] feat: Use shared session timestamp with part number for segment filenames --- .../Invoke-iKATRecording.ps1 | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index 5dbc8c7..3c6c080 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -55,22 +55,13 @@ $restartDelaySeconds = 5 $restartCount = 0 $restartWindowStart = Get-Date -function New-RecordingFilePath { - param( - [Parameter(Mandatory = $true)] - [string]$DestinationDir, - - [Parameter(Mandatory = $true)] - [string]$UserName - ) - - $timestamp = Get-Date -Format "yyyyMMdd_HHmmss_fff" - return Join-Path -Path $DestinationDir -ChildPath "$($UserName)_session_$timestamp.ts" -} +# Session timestamp is set once per logon — shared across all segment files for this session. +$sessionTimestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$partNumber = 1 while ($true) { - # 4. Prepare output file - $outputFile = New-RecordingFilePath -DestinationDir $OutputDir -UserName $TargetUser + # 4. Prepare output file — each segment shares the session timestamp and gets an incrementing part number. + $outputFile = Join-Path -Path $OutputDir -ChildPath "$($TargetUser)_${sessionTimestamp}_part${partNumber}.ts" # Construct the FFmpeg arguments # - gdigrab with no -video_size/-offset: auto-detects the full desktop at true physical pixel dimensions, @@ -134,7 +125,8 @@ while ($true) { break } - "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Explorer still running; FFmpeg exited mid-session. Restarting recording." | Add-Content "$logDir\debug.log" + $partNumber++ + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Explorer still running; FFmpeg exited mid-session. Starting segment $partNumber." | Add-Content "$logDir\debug.log" $now = Get-Date if ($now -gt $restartWindowStart.AddMinutes($restartWindowMinutes)) { From 61f497f583b80e8b3c9d9fbb9ce2ca3b27a8f318 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Fri, 27 Mar 2026 10:12:43 -0500 Subject: [PATCH 8/9] fix: Implement logarithmic backoff for FFmpeg restart delays --- .../Invoke-iKATRecording.ps1 | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 index 3c6c080..0d4ca6d 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 +++ b/Scripts/iKAT/Invoke-FFmpegCapture/Invoke-iKATRecording.ps1 @@ -49,9 +49,12 @@ $virtualScreen = [System.Windows.Forms.SystemInformation]::VirtualScreen "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Reported VirtualScreen: $($virtualScreen.Width)x$($virtualScreen.Height) offset $($virtualScreen.X),$($virtualScreen.Y)" | Add-Content "$logDir\debug.log" # 4. Restart guardrails for unexpected FFmpeg exits (ex: UAC secure desktop transitions) -$restartWindowMinutes = 10 -$maxRestartCount = 5 -$restartDelaySeconds = 5 +# - First 10 restarts: 1-second delay (handles rapid UAC transitions without noticeable lag) +# - Restarts 11+: logarithmic backoff so a genuinely broken FFmpeg backs off gradually. +# Formula: Round(Ln(restartCount - 9) * 5) seconds +# e.g. restart 11=3s, restart 15=9s, restart 20=12s, restart 30=15s, restart 60=20s +$restartWindowMinutes = 60 +$maxRestartCount = 60 $restartCount = 0 $restartWindowStart = Get-Date @@ -135,12 +138,15 @@ while ($true) { } $restartCount += 1 - "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg exited unexpectedly. Restart $restartCount of $maxRestartCount within $restartWindowMinutes minutes." | Add-Content "$logDir\debug.log" if ($restartCount -gt $maxRestartCount) { - "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg exited repeatedly. Recording stopped." | Add-Content "$logDir\debug.log" + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg exited $restartCount times in $restartWindowMinutes minutes. Aborting recording." | Add-Content "$logDir\debug.log" throw "FFmpeg exited repeatedly; aborting recording." } - Start-Sleep -Seconds $restartDelaySeconds + # Logarithmic backoff: first 10 restarts wait 1 second; beyond that delay grows slowly. + $restartDelay = if ($restartCount -le 10) { 1 } else { [math]::Round([math]::Log($restartCount - 9) * 5) } + "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - FFmpeg exited unexpectedly. Restart $restartCount of $maxRestartCount. Waiting ${restartDelay}s before next attempt." | Add-Content "$logDir\debug.log" + + Start-Sleep -Seconds $restartDelay } From c0b32cb4b217db715961c3d72f47d2aacb679705 Mon Sep 17 00:00:00 2001 From: Joey Maffiola <7maffiolajoey@gmail.com> Date: Fri, 27 Mar 2026 10:13:19 -0500 Subject: [PATCH 9/9] docs: Log logarithmic backoff implementation --- Scripts/iKAT/Invoke-FFmpegCapture/log.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Scripts/iKAT/Invoke-FFmpegCapture/log.md b/Scripts/iKAT/Invoke-FFmpegCapture/log.md index 6e0a397..d5da7e2 100644 --- a/Scripts/iKAT/Invoke-FFmpegCapture/log.md +++ b/Scripts/iKAT/Invoke-FFmpegCapture/log.md @@ -21,6 +21,7 @@ Thursday, March 26, 2026 @ 1:02 PM Changes have been made, implementing it on the iKat server now 1:30 PM: Several issues discovered and fixed during server deployment: + - Username typo corrected: dpurner → dpuerner in all scripts - .vbs launcher files had hardcoded dev machine paths; updated to C:\PowerShellScripts\Scripts\iKAT\Invoke-FFmpegCapture\ - Task Scheduler was not running ffmpeg in the user's interactive desktop session. Fixed by registering tasks with LogonType Interactive and RunLevel Limited. RunLevel Highest (elevation) also blocked gdigrab from accessing the desktop on RDS. @@ -30,3 +31,4 @@ Several issues discovered and fixed during server deployment: - Dina's task (iKAT-Record-Dina) re-registered with RunLevel Limited to match Joey's working configuration. Thursday, March 27, 2026 @ 10:00 AM: UAC restart loop implemented and verified. When gdigrab loses the desktop handle during a UAC secure desktop transition, FFmpeg exits with code 0 (clean exit, not a crash). The original fix checked for non-zero exit codes to decide whether to restart — this meant UAC-triggered exits were mistakenly treated as graceful logoffs and the loop broke. Fixed by replacing the exit-code check with an explorer.exe presence check: if explorer is still running, the user session is still active and FFmpeg is restarted regardless of exit code. Confirmed working on server: UAC prompt triggered two .ts segment files as expected. +Restart delay replaced with logarithmic backoff: first 10 restarts wait 1 second (fast recovery for UAC transitions); restarts 11+ use Round(Ln(restartCount - 9) * 5) seconds, growing from ~3s at restart 11 to ~20s at restart 60. Max restart count raised from 5 to 60, window from 10 to 60 minutes.