From d107f85d6f4936a90f05a4502270a679fd684ec8 Mon Sep 17 00:00:00 2001 From: Derek Cooper Date: Fri, 27 Mar 2026 17:03:09 -0700 Subject: [PATCH] Had to change a bunch to get everything to work but it should be working now --- README.md | 34 +++++---- config.psd1 | 4 +- obs-config/global.ini | 10 ++- .../plugin_config/obs-websocket/config.json | 6 -- obs-config/scenes/ITMonitor.json | 2 +- scripts/Invoke-ReplaySave.ps1 | 9 ++- scripts/Start-OBSReplayBuffer.ps1 | 69 +++++++++++++++++-- 7 files changed, 104 insertions(+), 30 deletions(-) delete mode 100644 obs-config/plugin_config/obs-websocket/config.json diff --git a/README.md b/README.md index 05f3473..0edf12f 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,8 @@ obs-replay-buffer/ │ └── Invoke-ReplaySave.ps1 └── obs-config/ ├── global.ini - ├── scenes/ - │ └── ITMonitor.json - └── plugin_config/ - └── obs-websocket/ - └── config.json + └── scenes/ + └── ITMonitor.json ``` ## Prerequisites @@ -57,7 +54,7 @@ Edit `config.psd1` before deploying: | `UNCPath` | Base UNC path for clip storage — username subfolder created automatically | `\\server\ITCaptures` | | `LogPath` | Directory path for script logs — supports `%USERNAME%` and other environment variables; each script writes its own log file | `\\server\ITLogs\%USERNAME%` | | `BufferSeconds` | Replay buffer duration in seconds | `120` | -| `WebSocketPort` | OBS WebSocket port — must match `obs-config/plugin_config/obs-websocket/config.json` | `4455` | +| `WebSocketPort` | OBS WebSocket port — must match `[OBSWebSocket]` in `obs-config/global.ini` | `4455` | | `OBSExecutable` | Full path to `obs64.exe` on the target 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` | @@ -82,7 +79,7 @@ powershell.exe -ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File This is the only DEM task needed. The script handles all config file deployment, folder creation, and launching both OBS and the tray icon. It will exit cleanly (code 1) if any required project file is inaccessible, or exit 0 if OBS is already running. -> **No DEM file transfer tasks are needed.** The script deploys `global.ini` and the WebSocket config from the repo to `%APPDATA%\obs-studio\` at each logon. `basic.ini` is written dynamically so the correct UNC path and primary monitor resolution are baked in per session. +> **No DEM file transfer tasks are needed.** The script deploys `global.ini` and the scene collection from the repo to `%APPDATA%\obs-studio\` at each logon. `basic.ini` is written dynamically so the correct UNC path, primary monitor resolution, and monitor device ID are baked in per session. ### Step 3 — OBS Studio (App Volumes or native install) @@ -107,13 +104,26 @@ Update `BufferSeconds` in `config.psd1`. The value is written into the OBS profi ## Notes -### Scene Collection JSON +### VDI Compatibility -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: +This tool is designed for VDI environments running without a physical GPU (e.g. Microsoft Basic Render Driver): -1. Launch OBS normally on a test machine -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 +- **Video encoder:** x264 (software) is used explicitly — hardware encoders (NVENC, QSV, AMF) are not available on most VDI machines +- **Display capture:** compatibility mode (BitBlt) is used instead of DXGI or WGC, which do not work reliably on virtual displays +- **Monitor ID:** the startup script uses `EnumDisplayDevices` with `EDD_GET_DEVICE_INTERFACE_NAME` to detect the active session's display device path at logon — this handles RDP/VDI sessions where the display UID varies per user or session + +### WebSocket Settings + +OBS 29+ stores WebSocket server settings in `global.ini` under `[OBSWebSocket]`, not in a separate `plugin_config` JSON file. The `global.ini` in this repo includes the WebSocket configuration directly: + +```ini +[OBSWebSocket] +FirstLoad=false +ServerEnabled=true +ServerPort=4455 +AlertsEnabled=false +AuthRequired=false +``` ### OBS Tray Icon vs. IT Tray Icon diff --git a/config.psd1 b/config.psd1 index 0da9312..68d16db 100644 --- a/config.psd1 +++ b/config.psd1 @@ -1,10 +1,10 @@ @{ # Base UNC path for saved clips — a subfolder per username is created automatically - UNCPath = '\\server\ITCaptures' + UNCPath = '\\server\share' # Directory path for script logs — supports %USERNAME% and other environment variables # Each script writes its own log: OBSReplayBuffer.log, OBSReplayBuffer-Tray.log, OBSReplayBuffer-Save.log - LogPath = '\\server\ITLogs\%USERNAME%' + LogPath = '\\server\share' # Replay buffer duration in seconds (120 = 2 minutes) BufferSeconds = 120 diff --git a/obs-config/global.ini b/obs-config/global.ini index 84ee45a..b955cce 100644 --- a/obs-config/global.ini +++ b/obs-config/global.ini @@ -1,8 +1,16 @@ [General] EnableAutoUpdates=false -FirstRun=false +FirstRun=true [BasicWindow] SysTrayEnabled=true SysTrayWhenStarted=true ShowOnStartup=false +ConfigOnNewProfile=false + +[OBSWebSocket] +FirstLoad=false +ServerEnabled=true +ServerPort=4455 +AlertsEnabled=false +AuthRequired=false diff --git a/obs-config/plugin_config/obs-websocket/config.json b/obs-config/plugin_config/obs-websocket/config.json deleted file mode 100644 index 339e05d..0000000 --- a/obs-config/plugin_config/obs-websocket/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "AlertsEnabled": false, - "AuthRequired": false, - "ServerEnabled": true, - "ServerPort": 4455 -} diff --git a/obs-config/scenes/ITMonitor.json b/obs-config/scenes/ITMonitor.json index d464be3..00cb0a3 100644 --- a/obs-config/scenes/ITMonitor.json +++ b/obs-config/scenes/ITMonitor.json @@ -12,7 +12,7 @@ "versioned_id": "monitor_capture", "settings": { "capture_cursor": true, - "compatibility": false, + "compatibility": true, "force_sdr": false, "monitor": 0 }, diff --git a/scripts/Invoke-ReplaySave.ps1 b/scripts/Invoke-ReplaySave.ps1 index 2f8c248..c36f1b6 100644 --- a/scripts/Invoke-ReplaySave.ps1 +++ b/scripts/Invoke-ReplaySave.ps1 @@ -62,8 +62,13 @@ try { Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Sent SaveReplayBuffer request (op 6)." # Receive RequestResponse (opcode 7) - $null = $ws.ReceiveAsync($buffer, $cts.Token).Result - Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Received response (op 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. Save completed successfully." diff --git a/scripts/Start-OBSReplayBuffer.ps1 b/scripts/Start-OBSReplayBuffer.ps1 index a69cb42..eb02e8a 100644 --- a/scripts/Start-OBSReplayBuffer.ps1 +++ b/scripts/Start-OBSReplayBuffer.ps1 @@ -29,7 +29,7 @@ Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Starting OBS Replay # --- Verify required project files are accessible --- $requiredPaths = @( (Join-Path $PSScriptRoot '..\obs-config\global.ini') - (Join-Path $PSScriptRoot '..\obs-config\plugin_config\obs-websocket\config.json') + (Join-Path $PSScriptRoot "..\obs-config\scenes\$($config.SceneCollection).json") (Join-Path $PSScriptRoot 'Show-ReplayTray.ps1') $config.OBSExecutable ) @@ -72,9 +72,54 @@ $sourceConfig = Join-Path $PSScriptRoot '..\obs-config' New-Item -ItemType Directory -Path $obsConfigRoot -Force | Out-Null Copy-Item -Path "$sourceConfig\global.ini" -Destination "$obsConfigRoot\global.ini" -Force -$wsConfigDir = "$obsConfigRoot\plugin_config\obs-websocket" -New-Item -ItemType Directory -Path $wsConfigDir -Force | Out-Null -Copy-Item -Path "$sourceConfig\plugin_config\obs-websocket\config.json" -Destination "$wsConfigDir\config.json" -Force +# --- Detect primary monitor device ID for OBS monitor capture --- +# Uses EnumDisplayDevices with EDD_GET_DEVICE_INTERFACE_NAME to get the exact +# device interface path for the active session's display — same source OBS uses. +Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; +public class DisplayHelper { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct DISPLAY_DEVICE { + public int cb; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string DeviceName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceString; + public int StateFlags; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceID; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceKey; + } + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern bool EnumDisplayDevices(string lpDevice, uint iDevNum, ref DISPLAY_DEVICE lpDisplayDevice, uint dwFlags); + public static string GetPrimaryMonitorInterfacePath() { + var adapter = new DISPLAY_DEVICE(); adapter.cb = Marshal.SizeOf(adapter); + for (uint i = 0; EnumDisplayDevices(null, i, ref adapter, 0); i++) { + if ((adapter.StateFlags & 0x4) != 0) { // DISPLAY_DEVICE_PRIMARY_DEVICE + var monitor = new DISPLAY_DEVICE(); monitor.cb = Marshal.SizeOf(monitor); + if (EnumDisplayDevices(adapter.DeviceName, 0, ref monitor, 0x1)) // EDD_GET_DEVICE_INTERFACE_NAME + return monitor.DeviceID; + } + } + return ""; + } +} +"@ + +$monitorDeviceId = [DisplayHelper]::GetPrimaryMonitorInterfacePath() +if ($monitorDeviceId) { + Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Detected monitor ID: $monitorDeviceId" +} else { + Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [WARN] No monitor detected - monitor_id will be empty." +} + +# --- Deploy scene collection with dynamic monitor ID --- +$scenesDir = "$obsConfigRoot\basic\scenes" +New-Item -ItemType Directory -Path $scenesDir -Force | Out-Null +$sceneJson = Get-Content -Path "$sourceConfig\scenes\$($config.SceneCollection).json" -Raw | ConvertFrom-Json +$sceneJson.sources | Where-Object { $_.id -eq 'monitor_capture' } | ForEach-Object { + $_.settings | Add-Member -MemberType NoteProperty -Name 'monitor_id' -Value $monitorDeviceId -Force +} +$sceneJson | ConvertTo-Json -Depth 20 | Set-Content -Path "$scenesDir\$($config.SceneCollection).json" -Encoding UTF8 + Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] OBS config files deployed to: $obsConfigRoot" @@ -82,6 +127,9 @@ Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] OBS config files de $profileDir = "$env:APPDATA\obs-studio\basic\profiles\$($config.ProfileName)" New-Item -ItemType Directory -Path $profileDir -Force | Out-Null +# OBS strips one leading backslash from paths — prepend an extra one for UNC paths +$obsFilePath = if ($userCapturePath.StartsWith('\\')) { '\' + $userCapturePath } else { $userCapturePath } + Set-Content -Path "$profileDir\basic.ini" -Encoding UTF8 -Value @( '[General]' "Name=$($config.ProfileName)" @@ -91,11 +139,20 @@ Set-Content -Path "$profileDir\basic.ini" -Encoding UTF8 -Value @( 'FilenameFormatting=%CCYY-%MM-%DD_%hh-%mm-%ss' '' '[SimpleOutput]' - "FilePath=$userCapturePath" + "FilePath=$obsFilePath" 'RecFormat2=mkv' - 'RecQuality=HQ' + 'RecQuality=Small' + 'RecEncoder=x264' + 'RecRB=true' + 'RecRBPrefix=Replay' "RecRBTime=$($config.BufferSeconds)" 'RecRBSize=512' + 'ABitrate=160' + 'VBitrate=2500' + 'UseAdvanced=false' + 'Preset=veryfast' + 'StreamAudioEncoder=aac' + 'RecAudioEncoder=aac' '' '[Video]' "BaseCX=$width"