192 lines
8.3 KiB
PowerShell
192 lines
8.3 KiB
PowerShell
#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 = {
|
|
param($fileName, $fileSizeMB, $contactNumber)
|
|
|
|
Add-Type -AssemblyName PresentationFramework
|
|
Add-Type -AssemblyName WindowsBase
|
|
Add-Type -AssemblyName System.Drawing
|
|
|
|
$hBitmap = [System.Drawing.SystemIcons]::Shield.ToBitmap().GetHbitmap()
|
|
$wpfImage = [System.Windows.Interop.Imaging]::CreateBitmapSourceFromHBitmap(
|
|
$hBitmap, [IntPtr]::Zero, [System.Windows.Int32Rect]::Empty,
|
|
[System.Windows.Media.Imaging.BitmapSizeOptions]::FromEmptyOptions()
|
|
)
|
|
[System.Runtime.InteropServices.Marshal]::DeleteObject($hBitmap)
|
|
|
|
[xml]$xaml = @"
|
|
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
Title="IT Screen Recorder" Height="260" Width="440"
|
|
WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
|
|
Topmost="True" ShowInTaskbar="False">
|
|
<Grid Margin="20">
|
|
<Grid.RowDefinitions>
|
|
<RowDefinition Height="Auto"/>
|
|
<RowDefinition Height="*"/>
|
|
<RowDefinition Height="Auto"/>
|
|
<RowDefinition Height="Auto"/>
|
|
</Grid.RowDefinitions>
|
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,14">
|
|
<Image x:Name="ImgHeader" Width="32" Height="32" Margin="0,0,10,0" VerticalAlignment="Center"/>
|
|
<TextBlock Text="Replay Saved Successfully" FontSize="16" FontWeight="SemiBold" FontFamily="Segoe UI" VerticalAlignment="Center"/>
|
|
</StackPanel>
|
|
<TextBlock x:Name="TxtBody" Grid.Row="1" TextWrapping="Wrap" FontSize="12" FontFamily="Segoe UI" LineHeight="22"/>
|
|
<TextBlock x:Name="TxtContact" Grid.Row="2" FontSize="11" FontFamily="Segoe UI" Foreground="#555" HorizontalAlignment="Center" Margin="0,12,0,16"/>
|
|
<Button x:Name="BtnOK" Grid.Row="3" Width="100" HorizontalAlignment="Center" IsDefault="True"/>
|
|
</Grid>
|
|
</Window>
|
|
"@
|
|
|
|
$window = [System.Windows.Markup.XamlReader]::Load([System.Xml.XmlNodeReader]::new($xaml))
|
|
$imgHeader = $window.FindName('ImgHeader')
|
|
$txtBody = $window.FindName('TxtBody')
|
|
$txtContact = $window.FindName('TxtContact')
|
|
$btnOK = $window.FindName('BtnOK')
|
|
|
|
$imgHeader.Source = $wpfImage
|
|
$txtContact.Text = "Questions? Contact IT at $contactNumber"
|
|
|
|
$txtBody.Inlines.Add("Your replay has been saved ($fileSizeMB MB):`n")
|
|
$bold1 = New-Object System.Windows.Documents.Run($fileName)
|
|
$bold1.FontWeight = [System.Windows.FontWeights]::Bold
|
|
$txtBody.Inlines.Add($bold1)
|
|
$txtBody.Inlines.Add("`n`nIT can retrieve this file if needed for a support case.")
|
|
|
|
$script:tick = 30
|
|
$btnOK.Content = "OK (30)"
|
|
$timer = New-Object System.Windows.Threading.DispatcherTimer
|
|
$timer.Interval = [TimeSpan]::FromSeconds(1)
|
|
$timer.Add_Tick({
|
|
$script:tick--
|
|
$btnOK.Content = "OK ($script:tick)"
|
|
if ($script:tick -le 0) {
|
|
$timer.Stop()
|
|
$window.Close()
|
|
}
|
|
})
|
|
$timer.Start()
|
|
$btnOK.Add_Click({ $timer.Stop(); $window.Close() })
|
|
$window.ShowDialog() | Out-Null
|
|
}
|
|
|
|
$notifyRunspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
|
|
$notifyRunspace.ApartmentState = [System.Threading.ApartmentState]::STA
|
|
$notifyRunspace.Open()
|
|
$notifyPS = [System.Management.Automation.PowerShell]::Create()
|
|
$notifyPS.Runspace = $notifyRunspace
|
|
$notifyPS.AddScript($notifyScript) | Out-Null
|
|
$notifyPS.AddArgument($savedFile.Name) | Out-Null
|
|
$notifyPS.AddArgument($fileSizeMB) | Out-Null
|
|
$notifyPS.AddArgument($config.ITContactNumber) | Out-Null
|
|
$notifyPS.Invoke()
|
|
$notifyPS.Dispose()
|
|
$notifyRunspace.Close()
|
|
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] User notification dismissed."
|
|
|
|
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
|
|
}
|