UWP版本的網易雲音樂已經上架,雖然還不支持Windows Phone但是整體而言功能已經比較齊全了!
那么如何在Windows 10 UWP實現后台播放呢?
我之前是一直在做Windows Phone 8.0的開發,在8.0中為了實現后台播放音樂可使用后台音頻代理,但是在Windows 10中會發現只有一個Windows Runtime Component...
(有必要補充一下的是我沒有Windows Phone 8.1 的開發經歷)
現在想把8.0的App遷移到Windows 10,所以必須要解決后台音頻播放的問題。
廢話就不多說了,下面進入正題。
.......讓我再嘮叨一句,學生黨,第一次來博客園寫東西沒什么經驗,各位多多原宥............
一:如何創建后台任務
下面先從最簡單的開始(如果你已經創建過后台任務的可以跳過)
先新建一個Black App,然后添加一個Windows運行時組件和一個類庫(為了共享一些代碼)
然后添加BackgroundAudioTask對BackgroundAudioShare的引用,同時添加主項目對BackgroundAudioTask和BackgroundAudioShare的引用
最后在 appxmanifest添加聲明,注意勾選Audio
注意下面這一張圖片
EntryPoint出填寫的是后台任務的 命名空間.類名
二:Windows10 中后台音頻代理的工作方式
先來借用msdn的一張圖片
2.1如何通信
之所以要和后台通信,最簡單的答案是我需要將音樂列表,播放順序之類的信息傳遞給后台。
圖上左側的是App的前台部分,右側為后台部分,前台與后台通信的方式可以用過BackgroundMedia實例的SendMessageToForeground和SendMessageToBackground方法,與MessageReceivedFromForeground和MessageReceivedFromBackground事件來實現。
2.2通信時需要注意的事項
1.當用戶第一次在前台中調用BackgroundMediaPlayer.Current或者是注冊BackgroundMediaPlayer.MessageReceivedFromBackground事件的時候會出發IBackgroundTask.Run()方法,也就是后台任務開始執行。為了防止錯過來自后台的消息,建議先注冊BackgroundMediaPlayer.MessageReceivedFromBackground事件。
2.SendMessageToForeground方法的參數為一個ValueSet,同理在對應的MessageReceivedFromBackground事件中可以接收到這個ValueSet,這樣的話我們就可以通過這個ValueSet傳遞我們想要的信息。
好了還是來看一下具體的實現過程:
我們先在BackgroundAudioShare工程中添加一個Models的文件夾,然后添加一個Music類
Music類的代碼如下:
public class Music { public string Id { get; set; } public string Title { get; set; } public string Artist { get; set; } /// <summary> /// 指定音樂文件位置 /// </summary> public Uri MusicUri { get; set; } /// <summary> /// 用於在UAC中顯示圖片 /// </summary> public Uri AlbumUri { get; set; } }
我們希望通過使用Json的方式將數據序列化后傳遞給后台或前台,所以這里我們需要為BackgroundAudioShare工程添加Json.Net引用。
可打開Tools->Nuget Package Manager->Package Manager Console這個工具,輸入
Install-Package Newtonsoft.Json
如下圖,注意選擇的工程為BackgroundAudioShare
或者是在BackgroundAudioShare工程的References上點擊Manage Nuget Packages,搜索Json.Net並安裝這里我就不給圖片展示了。
我更喜歡第一種方式因為更加快速,各位可以選擇喜歡的方式安裝。
然后我們向主工程中添加兩首音樂和兩張圖片,當然也可選擇使用鏈接的方式這樣就無需添加了,這個為了演示方便我添加兩首本地音樂用作演示
首先我們在MainPage的后台文件中添加一個音樂列表
private List<Music> list = new List<Music>();
然后添加一個InitializeMusicList方法,並且在構造函數中調用
private void InitializeMusicList() { var m1 = new Music(); m1.Id = "1"; m1.Title = "Tell Me Why"; m1.Artist = "Declan Galbraith"; m1.AlbumUri = new Uri("ms-appx:///Assets/Music/Tell Me Why.jpg", UriKind.Absolute); m1.MusicUri = new Uri("ms-appx:///Assets/Music/Tell Me Why.mp3", UriKind.Absolute); var m2 = new Music(); m2.Id = "1"; m2.Title = "潮鳴"; m2.Artist = "Clannad"; m2.AlbumUri = new Uri("ms-appx:///Assets/Music/潮鳴.jpg", UriKind.Absolute); m2.MusicUri = new Uri("ms-appx:///Assets/Music/潮鳴.mp3", UriKind.Absolute); List.Add(m1); List.Add(m2); }
在前台添加一個Button用於開始播放
<Button Content="Play" Click="Button_Click"></Button>
好的為了更好地完成前台和后台的通信我們需要在共享工程中添加一些代碼
首先是添加個Message文件夾
然后添加一個MessageType的枚舉
命名空間是BackgroundAudioShare.Message
public enum MessageType { /// <summary> /// 更新音樂列表 /// </summary> UpdateMusicList, /// <summary> /// 下一曲 /// </summary> SkipToNext, /// <summary> /// 上一曲 /// </summary> SkipToPrevious, /// <summary> /// 用於調到指定的某一首 /// </summary> TackChanged, /// <summary> /// 開始播放 /// </summary> StartPlayMusic, /// <summary> /// 后台任務啟動 /// </summary> BackgroundTaskStart }
再添加一個MessageService用戶完成傳遞信息的任務
public static class MessageService { /// <summary> /// ValueSet的Key /// </summary> public const string MessageType = "MessageType"; /// <summary> /// ValueSet的Key /// </summary> public const string MessageBody = "MessageBody"; /// <summary> /// 向前台傳送信息 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="message"></param> /// <param name="type"></param> public static void SendMessageToForeground(object message, MessageType type) { var payload = new ValueSet(); payload.Add(MessageType, type); payload.Add(MessageBody, JsonConvert.SerializeObject(message)); BackgroundMediaPlayer.SendMessageToForeground(payload); } /// <summary> /// 向后台傳送信息 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="message"></param> /// <param name="type"></param> public static void SendMessageToBackground(object message, MessageType type) { var payload = new ValueSet(); payload.Add(MessageType, type); payload.Add(MessageBody, JsonConvert.SerializeObject(message)); BackgroundMediaPlayer.SendMessageToBackground(payload); } public static bool ParseMessage<T>(ValueSet valueSet, out T message) { object messageBodyValue; message = default(T); // Get message payload if (valueSet.TryGetValue(MessageService.MessageBody, out messageBodyValue)) { // Validate type message = JsonConvert.DeserializeObject<T>(messageBodyValue.ToString()); return true; } return false; } }
再添加一個更新音樂列表的Message
public class UpdateMusicListMessage { public List<Music> List { get; set; } public UpdateMusicListMessage(List<Music> list) { List = list; } }
最后再添加一個EnumHelper
命名空間為BackgroundAudioShare
public static class EnumHelper { public static T Parse<T>(string value) where T : struct => (T)Enum.Parse(typeof(T), value); public static T Parse<T>(int value) where T : struct => (T)Enum.Parse(typeof(T), value.ToString()); }
好的現在就可以開始從前台向后台傳輸信息了
我們繼續在MainPage的后台文件中添加相應代碼
首先添加一個屬性一個字段
/// <summary> /// 獲取當前實例的引用 /// </summary> private MediaPlayer CurrentPlayer { get { MediaPlayer player = null; try { player = BackgroundMediaPlayer.Current; } catch { Debug.WriteLine("Failed to get MediaPlayer instance"); return null; } return player; } } /// <summary> /// 用於等待后台任務開啟 /// </summary> private AutoResetEvent _backgroundAudioTaskStarted = new AutoResetEvent(false);
然后添加Click的處理事件
private void Button_Click(object sender, RoutedEventArgs e) { StartbackgroundTask(); } private async void StartbackgroundTask() { BackgroundMediaPlayer.MessageReceivedFromBackground += BackgroundMediaPlayer_MessageReceivedFromBackground; await Dispatcher.RunAsync( Windows.UI.Core.CoreDispatcherPriority.Normal,() => { //如果后台任務開啟成功 var res = _backgroundAudioTaskStarted.WaitOne(2000); if (res) { MessageService.SendMessageToBackground(new UpdateMusicListMessage(List), MessageType.UpdateMusicList); MessageService.SendMessageToBackground(null, MessageType.StartPlayMusic); } }); } /// <summary> /// 用於接收來自后台的信息 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void BackgroundMediaPlayer_MessageReceivedFromBackground(object sender, MediaPlayerDataReceivedEventArgs e) { MessageType type = EnumHelper.Parse<MessageType>(e.Data[MessageService.MessageType].ToString()); switch (type) { case MessageType.BackgroundTaskStart: //后台任務開啟成功 _backgroundAudioTaskStarted.Set(); break; default: break; } }
現在已經完成了前台的任務,接下來我們去看一下后台應該如何編寫
這次我們先看代碼:
public sealed class MyBackgroundAudioTask : IBackgroundTask { private const string TrackIdKey = "trackid"; private const string TitleKey = "title"; private const string AlbumArtKey = "albumart"; private const string ArtistKey = "artist"; private BackgroundTaskDeferral _deferral; // Used to keep task alive private SystemMediaTransportControls _smtc; /// <summary> /// 音樂播放列表 /// </summary> private MediaPlaybackList _playbackList = new MediaPlaybackList(); public void Run(IBackgroundTaskInstance taskInstance) { _deferral = taskInstance.GetDeferral(); // Initialize SystemMediaTransportControls (SMTC) for integration with // the Universal Volume Control (UVC). // // The UI for the UVC must update even when the foreground process has been terminated // and therefore the SMTC is configured and updated from the background task. _smtc = BackgroundMediaPlayer.Current.SystemMediaTransportControls; _smtc.ButtonPressed += _smtc_ButtonPressed; _smtc.PropertyChanged += _smtc_PropertyChanged; _smtc.IsEnabled = true; _smtc.IsPauseEnabled = true; _smtc.IsPlayEnabled = true; _smtc.IsNextEnabled = true; _smtc.IsPreviousEnabled = true; // Add handlers for MediaPlayer BackgroundMediaPlayer.Current.CurrentStateChanged += Current_CurrentStateChanged; // Initialize message channel BackgroundMediaPlayer.MessageReceivedFromForeground += BackgroundMediaPlayer_MessageReceivedFromForeground; // Notify foreground app MessageService.SendMessageToForeground(null, MessageType.BackgroundTaskStart); taskInstance.Canceled += TaskInstance_Canceled; } private void BackgroundMediaPlayer_MessageReceivedFromForeground(object sender, MediaPlayerDataReceivedEventArgs e) { MessageType type = EnumHelper.Parse<MessageType>(e.Data[MessageService.MessageType].ToString()); switch (type) { case MessageType.StartPlayMusic: StartPlayback(); break; case MessageType.UpdateMusicList: { UpdateMusicListMessage message; var res = MessageService.ParseMessage(e.Data, out message); if (res) CreateMusicList(message.List); } break; default: break; } } private void StartPlayback() { //這里可以添加跟多的處理邏輯 try { BackgroundMediaPlayer.Current.Play(); } catch(Exception ex) { Debug.WriteLine(ex.Message); } } /// <summary> /// 創建音樂列表 /// </summary> /// <param name="list"></param> private void CreateMusicList(List<Music> musicList) { _playbackList = new MediaPlaybackList(); _playbackList.AutoRepeatEnabled = true; foreach (var music in musicList) { var source = MediaSource.CreateFromUri(music.MusicUri); //為音樂添加一些附加信息用於在UVC上顯示 source.CustomProperties[TrackIdKey] = music.Id; source.CustomProperties[TitleKey] = music.Title; source.CustomProperties[AlbumArtKey] = music.AlbumUri; source.CustomProperties[ArtistKey] = music.Artist; _playbackList.Items.Add(new MediaPlaybackItem(source)); } // Don't auto start BackgroundMediaPlayer.Current.AutoPlay = false; // Assign the list to the player BackgroundMediaPlayer.Current.Source = _playbackList; // Add handler for future playlist item changes _playbackList.CurrentItemChanged += PlaybackList_CurrentItemChanged; } private void PlaybackList_CurrentItemChanged(MediaPlaybackList sender, CurrentMediaPlaybackItemChangedEventArgs args) { // Get the new item var item = args.NewItem; // Update the system view UpdateUVCOnNewTrack(item); //通知前台 歌曲已經更改 } private void UpdateUVCOnNewTrack(MediaPlaybackItem item) { if (item == null) { _smtc.PlaybackStatus = MediaPlaybackStatus.Stopped; _smtc.DisplayUpdater.MusicProperties.Title = String.Empty; _smtc.DisplayUpdater.Update(); return; } // 從附加信息中提取相關內容然,更新UVC _smtc.PlaybackStatus = MediaPlaybackStatus.Playing; _smtc.DisplayUpdater.Type = MediaPlaybackType.Music; _smtc.DisplayUpdater.MusicProperties.Title = item.Source.CustomProperties[TitleKey] as string; _smtc.DisplayUpdater.MusicProperties.Artist = item.Source.CustomProperties[ArtistKey] as string; //_smtc.DisplayUpdater.MusicProperties.AlbumTitle = "追夢"; //_smtc.DisplayUpdater.MusicProperties.AlbumArtist = "追夢"; var albumArtUri = item.Source.CustomProperties[AlbumArtKey] as Uri; if (albumArtUri != null) _smtc.DisplayUpdater.Thumbnail = RandomAccessStreamReference.CreateFromUri(albumArtUri); else _smtc.DisplayUpdater.Thumbnail = null; _smtc.DisplayUpdater.Update(); } private void Current_CurrentStateChanged(MediaPlayer sender, object args) { //播放狀態更改, 更新UVC if (sender.CurrentState == MediaPlayerState.Playing) { _smtc.PlaybackStatus = MediaPlaybackStatus.Playing; } else if (sender.CurrentState == MediaPlayerState.Paused) { _smtc.PlaybackStatus = MediaPlaybackStatus.Paused; } else if (sender.CurrentState == MediaPlayerState.Closed) { _smtc.PlaybackStatus = MediaPlaybackStatus.Closed; } } private void _smtc_PropertyChanged(SystemMediaTransportControls sender, SystemMediaTransportControlsPropertyChangedEventArgs args) { // 比如音量改變,做出相應調整 } private void _smtc_ButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args) { switch (args.Button) { case SystemMediaTransportControlsButton.Play: Debug.WriteLine("UVC play button pressed"); // Add some code BackgroundMediaPlayer.Current.Play(); break; case SystemMediaTransportControlsButton.Pause: Debug.WriteLine("UVC pause button pressed"); try { BackgroundMediaPlayer.Current.Pause(); } catch (Exception ex) { Debug.WriteLine(ex.ToString()); } break; case SystemMediaTransportControlsButton.Next: Debug.WriteLine("UVC next button pressed"); SkipToNext(); break; case SystemMediaTransportControlsButton.Previous: Debug.WriteLine("UVC previous button pressed"); SkipToPrevious(); break; } } private void SkipToNext() { _smtc.PlaybackStatus = MediaPlaybackStatus.Changing; _playbackList.MoveNext(); // TODO: Work around playlist bug that doesn't continue playing after a switch; remove later BackgroundMediaPlayer.Current.Play(); } private void SkipToPrevious() { _smtc.PlaybackStatus = MediaPlaybackStatus.Changing; _playbackList.MovePrevious(); // TODO: Work around playlist bug that doesn't continue playing after a switch; remove later BackgroundMediaPlayer.Current.Play(); } private void TaskInstance_Canceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason) { _deferral.Complete(); } }
首先這是一個長期執行的任務所以需要調用_deferral = taskInstance.GetDeferral();
然后需要注意的是更新UVC和如何創建播放列表。這一部分代碼都比較簡單,就是事件比較多。
執行效果:
總結一下:上述的代碼只能說非常非常的簡陋,比如前台沒有一個播放列表的顯示,但是此篇隨筆主要在闡述如何實現后台播放,如果寫一個完整的Demo那么代碼可能會多了一點。但是下面我會總結一下需要注意的地方,各位可以參考一下。
首先是,前台不能拿到的信息才需要從后台傳遞過來,比如播放狀態更改之類的事件,前台是可以監聽的所以就無需從后台傳遞;但是因為UVC切換或者是歌曲播放結束所引發的當前播放的音樂的更改,前台無法拿到此時就需要后台主動通知前台。下面我總結了一個列表,希望對各位有幫助
事件 | 前台 | 后台 |
播放狀態更改 | 可以 | 可以 |
音量更改 | 可以 | 可以 |
UVC操作 | 不可以 | 可以 |
播放項目更改 | 不可以 | 可以 |
下面給出MSDN鏈接:MSDN
源代碼:360雲盤 (提取碼:e9bd)
第一次來博客園,謝謝啦