Home
Atom Feed

Using PlayReady and Smooth Streaming/DASH in a Windows 10 UWP app

With Windows 10, Microsoft has done some changes to how the PlayReady support is implemented. Before, you needed to download a separate extension to Visual Studio for PlayReady, but now it has been baked into the Player Framework extension. This gives us a simpler integration, but it is a very badly documented one. The following guide is the result of plenty of hours of trying and failing, and I hope to save you from the same fate. :-)

Please note that this sample only focuses on the PlayReady part, so there is no MVVM or any other useful features in this project.

First, go off and install the Player Framework extension, and then the Smooth Streaming extension for UWP apps.

Then start up Visual Studio and create a new Universal Windows Blank App (Not a Universal Windows 8.1 App). When it has been created, right-click the References node and select Add Reference. Select the Extensions node under the Universal Windows tree on the left side of the window that pops up. Then select Microsoft Player Framework 3.0.0.0Microsoft Player Framework Adaptive Streaming Plugin 3.0.0.0, Microsoft Universal Smooth Streaming Client SDK 1.0, Visual C++ 2015 Runtime for Universal Windows Platform Apps 14.0.0.0 and click OK.

Open the MainPage.xaml file and add a button:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
	<Grid.RowDefinitions>
		<RowDefinition Height="*"/>
		<RowDefinition Height="Auto"/>
		<RowDefinition Height="Auto"/>
		<RowDefinition Height="*"/>
	</Grid.RowDefinitions>

	<Button Grid.Row="1" Click="OnSmoothStreamButtonClicked" Content="Play Smooth Stream"/>
	<Button Grid.Row="2" Click="OnDashButtonClicked" Content="Play DASH"/>
</Grid>

We need to navigate to a view with a player on it to show a video, so open the codebehind file by pressing F7 and add the following:

private void OnSmoothStreamButtonClicked(object sender, RoutedEventArgs e)
{
        // You can get more streams (both protected and unprotected) here: http://playready.directtaps.net/smoothstreaming/
        this.Frame.Navigate(typeof(PlayerPage), new PlayerArguments { StreamUrl = "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest", RightsManagerUrl = "http://playready.directtaps.net/pr/svc/rightsmanager.asmx" });
}

private void OnDashButtonClicked(object sender, RoutedEventArgs e)
{
        // We don't need to specify rights manager url with this stream
        this.Frame.Navigate(typeof(PlayerPage), new PlayerArguments { StreamUrl = "http://bitdash-a.akamaihd.net/content/sintel-pr-wv/sintel.mpd" });
}

Then right-click the project in the Solution Explorer and click Add -> Add New Item. Select a Class template and call it PlayerArguments.  This class will let us send parameters to the player view that it can use to play the stream. Change the contents of the class so it looks like this:

public class PlayerArguments
{
    public string StreamUrl { get; set; }
    public string RightsManagerUrl { get; set; }
}

Now it is time to create the view where the actual playback will occur. Create a new item based on a Blank Page template and call it PlayerPage. Then put the following into the xaml file to create a player:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <playerFramework:MediaPlayer x:Name="Player"/>
</Grid>

Make sure you add a reference to Microsoft.PlayerFramework on the Page container:

xmlns:playerFramework="using:Microsoft.PlayerFramework"

PlayReady requires the use of plugins and configuration containers, so the next is a mouthful. I have added comments in the code to help with understanding what happens, but chime in the comments section below if you need more answers.

Open the codebehind and change the class to look like this:

public sealed partial class PlayerPage : Page
{
	private PlayerArguments arguments;

	public PlayerPage()
	{
		this.InitializeComponent();

		Player.MediaOpened += OnMediaOpened;
	}

	protected override void OnNavigatedTo(NavigationEventArgs e)
	{
		base.OnNavigatedTo(e);

		arguments = e.Parameter as PlayerArguments;

		if (arguments != null)
		{
			InitializePlugins();
			InitializeMediaExtensionManager();
			InitializeMediaProtectionManager();

			Player.Source = new Uri(arguments.StreamUrl);
		}
	}

	/// <summary>Initializes the Smooth Streaming plugin.</summary>
	private void InitializePlugins()
	{
		var adaptivePlugin = new AdaptivePlugin();

		Player.Plugins.Add(adaptivePlugin);
	}

	/// <summary>Initializes the media extension manager so we can handle PlayReady protected content.</summary>
	private void InitializeMediaExtensionManager()
	{
		var plugins = new MediaExtensionManager();

                // Add support for IIS Smooth Streaming Manifests
		plugins.RegisterByteStreamHandler("Microsoft.Media.AdaptiveStreaming.SmoothByteStreamHandler", ".ism", "text/xml");
		plugins.RegisterByteStreamHandler("Microsoft.Media.AdaptiveStreaming.SmoothByteStreamHandler", ".ism", "application/vnd.ms-sstr+xml");

                // Add support for PlayReady video and audio files
		plugins.RegisterByteStreamHandler("Microsoft.Media.Protection.PlayReady.PlayReadyByteStreamHandler", ".pyv", "");
		plugins.RegisterByteStreamHandler("Microsoft.Media.Protection.PlayReady.PlayReadyByteStreamHandler", ".pya", "");
	}

	/// <summary>Initializes the PlayReady protection manager.</summary>
	private void InitializeMediaProtectionManager()
	{
		var mediaProtectionManager = new MediaProtectionManager();
		mediaProtectionManager.ComponentLoadFailed += OnMediaProtectionManagerComponentLoadFailed;
		mediaProtectionManager.ServiceRequested += OnMediaProtectionManagerServiceRequested;

		// Set up the container GUID for the CFF format, see http://uvdemystified.com/uvfaq.html#3.       
                // The GUID represents MPEG DASH Content Protection using Microsoft PlayReady, see http://dashif.org/identifiers/protection/
                mediaProtectionManager.Properties["Windows.Media.Protection.MediaProtectionContainerGuid"] = "{9A04F079-9840-4286-AB92-E65BE0885F95}";

		// Set up the drm layer to use. Hardware DRM is the default, but not all older hardware supports this
		var supportsHardwareDrm = PlayReadyStatics.CheckSupportedHardware(PlayReadyHardwareDRMFeatures.HardwareDRM);
		if (!supportsHardwareDrm)
		{
			mediaProtectionManager.Properties["Windows.Media.Protection.UseSoftwareProtectionLayer"] = true;
		}

                // Set up the content protection manager so it uses the PlayReady Input Trust Authority (ITA) for the relevant media sources
                // The MediaProtectionSystemId GUID is format and case sensitive, see https://msdn.microsoft.com/en-us/library/windows.media.protection.mediaprotectionmanager.properties.aspx
                var cpsystems = new PropertySet();
                cpsystems[PlayReadyStatics.MediaProtectionSystemId.ToString("B").ToUpper()] = "Windows.Media.Protection.PlayReady.PlayReadyWinRTTrustedInput";
                mediaProtectionManager.Properties["Windows.Media.Protection.MediaProtectionSystemIdMapping"] = cpsystems;
                mediaProtectionManager.Properties["Windows.Media.Protection.MediaProtectionSystemId"] = PlayReadyStatics.MediaProtectionSystemId.ToString("B").ToUpper();

		Player.ProtectionManager = mediaProtectionManager;
	}

	private void OnMediaOpened(object sender, RoutedEventArgs e)
	{
		// Start playing the file when ready
		Player.Play();
	}

	private void OnMediaProtectionManagerComponentLoadFailed(MediaProtectionManager sender, ComponentLoadFailedEventArgs e)
	{
		Debug.WriteLine("ProtectionManager ComponentLoadFailed");
		e.Completion.Complete(false);
	}

	private async void OnMediaProtectionManagerServiceRequested(MediaProtectionManager sender, ServiceRequestedEventArgs e)
	{
		Debug.WriteLine("ProtectionManager ServiceRequested");

		var completionNotifier = e.Completion;
		var request = (IPlayReadyServiceRequest)e.Request;

		var result = false;

		if (request.Type == PlayReadyStatics.IndividualizationServiceRequestType)
		{
			result = await PlayReadyLicenseHandler.RequestIndividualizationToken(request as PlayReadyIndividualizationServiceRequest);
		}
		else if (request.Type == PlayReadyStatics.LicenseAcquirerServiceRequestType)
		{
			// NOTE: You might need to set the request.ChallengeCustomData, depending on your Rights Manager.
			request.Uri = new Uri(arguments.RightsManagerUrl);

			result = await PlayReadyLicenseHandler.RequestLicense(request as PlayReadyLicenseAcquisitionServiceRequest);
		}

		completionNotifier.Complete(result);
	}
}

The license handler is not created yet so we need to add a new class called PlayReadyLicenseHandler that looks like this:

public class PlayReadyLicenseHandler
{
	/// <summary>Request a token that identifies the player session.</summary>
	/// <param name="request">The request.</param>
	/// <returns><c>True</c> if successfull, <c>false</c> otherwise.</returns>
	public static async Task<bool> RequestIndividualizationToken(PlayReadyIndividualizationServiceRequest request)
	{
		Debug.WriteLine("ProtectionManager PlayReady Individualization Service Request in progress");

		try
		{
			Debug.WriteLine("Requesting individualization token from {0}", request.Uri);

			await request.BeginServiceRequest();
		}
		catch (Exception ex)
		{
			Debug.WriteLine("ProtectionManager PlayReady Individualization Service Request failed: " + ex.Message);

			return false;
		}

		Debug.WriteLine("ProtectionManager PlayReady Individualization Service Request successfull");

		return true;
	}

	/// <summary>Request a license for playing a stream.</summary>
	/// <param name="request">The request.</param>
	/// <returns><c>True</c> if successfull, <c>false</c> otherwise.</returns>
	public static async Task<bool> RequestLicense(PlayReadyLicenseAcquisitionServiceRequest request)
	{
		Debug.WriteLine("ProtectionManager PlayReady License Request in progress");

		try
		{
			Debug.WriteLine("Requesting license from {0} with custom data {1}", request.Uri, request.ChallengeCustomData);

			await request.BeginServiceRequest();
		}
		catch (Exception ex)
		{
			Debug.WriteLine("ProtectionManager PlayReady License Request failed: " + ex.Message);

			return false;
		}

		Debug.WriteLine("ProtectionManager PlayReady License Request successfull");

		return true;
	}
}

Change the Debug target to Local Machine and press F5 to start the app. When you then click one of the Play buttons, the stream should start. You can verify that the DRM is working by trying to take a screenshot of the app, the video area should be black if it works ;-)

Bonus tip

Not all Rights Managers are created equal. This sample uses the built-in response processor for PlayReady, but that processor does not play nice with custom responses your DRM provider might have. Any custom error messages will come show up in the application as "The text associated with this error code could not be found" errors. 

But the framework also has a solution to this - you can process the requests and responses manually. This is a bit more work, but then you can inspect the HTTP responses yourself. I've implemented this for the license acquisition call, by adding the following method to the PlayReadyLicenseHandler class:

public static async Task<bool> RequestLicenseManual(PlayReadyLicenseAcquisitionServiceRequest request)
{
	Debug.WriteLine("ProtectionManager PlayReady Manual License Request in progress");

	try
	{
		var r = request.GenerateManualEnablingChallenge();

		var content = new ByteArrayContent(r.GetMessageBody());

		foreach (var header in r.MessageHeaders.Where(x => x.Value != null))
		{
			if (header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase))
			{
				content.Headers.ContentType = MediaTypeHeaderValue.Parse(header.Value.ToString());
			}
			else
			{
				content.Headers.Add(header.Key, header.Value.ToString());
			}
		}

		var msg = new HttpRequestMessage(HttpMethod.Post, r.Uri) { Content = content };

		Debug.WriteLine("Requesting license from {0} with custom data {1}", msg.RequestUri, await msg.Content.ReadAsStringAsync());

		var client = new HttpClient();
		var response = await client.SendAsync(msg);

		if (response.IsSuccessStatusCode)
		{
			request.ProcessManualEnablingResponse(await response.Content.ReadAsByteArrayAsync());
		}
		else
		{
			Debug.WriteLine("ProtectionManager PlayReady License Request failed: " + await response.Content.ReadAsStringAsync());

			return false;
		}
	}
	catch (Exception ex)
	{
		Debug.WriteLine("ProtectionManager PlayReady License Request failed: " + ex.Message);

		return false;
	}

	Debug.WriteLine("ProtectionManager PlayReady License Request successfull");

	return true;
}

This is much easier than it was back in 2012 when we first created the app for TV 2 Sumo (using PlayerFramework 1.0) for Windows 8 :-)

If you want to download the full sample project, you can find it on GitHub.