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:
- 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
}
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 5 months ago