AVPlayer (JW Platform)

Learn how to integrate your DRM-protected content in iOS.

Studio DRM and AVPlayer provide a comprehensive approach to protecting your content with industry-standard Digital Rights Management (DRM). After enabling DRM on a property from your JWP dashboard and integrating with AVPlayer, DRM decryption of the content will be managed by AVPlayer and the OS.

For more information about the DRM workflow, refer to the High-Level Workflow Overview.


šŸ”‘

For the following use cases, use Studio DRM Standalone with your current streaming and hosting solution:

  • Choosing not to enable Studio DRM with JW Platform
  • Implementing live stream integrations


Requirements

Item Notes
DRM entitlement Contact your JWP representative for more information.
DRM-enabled property See: Enable a property
FairPlay Streaming Deployment Package See: Add FairPlay credentials to a property


Implementation

šŸ’”

We strongly recommend referring to and starting with our Studio DRM with JW Platform and AVPlayer demo for iOS and tvOS. This .zip file allows you to see both Apple's recommended full working implementation and an example of how to manage the licensing of online and offline multiple assets.


Use the following steps to set up DRM playback in your iOS app:

  1. Create a DeliveryAPI struct to easily parse the required JSON data.
import Foundation

// MARK: - DeliveryAPI
struct DeliveryAPI: Codable {
let title, welcomeDescription, kind: String
let playlist: [Playlist]
let feedInstanceID: String

enum CodingKeys: String, CodingKey {
    case title
    case welcomeDescription = "description"
    case kind, playlist
    case feedInstanceID = "feed_instance_id"
}
}

// MARK: - Playlist
struct Playlist: Codable {
let title, mediaid: String
let link: String
let image: String
let images: [Image]
let duration, pubdate: Int
let playlistDescription: String
let sources: [Source]
let tracks: [Track]

enum CodingKeys: String, CodingKey {
    case title, mediaid, link, image, images, duration, pubdate
    case playlistDescription = "description"
    case sources, tracks
}
}

// MARK: - Image
struct Image: Codable {
let src: String
let width: Int
let type: String
}

// MARK: - Source
struct Source: Codable {
let drm: DRM
let file: String
let type: String
}

// MARK: - DRM
struct DRM: Codable {
let widevine, playready: Playready?
let fairplay: Fairplay?
}

// MARK: - Fairplay
struct Fairplay: Codable {
let processSpcURL, certificateURL: String

enum CodingKeys: String, CodingKey {
    case processSpcURL = "processSpcUrl"
    case certificateURL = "certificateUrl"
}
}

// MARK: - Playready
struct Playready: Codable {
let url: String
}

// MARK: - Track
struct Track: Codable {
let file: String
let kind: String
}

// MARK: - Helper functions for creating encoders and decoders

func newJSONDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
    decoder.dateDecodingStrategy = .iso8601
}
return decoder
}

  1. Generate a signed URL for DRM playback.

    āš ļø

    We strongly recommend using a proxy service to generate the JSON web token (JWT). If you generate the JWT within a client-side native app, you risk exposing your API secret.


  1. Make a GET call with the signed URL.

    From the signed content URL response, the example code sample extracts the content URL (playlist[].sources[].file), the certificate URL (playlist[].sources[].drm.fairplay.certificateUrl) and the SPC Process URL (playlist[].sources[].drm.fairplay.spcProcessUrl) from the sources array and populates the specific stream configuration with them.

    āš ļø

    The ordering of items within playlist[].sources[] is not static. Therefore, do not use a defined index (playlist[].sources[0]) as part of your extraction process. The following example code sample demonstrates how to locate the correct playlist[].sources index.

    Also, both the media URL and its associated LAURLs are valid for only 10 minutes from when they are requested.

func fetchData(stream: Stream, completion: @escaping() -> Void) {
    // A semaphore to signal receipt of response to our request
    let sem = DispatchSemaphore.init(value: 0)
    
    let headers = ["Accept": "application/json; charset=utf-8"]
    
    let request = NSMutableURLRequest(url: NSURL(string: jwSource!)! as URL,
                                      cachePolicy: .useProtocolCachePolicy,
                                      timeoutInterval: 10.0)
    request.httpMethod = "GET"
    request.allHTTPHeaderFields = headers
    
    let dataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
        if (error != nil) {
            print(error!)
        } else {
            guard let data = data else { return }
            if let jsonData = try? newJSONDecoder().decode(DeliveryAPI.self, from: data){
                stream.certificateURL = jsonData.playlist[0].sources[1].drm.fairplay!.certificateURL
                stream.playlistURL = jsonData.playlist[0].sources[1].file
                stream.licenseURL = jsonData.playlist[0].sources[1].drm.fairplay!.processSpcURL
                let fileurl = NSURL(fileURLWithPath: stream.playlistURL)
                let cleanUrlString = fileurl.absoluteString!.replacingOccurrences(of: " -- file:///", with: "")
                let cleanUrlString2 = cleanUrlString.replacingOccurrences(of: "file:///", with: "")
                guard let cleanUrl2 = NSURL(string:cleanUrlString2) else { return }
                self.getContentKeyIDList(videoUrl: cleanUrl2){
                    stream.contentKeyIDList?.append((self.parsedContentKeyID?.absoluteString)!)
                    stream.contentID = (self.parsedContentKeyID?.lastPathComponent)!
                    StreamListManager.shared.updateStream(withMediaID: stream.mediaID!)
                    
                    self.transactions = self.transactions + 1
                    
                    if self.transactions == StreamListManager.shared.streams.count {
#if os(iOS)
                        StreamListManager.shared.writeSupportFile(streams: StreamListManager.shared.streams)
#endif
                        NotificationCenter.default.post(name: .AssetListManagerDidLoad,
                                                        object: self)
                    }
                }
                // Signal response received
                do { sem.signal()}
            }
            
            sem.wait()
            completion()
        }
    })
    
    dataTask.resume()
    
}
"sources": [{
        "drm": {
            "fairplay": {
                "processSpcUrl": "FairPlay LAURL",
                "certificateUrl": "FairPlay Certificate URL"
            }
        },
        "file": "SIGNED-M3U8-URL",
        "type": "application/vnd.apple.mpegurl"
    },
    ...
]

  1. Parse the stream.contentKeyIDList from the .m3u8 manifest. The stream.contentKeyIDList enables the OS to map the content and license correctly.
    public func getContentKeyIDList (videoUrl: NSURL, completion: @escaping() -> Void) {
    print("Parsing Content Key ID from manifest with \(videoUrl)")
    var request = URLRequest(url: videoUrl as URL)
    // A semaphore to signal receipt of response to our request
    let sem = DispatchSemaphore.init(value: 0)
    
    request.httpMethod = "GET"
    let session = URLSession(configuration: URLSessionConfiguration.default)
    let task = session.dataTask(with: request) { data, response, _ in
        guard let data = data else { return }
        let strData = String(data: data, encoding: .utf8)!
        if strData.contains("EXT-X-SESSION-KEY") || strData.contains("EXT-X-KEY") {
            let start = strData.range(of: "URI=\"")!.upperBound
            let end = strData[start...].range(of: "\"")!.lowerBound
            let keyUrlString = strData[start..<end]
            let keyUrl = URL(string: String(keyUrlString))
            self.parsedContentKeyID = keyUrl as NSURL?
            // Signal response received
            do { sem.signal()}
        } else {
            // This could be HLS content with variants
            if strData.contains("EXT-X-STREAM-INF") {
                // Prepare the new variant video url last path components
                let start = strData.range(of: "EXT-X-STREAM-INF")!.upperBound
                let end = strData[start...].range(of: ".m3u8")!.upperBound
                let strData2 = strData[start..<end]
                let start2 = strData2.range(of: "\n")!.lowerBound
                let end2 = strData2[start...].range(of: ".m3u8")!.upperBound
                let unparsedVariantUrl = strData[start2..<end2]
                let variantUrl = unparsedVariantUrl.replacingOccurrences(of: "\n", with: "")
                // Prepare the new variant video url
                let videoUrlString = videoUrl.absoluteString
                let replaceString = String(videoUrl.lastPathComponent!)
                if let unwrappedVideoUrlString = videoUrlString {
                    let newVideoUrlString = unwrappedVideoUrlString.replacingOccurrences(of: replaceString, with: variantUrl)
                    let pathURL = NSURL(string: newVideoUrlString)!
                    // Push the newly compiled variant video URL through this method
                    
                    print("parsing variant at: \(pathURL)")
                    self.getContentKeyIDList(videoUrl: pathURL){
                    }
                }
            } else {
                // Nothing we understand, yet
                print("Unable to parse URI from manifest. EXT-X-SESSION-KEY, EXT-X-KEY, or variant not found.")
            }
        }
        sem.wait()
        completion()
    }
    task.resume()
}

  1. Use the extracted license URL stream.certificateURL to retrieve the Application Certificate.

    In response to the various ContentKeyDelegate class calls required to complete the associated certification and licensing, the following code will obtain the Application Certificate and then the FairPlay License required for either online or offline playback.
func requestApplicationCertificate(url: String) throws -> Data? {
    var applicationCertificate: Data? = nil
    let request = URLRequest(url: URL(string: url)!)
    let session = URLSession.shared
    // A semaphore to signal receipt of response to our request
    let sem = DispatchSemaphore.init(value: 0)
    
    
    let task = session.dataTask(with: request,
                                completionHandler: { data, response, error -> Void in
        defer { sem.signal() }
        if let error = error{
            let certerr = "Unable to retrieve FPS certificate: \(String(describing: error))"
            print("\(certerr)")
            
            return
        }
        let httpResponse = response as! HTTPURLResponse
        let transactionID = httpResponse.allHeaderFields[self.studioDRMTransactionIdHeader] as? String
        if let unwrappedID = transactionID {
            if (httpResponse.statusCode >= 400){
                let statuserr = "Unexpected HTTP status code: \(httpResponse.statusCode) acquiring certificate from server with transaction ID: \(unwrappedID)"
                print("\(statuserr)")
                
                return
            }
        }
        applicationCertificate = data
    })
    task.resume()
    
    sem.wait()
    return applicationCertificate!
}

  1. Use the extracted license URL stream.licenseURL to retrieve the FairPlay license.
func synchFetchSPC(_ spcData:Data) throws -> (Data?,URLResponse?,NSError?) {
    
    var myResponse: URLResponse? = nil
    var myData: Data? = nil
    var myError: NSError? = nil
    let components = URLComponents(string: jwLicenseUrl!)
    var request = URLRequest(url: (components?.url!)!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-type")
    let base64Encoded = spcData.base64EncodedData(options: Data.Base64EncodingOptions(rawValue: 0))
    let alpha = NSString(data: base64Encoded, encoding: String.Encoding.utf8.rawValue)!
    let jsonDict : [String: AnyObject] = ["payload":alpha, "contentId":contentID as AnyObject]
    let stringBody = JSONStringifyCommand(jsonDict)
    request.httpBody = stringBody!.data(using: String.Encoding.utf8)
    
    let session = URLSession.shared
    // A semaphore to signal receipt of response to our request
    let sem = DispatchSemaphore.init(value: 0)
    
    let task = session.dataTask(with: request,
                                completionHandler: { data, response, error -> Void in
        defer { sem.signal() }
        
        if let error = error{
            let urlResponse = response as? HTTPURLResponse
            let transactionID = urlResponse!.allHeaderFields[self.studioDRMTransactionIdHeader] as? String
            
            if let unwrappedID = transactionID {
                
                let licerr = "Unable to retrieve FPS license with status code: \(urlResponse!.statusCode) and transaction ID: \(unwrappedID) and error: \(String(describing: error))"
                
                print("\(licerr)")
                
                return
            }
        }
        myResponse = response!
        myData = data
        myError = error as NSError?
        
    })
    task.resume()
    // block thread until completion handler is called
    sem.wait()
    
    return (myData, myResponse, myError)
}

func JSONStringifyCommand( _ messageDictionary : Dictionary <String, AnyObject>) -> String? {
    
    let options = JSONSerialization.WritingOptions(rawValue: 0)
    
    if JSONSerialization.isValidJSONObject(messageDictionary) {
        
        do {
            let data =  try JSONSerialization.data(withJSONObject: messageDictionary, options: options)
            if let string = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
                return string as String
            }
            
        } catch {
            print("error JSONStringify")
        }
    }
    
    return nil
}