diff --git a/README.md b/README.md
index 4be876b..5cb6c7e 100644
--- a/README.md
+++ b/README.md
@@ -19,9 +19,10 @@ IT support tool that runs OBS Studio's replay buffer silently on VDI sessions, g
|---|---|
| **DEM Logon Task** | Runs `Start-OBSReplayBuffer.ps1` at logon — only DEM task required |
| **`config.psd1`** | Central config — UNC path, buffer duration, OBS executable path, IT contact number |
-| **`Start-OBSReplayBuffer.ps1`** | Validates project files (with retry), deploys OBS config, creates user capture folder, writes OBS profile, launches OBS, confirms replay buffer started via log, shows startup notification, launches tray icon |
+| **`Start-OBSReplayBuffer.ps1`** | Validates project files (with retry), deploys OBS config, creates user capture folder, writes OBS profile, launches OBS, confirms replay buffer started via log, launches startup notification, launches tray icon |
| **`Show-ReplayTray.ps1`** | WinForms system tray icon — right-click menu with Save Replay / Exit |
-| **`Invoke-ReplaySave.ps1`** | Sends `SaveReplayBuffer` to OBS via WebSocket v5, verifies the file was written to disk, shows save confirmation popup |
+| **`Invoke-ReplaySave.ps1`** | Sends `SaveReplayBuffer` to OBS via WebSocket v5, verifies the file was written to disk, launches save confirmation notification |
+| **`Show-Notification.ps1`** | WPF popup launched as a separate interactive process — handles both `BufferStarted` and `ReplaySaved` notification types via `-Type` parameter |
| **`obs-config/`** | OBS config files — deployed to `%APPDATA%\obs-studio\` at logon by the script |
## Repository Structure
@@ -32,7 +33,8 @@ obs-replay-buffer/
├── scripts/
│ ├── Start-OBSReplayBuffer.ps1
│ ├── Show-ReplayTray.ps1
-│ └── Invoke-ReplaySave.ps1
+│ ├── Invoke-ReplaySave.ps1
+│ └── Show-Notification.ps1
└── obs-config/
├── global.ini
└── scenes/
diff --git a/scripts/Invoke-ReplaySave.ps1 b/scripts/Invoke-ReplaySave.ps1
index 28bbbd0..4c25307 100644
--- a/scripts/Invoke-ReplaySave.ps1
+++ b/scripts/Invoke-ReplaySave.ps1
@@ -92,88 +92,19 @@ try {
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Replay file confirmed: $($savedFile.FullName) ($fileSizeMB MB)"
# --- Notify user that the replay was saved ---
- $notifyScript = {
- param($fileName, $fileSizeMB, $contactNumber)
-
- Add-Type -AssemblyName PresentationFramework
- Add-Type -AssemblyName WindowsBase
- Add-Type -AssemblyName System.Drawing
-
- $hBitmap = [System.Drawing.SystemIcons]::Shield.ToBitmap().GetHbitmap()
- $wpfImage = [System.Windows.Interop.Imaging]::CreateBitmapSourceFromHBitmap(
- $hBitmap, [IntPtr]::Zero, [System.Windows.Int32Rect]::Empty,
- [System.Windows.Media.Imaging.BitmapSizeOptions]::FromEmptyOptions()
- )
- [System.Runtime.InteropServices.Marshal]::DeleteObject($hBitmap)
-
- [xml]$xaml = @"
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-"@
-
- $window = [System.Windows.Markup.XamlReader]::Load([System.Xml.XmlNodeReader]::new($xaml))
- $imgHeader = $window.FindName('ImgHeader')
- $txtBody = $window.FindName('TxtBody')
- $txtContact = $window.FindName('TxtContact')
- $btnOK = $window.FindName('BtnOK')
-
- $imgHeader.Source = $wpfImage
- $txtContact.Text = "Questions? Contact IT at $contactNumber"
-
- $txtBody.Inlines.Add("Your replay has been saved ($fileSizeMB MB):`n")
- $bold1 = New-Object System.Windows.Documents.Run($fileName)
- $bold1.FontWeight = [System.Windows.FontWeights]::Bold
- $txtBody.Inlines.Add($bold1)
- $txtBody.Inlines.Add("`n`nIT can retrieve this file if needed for a support case.")
-
- $script:tick = 30
- $btnOK.Content = "OK (30)"
- $timer = New-Object System.Windows.Threading.DispatcherTimer
- $timer.Interval = [TimeSpan]::FromSeconds(1)
- $timer.Add_Tick({
- $script:tick--
- $btnOK.Content = "OK ($script:tick)"
- if ($script:tick -le 0) {
- $timer.Stop()
- $window.Close()
- }
- })
- $timer.Start()
- $btnOK.Add_Click({ $timer.Stop(); $window.Close() })
- $window.ShowDialog() | Out-Null
- }
-
- $notifyRunspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
- $notifyRunspace.ApartmentState = [System.Threading.ApartmentState]::STA
- $notifyRunspace.Open()
- $notifyPS = [System.Management.Automation.PowerShell]::Create()
- $notifyPS.Runspace = $notifyRunspace
- $notifyPS.AddScript($notifyScript) | Out-Null
- $notifyPS.AddArgument($savedFile.Name) | Out-Null
- $notifyPS.AddArgument($fileSizeMB) | Out-Null
- $notifyPS.AddArgument($config.ITContactNumber) | Out-Null
- $notifyPS.Invoke()
- $notifyPS.Dispose()
- $notifyRunspace.Close()
- Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] User notification dismissed."
+ $notifyScript = Join-Path $PSScriptRoot 'Show-Notification.ps1'
+ Start-Process -FilePath 'powershell.exe' -ArgumentList @(
+ '-STA'
+ '-ExecutionPolicy', 'Bypass'
+ '-NonInteractive'
+ '-WindowStyle', 'Hidden'
+ '-File', $notifyScript
+ '-Type', 'ReplaySaved'
+ '-ContactNumber', $config.ITContactNumber
+ '-FileName', $savedFile.Name
+ '-FileSizeMB', $fileSizeMB
+ ) -WindowStyle Hidden
+ Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] User notification launched."
return $true
}
diff --git a/scripts/Show-Notification.ps1 b/scripts/Show-Notification.ps1
new file mode 100644
index 0000000..977c00c
--- /dev/null
+++ b/scripts/Show-Notification.ps1
@@ -0,0 +1,141 @@
+#Requires -Version 5.1
+<#
+.SYNOPSIS
+ Shows a WPF notification popup for the OBS Replay Buffer tool.
+ Must be launched via Start-Process with powershell.exe -STA.
+
+.PARAMETER Type
+ 'BufferStarted' — shown at logon when the replay buffer is active.
+ 'ReplaySaved' — shown after a successful replay save.
+#>
+
+param(
+ [Parameter(Mandatory)]
+ [ValidateSet('BufferStarted', 'ReplaySaved')]
+ [string]$Type,
+
+ [string]$ContactNumber = '',
+ [int]$BufferMinutes = 0,
+ [string]$FileName = '',
+ [double]$FileSizeMB = 0
+)
+
+Add-Type -AssemblyName PresentationFramework
+Add-Type -AssemblyName WindowsBase
+Add-Type -AssemblyName System.Drawing
+
+$hBitmap = [System.Drawing.SystemIcons]::Shield.ToBitmap().GetHbitmap()
+$wpfImage = [System.Windows.Interop.Imaging]::CreateBitmapSourceFromHBitmap(
+ $hBitmap, [IntPtr]::Zero, [System.Windows.Int32Rect]::Empty,
+ [System.Windows.Media.Imaging.BitmapSizeOptions]::FromEmptyOptions()
+)
+[System.Runtime.InteropServices.Marshal]::DeleteObject($hBitmap)
+
+if ($Type -eq 'BufferStarted') {
+ [xml]$xaml = @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"@
+
+ $window = [System.Windows.Markup.XamlReader]::Load([System.Xml.XmlNodeReader]::new($xaml))
+ $imgHeader = $window.FindName('ImgHeader')
+ $imgTray = $window.FindName('ImgTray')
+ $txtBody = $window.FindName('TxtBody')
+ $txtContact = $window.FindName('TxtContact')
+ $btnOK = $window.FindName('BtnOK')
+
+ $imgHeader.Source = $wpfImage
+ $imgTray.Source = $wpfImage
+ $txtContact.Text = "Questions? Contact IT at $ContactNumber"
+
+ $txtBody.Inlines.Add("Your screen is being recorded into a rolling $BufferMinutes-minute replay buffer. Nothing is saved until you request it.`n`nTo save a clip, ")
+ $bold1 = New-Object System.Windows.Documents.Run('right-click the shield icon')
+ $bold1.FontWeight = [System.Windows.FontWeights]::Bold
+ $txtBody.Inlines.Add($bold1)
+ $txtBody.Inlines.Add(' in your system tray (bottom-right of your screen) and select ')
+ $bold2 = New-Object System.Windows.Documents.Run('Save Replay')
+ $bold2.FontWeight = [System.Windows.FontWeights]::Bold
+ $txtBody.Inlines.Add($bold2)
+ $txtBody.Inlines.Add('.')
+
+} else {
+ [xml]$xaml = @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"@
+
+ $window = [System.Windows.Markup.XamlReader]::Load([System.Xml.XmlNodeReader]::new($xaml))
+ $imgHeader = $window.FindName('ImgHeader')
+ $txtBody = $window.FindName('TxtBody')
+ $txtContact = $window.FindName('TxtContact')
+ $btnOK = $window.FindName('BtnOK')
+
+ $imgHeader.Source = $wpfImage
+ $txtContact.Text = "Questions? Contact IT at $ContactNumber"
+
+ $txtBody.Inlines.Add("Your replay has been saved ($FileSizeMB MB):`n")
+ $bold1 = New-Object System.Windows.Documents.Run($FileName)
+ $bold1.FontWeight = [System.Windows.FontWeights]::Bold
+ $txtBody.Inlines.Add($bold1)
+ $txtBody.Inlines.Add("`n`nIT can retrieve this file if needed for a support case.")
+}
+
+# Countdown timer — auto-dismisses after 30 seconds
+$script:tick = 30
+$btnOK.Content = "OK (30)"
+$timer = New-Object System.Windows.Threading.DispatcherTimer
+$timer.Interval = [TimeSpan]::FromSeconds(1)
+$timer.Add_Tick({
+ $script:tick--
+ $btnOK.Content = "OK ($script:tick)"
+ if ($script:tick -le 0) {
+ $timer.Stop()
+ $window.Close()
+ }
+})
+$timer.Start()
+$btnOK.Add_Click({ $timer.Stop(); $window.Close() })
+$window.ShowDialog() | Out-Null
diff --git a/scripts/Start-OBSReplayBuffer.ps1 b/scripts/Start-OBSReplayBuffer.ps1
index 7de1295..948aad9 100644
--- a/scripts/Start-OBSReplayBuffer.ps1
+++ b/scripts/Start-OBSReplayBuffer.ps1
@@ -239,104 +239,19 @@ while (-not $rbStarted) {
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] Replay Buffer confirmed started."
# --- Notify user that the replay buffer is active ---
-# Runs in a dedicated STA runspace because WPF requires STA apartment state
-$notifyScript = {
- param($contactNumber, $bufferMinutes)
-
- Add-Type -AssemblyName PresentationFramework
- Add-Type -AssemblyName WindowsBase
- Add-Type -AssemblyName System.Drawing
-
- # Convert the Shield system icon (same one used by the tray) to a WPF BitmapSource
- $hBitmap = [System.Drawing.SystemIcons]::Shield.ToBitmap().GetHbitmap()
- $wpfImage = [System.Windows.Interop.Imaging]::CreateBitmapSourceFromHBitmap(
- $hBitmap, [IntPtr]::Zero, [System.Windows.Int32Rect]::Empty,
- [System.Windows.Media.Imaging.BitmapSizeOptions]::FromEmptyOptions()
- )
- [System.Runtime.InteropServices.Marshal]::DeleteObject($hBitmap)
-
- [xml]$xaml = @"
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-"@
-
- $window = [System.Windows.Markup.XamlReader]::Load([System.Xml.XmlNodeReader]::new($xaml))
- $imgHeader = $window.FindName('ImgHeader')
- $imgTray = $window.FindName('ImgTray')
- $txtBody = $window.FindName('TxtBody')
- $txtContact = $window.FindName('TxtContact')
- $btnOK = $window.FindName('BtnOK')
-
- $imgHeader.Source = $wpfImage
- $imgTray.Source = $wpfImage
- $txtContact.Text = "Questions? Contact IT at $contactNumber"
-
- # Build body text with inline bold runs
- $txtBody.Inlines.Add("Your screen is being recorded into a rolling $bufferMinutes-minute replay buffer. Nothing is saved until you request it.`n`nTo save a clip, ")
- $bold1 = New-Object System.Windows.Documents.Run('right-click the shield icon')
- $bold1.FontWeight = [System.Windows.FontWeights]::Bold
- $txtBody.Inlines.Add($bold1)
- $txtBody.Inlines.Add(' in your system tray (bottom-right of your screen) and select ')
- $bold2 = New-Object System.Windows.Documents.Run('Save Replay')
- $bold2.FontWeight = [System.Windows.FontWeights]::Bold
- $txtBody.Inlines.Add($bold2)
- $txtBody.Inlines.Add('.')
-
- # Countdown timer — auto-dismisses after 30 seconds
- $script:tick = 30
- $btnOK.Content = "OK (30)"
- $timer = New-Object System.Windows.Threading.DispatcherTimer
- $timer.Interval = [TimeSpan]::FromSeconds(1)
- $timer.Add_Tick({
- $script:tick--
- $btnOK.Content = "OK ($script:tick)"
- if ($script:tick -le 0) {
- $timer.Stop()
- $window.Close()
- }
- })
- $timer.Start()
- $btnOK.Add_Click({ $timer.Stop(); $window.Close() })
- $window.ShowDialog() | Out-Null
-}
-
-$notifyRunspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
-$notifyRunspace.ApartmentState = [System.Threading.ApartmentState]::STA
-$notifyRunspace.Open()
-$notifyPS = [System.Management.Automation.PowerShell]::Create()
-$notifyPS.Runspace = $notifyRunspace
-$notifyPS.AddScript($notifyScript) | Out-Null
-$notifyPS.AddArgument($config.ITContactNumber) | Out-Null
-$notifyPS.AddArgument([math]::Round($config.BufferSeconds / 60)) | Out-Null
-$notifyPS.Invoke()
-$notifyPS.Dispose()
-$notifyRunspace.Close()
-Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [INFO] User notification dismissed."
+$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'