Background audio in Windows Phone 7.5 (Part 3)

The story so far

In the first part of this series on Background Audio the Background Audio feature was introduced.

In the second part of the series various ways to pass information between the application and the audio agent were outlined, including how to specify a playlist from the main application that the agent will play.

Where we are now

In this third part of the series we will focus on the challenges when using Background Audio when streaming from a remote server.

Challenges with streaming audio

As discussed in the last blog post, the events that are raised in the app code are not complete and as full as the events raised in agent code. Taking the example of a track coming to an end, the app code only sees the “Unknown” and “Playing” events in its “App Side BackgroundAudioPlayer” object (BAP):

1

For a non-streaming item being played (e.g. an MP3 in the app’s Isolated Storage) this is not a big issue – to keep the user informed of the brief period before audio starts the app code can watch for the “Playing” event. In the interim it can display an appropriate message or a ProgressBar to the user.

However for streaming audio this becomes trickier. There are several things to watch for:

  1. The server is not available (because the phone has no working data connection, server is offline or there is no route to the server)
  2. The internet connection is slow to respond or has low bandwidth.

 

Handling not being able to play the remote media

Catching the error and stopping the agent from getting into a bad state

In the case of the agent not being able to find the file to play, the “TrackReady” event will not be raised, but OnError() will be called in your agent. This is a challenge as your agent will then need to decipher the error and communicate this back to your app code. Assuming that you can communicate this error back to your app code using a static class called “BackgroundErrorNotifier”, you would need to add the following lines to your agent.

 protected override void OnError(BackgroundAudioPlayer player, AudioTrack track, Exception error, bool isFatal)
{
    if (isFatal)
    {
        

BackgroundErrorNotifier.AddError(error);

         Abort();
    }
    else {
        

BackgroundErrorNotifier.AddError(error);

         // force the track to stop 

player.Track = null;

         NotifyComplete();
    }            
}

The BackgroundErrorNotifier.AddError() method would have to use either Isolated Storage or Local Database to communicate back to the app code as discussed in the previous blog post – the details of this aren’t shown here.

The most important thing to note is that you must set player.Track to null. If you don’t do this the agent thinks that there is still a track to play and future calls to agent side BAP’s Play() or Pause() methods will cause the agent to crash. Setting the current track to null puts the agent back in a known good state – however this isn’t implemented for you in the Background Audio project template code.

What does this error mean anyway?

In this case the Exception is always of type System.Exception with an Exception.Message which corresponds to an error code.

There are a few different error messages you can get depending on what actually went wrong, but they need translating from number form into something more useful if you’re going to try and describe to your users what went wrong:

Exception.Message Hex equivalent Meaning
“-2147012889” 80072EE7 Can’t find the server (e.g. phone is in flight mode)
“-2147012696” 80072FA8 No available network connection
"-1072889830” C00D001A Can’t find media file (e.g. the MP3 file you were pointing to is no longer on the server)
“-2147467259” 80004005 Non-specific error code

 

Handling slow communications or low bandwidth

If the server is slow to respond and/or there is only low bandwidth available the “TrackReady” event will be raised after a few seconds, and the data will take a long time to buffer. One of the apps that came through our Depth Partner Support program has exhibited this problem. The server was based in a different continent from where the app was being tested. Under test we were seeing it took 7-8 seconds before “TrackReady” event fired and a further 70 seconds before the audio started playing due to the length of time it was taking for the audio to buffer. Once the buffering had stopped the audio stream was playing fine.

Bear the above in mind if you are expecting users of your application to be using audio streams which are hosted on servers which are physically a long way from them.

In this case once “TrackReady” has been received you know that the Background Audio Zune Media Queue is happy with the file (as otherwise one of the errors described above would have been triggered). That means that there is a valid AudioTrack object that can be used to pass information from agent to the app.

In this scenario you can use the AudioTrack.Tag property string to indicate if the media is buffering or not. Start by setting the Tag to “Buffering” as soon as the track is created and passed back to the agent side player…

 private AudioTrack GetNextTrack()
{     // This is an extremely simple agent which plays a single track AudioTrack track = new AudioTrack(
        new Uri(
            "https://traffic.libsyn.com/wpradio/WPRadio_52.mp3"), 
            "WP Radio #52", 
            "WP Radio", 
            "", 
            null);        
    // set buffer flag track.BeginEdit();
    track.Tag = "Buffering";
    track.EndEdit();                                        
    return track;
}

… and then once the agent finally stops buffering change the Tag to “Playing” and the app code can tell the track is *really* playing.

 protected override void OnPlayStateChanged(BackgroundAudioPlayer player, AudioTrack track, PlayState playState)
{
    switch (playState)
    {
        case PlayState.TrackEnded:
            player.Track = GetNextTrack();
            break;
        case PlayState.TrackReady:
            player.Play();
            break;
        case PlayState.Shutdown:
            // TODO: Handle the shutdown state here (e.g. save state) break;
        case PlayState.Unknown:
            break;
        case PlayState.Stopped:
            break;
        case PlayState.Paused:
            break;
        case PlayState.Playing:                 
            break;
        case PlayState.BufferingStarted:
            break;
        case PlayState.BufferingStopped:
            track.BeginEdit();
            track.Tag = "Playing";
            track.EndEdit();   
            break;
        case PlayState.Rewinding:
            break;
        case PlayState.FastForwarding:
            break;
    }
    NotifyComplete();
}

A couple of things to watch for:

  • firstly you must call BeginEdit() and EndEdit() on the AudioTrack before you modify it.
  • secondly I found you need to set an actual string for the Tag, and can’t just use null or the empty string.

Joining this all together in the app code

In the app code, you will clearly only be able to pick up errors and changes in the AudioTrack.Tag property if the app is in the foreground and running. In fact due to the cost of polling for these changes it is only worth checking for errors when the “Now Playing” experience of your app is being shown.

In this final sample, it is assumed that you are implementing your “Now Playing” UI in a dedicated PhoneApplicationPage. That allows you to set up a timer to poll for agent errors and buffering state in OnNavigatedTo. This timer will be stopped in OnNavigatedFrom.

It is assumed that the UI contains a progress bar (progressBar), and a TextBlock for each of the following:

  • Track title (txtTitle)
  • Track position (txtPosition)

The errors are assumed to be retrieved as a single string from a helper class BackgroundErrorNotifier.GetError()

 public partial class NowPlayingPage : PhoneApplicationPage {
    DispatcherTimer playTimer;
    EventHandler playTimerTickEventHandler;
    EventHandler playStateChangedEventHandler;

    public NowPlayingPage()
    {
        InitializeComponent();
        // create timer and event handlers playTimer = new DispatcherTimer();
        playTimer.Interval = TimeSpan.FromMilliseconds(1000);
        playTimerTickEventHandler = new EventHandler(playTimer_Tick);
        playStateChangedEventHandler = new EventHandler(Instance_PlayStateChanged);
    }
    
    protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);
        // register event handlers and start timer BackgroundAudioPlayer.Instance.PlayStateChanged += playStateChangedEventHandler;
        playTimer.Tick += playTimerTickEventHandler;
        playTimer.Start();

        // force a UI refresh of the current state of the background audio Instance_PlayStateChanged(this, null);            
    }

    protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
    {
        // stop timer and unregister event handlers playTimer.Stop();
        playTimer.Tick -= playTimerTickEventHandler;
        BackgroundAudioPlayer.Instance.PlayStateChanged -= playStateChangedEventHandler;
        base.OnNavigatedFrom(e);
    }

    void playTimer_Tick(object sender, EventArgs e)
    {
        // check for errors string errorString = BackgroundErrorNotifier.GetError();
        if (errorString != null)
        {
            MessageBox.Show(errorString, "Audio error", MessageBoxButton.OK);
            progressBar.IsIndeterminate = false;
        }

        // update UI if audio is active var player = BackgroundAudioPlayer.Instance;
        AudioTrack currentTrack = player.Track;
        PlayState bapState = player.PlayerState;
        if ((bapState != PlayState.Unknown)
            && (currentTrack != null))
        {
            TimeSpan position = player.Position;
            txtPosition.Text = position.ToString(@"hh\:mm\:ss");
            if ((bapState == PlayState.Playing) && 
                (currentTrack.Tag == "Buffering"))
            {
                progressBar.IsIndeterminate = true;
            }
            else {
                progressBar.IsIndeterminate = false;
            }                
        }            
    }

    void Instance_PlayStateChanged(object sender, EventArgs e)
    {
        var player = BackgroundAudioPlayer.Instance;
        PlayState bapState = player.PlayerState;
        AudioTrack track = player.Track;
        if (track != null)
        {
            txtTitle.Text = track.Title;
        }
        else {
            txtTitle.Text = "No track information!";
        }
        switch (bapState)
        {
            case PlayState.Unknown:
                break;
            case PlayState.Playing:                    
            default:
                progressBar.IsIndeterminate = false;
                break;
        }
    }

    private void btnPlay_Click(object sender, RoutedEventArgs e)
    {
        var player = BackgroundAudioPlayer.Instance;
        if (player.PlayerState != PlayState.Playing)
        {
            player.Play();
        }
        else {
            player.Pause();
        }
        progressBar.IsIndeterminate = true;
    }

    private void btnNext_Click(object sender, RoutedEventArgs e)
    {
        BackgroundAudioPlayer.Instance.SkipNext();
        progressBar.IsIndeterminate = true;  
    }
}

Conclusion

Background audio is a great addition to the Windows Phone SDK in “Mango”. However as these blog posts have shown some of the implementation details require careful attention in order to get a great user-experience.

Hopefully this blog post series will be useful in helping you achieve that!

 

Full source code available for download here.

 

Written by Paul Annetts