Compartir a través de


Using Microsoft PlayReady with Per-stream Keys

Overview

Problem Statement

In some circumstances it is desirable to play protected content whose streams do not all use the same decryption key. One example is High Definition (HD) UltraViolet content, because the UltraViolet Common File Format (CFF) specification states that for HD content, the audio and video tracks SHOULD be encrypted with separate keys.

Provided Solutions

Approaches to handling per-key streams are provided for applications running on the following platforms. In the case of platforms supporting Media Foundation, two slightly different approaches that achieve the same purpose are documented.

  • Silverlight 5—On Demand Header approach
  • Media Foundation—On Demand Header approach
  • Media Foundation—Per Stream Header approach

Silverlight 5—On-demand Header Approach

On Silverlight 5 and Silverlight based platforms, such as Windows Phone 8, and the Xbox Application Development Kit, the media pipeline assumes either that each stream in a piece of content is encrypted to the same key or that keys rotate throughout playback. This presents an issue for content that has streams encrypted with different keys.

This limitation may be worked around through the use of a custom MediaStreamSource implementation that provides the needed protection system information to the media pipeline. This is accomplished by using the OnDemand decryption paradigm, commonly used for content that rotates keys during playback.

The ability of the source to request decryptors after the media topology is created is used to specify decryptors for each stream, with persistent licenses acquired prior to decryptor creation.

Constraints

Licenses MUST be pre-acquired by the application before they are needed. Reactive license acquisition triggered by the pipeline is not possible.

Following from #1, licenses MUST be persistent licenses. Non-persistent InMemory licenses are not supported in this approach.

The application MUST know the KeyIDs that will be needed in advance of needing them. This can be accomplished by:

  1. Storing the KeyID in the content in an accessible manner to your application. In the case of CFF, parsing the ‘tenc’ box for each track and retrieving the KeyIDs is a viable option. In this situation the application would make a unique request for each KeyID.
  2. Using some extra bit of information in the content to tell the License Server to issue multiple licenses in the response. For example, with UltraViolet CFF content, the asset physical identifier (APID) SHOULD be used in the ChallengeCustomData. The PlayReady license server business logic would be programmed such that it understands this APID to map to the multiple Keys needed by the content.

Licenses

Acquiring non-persistent licenses during reactive license acquisition is not an option. Instead, you MUST acquire licenses with Windows.Media.LicenseAcquirer.AcquireLicenseAsync. It is not necessary to acquire licenses before beginning topology creation, but it is necessary to have completed it before calling BeginDrmSetupDecryptor for the key needed.

Example 1: Acquiring licenses with Custom Data based Business logic

In this case, use the Windows.Media.LicenseAcquirer.AcquireLicenseAsync(GUID) variant. To domain bind your licenses, pass the domain ID for the domain into the AcquireLicenseAsync's GUID parameter. Otherwise (if you do not use domains) use Guid.Empty for this parameter as shown in the following snippet.

LicenseAcquirer la = new LicenseAcquirer();
la.LicenseServerUriOverride = new Uri(@"https://www.contoso.com/rightsmanager.asmx");
la.ChallengeCustomData = "MyContentIdentifier";
la.AcquireLicenseCompleted += la_AcquireLicenseCompleted;
la.AcquireLicenseAsync(Guid.Empty);
        

Example 2: Acquiring licenses by KeyID

In this example, each Key is acquired using a KeyID that the application retrieves from the content in some way.

Guid keyId1 = new Guid("18CA7C31-4D60-4FF6-955D-CB22085CDAD2");
Guid keyId2 = new Guid("94305D3E-6AF3-40CD-BFA4-C99D8170F9CE");
Uri  laUri  = new Uri( @"https://www.contoso.com/rightsmanager.asmx");

LicenseAcquirer laKey1 = new LicenseAcquirer();
LicenseAcquirer laKey2 = new LicenseAcquirer();

laKey1.LicenseServerUriOverride = laUri;
laKey1.AcquireLicenseCompleted += la_AcquireLicenseCompleted_1;
laKey1.AcquireLicenseAsync(keyId1, ContentKeyType.Aes128Bit, Guid.Empty);
laKey2.LicenseServerUriOverride = laUri;
laKey2.AcquireLicenseCompleted += la_AcquireLicenseCompleted_2;
laKey2.AcquireLicenseAsync(keyId2, ContentKeyType.Aes128Bit, Guid.Empty);
        

MediaStreamSource

This document assumes general familiarity with implementing a MediaStreamSource for Silverlight. See Implementing MediaStream Sources for more information on implementing a MediaStreamSource for Silverlight.

Opening the Source

When handling OpenMediaAsync to setup the topology, inform the topology that some (or all) of the media will be protected. To do so, set the DRMHeader attribute on the mediaStreamAttributes member of ReportOpenMediaCompleted. For the purposes of this exercise, the header should have the following attributes:

  1. Version Number = “4.1.0.0”.
  2. No KID nodes should be included.
  3. No Checksum should be included.
  4. The DECRYPTORSETUP node should be “ONDEMAND”.
  5. It is acceptable to include other nodes, such as the LA_URL node, but the PlayReady code will not do anything with them.

Here is an example header:

<WRMHEADER xmlns="https://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.1.0.0">
  <DATA>
    <DECRYPTORSETUP>ONDEMAND</DECRYPTORSETUP>
  </DATA>
</WRMHEADER>
        

This value may be included in a complete PlayReady Object (PRO) or it may be set as-is (XML). In either case, the value that is set is to be little-endian base-16 encoded. The base-16 encoded form of the above header is included below.

3C00570052004D00480045004100440045005200200078006D006C006E0073003D00220068007400740070003A002F002F0073006300680065006D00610073002E006D006900630072006F0073006F00660074002E0063006F006D002F00440052004D002F0032003000300037002F00300033002F0050006C00610079005200650061006400790048006500610064006500720022002000760065007200730069006F006E003D00220034002E0031002E0030002E00300022003E003C0044004100540041003E003C0044004500430052005900500054004F005200530045005400550050003E004F004E00440045004D0041004E0044003C002F0044004500430052005900500054004F005200530045005400550050003E003C002F0044004100540041003E003C002F00570052004D004800450041004400450052003E00

More detailed information about the PlayReady Object format and the PlayReady header can be obtained by going to the PlayReady Documents Web site.

After setting the DRM header and invoking ReportOpenMediaCompleted, the pipeline is ready to expect DRM content, but it does not yet know how to handle encountered encrypted content.

Note

It is highly recommended that license acquisition be completed before beginning the pipeline.

Create Decryptors

Before passing protected samples to the topology, you must create decryptor objects. The objects themselves are managed by the topology, but the MediaStreamSource must create them. To do this, invoke BeginDrmSetupDecryptor for each key that is needed. The DrmKeyRotationData parameter should most likely be left null. The exception to this is if you are using leaf licenses embedded with the content.

Note

For Xbox it is important that the binary representation of the Guid KeyID matches the binary representation in the DRM License issued. When KeyIDs are inserted into Guid objects, care must be taken around the endianness of the object. On Xbox, The System.Guid class has some interesting behavior. When setting the Guid object for the KeyId on BeginDrmSetupDecryptor, the endianness of the Guid should be flipped from the endianness used for LicenseAcquisition.AcquireLicenseAsync and ReportGetSampleCompleted. See the FlipGuid method in the Appendix for an example of how to do this.

Example 3: Creating a Decryptor

bool fDataReady = false;
int  decryptorSetupCount = 0;
private void PrepareStreamDRM()
{
  DrmSetupDecryptorCompleted += new EventHandler<DrmSetupDecryptorCompletedEventArgs>(DrmSetupDecryptorCompletedHandler);
    decryptorSetupCount = 0;
 
#if XBOX
  keyId1 = FlipGuid( ref KeyId1 );
  keyId2 = FlipGuid( ref KeyId2 );
#endif
  BeginDrmSetupDecryptor(null, keyId1);
  BeginDrmSetupDecryptor(null, keyId2);
}
 
private void DrmSetupDecryptorCompletedHandler(object sender, DrmSetupDecryptorCompletedEventArgs eventArgs)
{
  if (eventArgs.Error != null)
  {
    Debug.WriteLine("DrmSetupDecryptor FAILED " + eventArgs.KeyID.ToString() + " Error " + eventArgs.Error.ToString());
//handle errors correctly here. Proper error flow not shown
  }
  else
  {
    Debug.WriteLine("DrmSetupDecryptor Succeeded " + eventArgs.KeyID.ToString());
  }
  decryptorSetupCount++;
  if (decryptorSetupCount == 2 )
  {
    fDataReady = true;
  }
}

Passing Samples

After receiving successful DrmSetupDecryptorCompleted events for each stream in use, the pipeline is ready to receive samples. It is very important that encrypted samples are not sent until the decryptor is successfully created.

After creating the decryptor for the key, you need to pass the sample to the topology with the data needed to use the decryptor. To do this, set three properties on the MediaStreamSample attributes collection:

  • MediaSampleAttributeKeys.DRMInitializationVector
  • MediaSampleAttributeKeys.DRMAlgorithmID
  • MediaSampleAttributeKeys.DRMKeyIdentifier

The DRMInitializationVector data is a Base64-encoded string representation of the AES Counter Mode initialization vector needed for the sample. The DRMAlgorithmID value is the string representation of a member of the ContentKeyType.Aes128Ctr enumeration. The DRMKeyIdentifier is the string representation of the Guid KeyId for the sample. It should be formatted as "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx". See MediaSampleAttributeKeys Enumeration for more on these properties.

Clear Samples

Once the pipeline is setup for DRM content, all samples need to have these attributes set. If clear samples are needed, the attributes should be set as follows:

  1. MediaSampleAttributeKeys.DRMAlgorithmID MUST be set to ContentKeyType.Unprotected.ToString().
  2. MediaSampleAttributeKeys.DRMKeyIdentifier MAY be set to Guid.Empty.ToString() OR MAY be omitted.
  3. MediaSampleAttributeKeys.DRMInitializationVector MUST be left unset.

Example 4: Setting Sample Attributes

Guid guidKeyId = new Guid(sampleKeyIdBytes);
Debug.WriteLine("Passing sample with KeyID: " + guidKeyId.ToString());
if (guidKeyId == Guid.Empty)
{
  sampleAttributes[MediaSampleAttributeKeys.DRMAlgorithmID] = ContentKeyType.Unprotected.ToString();
}
else
{
  sampleAttributes[MediaSampleAttributeKeys.DRMAlgorithmID] = ContentKeyType.Aes128Ctr.ToString();
  sampleAttributes[MediaSampleAttributeKeys.DRMInitializationVector] = System.Convert.ToBase64String(sampleIdBytes);
  sampleAttributes[MediaSampleAttributeKeys.DRMKeyIdentifier] = guidKeyId.ToString();
}
        

Media Foundation—On-demand Header Approach

On platforms that directly expose Media Foundation sources and Trust Authorities (such as Windows 8) there is a close connection between the implemented source and the PlayReady Input Trust Authority (ITA). Because of the inherent connection between the two additional options beyond the recommend Silverlight 5 approach are available.

As in the Silverlight work-around, a source can provide an OnDemand PlayReady content header to the ITA, most commonly used for content that rotates keys during playback. In this case, you will make use of the ability of the source to request decryptors after the media topology is created to specify decryptors for each stream. Additionally, you will need to use persistent licenses acquired prior to decryptor creation.

Constraints

All the constraints from the previous section on Silverlight 5 apply. Additionally, the following constraint may apply, if the content is the only place that contains the KeyIDs for the licenses necessary (that is, the application has no other proprietary way of finding out the KeyID values):

  • A private channel would need to exist between the source and the application in which the source tells the application the KeyIds such that license acquisition can happen before playback.

Licenses

In this case, acquiring non-persistent licenses during reactive license acquisition is not an option. Instead, acquire licenses with Microsoft.Media.PlayReadyClient.PlayReadyLicenseAcquisitionServiceRequest. It is not necessary to acquire licenses before beginning topology creation, but it is necessary before the source queues an encrypted sample. Note that all samples assume that the Microsoft.Media.PlayReadyClient namespace is being used.

Example 1: Acquiring licenses with Custom Data based Business logic

If you are intending to domain bind your licenses, you should pass the GUID for the domain here, otherwise don’t set the domain ID in the following snippet.

PlayReadyLicenseAcquisitionServiceRequest laSR = new PlayReadyLicenseAcquisitionServiceRequest();
laSR.Uri = new Uri(@"https://www.contoso.com/rightsmanager.asmx");
laSR.ChallengeCustomData = "MyContentIdentifier";
laSR.BeginServiceRequest().Completed = new AsyncActionCompletedHandler(onBeginServiceRequestCompleted);
        

Example 2: Acquiring licenses by KeyID

In this example, each Key is acquired using a KeyID that the application retrieves from the content in some way.

Uri  laUri  = new Uri( @"https://www.contoso.com/rightsmanager.asmx");
PlayReadyContentHeader headerKeyId1 = new PlayReadyContentHeader(new Guid("18CA7C31-4D60-4FF6-955D-CB22085CDAD2"), PlayReadyEncryptionAlgorithm.aes128Ctr, laUri, laUri, null, Guid.Empty);
PlayReadyContentHeader headerKeyId2 = new PlayReadyContentHeader(new Guid("94305D3E-6AF3-40CD-BFA4-C99D8170F9CE "), PlayReadyEncryptionAlgorithm.aes128Ctr, laUri, laUri, null, Guid.Empty);

PlayReadyLicenseAcquisitionServiceRequest laKey1 = new PlayReadyLicenseAcquisitionServiceRequest();
PlayReadyLicenseAcquisitionServiceRequest laKey2 = new PlayReadyLicenseAcquisitionServiceRequest();

laKey1.ContentHeader = headerKeyId1;
laKey2.ContentHeader = headerKeyId2;
laKey1.BeginServiceRequest().Completed = new AsyncActionCompletedHandler(onBeginServiceRequestCompleted_1);
laKey2.BeginServiceRequest().Completed = new AsyncActionCompletedHandler(onBeginServiceRequestCompleted_2);
        

IMFMediaSource

This document assumes general familiarity with implementing an MF Media Source that works with PlayReady on Windows 8. This includes working with a single key content solutions. See Writing a Custom Media Source for more information on this topic.

Populating the PlayReady ITA with header data

The source is responsible for instantiating the PlayReady ITA and during that operation it is responsible for providing per stream initialization data. In this case it should provide the noted OnDemand header documented in the Silverlight 5 section above.

Passing Samples

The media source is responsible for tagging samples with attributes appropriate for the scenario it is supporting. In this case it should tag protected samples with the KID of the sample as well as the IV (MFSampleExtension_Content_KeyID and MFASFSampleExtension_Encryption_SampleID). Setting these properties will instruct the PlayReady ITA to automatically fetch the necessary decryptors for the content.

Clear Samples

Once the pipeline is setup for DRM content, all samples need to have these attributes set. If clear samples are needed, NONE of the DRM related attributes can be set.

Example 4: Setting Sample Attributes

if (guidKeyId != GUID_NULL)
{
  QWORD qwIV = 0x000001;
  pInputSample->SetBlob( MFASFSampleExtension_Encryption_SampleID, (BYTE*)&qwIV, sizeof(qwIV) );
  pInputSample->SetGUID( MFSampleExtension_Content_KeyID, &guidKeyId );
        

Media Foundation—Per-stream Header Approach

There is an alternative to using OnDemand headers. The PlayReady Windows 8 ITA fully supports being initialized with a unique header per stream. This allows the full range of features such as reactive license acquisition, in memory licenses, etc. This approach would require that the source have (or generate at runtime) necessary content headers with which to initialize the ITA.

Constraints

The constrains identified in the Silverlight 5 or Media Foundation On-demand Header approaches do not apply in this case.

Please note that even if it is a goal that the content be playable in both Silverlight 5 and Windows 8, the “per stream header” approach can still work. In this case, the content would contain the ONDEMAND header as described in previous sections, but the source, when creating the ITA, will dynamically generate two (or more, if necessary) PlayReady Objects (one for each stream that is encrypted to a different key, containing the KeyID for it) and serialize them.

Details

When the source creates the PlayReady ITA (which may happen inside IMFTrustedInput::CreateInputTrustAuthority()), it typically calls IPMPHost::CreateRemoteObject() passing in the CLSID of the ITA, as well as an IStream. In this case, the IStream would be initialized with the following serialized data structure:

// Package the protection system data blob in the format mentioned below
// Format: (All DWORD values are serialized in little-endian order)
// [GUID (F4637010-03C3-42CD-B932-B48ADF3A6A54})]
// [DWORD (stream count, use the actual stream count even if all streams are encrypted using the same data, note that zero is invalid)]
// [DWORD (next stream ID, use -1 if all remaining streams encrypted using the same data)]
// [DWORD (next stream's binary data size)]
// [BYTE* (next stream's binary data)]
// {Repeat from "next stream ID" above for each stream}
      

While in the “single key” case the source would put in -1 in the “next stream ID” element, in this case, the source would serialize the PRO for each stream.

Appendix

Common License Acquisition completed method

The method below demonstrates error handling and completion for either method of acquiring licenses.

void la_AcquireLicenseCompleted(object sender, AcquireLicenseCompletedEventArgs e)
{
  if (e.Error != null)
  {
    Debug.WriteLine("LicenseAcquisition FAILED! ");
    Debug.WriteLine("Error:\n" + e.Error.ToString());
  }
  else if (e.Cancelled)
  {
    Debug.WriteLine("LicenseAcquisition CANCELLED");
  }
  else
  {
    Debug.WriteLine("LicenseAcquisition SUCCEEDED");

    // CONTINUE Pipeline Creation and set up. In the case of
    // multiple license acquisition calls (Example 2), record
    // which call succeeded to be able to configure the correct
    // stream and to know when license acquisition is truly
    // completed.
  }
}
      

FlipGuid

This method demonstrates flipping the endian-ness of a Guid object. The flipping of a Guid needs to handle the constituent parts of the Guid and not simply the entire Byte array.

public static Guid FlipGuid(ref Guid guidIn)
{
  byte[] rawBytes = guidIn.ToByteArray();

  // Handle the DWORD at the beginning
  ByteSwap(ref rawBytes[0], ref rawBytes[3]);
  ByteSwap(ref rawBytes[1], ref rawBytes[2]);

  // Handle the first WORD after the DWORD
  ByteSwap(ref rawBytes[4], ref rawBytes[5]);

  // Handle the second WORD
  ByteSwap(ref rawBytes[6], ref rawBytes[7]);

  Guid guidOut = new Guid(rawBytes);

  return guidOut;
}