Windows Phone 8 Development Internals: Phone and Media Services

  • 6/15/2013

Media playback

The platform provides three main APIs for playing audio and video, each with varying levels of flexibility and control: the MediaPlayerLauncher, MediaElement, and MediaStreamSource. All three are described in the following sections.

The MediaPlayerLauncher

The MediaPlayerLauncher, like all Launchers and Choosers provided by the app platform, is very easy to use. It is a simple wrapper that provides access to the underlying media player app without exposing any of the complexity. To use this in your app, you follow the same pattern as for other Launchers. You can see this at work in the TestMediaPlayer solution in the sample code (see Figure 5-7). Observe that the media player defaults to landscape mode for videos (you can’t change this).

Figure 5-7

Figure 5-7 You can very quickly add media player support for audio and video playback.

The following listing shows how to invoke the MediaPlayerLauncher with a media file that is deployed as Content within the app’s XAP—the path will be relative to the app’s install folder:

MediaPlayerLauncher player = new MediaPlayerLauncher();
player.Media = new Uri(@"Assets/Media/ViaductTraffic.mp4", UriKind.Relative);
player.Controls = MediaPlaybackControls.Pause | MediaPlaybackControls.Stop;
player.Location = MediaLocationType.Install;
player.Show();

You can assign the Controls property from a flags enum of possible controls. The preceding listing specifies only the Pause and Stop controls, whereas the listing that follows specifies all available controls (including Pause, Stop, Fast Forward, Rewind). The listing also shows how to specify a remote URL for the media file, together with the MediaLocationType.Data.

MediaPlayerLauncher player = new MediaPlayerLauncher();
player.Media = new Uri(
  @"http://media.ch9.ms/ch9/1eb0/f9621a51-7c01-4394-ae51-b581ab811eb0/DevPlatformDrillDown.wmv",
  UriKind.Absolute);
player.Controls = MediaPlaybackControls.All;
player.Location = MediaLocationType.Data;
player.Show();

The MediaElement class

The TestMediaElement solution in the sample code uses the MediaElement control. Unlike the media player, the MediaElement class is a FrameworkElement type that you can use in your app—superficially, at least—in a similar way as the Image type. This means that you get to choose the orientation and size of the element, apply transforms, and so on. You can set up a MediaElement in code or in XAML. The MediaElement class exposes a set of media-specific properties, including the following:

  • AutoPlay. This property defines whether to start playing the content automatically. The default is true, but in many cases you probably want to set it to false because of app lifecycle/tombstoning issues.

  • IsMuted. This defines whether sound is on (the default).

  • Volume. This property is set in the range 0 to 1, where 1 (the default) is full volume.

  • Stretch. This is the same property used by an Image control to govern how the content fills the control (the default is Fill).

The code that follows shows how to set up a MediaElement in XAML. This example sets up suitable properties, such as the media source file, whether the audio is muted, the audio volume, and how to render the image within the control.

<MediaElement
    x:Name="myVideo" Source="Assets/Media/campus_20111017.wmv" AutoPlay="False"
    IsMuted="False" Volume="0.5" Stretch="UniformToFill"/>

All that remains is to invoke methods such as Play and Pause; in this example, these are triggered in app bar button Click handlers.

private void appBarPlay_Click(object sender, EventArgs e)
{
    myVideo.Play();
}
private void appBarPause_Click(object sender, EventArgs e)
{
    myVideo.Pause();
}

This is enough to get started, but it is a little fragile: there is a very small chance in the app as it stands that the media file is not fully opened at the point when the user taps the play button, and this would raise an exception. To make the app more robust in this scenario, it would be better to have the app bar buttons initially disabled and to handle the MediaOpened event on the MediaElement object to enable them. You can set up a MediaOpened handler in XAML and then implement this in code to enable the app bar buttons.

<MediaElement
    x:Name="myVideo" Source="Assets/Media/campus_20111017.wmv" AutoPlay="False"
    IsMuted="False" Volume="0.5" Stretch="UniformToFill"
    MediaOpened="myVideo_MediaOpened" />
private void myVideo_MediaOpened(object sender, System.Windows.RoutedEventArgs e)
{
    ((ApplicationBarIconButton)ApplicationBar.Buttons[0]).IsEnabled = true;
    ((ApplicationBarIconButton)ApplicationBar.Buttons[1]).IsEnabled = true;
}

The MediaStreamSource and ManagedMediaHelpers classes

The MediaPlayerLauncher provides the simplest approach for playing media in your app. Stepping it up a notch, if you need more flexibility, the MediaElement class offers a good set of functionality and is suitable for most phone apps. However, if you actually need lower-level access to the media file contents, you can use the MediaStreamSource class. This class offers more control over the delivery of content to the media pipeline and is particularly useful if you want to use media files in a format that are not natively supported by MediaElement, or for scenarios that are simply not yet supported in the platform, such as RTSP:T protocol support, SHOUTcast protocol support, seamless audio looping, ID3 v1/v2 metadata support, adaptive streaming, or multi-bitrate support.

Unfortunately, the MediaStreamSource class is not well documented. Fortunately, Microsoft has made available a set of helper classes, which you can obtain at https://github.com/loarabia/ManagedMediaHelpers. These were originally designed for use in Microsoft Silverlight Desktop and Windows Phone 7 apps, but they also work in Windows Phone 8 apps. The classes are provided in source-code format and include library projects and demonstration apps for Silverlight Desktop and Windows Phone. Keep in mind that the library source code is all in the Desktop projects; the phone projects merely reference the Desktop source files. The phone demonstration app is, of course, independent.

Here’s how you can use these. First, create a phone app solution, as normal. Then, add the ManagedMediaHelpers library projects (either take copies, so that you have all the sources available, or build the library assemblies, and then use CopyLocal=true to reference them in your solution). If you add the library phone projects to your solution, you then need to copy across all the source files from the Desktop projects. You need two library projects: the MediaParsers.Phone and Mp3MediaStreamSource.Phone projects. These projects provide wrapper classes for the MP3 file format. Using this approach, you must copy the four C# files from the MediaParsers.Desktop project, and the one C# file from the Mp3MediaStreamSource.SL4 project. The Mp3MediaStreamSource.Phone project has a reference to the MediaParsers.Phone project. Your app needs to have a reference to the Mp3MediaStreamSource.Phone project. Figure 5-8 shows this setup, which is the TestMediaHelpers solution in the sample code.

Figure 5-8

Figure 5-8 You can use the ManagedMediaHelpers for low-level control of media playback.

Having set up the projects, you can then declare an Mp3MediaStreamSource object. The sample app fetches a remote MP3 file by using an HttpWebRequest. When we get the data back, we use it to initialize the Mp3MediaStreamSource and set that as the source for a MediaElement object, which is declared in XAML.

private HttpWebRequest request;
private Mp3MediaStreamSource mss;
private string mediaFileLocation =
    @"http://media.ch9.ms/ch9/755d/4f893d13-fa05-4871-9123-3eadd2f0755d/EightPlatformAnnouncements.mp3";
public MainPage()
{
    InitializeComponent();
    Get = (ApplicationBarIconButton)ApplicationBar.Buttons[0];
    Play = (ApplicationBarIconButton)ApplicationBar.Buttons[1];
    Pause = (ApplicationBarIconButton)ApplicationBar.Buttons[2];
}
private void Get_Click(object sender, EventArgs e)
{
    request = WebRequest.CreateHttp(mediaFileLocation);
    request.AllowReadStreamBuffering = true;
    request.BeginGetResponse(new AsyncCallback(RequestCallback), null);
}
private void RequestCallback(IAsyncResult asyncResult)
{
    HttpWebResponse response =
        request.EndGetResponse(asyncResult) as HttpWebResponse;
    Stream s = response.GetResponseStream();
    mss = new Mp3MediaStreamSource(s, response.ContentLength);
    Dispatcher.BeginInvoke(() =>
    {
        mp3Element.SetSource(mss);
        Play.IsEnabled = true;
        Get.IsEnabled = false;
    });
}
private void Play_Click(object sender, EventArgs e)
{
    mp3Element.Play();
    Play.IsEnabled = false;
    Pause.IsEnabled = true;
}
private void Pause_Click(object sender, EventArgs e)
{
    mp3Element.Pause();
    Pause.IsEnabled = false;
    Play.IsEnabled = true;
}

Observe that this code sets the AllowReadStreamBuffering property to true. If you enable buffering like this, it becomes easier to work with the stream source because all the data is downloaded first. On the other hand, you can’t start processing the data until the entire file is downloaded—plus, it uses more memory. The alternative is to use the asynchronous methods and read the stream in the background. This simple example shows you how you can easily use the MediaStreamSource type via the ManagedMediaHelpers, although it doesn’t really show the power of these APIs—by definition, these are advanced scenarios.

The MediaElement controls

When you point a MediaElement to a remote media source and start playing, the content is downloaded to the device, and playback starts as soon as there is enough data in the buffer to play. Download and buffering continues in the background while the previously buffered content is playing. If you’re interested in the progress of these operations, you can handle the BufferingChanged and DownloadChanged events exposed by the MediaElement class. The standard media player app on the device, invoked via MediaPlayerLauncher, offers a good set of UI controls for starting, stopping, and pausing, as well as a timeline progress bar that tracks the current position in the playback, and a countdown from the total duration of the content. By contrast, the MediaElement class does not provide such UI controls; however, you can emulate these features by using the properties exposed from MediaElement, notably the Position and NaturalDuration values.

Figure 5-9 shows the TestVideo solution in the sample code. This uses a MediaElement combined with a Slider and TextBlock controls to mirror some of the UI features of the standard media player.

Figure 5-9

Figure 5-9 You can report media playback progress with custom UI.

The app XAML declares a MediaElement, a Slider, and a couple of TextBlock controls (to represent the playback timer count-up and count-down values).

<StackPanel x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <MediaElement
        x:Name="Player" Height="297" Width="443" AutoPlay="False" Stretch="UniformToFill"
        Source="http://media.ch9.ms/ch9/b428/b746df27-e928-4306-9464-4b77c289b428/SharedWindowsCore.wmv"/>
    <Slider
        x:Name="MediaProgress" Height="90" Margin="-5,0"
        Maximum="1" LargeChange="0.1" ValueChanged="MediaProgress_ValueChanged"/>
</StackPanel>
<TextBlock
    Grid.Row="1" x:Name="ElapsedTime" Text="00:00" IsHitTestVisible="False"
    Width="60" Height="30" Margin="19,180,0,0" HorizontalAlignment="Left" />
<TextBlock
    Grid.Row="1" x:Name="RemainingTime" Text="00:00" IsHitTestVisible="False"
    Width="60" Height="30" Margin="0,180,6,0" HorizontalAlignment="Right"/>

The MediaElement points to a video file on Channel9 (as it happens, this example is a presentation by Joe Belfiore, Microsoft vice president for Windows Phone). For the Slider, the important piece is to handle the ValueChanged event. Note that the two TextBlock controls are not part of the same StackPanel—this gives us the opportunity to specifiy Margin values and effectively overlay them on top of the Slider. Because of this, we need to be careful to make the TextBlock controls non–hit-testable so that they don’t pick up touch gestures intended for the Slider.

The Slider performs a dual role: the first aspect is a passive role, in which we update it programmatically to synchronize it with the current playback position; the second aspect is an active role, in which the user can click or drag the Slider position—we respond to this in the app by setting the MediaElement.Position value. In the MainPage code-behind, we declare a TimeSpan field for the total duration of the video file, and a bool to track whether we’re updating the Slider based on the current playback position.

private bool isUpdatingSliderFromMedia;
private TimeSpan totalTime;
private DispatcherTimer timer;
public MainPage()
{
    InitializeComponent();
    timer = new DispatcherTimer();
    timer.Interval = TimeSpan.FromSeconds(0.5);
    timer.Tick += new EventHandler(timer_Tick);
    timer.Start();
    appBarPlay = ApplicationBar.Buttons[0] as ApplicationBarIconButton;
    appBarPause = ApplicationBar.Buttons[1] as ApplicationBarIconButton;
}
private void Player_MediaOpened(object sender, RoutedEventArgs e)
{
    timer.Start();
}

Here’s how to implement the first role. We implement a DispatcherTimer with a half-second interval, updating the Slider on each tick. We wait until the MediaElement reports that the media source is successfully opened and then start the timer. When the timer event is raised, the first thing to do is to cache the total duration of the video file—this is a one-off operation. Next, we calculate the time remaining and render this in the corresponding TextBlock. Assuming that the playback has actually started (even if it is now paused), we then calculate how much of the video playback is complete and use the resulting value to update the position of the Slider. We also need to update the current “elapsed time” value to match the playback position. Throughout this operation, we toggle the isUpdatingSliderFromMedia flag. This will be used in another method.

private void timer_Tick (object sender, EventArgs e)
{
    if (totalTime == TimeSpan.Zero)
    {
        totalTime = Player.NaturalDuration.TimeSpan;
    }
    TimeSpan remainingTime = totalTime - Player.Position;
    String remainingTimeText = String.Format("{0:00}:{1:00}",
        (remainingTime.Hours * 60) + remainingTime.Minutes, remainingTime.Seconds);
    RemainingTime.Text = remainingTimeText;
    isUpdatingSliderFromMedia = true;
    if (Player.Position.TotalSeconds > 0)
    {
        double fractionComplete = Player.Position.TotalSeconds / totalTime.TotalSeconds;
        MediaProgress.Value = fractionComplete;
        TimeSpan elapsedTime = Player.Position;
        String elapsedTimeText = String.Format("{0:00}:{1:00}",
            (elapsedTime.Hours * 60) + elapsedTime.Minutes, elapsedTime.Seconds);
        ElapsedTime.Text = elapsedTimeText;
        isUpdatingSliderFromMedia = false;
    }
}

In the handler for the ValueChanged event on the Slider, we check first that we’re not in this handler as a result of what we did in the previous method. That is, we need to verify that we’re not here because we’re updating the Slider from the media position. The other scenario for which we’d be in this handler is if the user is clicking or dragging the Slider position. In this case, assuming that the media content can actually be repositioned (CanSeek is true), we reset its position based on the Slider position. This is the inverse of the normal behavior, for which we set the Slider position based on the media position.

private void MediaProgress_ValueChanged(
    object sender, RoutedPropertyChangedEventArgs<double> e)
{
    if (!isUpdatingSliderFromMedia && Player.CanSeek)
    {
        TimeSpan duration = Player.NaturalDuration.TimeSpan;
        int newPosition = (int)(duration.TotalSeconds * MediaProgress.Value);
        Player.Position =  TimeSpan.FromSeconds(newPosition);
    }
}

The app bar buttons invoke the MediaElement Play and Pause methods, each of which is very simple. In the case of Pause, we need to first establish that this media content can actually be paused. If you don’t check CanSeek or CanPause, and just go ahead and attempt to set Position or call Pause, in neither case is an exception thrown. Rather, the method simply does nothing. So, these checks are arguably redundant, except that you should use them to avoid executing unnecessary code.

private void appBarPause_Click(object sender, EventArgs e)
{
    if (Player.CanPause)
    {
        Player.Pause();
    }
}