Commands/Connect-OBS.ps1

function Connect-OBS
{
    <#
    .SYNOPSIS
        Connects to Open Broadcast Studio
    .DESCRIPTION
        Connects to the obs-websocket.

        This must occur at least once to use obs-powershell.
    .EXAMPLE
        Connect-OBS
    .LINK
        Disconnect-OBS
    #>

    param(
    # A credential describing the connection.
    # The username should be the IPAddress, and the password should be the obs-websocket password.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Management.Automation.PSCredential]
    $Credential,

    # The websocket password.
    # You can see the websocket password in Tools -> obs-websocket settings -> show connect info
    [Parameter(ValueFromPipelineByPropertyName)]
    [securestring]
    $WebSocketPassword,

    # The websocket URL. If not provided, this will default to loopback on port 4455.
    [Parameter(ValueFromPipelineByPropertyName)]
    [ValidateScript({
        if ($_.Scheme -ne 'ws') {
            throw "Not a websocket uri"
        }
        return $true
    })]
    [uri]
    $WebSocketUri = "ws://$([ipaddress]::Loopback):4455"
    )

    begin {
        $obsWatcherJobDefinition = {
            param([Management.Automation.PSCredential]$Credential)

            function OBSIdentify {
                $secret = "$obsPwd$($messageData.d.authentication.salt)"
                $enc = [Security.Cryptography.SHA256Managed]::new()
                $secretSalted64 = [Convert]::ToBase64String(
                    $enc.ComputeHash([Text.Encoding]::ascii.GetBytes($secret)
                ))

                $saltedSecretAndChallenge = "$secretSalted64$(
                    $messageData.d.authentication.challenge
                )"


                $enc = [Security.Cryptography.SHA256Managed]::new()
                $challenge64 = [Convert]::ToBase64String(
                    $enc.ComputeHash([Text.Encoding]::ascii.GetBytes(
                        $saltedSecretAndChallenge
                    ))
                )                

                $identifyMessage = [Ordered]@{
                    op = 1
                    d = [Ordered]@{ 
                        rpcVersion = 1
                        authentication = $challenge64                        
                    }
                }
                $PayloadJson = $identifyMessage | ConvertTo-Json -Compress
                $SendSegment  = [ArraySegment[Byte]]::new([Text.Encoding]::UTF8.GetBytes($PayloadJson))
                $null = $Websocket.SendAsync($SendSegment,'Text', $true, [Threading.CancellationToken]::new($false))
            }

            $webSocketUri = $Credential.UserName
            $WebSocketPassword = $Credential.GetNetworkCredential().Password
            $Websocket   = [Net.WebSockets.ClientWebSocket]::new() # [Net.WebSockets.ClientWebSocket]::new()
                        $waitFor = [Timespan]'00:00:05'
            $ConnectTask = $Websocket.ConnectAsync($webSocketUri, [Threading.CancellationToken]::new($false))
            $null = $ConnectTask.ConfigureAwait($false)
    
    
            $obsPwd = $WebSocketPassword
            $WaitInterval = [Timespan]::FromMilliseconds(7)
    
            $BufferSize = 16kb
    
            $maxWaitTime = [DateTime]::Now + $WaitFor
            while (!$ConnectTask.IsCompleted -and [DateTime]::Now -lt $maxWaitTime) {

            }
    
            $Websocket
   
    try {
     
        while ($true) {
            # Create a buffer for the response
            $buffer = [byte[]]::new($BufferSize)
            $receiveSegment = [ArraySegment[byte]]::new($buffer)
            if (!($Websocket.State -eq 'Open')) {
                throw 'Websocket is not open anymore. {0}' -f $Websocket.State
            }
            # Receive the next response from the WebSocket,
            $receiveTask = $Websocket.ReceiveAsync($receiveSegment, [Threading.CancellationToken]::new($false))
            # then wait for it to complete.
            while (-not $receiveTask.IsCompleted) { Start-Sleep -Milliseconds $WaitInterval.TotalMilliseconds }
            # "Receiving $($receiveTask.Result.Count)" | Out-Host
            $msg    = # Get the response and trim with extreme prejudice.
                [Text.Encoding]::UTF8.GetString($buffer, 0, $receiveTask.Result.Count).Trim() -replace '\s+$'
            
            if ($msg) {
                $messageData = ConvertFrom-Json $msg
                $MessageData.pstypenames.insert(0,'OBS.WebSocket.Message')                
                $newEventSplat = @{}
                
                if ($messageData.op -eq 0 -and $messageData.d.authentication) {
                    . OBSIdentify
                }
                                
                $newEventSplat.SourceIdentifier = 'OBS.WebSocket.Message'
                $newEventSplat.MessageData = $MessageData
    
                New-Event @newEventSplat
                
                if ($messageData.op -eq 5) {
                    $newEventSplat = @{}
                    $newEventSplat.SourceIdentifier = "OBS.Event.$($messageData.d.eventType)"
                    if ($messageData.d.eventData) {
                        $newEventSplat.MessageData = [PSObject]::new($messageData.d.eventData)
                        $newEventSplat.MessageData.pstypenames.insert(0,"OBS.$($MessageData.d.eventType).response")
                        $newEventSplat.MessageData.pstypenames.insert(0,"$($newEventSplat.SourceIdentifier)")
                    }
                    New-Event @newEventSplat
                }
                
                if ($messageData.op -eq 7) {
                    $newEventSplat = @{}
                    $newEventSplat.SourceIdentifier = $MessageData.d.requestId
                    if ($messageData.d.responseData) {
                        $newEventSplat.MessageData = [PSObject]::new($MessageData.d.responseData)
                        $newEventSplat.MessageData.pstypenames.insert(0,"OBS.$($MessageData.d.requestType).response")
                        $newEventSplat.MessageData.pstypenames.insert(0,"$($newEventSplat.SourceIdentifier)")
                    }
                    New-Event @newEventSplat
                }
                
            }
    
            $buffer.Clear()
            
        }
    
    } catch {
        Write-Error -Exception $_.Exception -Message "StreamDeck Exception: $($_ | Out-String)" -ErrorId "WebSocket.State.$($Websocket.State)"
    }
        }
        

        if (-not $script:ObsConnections) {
            $script:ObsConnections = [Ordered]@{}
        }
    }

    process {
        if (-not $Credential) {
            if ($WebSocketPassword) {
                $Credential = [Management.Automation.PSCredential]::new($WebSocketUri, $WebSocketPassword)
            }
        }

        if (-not $Credential) {
            Write-Error "Must provide -Credential or -WebSocketPassword"
            return
        }

        $connectionExists = $script:ObsConnections[$Credential.UserName]
        if ($connectionExists -and 
            $connectionExists.State -eq 'Running' -and
            $connectionExists.WebSocket.State -eq 'Open'        
        ) {
            $connectionExists
            Write-Verbose "Already connected"
            return
        }

        $obsWatcher      =
            Start-ThreadJob -ScriptBlock $obsWatcherJobDefinition -Name "OBS.Connection.$($Credential.UserName)" -ArgumentList $Credential
        
        $whenOutputAddedHandler =
            Register-ObjectEvent -InputObject $obsWatcher.Output -EventName DataAdded -Action {
            
                $dataAdded = $sender[$event.SourceArgs[1].Index]
                if ($dataAdded -is [Management.Automation.PSEventArgs]) {
                    $newEventSplat = [Ordered]@{
                        SourceIdentifier = $dataAdded.SourceIdentifier
                    }
                    $newEventSplat.Sender = $eventSubscriber
                    if ($dataAdded.MessageData) {
                        $newEventSplat.MessageData = $dataAdded.MessageData
                    }
                    if ($dataAdded.SourceEventArgs) {
                        $newEventSplat.EventArguments = $dataAdded.SourceEventArgs
                    }
                    $messageData = $dataAdded.MessageData
                    New-Event @newEventSplat
                }
                elseif ($dataAdded -is [Net.WebSockets.ClientWebSocket]) {
                    $eventSubscriber | Add-Member NoteProperty WebSocket $dataAdded -Force -PassThru
                }
                else {

                }
            }

        $whenOutputAddedHandler | Add-Member NoteProperty Watcher $obsWatcher -Force

        $whenOutputAdded = @(
            foreach ($subscriber in Get-EventSubscriber) {
                if ($subscriber.Action -eq $whenOutputAddedHandler) {
                    $subscriber;break
                }
            }
        )

        $obsWatcher | Add-Member NoteProperty WhenOutputAddedHandler $whenOutputAddedHandler -Force
        $obsWatcher | Add-Member NoteProperty WhenOutputAdded $whenOutputAdded -Force
        $obsWatcher | Add-Member NoteProperty StartTime ([datetime]::Now) -Force
        $obsWatcher | Add-Member ScriptProperty WebSocket {
            $this.WhenOutputAdded.WebSocket
        } -Force

        $obsWatcher.pstypenames.insert(0, 'OBS.Connection')
        $script:ObsConnections[$Credential.UserName] = $obsWatcher
        $obsWatcher
    }
}