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


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
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 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, 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()
}