iOS: Protect your content with Studio DRM with JW Platform

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

JW Player provides a simplified approach to protecting your content with industry-standard Digital Rights Management (DRM). By enabling DRM on a property from your JW Player dashboard, the complex aspects of DRM management are managed by JW Player 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 JW Player 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

JW Player 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


Requirements


Enabling Apple FairPlay Streaming

If you want to use Apple FairPlay DRM, you must get a FairPlay Streaming Deployment package from Apple and upload the credentials to each DRM-enabled property in your JW dashboard.

🚧

You can use the same Fairplay Deployment package on all your properties, but you must upload the package to each property, explicitly.


Acquire FairPlay credentials

You must have the FairPlay credentials listed in the following table.

CredentialNotes
Encoded *.p12 keystore fileThe *.p12 keystore file must contain your FPS Deployment certificate and private key. This file must be password-protected (be sure to password-protect the whole file, NOT just the private key)
ASKApp secret key

To create the .p12 keystore file in OpenSSL, use the following commands:

$ openssl pkcs12 -export -out NAME.p12 -inkey PRIVATE_KEY.pem -in CERT.pem -passout pass:PASSWORD


Add FairPlay credentials to a property

  1. From the property list page, click on the name of a Studio DRM-enabled property.
  2. On the Content Protection tab, click Add FPS Credentials to add your credentials to the property. Repeat this step for all the DRM-enabled properties on which you want to enable FairPlay.


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 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 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)\


  1. 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
    }
}


  1. 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()
}


Did this page help you?