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:
-
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 }
-
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.
-
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 thesources
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" }, ... ]
-
-
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)
-
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 } }
-
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() }
Updated 7 months ago