Skip to content

​fix(NowPlayingBridge): Fix COM marshalling failure when reading SMTC thumbnail stream #1

Description

@AritoUser

When running NowPlayingBridge.ps1 to extract track metadata and cover art from SMTC sessions (such as Spotify or Chromium browsers), metadata fields (title, artist, album) are retrieved properly, but cover extraction consistently fails.
Inspection via -IncludeDebugInfo reveals that the script enters the catch block during the read_thumbnail stage. As a result, any partially created cover file is purged via:
if (Test-Path -LiteralPath $coverPath) {
Remove-Item -LiteralPath $coverPath -Force
}

Root Cause
The issue stems from WinRT COM marshalling behavior in PowerShell:

  • COM Object Wrapping: WinRT stream instances obtained via reflection-based helpers resolve as generic System.__ComObject instances inside PowerShell runtime environments.

  • Buffer Invocation Failure: Calling $stream.ReadAsync($buffer, $size, ...) and passing it to [WindowsRuntimeBufferExtensions]::ToArray($buffer) expects strongly typed WinRT interfaces (IBuffer), causing type conversion and marshalling exceptions at runtime.

  • File Extension Usability: Saving the output file strictly as .img (nowplaying_cover.img) prevents downstream tools, default shell handlers, and some image decoding libraries from parsing standard JPEG/PNG MIME streams automatically.
    Steps to Reproduce

  • Start Spotify or a YouTube/SoundCloud playback session in a browser (Edge/Chrome/Firefox).

  • Execute the bridge script:
    powershell.exe -ExecutionPolicy Bypass -File .\src\NowPlayingBridge.ps1 -OutputDir "." -IncludeDebugInfo

  • Inspect nowplaying.json.

  • Observe that hasCover returns false and debug.cover.status reports an error during read_thumbnail.
    Proposed Solution / Implementation
    Instead of manually allocating a Windows.Storage.Streams.Buffer and calling WinRT ReadAsync through dynamic COM dispatch, convert the IRandomAccessStreamWithContentType directly to a managed .NET read stream via [System.IO.WindowsRuntimeStreamExtensions]::AsStreamForRead($stream).
    Refactored Thumbnail Read Block
    Replace lines 212–276 in src/NowPlayingBridge.ps1 with the following clean stream-copy routine:
    if ($null -ne $thumb -and $null -eq $coverDebug) {
    $stream = $null
    $netStream = $null
    $memoryStream = $null

    try {
    $streamTask = $thumb.OpenReadAsync()
    $stream = Await-WinRtTask $streamTask ([Windows.Storage.Streams.IRandomAccessStreamWithContentType])

     if ($null -eq $stream) {
         throw "OpenReadAsync returned no stream."
     }
    
     if ($stream.Size -eq 0) {
         throw "Thumbnail stream is empty."
     }
    
     # Convert the WinRT stream to a managed .NET Stream
     $netStream = [System.IO.WindowsRuntimeStreamExtensions]::AsStreamForRead($stream)
     $memoryStream = [System.IO.MemoryStream]::new()
     $netStream.CopyTo($memoryStream)
    
     $bytes = $memoryStream.ToArray()
     if ($null -eq $bytes -or $bytes.Length -eq 0) {
         throw "Converted byte stream produced an empty array."
     }
    
     # Write output file
     [System.IO.File]::WriteAllBytes($coverPath, $bytes)
    
     $result.hasCover = $true
     $coverDebug = @{
         status  = "ok"
         stage   = "read_thumbnail"
         message = "Thumbnail successfully read and written."
         bytes   = $bytes.Length
     }
    

    }
    catch {
    if (Test-Path -LiteralPath $coverPath) {
    Remove-Item -LiteralPath $coverPath -Force -ErrorAction SilentlyContinue
    }

     $coverDebug = @{
         status    = "error"
         stage     = "read_thumbnail"
         message   = $_.Exception.Message
         exception = $_.Exception.ToString()
     }
    

    }
    finally {
    if ($null -ne $memoryStream) { $memoryStream.Dispose() }
    if ($null -ne $netStream) { $netStream.Dispose() }
    if ($null -ne $stream) { $stream.Dispose() }
    }
    }

Benefits

  • Eliminates dynamic COM reflection on IBuffer.
  • Avoids marshalling mismatches between Windows Runtime and .NET assemblies.
  • Properly handles stream disposal via finally.
  • Works reliably across Windows PowerShell 5.1 and modern Windows builds.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions