DubyaDude / WindowsMediaController

Allows developers to more easily get information from and interact with the Windows 10/11 OS media interface. (Also referred to Windows System Media Transport Controls (SMTC))
https://nuget.org/packages/Dubya.WindowsMediaController
MIT License
129 stars 9 forks source link

How to get output on Powershell or Cmd #8

Closed yw4z closed 1 year ago

yw4z commented 1 year ago

Thanks for sharing this one im trying to get playback status, other infos would be nice too is that possible to get data on powershell

DubyaDude commented 1 year ago

Yes, it is possible to get it directly via Powershell. I created some sample code (My PowerShell is a bit rusty):

# Importing GlobalSystemMediaTransportControlsSessionManager ( Source: https://superuser.com/a/1342416 )
[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control,ContentType=WindowsRuntime] | Out-Null
Add-Type -AssemblyName System.Runtime.WindowsRuntime

# Adding Awaitable ( Source: https://superuser.com/a/1342416 )
$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | ? { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]
Function Await($WinRtTask, $ResultType) {
    $asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
    $netTask = $asTask.Invoke($null, @($WinRtTask))
    $netTask.Wait(-1) | Out-Null
    $netTask.Result
}

# Getting the session manager ( Matches code: https://github.com/DubyaDude/WindowsMediaController/blob/61ba73aad7632bdda4ba415d9391882287de656e/WindowsMediaController/Main.cs#L63 )
$WindowsSessionManager = Await ([Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager]::RequestAsync()) ([Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager])

# Getting current sessions ( Matches code: https://github.com/DubyaDude/WindowsMediaController/blob/61ba73aad7632bdda4ba415d9391882287de656e/WindowsMediaController/Main.cs#L76 )
$controlSessionList = $WindowsSessionManager.GetSessions()

foreach ($controlSession in $controlSessionList) {
    # Getting the session source app ( Matches code: https://github.com/DubyaDude/WindowsMediaController/blob/61ba73aad7632bdda4ba415d9391882287de656e/WindowsMediaController/Main.cs#L172 )
    Write-Output $controlSession.SourceAppUserModelId

    # Getting the session's playback info ( Matches code: https://github.com/DubyaDude/WindowsMediaController/blob/61ba73aad7632bdda4ba415d9391882287de656e/WindowsMediaController/Main.cs#L180 )
    $playbackInfo = $controlSession.GetPlaybackInfo()
    Write-Output $playbackInfo.PlaybackStatus

    # Getting the session's media properties ( Matches code: https://github.com/DubyaDude/WindowsMediaController/blob/61ba73aad7632bdda4ba415d9391882287de656e/WindowsMediaController/Main.cs#L195 )
    $mediaProperties = Await ($controlSession.TryGetMediaPropertiesAsync()) ([Windows.Media.Control.GlobalSystemMediaTransportControlsSessionMediaProperties])
    Write-Output $mediaProperties.Title
    Write-Output $mediaProperties.Artist
}

With the above code, I got the following output while listening to Spotify:

> ".\MTC_Sample.ps1"
Spotify.exe
Playing
Tokyo Drift (Fast & Furious) - From "The Fast And The Furious: Tokyo Drift" Soundtrack
Teriyaki Boyz

For exacts of what you can get from this, I'd recommend looking at this CS file in this repo: https://github.com/DubyaDude/WindowsMediaController/blob/master/WindowsMediaController/Main.cs And Microsofts documentation that I have linked in the README: https://github.com/DubyaDude/WindowsMediaController/blob/master/README.md#useful-microsoft-documentations

Let me know if you have any other questions :)

yw4z commented 1 year ago

Hi, thanks for guide, you helped much i'm not sure but is there a way to implement OnAnyPlaybackStateChanged as background job i created a WPF app in powershell. backround jobs mostly freezing UI as far i see here is preview of my app, still wip tray icons included app

Screenshot (15)

i worked on SystemMediaTransportControls a bit, and dropping result here for future readers

[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control,ContentType=WindowsRuntime]|Out-Null
Add-Type -AssemblyName System.Runtime.WindowsRuntime

$asTaskGeneric=([System.WindowsRuntimeSystemExtensions].GetMethods()|?{$_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1'})[0]
Function Await($WinRtTask,$ResultType){
    $netTask=($asTaskGeneric.MakeGenericMethod($ResultType)).Invoke($null,@($WinRtTask))
    $netTask.Wait(-1)|Out-Null
    $netTask.Result
}

$playingstatus=$false
$playinginfo="Play"
$playingArr=$false

$smtc=[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager]
$smtcArr=@()
$i=-1
foreach($Session in (Await($smtc::RequestAsync())($smtc)).GetSessions()){
    $a=@()
    $i++
    $a+="----------"
    $a+=$Session.SourceAppUserModelId
    $a+=$Session.GetPlaybackInfo().PlaybackStatus
    $prop=Await($Session.TryGetMediaPropertiesAsync())([Windows.Media.Control.GlobalSystemMediaTransportControlsSessionMediaProperties])
    $a+=$prop.Artist + " - " + $prop.Title
    if($a -contains "Playing"){$playingArr=$i}
    $a+="----------"
    $smtcArr+=,$a
}

$smtcCount=$smtcArr.Count
write-host "********** $smtcCount Apps in Playback Session"
$smtcArr
if($playingArr -ne $false){
    write-host "********** Playing App"
    $smtcArr[$playingArr]
}else{
    write-host "********** No Playback"
}

it prints apps in session also prints playing app in array for easier selection

********** 2 Apps in Playback Session
----------
MusicBee.exe
Paused
2X2A - Skin
----------
----------
vivaldi.exe
Playing
exocollective - pexØt - Closer
----------
********** Playing App
----------
vivaldi.exe
Playing
exocollective - pexØt - Closer
----------

If there is no playback prints

********** 0 Apps in Playback Session
********** No Playback
DubyaDude commented 1 year ago

Unsure what you mean by background task, but maybe making it more 'event-based' rather then doing a consistent query of session, media, etc.

Using this: https://stackoverflow.com/a/64232782 I was able to get the methods for adding/removing to the events that are used in this project.

Using command:

[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager].GetEvents() | select Name, *Method, EventHandlerType

I got the output:

Name             : CurrentSessionChanged
AddMethod        : System.Runtime.InteropServices.WindowsRuntime.EventRegistrationToken add_CurrentSessionChanged(Windows.Foundation.TypedEventHandler`2[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control.CurrentSessionChangedEventArgs])
RemoveMethod     : Void remove_CurrentSessionChanged(System.Runtime.InteropServices.WindowsRuntime.EventRegistrationToken)
RaiseMethod      :
EventHandlerType : Windows.Foundation.TypedEventHandler`2[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control.CurrentSessionChangedEventArgs]

Name             : SessionsChanged
AddMethod        : System.Runtime.InteropServices.WindowsRuntime.EventRegistrationToken add_SessionsChanged(Windows.Foundation.TypedEventHandler`2[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control.SessionsChangedEventArgs])
RemoveMethod     : Void remove_SessionsChanged(System.Runtime.InteropServices.WindowsRuntime.EventRegistrationToken)
RaiseMethod      :
EventHandlerType : Windows.Foundation.TypedEventHandler`2[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control.SessionsChangedEventArgs]

And running

[Windows.Media.Control.GlobalSystemMediaTransportControlsSession].GetEvents() | select Name, *Method, EventHandlerType

Results in:

Name             : MediaPropertiesChanged
AddMethod        : System.Runtime.InteropServices.WindowsRuntime.EventRegistrationToken add_MediaPropertiesChanged(Windows.Foundation.TypedEventHandler`2[Windows.Media.Control.GlobalSystemMediaTransportControlsSession,Windows.Media.Control.MediaPropertiesChangedEventArgs])
RemoveMethod     : Void remove_MediaPropertiesChanged(System.Runtime.InteropServices.WindowsRuntime.EventRegistrationToken)
RaiseMethod      :
EventHandlerType : Windows.Foundation.TypedEventHandler`2[Windows.Media.Control.GlobalSystemMediaTransportControlsSession,Windows.Media.Control.MediaPropertiesChangedEventArgs]

Name             : PlaybackInfoChanged
AddMethod        : System.Runtime.InteropServices.WindowsRuntime.EventRegistrationToken add_PlaybackInfoChanged(Windows.Foundation.TypedEventHandler`2[Windows.Media.Control.GlobalSystemMediaTransportControlsSession,Windows.Media.Control.PlaybackInfoChangedEventArgs])
RemoveMethod     : Void remove_PlaybackInfoChanged(System.Runtime.InteropServices.WindowsRuntime.EventRegistrationToken)
RaiseMethod      :
EventHandlerType : Windows.Foundation.TypedEventHandler`2[Windows.Media.Control.GlobalSystemMediaTransportControlsSession,Windows.Media.Control.PlaybackInfoChangedEventArgs]

Name             : TimelinePropertiesChanged
AddMethod        : System.Runtime.InteropServices.WindowsRuntime.EventRegistrationToken add_TimelinePropertiesChanged(Windows.Foundation.TypedEventHandler`2[Windows.Media.Control.GlobalSystemMediaTransportControlsSession,Windows.Media.Control.TimelinePropertiesChangedEventArgs])
RemoveMethod     : Void remove_TimelinePropertiesChanged(System.Runtime.InteropServices.WindowsRuntime.EventRegistrationToken)
RaiseMethod      :
EventHandlerType : Windows.Foundation.TypedEventHandler`2[Windows.Media.Control.GlobalSystemMediaTransportControlsSession,Windows.Media.Control.TimelinePropertiesChangedEventArgs]

Which makes it seems it's possible to get it to be event-based like this project is. So instead of _WindowsSessionManager.SessionsChanged += ... it'll be $WindowsSessionManager.add_SessionsChanged(...) However, I couldn't get the event to work, let me know if you can.

DubyaDude commented 1 year ago

Also, please be aware of issue https://github.com/DubyaDude/WindowsMediaController/issues/6 which seems to affect a few events I'd recommend you have the Sample CMD running alongside your PS version so make sure you're not encountering this bug. If you are, sign out and sign in to windows.

yw4z commented 1 year ago

yep i have tried to add event with

Add-Type -AssemblyName System.Runtime.WindowsRuntime

[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control,ContentType=WindowsRuntime]|Out-Null
$t=([System.WindowsRuntimeSystemExtensions].GetMethods()|?{$_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1'})[0]
function aw($w,$r){$m=($t.MakeGenericMethod($r)).Invoke($null,@($w));$m.Wait(-1)|Out-Null;$m.Result}

$AppDomain=[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager];
$Action={Write-Host -ForegroundColor Green -Object 'Assembly was loaded!';};
Register-ObjectEvent -InputObject $AppDomain -EventName CurrentSessionChanged -Action $Action -OutVariable EventSubscription -SourceIdentifier CurrentSessionChanged;

returns

Register-ObjectEvent : Windows PowerShell cannot subscribe to Windows RT events.

thanks anyways

here is my taskbar code if you want to further experiment

(Get-Process -Id $pid).PriorityClass='idle'
Set-PSDebug -off

Add-Type -AssemblyName System.Windows.Forms,System.Runtime.WindowsRuntime
#thanks to [DubyaDude](https://github.com/DubyaDude) on imporing SMTC to powershell
[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control,ContentType=WindowsRuntime]|Out-Null
$t=([System.WindowsRuntimeSystemExtensions].GetMethods()|?{$_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1'})[0]
function aw($w,$r){$m=($t.MakeGenericMethod($r)).Invoke($null,@($w));$m.Wait(-1)|Out-Null;$m.Result}

# .\ parent folder selection not works with vbs, you should define complete path
$f="C:\ProgramData\Apps\System\Taskbar\TrayIcons"

function n{New-Object Windows.Forms.NotifyIcon}
function k($v){(New-object -com wscript.shell).SendKeys([char]$v)|Out-Null}
function i($v){New-Object System.Drawing.Icon("$f\ico\$v.ico")}

$p=n;$l=i("pl");$u=i("ps")
$p.add_click({k(179)})

function pf{
$x=@()
$ps=$false
$pt="Play"
$sm=[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager]
foreach($s in (aw($sm::RequestAsync())($sm)).GetSessions()){
    $a=@()
    $a+=$s.GetPlaybackInfo().PlaybackStatus
    $pr=aw($s.TryGetMediaPropertiesAsync())([Windows.Media.Control.GlobalSystemMediaTransportControlsSessionMediaProperties])
    $a+=$pr.Artist+" - "+$pr.Title
    if($a -contains "Playing"){$ps=$true;$pt=$a[1]}
    $x+=$a
    $s=$null
}
$p.icon=if($ps){$u}else{$l}
#sets tray icon tooltip to artist - track tile
$p.text=$pt
$x=$a=@()
}

$n=n;$n.icon=i("nx");$n.text="Next"
#Right click on next button sends previous shortcut
$n.add_MouseDown({if($_.Button -eq [Windows.Forms.MouseButtons]::left){k(176)}else{k(177)}})

$o=n;$o.icon=i("op");$o.text="Options"
$o.add_click({& "$f\Options.ps1"})

$o,$n,$p|%{$_.visible=$true}

#while keeps taskbar icons visible, and keeps script running
#reducing sleep time to increases CPU usage much
#clearing host keeps ram usage stable, otherwise smtc outputs increase ram usage over time
While(1){
pf|Out-Null
[System.GC]::Collect()
Clear-Host
[Microsoft.PowerShell.PSConsoleReadLine]::ClearHistory()
start-sleep -s 1
}

sorry for short variable names but it reduces RAM usage much on powershell scripts if you know any more method to reduce RAM usage im listening it takes around 30-40 mb atm, it goes to 70 mb after launching my popup WPF window

i prefer to not using sample cmd exe at this point smtc works nice, thanks to you

icon files icons.zip

im reducing priorty to low for easier process selection on windows startup

i launch with a vbs file that checks is there a powershell process with low priority. this one prevents multiple launches

set ws = CreateObject("WScript.Shell")

Set WMIs=GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
prV=0
'checks is there a powershell script running with low priority
For Each pr in WMIs.ExecQuery("Select * from Win32_Process Where Name = 'Powershell.exe'")
    If(pr.Priority=4)then
        prV=1
    end if
Next

If(prV=0)then
    ws.Run "powershell -NoProfile -NoLogo -WindowStyle hidden -NonInteractive -ExecutionPolicy Unrestricted -File C:\ProgramData\Apps\System\Taskbar\TrayIcons\media.ps1", 0, false
end if
DubyaDude commented 1 year ago

I see, doing a bit of research on Register-ObjectEvent : Windows PowerShell cannot subscribe to Windows RT events. I found a post where they use .NET event wrapper class to be able to use WinRT events: https://deletethis.net/dave/2016-06/WinRT+Toast+from+PowerShell I'll try to code something up with that if you don't get to it first, though might be a bit busy today.

Regarding the RAM, I've only used Powershell for some automation at my old job. So, I'm not too proficient in a lot of the details/complicated parts.

DubyaDude commented 1 year ago

Unfortunately, seems I couldn't get it working. The issue of not supporting WinRT events seems to be a very long-standing one (https://github.com/PowerShell/PowerShell/issues/2181).

I'll close this as it's the most I can do in terms of helping you. If you have any further questions don't hesitate to ask.

yw4z commented 1 year ago

thanks for all infos, you really helped much