Skip to content

Instantly share code, notes, and snippets.

@Geofferey
Created July 13, 2026 07:52
Show Gist options
  • Select an option

  • Save Geofferey/ca22e4a839bbd92af1da7d87bc97ac06 to your computer and use it in GitHub Desktop.

Select an option

Save Geofferey/ca22e4a839bbd92af1da7d87bc97ac06 to your computer and use it in GitHub Desktop.
GStreamer-Basic-WHIP-GUI
#requires -Version 5.1
<#
.SYNOPSIS
Minimal Windows GUI wrapper for a low-latency GStreamer WHIP desktop stream.
.DESCRIPTION
Uses the proven pipeline:
D3D11 desktop capture -> D3D11 conversion -> NVENC H.264 -> WHIP
Optional audio paths:
Windows desktop loopback -> Opus
Default microphone -> Opus
Desktop + microphone -> audiomixer -> Opus
The GUI launches gst-launch-1.0.exe as a separate process. Stop/Restart kills
the complete process tree so capture, encoder, clocks, and WHIP state are rebuilt.
#>
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
$script:AppName = 'GStreamer Basic WHIP Streamer'
$script:ConfigDirectory = Join-Path $env:APPDATA 'GStreamerBasicWhipStreamer'
$script:ConfigPath = Join-Path $script:ConfigDirectory 'settings.json'
$script:LogDirectory = Join-Path $env:LOCALAPPDATA 'GStreamerBasicWhipStreamer\Logs'
$script:GstProcess = $null
$script:StopRequested = $false
$script:RestartAt = $null
$script:StdOutPath = $null
$script:StdErrPath = $null
$script:StdOutPosition = [int64]0
$script:StdErrPosition = [int64]0
function Find-GstLaunch {
$command = Get-Command 'gst-launch-1.0.exe' -ErrorAction SilentlyContinue
if ($command) {
return $command.Source
}
$candidates = @(
(Join-Path $env:ProgramFiles 'gstreamer\1.0\msvc_x86_64\bin\gst-launch-1.0.exe'),
(Join-Path $env:ProgramFiles 'gstreamer\1.0\mingw_x86_64\bin\gst-launch-1.0.exe'),
(Join-Path ${env:ProgramFiles(x86)} 'gstreamer\1.0\msvc_x86_64\bin\gst-launch-1.0.exe'),
(Join-Path ${env:ProgramFiles(x86)} 'gstreamer\1.0\mingw_x86_64\bin\gst-launch-1.0.exe')
)
foreach ($candidate in $candidates) {
if ($candidate -and (Test-Path -LiteralPath $candidate)) {
return $candidate
}
}
return ''
}
function Format-InvariantNumber {
param(
[Parameter(Mandatory)]
[double]$Value,
[string]$Format = '0.00'
)
return $Value.ToString($Format, [System.Globalization.CultureInfo]::InvariantCulture)
}
function Quote-GstValue {
param([Parameter(Mandatory)][string]$Value)
$escaped = $Value.Replace('\', '\\').Replace('"', '\"')
return '"' + $escaped + '"'
}
function Read-NewLogText {
param(
[string]$Path,
[ref]$Position
)
if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path)) {
return ''
}
try {
$stream = New-Object System.IO.FileStream(
$Path,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read,
[System.IO.FileShare]::ReadWrite
)
try {
if ($Position.Value -gt $stream.Length) {
$Position.Value = [int64]0
}
$null = $stream.Seek($Position.Value, [System.IO.SeekOrigin]::Begin)
$reader = New-Object System.IO.StreamReader($stream)
try {
$text = $reader.ReadToEnd()
$Position.Value = $stream.Position
return $text
}
finally {
$reader.Dispose()
}
}
finally {
$stream.Dispose()
}
}
catch {
return ''
}
}
$form = New-Object System.Windows.Forms.Form
$form.Text = $script:AppName
$form.StartPosition = 'CenterScreen'
$form.Size = New-Object System.Drawing.Size(930, 790)
$form.MinimumSize = New-Object System.Drawing.Size(850, 700)
$form.Font = New-Object System.Drawing.Font('Segoe UI', 9)
$toolTip = New-Object System.Windows.Forms.ToolTip
$toolTip.AutoPopDelay = 12000
$toolTip.InitialDelay = 400
$toolTip.ReshowDelay = 100
$settingsGroup = New-Object System.Windows.Forms.GroupBox
$settingsGroup.Text = 'Stream Settings'
$settingsGroup.Location = New-Object System.Drawing.Point(10, 10)
$settingsGroup.Size = New-Object System.Drawing.Size(895, 315)
$settingsGroup.Anchor = 'Top,Left,Right'
$form.Controls.Add($settingsGroup)
function Add-Label {
param(
[System.Windows.Forms.Control]$Parent,
[string]$Text,
[int]$X,
[int]$Y,
[int]$Width = 120
)
$label = New-Object System.Windows.Forms.Label
$label.Text = $Text
$label.Location = New-Object System.Drawing.Point($X, $Y)
$label.Size = New-Object System.Drawing.Size($Width, 22)
$label.TextAlign = 'MiddleLeft'
$Parent.Controls.Add($label)
return $label
}
$null = Add-Label $settingsGroup 'GStreamer executable' 15 25 130
$txtGstPath = New-Object System.Windows.Forms.TextBox
$txtGstPath.Location = New-Object System.Drawing.Point(150, 25)
$txtGstPath.Size = New-Object System.Drawing.Size(575, 23)
$txtGstPath.Anchor = 'Top,Left,Right'
$txtGstPath.Text = Find-GstLaunch
$settingsGroup.Controls.Add($txtGstPath)
$btnBrowseGst = New-Object System.Windows.Forms.Button
$btnBrowseGst.Text = 'Browse...'
$btnBrowseGst.Location = New-Object System.Drawing.Point(735, 23)
$btnBrowseGst.Size = New-Object System.Drawing.Size(70, 27)
$btnBrowseGst.Anchor = 'Top,Right'
$settingsGroup.Controls.Add($btnBrowseGst)
$btnCheckGst = New-Object System.Windows.Forms.Button
$btnCheckGst.Text = 'Check'
$btnCheckGst.Location = New-Object System.Drawing.Point(810, 23)
$btnCheckGst.Size = New-Object System.Drawing.Size(65, 27)
$btnCheckGst.Anchor = 'Top,Right'
$settingsGroup.Controls.Add($btnCheckGst)
$null = Add-Label $settingsGroup 'WHIP endpoint' 15 60 130
$txtWhipUrl = New-Object System.Windows.Forms.TextBox
$txtWhipUrl.Location = New-Object System.Drawing.Point(150, 60)
$txtWhipUrl.Size = New-Object System.Drawing.Size(725, 23)
$txtWhipUrl.Anchor = 'Top,Left,Right'
$txtWhipUrl.Text = 'http://10.0.0.25:8889/live/whip'
$settingsGroup.Controls.Add($txtWhipUrl)
$toolTip.SetToolTip($txtWhipUrl, 'MediaMTX example: http://server:8889/live/whip. Cloud providers may use an HTTPS publishing URL.')
$null = Add-Label $settingsGroup 'Monitor index' 15 96 100
$numMonitor = New-Object System.Windows.Forms.NumericUpDown
$numMonitor.Location = New-Object System.Drawing.Point(115, 96)
$numMonitor.Size = New-Object System.Drawing.Size(65, 23)
$numMonitor.Minimum = -1
$numMonitor.Maximum = 32
$numMonitor.Value = -1
$settingsGroup.Controls.Add($numMonitor)
$toolTip.SetToolTip($numMonitor, '-1 uses the primary monitor. Other values select a monitor by GStreamer index.')
$chkCursor = New-Object System.Windows.Forms.CheckBox
$chkCursor.Text = 'Show cursor'
$chkCursor.Location = New-Object System.Drawing.Point(195, 96)
$chkCursor.Size = New-Object System.Drawing.Size(105, 23)
$chkCursor.Checked = $true
$settingsGroup.Controls.Add($chkCursor)
$chkAutoRestart = New-Object System.Windows.Forms.CheckBox
$chkAutoRestart.Text = 'Auto-restart if pipeline exits'
$chkAutoRestart.Location = New-Object System.Drawing.Point(315, 96)
$chkAutoRestart.Size = New-Object System.Drawing.Size(190, 23)
$chkAutoRestart.Checked = $true
$settingsGroup.Controls.Add($chkAutoRestart)
$toolTip.SetToolTip($chkAutoRestart, 'Rebuilds the entire GStreamer process after an unexpected exit.')
$chkVerbose = New-Object System.Windows.Forms.CheckBox
$chkVerbose.Text = 'Verbose GStreamer output'
$chkVerbose.Location = New-Object System.Drawing.Point(520, 96)
$chkVerbose.Size = New-Object System.Drawing.Size(180, 23)
$chkVerbose.Checked = $false
$settingsGroup.Controls.Add($chkVerbose)
$null = Add-Label $settingsGroup 'Width' 15 132 55
$numWidth = New-Object System.Windows.Forms.NumericUpDown
$numWidth.Location = New-Object System.Drawing.Point(70, 132)
$numWidth.Size = New-Object System.Drawing.Size(85, 23)
$numWidth.Minimum = 320
$numWidth.Maximum = 7680
$numWidth.Increment = 16
$numWidth.Value = 1920
$settingsGroup.Controls.Add($numWidth)
$null = Add-Label $settingsGroup 'Height' 170 132 55
$numHeight = New-Object System.Windows.Forms.NumericUpDown
$numHeight.Location = New-Object System.Drawing.Point(225, 132)
$numHeight.Size = New-Object System.Drawing.Size(85, 23)
$numHeight.Minimum = 240
$numHeight.Maximum = 4320
$numHeight.Increment = 16
$numHeight.Value = 1080
$settingsGroup.Controls.Add($numHeight)
$null = Add-Label $settingsGroup 'FPS' 330 132 40
$numFps = New-Object System.Windows.Forms.NumericUpDown
$numFps.Location = New-Object System.Drawing.Point(370, 132)
$numFps.Size = New-Object System.Drawing.Size(65, 23)
$numFps.Minimum = 1
$numFps.Maximum = 240
$numFps.Value = 60
$settingsGroup.Controls.Add($numFps)
$null = Add-Label $settingsGroup 'Video bitrate (kbps)' 455 132 125
$numVideoBitrate = New-Object System.Windows.Forms.NumericUpDown
$numVideoBitrate.Location = New-Object System.Drawing.Point(585, 132)
$numVideoBitrate.Size = New-Object System.Drawing.Size(95, 23)
$numVideoBitrate.Minimum = 250
$numVideoBitrate.Maximum = 100000
$numVideoBitrate.Increment = 500
$numVideoBitrate.Value = 12000
$settingsGroup.Controls.Add($numVideoBitrate)
$null = Add-Label $settingsGroup 'GOP (seconds)' 700 132 95
$numGopSeconds = New-Object System.Windows.Forms.NumericUpDown
$numGopSeconds.Location = New-Object System.Drawing.Point(795, 132)
$numGopSeconds.Size = New-Object System.Drawing.Size(65, 23)
$numGopSeconds.Minimum = 1
$numGopSeconds.Maximum = 10
$numGopSeconds.Value = 1
$settingsGroup.Controls.Add($numGopSeconds)
$null = Add-Label $settingsGroup 'NVENC preset' 15 168 90
$cmbPreset = New-Object System.Windows.Forms.ComboBox
$cmbPreset.Location = New-Object System.Drawing.Point(105, 168)
$cmbPreset.Size = New-Object System.Drawing.Size(80, 23)
$cmbPreset.DropDownStyle = 'DropDownList'
$null = $cmbPreset.Items.AddRange(@('p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7'))
$cmbPreset.SelectedItem = 'p1'
$settingsGroup.Controls.Add($cmbPreset)
$toolTip.SetToolTip($cmbPreset, 'p1 is fastest/lowest latency. Higher values trade speed for compression quality.')
$null = Add-Label $settingsGroup 'H.264 profile' 205 168 90
$cmbProfile = New-Object System.Windows.Forms.ComboBox
$cmbProfile.Location = New-Object System.Drawing.Point(295, 168)
$cmbProfile.Size = New-Object System.Drawing.Size(145, 23)
$cmbProfile.DropDownStyle = 'DropDownList'
$null = $cmbProfile.Items.AddRange(@('constrained-baseline', 'baseline', 'main', 'high'))
$cmbProfile.SelectedItem = 'constrained-baseline'
$settingsGroup.Controls.Add($cmbProfile)
$null = Add-Label $settingsGroup 'Rate control' 460 168 80
$txtRateControl = New-Object System.Windows.Forms.TextBox
$txtRateControl.Location = New-Object System.Drawing.Point(540, 168)
$txtRateControl.Size = New-Object System.Drawing.Size(75, 23)
$txtRateControl.Text = 'CBR'
$txtRateControl.ReadOnly = $true
$settingsGroup.Controls.Add($txtRateControl)
$null = Add-Label $settingsGroup 'Queue policy' 635 168 80
$txtQueuePolicy = New-Object System.Windows.Forms.TextBox
$txtQueuePolicy.Location = New-Object System.Drawing.Point(715, 168)
$txtQueuePolicy.Size = New-Object System.Drawing.Size(145, 23)
$txtQueuePolicy.Text = '2 frames, drop stale'
$txtQueuePolicy.ReadOnly = $true
$settingsGroup.Controls.Add($txtQueuePolicy)
$chkDesktopAudio = New-Object System.Windows.Forms.CheckBox
$chkDesktopAudio.Text = 'Desktop audio'
$chkDesktopAudio.Location = New-Object System.Drawing.Point(15, 208)
$chkDesktopAudio.Size = New-Object System.Drawing.Size(115, 23)
$chkDesktopAudio.Checked = $true
$settingsGroup.Controls.Add($chkDesktopAudio)
$null = Add-Label $settingsGroup 'Volume %' 135 208 65
$numDesktopVolume = New-Object System.Windows.Forms.NumericUpDown
$numDesktopVolume.Location = New-Object System.Drawing.Point(200, 208)
$numDesktopVolume.Size = New-Object System.Drawing.Size(65, 23)
$numDesktopVolume.Minimum = 0
$numDesktopVolume.Maximum = 200
$numDesktopVolume.Value = 100
$settingsGroup.Controls.Add($numDesktopVolume)
$chkMic = New-Object System.Windows.Forms.CheckBox
$chkMic.Text = 'Default microphone'
$chkMic.Location = New-Object System.Drawing.Point(295, 208)
$chkMic.Size = New-Object System.Drawing.Size(140, 23)
$chkMic.Checked = $false
$settingsGroup.Controls.Add($chkMic)
$null = Add-Label $settingsGroup 'Volume %' 440 208 65
$numMicVolume = New-Object System.Windows.Forms.NumericUpDown
$numMicVolume.Location = New-Object System.Drawing.Point(505, 208)
$numMicVolume.Size = New-Object System.Drawing.Size(65, 23)
$numMicVolume.Minimum = 0
$numMicVolume.Maximum = 200
$numMicVolume.Value = 100
$settingsGroup.Controls.Add($numMicVolume)
$null = Add-Label $settingsGroup 'Opus bitrate (kbps)' 600 208 125
$numAudioBitrate = New-Object System.Windows.Forms.NumericUpDown
$numAudioBitrate.Location = New-Object System.Drawing.Point(730, 208)
$numAudioBitrate.Size = New-Object System.Drawing.Size(80, 23)
$numAudioBitrate.Minimum = 32
$numAudioBitrate.Maximum = 512
$numAudioBitrate.Increment = 16
$numAudioBitrate.Value = 128
$settingsGroup.Controls.Add($numAudioBitrate)
$audioNote = New-Object System.Windows.Forms.Label
$audioNote.Text = 'Desktop audio uses WASAPI loopback. Microphone uses the Windows/GStreamer default capture device.'
$audioNote.Location = New-Object System.Drawing.Point(15, 242)
$audioNote.Size = New-Object System.Drawing.Size(845, 22)
$audioNote.ForeColor = [System.Drawing.Color]::DimGray
$settingsGroup.Controls.Add($audioNote)
$latencyNote = New-Object System.Windows.Forms.Label
$latencyNote.Text = 'Low-latency settings are enforced: D3D11 GPU path, NVENC ultra-low-latency, B-frame-free baseline caps, 10 ms Opus frames, and leaky queues.'
$latencyNote.Location = New-Object System.Drawing.Point(15, 270)
$latencyNote.Size = New-Object System.Drawing.Size(850, 30)
$latencyNote.ForeColor = [System.Drawing.Color]::DimGray
$settingsGroup.Controls.Add($latencyNote)
$commandGroup = New-Object System.Windows.Forms.GroupBox
$commandGroup.Text = 'Generated Command'
$commandGroup.Location = New-Object System.Drawing.Point(10, 335)
$commandGroup.Size = New-Object System.Drawing.Size(895, 118)
$commandGroup.Anchor = 'Top,Left,Right'
$form.Controls.Add($commandGroup)
$txtCommand = New-Object System.Windows.Forms.TextBox
$txtCommand.Location = New-Object System.Drawing.Point(12, 22)
$txtCommand.Size = New-Object System.Drawing.Size(870, 82)
$txtCommand.Multiline = $true
$txtCommand.ScrollBars = 'Both'
$txtCommand.WordWrap = $false
$txtCommand.ReadOnly = $true
$txtCommand.Font = New-Object System.Drawing.Font('Consolas', 8.5)
$txtCommand.Anchor = 'Top,Bottom,Left,Right'
$commandGroup.Controls.Add($txtCommand)
$btnStart = New-Object System.Windows.Forms.Button
$btnStart.Text = 'Start Stream'
$btnStart.Location = New-Object System.Drawing.Point(10, 465)
$btnStart.Size = New-Object System.Drawing.Size(120, 34)
$btnStart.Font = New-Object System.Drawing.Font('Segoe UI', 9, [System.Drawing.FontStyle]::Bold)
$form.Controls.Add($btnStart)
$btnStop = New-Object System.Windows.Forms.Button
$btnStop.Text = 'Stop'
$btnStop.Location = New-Object System.Drawing.Point(140, 465)
$btnStop.Size = New-Object System.Drawing.Size(90, 34)
$btnStop.Enabled = $false
$form.Controls.Add($btnStop)
$btnRestart = New-Object System.Windows.Forms.Button
$btnRestart.Text = 'Restart Pipeline'
$btnRestart.Location = New-Object System.Drawing.Point(240, 465)
$btnRestart.Size = New-Object System.Drawing.Size(125, 34)
$btnRestart.Enabled = $false
$form.Controls.Add($btnRestart)
$btnCopyCommand = New-Object System.Windows.Forms.Button
$btnCopyCommand.Text = 'Copy Command'
$btnCopyCommand.Location = New-Object System.Drawing.Point(375, 465)
$btnCopyCommand.Size = New-Object System.Drawing.Size(115, 34)
$form.Controls.Add($btnCopyCommand)
$btnClearLog = New-Object System.Windows.Forms.Button
$btnClearLog.Text = 'Clear Log'
$btnClearLog.Location = New-Object System.Drawing.Point(500, 465)
$btnClearLog.Size = New-Object System.Drawing.Size(90, 34)
$form.Controls.Add($btnClearLog)
$statusLabel = New-Object System.Windows.Forms.Label
$statusLabel.Text = 'Stopped'
$statusLabel.Location = New-Object System.Drawing.Point(610, 470)
$statusLabel.Size = New-Object System.Drawing.Size(295, 25)
$statusLabel.TextAlign = 'MiddleRight'
$statusLabel.Anchor = 'Top,Right'
$statusLabel.Font = New-Object System.Drawing.Font('Segoe UI', 9, [System.Drawing.FontStyle]::Bold)
$form.Controls.Add($statusLabel)
$logGroup = New-Object System.Windows.Forms.GroupBox
$logGroup.Text = 'GStreamer Output'
$logGroup.Location = New-Object System.Drawing.Point(10, 510)
$logGroup.Size = New-Object System.Drawing.Size(895, 230)
$logGroup.Anchor = 'Top,Bottom,Left,Right'
$form.Controls.Add($logGroup)
$txtLog = New-Object System.Windows.Forms.TextBox
$txtLog.Location = New-Object System.Drawing.Point(12, 22)
$txtLog.Size = New-Object System.Drawing.Size(870, 195)
$txtLog.Multiline = $true
$txtLog.ScrollBars = 'Both'
$txtLog.WordWrap = $false
$txtLog.ReadOnly = $true
$txtLog.Font = New-Object System.Drawing.Font('Consolas', 8.5)
$txtLog.Anchor = 'Top,Bottom,Left,Right'
$logGroup.Controls.Add($txtLog)
function Append-Log {
param([string]$Text)
if ([string]::IsNullOrEmpty($Text)) {
return
}
$txtLog.AppendText($Text)
if (-not $Text.EndsWith([Environment]::NewLine)) {
$txtLog.AppendText([Environment]::NewLine)
}
if ($txtLog.TextLength -gt 250000) {
$txtLog.Text = $txtLog.Text.Substring($txtLog.TextLength - 180000)
}
$txtLog.SelectionStart = $txtLog.TextLength
$txtLog.ScrollToCaret()
}
function Set-RunState {
param([bool]$Running)
$btnStart.Enabled = -not $Running
$btnStop.Enabled = $Running
$btnRestart.Enabled = $Running
}
function Build-GstArguments {
$url = $txtWhipUrl.Text.Trim()
$quotedUrl = Quote-GstValue $url
$monitor = [int]$numMonitor.Value
$cursor = if ($chkCursor.Checked) { 'true' } else { 'false' }
$width = [int]$numWidth.Value
$height = [int]$numHeight.Value
$fps = [int]$numFps.Value
$videoBitrate = [int]$numVideoBitrate.Value
$gopSize = [Math]::Max(1, $fps * [int]$numGopSeconds.Value)
$preset = [string]$cmbPreset.SelectedItem
$profile = [string]$cmbProfile.SelectedItem
$audioBitrate = [int]$numAudioBitrate.Value * 1000
$desktopVolume = Format-InvariantNumber ([double]$numDesktopVolume.Value / 100.0)
$micVolume = Format-InvariantNumber ([double]$numMicVolume.Value / 100.0)
$videoBranch = @(
'd3d11screencapturesrc'
"monitor-index=$monitor"
"show-cursor=$cursor"
'!'
"`"video/x-raw(memory:D3D11Memory),framerate=$fps/1`""
'!'
'd3d11convert'
'!'
"`"video/x-raw(memory:D3D11Memory),format=NV12,width=$width,height=$height,framerate=$fps/1`""
'!'
'queue'
'max-size-buffers=2'
'max-size-bytes=0'
'max-size-time=0'
'leaky=downstream'
'!'
'nvd3d11h264enc'
"bitrate=$videoBitrate"
'rc-mode=cbr'
"preset=$preset"
'tune=ultra-low-latency'
"gop-size=$gopSize"
'rc-lookahead=0'
'repeat-sequence-header=true'
'!'
'h264parse'
'config-interval=-1'
'!'
"`"video/x-h264,profile=$profile,stream-format=byte-stream,alignment=au`""
) -join ' '
$desktopEnabled = $chkDesktopAudio.Checked
$micEnabled = $chkMic.Checked
if (-not $desktopEnabled -and -not $micEnabled) {
$pipeline = "$videoBranch ! whipclientsink video-caps=`"video/x-h264`" signaller::whip-endpoint=$quotedUrl"
}
elseif ($desktopEnabled -and -not $micEnabled) {
$desktopBranch = @(
'wasapi2src'
'loopback=true'
'low-latency=true'
'!'
'queue'
'max-size-buffers=4'
'max-size-bytes=0'
'max-size-time=0'
'leaky=downstream'
'!'
'audioconvert'
'!'
'audioresample'
'!'
'"audio/x-raw,format=S16LE,rate=48000,channels=2"'
'!'
'volume'
"volume=$desktopVolume"
'!'
'opusenc'
"bitrate=$audioBitrate"
'bitrate-type=cbr'
'frame-size=10'
'audio-type=restricted-lowdelay'
'!'
'"audio/x-opus"'
'!'
'whip.audio_0'
) -join ' '
$pipeline = "whipclientsink name=whip video-caps=`"video/x-h264`" audio-caps=`"audio/x-opus`" signaller::whip-endpoint=$quotedUrl $videoBranch ! whip.video_0 $desktopBranch"
}
elseif (-not $desktopEnabled -and $micEnabled) {
$micBranch = @(
'wasapi2src'
'low-latency=true'
'!'
'queue'
'max-size-buffers=4'
'max-size-bytes=0'
'max-size-time=0'
'leaky=downstream'
'!'
'audioconvert'
'!'
'audioresample'
'!'
'"audio/x-raw,format=S16LE,rate=48000,channels=2"'
'!'
'volume'
"volume=$micVolume"
'!'
'opusenc'
"bitrate=$audioBitrate"
'bitrate-type=cbr'
'frame-size=10'
'audio-type=restricted-lowdelay'
'!'
'"audio/x-opus"'
'!'
'whip.audio_0'
) -join ' '
$pipeline = "whipclientsink name=whip video-caps=`"video/x-h264`" audio-caps=`"audio/x-opus`" signaller::whip-endpoint=$quotedUrl $videoBranch ! whip.video_0 $micBranch"
}
else {
$desktopMixBranch = @(
'wasapi2src'
'loopback=true'
'low-latency=true'
'!'
'queue'
'max-size-buffers=8'
'max-size-bytes=0'
'max-size-time=0'
'leaky=downstream'
'!'
'audioconvert'
'!'
'audioresample'
'!'
'"audio/x-raw,format=F32LE,rate=48000,channels=2"'
'!'
'volume'
"volume=$desktopVolume"
'!'
'mix.'
) -join ' '
$micMixBranch = @(
'wasapi2src'
'low-latency=true'
'!'
'queue'
'max-size-buffers=8'
'max-size-bytes=0'
'max-size-time=0'
'leaky=downstream'
'!'
'audioconvert'
'!'
'audioresample'
'!'
'"audio/x-raw,format=F32LE,rate=48000,channels=2"'
'!'
'volume'
"volume=$micVolume"
'!'
'mix.'
) -join ' '
$mixedOutput = @(
'mix.'
'!'
'queue'
'max-size-buffers=8'
'max-size-bytes=0'
'max-size-time=0'
'leaky=downstream'
'!'
'audioconvert'
'!'
'"audio/x-raw,format=S16LE,rate=48000,channels=2"'
'!'
'opusenc'
"bitrate=$audioBitrate"
'bitrate-type=cbr'
'frame-size=10'
'audio-type=restricted-lowdelay'
'!'
'"audio/x-opus"'
'!'
'whip.audio_0'
) -join ' '
$pipeline = "whipclientsink name=whip video-caps=`"video/x-h264`" audio-caps=`"audio/x-opus`" signaller::whip-endpoint=$quotedUrl audiomixer name=mix $videoBranch ! whip.video_0 $desktopMixBranch $micMixBranch $mixedOutput"
}
$flags = '-e'
if ($chkVerbose.Checked) {
$flags += ' -v'
}
return "$flags $pipeline"
}
function Update-CommandPreview {
try {
$gstPath = $txtGstPath.Text.Trim()
if ([string]::IsNullOrWhiteSpace($gstPath)) {
$gstPath = 'gst-launch-1.0.exe'
}
$txtCommand.Text = '& ' + (Quote-GstValue $gstPath) + ' ' + (Build-GstArguments)
}
catch {
$txtCommand.Text = "Unable to build command: $($_.Exception.Message)"
}
}
function Save-Settings {
try {
if (-not (Test-Path -LiteralPath $script:ConfigDirectory)) {
$null = New-Item -ItemType Directory -Path $script:ConfigDirectory -Force
}
$settings = [ordered]@{
GstPath = $txtGstPath.Text
WhipUrl = $txtWhipUrl.Text
MonitorIndex = [int]$numMonitor.Value
ShowCursor = $chkCursor.Checked
AutoRestart = $chkAutoRestart.Checked
Verbose = $chkVerbose.Checked
Width = [int]$numWidth.Value
Height = [int]$numHeight.Value
Fps = [int]$numFps.Value
VideoBitrateKbps = [int]$numVideoBitrate.Value
GopSeconds = [int]$numGopSeconds.Value
Preset = [string]$cmbPreset.SelectedItem
Profile = [string]$cmbProfile.SelectedItem
DesktopAudio = $chkDesktopAudio.Checked
DesktopVolume = [int]$numDesktopVolume.Value
Microphone = $chkMic.Checked
MicrophoneVolume = [int]$numMicVolume.Value
AudioBitrateKbps = [int]$numAudioBitrate.Value
}
$settings | ConvertTo-Json | Set-Content -LiteralPath $script:ConfigPath -Encoding UTF8
}
catch {
Append-Log "Could not save settings: $($_.Exception.Message)"
}
}
function Load-Settings {
if (-not (Test-Path -LiteralPath $script:ConfigPath)) {
return
}
try {
$settings = Get-Content -LiteralPath $script:ConfigPath -Raw | ConvertFrom-Json
if ($settings.GstPath) { $txtGstPath.Text = [string]$settings.GstPath }
if ($settings.WhipUrl) { $txtWhipUrl.Text = [string]$settings.WhipUrl }
if ($null -ne $settings.MonitorIndex) { $numMonitor.Value = [decimal]$settings.MonitorIndex }
if ($null -ne $settings.ShowCursor) { $chkCursor.Checked = [bool]$settings.ShowCursor }
if ($null -ne $settings.AutoRestart) { $chkAutoRestart.Checked = [bool]$settings.AutoRestart }
if ($null -ne $settings.Verbose) { $chkVerbose.Checked = [bool]$settings.Verbose }
if ($settings.Width) { $numWidth.Value = [decimal]$settings.Width }
if ($settings.Height) { $numHeight.Value = [decimal]$settings.Height }
if ($settings.Fps) { $numFps.Value = [decimal]$settings.Fps }
if ($settings.VideoBitrateKbps) { $numVideoBitrate.Value = [decimal]$settings.VideoBitrateKbps }
if ($settings.GopSeconds) { $numGopSeconds.Value = [decimal]$settings.GopSeconds }
if ($settings.Preset -and $cmbPreset.Items.Contains([string]$settings.Preset)) { $cmbPreset.SelectedItem = [string]$settings.Preset }
if ($settings.Profile -and $cmbProfile.Items.Contains([string]$settings.Profile)) { $cmbProfile.SelectedItem = [string]$settings.Profile }
if ($null -ne $settings.DesktopAudio) { $chkDesktopAudio.Checked = [bool]$settings.DesktopAudio }
if ($null -ne $settings.DesktopVolume) { $numDesktopVolume.Value = [decimal]$settings.DesktopVolume }
if ($null -ne $settings.Microphone) { $chkMic.Checked = [bool]$settings.Microphone }
if ($null -ne $settings.MicrophoneVolume) { $numMicVolume.Value = [decimal]$settings.MicrophoneVolume }
if ($settings.AudioBitrateKbps) { $numAudioBitrate.Value = [decimal]$settings.AudioBitrateKbps }
}
catch {
Append-Log "Could not load settings: $($_.Exception.Message)"
}
}
function Validate-Configuration {
$gstPath = $txtGstPath.Text.Trim()
if ([string]::IsNullOrWhiteSpace($gstPath) -or -not (Test-Path -LiteralPath $gstPath)) {
[System.Windows.Forms.MessageBox]::Show(
'Select a valid gst-launch-1.0.exe path.',
$script:AppName,
'OK',
'Warning'
) | Out-Null
return $false
}
$url = $txtWhipUrl.Text.Trim()
if ($url -notmatch '^https?://') {
[System.Windows.Forms.MessageBox]::Show(
'The WHIP endpoint must begin with http:// or https://.',
$script:AppName,
'OK',
'Warning'
) | Out-Null
return $false
}
return $true
}
function Start-GstStream {
if ($script:GstProcess -and -not $script:GstProcess.HasExited) {
return
}
if (-not (Validate-Configuration)) {
return
}
Save-Settings
if (-not (Test-Path -LiteralPath $script:LogDirectory)) {
$null = New-Item -ItemType Directory -Path $script:LogDirectory -Force
}
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss-fff'
$script:StdOutPath = Join-Path $script:LogDirectory "gst-$stamp-out.log"
$script:StdErrPath = Join-Path $script:LogDirectory "gst-$stamp-err.log"
$script:StdOutPosition = [int64]0
$script:StdErrPosition = [int64]0
$script:StopRequested = $false
$script:RestartAt = $null
$gstPath = $txtGstPath.Text.Trim()
$arguments = Build-GstArguments
Append-Log "[$(Get-Date -Format 'HH:mm:ss')] Starting full GStreamer pipeline..."
Append-Log "Executable: $gstPath"
Append-Log "Arguments: $arguments"
try {
$script:GstProcess = Start-Process -FilePath $gstPath -ArgumentList $arguments -RedirectStandardOutput $script:StdOutPath -RedirectStandardError $script:StdErrPath -WindowStyle Hidden -PassThru
$statusLabel.Text = "Streaming — PID $($script:GstProcess.Id)"
$statusLabel.ForeColor = [System.Drawing.Color]::DarkGreen
Set-RunState $true
}
catch {
$script:GstProcess = $null
$statusLabel.Text = 'Start failed'
$statusLabel.ForeColor = [System.Drawing.Color]::DarkRed
Set-RunState $false
Append-Log "START ERROR: $($_.Exception.Message)"
}
}
function Stop-GstStream {
param([switch]$Restart)
$script:StopRequested = $true
if ($Restart) {
$script:RestartAt = (Get-Date).AddMilliseconds(800)
}
else {
$script:RestartAt = $null
}
if ($script:GstProcess -and -not $script:GstProcess.HasExited) {
$statusLabel.Text = 'Stopping...'
$statusLabel.ForeColor = [System.Drawing.Color]::DarkOrange
Append-Log "[$(Get-Date -Format 'HH:mm:ss')] Stopping complete process tree..."
try {
$taskKillArguments = "/PID $($script:GstProcess.Id) /T /F"
$null = Start-Process -FilePath 'taskkill.exe' -ArgumentList $taskKillArguments -WindowStyle Hidden -Wait -PassThru
}
catch {
try {
$script:GstProcess.Kill()
}
catch {
Append-Log "STOP ERROR: $($_.Exception.Message)"
}
}
}
elseif ($Restart) {
$script:GstProcess = $null
}
else {
$script:GstProcess = $null
$statusLabel.Text = 'Stopped'
$statusLabel.ForeColor = [System.Drawing.Color]::Black
Set-RunState $false
}
}
function Test-GStreamerElements {
$gstPath = $txtGstPath.Text.Trim()
if ([string]::IsNullOrWhiteSpace($gstPath) -or -not (Test-Path -LiteralPath $gstPath)) {
[System.Windows.Forms.MessageBox]::Show('Select a valid gst-launch-1.0.exe first.', $script:AppName, 'OK', 'Warning') | Out-Null
return
}
$inspectPath = Join-Path (Split-Path -Parent $gstPath) 'gst-inspect-1.0.exe'
if (-not (Test-Path -LiteralPath $inspectPath)) {
[System.Windows.Forms.MessageBox]::Show('gst-inspect-1.0.exe was not found beside gst-launch-1.0.exe.', $script:AppName, 'OK', 'Warning') | Out-Null
return
}
$elements = @(
'd3d11screencapturesrc',
'd3d11convert',
'nvd3d11h264enc',
'h264parse',
'whipclientsink',
'wasapi2src',
'audioconvert',
'audioresample',
'opusenc',
'audiomixer',
'volume'
)
$missing = New-Object System.Collections.Generic.List[string]
$form.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
try {
foreach ($element in $elements) {
& $inspectPath $element *> $null
if ($LASTEXITCODE -ne 0) {
$missing.Add($element)
}
}
}
finally {
$form.Cursor = [System.Windows.Forms.Cursors]::Default
}
if ($missing.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show('All required GStreamer elements were found.', $script:AppName, 'OK', 'Information') | Out-Null
}
else {
[System.Windows.Forms.MessageBox]::Show(
"Missing GStreamer elements:`r`n`r`n$($missing -join "`r`n")",
$script:AppName,
'OK',
'Error'
) | Out-Null
}
}
$previewHandler = {
Update-CommandPreview
}
$txtGstPath.Add_TextChanged($previewHandler)
$txtWhipUrl.Add_TextChanged($previewHandler)
$numMonitor.Add_ValueChanged($previewHandler)
$chkCursor.Add_CheckedChanged($previewHandler)
$chkAutoRestart.Add_CheckedChanged($previewHandler)
$chkVerbose.Add_CheckedChanged($previewHandler)
$numWidth.Add_ValueChanged($previewHandler)
$numHeight.Add_ValueChanged($previewHandler)
$numFps.Add_ValueChanged($previewHandler)
$numVideoBitrate.Add_ValueChanged($previewHandler)
$numGopSeconds.Add_ValueChanged($previewHandler)
$cmbPreset.Add_SelectedIndexChanged($previewHandler)
$cmbProfile.Add_SelectedIndexChanged($previewHandler)
$chkDesktopAudio.Add_CheckedChanged($previewHandler)
$numDesktopVolume.Add_ValueChanged($previewHandler)
$chkMic.Add_CheckedChanged($previewHandler)
$numMicVolume.Add_ValueChanged($previewHandler)
$numAudioBitrate.Add_ValueChanged($previewHandler)
$btnBrowseGst.Add_Click({
$dialog = New-Object System.Windows.Forms.OpenFileDialog
$dialog.Title = 'Select gst-launch-1.0.exe'
$dialog.Filter = 'GStreamer launcher (gst-launch-1.0.exe)|gst-launch-1.0.exe|Executable files (*.exe)|*.exe|All files (*.*)|*.*'
if ($txtGstPath.Text -and (Test-Path -LiteralPath $txtGstPath.Text)) {
$dialog.InitialDirectory = Split-Path -Parent $txtGstPath.Text
$dialog.FileName = Split-Path -Leaf $txtGstPath.Text
}
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$txtGstPath.Text = $dialog.FileName
}
$dialog.Dispose()
})
$btnCheckGst.Add_Click({ Test-GStreamerElements })
$btnStart.Add_Click({ Start-GstStream })
$btnStop.Add_Click({ Stop-GstStream })
$btnRestart.Add_Click({ Stop-GstStream -Restart })
$btnCopyCommand.Add_Click({
try {
[System.Windows.Forms.Clipboard]::SetText($txtCommand.Text)
$statusLabel.Text = 'Command copied'
$statusLabel.ForeColor = [System.Drawing.Color]::DarkBlue
}
catch {
Append-Log "Clipboard error: $($_.Exception.Message)"
}
})
$btnClearLog.Add_Click({ $txtLog.Clear() })
$pollTimer = New-Object System.Windows.Forms.Timer
$pollTimer.Interval = 400
$pollTimer.Add_Tick({
$stdoutText = Read-NewLogText -Path $script:StdOutPath -Position ([ref]$script:StdOutPosition)
if ($stdoutText) { Append-Log $stdoutText }
$stderrText = Read-NewLogText -Path $script:StdErrPath -Position ([ref]$script:StdErrPosition)
if ($stderrText) { Append-Log $stderrText }
if ($script:GstProcess -and $script:GstProcess.HasExited) {
$exitCode = $script:GstProcess.ExitCode
$wasRequested = $script:StopRequested
try { $script:GstProcess.Dispose() } catch {}
$script:GstProcess = $null
Set-RunState $false
if ($wasRequested) {
$statusLabel.Text = 'Stopped'
$statusLabel.ForeColor = [System.Drawing.Color]::Black
Append-Log "[$(Get-Date -Format 'HH:mm:ss')] Pipeline stopped."
}
else {
$statusLabel.Text = "Pipeline exited — code $exitCode"
$statusLabel.ForeColor = [System.Drawing.Color]::DarkRed
Append-Log "[$(Get-Date -Format 'HH:mm:ss')] Pipeline exited unexpectedly with code $exitCode."
if ($chkAutoRestart.Checked) {
$script:RestartAt = (Get-Date).AddSeconds(2)
Append-Log 'Automatic full restart scheduled in 2 seconds.'
}
}
$script:StopRequested = $false
}
if (-not $script:GstProcess -and $script:RestartAt -and (Get-Date) -ge $script:RestartAt) {
$script:RestartAt = $null
Start-GstStream
}
})
$pollTimer.Start()
$form.Add_Shown({
Load-Settings
Update-CommandPreview
})
$form.Add_FormClosing({
Save-Settings
$chkAutoRestart.Checked = $false
$script:RestartAt = $null
if ($script:GstProcess -and -not $script:GstProcess.HasExited) {
Stop-GstStream
}
$pollTimer.Stop()
})
[void]$form.ShowDialog()
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -STA -File "%~dp0GStreamer-Basic-WHIP-GUI.ps1"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment