媒体播放后恢复用户音乐
如何在我们的 Silverlight/XNA WP7 应用程序中播放视频或音频后恢复用户正在播放的音乐。

引言
如果您正在编写一款在 Windows Phone 7 上播放媒体的软件,并且希望该软件能在应用商店上架,那么您必须满足 WP7 应用程序认证要求 6.5.3。
播放视频或音频片段的应用程序
“应用程序可以中断当前正在播放的音乐,以播放一个非交互式的全动态视频或音频片段(例如,过场动画或媒体剪辑),而无需征得用户同意。如果在播放该片段之前有音乐正在播放,应用程序必须在片段完成后恢复音乐。”
背景
据我所知,由于一个我听说正在修复的小 bug,这目前还不能完全实现!目前没有任何 SDK 功能可以让你在媒体播放后简单地恢复播放。之所以不能实现,需要对系统 MediaPlayer 有一点了解。
- 与此 MediaPlayer 交互的唯一方法是通过
Microsoft.Xna.Framework.Media
命名空间。
MSDN 链接: http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.media.aspx - 这里的
MediaPlayer
类提供了控制系统MediaPlayer
的函数。这个MediaPlayer
将当前播放的歌曲存储在一个特殊的队列中,该队列也可以像数组一样进行索引。
基本上,这个队列存在两个主要问题。
- 如果您在某个
MediaElement
或SmoothStreamingMediaElement
上播放MediaContent
,它也会覆盖系统的MediaElement
队列,所以您无法恢复当前的MediaPlay
。 - 这是一个只读集合,这意味着如果您存储了播放前播放的歌曲,您就无法将它们恢复到队列中。
在 XNA 游戏中,您可以在不损坏队列的情况下使用音乐效果,但如果您想播放背景音乐,它会停止音乐并覆盖队列。微软 Game Twin Blades 的解决方案之一是,音乐在游戏开始时播放,它会询问用户是否要继续收听音乐,还是在听完游戏音乐后再停止。如果我们选择不停止音乐,我们就可以在后台播放自己的音乐。
解决方法
目前,我们只有一种方法可以覆盖队列,那就是播放媒体。手机上有四种背景音乐播放场景:
- 用户收听广播
- 用户收听音乐收藏中的一首歌曲
- 用户收听音乐收藏中的一张专辑
- 用户收听音乐收藏中的一个播放列表
- 用户将来自 2 张或更多专辑/播放列表的歌曲添加到“正在播放”集合中(请参见下文的解释)
因此,我们在播放媒体之前存储队列和当前的广播状态,然后在需要恢复之前的状态时,我们只需检查这 4 种场景是否与存储的歌曲列表匹配。
在存储了 RadioPower
状态后,我们需要遍历播放列表和专辑,查看哪个与存储的队列具有相同的项目数量,并包含存储队列中的所有歌曲。然后找到当前播放歌曲的索引,以便从那里开始播放。这样,我们就可以在 MediaPlayback
之后恢复 RadioPlay
或 MusicPlay
。这种变通方法的缺点是,我们无法在歌曲的暂停处恢复(XNA 播放器没有搜索方法),歌曲将从头开始播放。对于经常收听长 DJ Set 或 Mixes 的人来说,这可能是一个问题,但总比没有好。请注意,广播有时在实际开启时会返回 OFF 电源状态,所以我还在监视队列的 ActiveSongIndex
是否等于 -1
。播放实际歌曲时,它始终大于 -1
。
有关队列的更多信息,请访问 http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.media.mediaqueue_members.aspx。
用户将来自 2 张或更多专辑/播放列表的歌曲添加到“正在播放”集合中
由于 这个原因,我们无法创建自定义 SongCollections
。
“
Microsoft.Xna.Framework.Media
命名空间中的方法和属性返回的所有集合、播放列表和队列都是不可变的。您不能向这些集合或播放列表添加或删除对象。要创建自定义歌曲“播放列表”,游戏必须维护自己的歌曲列表,并通过调用MediaPlayer.Play
来一次播放一首歌曲。”
这就是第五种场景无法恢复的原因。我们也无法在音乐中心添加歌曲到“正在播放”集合。我认为最好的解决方案是让用户手动恢复音乐。通过这种方法,队列保持不变,并且可以由用户恢复。如果有人有更好的解决方案,请发给我,我将在此发布。
在我的代码中,我创建了一些函数,这些函数可以在 **MS-PL 许可**下 **自由使用**,它们位于 XnaMusicUtil.cs 文件中。
幕后
首先,看一下 Save
函数。
public static void SaveCurrentMediaState(bool isStopping = false)
{
currQueue.Clear();
currSong = null;
isRadio = false;
hasSaved = false;
currState = MediaPlayer.State;
Debug.WriteLine(MediaPlayer.State.ToString()); // State of song:
// Playing / Stopped / Paused
radioFrequency = Microsoft.Devices.Radio.FMRadio.Instance.Frequency;
//Microsoft.Devices.MediaHistory mh = Microsoft.Devices.MediaHistory.Instance;
//Microsoft.Devices.MediaHistoryItem item = mh.NowPlaying;
if (MediaPlayer.Queue != null)
{
switch (MediaPlayer.Queue.Count)
{
case 0:
break;
case 1:// only one song in the queue, can be radio or a real song
if ((Microsoft.Devices.Radio.FMRadio.Instance.PowerMode ==
Microsoft.Devices.Radio.RadioPowerMode.On)||
(MediaPlayer.Queue.ActiveSongIndex==-1))
{
isRadio = true;
Debug.WriteLine("Radio :" +
MediaPlayer.Queue.ActiveSong.Name); // Currently playing song
hasSaved = true;
}
else
{
isRadio = false;
currQueue.Add(MediaPlayer.Queue[0]);
if (MediaPlayer.Queue.ActiveSong != null)
{
currSong = MediaPlayer.Queue.ActiveSong;
Debug.WriteLine(MediaPlayer.Queue.ActiveSong.Name); // Currently
//playing song
}
hasSaved = true;
}
break;
default://mor song in the queue, save the whole queue and the active song too
isRadio = false;
for (int i = 0; i < MediaPlayer.Queue.Count; i++)
{
currQueue.Add(MediaPlayer.Queue[i]);
}
if (MediaPlayer.Queue.ActiveSong != null)
{
currSong = MediaPlayer.Queue.ActiveSong;
Debug.WriteLine(MediaPlayer.Queue.ActiveSong.Name); // Currently
//playing song
}
hasSaved = true;
break;
}
}
//if the user set the isStopping parameter we are stopping the playback right now
if ((MediaPlayer.State == MediaState.Playing) && (isStopping)) MediaPlayer.Stop();
if ((Microsoft.Devices.Radio.FMRadio.Instance.PowerMode ==
Microsoft.Devices.Radio.RadioPowerMode.On) &&
(isStopping)) Microsoft.Devices.Radio.FMRadio.Instance.PowerMode =
Microsoft.Devices.Radio.RadioPowerMode.Off;
//FrameworkDispatcher.Update();
}
与 Restore
函数相比,保存当前状态非常简单。如果收音机开启或者 ActiveSong
等于 -1
,则用户正在收听 Radio
。在其他所有情况下,如果队列中有项目,那么用户正在听某种音乐。读取并存储,仅此而已。如果希望在保存结束时停止音乐,只需将 isStopping
参数设置为 True
即可。
现在是恢复。
public static void RestoreCurrentMediaState()
{
bool isFound = true;
if (hasSaved)
{
if (isRadio)
{
Microsoft.Devices.Radio.FMRadio.Instance.PowerMode =
Microsoft.Devices.Radio.RadioPowerMode.On;
Microsoft.Devices.Radio.FMRadio.Instance.Frequency = radioFrequency;
if (Microsoft.Devices.Radio.FMRadio.Instance.Frequency !=
radioFrequency) Microsoft.Devices.Radio.FMRadio.Instance.Frequency =
radioFrequency; //doublecheck
}
else
{
MediaLibrary ml = new MediaLibrary();
Debug.WriteLine("Before restore: " +
MediaPlayer.State.ToString()); // State of song: Playing / Stopped / Paused
switch (currQueue.Count)
{
case 0:
break;
case 1:
if (currSong != null)//only one song in the queue,
//check if its available to play and play
{
if (ml.Songs.Contains(currSong)) MediaPlayer.Play(currSong);
}
break;
default:
if (ml.Playlists.Count > 0)
{
for (int i = 0; i < ml.Playlists.Count; i++)
{
isFound = MatchAndPlay(ml.Playlists[i].Songs);
if (isFound) break;
}
}
if ((ml.Albums.Count > 0) && (!isFound)) //not a playlist,
//search albums
{
for (int i = 0; i < ml.Albums.Count; i++)
{
isFound = MatchAndPlay(ml.Albums[i].Songs);
if (isFound) break;
}
}
if ((currSong != null) && (!isFound))//happens when the user
//adds items to the Now Playing collection from
//2 or more different albums
{
//MessageBox.Show("We can't resume your music,
//please resume it manually");
}
break;
}
}
switch (currState) //restore the stored state
{
case MediaState.Paused:
MediaPlayer.Pause();
break;
case MediaState.Playing:
break;
case MediaState.Stopped:
MediaPlayer.Stop();
break;
default:
break;
}
}
currQueue.Clear();
hasSaved = false;
//FrameworkDispatcher.Update();
}
请注意,我们必须按顺序播放歌曲才能恢复队列,但如果原始状态是停止或暂停,我们会在播放后立即停止/暂停它们。这可能会产生一个小的音频故障,这是由于快速的音频开启-关闭造成的。我甚至不确定最终设备是否有这种“效果”。
播放列表和专辑都是 SongCollection
类。我使用以下函数将它们与保存的队列进行比较。如果 SongCollection
包含队列中的所有歌曲,则它将确定最后播放的歌曲的索引,并从该索引开始播放。
private static bool MatchAndPlay(SongCollection sc)//used for Compare Albums
//and Playlists with the saved queue
{
bool containsAll = true;
bool isFound = false;
int currIndex = 0;
if (sc.Count == currQueue.Count)
{
for (int j = 0; j < currQueue.Count; j++)
{
if (!sc.Contains(currQueue[j])) containsAll = false;
}
isFound = containsAll;
if (isFound)// the currently checked SongCollection(Album,Playlist)
// contains all the songs from our saved queue
{
for (int k = 0; k < sc.Count; k++)//SongCollection does not
//have .indexof so we iterate
{
if (sc[k] == currSong) //search for the saved activesong in the
// SongCollection to start the playback with it
{
currIndex = k;
MediaPlayer.Play(sc, currIndex);
break;
}
}
}
}
return isFound;
}
Using the Code
这段代码既可以在 Silverlight 环境中使用,也可以在 XNA 环境中使用。例如,如果您有一个专用于 MediaElement
的页面,可以像这样使用保存和恢复函数:
void MediaElementPage_Unloaded(object sender, RoutedEventArgs e)
{
mediaElement.Stop();
XnaMusicUtil.RestoreCurrentMediaState();
}
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
XnaMusicUtil.SaveCurrentMediaState();
base.OnNavigatedTo(e);
}
在 App.xaml.cs 中,为了 proper 的 Tombstoning(断崖式休眠)处理:
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
XnaMusicUtil.RestoreCurrentMediaState();
}
好消息
这篇文章也可以在我的 WP7 博客上找到:
XNA 框架需要定期调用 FrameworkDispatcher.Update()
函数才能正常工作。
在我测试期间,我发现只需在其他 XNA 函数调用之后调用此函数就足够了,但如果您想确保代码不会因此崩溃,您需要在此处实现 XNAAsyncDispatcher
:
也请阅读整篇文章,它对在 Silverlight 环境中使用 XNA 提供了很好的解释。
历史
- 2010 年 11 月 9 日:文章和代码已更新