Files
OBS-Replay-Buffer-for-IT-Su…/scripts/Start-OBSReplayBuffer.ps1

258 lines
11 KiB
PowerShell

#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') -Force -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) {
$timeout = [datetime]::UtcNow.AddMinutes(2)
while (-not (Test-Path -Path $path)) {
if ([datetime]::UtcNow -ge $timeout) {
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [ERROR] Required path not accessible after 2 min timeout: $path"
Stop-Transcript -ErrorAction SilentlyContinue
exit 1
}
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [WARN] Path not yet accessible, retrying: $path"
Start-Sleep -Seconds 5
}
}
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
)
$obsLaunchTime = Get-Date
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)"
# --- Wait for OBS process to appear ---
$obsTimeout = [datetime]::UtcNow.AddSeconds(30)
while (-not (Get-Process -Name 'obs64' -ErrorAction SilentlyContinue)) {
if ([datetime]::UtcNow -ge $obsTimeout) {
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [ERROR] OBS process did not appear within 30 seconds."
Stop-Transcript -ErrorAction SilentlyContinue
exit 1
}
Start-Sleep -Seconds 2
}
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] OBS process detected."
# --- Locate the OBS log file created for this session ---
$obsLogDir = "$env:APPDATA\obs-studio\logs"
$logTimeout = [datetime]::UtcNow.AddSeconds(30)
$obsLogFile = $null
while (-not $obsLogFile) {
if ([datetime]::UtcNow -ge $logTimeout) {
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [ERROR] OBS did not create a log file within 30 seconds."
Stop-Transcript -ErrorAction SilentlyContinue
exit 1
}
$obsLogFile = Get-ChildItem -Path $obsLogDir -Filter '*.txt' -ErrorAction SilentlyContinue |
Where-Object { $_.CreationTime -ge $obsLaunchTime } |
Sort-Object CreationTime -Descending |
Select-Object -First 1
if (-not $obsLogFile) { Start-Sleep -Seconds 2 }
}
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Monitoring OBS log: $($obsLogFile.FullName)"
# --- Poll OBS log for replay buffer start confirmation ---
# Matches "Replay Buffer Output started" (older OBS) and "[output 'ReplayBuffer']: started" (newer OBS)
$rbTimeout = [datetime]::UtcNow.AddMinutes(2)
$rbStarted = $false
while (-not $rbStarted) {
if ([datetime]::UtcNow -ge $rbTimeout) {
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [ERROR] Replay Buffer did not start within 2 minutes. See: $($obsLogFile.FullName)"
Stop-Transcript -ErrorAction SilentlyContinue
exit 1
}
if (Select-String -Path $obsLogFile.FullName -Pattern 'replay.buffer.*start' -Quiet -CaseSensitive:$false -ErrorAction SilentlyContinue) {
$rbStarted = $true
} else {
Start-Sleep -Seconds 3
}
}
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Replay Buffer confirmed started."
# --- Notify user that the replay buffer is active ---
$notifyScript = Join-Path $PSScriptRoot 'Show-Notification.ps1'
$bufferMinutes = [math]::Round($config.BufferSeconds / 60)
Start-Process -FilePath 'powershell.exe' -ArgumentList "-STA -ExecutionPolicy Bypass -NonInteractive -WindowStyle Hidden -File `"$notifyScript`" -Type BufferStarted -ContactNumber `"$($config.ITContactNumber)`" -BufferMinutes $bufferMinutes" -WindowStyle Hidden
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] User notification launched."
# --- 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