4

I am trying to play a Fairplay DRM protected (encrypted through Azure Media Services) HLS video stream on iOS Device. I have used the code and process described in the following links:

https://icapps.com/blog/how-integrate-basic-hls-stream-fairplay

https://gist.github.com/fousa/5709fb7c84e5b53dbdae508c9cb4fadc

Following is the code I have written for this.

import UIKit
import AVFoundation

class ViewController: UIViewController, AVAssetResourceLoaderDelegate {

@IBOutlet weak var videoView: UIView!
var player: AVPlayer!
override func viewDidLoad() {
    super.viewDidLoad()
    let streamURL = "someexampleurl.com/stream.m3u8"
    if let url = URL(string: streamURL) {
        //2. Create AVPlayer object
        let asset = AVURLAsset(url: url)
        let queue = DispatchQueue(label: "Some queue")
        asset.resourceLoader.setDelegate(self, queue: queue)
        let playerItem = AVPlayerItem(asset: asset)
        player = AVPlayer(playerItem: playerItem)
        //3. Create AVPlayerLayer object
        let playerLayer = AVPlayerLayer(player: player)
        playerLayer.frame = self.videoView.bounds //bounds of the view in which AVPlayer should be displayed
        playerLayer.videoGravity = .resizeAspect

        //4. Add playerLayer to view's layer
        self.videoView.layer.addSublayer(playerLayer)

        //5. Play Video
        player.play()

    }
    // Do any additional setup after loading the view.
}

  func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
     // We first check if a url is set in the manifest.
     guard let url = loadingRequest.request.url else {
     print("", #function, "Unable to read the url/host data.")
     loadingRequest.finishLoading(with: NSError(domain: "com.error", code: -1, userInfo: 
     nil))
     return false
  }
  print("", #function, url)

// When the url is correctly found we try to load the certificate date. Watch out! For this
// example the certificate resides inside the bundle. But it should be preferably fetched from
// the server.
  guard
    let certificateURL = Bundle.main.url(forResource: "certfps", withExtension: "cer"),
    let certificateData = try? Data(contentsOf: certificateURL) else {
    print("", #function, "Unable to read the certificate data.")
    loadingRequest.finishLoading(with: NSError(domain: "com.error", code: -2, userInfo: nil))
    return false
  }

  // Request the Server Playback Context.

  let contentId = "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

  guard
      let contentIdData = contentId.data(using: String.Encoding.utf8),
      let spcData = try? loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData, options: nil),
      let dataRequest = loadingRequest.dataRequest else {
      loadingRequest.finishLoading(with: NSError(domain: "com.error", code: -3, userInfo: nil))
      print("", #function, "Unable to read the SPC data.")
      return false
  }

  // Request the Content Key Context from the Key Server Module.
  let ckcURL = URL(string: "https://xxxxx.keydelivery.northeurope.media.azure.net/FairPlay/?kid=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")!
  var request = URLRequest(url: ckcURL)
  request.httpMethod = "POST"
  let assetIDString = "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  let postString = "spc=\(spcData.base64EncodedString())&assetId=\(assetIDString)"
  request.setValue(String(postString.count), forHTTPHeaderField: "Content-Length")
  request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

  request.httpBody = postString.data(using: .ascii, allowLossyConversion: true)
  let session = URLSession(configuration: URLSessionConfiguration.default)
  let task = session.dataTask(with: request) { data, response, error in
    if let data = data {
      // The CKC is correctly returned and is now send to the `AVPlayer` instance so we
      // can continue to play the stream.
        if var responseString = String(data: data, encoding: .utf8) {
            responseString = responseString.replacingOccurrences(of: "<ckc>", with: "").replacingOccurrences(of: "</ckc>", with: "")
            var ckcData = Data(base64Encoded: responseString)!
            dataRequest.respond(with: ckcData)
            loadingRequest.finishLoading()
        } else {
           // print("Error encountered while fetching FairPlay license for URL: \(self.drmUrl), \(error?.localizedDescription ?? "Unknown error")")
        }


  task.resume()

  return true
}

}

Everything above works but in the CKC response I get

{
   "Error": {
   "Message": "Failed content key policy evaluation.",
   "Code": "AuthorizationPolicyEvaluationFailure"
   }
}

Can anyone please here let me know what I am missing here, this is my first time trying this out so I could be making a very obvious mistake so please bear with that.

Any help regarding this would be really great (I have been hitting my head on this for multiple days now.)

Thanks.

Manish Gupta
  • 1,405
  • 14
  • 32
  • 1
    Do you have control over `xxxxx.keydelivery.northeurope.media.azure.net`? The error seems related to content key policy, see: https://learn.microsoft.com/en-us/azure/media-services/latest/offline-fairplay-for-ios – aergistal Feb 12 '20 at 13:42

3 Answers3

2

One thing that will probably help with troubleshooting is to enable the license delivery logging. You can do this in the Azure portal by going to your Media Services account, in the Monitoring section go to Diagnostic settings. Click 'Add diagnostic setting'. Give the setting a name and then, at least initially, tell it to archive to a storage account. Log the 'KeyDeliveryRequests'. Once you save this reproduce the issue. Then go to your Storage account and look for the log result. The Storage container ‘insights-logs-keydeliveryrequests’ will contain the logs.

David Bristol
  • 621
  • 3
  • 6
  • 1
    Hi @David, thanks for the logging tip, this will definitely help in the future but I figured out the thing I was missing was not passing the JWT in the "Authorization" header for the CKC request. – Jatin Mishra Feb 13 '20 at 05:49
2

you can add request header parameter like "authorization" (probably a base 64 token called JWT), "mimetype" in making CKC request, it would work.

mani gupta
  • 109
  • 1
  • 13
1

Finally, I figured the thing I was missing was not passing the JWT in the "Authorization" header for the CKC request. Passing the JWT did the trick. :)

Note: JWT stands for the JSON web token generated during the media encryption in azure media services.