AVPlayer (JW Platform)
Learn how to integrate your DRM-protected content in iOS.
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 15 days ago