Playing a PlayReady DRM protected video with Axinom DRM on Windows Universal Platform

Windows/Xbox Player

This tutorial introduces the most straightforward minimal code to playback an Axinom DRM protected video on Universal Windows Platform ( UWP) based apps, running on Windows 10/11 desktop/tablet on modern Xbox One.

Overview

What player to use

On .NET C# UWP, video playback is implemented with the help of MediaElement or MediaPlayerElement. In this tutorial, we focus on the former. The second one is a bit more advanced, but under the hood, it uses for video playback an instance of MediaElement.

What streaming format to use

Nowadays, the most viable DRM-protected streaming format for UWP apps is MPEG-DASH. Windows supports it natively. This tutorial also uses MPEG-DASH. Alternatively, it’s possible to also use DRM protected Microsoft Smooth Steaming format, but not natively, with some extra work. Unprotected HTTP Live Streaming ( HLS) playback is natively supported, but not DRM-protected HLS playback.

What DRM technology provider to use

On UWP, the only supported DRM technology is the Microsoft PlayReady.

Integrating with Axinom DRM

Use Visual Studio 2017 or newer, with UWP-development tools installed.

Create a new empty project, of type Visual C# → Windows Universal → Blank App (Universal Windows)

The created project is a single ready-to-run empty XAML page with design defined on MainPage.xaml, having a single root Grid element and code side on MainPage.xaml.cs.

Insert into the Grid the following two lines: the player itself and one label that we use to render error messages on top of the player.

MainPage.xaml:
<MediaElement Name="mediaElement" AreTransportControlsEnabled="True" />
<TextBlock Name="logTextBlock" Foreground="Red" FontSize="20"
            HorizontalAlignment="Center" VerticalAlignment="Center" />

Add to the MainPage code-behind, into the public sealed partial class MainPage {..} parentheses, the following code, also resolve the missing using directives with the help of the Visual Studio IntelliSense.

MainPage.xaml.cs
string laUrl = "https://drm-playready-licensing.axprod.net/AcquireLicense"; // (1)
string videoUrl = "https://(please specify).mpd";                           // (2)
string licenseToken = "eyJhbGciOiJIUzI1NiIs..(please specify)";             // (3)
string lastErrorMessage = string.Empty;

public MainPage()
{
    this.InitializeComponent();
    this.Loaded += (sender, e) =>
    {
        mediaElement.ProtectionManager = InitPlayReady(); // (4)
        mediaElement.MediaFailed += (sender2, e2) =>      // (5)
        {
            logTextBlock.Text = lastErrorMessage;
        };
        mediaElement.Source = new Uri(videoUrl);
        mediaElement.Play();
    };
}

private MediaProtectionManager InitPlayReady()
{
    // (6)
    var manager = new MediaProtectionManager();
    var props = new Windows.Foundation.Collections.PropertySet();
    props.Add("{F4637010-03C3-42CD-B932-B48ADF3A6A54}",
        "Windows.Media.Protection.PlayReady.PlayReadyWinRTTrustedInput");
    manager.Properties.Add(
        "Windows.Media.Protection.MediaProtectionSystemIdMapping", props);
    manager.Properties.Add(
        "Windows.Media.Protection.MediaProtectionSystemId",
        "{F4637010-03C3-42CD-B932-B48ADF3A6A54}");
    manager.Properties.Add(
        "Windows.Media.Protection.MediaProtectionContainerGuid",
        "{9A04F079-9840-4286-AB92-E65BE0885F95}");


    MediaProtectionServiceCompletion completionNotifier;


    manager.ServiceRequested += async (sender, e) =>
    {
        completionNotifier = e.Completion;
        var serviceRequest = (IPlayReadyServiceRequest)e.Request;
        // (7)
        if (serviceRequest is PlayReadyIndividualizationServiceRequest)
        {
            var req = serviceRequest as PlayReadyIndividualizationServiceRequest;
            await req.BeginServiceRequest();
            completionNotifier.Complete(true);
        }
        // (8)
        else if (serviceRequest is PlayReadyLicenseAcquisitionServiceRequest)
        {
            var wasSuccess = false;
            try {

                // (9)
                // serviceRequest.Uri = new Uri(laUrl + "?AxDRMMessage=" + licenseToken);
                // await serviceRequest.BeginServiceRequest();
                // wasSuccess = true;

                wasSuccess = await AcquireLicense(serviceRequest); // (10)
            }
            catch (Exception ex) { lastErrorMessage = ex.Message; }
            completionNotifier.Complete(wasSuccess);
        }
    };
    return manager;
}


private async Task<bool> AcquireLicense(IPlayReadyServiceRequest serviceRequest)
{
    var wasSuccess = false;

    // (11)
    PlayReadySoapMessage msg = serviceRequest.GenerateManualEnablingChallenge();
    // (12)
    HttpContent httpContent = new ByteArrayContent(msg.GetMessageBody());

    // (13)
    IPropertySet headers = msg.MessageHeaders;
    foreach (string header in headers.Keys)
    {
        string headerValue = headers[header].ToString();
        if (header.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)) {
            httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse(headerValue);
        }
        else {
            httpContent.Headers.Add(header.ToString(), headerValue);
        }
    }
    httpContent.Headers.Add("msprdrm_server_redirect_compat","false");
    httpContent.Headers.Add("msprdrm_server_exception_compat", "false");

    // (14)
    httpContent.Headers.Add("X-AxDRM-Message", licenseToken);

    HttpContent responseHttpContent = null;
    HttpResponseMessage response = null;
    try {
        var httpClient = new HttpClient();
        response = await httpClient.PostAsync(new Uri(laUrl), httpContent);
        response.EnsureSuccessStatusCode(); // (15)
        responseHttpContent = response.Content;
        lastErrorMessage = string.Empty;
    }
    catch (Exception ex) {
        // (16)
        lastErrorMessage =
            response.Headers.Any(p => p.Key.ToLower() == "x-axdrm-errormessage")
            ? response.Headers.First(p => p.Key.ToLower() == "x-axdrm-errormessage").Value.First()
            : ex.Message;
    }


    if (responseHttpContent != null) {
        // (17)
        Exception exResult =
            serviceRequest.ProcessManualEnablingResponse(
            await responseHttpContent.ReadAsByteArrayAsync());
        if (exResult != null) { throw exResult; }
        wasSuccess = true;
    }

    return wasSuccess;
}

Code explanation

  1. - The exact PlayReady license acquisition URL you find from your Axinom DRM configuration, under My Mosaic / DRM. It is recommended to use your specific URL mentioned there instead of a generic URL in this sample because so it is guaranteed that all the errors raised on the service side are properly logged and visible under My Mosaic / Errors page.

  2. - DASH-formatted, PlayReady DRM protected, video URL. If you need a sample video, you can find some test vectors at Video Playback tool page.

  3. - JWT license token, containing a License Service Message with a proper Entitlement Message authorizing the keyID of your video. Check Sign License Service Message for more details.

  4. - We initialize for the PlayerElement a configured Media Protection Manager (see below).

  5. - We hook a MediaFailed event to the player, that is raised on all kinds of playback issues, also in case when DRM protected playback failures.

  6. - We register some specific GUIDs for the Media Protection Manager, which act like feature turn-on switches for DASH & PlayReady scenario handling. We hook a ServiceRequested event to the manager, listening to all the various types of requests, where we have interest only for two types:

  7. - The client individualization request is called during the license acquisition chain only the very first time when a yet unknown client asks from the PlayReady service the very first license. It is a step where some client hardware and software specifics are shared with the license service to prepare for follow-up communications.

  8. - The license acquisition request is called each time the player has been initialized with a video source, has downloaded the manifest over the source, interpreted the manifest, and learned that the content is a PlayReady protected one. If no valid PlayReady license is found from the local repository for the content key, the protection manager starts the license acquisition request.

  9. - These three commented-out lines of code show the simplest way to send a license request. It uses a query parameter for the license token instead of a custom HTTP header (10). We don’t recommend this way because:

    • (A) It uses a query parameter for a possibly very long token. Depending on the entitlement message length (e.g., the number of content keys in the entitlement message), the token might easily be longer than the RFC specified 1024 chars.

    • (B) In this way, it is not possible to interpret the custom Axinom service error messages, as there is nothing we can hook on to read them in case of error.

  10. - This is the recommended way to handle the license acquisition request by injecting the token into the license request as a custom HTTP header, using our own HttpClient instance, carefully building the request, interpreting the response, and extracting the possible custom error.

  11. - We let the service generate the license request object that we were supposed to send out for this request.

  12. - We create a new request content object and set the results from the step 11 as its body.

  13. - We iterate over the headers of the entity from step 11 and inject all the headers to the new request object of step 12.

  14. - Now an important one → we add a new header with the key X-AxDRM-Message and with the value of our license token to the request we are building.

  15. - The license request was triggered, and a response was received. This line here assures that if the status-code of the response was not 200, it forwards the execution to the catch part of the try-catch block.

  16. - We try to read out of the response header the value for the key X-AxDRM-ErrorMessage. This header is set by the Axinom license service, and contains usually human-readable problem descriptions.

  17. - Testing if the received license is valid.

Note
If you wish to use DRM protection on Xbox One, please consider that the retail Xbox One minimum security level for PlayReady is 3000. In development mode, also lower security levels are supported. Using PlayReady on Xbox One also expects one additional "Capability" to be added into appxmanifest XML:
<Capabilities>
    ...
    <DeviceCapability Name="6a7e5907-885c-4bcb-b40a-073c067bd3d5" />
</Capabilities>

Please refer to Microsoft PlayReady sample project for further sample scenarios, including offline licenses for a download scenario.