How to integrate a basic HLS stream with FairPlay

How to integrate a basic HLS stream with FairPlay

One of our iOS teams at iCapps is currently working on a project where we need to be able to play HLS video streams that are protected. It took a while to find out how everything works from a technical perspective. So once we found a working solution, I decided to share this knowledge in a blogpost.

HLS

Now just for the record: what is HLS? HLS stands for HTTP Live Streaming. It's a protocol that allows you to send on-demand or live video or audio streams to your devices. This technology is developed by Apple, and is optimised to deliver the best possible quality. It adapts seamlessly to the network conditions, so when the network is unreliable, HLS will adapt without causing user-visible playback stalling.

Why would you want to use HLS and not MPEG-DASH or others? Well, when you want to stream to Apple systems, then I recommend using it. This is especially true in the case of Apple TV. HLS is currently the only streaming protocol that can be used by developers.

Fairplay

Digital Rights Management (DRM in short) allows a content provider to securely manage multimedia content. Its main goal is to protect by encrypting the video fragment so it cannot be copied without authorization. The DRM technology used by and developed by Apple is FairPlay.

FairPlay allows you to securely send the streaming content to the device. It securely delivers keys to Apple devices that make it possible to decrypt the encrypted content.

A big advantage of using HSL with FairPlay is that you can even stream the encrypted content over AirPlay. So no more black screens on your TV!

How does it work?

Here is a small overview of what happens when you want to play protected content on your device,

  1. The application will send a video or audio stream to the native player.
  2. The native player will read the playlist from the manifest and will come back to the app when the content is protected.
  3. Your app will request an encrypted Server Playback Context (SPC) message from the OS. This will be generated by combining the correct FairPlay application certificate and a content identifier.
  4. The encrypted SPC message is uploaded to the Key Server Module (KSM), also known as your content provider's DRM server.
  5. If the SPC is correct, the KSM will return an encrypted Content Key Context (CKC) message.
  6. This CKC is sent back to the native player and now the player is capable of decrypting the stream. 

Here is a small graph of what happens in a nutshell. Credits go to Apple.

I'm not a big fan of all the abbreviations used in the above explanation. But it helped me to get through the decryption process.

AVPlayer

Explaining what needs to happen was the easy part. Now I'll go through the above steps from a technical perspective.

  1. Send an encrypted audio or video stream to AVPlayer.

let url = URL(string: "https://icapps.com/stream.m3u8")!
let asset = AVURLAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: playerItem)

  1. When you try to run the above code snippet you will notice that nothing happens. Well, that is obvious because the stream is encrypted. But how do we respond to encrypted content? You can start by setting the resourceLoader's delegate.

let asset = AVURLAsset(url: url)
let queue = DispatchQueue(label: "Some queue")
asset.resourceLoader.setDelegate(self, queue: queue)

The only delegate method that you should implement at the moment is this one:

func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool

This will be triggered automagically when you try to stream encrypted content. Be aware: this only works when running on a device. When running on the simulator, it's never triggered. It can cost you some development time before you get over this gotcha.

  1. In this delegate method we need to request the SPC from the OS.  For this you'll need two things: the certificate from your content provider and a content identifier. In our case the public key is fetched asynchronously and the content identifier is the host as defined in the manifest.

// Read the content identifier from the manifest.
let url = loadingRequest.request.url!
let identifier = url.host

// Create data objects.
let certificateData = "certificate".data(using: .utf8)!
let identifierData = identifier.data(using: .utf8)!

// Request the SPC.
let spc = try! loadingRequest.streamingContentKeyRequestData(forApp: publicKeyData, contentIdentifier: identifierData, options: nil)

When all the above succeeds, we've got our SPC that we can send to the KSM. The code above explicitly unwraps the optional values, but you should handle this correctly. And when something fails, then you should finish the request gracefully.

let error = NSError(domain: "...", code: -3, userInfo: nil)
loadingRequest.finishLoading(with: error)

// Only needed when no asynchronous call was performed at the time. return false

  1. Next up, upload the SPC to your content provider's server. I will show you a simple example of a request, but this should be done according to the documentation as provided by your content provider.

// Fetch CKC.
let url = URL(string: "https://icapps.com/getckc?...")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
// Pass the SPC data in the body of the request.
request.httpBody = spc
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration)
let task = session.dataTask(with: request) { data, response, error in
        if let data = data {
             // The encrypted CKC message.
        } else {
             // Failed.
        }
}
task.resume()

// Tell the `AVPlayer` instance to wait.
return true

  1. The data object returned by the task will contain the CKC.
  2. This CKC should be sent to the native player instance, but in this case this will be the data request.

if let data = data {
             let dataRequest = loadingRequest.dataRequest!
             // This command sends the CKC to the player.
             dataRequest.respond(with: data)
             loadingRequest.finishLoading()
             }

Now enjoy your player playing the encrypted content.

A more complete code snippet can be found here: https://gist.github.com/fousa/5709fb7c84e5b53dbdae508c9cb4fadc

Abbreviations

  • DRM Digital Rights Management
  • HLS HTTP Live Streaming
  • KSM Key Server Module
  • SPC Server Playback Context
  • CKC Content Key Context

References

In the links below you can find more information on HLS & FairPlay.

FairPlay Streaming Overview - Apple Developer
HTTP Live Streaming
FairPlay Streaming