Had to change a bunch to get everything to work but it should be working now
This commit is contained in:
34
README.md
34
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"AlertsEnabled": false,
|
|
||||||
"AuthRequired": false,
|
|
||||||
"ServerEnabled": true,
|
|
||||||
"ServerPort": 4455
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user