Introduction
If you are writing a software which plays media on Windows Phone 7 and you want that software up in the marketplace, you will have to fulfill the wp7 application certification requirement 6.5.3:
Applications that Play a Video or Audio Segment
"An application may interrupt the currently playing music to play a non-interactive full motion video or audio segment (e.g. cut-scene or media clip) without asking for user consent. If music was playing prior to the segment, the application must resume music when the segment has completed."
Background
As I know, this can't be done completely yet, because of a little bug which I heard is under fixing! Currently there is no SDK functionality that lets you just resume the playback after your mediaplay. The reason why it can't be done requires a little understanding about the system's MediaPlayer:
- The only way to interact with this MediaPlayer is through the
Microsoft.Xna.Framework.Media
namespace.
MSDN Link: http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.media.aspx
- The
MediaPlayer
class here provides the functions to control the system's MediaPlayer
. This MediaPlayer
stores the currently played songs in a special Queue which can be indexed like arrays too.
Basically there are 2 main problems with this Queue:
- If you play a
MediaContent
on some MediaElement
or SmoothStreamingMediaElement
, it overrides the System's MediaElement
Queue too, so you can't resume the Current MediaPlay
.
- It's a read-only collection, that means if you store the Songs played before your play, you can't restore them back to the Queue.
In XNA games, you can use music effects without damaging the queue, but if you want to play background music, that will stop the music and override the queue. One of Microsoft's solution with the Game Twin Blades is that the music plays when the game starts, it asks the user whether he/she wants to continue listening to the music or stop and hear the game music after. If we choose not to stop the music, we can play with our own music in the background.
The Workaround
Currently we have only one way to override the Queue, Playing Media. There are 4 scenarios of Background music playing on the Phone:
- The user listens to the Radio
- The user listens to a Song from the Music collection
- The user listens to an Album from the Music Collection
- The user listens to a PlayList from the Music Collection
- The user adds songs from 2 or more albums/playlists to the now playing collection (find the explanation below)
So we store the Queue and the current Radio state before our media play, and then when we need to restore the previous state, we just check these 4 scenarios if it fits the stored list of songs or not.
After checking the stored RadioPower
state, we need to iterate over the playlists and albums, and see which one has the same item number, and contains all songs from the stored queue. Then find the index of the currently played song to start the Playing from there. This way, we can restore the RadioPlay
or the MusicPlay
after our MediaPlayback
. The downsides of this workaround is that we can't resume the current song where we left it (no XNA player method for seeking), the song will start from the beginning. It may be an issue for those who listen to long DJ Sets or Mixes regularly, but better than nothing. Note that the Radio sometimes gives back OFF power state while it's actually ON, so I am also watching that the queue's ActiveSongIndex
is equal to -1
or not. When playing a real song, it is always greater than -1
.
More from the Queue can be found at http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.media.mediaqueue_members.aspx.
The User Adds Songs from 2 or More Albums/Playlists to the Now Playing Collection
We can't create custom SongCollections
because of this:
"All of the collections, playlists, and queues returned by methods and properties in the Microsoft.Xna.Framework.Media namespace are immutable. You cannot add or remove objects from those collections or playlists. To create a custom 'playlist' of songs, games must maintain their own list of songs to play, and play those songs one at a time by calling MediaPlayer.Play."
This is the reason the 5th scenario cannot be restored. We also can't add items to the Now Playing collection in the Music Hub. I think the best solution is to simply let the user resume the music manually. With this approach, the queue stays intact and can be resumed by the user. If anybody has a better solution, please send it to me and I will post it here.
In my code, I made some functions which can be used freely for anyone under the MS-PL license, they are in the XnaMusicUtil.cs.
Under the Hood
First, have a look at the Save
Function:
public static void SaveCurrentMediaState(bool isStopping = false)
{
currQueue.Clear();
currSong = null;
isRadio = false;
hasSaved = false;
currState = MediaPlayer.State;
Debug.WriteLine(MediaPlayer.State.ToString()); radioFrequency = Microsoft.Devices.Radio.FMRadio.Instance.Frequency;
if (MediaPlayer.Queue != null)
{
switch (MediaPlayer.Queue.Count)
{
case 0:
break;
case 1: 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); 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); }
hasSaved = true;
}
break;
default: 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); }
hasSaved = true;
break;
}
}
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;
}
Saving the current state is really easy compared to the Restore
function. If the radio is on or the ActiveSong
equals -1
, the user listens to the Radio
. In every other case, if the queue has items, then the user Listens to some sort of Music. Read and store, that's all. We can stop the Music at the end of the Save if we want by simply setting the isStopping
parameter to True
.
Now the Restoring:
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; }
else
{
MediaLibrary ml = new MediaLibrary();
Debug.WriteLine("Before restore: " +
MediaPlayer.State.ToString()); switch (currQueue.Count)
{
case 0:
break;
case 1:
if (currSong != null) {
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)) {
for (int i = 0; i < ml.Albums.Count; i++)
{
isFound = MatchAndPlay(ml.Albums[i].Songs);
if (isFound) break;
}
}
if ((currSong != null) && (!isFound)) {
}
break;
}
}
switch (currState) {
case MediaState.Paused:
MediaPlayer.Pause();
break;
case MediaState.Playing:
break;
case MediaState.Stopped:
MediaPlayer.Stop();
break;
default:
break;
}
}
currQueue.Clear();
hasSaved = false;
}
Notice that we have to Play the Songs in order to restore the Queue, but if the original state was Stopped or Paused, we Stop/Pause them right after Playing. This could produce a small audio glitch resulted by the quick Audio on-off. I'm not even sure the final devices have this "effect" or not.
Both the Playlists and the Albums are SongCollection
classes. I'm using the following function to compare them with the saved queue. If the SongCollection
has all the Songs of the queue, then it determines the index of the last played song and starts the play with it.
private static bool MatchAndPlay(SongCollection sc) {
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) {
for (int k = 0; k < sc.Count; k++) {
if (sc[k] == currSong) {
currIndex = k;
MediaPlayer.Play(sc, currIndex);
break;
}
}
}
}
return isFound;
}
Using the Code
This code can be used both in Silverlight and XNA environments. For example, if you have a page dedicated to a MediaElement
use the save and restore function like this:
void MediaElementPage_Unloaded(object sender, RoutedEventArgs e)
{
mediaElement.Stop();
XnaMusicUtil.RestoreCurrentMediaState();</strong>
}
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
XnaMusicUtil.SaveCurrentMediaState();</strong>
base.OnNavigatedTo(e);
}
In the App.xaml.cs for proper Tombstoning:
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
XnaMusicUtil.RestoreCurrentMediaState();</strong>
}
Good to Know
This article can be found on my WP7 Blog too:
The XNA framework requires the FrameworkDispatcher.Update()
function to call regularly for proper function.
During my tests, I figured out that calling this function just after the other XNA function calls is enough, but if you want to make sure your code won't collapse because of this, you have to implement the XNAAsyncDispatcher
from here:
Read the whole article too, it contains a good explanation about using XNA in Silverlight environment.
History
- 2010. November 9: Article and code updated