#Requires -Version 5.1 <# .SYNOPSIS Sends a SaveReplayBuffer request to OBS via WebSocket v5 (built into OBS 28+). Returns $true on success, $false on any failure. .PARAMETER Port OBS WebSocket port. Defaults to the value in config.psd1. #> param( [int]$Port = 0 ) $ErrorActionPreference = 'SilentlyContinue' $config = Import-PowerShellDataFile -Path (Join-Path $PSScriptRoot '..\config.psd1') if ($Port -eq 0) { $Port = $config.WebSocketPort } # --- Start transcript logging --- try { $logDir = [System.Environment]::ExpandEnvironmentVariables($config.LogPath) New-Item -ItemType Directory -Path $logDir -Force -ErrorAction SilentlyContinue | Out-Null Start-Transcript -Path (Join-Path $logDir 'OBSReplayBuffer-Save.log') -Force -ErrorAction Stop } catch { # Log path unavailable — continue without transcript } Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Invoke-ReplaySave started. Target: ws://127.0.0.1:$Port" $ws = $null $cts = $null try { $ws = New-Object System.Net.WebSockets.ClientWebSocket $cts = New-Object System.Threading.CancellationTokenSource(5000) # 5-second timeout $uri = [System.Uri]"ws://127.0.0.1:$Port" $ws.ConnectAsync($uri, $cts.Token).Wait() Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] WebSocket connected." $buffer = New-Object byte[] 8192 # Receive Hello (opcode 0) $null = $ws.ReceiveAsync($buffer, $cts.Token).Result Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Received Hello (op 0)." # Send Identify (opcode 1) — no authentication $identify = [System.Text.Encoding]::UTF8.GetBytes('{"op":1,"d":{"rpcVersion":1}}') $ws.SendAsync($identify, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $cts.Token).Wait() Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Sent Identify (op 1)." # Receive Identified (opcode 2) $null = $ws.ReceiveAsync($buffer, $cts.Token).Result Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Received Identified (op 2)." # Send SaveReplayBuffer request (opcode 6) $saveRequestTime = Get-Date $request = [System.Text.Encoding]::UTF8.GetBytes('{"op":6,"d":{"requestType":"SaveReplayBuffer","requestId":"itrecord-1"}}') $ws.SendAsync($request, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $cts.Token).Wait() Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Sent SaveReplayBuffer request (op 6)." # Receive RequestResponse (opcode 7) $recv = $ws.ReceiveAsync($buffer, $cts.Token).Result $json = [System.Text.Encoding]::UTF8.GetString($buffer, 0, $recv.Count) | ConvertFrom-Json $status = $json.d.requestStatus Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Received response (op 7): result=$($status.result) code=$($status.code) comment=$($status.comment)" if (-not $status.result) { throw "OBS rejected SaveReplayBuffer: code=$($status.code) comment=$($status.comment)" } $ws.CloseAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, 'Done', $cts.Token).Wait() Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] WebSocket closed." # --- Verify replay file was written to disk --- $captureDir = Join-Path $config.UNCPath $env:USERNAME $fileTimeout = [datetime]::UtcNow.AddSeconds(30) $savedFile = $null while (-not $savedFile) { if ([datetime]::UtcNow -ge $fileTimeout) { throw "OBS accepted the save request but no replay file appeared in '$captureDir' within 30 seconds." } $savedFile = Get-ChildItem -Path $captureDir -Filter '*.mkv' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -ge $saveRequestTime } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $savedFile) { Start-Sleep -Seconds 2 } } $fileSizeMB = [math]::Round($savedFile.Length / 1MB, 2) Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Replay file confirmed: $($savedFile.FullName) ($fileSizeMB MB)" # --- Notify user that the replay was saved --- $notifyScript = Join-Path $PSScriptRoot 'Show-Notification.ps1' Start-Process -FilePath 'powershell.exe' -ArgumentList @( '-STA' '-ExecutionPolicy', 'Bypass' '-NonInteractive' '-WindowStyle', 'Hidden' '-File', $notifyScript '-Type', 'ReplaySaved' '-ContactNumber', $config.ITContactNumber '-FileName', $savedFile.Name '-FileSizeMB', $fileSizeMB ) -WindowStyle Hidden Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] User notification launched." return $true } catch { $inner = $_.Exception.InnerException while ($inner.InnerException) { $inner = $inner.InnerException } $msg = if ($inner) { $inner.Message } else { $_.Exception.Message } Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [ERROR] Save failed: $msg" return $false } finally { if ($ws) { $ws.Dispose() } if ($cts) { $cts.Dispose() } Stop-Transcript -ErrorAction SilentlyContinue }