Had to change a bunch to get everything to work but it should be working now

This commit is contained in:
2026-03-27 17:03:09 -07:00
parent 2bf42e7e7f
commit d107f85d6f
7 changed files with 104 additions and 30 deletions

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
{
"AlertsEnabled": false,
"AuthRequired": false,
"ServerEnabled": true,
"ServerPort": 4455
}

View File

@@ -12,7 +12,7 @@
"versioned_id": "monitor_capture",
"settings": {
"capture_cursor": true,
"compatibility": false,
"compatibility": true,
"force_sdr": false,
"monitor": 0
},

View File

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

View File

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