From 67d5c9ca5f25ef1b549b56f3875776b4f989804a Mon Sep 17 00:00:00 2001 From: Derek Cooper Date: Fri, 27 Mar 2026 11:16:39 -0700 Subject: [PATCH] Initial commit --- README.md | 129 ++++++++++++++++++ config.psd1 | 17 +++ obs-config/global.ini | 7 + .../plugin_config/obs-websocket/config.json | 6 + obs-config/scenes/ITMonitor.json | 104 ++++++++++++++ scripts/Invoke-ReplaySave.ps1 | 61 +++++++++ scripts/Show-ReplayTray.ps1 | 60 ++++++++ scripts/Start-OBSReplayBuffer.ps1 | 86 ++++++++++++ 8 files changed, 470 insertions(+) create mode 100644 README.md create mode 100644 config.psd1 create mode 100644 obs-config/global.ini create mode 100644 obs-config/plugin_config/obs-websocket/config.json create mode 100644 obs-config/scenes/ITMonitor.json create mode 100644 scripts/Invoke-ReplaySave.ps1 create mode 100644 scripts/Show-ReplayTray.ps1 create mode 100644 scripts/Start-OBSReplayBuffer.ps1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..87b6382 --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# OBS Replay Buffer for IT Support + +IT support tool that runs OBS Studio's replay buffer silently on VDI sessions, giving users a system tray button to save the last N minutes of screen activity when an issue occurs. Clips are saved to a central UNC share organized by username. + +## How It Works + +1. DEM assigns this to targeted users at logon via App Volumes (OBS) + file transfer + logon task +2. OBS launches hidden with the replay buffer running — users never see the OBS interface +3. A system tray icon appears — right-click → **Save Replay** +4. The clip saves to `\\server\ITCaptures\\YYYY-MM-DD_HH-MM-SS.mkv` + +## Architecture + +| Component | Role | +|---|---| +| **App Volumes AppStack** | Delivers OBS Studio to targeted VDI sessions | +| **DEM File Transfer** | Drops OBS config files into `%APPDATA%\obs-studio\` at logon | +| **DEM Logon Task** | Runs `Start-OBSReplayBuffer.ps1` at logon | +| **`config.psd1`** | Central config — UNC path, buffer duration, OBS executable path | +| **`Start-OBSReplayBuffer.ps1`** | Creates user capture folder, writes OBS profile, launches OBS + tray icon | +| **`Show-ReplayTray.ps1`** | WinForms system tray icon — right-click menu with Save Replay / Exit | +| **`Invoke-ReplaySave.ps1`** | Sends `SaveReplayBuffer` to OBS via WebSocket v5 (built into OBS 28+) | +| **`obs-config/`** | OBS profile, scene collection, and WebSocket config delivered by DEM | + +## Repository Structure + +``` +obs-replay-buffer/ +├── config.psd1 +├── scripts/ +│ ├── Start-OBSReplayBuffer.ps1 +│ ├── Show-ReplayTray.ps1 +│ └── Invoke-ReplaySave.ps1 +└── obs-config/ + ├── global.ini + ├── profiles/ + │ └── ITMonitor/ + │ └── basic.ini + ├── scenes/ + │ └── ITMonitor.json + └── plugin_config/ + └── obs-websocket/ + └── config.json +``` + +## Prerequisites + +- **OBS Studio 28+** packaged as an App Volumes AppStack (WebSocket v5 is built in — no plugin needed) +- **VMware App Volumes** to deliver OBS to targeted users +- **VMware DEM** to deliver config files and run the logon task +- A **UNC share** writable by VDI users for clip storage +- A **network share** to host this repo's files, accessible from all VDI machines + +## Configuration + +Edit `config.psd1` before deploying: + +| Key | Description | Default | +|---|---|---| +| `UNCPath` | Base UNC path for clip storage — username subfolder created automatically | `\\server\ITCaptures` | +| `BufferSeconds` | Replay buffer duration in seconds | `120` | +| `WebSocketPort` | OBS WebSocket port — must match `obs-config/plugin_config/obs-websocket/config.json` | `4455` | +| `OBSExecutable` | Full path to `obs64.exe` on the VDI machine | `C:\Program Files\obs-studio\bin\64bit\obs64.exe` | +| `ProfileName` | OBS profile name — must match the folder under `obs-config/profiles/` | `ITMonitor` | +| `SceneCollection` | OBS scene collection name — must match the filename under `obs-config/scenes/` | `ITMonitor` | + +## Deployment + +### Step 1 — Host the repo on a network share + +Place this repo (or a copy of it) on a share accessible from all VDI machines, e.g.: + +``` +\\stcu-fs01\IT-Tools\OBS-Record\ +``` + +### Step 2 — DEM: File Transfer tasks + +Create these file transfer tasks in DEM, scoped to the target user group. All run at logon. + +| Source (on network share) | Destination (on VDI session) | +|---|---| +| `obs-config\profiles\ITMonitor\` | `%APPDATA%\obs-studio\basic\profiles\ITMonitor\` | +| `obs-config\scenes\ITMonitor.json` | `%APPDATA%\obs-studio\basic\scenes\ITMonitor.json` | +| `obs-config\plugin_config\obs-websocket\config.json` | `%APPDATA%\obs-studio\plugin_config\obs-websocket\config.json` | +| `obs-config\global.ini` | `%APPDATA%\obs-studio\global.ini` | + +> **Note:** `basic.ini` is intentionally not delivered by DEM — it is written dynamically at logon by `Start-OBSReplayBuffer.ps1` so that the correct UNC path (including username) and primary monitor resolution are baked in per session. + +### Step 3 — DEM: Logon task + +Create one logon task in DEM (scoped to the same target group). Set it to **not wait for completion**. + +``` +powershell.exe -ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File "\\stcu-fs01\IT-Tools\OBS-Record\scripts\Start-OBSReplayBuffer.ps1" +``` + +This script handles launching both OBS and the tray icon — no second task needed. + +### Step 4 — Assign App Volumes AppStack + +Assign the OBS AppStack to the same user group in App Volumes. The logon task will exit cleanly if OBS is not present or already running. + +## User Experience + +- A shield icon appears in the system tray at logon — no other UI is visible +- Right-click the icon → **Save Replay** to save the last `BufferSeconds` of screen activity +- A balloon notification confirms success or failure +- Clips appear at `\\server\ITCaptures\\` named by timestamp + +## Changing the Buffer Duration + +Update `BufferSeconds` in `config.psd1`. The value is written into the OBS profile at each logon, so no repackaging of the App Volumes AppStack is needed. + +## Notes + +### Scene Collection JSON + +The `obs-config/scenes/ITMonitor.json` file was generated to target monitor index `0` (primary display). If it does not capture correctly on first use: + +1. Launch OBS normally on a test machine with the AppStack assigned +2. Manually configure a Display Capture source pointed at the primary monitor +3. Save, then copy `%APPDATA%\obs-studio\basic\scenes\ITMonitor.json` back into this repo + +The `basic.ini` and WebSocket config are reliable and should not need adjustment. + +### OBS Tray Icon vs. IT Tray Icon + +OBS itself also places a tray icon when minimized to tray. Users will see two icons — the OBS icon and the IT Screen Recorder shield icon. The OBS icon can be right-clicked to quit OBS, which would break the replay buffer. If this is a concern, a future enhancement could hide the OBS tray icon via a startup flag or OBS config setting. diff --git a/config.psd1 b/config.psd1 new file mode 100644 index 0000000..42294a4 --- /dev/null +++ b/config.psd1 @@ -0,0 +1,17 @@ +@{ + # Base UNC path for saved clips — a subfolder per username is created automatically + UNCPath = '\\server\ITCaptures' + + # Replay buffer duration in seconds (120 = 2 minutes) + BufferSeconds = 120 + + # OBS WebSocket port — must match obs-config/plugin_config/obs-websocket/config.json + WebSocketPort = 4455 + + # Full path to OBS executable (standard App Volumes / Program Files install) + OBSExecutable = 'C:\Program Files\obs-studio\bin\64bit\obs64.exe' + + # Must match the profile folder name and scene collection filename + ProfileName = 'ITMonitor' + SceneCollection = 'ITMonitor' +} diff --git a/obs-config/global.ini b/obs-config/global.ini new file mode 100644 index 0000000..ce27af6 --- /dev/null +++ b/obs-config/global.ini @@ -0,0 +1,7 @@ +[General] +EnableAutoUpdates=false + +[BasicWindow] +SysTrayEnabled=true +SysTrayWhenStarted=true +ShowOnStartup=false diff --git a/obs-config/plugin_config/obs-websocket/config.json b/obs-config/plugin_config/obs-websocket/config.json new file mode 100644 index 0000000..69c3968 --- /dev/null +++ b/obs-config/plugin_config/obs-websocket/config.json @@ -0,0 +1,6 @@ +{ + "alerts_enabled": false, + "auth_required": false, + "server_enabled": true, + "server_port": 4455 +} diff --git a/obs-config/scenes/ITMonitor.json b/obs-config/scenes/ITMonitor.json new file mode 100644 index 0000000..d464be3 --- /dev/null +++ b/obs-config/scenes/ITMonitor.json @@ -0,0 +1,104 @@ +{ + "name": "ITMonitor", + "current_scene": "Screen Capture", + "current_program_scene": "Screen Capture", + "scene_order": [ + { "name": "Screen Capture" } + ], + "sources": [ + { + "id": "monitor_capture", + "name": "Primary Display", + "versioned_id": "monitor_capture", + "settings": { + "capture_cursor": true, + "compatibility": false, + "force_sdr": false, + "monitor": 0 + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": true, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_field_order": 0, + "deinterlace_mode": 0, + "filter_data": [], + "filters": [] + }, + { + "id": "scene", + "name": "Screen Capture", + "versioned_id": "scene", + "settings": { + "id_counter": 1, + "custom_size": false, + "items": [ + { + "name": "Primary Display", + "visible": true, + "locked": false, + "rot": 0.0, + "pos": { "x": 0.0, "y": 0.0 }, + "scale": { "x": 1.0, "y": 1.0 }, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds": { "x": 0.0, "y": 0.0 }, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "crop_left": 0, + "group_item_backup": false, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { "id": "" }, + "hide_transition": { "id": "" } + } + ] + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": { + "OBSBasic.SelectScene": [] + }, + "deinterlace_field_order": 0, + "deinterlace_mode": 0, + "filter_data": [], + "filters": [] + } + ], + "groups": [], + "transitions": [ + { + "id": "fade_transition", + "name": "Fade", + "settings": { "duration": 300 }, + "hotkeys": {} + } + ], + "current_transition": "Fade", + "transition_duration": 300, + "preview_locked": false, + "scaling_enabled": false, + "scaling_level": 1, + "scaling_off_x": 0.0, + "scaling_off_y": 0.0 +} diff --git a/scripts/Invoke-ReplaySave.ps1 b/scripts/Invoke-ReplaySave.ps1 new file mode 100644 index 0000000..d2dc332 --- /dev/null +++ b/scripts/Invoke-ReplaySave.ps1 @@ -0,0 +1,61 @@ +#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' + +if ($Port -eq 0) { + $config = Import-PowerShellDataFile -Path (Join-Path $PSScriptRoot '..\config.psd1') + $Port = $config.WebSocketPort +} + +$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://localhost:$Port" + + $ws.ConnectAsync($uri, $cts.Token).Wait() + + $buffer = New-Object byte[] 8192 + + # Receive Hello (opcode 0) + $null = $ws.ReceiveAsync($buffer, $cts.Token).Result + + # 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() + + # Receive Identified (opcode 2) + $null = $ws.ReceiveAsync($buffer, $cts.Token).Result + + # Send SaveReplayBuffer request (opcode 6) + $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() + + # Receive RequestResponse (opcode 7) + $null = $ws.ReceiveAsync($buffer, $cts.Token).Result + + $ws.CloseAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, 'Done', $cts.Token).Wait() + + return $true +} +catch { + return $false +} +finally { + if ($ws) { $ws.Dispose() } + if ($cts) { $cts.Dispose() } +} diff --git a/scripts/Show-ReplayTray.ps1 b/scripts/Show-ReplayTray.ps1 new file mode 100644 index 0000000..387b1e2 --- /dev/null +++ b/scripts/Show-ReplayTray.ps1 @@ -0,0 +1,60 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + System tray icon that lets users trigger an OBS replay buffer save. + Right-click the tray icon to access Save Replay or Exit. + +.NOTES + This script blocks (runs a WinForms message loop) — launch it via Start-Process. + Start-OBSReplayBuffer.ps1 handles launching this automatically at logon. +#> + +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +$saveScript = Join-Path $PSScriptRoot 'Invoke-ReplaySave.ps1' + +# --- Tray icon --- +$tray = New-Object System.Windows.Forms.NotifyIcon +$tray.Icon = [System.Drawing.SystemIcons]::Shield +$tray.Text = 'IT Screen Recorder' +$tray.Visible = $true + +# --- Context menu --- +$menu = New-Object System.Windows.Forms.ContextMenuStrip +$saveItem = New-Object System.Windows.Forms.ToolStripMenuItem('Save Replay') +$sep = New-Object System.Windows.Forms.ToolStripSeparator +$exitItem = New-Object System.Windows.Forms.ToolStripMenuItem('Exit') + +$saveItem.Add_Click({ + $saveItem.Enabled = $false + $saveItem.Text = 'Saving...' + + $result = & powershell.exe -ExecutionPolicy Bypass -NonInteractive -File $saveScript + + if ($result -eq $true) { + $tray.ShowBalloonTip(4000, 'IT Screen Recorder', 'Replay saved.', [System.Windows.Forms.ToolTipIcon]::Info) + } + else { + $tray.ShowBalloonTip(4000, 'IT Screen Recorder', 'Could not save replay. Please contact the helpdesk.', [System.Windows.Forms.ToolTipIcon]::Error) + } + + $saveItem.Text = 'Save Replay' + $saveItem.Enabled = $true +}) + +$exitItem.Add_Click({ + $tray.Visible = $false + [System.Windows.Forms.Application]::Exit() +}) + +$menu.Items.Add($saveItem) | Out-Null +$menu.Items.Add($sep) | Out-Null +$menu.Items.Add($exitItem) | Out-Null + +$tray.ContextMenuStrip = $menu + +# --- Message loop (blocks until Exit is chosen) --- +[System.Windows.Forms.Application]::Run() + +$tray.Dispose() diff --git a/scripts/Start-OBSReplayBuffer.ps1 b/scripts/Start-OBSReplayBuffer.ps1 new file mode 100644 index 0000000..f5a1eea --- /dev/null +++ b/scripts/Start-OBSReplayBuffer.ps1 @@ -0,0 +1,86 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Launches OBS Studio hidden with the replay buffer running, then starts the tray icon. + Intended to be called by DEM as a logon task (fire and forget — do not wait for completion). + +.NOTES + Reads settings from config.psd1 one level above this script. + Writes the OBS profile basic.ini dynamically so the correct UNC path and buffer + duration are baked in at session start. +#> + +$ErrorActionPreference = 'Stop' + +$configPath = Join-Path $PSScriptRoot '..\config.psd1' +$config = Import-PowerShellDataFile -Path $configPath + +# --- Skip if OBS is already running (handles reconnect scenarios) --- +if (Get-Process -Name 'obs64' -ErrorAction SilentlyContinue) { + exit 0 +} + +# --- Create user capture subfolder on UNC share --- +$userCapturePath = Join-Path $config.UNCPath $env:USERNAME +if (-not (Test-Path -Path $userCapturePath)) { + New-Item -ItemType Directory -Path $userCapturePath -Force | Out-Null +} + +# --- Detect primary monitor resolution --- +Add-Type -AssemblyName System.Windows.Forms +$screen = [System.Windows.Forms.Screen]::PrimaryScreen +$width = $screen.Bounds.Width +$height = $screen.Bounds.Height + +# --- Write OBS profile (basic.ini) --- +$profileDir = "$env:APPDATA\obs-studio\basic\profiles\$($config.ProfileName)" +New-Item -ItemType Directory -Path $profileDir -Force | Out-Null + +@" +[General] +Name=$($config.ProfileName) + +[Output] +Mode=Simple +FilenameFormatting=%CCYY-%MM-%DD_%hh-%mm-%ss + +[SimpleOutput] +FilePath=$userCapturePath +RecFormat2=mkv +RecQuality=HQ +RecRBTime=$($config.BufferSeconds) +RecRBSize=512 + +[Video] +BaseCX=$width +BaseCY=$height +OutputCX=$width +OutputCY=$height +FPSType=1 +FPSNum=30 +FPSDen=1 +ScaleType=bicubic +ColorFormat=NV12 +ColorSpace=709 +ColorRange=Partial +"@ | Set-Content -Path "$profileDir\basic.ini" -Encoding UTF8 + +# --- Launch OBS hidden --- +$obsArgs = @( + '--minimize-to-tray' + '--startreplaybuffer' + '--disable-updater' + "--profile `"$($config.ProfileName)`"" + "--collection `"$($config.SceneCollection)`"" +) + +Start-Process -FilePath $config.OBSExecutable -ArgumentList $obsArgs -WindowStyle Hidden + +# --- Launch tray icon as a separate background process --- +$trayScript = Join-Path $PSScriptRoot 'Show-ReplayTray.ps1' +Start-Process -FilePath 'powershell.exe' -ArgumentList @( + '-ExecutionPolicy', 'Bypass' + '-NonInteractive' + '-WindowStyle', 'Hidden' + '-File', $trayScript +) -WindowStyle Hidden