Initial commit

This commit is contained in:
2026-03-27 11:16:39 -07:00
commit 67d5c9ca5f
8 changed files with 470 additions and 0 deletions

129
README.md Normal file
View File

@@ -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\<username>\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\<username>\` 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.

17
config.psd1 Normal file
View File

@@ -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'
}

7
obs-config/global.ini Normal file
View File

@@ -0,0 +1,7 @@
[General]
EnableAutoUpdates=false
[BasicWindow]
SysTrayEnabled=true
SysTrayWhenStarted=true
ShowOnStartup=false

View File

@@ -0,0 +1,6 @@
{
"alerts_enabled": false,
"auth_required": false,
"server_enabled": true,
"server_port": 4455
}

View File

@@ -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
}

View File

@@ -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() }
}

View File

@@ -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()

View File

@@ -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