Share via


Sample: PlayReady-ND Sample App

A sample PlayReady-ND channel guide app (PRNDSampleApp) for Windows, Android, and iOS is available to all PlayReady licensees. The sample app illustrates how to implement standard PlayReady-ND operations (authorization, registration, proximity detection, license fetching, and content streaming) by using the PlayReady-ND client APIs.

Sample App Overview

The sample app is a simplified channel guide for cable, satellite, or other online television broadcasts. When the user clicks a channel in the guide, a client receiver initiates the sequence of PlayReady-ND operations (individualization, registration, proximity detection, and license fetching) needed to stream the content, then downloads the video and audio streams from the transmitter to the appropriate stream parser plug-ins for decryption.

Sample App Structure

The PRNDSampleApp project includes two parts, each with its own Visual Studio solution:

  • Microsoft.Media.PlayReadyClient.NetworkDevice.Plugins: Contains the the HTTP download engine used to stream protected media content from the transmitter, and the demultiplexer for the M2TS input stream and client plug-ins to parse media and closed captioning streams within the encrypted M2TS stream from the transmitter.
  • PRNDSampleApp: Contains the user interface for the app (including layout for closed captions and other elements), defines handlers for the events that indicate the completion of different PlayReady-ND operations, and includes an EIA-608 closed caption parser.

Execution Sequence

PRNDSampleApp goes through the following steps to download media input, validate the transmitter and receiver, decrypt protected content, and stream that content:

  1. When the user clicks on a channel in the UI, creates a media stream source.
  2. Verifies that the PlayReady client is individualized and calls the individualization service to individualize the client if it is not.
  3. Creates three client plug-ins:
    • A TCP messenger
    • An HTTP download engine to stream data from the transmitter
    • A stream parser to parse the downloaded stream and send the streams that it contains to the appropriate client parser plug-ins.
  4. Registers the app to receive events when the processes of registration, proximity detection, and license fetching have completed, and closed captioning data has been received.
  5. Starts the registration, proximity detection, license fetching, and closed caption acquisition processes.
  6. If the last step is successful, uses the HTTP download engine to stream media from the URI for the channel that the user selected in step 1.
  7. The stream parser parsers the downloaded streams and sends the samples to the PlayReady client for decryption and rendering.

The rest of this section shows code from the sample app that illustrates each of these steps.

Initialization

The sample app begins by calling the OnSelectionChanged event handler in MainPage.xaml.cs after the user selects a channel from the guide. OnSelectionChanged, in turn, calls the LoadChannel method to load the selected channel.

private void OnSelectionChanged( object sender, SelectionChangedEventArgs e )
{
  if (ContentBox.SelectedIndex > -1)
  {
    lock (_lock)
    {
      String chURL = "";

      // Channels populated dynamically; get URL from StorageFile
      StorageFile selectedContent = _channelList[ContentBox.SelectedIndex];
      chURL = GetChannelURL(selectedContent);
      .
      .
      .
      LoadChannel(chURL);
      .
      .
      .
    }
  }
}
  

The LoadChannel method calls the CreateMediaStreamSource method and passes it the URI corresponding to the selected channel. CreateMediaStreamSource creates a media source object that the app uses to downloads media content from the selected channel.

private void LoadChannel( String chURL )
{
  MediaEme.MediaElt.Stop();
  GC.Collect();
  CreateMediaStreamSource( chURL );
}
    

Creating and Initializing Client Plug-Ins and Checking Individualization

The CreateMediaStreamSource call in the previous section calls the initPrndClient method to start the process of creating and initializing plug-ins. The initPrndClient method first calls the Indivmethod to ensure that the client has been individualized (that is, that it can bind to licenses). If not, Indiv calls the RequestIndiv method defined in ExtendedMediaElement.cs to individualize the client.

private void Indiv()
{
  bool needIndiv = false;
  try
  {
    // Try to create LA service request see if we need to indiv
    new PlayReadyLicenseAcquisitionServiceRequest();
  }
  catch (Exception exp)
  {
    if ((uint)exp.HResult == MSPR_E_NEEDS_INDIVIDUALIZATION)
    {
      // Do indiv
      needIndiv = true;
      RequestIndiv();
    }
    .
    .
    .
  }
}
    

Registering for PlayReady-ND Events

Once the initPrndClient call has verified the client is individualized, it creates an NDClient object to hold the HTTP download engine, the stream parser/multiplexer, and messenger plug-ins, and registers handlers for events that signal when each PlayReady-ND operation has completed:

if (PrndClient == null)
{
  NDTCPMessenger msgr = new NDTCPMessenger(remoteHostName, Convert.ToUInt32(remoteServiceName));
  HTTPDownloadEngine downloadEngine = new HTTPDownloadEngine();
  _demuxer = new M2TSDemuxer();
   
  PrndClient = new NDClient(downloadEngine, _demuxer, msgr);
  PrndClient.RegistrationCompleted += OnRegistrationCompleted;
  PrndClient.ProximityDetectionCompleted += OnProximityDetectionCompleted;
  PrndClient.LicenseFetchCompleted += OnFetchLicenseCompleted;
  PrndClient.ReRegistrationNeeded += OnRehandshake;
  PrndClient.ClosedCaptionDataReceived += OnClosedCaptionDataReceived;
}
    

The CreateMediaStreamSource method that called initPrndClient parses the URI that it received as a parameter and uses each substring in the URI to identify the plug-ins it creates and the protocol to use for each plug-in:

Trace.WriteLine("Begin playback for " + lastUri);
string remoteHostName = null;
string remoteServiceName = null;
string contentId = null;

string[] substrings = lastUri.Split('?');
Uri contentUri = new Uri(substrings[0], UriKind.Absolute);

string[] parameters = substrings[1].Split('&');
for (int idx = 0; idx < parameters.Length; idx++)
{
  string[] nvp = parameters[idx].Split('=');
  if (nvp[0] == "PRND_TCP_HOST")
  {
    remoteHostName = nvp[1];
  }
  else if (nvp[0] == "PRND_TCP_PORT")
  {
    remoteServiceName = nvp[1];
  }
  else if (nvp[0] == "PRND_CID")
  {
    contentId = nvp[1];
  }
}
if (remoteHostName == null || remoteServiceName == null || contentId == null)
{
  return;
}
if (MediaEme.PrndClient == null)
{
  MediaEme.initPrndClient(remoteHostName, remoteServiceName);
}
  

Registration, Proximity Detection, and License Fetching

The CreateMediaStreamSource call also triggers the registration, proximity detection, and license fetching processes by starting an asynchronous session (after making sure any previous asynchronous session has completed), as shown in the following license fetch example:

System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
byte[] contentID = encoding.GetBytes(contentId);

NDLicenseFetchDescriptor licDesc = new NDLicenseFetchDescriptor(NDContentIDType.Custom, contentID, null);

// Cancel previous session if it is still in progress.
if (_startResult != null)
{
  _startResult.Cancel();
  _startResult = null;

  // Wait for the previous session to be completed/canceled
  _startAsyncCompletedEvent.WaitOne();
}

_startResult = MediaEme.PrndClient.StartAsync(contentUri, true, null, licDesc);
_startResult.Completed = new AsyncOperationCompletedHandler<INDStartResult>(OnMSSCreationCompleted);
     

In the case of each operation, the app receives an event that indicates whether or not the operation completed successfully and calls the On* handler for that event defined in ExtendedMediaElement.cs to process it. StartAsync will perform registration, proximity detection, and license fetch and trigger callbacks registered for each stage (registration completed, proximity detection completed, and license fetch completed). The following example from ExtendedMediaElement.cs shows the method that the app uses to handle a RegistrationCompleted event:

private void OnRegistrationCompleted( object sender, INDRegistrationCompletedEventArgs e )
{
  RegistrationCompleted = true;
  // Accept the cert
  e.TransmitterCertificateAccepted = true;
  INDCustomData customdata = e.ResponseCustomData;

  LogMessage( "PlayReady ND registration completed" );
  LogMessage( String.Format( "CustomDataTypeID: {0}\nCustomData: {1}\nCert Type: {2}\nSecurity Level: {3}\nModel Manufacturer Name: {4}\n",
  customdata != null ? customdata.CustomDataTypeID.ToString() : "",
  customdata != null ? customdata.CustomData.ToString() : "",
  e.TransmitterProperties != null ? e.TransmitterProperties.CertificateType : 0,
  e.TransmitterProperties != null ? e.TransmitterProperties.SecurityLevel : 0,
  e.TransmitterProperties != null ? e.TransmitterProperties.ModelManufacturerName : ""));
}
    

The following example shows the method that the app uses to handle a ProximityDetectionCompleted event:

private void OnProximityDetectionCompleted( object sender, INDProximityDetectionCompletedEventArgs e )
{
  ProximityDetectionCompleted = true;
  LogMessage( "Proximity Detection compleleted" );
  LogMessage( String.Format( "Took: {0} retries",
  e.ProximityDetectionRetryCount ) );
}
    

The following example shows the method that the app uses to handle a LicenseFetchCompleted event:

private void OnFetchLicenseCompleted( object sender, INDLicenseFetchCompletedEventArgs e )
{
  INDCustomData customdata = e.ResponseCustomData;

  FetchLicenseCompleted = true;
  LogMessage( "License Fetch compleleted" );
  LogMessage( String.Format( "CustomDataTypeID: {0}\nCustomData: {1}",
  customdata != null ? customdata.CustomDataTypeID.ToString() : "",
  customdata != null ? customdata.CustomData.ToString() : ""));
}
    

Content Streaming and Closed Caption Acquisition

After the prerequisite procedures finish successfully, the app can use the HTTP download engine that it created in step 3 under "Execution Sequence" to download the media content from the URI for the channel that the user selected from the guide. (The URI is sent to the NDClient as one of the parameters to StartAsync. NDClient in turn invokes the download engine's Open method, passing in this URI.)

The first step in this process occurs when the initPrndClient call in the app instantiates the client plug-in for the HTTP download engine:

if (PrndClient == null)
{
  NDTCPMessenger msgr = new NDTCPMessenger(remoteHostName, Convert.ToUInt32(remoteServiceName));
  HTTPDownloadEngine downloadEngine = new HTTPDownloadEngine();
  _demuxer = new M2TSDemuxer();

  PrndClient = new NDClient(downloadEngine, _demuxer, msgr);
  PrndClient.RegistrationCompleted += OnRegistrationCompleted;
  PrndClient.ProximityDetectionCompleted += OnProximityDetectionCompleted;
  PrndClient.LicenseFetchCompleted += OnFetchLicenseCompleted;
  PrndClient.ReRegistrationNeeded += OnRehandshake;
  PrndClient.ClosedCaptionDataReceived += OnClosedCaptionDataReceived;
  .
  .
  .
}

The clients that the sample app works with (the HTTP download engine, M2TS demultiplexer, and TCP manager) create NDStreamParserNotifier objects to inform the sample app when it needs to respond to events. For example, this code in the M2TSDemuxer class creates the notification that the demultiplexer client uses to notify the sample app when it has data ready to transfer:

/// The stream parser must instantiate and use this interface for sending notifications
/// to the ND client.
///
private NDStreamParserNotifier _notifier;

///  Default constructor.
   
public M2TSDemuxer()
{
  // Create the download engine notifier for sending notifications to the
  // ND client.
  _notifier = new NDStreamParserNotifier();
}
    

The HTTPDownloadEngine object also includes an Open method that opens the URI for the user-selected channel and begins reading data from it:

public void Open(Uri uri, [System.Runtime.InteropServices.WindowsRuntime.ReadOnlyArray()] byte[] sessionIDBytes)
{
  lock (_lock)
  {
    // Note: Below implementation is a receiver specific that may not work for others
    string queryToAppend = "CONTENTPROTECTIONTYPE=PRND&PRNDSESSION=" + BitConverter.ToString(sessionIDBytes).Replace("-", string.Empty);
    string uriString = uri.AbsoluteUri + uri.Query + (uri.Query.Length == 0 ? "?" : '&') + queryToAppend;

    _source = new Uri(uriString);
    _paused = false;
    _state = BaseSourceState.Opened;
    _pendingArgs = new Queue\DataReceivedEventArgs>();

    _request = (HttpWebRequest)WebRequest.Create(_source);

    // Make sure the request does not try to read the whole stream in
    _request.AllowReadStreamBuffering = false;

    // Store our state for the request
    HttpRequestState state = new HttpRequestState();
    state.Request = _request;

    // Launch the request
    _request.BeginGetResponse(new AsyncCallback(OnGetResponse), state);
  }
}
    

From this point on, the sample app manages its own communications with the other clients. When a client event indicates that the app needs to take action, the client calls the appropriate event handler, which creates and sends a notifier to inform the app of the event. For example, this code in the HTTPDownloadEngine class sends a notifier to the app when it reads a PlayReady header in one of the streams it sends to the app:

private void OnGetResponse( IAsyncResult result )
{
  Trace.WriteLine( "*** Got response" );
  try
  {
    HttpRequestState state = (HttpRequestState)result.AsyncState;

    state.Response = (HttpWebResponse)state.Request.EndGetResponse( result );

    // Get the stream of bytes from our response.
    Stream responseStream = state.Response.GetResponseStream();
    state.Stream = responseStream;

    // Look for any PlayReady headers from this stream. If so, make sure we notify the app.
    string headerValue = state.Response.Headers[ Constants.PlayReadyResponseHeader ];
    if( !string.IsNullOrEmpty( headerValue ) )
    {
      Notifier.OnPlayReadyObjectReceived( HexToByteArray( headerValue ) );
    }

    BeginFirstRead( state );
  }
  catch( Exception e )
  {
    .
    .
    .
  }
}
    

The clients communicate the following types of information to the app:

  1. HTTP download engine: gets content from the URI that the user selected and notifies the app.
  2. The app sends the downloaded media stream to the M2TS demultiplexer for parsing.
  3. The M2TS demultiplexer parses the different media streams from the downloaded stream and notifies the app that stream data is available. The M2TS demultiplexer supports the streams in H.264, MPEG-2, AAC, and AC3 foramt.

If closed captioning is present in the media stream, the app uses the following method to handle the ClosedCaptionDataReceived event:

private void OnClosedCaptionDataReceived(object sender, INDClosedCaptionDataReceivedEventArgs e)
{
  if(Caption)
  {
    CC608Parser.Parse(e.ClosedCaptionDataFormat, e.PresentationTimestamp, e.ClosedCaptionData);
  }
}
    

See Also

Using the PlayReady Device Porting Kit Test Transmitter