#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 # --- Start transcript logging --- try { $logDir = [System.Environment]::ExpandEnvironmentVariables($config.LogPath) New-Item -ItemType Directory -Path $logDir -Force -ErrorAction SilentlyContinue | Out-Null Start-Transcript -Path (Join-Path $logDir 'OBSReplayBuffer.log') -Append -ErrorAction Stop } catch { # Log path unavailable — continue without transcript } Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Starting OBS Replay Buffer for user: $env:USERNAME" # --- Verify required project files are accessible --- $requiredPaths = @( (Join-Path $PSScriptRoot '..\obs-config\global.ini') (Join-Path $PSScriptRoot "..\obs-config\scenes\$($config.SceneCollection).json") (Join-Path $PSScriptRoot 'Show-ReplayTray.ps1') $config.OBSExecutable ) foreach ($path in $requiredPaths) { if (-not (Test-Path -Path $path)) { Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [ERROR] Required path not accessible: $path" Stop-Transcript -ErrorAction SilentlyContinue exit 1 } } Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] All required paths verified." # --- Skip if OBS is already running (handles reconnect scenarios) --- if (Get-Process -Name 'obs64' -ErrorAction SilentlyContinue) { Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] OBS already running - exiting." Stop-Transcript -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 Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Created capture folder: $userCapturePath" } else { Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Capture folder exists: $userCapturePath" } # --- 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-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Detected resolution: ${width}x${height}" # --- Deploy global OBS config and WebSocket plugin config --- $obsConfigRoot = "$env:APPDATA\obs-studio" $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 # --- 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" # --- Write OBS profile (basic.ini) --- $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)" '' '[Output]' 'Mode=Simple' 'FilenameFormatting=%CCYY-%MM-%DD_%hh-%mm-%ss' '' '[SimpleOutput]' "FilePath=$obsFilePath" 'RecFormat2=mkv' '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" "BaseCY=$height" "OutputCX=$width" "OutputCY=$height" 'FPSType=1' 'FPSNum=30' 'FPSDen=1' 'ScaleType=bicubic' 'ColorFormat=NV12' 'ColorSpace=709' 'ColorRange=Partial' ) Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] OBS profile written: $profileDir\basic.ini" # --- 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 -WorkingDirectory (Split-Path $config.OBSExecutable) Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] OBS launched: $($config.OBSExecutable)" # --- 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 Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Tray icon launched." Stop-Transcript -ErrorAction SilentlyContinue