JWP iOS SDK (JW Platform)

Learn a simplified approach to protecting your iOS content with DRM.



JWP provides a simplified approach to protecting your content with industry-standard Digital Rights Management (DRM). By enabling DRM on a property from your JWP dashboard, the complex aspects of DRM management are managed by JWP on your behalf:

  • Several configured DRM Policies
  • DRM media content key generation and management for FairPlay Streaming
  • License delivery services for content playback on any Apple device

With JWP managing the technical aspects of DRM, you can focus on the design and implementation of engaging content experiences. For more information about the DRM workflow, please 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


Compatibility

JWP supports industry-standard DRM. The following table shows the DRM technology that is supported with the iOS SDK and which browsers and operating systems support this technology.

Browser | OS FairPlay PlayReady Widevine
iOS/iPadOS 12+
(native)
βœ“
Safari (iOS)
2 most recent stable versions
βœ“


Prerequisites

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
iOS SDK See: Add the SDK (iOS)
Player in a View See: Set up a player (iOS)


Implementation

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 fairplay: Fairplay?
        let widevine: Widevine?
        let playready: Playready?
    }
    
    // MARK: - Fairplay
    struct Fairplay: Codable {
        let processSpcUrl, certificateUrl: String
    
        enum CodingKeys: String, CodingKey {
            case processSpcURL = "processSpcUrl"
            case certificateURL = "certificateUrl"
        }
    }
    
    // MARK: - Widevine
    struct Widevine: Codable {
        let url: String
    }
    
    // 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()
        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 source will extract 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 populate 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 above example source 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(completion: @escaping() -> Void) {
            // Create a boolean to let us know when data fetch is complete
            var gotURI = false
            // Construct HTTP request headers
            let headers = ["Accept": "application/json; charset=utf-8"]
            // Construct HTTP request
            let request = NSMutableURLRequest(url: NSURL(string: jwSource!)! as URL,
                                              cachePolicy: .useProtocolCachePolicy,
                                              timeoutInterval: 10.0)
            request.httpMethod = "GET"
            // Add headers to request
            request.allHTTPHeaderFields = headers
            // Construct data task to handle response data or error
            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 }
                    // Decode JSON response using DeliveryAPI struct
                    if let jsonData = try? newJSONDecoder().decode(DeliveryAPI.self, from: data){
                // Check we have a "fairplay" element in our data, and if so, get the index of the source
    let arr:Array = jsonData.playlist[0].sources
    var fpIndex: Int? = nil
    for (index, element) in arr.enumerated(){
        if element.drm.fairplay != nil {
            fpIndex = index
        }
    }
    // Allocate DeliveryAPI relevant fields to our stream config
    jwpCertificateEndpoint = jsonData.playlist[0].sources[fpIndex!].drm.fairplay!.certificateURL
    jwpVideoEndpoint = jsonData.playlist[0].sources[fpIndex!].file
    jwpSpcProcessEndpoint = jsonData.playlist[0].sources[fpIndex!].drm.fairplay!.processSpcURL
                        // All done, set Boolean to complete the method
                        gotURI = true
                    }
                    while !gotURI {
                        // Wait until Boolean is true
                    }
                    completion()
                }
            })
            dataTask.resume()
        }
    

    "sources": [{
            "drm": {
                "fairplay": {
                    "processSpcUrl": "FairPlay LAURL",
                    "certificateUrl": "FairPlay Certificate URL"
                }
            },
            "file": "SIGNED-M3U8-URL",
            "type": "application/vnd.apple.mpegurl"
        },
        ...
    ]
    

  1. Use the extracted content URL to set up the player.

    let url = URL(string: jwpVideoEndpoint!)!
    let playerItem = try! JWPlayerItemBuilder().file(url).build()
    let config = try! JWPlayerConfigurationBuilder().playlist([playerItem]).build()
     
    player.contentKeyDataSource = self
    player.configurePlayer(with: config)
    

  2. Use the extracted certificate URL to retrieve the application certificate.

    func appIdentifierForURL(_ url: URL, completionHandler handler: @escaping (Data?) -> Void) {
        
        guard let certUrl = URL(string: jwpCertificateEndpoint!),
              let appIdData = try? requestApplicationCertificate() else {
                  handler(nil)
                  return
              }
        handler(appIdData)
    }
    
    func requestApplicationCertificate() throws -> Data? {
            var applicationCertificate: Data? = nil
            let request = URLRequest(url: URL(string: (jwpCertificateEndpoint))!)
            
            let session = URLSession.shared
            var gotResp = false
            let task = session.dataTask(with: request,
                                        completionHandler: { data, response, error -> Void in
                if let error = error{
                    let message = ["requestApplicationCertificate": "Unable to retrieve FPS certificate: \(String(describing: error))."]
                    print(message)
                    
                    return
                }
                let httpResponse = response as! HTTPURLResponse
                let transactionID = httpResponse.allHeaderFields["x-amz-cf-id"] as? String
                if let unwrappedID = transactionID {
                    if (httpResponse.statusCode >= 400){
                        let message = ["requestApplicationCertificate": "Unexpected HTTP status code: \(httpResponse.statusCode) from FPS certificate server with transaction ID: \(unwrappedID)"]
                        print(message)
                        
                        return
                    }
                }
                applicationCertificate = data
                gotResp = true
            })
            task.resume()
            
            while !gotResp {
                // wait
            }
            return applicationCertificate
        }
    }
    

  3. Use the extracted license URL to retrieve the FairPlay license.

    func contentKeyWithSPCData(_ spcData: Data, completionHandler handler: @escaping (Data?, Date?, String?) -> Void) {
        guard let contentUUID = self.contentUUID else {
            handler(nil, nil, nil)
            return
        }
        
        var ckcRequest = URLRequest(url: URL(string: jwpSpcProcessEndpoint!)!)
        ckcRequest.httpMethod = "POST"
        ckcRequest.httpBody = spcData
        ckcRequest.addValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
        
        URLSession.shared.dataTask(with: ckcRequest) { (data, response, error) in
      let httpResponse = response as? HTTPURLResponse
      let transactionID = httpResponse!.allHeaderFields["x-amz-cf-id"] as? String
    guard error == nil, (200...299).contains(httpResponse.statusCode) else {
    if let unwrappedID = transactionID {
                        let message = ["contentKeyWithSPCData": "Unexpected HTTP status code: \(httpResponse!.statusCode) from FPS certificate server with transaction ID: \(unwrappedID)"]
                        print(message)
                    }
    
                handler(nil, nil, nil)
                return
            }
            handler(data, nil, nil)
        }.resume()
    }