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 │ └── Invoke-ReplaySave.ps1
└── obs-config/ └── obs-config/
├── global.ini ├── global.ini
── scenes/ ── scenes/
└── ITMonitor.json └── ITMonitor.json
└── plugin_config/
└── obs-websocket/
└── config.json
``` ```
## Prerequisites ## Prerequisites
@@ -57,7 +54,7 @@ Edit `config.psd1` before deploying:
| `UNCPath` | Base UNC path for clip storage — username subfolder created automatically | `\\server\ITCaptures` | | `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%` | | `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` | | `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` | | `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` | | `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` | | `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. 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) ### 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 ## 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 - **Video encoder:** x264 (software) is used explicitly — hardware encoders (NVENC, QSV, AMF) are not available on most VDI machines
2. Manually configure a Display Capture source pointed at the primary monitor - **Display capture:** compatibility mode (BitBlt) is used instead of DXGI or WGC, which do not work reliably on virtual displays
3. Save, then copy `%APPDATA%\obs-studio\basic\scenes\ITMonitor.json` back into this repo - **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 ### 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 # 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 # 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 # 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) # Replay buffer duration in seconds (120 = 2 minutes)
BufferSeconds = 120 BufferSeconds = 120

View File

@@ -1,8 +1,16 @@
[General] [General]
EnableAutoUpdates=false EnableAutoUpdates=false
FirstRun=false FirstRun=true
[BasicWindow] [BasicWindow]
SysTrayEnabled=true SysTrayEnabled=true
SysTrayWhenStarted=true SysTrayWhenStarted=true
ShowOnStartup=false 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", "versioned_id": "monitor_capture",
"settings": { "settings": {
"capture_cursor": true, "capture_cursor": true,
"compatibility": false, "compatibility": true,
"force_sdr": false, "force_sdr": false,
"monitor": 0 "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)." Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Sent SaveReplayBuffer request (op 6)."
# Receive RequestResponse (opcode 7) # Receive RequestResponse (opcode 7)
$null = $ws.ReceiveAsync($buffer, $cts.Token).Result $recv = $ws.ReceiveAsync($buffer, $cts.Token).Result
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Received response (op 7)." $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() $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." 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 --- # --- Verify required project files are accessible ---
$requiredPaths = @( $requiredPaths = @(
(Join-Path $PSScriptRoot '..\obs-config\global.ini') (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') (Join-Path $PSScriptRoot 'Show-ReplayTray.ps1')
$config.OBSExecutable $config.OBSExecutable
) )
@@ -72,9 +72,54 @@ $sourceConfig = Join-Path $PSScriptRoot '..\obs-config'
New-Item -ItemType Directory -Path $obsConfigRoot -Force | Out-Null New-Item -ItemType Directory -Path $obsConfigRoot -Force | Out-Null
Copy-Item -Path "$sourceConfig\global.ini" -Destination "$obsConfigRoot\global.ini" -Force Copy-Item -Path "$sourceConfig\global.ini" -Destination "$obsConfigRoot\global.ini" -Force
$wsConfigDir = "$obsConfigRoot\plugin_config\obs-websocket" # --- Detect primary monitor device ID for OBS monitor capture ---
New-Item -ItemType Directory -Path $wsConfigDir -Force | Out-Null # Uses EnumDisplayDevices with EDD_GET_DEVICE_INTERFACE_NAME to get the exact
Copy-Item -Path "$sourceConfig\plugin_config\obs-websocket\config.json" -Destination "$wsConfigDir\config.json" -Force # 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" 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)" $profileDir = "$env:APPDATA\obs-studio\basic\profiles\$($config.ProfileName)"
New-Item -ItemType Directory -Path $profileDir -Force | Out-Null 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 @( Set-Content -Path "$profileDir\basic.ini" -Encoding UTF8 -Value @(
'[General]' '[General]'
"Name=$($config.ProfileName)" "Name=$($config.ProfileName)"
@@ -91,11 +139,20 @@ Set-Content -Path "$profileDir\basic.ini" -Encoding UTF8 -Value @(
'FilenameFormatting=%CCYY-%MM-%DD_%hh-%mm-%ss' 'FilenameFormatting=%CCYY-%MM-%DD_%hh-%mm-%ss'
'' ''
'[SimpleOutput]' '[SimpleOutput]'
"FilePath=$userCapturePath" "FilePath=$obsFilePath"
'RecFormat2=mkv' 'RecFormat2=mkv'
'RecQuality=HQ' 'RecQuality=Small'
'RecEncoder=x264'
'RecRB=true'
'RecRBPrefix=Replay'
"RecRBTime=$($config.BufferSeconds)" "RecRBTime=$($config.BufferSeconds)"
'RecRBSize=512' 'RecRBSize=512'
'ABitrate=160'
'VBitrate=2500'
'UseAdvanced=false'
'Preset=veryfast'
'StreamAudioEncoder=aac'
'RecAudioEncoder=aac'
'' ''
'[Video]' '[Video]'
"BaseCX=$width" "BaseCX=$width"