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.
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])
}
catch {
if (Test-Path -LiteralPath $coverPath) {
Remove-Item -LiteralPath $coverPath -Force -ErrorAction SilentlyContinue
}
}
finally {
if ($null -ne $memoryStream) { $memoryStream.Dispose() }
if ($null -ne $netStream) { $netStream.Dispose() }
if ($null -ne $stream) { $stream.Dispose() }
}
}
Benefits