.NET App 與Windows系統媒體控制(SMTC)互動

TwilightLemon發表於2024-07-02

當你使用Edge等瀏覽器或系統軟體播放媒體時,Windows控制中心就會出現相應的媒體資訊以及控制播放的功能,如圖。

SMTC (SystemMediaTransportControls) 是一個Windows App SDK (舊為UWP) 中提供的一個API,用於與系統媒體互動。接入SMTC的好處在於,將媒體控制和媒體資訊共享給系統,使用通用的特性(例如接受鍵盤快捷鍵的播放暫停、接受藍芽裝置的控制),便於與其它支援SMTC的應用互動等。

在UWP App中使用它很簡單,只需要呼叫SystemMediaTransportControls.GetForCurrentView()方法即可,但是該方法僅限在有效的UWP App中呼叫,否則將丟擲“Invalid window handle”異常。實際上,在官方文件中提到所有XXXForCurrentView方法均不適用於UWP App以外的程式呼叫。

這些 XxxForCurrentView 方法對 ApplicationView 型別具有隱式依賴關係,桌面應用不支援該型別。由於桌面應用不支援 ApplicationView,因此也不支援任何 XxxForCurrentView 方法。

此外官方文件還給出一個可替代的介面ISystemMediaTransportControlsInterop,然而這個介面在給的SDK中有保護性,無法訪問。

至此,直接建立SMTC的方法走不通。但是我發現一個奇怪的地方,UWP提供的在Windows.Media.Playback名稱空間下的MediaPlayer可以和SMTC自動整合,並且可以透過SystemMediaTransportControls屬性直接拿到SMTC物件。MediaPlayer內部透過某種COM元件直接建立了該NativeObject,而沒有走API提供的GetForCurrentView或FromAbi方法。也就是說,SMTC元件其實不需要使用合法的UWP Window控制代碼來建立,只不過可能為了某些特性而加上了該限制(後文將提到)。幸運的是,MediaPlayer幫我們繞過了這點。

下文講解手動與SMTC互動而不是直接使用MediaPlayer進行播放,你的專案可能已經有了其它的解碼器(如WPF版本的MediaPlayer、Bass.Net解碼器、NAudio等),則只需要將互動部分接入SMTC而不更換解碼器。

文末提供了我封裝好的SMTCCreator和SMTCListener,可以直接使用。

一、引用WinRT API到專案

最便捷的方法是直接修改目標框架到win10,這樣就能自動引入WinRT API:

<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>

或者一些其他的方法,可以參考這篇部落格:如何在WPF中呼叫Windows 10/11 API(UWP/WinRT) - zhaotianff - 部落格園 (cnblogs.com)

二、透過MediaPlayer獲取SMTC物件

using Windows.Media;
using Windows.Storage.Streams;
...
private readonly Windows.Media.Playback.MediaPlayer _player = new();
private readonly SystemMediaTransportControls _smtc;
...
//先禁用系統播放器的命令
_player.CommandManager.IsEnabled = false;
//直接建立SystemMediaTransportControls物件被平臺限制,神奇的是MediaPlayer物件可以建立該NativeObject
_smtc = _player.SystemMediaTransportControls;
//啟用smtc以進行自定義
_smtc.IsEnabled = true;

拿到SMTC物件之後的操作與UWP中無異,這裡簡單看一下:

1.設定可互動性

_smtc.IsPlayEnabled = true;
_smtc.IsPauseEnabled = true;
_smtc.IsNextEnabled = true;
_smtc.IsPreviousEnabled = true;

2.設定媒體資訊

1 var updater = _smtc.DisplayUpdater;
2 updater.AppMediaId = "xxx"; //用於區分不同來源的媒體
3 updater.Type = MediaPlaybackType.Music; //必須指定媒體型別否則拋異常
4 updater.MusicProperties.Title = “Title”;//標題
5 /*...省略相同的欄位設定...*/
6 updater.Thumbnail = RandomAccessStreamReference.CreateFromUri(new Uri(ImgUrl));//設定封面圖
7 updater.Update();//最後呼叫以生效

播放狀態需要單獨設定:

_smtc.PlaybackStatus = MediaPlaybackStatus.Playing; //Paused \ Stopped
//直接設定無需更新

3.響應SMTC互動

 1 _smtc.ButtonPressed += _smtc_ButtonPressed;
 2 ...
 3  private void _smtc_ButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args)
 4         {
 5             switch(args.Button)
 6             {
 7                 case SystemMediaTransportControlsButton.Play:
 8                     //Play
 9                     break;
10                 case SystemMediaTransportControlsButton.Pause:
11                     //Pause
12                     break;
13                 case SystemMediaTransportControlsButton.Next:
14                     //Next
15                     break;
16                 case SystemMediaTransportControlsButton.Previous:
17                     //Previous
18                     break;
19             }
20         }

注意,文中所有SMTC的事件均由系統觸發,意味著非同一執行緒,需要用Dispatcher來操作UI

三、獲取和控制系統媒體

好訊息是,負責這部分的模組GlobalSystemMediaTransportControlsSession公開可以任意使用,不受UWP平臺限制。

1.獲取媒體資訊

 1 var gsmtcsm = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();//獲取SMTC會話管理器
 2 gsmtcsm.CurrentSessionChanged += xxx; //當前會話改變或退出時發生,微軟對CurrentSession的解釋是使用者可能最希望控制的媒體會話,實測為Windows控制中心頂部顯示的媒體,當有多個媒體時使用者可以在此選擇切換
 3 ...
 4 var session = gsmtcsm.GetCurrentSession();
 5 if(session == null)
 6     return; //當前沒有註冊的SMTC會話
 7 
 8 //接下來操作session即可,下面僅提供參考資訊
 9 
10 //媒體資訊改變時發生,奇怪的是這些事件提供的引數e並沒有任何資訊
11 session.MediaPropertiesChanged += async (sender, e)=>{
12     //觸發事件時主動拉取資訊
13     var info = await _globalSMTCSession.TryGetMediaPropertiesAsync();
14 }; 
15 //播放狀態改變時發生
16 session.PlaybackInfoChanged +=(sender,e)=>{
17     var status = globalSMTCSession.GetPlaybackInfo().PlaybackStatus;
18 };

2.控制媒體播放

直接呼叫即可

await session.TryPauseAsync();
await session.TryPlayAsync();
await session.TrySkipPreviousAsync();
await session.TrySkipNextAsync();

四、一些奇怪的地方

1.無法顯示媒體來源,並且不會清空上一個來源的資訊

可能是因為沒有提供合法的UWP控制代碼,Windows雖然能確定是哪個exe呼叫的SMTC,但是拒絕直接顯示exe的資訊。邏輯上來說這個來源資訊會被空覆蓋掉,但是並沒有。

2.資訊更新不一致和延時

系統顯示的會話以及提供GlobalSMTCSessionMng.獲取的資訊有時會不一致,二者都有可能和應用真實在播放的不一致,後者獲取的封面圖有時也會不一致。此外,MusicProperty的更新有時並不會實時反饋到GlobalSMTCSession的Changed事件,我測試的時候當系統記憶體爆滿(98% 我開了一堆瀏覽器標籤頁和4個vs)的時候,更新丟失的機率在70%左右,而資源充足時可以做到幾乎即時更新。

3.暫未實現點選跳轉到App

正統UWP App的SMTC會話是可以點選跳轉到App播放介面的,但是我並沒有找到相關的事件。

4.奇怪的MediaId

Windows系統似乎透過這個來區分不同的媒體來源(明明可以獲得呼叫者- -),神奇的是如果你為兩個應用設定了同樣的MediaId,那麼只有兩個都關閉時,SMTC會話才會釋放。此外透過GlobalSMTCSession.SourceAppUserModelId並不能獲得你設定的MediaId,而是呼叫者的檔名"xxx.exe"。

五、使用我封裝的庫

Demo和庫已經開源:TwilightLemon/MediaTest: .NET 8 WPF using SMTC (github.com)

簡單地將現有的解碼器接入SMTC:

SMTCCreator? _smtcCreator = null;
...
 _smtcCreator ??= new SMTCCreator("MediaTest");
//修改播放狀態
_smtcCreator.SetMediaStatus(SMTCMediaStatus.Playing);
//設定媒體資訊
_smtcCreator.Info.SetAlbumTitle("AlbumTitle")
                    .SetArtist("Taylor Swift")
                    .SetTitle("Dancing With Our Hands Tied")
                    .SetThumbnail("https://y.qq.com/music/photo_new/T002R300x300M000003OK4yP2MBOip_1.jpg?max_age=2592000")
                    .Update();
//註冊互動響應
_smtcCreator.PlayOrPause += _smtcCreator_PlayOrPause;
_smtcCreator.Previous += _smtcCreator_Previous;
_smtcCreator.Next += _smtcCreator_Next;

//合適的時候呼叫釋放資源
_smtcCreator.Dispose();

簡單地控制系統媒體:

SMTCListener _smtcListener = null;
...
_smtcListener = await SMTCListener.CreateInstance();
_smtcListener.MediaPropertiesChanged += _smtcListener_MediaPropertiesChanged;
_smtcListener.PlaybackInfoChanged += _smtcListener_PlaybackInfoChanged;
_smtcListener.SessionExited += _smtcListener_SessionExited;
...
//媒體退出
 private void _smtcListener_SessionExited(object? sender, EventArgs e) { }

//播放狀態改變
private void _smtcListener_PlaybackInfoChanged(object? sender, EventArgs e)
{
    Dispatcher.Invoke(() =>
    {
        var info = _smtcListener.GetPlaybackStatus();
        if (info == null) return;
        StatusTb.Text = info.ToString();
    });
}
//媒體資訊改變
private void _smtcListener_MediaPropertiesChanged(object? sender, EventArgs e)
{
    Dispatcher.Invoke(async () =>
    {
        var info = await _smtcListener.GetMediaInfoAsync();
        if (info == null) return;
        TitleTb.Text = info.Title;
        ArtistTb.Text = info.Artist;
        AlbumTitleTb.Text = info.AlbumTitle;
        //獲取封面圖的方法
        if (info.Thumbnail != null)
        {
            var img = new BitmapImage();
            img.BeginInit();
            img.StreamSource = (await info.Thumbnail.OpenReadAsync()).AsStream();
            img.EndInit();
            ThumbnailImg.Source = img;
        }
    });
}
...
//控制播放
await _smtcListener.Previous();
await _smtcListener.Next();
await _smtcListener.Pause();
await _smtcListener.Play();

六、寫在最後

參考資料:

1)SystemMediaTransportControls 類 (Windows.Media) - Windows UWP applications | Microsoft Learn

2)桌面應用中不支援 Windows 執行時 API - Windows 應用 |Microsoft學習 --- Windows Runtime APIs not supported in desktop apps - Windows apps | Microsoft Learn

3)GlobalSystemMediaTransportControlsSessionManager Class (Windows.Media.Control) - Windows UWP applications | Microsoft Learn

打個小廣告,我的頂部欄專案正在開發中,現已整合SMTC和眾多小功能,歡迎支援:TwilightLemon/MyToolBar: 為Surface Pro而生的頂部工具欄 支援觸控和筆快捷方式 (github.com)

全域性媒體播放控制:

未來將支援更多外掛:

本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名TwilightLemon,不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。

相關文章