2024-09-20 09:46:10 -06:00
|
|
|
//
|
|
|
|
// DRMManager.swift
|
|
|
|
// react-native-video
|
|
|
|
//
|
|
|
|
// Created by Krzysztof Moch on 13/08/2024.
|
|
|
|
//
|
|
|
|
|
|
|
|
import AVFoundation
|
|
|
|
|
|
|
|
class DRMManager: NSObject {
|
|
|
|
static let queue = DispatchQueue(label: "RNVideoContentKeyDelegateQueue")
|
|
|
|
let contentKeySession: AVContentKeySession?
|
|
|
|
|
|
|
|
var drmParams: DRMParams?
|
|
|
|
var reactTag: NSNumber?
|
|
|
|
var onVideoError: RCTDirectEventBlock?
|
|
|
|
var onGetLicense: RCTDirectEventBlock?
|
|
|
|
|
|
|
|
// Licenses handled by onGetLicense (from JS side)
|
|
|
|
var pendingLicenses: [String: AVContentKeyRequest] = [:]
|
|
|
|
|
|
|
|
override init() {
|
|
|
|
#if targetEnvironment(simulator)
|
|
|
|
contentKeySession = nil
|
|
|
|
super.init()
|
|
|
|
#else
|
|
|
|
contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming)
|
|
|
|
super.init()
|
|
|
|
|
|
|
|
contentKeySession?.setDelegate(self, queue: DRMManager.queue)
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
func createContentKeyRequest(
|
|
|
|
asset: AVContentKeyRecipient,
|
|
|
|
drmParams: DRMParams?,
|
|
|
|
reactTag: NSNumber?,
|
|
|
|
onVideoError: RCTDirectEventBlock?,
|
|
|
|
onGetLicense: RCTDirectEventBlock?
|
|
|
|
) {
|
|
|
|
self.reactTag = reactTag
|
|
|
|
self.onVideoError = onVideoError
|
|
|
|
self.onGetLicense = onGetLicense
|
|
|
|
self.drmParams = drmParams
|
|
|
|
|
|
|
|
if drmParams?.type != "fairplay" {
|
|
|
|
self.onVideoError?([
|
|
|
|
"error": RCTVideoErrorHandler.createError(from: RCTVideoError.unsupportedDRMType),
|
|
|
|
"target": self.reactTag as Any,
|
|
|
|
])
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
#if targetEnvironment(simulator)
|
|
|
|
DebugLog("Simulator is not supported for FairPlay DRM.")
|
|
|
|
self.onVideoError?([
|
|
|
|
"error": RCTVideoErrorHandler.createError(from: RCTVideoError.simulatorDRMNotSupported),
|
|
|
|
"target": self.reactTag as Any,
|
|
|
|
])
|
|
|
|
#endif
|
|
|
|
|
|
|
|
contentKeySession?.addContentKeyRecipient(asset)
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Internal
|
|
|
|
|
|
|
|
func handleContentKeyRequest(keyRequest: AVContentKeyRequest) {
|
|
|
|
Task {
|
|
|
|
do {
|
|
|
|
if drmParams?.localSourceEncryptionKeyScheme != nil {
|
|
|
|
#if os(iOS)
|
|
|
|
try keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError()
|
|
|
|
return
|
|
|
|
#else
|
2024-09-28 08:39:09 -06:00
|
|
|
throw RCTVideoError.offlineDRMNotSupported
|
2024-09-20 09:46:10 -06:00
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
try await processContentKeyRequest(keyRequest: keyRequest)
|
|
|
|
} catch {
|
|
|
|
handleError(error, for: keyRequest)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func finishProcessingContentKeyRequest(keyRequest: AVContentKeyRequest, license: Data) throws {
|
|
|
|
let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: license)
|
|
|
|
keyRequest.processContentKeyResponse(keyResponse)
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleError(_ error: Error, for keyRequest: AVContentKeyRequest) {
|
|
|
|
let rctError: RCTVideoError
|
|
|
|
if let videoError = error as? RCTVideoError {
|
|
|
|
// handle RCTVideoError errors
|
|
|
|
rctError = videoError
|
|
|
|
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
self?.onVideoError?([
|
|
|
|
"error": RCTVideoErrorHandler.createError(from: rctError),
|
|
|
|
"target": self?.reactTag as Any,
|
|
|
|
])
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let err = error as NSError
|
|
|
|
|
|
|
|
// handle Other errors
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
self?.onVideoError?([
|
|
|
|
"error": [
|
|
|
|
"code": err.code,
|
|
|
|
"localizedDescription": err.localizedDescription,
|
|
|
|
"localizedFailureReason": err.localizedFailureReason ?? "",
|
|
|
|
"localizedRecoverySuggestion": err.localizedRecoverySuggestion ?? "",
|
|
|
|
"domain": err.domain,
|
|
|
|
],
|
|
|
|
"target": self?.reactTag as Any,
|
|
|
|
])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
keyRequest.processContentKeyResponseError(error)
|
|
|
|
contentKeySession?.expire()
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Private
|
|
|
|
|
|
|
|
private func processContentKeyRequest(keyRequest: AVContentKeyRequest) async throws {
|
|
|
|
guard let assetId = getAssetId(keyRequest: keyRequest),
|
|
|
|
let assetIdData = assetId.data(using: .utf8) else {
|
|
|
|
throw RCTVideoError.invalidContentId
|
|
|
|
}
|
|
|
|
|
|
|
|
let appCertificate = try await requestApplicationCertificate()
|
|
|
|
let spcData = try await keyRequest.makeStreamingContentKeyRequestData(forApp: appCertificate, contentIdentifier: assetIdData)
|
|
|
|
|
|
|
|
if onGetLicense != nil {
|
|
|
|
try await requestLicenseFromJS(spcData: spcData, assetId: assetId, keyRequest: keyRequest)
|
|
|
|
} else {
|
|
|
|
let license = try await requestLicense(spcData: spcData)
|
|
|
|
try finishProcessingContentKeyRequest(keyRequest: keyRequest, license: license)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func requestApplicationCertificate() async throws -> Data {
|
|
|
|
guard let urlString = drmParams?.certificateUrl,
|
|
|
|
let url = URL(string: urlString) else {
|
|
|
|
throw RCTVideoError.noCertificateURL
|
|
|
|
}
|
|
|
|
|
|
|
|
let (data, response) = try await URLSession.shared.data(from: url)
|
|
|
|
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse,
|
|
|
|
httpResponse.statusCode == 200 else {
|
|
|
|
throw RCTVideoError.noCertificateData
|
|
|
|
}
|
|
|
|
|
|
|
|
if drmParams?.base64Certificate == true {
|
|
|
|
guard let certData = Data(base64Encoded: data) else {
|
|
|
|
throw RCTVideoError.noCertificateData
|
|
|
|
}
|
|
|
|
return certData
|
|
|
|
}
|
|
|
|
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
private func requestLicense(spcData: Data) async throws -> Data {
|
|
|
|
guard let licenseServerUrlString = drmParams?.licenseServer,
|
|
|
|
let licenseServerUrl = URL(string: licenseServerUrlString) else {
|
|
|
|
throw RCTVideoError.noLicenseServerURL
|
|
|
|
}
|
|
|
|
|
|
|
|
var request = URLRequest(url: licenseServerUrl)
|
|
|
|
request.httpMethod = "POST"
|
|
|
|
request.httpBody = spcData
|
|
|
|
|
|
|
|
if let headers = drmParams?.headers {
|
|
|
|
for (key, value) in headers {
|
|
|
|
if let stringValue = value as? String {
|
|
|
|
request.setValue(stringValue, forHTTPHeaderField: key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
|
|
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
|
|
throw RCTVideoError.licenseRequestFailed(0)
|
|
|
|
}
|
|
|
|
|
|
|
|
guard httpResponse.statusCode == 200 else {
|
|
|
|
throw RCTVideoError.licenseRequestFailed(httpResponse.statusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
guard !data.isEmpty else {
|
|
|
|
throw RCTVideoError.noDataFromLicenseRequest
|
|
|
|
}
|
|
|
|
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
private func getAssetId(keyRequest: AVContentKeyRequest) -> String? {
|
|
|
|
if let assetId = drmParams?.contentId {
|
|
|
|
return assetId
|
|
|
|
}
|
|
|
|
|
|
|
|
if let url = keyRequest.identifier as? String {
|
|
|
|
return url.replacingOccurrences(of: "skd://", with: "")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|