e27baeb065
Adds offline decryption key and uses it to decrypt content during offline playback Jira: VEX-5938 https://jira.tenkasu.net/browse/VEX-5938 - Update to accept scheme for key required to play offline playback - Uses provided scheme to intercept call from player and return the key - Fixes player item observer removal pattern ### Reviews - Major reviewer (domain expert): @armadilio3
169 lines
6.9 KiB
Swift
169 lines
6.9 KiB
Swift
import AVFoundation
|
|
import Promises
|
|
|
|
struct RCTVideoDRM {
|
|
@available(*, unavailable) private init() {}
|
|
|
|
static func fetchLicense(
|
|
licenseServer: String,
|
|
spcData: Data?,
|
|
contentId: String,
|
|
headers: [String:Any]?
|
|
) -> Promise<Data> {
|
|
let request = createLicenseRequest(licenseServer:licenseServer, spcData:spcData, contentId:contentId, headers:headers)
|
|
|
|
return Promise<Data>(on: .global()) { fulfill, reject in
|
|
let postDataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler:{ (data:Data!,response:URLResponse!,error:Error!) in
|
|
|
|
let httpResponse:HTTPURLResponse! = (response as! HTTPURLResponse)
|
|
|
|
guard error == nil else {
|
|
print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)")
|
|
reject(error)
|
|
return
|
|
}
|
|
guard httpResponse.statusCode == 200 else {
|
|
print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)")
|
|
reject(RCTVideoErrorHandler.licenseRequestNotOk(httpResponse.statusCode))
|
|
return
|
|
}
|
|
|
|
guard data != nil, let decodedData = Data(base64Encoded: data, options: []) else {
|
|
reject(RCTVideoErrorHandler.noDataFromLicenseRequest)
|
|
return
|
|
}
|
|
|
|
fulfill(decodedData)
|
|
})
|
|
postDataTask.resume()
|
|
}
|
|
}
|
|
|
|
static func createLicenseRequest(
|
|
licenseServer: String,
|
|
spcData: Data?,
|
|
contentId: String,
|
|
headers: [String:Any]?
|
|
) -> URLRequest {
|
|
var request = URLRequest(url: URL(string: licenseServer)!)
|
|
request.httpMethod = "POST"
|
|
|
|
if let headers = headers {
|
|
for item in headers {
|
|
guard let key = item.key as? String, let value = item.value as? String else {
|
|
continue
|
|
}
|
|
request.setValue(value, forHTTPHeaderField: key)
|
|
}
|
|
}
|
|
|
|
let spcEncoded = spcData?.base64EncodedString(options: [])
|
|
let spcUrlEncoded = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, spcEncoded as? CFString? as! CFString, nil, "?=&+" as CFString, CFStringBuiltInEncodings.UTF8.rawValue) as? String
|
|
let post = String(format:"spc=%@&%@", spcUrlEncoded as! CVarArg, contentId)
|
|
let postData = post.data(using: String.Encoding.utf8, allowLossyConversion:true)
|
|
request.httpBody = postData
|
|
|
|
return request
|
|
}
|
|
|
|
static func fetchSpcData(
|
|
loadingRequest: AVAssetResourceLoadingRequest,
|
|
certificateData: Data,
|
|
contentIdData: Data
|
|
) -> Promise<Data> {
|
|
return Promise<Data>(on: .global()) { fulfill, reject in
|
|
var spcError:NSError!
|
|
var spcData: Data?
|
|
do {
|
|
spcData = try loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData as Data, options: nil)
|
|
} catch _ {
|
|
print("SPC error")
|
|
}
|
|
|
|
if spcError != nil {
|
|
reject(spcError)
|
|
}
|
|
|
|
guard let spcData = spcData else {
|
|
reject(RCTVideoErrorHandler.noSPC)
|
|
return
|
|
}
|
|
|
|
fulfill(spcData)
|
|
}
|
|
}
|
|
|
|
static func createCertificateData(certificateStringUrl:String?, base64Certificate:Bool?) -> Promise<Data> {
|
|
return Promise<Data>(on: .global()) { fulfill, reject in
|
|
|
|
guard let certificateStringUrl = certificateStringUrl,
|
|
let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else {
|
|
reject(RCTVideoErrorHandler.noCertificateURL)
|
|
return
|
|
}
|
|
|
|
var certificateData:Data?
|
|
do {
|
|
certificateData = try Data(contentsOf: certificateURL)
|
|
if (base64Certificate != nil) {
|
|
certificateData = Data(base64Encoded: certificateData! as Data, options: .ignoreUnknownCharacters)
|
|
}
|
|
} catch {}
|
|
|
|
guard let certificateData = certificateData else {
|
|
reject(RCTVideoErrorHandler.noCertificateData)
|
|
return
|
|
}
|
|
|
|
fulfill(certificateData)
|
|
}
|
|
}
|
|
|
|
static func handleWithOnGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId:String?, certificateUrl:String?, base64Certificate:Bool?) -> Promise<Data> {
|
|
let contentIdData = contentId?.data(using: .utf8)
|
|
|
|
return RCTVideoDRM.createCertificateData(certificateStringUrl:certificateUrl, base64Certificate:base64Certificate)
|
|
.then{ certificateData -> Promise<Data> in
|
|
guard let contentIdData = contentIdData else {
|
|
throw RCTVideoError.invalidContentId as! Error
|
|
}
|
|
|
|
return RCTVideoDRM.fetchSpcData(
|
|
loadingRequest:loadingRequest,
|
|
certificateData:certificateData,
|
|
contentIdData:contentIdData
|
|
)
|
|
}
|
|
}
|
|
|
|
static func handleInternalGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId:String?, licenseServer:String?, certificateUrl:String?, base64Certificate:Bool?, headers: [String:Any]?) -> Promise<Data> {
|
|
let url = loadingRequest.request.url
|
|
|
|
guard let contentId = contentId ?? url?.absoluteString.replacingOccurrences(of: "skd://", with:"") else {
|
|
return Promise(RCTVideoError.invalidContentId as! Error)
|
|
}
|
|
|
|
let contentIdData = NSData(bytes: contentId.cString(using: String.Encoding.utf8), length:contentId.lengthOfBytes(using: String.Encoding.utf8)) as Data
|
|
|
|
return RCTVideoDRM.createCertificateData(certificateStringUrl:certificateUrl, base64Certificate:base64Certificate)
|
|
.then{ certificateData in
|
|
return RCTVideoDRM.fetchSpcData(
|
|
loadingRequest:loadingRequest,
|
|
certificateData:certificateData,
|
|
contentIdData:contentIdData
|
|
)
|
|
}
|
|
.then{ spcData -> Promise<Data> in
|
|
guard let licenseServer = licenseServer else {
|
|
throw RCTVideoError.noLicenseServerURL as! Error
|
|
}
|
|
return RCTVideoDRM.fetchLicense(
|
|
licenseServer: licenseServer,
|
|
spcData: spcData,
|
|
contentId: contentId,
|
|
headers: headers
|
|
)
|
|
}
|
|
}
|
|
}
|