AVPlayer (JW Platform)
Learn how to integrate your DRM-protected content in iOS.
As an alternative, you can use the JWP iOS SDK to manage the technical aspects of DRM.
Studio DRM and AVPlayer provide a comprehensive approach to protecting your content with industry-standard Digital Rights Management (DRM). After enabling DRM on a property from your JWP dashboard and integrating with AVPlayer, DRM decryption of the content will be managed by AVPlayer and the OS.
For more information about the DRM workflow, 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
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 |
Implementation
We strongly recommend referring to and starting with our Studio DRM with JW Platform and AVPlayer demo for iOS and tvOS. This .zip file allows you to see both Apple's recommended full working implementation and an example of how to manage the licensing of online and offline multiple assets.
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, tvOS 10.0, watchOS 3.0, *) { decoder.dateDecodingStrategy = .iso8601 } 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 code sample extracts 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 populates 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 following example code sample demonstrates how to locate the correctplaylist[].sources
index.Also, both the media URL and its associated LAURLs are valid for only 10 minutes from when they are requested.
func fetchData(stream: Stream, completion: @escaping() -> Void) {
// A semaphore to signal receipt of response to our request
let sem = DispatchSemaphore.init(value: 0)
let headers = ["Accept": "application/json; charset=utf-8"]
let request = NSMutableURLRequest(url: NSURL(string: jwSource!)! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
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 }
if let jsonData = try? newJSONDecoder().decode(DeliveryAPI.self, from: data){
stream.certificateURL = jsonData.playlist[0].sources[1].drm.fairplay!.certificateURL
stream.playlistURL = jsonData.playlist[0].sources[1].file
stream.licenseURL = jsonData.playlist[0].sources[1].drm.fairplay!.processSpcURL
let fileurl = NSURL(fileURLWithPath: stream.playlistURL)
let cleanUrlString = fileurl.absoluteString!.replacingOccurrences(of: " -- file:///", with: "")
let cleanUrlString2 = cleanUrlString.replacingOccurrences(of: "file:///", with: "")
guard let cleanUrl2 = NSURL(string:cleanUrlString2) else { return }
self.getContentKeyIDList(videoUrl: cleanUrl2){
stream.contentKeyIDList?.append((self.parsedContentKeyID?.absoluteString)!)
stream.contentID = (self.parsedContentKeyID?.lastPathComponent)!
StreamListManager.shared.updateStream(withMediaID: stream.mediaID!)
self.transactions = self.transactions + 1
if self.transactions == StreamListManager.shared.streams.count {
#if os(iOS)
StreamListManager.shared.writeSupportFile(streams: StreamListManager.shared.streams)
#endif
NotificationCenter.default.post(name: .AssetListManagerDidLoad,
object: self)
}
}
// Signal response received
do { sem.signal()}
}
sem.wait()
completion()
}
})
dataTask.resume()
}
"sources": [{
"drm": {
"fairplay": {
"processSpcUrl": "FairPlay LAURL",
"certificateUrl": "FairPlay Certificate URL"
}
},
"file": "SIGNED-M3U8-URL",
"type": "application/vnd.apple.mpegurl"
},
...
]
-
Parse the
stream.contentKeyIDList
from the .m3u8 manifest. Thestream.contentKeyIDList
enables the OS to map the content and license correctly.public func getContentKeyIDList (videoUrl: NSURL, completion: @escaping() -> Void) { print("Parsing Content Key ID from manifest with \(videoUrl)") var request = URLRequest(url: videoUrl as URL) // A semaphore to signal receipt of response to our request let sem = DispatchSemaphore.init(value: 0) request.httpMethod = "GET" let session = URLSession(configuration: URLSessionConfiguration.default) let task = session.dataTask(with: request) { data, response, _ in guard let data = data else { return } let strData = String(data: data, encoding: .utf8)! if strData.contains("EXT-X-SESSION-KEY") || strData.contains("EXT-X-KEY") { let start = strData.range(of: "URI=\"")!.upperBound let end = strData[start...].range(of: "\"")!.lowerBound let keyUrlString = strData[start..<end] let keyUrl = URL(string: String(keyUrlString)) self.parsedContentKeyID = keyUrl as NSURL? // Signal response received do { sem.signal()} } else { // This could be HLS content with variants if strData.contains("EXT-X-STREAM-INF") { // Prepare the new variant video url last path components let start = strData.range(of: "EXT-X-STREAM-INF")!.upperBound let end = strData[start...].range(of: ".m3u8")!.upperBound let strData2 = strData[start..<end] let start2 = strData2.range(of: "\n")!.lowerBound let end2 = strData2[start...].range(of: ".m3u8")!.upperBound let unparsedVariantUrl = strData[start2..<end2] let variantUrl = unparsedVariantUrl.replacingOccurrences(of: "\n", with: "") // Prepare the new variant video url let videoUrlString = videoUrl.absoluteString let replaceString = String(videoUrl.lastPathComponent!) if let unwrappedVideoUrlString = videoUrlString { let newVideoUrlString = unwrappedVideoUrlString.replacingOccurrences(of: replaceString, with: variantUrl) let pathURL = NSURL(string: newVideoUrlString)! // Push the newly compiled variant video URL through this method print("parsing variant at: \(pathURL)") self.getContentKeyIDList(videoUrl: pathURL){ } } } else { // Nothing we understand, yet print("Unable to parse URI from manifest. EXT-X-SESSION-KEY, EXT-X-KEY, or variant not found.") } } sem.wait() completion() } task.resume() }
-
Use the extracted license URL
stream.certificateURL
to retrieve the Application Certificate.
In response to the variousContentKeyDelegate
class calls required to complete the associated certification and licensing, the following code will obtain the Application Certificate and then the FairPlay License required for either online or offline playback.func requestApplicationCertificate(url: String) throws -> Data? { var applicationCertificate: Data? = nil let request = URLRequest(url: URL(string: url)!) let session = URLSession.shared // A semaphore to signal receipt of response to our request let sem = DispatchSemaphore.init(value: 0) let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in defer { sem.signal() } if let error = error{ let certerr = "Unable to retrieve FPS certificate: \(String(describing: error))" print("\(certerr)") return } let httpResponse = response as! HTTPURLResponse let transactionID = httpResponse.allHeaderFields[self.studioDRMTransactionIdHeader] as? String if let unwrappedID = transactionID { if (httpResponse.statusCode >= 400){ let statuserr = "Unexpected HTTP status code: \(httpResponse.statusCode) acquiring certificate from server with transaction ID: \(unwrappedID)" print("\(statuserr)") return } } applicationCertificate = data }) task.resume() sem.wait() return applicationCertificate! }
-
Use the extracted license URL
stream.licenseURL
to retrieve the FairPlay license.func synchFetchSPC(_ spcData:Data) throws -> (Data?,URLResponse?,NSError?) { var myResponse: URLResponse? = nil var myData: Data? = nil var myError: NSError? = nil let components = URLComponents(string: jwLicenseUrl!) var request = URLRequest(url: (components?.url!)!) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-type") let base64Encoded = spcData.base64EncodedData(options: Data.Base64EncodingOptions(rawValue: 0)) let alpha = NSString(data: base64Encoded, encoding: String.Encoding.utf8.rawValue)! let jsonDict : [String: AnyObject] = ["payload":alpha, "contentId":contentID as AnyObject] let stringBody = JSONStringifyCommand(jsonDict) request.httpBody = stringBody!.data(using: String.Encoding.utf8) let session = URLSession.shared // A semaphore to signal receipt of response to our request let sem = DispatchSemaphore.init(value: 0) let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in defer { sem.signal() } if let error = error{ let urlResponse = response as? HTTPURLResponse let transactionID = urlResponse!.allHeaderFields[self.studioDRMTransactionIdHeader] as? String if let unwrappedID = transactionID { let licerr = "Unable to retrieve FPS license with status code: \(urlResponse!.statusCode) and transaction ID: \(unwrappedID) and error: \(String(describing: error))" print("\(licerr)") return } } myResponse = response! myData = data myError = error as NSError? }) task.resume() // block thread until completion handler is called sem.wait() return (myData, myResponse, myError) } func JSONStringifyCommand( _ messageDictionary : Dictionary <String, AnyObject>) -> String? { let options = JSONSerialization.WritingOptions(rawValue: 0) if JSONSerialization.isValidJSONObject(messageDictionary) { do { let data = try JSONSerialization.data(withJSONObject: messageDictionary, options: options) if let string = NSString(data: data, encoding: String.Encoding.utf8.rawValue) { return string as String } } catch { print("error JSONStringify") } } return nil }
Updated 7 months ago