當(dāng)你使用Edge等瀏覽器或系統(tǒng)軟件播放媒體時(shí),Windows控制中心就會出現(xiàn)相應(yīng)的媒體信息以及控制播放的功能,如圖。
SMTC (SystemMediaTransportControls) 是一個(gè)Windows App SDK (舊為UWP) 中提供的一個(gè)API,用于與系統(tǒng)媒體交互。接入SMTC的好處在于,將媒體控制和媒體信息共享給系統(tǒng),使用通用的特性(例如接受鍵盤快捷鍵的播放暫停、接受藍(lán)牙設(shè)備的控制),便于與其它支持SMTC的應(yīng)用交互等。
在UWP App中使用它很簡單,只需要調(diào)用SystemMediaTransportControls.GetForCurrentView()方法即可,但是該方法僅限在有效的UWP App中調(diào)用,否則將拋出“Invalid window handle”異常。實(shí)際上,在官方文檔中提到所有XXXForCurrentView方法均不適用于UWP App以外的程序調(diào)用。
這些 XxxForCurrentView 方法對 ApplicationView 類型具有隱式依賴關(guān)系,桌面應(yīng)用不支持該類型。由于桌面應(yīng)用不支持 ApplicationView,因此也不支持任何 XxxForCurrentView 方法。
此外官方文檔還給出一個(gè)可替代的接口ISystemMediaTransportControlsInterop,然而這個(gè)接口在給的SDK中有保護(hù)性,無法訪問。
至此,直接創(chuàng)建SMTC的方法走不通。但是我發(fā)現(xiàn)一個(gè)奇怪的地方,UWP提供的在Windows.Media.Playback命名空間下的MediaPlayer可以和SMTC自動(dòng)集成,并且可以通過SystemMediaTransportControls屬性直接拿到SMTC對象。MediaPlayer內(nèi)部通過某種COM組件直接創(chuàng)建了該NativeObject,而沒有走API提供的GetForCurrentView或FromAbi方法。也就是說,SMTC組件其實(shí)不需要使用合法的UWP Window句柄來創(chuàng)建,只不過可能為了某些特性而加上了該限制(后文將提到)。幸運(yùn)的是,MediaPlayer幫我們繞過了這點(diǎn)。
下文講解手動(dòng)與SMTC交互而不是直接使用MediaPlayer進(jìn)行播放,你的項(xiàng)目可能已經(jīng)有了其它的解碼器(如WPF版本的MediaPlayer、Bass.Net解碼器、NAudio等),則只需要將交互部分接入SMTC而不更換解碼器。
文末提供了我封裝好的SMTCCreator和SMTCListener,可以直接使用。
一、引用WinRT API到項(xiàng)目
最便捷的方法是直接修改目標(biāo)框架到win10,這樣就能自動(dòng)引入WinRT API:
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
或者一些其他的方法,可以參考這篇博客:如何在WPF中調(diào)用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;
...
//先禁用系統(tǒng)播放器的命令
_player.CommandManager.IsEnabled = false;
//直接創(chuàng)建SystemMediaTransportControls對象被平臺限制,神奇的是MediaPlayer對象可以創(chuàng)建該NativeObject
_smtc = _player.SystemMediaTransportControls;
//啟用smtc以進(jìn)行自定義
_smtc.IsEnabled = true;
拿到SMTC對象之后的操作與UWP中無異,這里簡單看一下:
1.設(shè)置可交互性
_smtc.IsPlayEnabled = true;
_smtc.IsPauseEnabled = true;
_smtc.IsNextEnabled = true;
_smtc.IsPreviousEnabled = true;
2.設(shè)置媒體信息
var updater = _smtc.DisplayUpdater;
updater.AppMediaId = "xxx"; //用于區(qū)分不同來源的媒體
updater.Type = MediaPlaybackType.Music; //必須指定媒體類型否則拋異常
updater.MusicProperties.Title = “Title”;//標(biāo)題
/*...省略相同的字段設(shè)置...*/
updater.Thumbnail = RandomAccessStreamReference.CreateFromUri(new Uri(ImgUrl));//設(shè)置封面圖
updater.Update();//最后調(diào)用以生效
播放狀態(tài)需要單獨(dú)設(shè)置:
_smtc.PlaybackStatus = MediaPlaybackStatus.Playing; //Paused \ Stopped
//直接設(shè)置無需更新
3.響應(yīng)SMTC交互
_smtc.ButtonPressed += _smtc_ButtonPressed;
...
private void _smtc_ButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args)
{
switch(args.Button)
{
case SystemMediaTransportControlsButton.Play:
//Play
break;
case SystemMediaTransportControlsButton.Pause:
//Pause
break;
case SystemMediaTransportControlsButton.Next:
//Next
break;
case SystemMediaTransportControlsButton.Previous:
//Previous
break;
}
}
注意,文中所有SMTC的事件均由系統(tǒng)觸發(fā),意味著非同一線程,需要用Dispatcher來操作UI
三、獲取和控制系統(tǒng)媒體
好消息是,負(fù)責(zé)這部分的模塊GlobalSystemMediaTransportControlsSession公開可以任意使用,不受UWP平臺限制。
1.獲取媒體信息
var gsmtcsm = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();//獲取SMTC會話管理器
gsmtcsm.CurrentSessionChanged += xxx; //當(dāng)前會話改變或退出時(shí)發(fā)生,微軟對CurrentSession的解釋是用戶可能最希望控制的媒體會話,實(shí)測為Windows控制中心頂部顯示的媒體,當(dāng)有多個(gè)媒體時(shí)用戶可以在此選擇切換
...
var session = gsmtcsm.GetCurrentSession();
if(session == null)
return; //當(dāng)前沒有注冊的SMTC會話
//接下來操作session即可,下面僅提供參考信息
//媒體信息改變時(shí)發(fā)生,奇怪的是這些事件提供的參數(shù)e并沒有任何信息
session.MediaPropertiesChanged += async (sender, e)=>{
//觸發(fā)事件時(shí)主動(dòng)拉取信息
var info = await _globalSMTCSession.TryGetMediaPropertiesAsync();
};
//播放狀態(tài)改變時(shí)發(fā)生
session.PlaybackInfoChanged +=(sender,e)=>{
var status = globalSMTCSession.GetPlaybackInfo().PlaybackStatus;
};
2.控制媒體播放
await session.TryPauseAsync();
await session.TryPlayAsync();
await session.TrySkipPreviousAsync();
await session.TrySkipNextAsync();
四、一些奇怪的地方
1.無法顯示媒體來源,并且不會清空上一個(gè)來源的信息
可能是因?yàn)闆]有提供合法的UWP句柄,Windows雖然能確定是哪個(gè)exe調(diào)用的SMTC,但是拒絕直接顯示exe的信息。邏輯上來說這個(gè)來源信息會被空覆蓋掉,但是并沒有。
2.信息更新不一致和延時(shí)
系統(tǒng)顯示的會話以及提供GlobalSMTCSessionMng.獲取的信息有時(shí)會不一致,二者都有可能和應(yīng)用真實(shí)在播放的不一致,后者獲取的封面圖有時(shí)也會不一致。此外,MusicProperty的更新有時(shí)并不會實(shí)時(shí)反饋到GlobalSMTCSession的Changed事件,我測試的時(shí)候當(dāng)系統(tǒng)內(nèi)存爆滿(98% 我開了一堆瀏覽器標(biāo)簽頁和4個(gè)vs)的時(shí)候,更新丟失的概率在70%左右,而資源充足時(shí)可以做到幾乎即時(shí)更新。
3.暫未實(shí)現(xiàn)點(diǎn)擊跳轉(zhuǎn)到App
正統(tǒng)UWP App的SMTC會話是可以點(diǎn)擊跳轉(zhuǎn)到App播放界面的,但是我并沒有找到相關(guān)的事件。
4.奇怪的MediaId
Windows系統(tǒng)似乎通過這個(gè)來區(qū)分不同的媒體來源(明明可以獲得調(diào)用者- -),神奇的是如果你為兩個(gè)應(yīng)用設(shè)置了同樣的MediaId,那么只有兩個(gè)都關(guān)閉時(shí),SMTC會話才會釋放。此外通過GlobalSMTCSession.SourceAppUserModelId并不能獲得你設(shè)置的MediaId,而是調(diào)用者的文件名"xxx.exe"。
五、使用我封裝的庫
Demo和庫已經(jīng)開源:TwilightLemon/MediaTest: .NET 8 WPF using SMTC (github.com)
簡單地將現(xiàn)有的解碼器接入SMTC:
SMTCCreator? _smtcCreator = null;
...
_smtcCreator ??= new SMTCCreator("MediaTest");
//修改播放狀態(tài)
_smtcCreator.SetMediaStatus(SMTCMediaStatus.Playing);
//設(shè)置媒體信息
_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();
//注冊交互響應(yīng)
_smtcCreator.PlayOrPause += _smtcCreator_PlayOrPause;
_smtcCreator.Previous += _smtcCreator_Previous;
_smtcCreator.Next += _smtcCreator_Next;//合適的時(shí)候調(diào)用釋放資源_smtcCreator.Dispose();
簡單地控制系統(tǒng)媒體:
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) { }
//播放狀態(tài)改變
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)桌面應(yīng)用中不支持 Windows 運(yùn)行時(shí) API - Windows 應(yīng)用 |Microsoft學(xué)習(xí) --- Windows Runtime APIs not supported in desktop apps - Windows apps | Microsoft Learn
3)GlobalSystemMediaTransportControlsSessionManager Class (Windows.Media.Control) - Windows UWP applications | Microsoft Learn
打個(gè)小廣告,我的頂部欄項(xiàng)目正在開發(fā)中,現(xiàn)已集成SMTC和眾多小功能,歡迎支持:TwilightLemon/MyToolBar: 為Surface Pro而生的頂部工具欄 支持觸控和筆快捷方式 (github.com)
全局媒體播放控制:
未來將支持更多插件:
本作品采用 知識共享署名-非商業(yè)性使用-相同方式共享 4.0 國際許可協(xié)議 進(jìn)行許可。歡迎轉(zhuǎn)載、使用、重新發(fā)布,但務(wù)必保留文章署名TwilightLemon,不得用于商業(yè)目的,基于本文修改后的作品務(wù)必以相同的許可發(fā)布。
該文章在 2024/9/13 9:01:21 編輯過