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
    }