react-native-video/ios/Video/Features/DRMManager.swift
Krzysztof Moch 0e4c95def9
feat(iOS): rewrite DRM Module (#4136)
* minimal api

* add suport for `getLicense`

* update logic for obtaining `assetId`

* add support for localSourceEncryptionKeyScheme

* fix typo

* fix pendingLicenses key bug

* lint code

* code clean

* code clean

* remove old files

* fix tvOS build

* fix errors loop

* move `localSourceEncryptionKeyScheme` into drm params

* add check for drm type

* use DebugLog

* lint

* update docs

* lint code

* fix bad rebase

* update docs

* fix crashes on simulators

* show error on simulator when using DRM

* fix typos

* code clean
2024-09-20 17:46:10 +02:00

214 lines
7.0 KiB
Swift

//
// 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
throw RCTVideoError.offlineDRMNotSuported
#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
}
}