From fd2f106d63e27dc068877add1af19512d2c8acf4 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Wed, 14 Aug 2024 13:28:25 +0200 Subject: [PATCH 01/22] minimal api --- ...MManager+AVContentKeySessionDelegate.swift | 49 ++++++++ ios/Video/Features/DRMManager.swift | 118 ++++++++++++++++++ .../Features/RCTVideoErrorHandling.swift | 11 ++ ios/Video/RCTVideo.swift | 23 ++-- 4 files changed, 188 insertions(+), 13 deletions(-) create mode 100644 ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift create mode 100644 ios/Video/Features/DRMManager.swift diff --git a/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift new file mode 100644 index 0000000000..955f56ef87 --- /dev/null +++ b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift @@ -0,0 +1,49 @@ +// +// DRMManager+AVContentKeySessionDelegate.swift +// react-native-video +// +// Created by Krzysztof Moch on 14/08/2024. +// + +import AVFoundation + +extension DRMManager: AVContentKeySessionDelegate { + + func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) { + handleContentKeyRequest(keyRequest: keyRequest) + } + + func contentKeySession(_ session: AVContentKeySession, didProvideRenewingContentKeyRequest keyRequest: AVContentKeyRequest) { + handleContentKeyRequest(keyRequest: keyRequest) + } + + func contentKeySession(_ session: AVContentKeySession, shouldRetry keyRequest: AVContentKeyRequest, reason retryReason: AVContentKeyRequest.RetryReason) -> Bool { + let reasons = [ + AVContentKeyRequest.RetryReason.timedOut, + AVContentKeyRequest.RetryReason.receivedResponseWithExpiredLease, + AVContentKeyRequest.RetryReason.receivedObsoleteContentKey + ] + + // Check if we should retry + return reasons.contains(where: { r in r == retryReason }) + } + + func contentKeySession(_ session: AVContentKeySession, contentKeyRequest keyRequest: AVContentKeyRequest, didFailWithError err: any Error) { + guard let onVideoError, let reactTag else { + return + } + + let error = err as NSError + + onVideoError([ + "error": [ + "code": NSNumber(value: error.code), + "localizedDescription": error.localizedDescription, + "localizedFailureReason": error.localizedFailureReason ?? "", + "localizedRecoverySuggestion": error.localizedRecoverySuggestion ?? "", + "domain": error.domain, + ], + "target": reactTag + ]) + } +} diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift new file mode 100644 index 0000000000..88bf99b07a --- /dev/null +++ b/ios/Video/Features/DRMManager.swift @@ -0,0 +1,118 @@ +// +// 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? + + override init() { + contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming) + super.init() + + contentKeySession.setDelegate(self, queue: DRMManager.queue) + } + + public func addAsset(asset: AVContentKeyRecipient, drmParams: DRMParams?, reactTag: NSNumber?, onVideoError: RCTDirectEventBlock?, onGetLicense: RCTDirectEventBlock?) { + self.reactTag = reactTag + self.onVideoError = onVideoError + self.onGetLicense = onGetLicense + self.drmParams = drmParams + + contentKeySession.addContentKeyRecipient(asset) + } + + func handleContentKeyRequest(keyRequest: AVContentKeyRequest) { + Task { + do { + try await processContentKeyRequest(keyRequest: keyRequest) + } catch { + keyRequest.processContentKeyResponseError(error) + } + } + } + + private func processContentKeyRequest(keyRequest: AVContentKeyRequest) async throws { + guard let assetId = drmParams?.contentId, let assetIdData = assetId.data(using: .utf8) else { + throw RCTVideoErrorHandler.invalidContentId + } + + let appCertificte = try await self.requestApplicationCertificate(keyRequest: keyRequest) + + let spcData = try await keyRequest.makeStreamingContentKeyRequestData(forApp: appCertificte, contentIdentifier: assetIdData) + let licence = try await self.requestLicence(spcData: spcData, keyRequest: keyRequest) + let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: licence) + keyRequest.processContentKeyResponse(keyResponse) + } + + private func requestApplicationCertificate(keyRequest: AVContentKeyRequest) async throws -> Data { + guard let urlString = drmParams?.certificateUrl, let url = URL(string: urlString) else { + throw RCTVideoErrorHandler.noCertificateURL + } + + let urlRequest = URLRequest(url: url) + let (data, response) = try await URLSession.shared.data(from: urlRequest) + + if let httpsResponse = response as? HTTPURLResponse { + if httpsResponse.statusCode != 200 { + throw RCTVideoErrorHandler.noCertificateData + } + } + + guard let certData = (drmParams?.base64Certificate != nil ? Data(base64Encoded: data) : data) else { + throw RCTVideoErrorHandler.noCertificateData + } + + return data + } + + private func requestLicence(spcData: Data, keyRequest: AVContentKeyRequest) async throws -> Data { + let licence: Data? = nil + + guard let licenceSeverUrlString = drmParams?.licenseServer else { + throw RCTVideoErrorHandler.noLicenseServerURL + } + + guard let licenceSeverUrl = URL(string: licenceSeverUrlString) else { + throw RCTVideoErrorHandler.noLicenseServerURL + } + + var urlRequest = URLRequest(url: licenceSeverUrl) + urlRequest.httpMethod = "POST" + + if let headers = drmParams?.headers { + for item in headers { + guard let value = item.value as? String else { + continue + } + urlRequest.setValue(value, forHTTPHeaderField: item.key) + } + } + + urlRequest.httpBody = spcData + + let (data, response) = try await URLSession.shared.data(from: urlRequest) + + if let httpsResponse = response as? HTTPURLResponse { + if httpsResponse.statusCode != 200 { + throw RCTVideoErrorHandler.licenseRequestNotOk(httpsResponse.statusCode) + } + } + + if data.isEmpty { + throw RCTVideoErrorHandler.noDataFromLicenseRequest + } + + return data + } +} diff --git a/ios/Video/Features/RCTVideoErrorHandling.swift b/ios/Video/Features/RCTVideoErrorHandling.swift index 7dc687839e..4b420bd34a 100644 --- a/ios/Video/Features/RCTVideoErrorHandling.swift +++ b/ios/Video/Features/RCTVideoErrorHandling.swift @@ -12,6 +12,7 @@ enum RCTVideoError: Int { case noFairplayDRM case noDRMData case invalidContentId + case invalidAppCert } // MARK: - RCTVideoErrorHandler @@ -111,4 +112,14 @@ enum RCTVideoErrorHandler { NSLocalizedRecoverySuggestionErrorKey: "Is the contentId and url ok?", ] ) + + static let invalidAppCert = NSError( + domain: "RCTVideo", + code: RCTVideoError.invalidAppCert.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining Application Certificate license.", + NSLocalizedFailureReasonErrorKey: "No valide Application Certificate received", + NSLocalizedRecoverySuggestionErrorKey: "Is the Application Certificate valid?", + ] + ) } diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 908e3eb881..3fbf5ee42e 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -97,7 +97,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH private var _didRequestAds = false private var _adPlaying = false - private var _resouceLoaderDelegate: RCTResourceLoaderDelegate? + private lazy var _drmManager: DRMManager? = DRMManager() private var _playerObserver: RCTPlayerObserver = .init() #if USE_VIDEO_CACHING @@ -459,14 +459,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH #endif if source.drm != nil || _localSourceEncryptionKeyScheme != nil { - _resouceLoaderDelegate = RCTResourceLoaderDelegate( - asset: asset, - drm: source.drm, - localSourceEncryptionKeyScheme: _localSourceEncryptionKeyScheme, - onVideoError: onVideoError, - onGetLicense: onGetLicense, - reactTag: reactTag - ) + if _drmManager == nil { + _drmManager = DRMManager() + } + + _drmManager?.addAsset(asset: asset, drmParams: source.drm, reactTag: reactTag, onVideoError: onVideoError, onGetLicense: onGetLicense) } return await playerItemPrepareText(source: source, asset: asset, assetOptions: assetOptions, uri: source.uri ?? "") @@ -562,7 +559,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } self.removePlayerLayer() self._playerObserver.player = nil - self._resouceLoaderDelegate = nil + self._drmManager = nil self._playerObserver.playerItem = nil // perform on next run loop, otherwise other passed react-props may not be set @@ -1295,7 +1292,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH ReactNativeVideoManager.shared.onInstanceRemoved(id: instanceId, player: _player as Any) _player = nil - _resouceLoaderDelegate = nil + _drmManager = nil _playerObserver.clearPlayer() self.removePlayerLayer() @@ -1329,11 +1326,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } func setLicenseResult(_ license: String!, _ licenseUrl: String!) { - _resouceLoaderDelegate?.setLicenseResult(license, licenseUrl) + // _resouceLoaderDelegate?.setLicenseResult(license, licenseUrl) } func setLicenseResultError(_ error: String!, _ licenseUrl: String!) { - _resouceLoaderDelegate?.setLicenseResultError(error, licenseUrl) + // _resouceLoaderDelegate?.setLicenseResultError(error, licenseUrl) } // MARK: - RCTPlayerObserverHandler From 689d8d649bcd13f9a3aa62b869cee4fbefe74208 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Wed, 14 Aug 2024 15:26:34 +0200 Subject: [PATCH 02/22] add suport for `getLicense` --- .../Features/DRMManager+OnGetLicense.swift | 75 +++++++++++++++++++ ios/Video/Features/DRMManager.swift | 21 ++++-- ios/Video/RCTVideo.swift | 6 +- 3 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 ios/Video/Features/DRMManager+OnGetLicense.swift diff --git a/ios/Video/Features/DRMManager+OnGetLicense.swift b/ios/Video/Features/DRMManager+OnGetLicense.swift new file mode 100644 index 0000000000..a7f0163cb0 --- /dev/null +++ b/ios/Video/Features/DRMManager+OnGetLicense.swift @@ -0,0 +1,75 @@ +// +// DRMManager+OnGetLicense.swift +// react-native-video +// +// Created by Krzysztof Moch on 14/08/2024. +// + +import AVFoundation + +extension DRMManager { + func requestLicneseFromJS(spcData: Data, assetId: String, keyRequest: AVContentKeyRequest) throws { + guard let onGetLicense else { + throw RCTVideoErrorHandler.noDataFromLicenseRequest + } + + guard let licenceSeverUrl = drmParams?.licenseServer, licenceSeverUrl.isEmpty else { + throw RCTVideoErrorHandler.noLicenseServerURL + } + + _pendingLicenses[licenceSeverUrl] = keyRequest + + onGetLicense([ + "licenseUrl": licenceSeverUrl, + "loadedLicenseUrl": keyRequest.identifier as Any, + "contetId": assetId, + "spcBase64": spcData.base64EncodedString(), + "target": reactTag as Any + ]) + } + + public func setJSLicneseResult(license: String, licenseUrl: String) { + // Check if the loading request exists in _loadingRequests based on licenseUrl + guard let keyContentRequest = _pendingLicenses[licenseUrl] else { + setJSLinceseError(error: "Loading request for licenseUrl \(licenseUrl) not found", licenseUrl: licenseUrl) + return + } + + guard let responseData = Data(base64Encoded: license) else { + setJSLinceseError(error: "No data from JS license response", licenseUrl: licenseUrl) + return + } + + do { + try finishProcessingContentKeyRequest(keyRequest: keyContentRequest, licence: responseData) + } catch { + keyContentRequest.processContentKeyResponseError(error) + } + + _pendingLicenses.removeValue(forKey: licenseUrl) + } + + public func setJSLinceseError(error: String, licenseUrl: String) { + if let onVideoError, let reactTag { + let err = RCTVideoErrorHandler.fromJSPart(error) + onVideoError([ + "error": [ + "code": NSNumber(value: err.code), + "localizedDescription": err.localizedDescription, + "localizedFailureReason": err.localizedFailureReason ?? "", + "localizedRecoverySuggestion": err.localizedRecoverySuggestion ?? "", + "domain": err.domain, + ], + "target": reactTag, + ]) + } + + // Check if the loading request exists in _loadingRequests based on licenseUrl + guard _pendingLicenses.contains(where: { url, _ in url == licenseUrl}) else { + print("Loading request for licenseUrl \(licenseUrl) not found") + return + } + + _pendingLicenses.removeValue(forKey: licenseUrl) + } +} diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index 88bf99b07a..f8675daff6 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -16,6 +16,9 @@ class DRMManager: NSObject { var onVideoError: RCTDirectEventBlock? var onGetLicense: RCTDirectEventBlock? + // Licneses handled by onGetLicense + var _pendingLicenses: [String: AVContentKeyRequest] = [:] + override init() { contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming) super.init() @@ -23,7 +26,7 @@ class DRMManager: NSObject { contentKeySession.setDelegate(self, queue: DRMManager.queue) } - public func addAsset(asset: AVContentKeyRecipient, drmParams: DRMParams?, reactTag: NSNumber?, onVideoError: RCTDirectEventBlock?, onGetLicense: RCTDirectEventBlock?) { + public func createContentKeyRequest(asset: AVContentKeyRecipient, drmParams: DRMParams?, reactTag: NSNumber?, onVideoError: RCTDirectEventBlock?, onGetLicense: RCTDirectEventBlock?) { self.reactTag = reactTag self.onVideoError = onVideoError self.onGetLicense = onGetLicense @@ -42,17 +45,25 @@ class DRMManager: NSObject { } } + func finishProcessingContentKeyRequest(keyRequest: AVContentKeyRequest, licence: Data) throws { + let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: licence) + keyRequest.processContentKeyResponse(keyResponse) + } + private func processContentKeyRequest(keyRequest: AVContentKeyRequest) async throws { guard let assetId = drmParams?.contentId, let assetIdData = assetId.data(using: .utf8) else { throw RCTVideoErrorHandler.invalidContentId } let appCertificte = try await self.requestApplicationCertificate(keyRequest: keyRequest) - let spcData = try await keyRequest.makeStreamingContentKeyRequestData(forApp: appCertificte, contentIdentifier: assetIdData) - let licence = try await self.requestLicence(spcData: spcData, keyRequest: keyRequest) - let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: licence) - keyRequest.processContentKeyResponse(keyResponse) + + if onGetLicense == nil { + let licence = try await self.requestLicence(spcData: spcData, keyRequest: keyRequest) + try finishProcessingContentKeyRequest(keyRequest: keyRequest, licence: licence) + } else { + try requestLicneseFromJS(spcData: spcData, assetId: assetId, keyRequest: keyRequest) + } } private func requestApplicationCertificate(keyRequest: AVContentKeyRequest) async throws -> Data { diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 3fbf5ee42e..8d458ffb86 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -463,7 +463,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _drmManager = DRMManager() } - _drmManager?.addAsset(asset: asset, drmParams: source.drm, reactTag: reactTag, onVideoError: onVideoError, onGetLicense: onGetLicense) + _drmManager?.createContentKeyRequest(asset: asset, drmParams: source.drm, reactTag: reactTag, onVideoError: onVideoError, onGetLicense: onGetLicense) } return await playerItemPrepareText(source: source, asset: asset, assetOptions: assetOptions, uri: source.uri ?? "") @@ -1326,11 +1326,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } func setLicenseResult(_ license: String!, _ licenseUrl: String!) { - // _resouceLoaderDelegate?.setLicenseResult(license, licenseUrl) + _drmManager?.setJSLicneseResult(license: license, licenseUrl: licenseUrl) } func setLicenseResultError(_ error: String!, _ licenseUrl: String!) { - // _resouceLoaderDelegate?.setLicenseResultError(error, licenseUrl) + _drmManager?.setJSLinceseError(error: error, licenseUrl: licenseUrl) } // MARK: - RCTPlayerObserverHandler From 346ece93f5ecca9fa839299ec497d60136c292e1 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Wed, 14 Aug 2024 15:52:22 +0200 Subject: [PATCH 03/22] update logic for obtaining `assetId` --- ios/Video/Features/DRMManager.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index f8675daff6..1723e1c653 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -51,7 +51,7 @@ class DRMManager: NSObject { } private func processContentKeyRequest(keyRequest: AVContentKeyRequest) async throws { - guard let assetId = drmParams?.contentId, let assetIdData = assetId.data(using: .utf8) else { + guard let assetId = getAssetId(keyRequest: keyRequest), let assetIdData = assetId.data(using: .utf8) else { throw RCTVideoErrorHandler.invalidContentId } @@ -126,4 +126,19 @@ class DRMManager: NSObject { return data } + + private func getAssetId(keyRequest: AVContentKeyRequest) -> String? { + guard let assetIdString = drmParams?.contentId else { + let url = keyRequest.identifier as? String + + if let url { + let assetId = url.replacingOccurrences(of: "skd://", with: "") + return assetId + } + + return nil + } + + return assetIdString + } } From 3117953123e4d663b30af680472bf15285de49a7 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Wed, 21 Aug 2024 09:48:39 +0200 Subject: [PATCH 04/22] add support for localSourceEncryptionKeyScheme --- ...MManager+AVContentKeySessionDelegate.swift | 4 ++ .../Features/DRMManager+Persitable.swift | 42 +++++++++++++++++++ ios/Video/Features/DRMManager.swift | 10 +++++ 3 files changed, 56 insertions(+) create mode 100644 ios/Video/Features/DRMManager+Persitable.swift diff --git a/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift index 955f56ef87..cf77092d24 100644 --- a/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift +++ b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift @@ -28,6 +28,10 @@ extension DRMManager: AVContentKeySessionDelegate { return reasons.contains(where: { r in r == retryReason }) } + func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVPersistableContentKeyRequest) { + handlePersistableKeyRequest(keyRequest: keyRequest) + } + func contentKeySession(_ session: AVContentKeySession, contentKeyRequest keyRequest: AVContentKeyRequest, didFailWithError err: any Error) { guard let onVideoError, let reactTag else { return diff --git a/ios/Video/Features/DRMManager+Persitable.swift b/ios/Video/Features/DRMManager+Persitable.swift new file mode 100644 index 0000000000..e26432b36a --- /dev/null +++ b/ios/Video/Features/DRMManager+Persitable.swift @@ -0,0 +1,42 @@ +// +// DRMManager+Persitable.swift +// react-native-video +// +// Created by Krzysztof Moch on 19/08/2024. +// + +import AVFoundation + +extension DRMManager { + func handlePersistableKeyRequest(keyRequest: AVPersistableContentKeyRequest) { + do { + if localSourceEncryptionKeyScheme != nil { + try handleEmbemedKey(keyRequest: keyRequest) + } + + // Offline DRM is not supported yet - if you need it please checkout below issue + // https://github.com/TheWidlarzGroup/react-native-video/issues/3539 + throw NSError() + } catch { + keyRequest.processContentKeyResponseError(error) + } + } + + func handleEmbemedKey(keyRequest: AVPersistableContentKeyRequest) throws { + guard let localSourceEncryptionKeyScheme else { + throw RCTVideoErrorHandler.noDRMData + } + + guard let uri = keyRequest.identifier as? String, let url = URL(string: uri) else { + throw RCTVideoErrorHandler.noDRMData + } + + guard let persistentKeyData = RCTVideoUtils.extractDataFromCustomSchemeUrl(from: url, scheme: localSourceEncryptionKeyScheme) else { + throw RCTVideoErrorHandler.noDataFromLicenseRequest + } + + let persistentKey = try keyRequest.persistableContentKey(fromKeyVendorResponse: persistentKeyData) + + try finishProcessingContentKeyRequest(keyRequest: keyRequest, licence: persistentKey) + } +} diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index 1723e1c653..ed745bc312 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -12,6 +12,7 @@ class DRMManager: NSObject { let contentKeySession: AVContentKeySession var drmParams: DRMParams? + var localSourceEncryptionKeyScheme: String? var reactTag: NSNumber? var onVideoError: RCTDirectEventBlock? var onGetLicense: RCTDirectEventBlock? @@ -38,6 +39,12 @@ class DRMManager: NSObject { func handleContentKeyRequest(keyRequest: AVContentKeyRequest) { Task { do { + // If localSourceEncryptionKeyScheme we will handle it in PersistableContentKeyRequest + if localSourceEncryptionKeyScheme != nil { + try keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError() + return + } + try await processContentKeyRequest(keyRequest: keyRequest) } catch { keyRequest.processContentKeyResponseError(error) @@ -59,9 +66,12 @@ class DRMManager: NSObject { let spcData = try await keyRequest.makeStreamingContentKeyRequestData(forApp: appCertificte, contentIdentifier: assetIdData) if onGetLicense == nil { + // try get licence on native part let licence = try await self.requestLicence(spcData: spcData, keyRequest: keyRequest) try finishProcessingContentKeyRequest(keyRequest: keyRequest, licence: licence) } else { + // try get licence from JS (method provided by user) + // We will set loading request, and after callback we will set result to keyRequest try requestLicneseFromJS(spcData: spcData, assetId: assetId, keyRequest: keyRequest) } } From b38be406faac80190dda63c75ca84fe5a195c4ca Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Sat, 31 Aug 2024 14:13:22 +0200 Subject: [PATCH 05/22] fix typo --- .../Features/DRMManager+OnGetLicense.swift | 16 +++++++------- .../Features/DRMManager+Persitable.swift | 2 +- ios/Video/Features/DRMManager.swift | 22 +++++++++---------- ios/Video/RCTVideo.swift | 4 ++-- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/ios/Video/Features/DRMManager+OnGetLicense.swift b/ios/Video/Features/DRMManager+OnGetLicense.swift index a7f0163cb0..c6ad1ca9df 100644 --- a/ios/Video/Features/DRMManager+OnGetLicense.swift +++ b/ios/Video/Features/DRMManager+OnGetLicense.swift @@ -13,14 +13,14 @@ extension DRMManager { throw RCTVideoErrorHandler.noDataFromLicenseRequest } - guard let licenceSeverUrl = drmParams?.licenseServer, licenceSeverUrl.isEmpty else { + guard let licenseSeverUrl = drmParams?.licenseServer, !licenseSeverUrl.isEmpty else { throw RCTVideoErrorHandler.noLicenseServerURL } - _pendingLicenses[licenceSeverUrl] = keyRequest + _pendingLicenses[licenseSeverUrl] = keyRequest onGetLicense([ - "licenseUrl": licenceSeverUrl, + "licenseUrl": licenseSeverUrl, "loadedLicenseUrl": keyRequest.identifier as Any, "contetId": assetId, "spcBase64": spcData.base64EncodedString(), @@ -28,20 +28,20 @@ extension DRMManager { ]) } - public func setJSLicneseResult(license: String, licenseUrl: String) { + public func setJSLicenseResult(license: String, licenseUrl: String) { // Check if the loading request exists in _loadingRequests based on licenseUrl guard let keyContentRequest = _pendingLicenses[licenseUrl] else { - setJSLinceseError(error: "Loading request for licenseUrl \(licenseUrl) not found", licenseUrl: licenseUrl) + setJSLicenseError(error: "Loading request for licenseUrl \(licenseUrl) not found", licenseUrl: licenseUrl) return } guard let responseData = Data(base64Encoded: license) else { - setJSLinceseError(error: "No data from JS license response", licenseUrl: licenseUrl) + setJSLicenseError(error: "No data from JS license response", licenseUrl: licenseUrl) return } do { - try finishProcessingContentKeyRequest(keyRequest: keyContentRequest, licence: responseData) + try finishProcessingContentKeyRequest(keyRequest: keyContentRequest, license: responseData) } catch { keyContentRequest.processContentKeyResponseError(error) } @@ -49,7 +49,7 @@ extension DRMManager { _pendingLicenses.removeValue(forKey: licenseUrl) } - public func setJSLinceseError(error: String, licenseUrl: String) { + public func setJSLicenseError(error: String, licenseUrl: String) { if let onVideoError, let reactTag { let err = RCTVideoErrorHandler.fromJSPart(error) onVideoError([ diff --git a/ios/Video/Features/DRMManager+Persitable.swift b/ios/Video/Features/DRMManager+Persitable.swift index e26432b36a..0cc9f2e49e 100644 --- a/ios/Video/Features/DRMManager+Persitable.swift +++ b/ios/Video/Features/DRMManager+Persitable.swift @@ -37,6 +37,6 @@ extension DRMManager { let persistentKey = try keyRequest.persistableContentKey(fromKeyVendorResponse: persistentKeyData) - try finishProcessingContentKeyRequest(keyRequest: keyRequest, licence: persistentKey) + try finishProcessingContentKeyRequest(keyRequest: keyRequest, license: persistentKey) } } diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index ed745bc312..e3d756f30a 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -52,8 +52,8 @@ class DRMManager: NSObject { } } - func finishProcessingContentKeyRequest(keyRequest: AVContentKeyRequest, licence: Data) throws { - let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: licence) + func finishProcessingContentKeyRequest(keyRequest: AVContentKeyRequest, license: Data) throws { + let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: license) keyRequest.processContentKeyResponse(keyResponse) } @@ -66,11 +66,11 @@ class DRMManager: NSObject { let spcData = try await keyRequest.makeStreamingContentKeyRequestData(forApp: appCertificte, contentIdentifier: assetIdData) if onGetLicense == nil { - // try get licence on native part - let licence = try await self.requestLicence(spcData: spcData, keyRequest: keyRequest) - try finishProcessingContentKeyRequest(keyRequest: keyRequest, licence: licence) + // try get license on native part + let license = try await self.requestlicense(spcData: spcData, keyRequest: keyRequest) + try finishProcessingContentKeyRequest(keyRequest: keyRequest, license: license) } else { - // try get licence from JS (method provided by user) + // try get license from JS (method provided by user) // We will set loading request, and after callback we will set result to keyRequest try requestLicneseFromJS(spcData: spcData, assetId: assetId, keyRequest: keyRequest) } @@ -97,18 +97,18 @@ class DRMManager: NSObject { return data } - private func requestLicence(spcData: Data, keyRequest: AVContentKeyRequest) async throws -> Data { - let licence: Data? = nil + private func requestlicense(spcData: Data, keyRequest: AVContentKeyRequest) async throws -> Data { + let license: Data? = nil - guard let licenceSeverUrlString = drmParams?.licenseServer else { + guard let licenseSeverUrlString = drmParams?.licenseServer else { throw RCTVideoErrorHandler.noLicenseServerURL } - guard let licenceSeverUrl = URL(string: licenceSeverUrlString) else { + guard let licenseSeverUrl = URL(string: licenseSeverUrlString) else { throw RCTVideoErrorHandler.noLicenseServerURL } - var urlRequest = URLRequest(url: licenceSeverUrl) + var urlRequest = URLRequest(url: licenseSeverUrl) urlRequest.httpMethod = "POST" if let headers = drmParams?.headers { diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 8d458ffb86..5694c10684 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -1326,11 +1326,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } func setLicenseResult(_ license: String!, _ licenseUrl: String!) { - _drmManager?.setJSLicneseResult(license: license, licenseUrl: licenseUrl) + _drmManager?.setJSLicenseResult(license: license, licenseUrl: licenseUrl) } func setLicenseResultError(_ error: String!, _ licenseUrl: String!) { - _drmManager?.setJSLinceseError(error: error, licenseUrl: licenseUrl) + _drmManager?.setJSLicenseError(error: error, licenseUrl: licenseUrl) } // MARK: - RCTPlayerObserverHandler From bbb96bb2c90726eefcc27e72800be1168c8c1edf Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Sat, 31 Aug 2024 14:45:03 +0200 Subject: [PATCH 06/22] fix pendingLicenses key bug --- ios/Video/Features/DRMManager+OnGetLicense.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ios/Video/Features/DRMManager+OnGetLicense.swift b/ios/Video/Features/DRMManager+OnGetLicense.swift index c6ad1ca9df..86b8048654 100644 --- a/ios/Video/Features/DRMManager+OnGetLicense.swift +++ b/ios/Video/Features/DRMManager+OnGetLicense.swift @@ -17,12 +17,14 @@ extension DRMManager { throw RCTVideoErrorHandler.noLicenseServerURL } - _pendingLicenses[licenseSeverUrl] = keyRequest - + guard let loadedLicenseUrl = keyRequest.identifier as? String else { + throw RCTVideoErrorHandler.invalidContentId + } + _pendingLicenses[loadedLicenseUrl] = keyRequest onGetLicense([ "licenseUrl": licenseSeverUrl, - "loadedLicenseUrl": keyRequest.identifier as Any, - "contetId": assetId, + "loadedLicenseUrl": loadedLicenseUrl, + "contentId": assetId, "spcBase64": spcData.base64EncodedString(), "target": reactTag as Any ]) From 75678c459d9991ec5cf38f59ce67481348764ad8 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Sat, 31 Aug 2024 14:45:12 +0200 Subject: [PATCH 07/22] lint code --- ...MManager+AVContentKeySessionDelegate.swift | 29 ++++---- .../Features/DRMManager+OnGetLicense.swift | 36 ++++----- .../Features/DRMManager+Persitable.swift | 12 +-- ios/Video/Features/DRMManager.swift | 74 ++++++++++--------- .../Features/RCTVideoErrorHandling.swift | 2 +- ios/Video/RCTVideo.swift | 10 ++- 6 files changed, 87 insertions(+), 76 deletions(-) diff --git a/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift index cf77092d24..06482e5451 100644 --- a/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift +++ b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift @@ -8,37 +8,36 @@ import AVFoundation extension DRMManager: AVContentKeySessionDelegate { - - func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) { + func contentKeySession(_: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) { handleContentKeyRequest(keyRequest: keyRequest) } - - func contentKeySession(_ session: AVContentKeySession, didProvideRenewingContentKeyRequest keyRequest: AVContentKeyRequest) { + + func contentKeySession(_: AVContentKeySession, didProvideRenewingContentKeyRequest keyRequest: AVContentKeyRequest) { handleContentKeyRequest(keyRequest: keyRequest) } - - func contentKeySession(_ session: AVContentKeySession, shouldRetry keyRequest: AVContentKeyRequest, reason retryReason: AVContentKeyRequest.RetryReason) -> Bool { + + func contentKeySession(_: AVContentKeySession, shouldRetry _: AVContentKeyRequest, reason retryReason: AVContentKeyRequest.RetryReason) -> Bool { let reasons = [ AVContentKeyRequest.RetryReason.timedOut, AVContentKeyRequest.RetryReason.receivedResponseWithExpiredLease, - AVContentKeyRequest.RetryReason.receivedObsoleteContentKey + AVContentKeyRequest.RetryReason.receivedObsoleteContentKey, ] - + // Check if we should retry return reasons.contains(where: { r in r == retryReason }) } - - func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVPersistableContentKeyRequest) { + + func contentKeySession(_: AVContentKeySession, didProvide keyRequest: AVPersistableContentKeyRequest) { handlePersistableKeyRequest(keyRequest: keyRequest) } - - func contentKeySession(_ session: AVContentKeySession, contentKeyRequest keyRequest: AVContentKeyRequest, didFailWithError err: any Error) { + + func contentKeySession(_: AVContentKeySession, contentKeyRequest _: AVContentKeyRequest, didFailWithError err: any Error) { guard let onVideoError, let reactTag else { return } - + let error = err as NSError - + onVideoError([ "error": [ "code": NSNumber(value: error.code), @@ -47,7 +46,7 @@ extension DRMManager: AVContentKeySessionDelegate { "localizedRecoverySuggestion": error.localizedRecoverySuggestion ?? "", "domain": error.domain, ], - "target": reactTag + "target": reactTag, ]) } } diff --git a/ios/Video/Features/DRMManager+OnGetLicense.swift b/ios/Video/Features/DRMManager+OnGetLicense.swift index 86b8048654..c8972bdfcd 100644 --- a/ios/Video/Features/DRMManager+OnGetLicense.swift +++ b/ios/Video/Features/DRMManager+OnGetLicense.swift @@ -7,51 +7,53 @@ import AVFoundation -extension DRMManager { - func requestLicneseFromJS(spcData: Data, assetId: String, keyRequest: AVContentKeyRequest) throws { +public extension DRMManager { + internal func requestLicneseFromJS(spcData: Data, assetId: String, keyRequest: AVContentKeyRequest) throws { guard let onGetLicense else { throw RCTVideoErrorHandler.noDataFromLicenseRequest } - + guard let licenseSeverUrl = drmParams?.licenseServer, !licenseSeverUrl.isEmpty else { throw RCTVideoErrorHandler.noLicenseServerURL } - + guard let loadedLicenseUrl = keyRequest.identifier as? String else { throw RCTVideoErrorHandler.invalidContentId } + _pendingLicenses[loadedLicenseUrl] = keyRequest + onGetLicense([ "licenseUrl": licenseSeverUrl, "loadedLicenseUrl": loadedLicenseUrl, "contentId": assetId, "spcBase64": spcData.base64EncodedString(), - "target": reactTag as Any + "target": reactTag as Any, ]) } - - public func setJSLicenseResult(license: String, licenseUrl: String) { + + func setJSLicenseResult(license: String, licenseUrl: String) { // Check if the loading request exists in _loadingRequests based on licenseUrl guard let keyContentRequest = _pendingLicenses[licenseUrl] else { setJSLicenseError(error: "Loading request for licenseUrl \(licenseUrl) not found", licenseUrl: licenseUrl) return } - + guard let responseData = Data(base64Encoded: license) else { setJSLicenseError(error: "No data from JS license response", licenseUrl: licenseUrl) return } - + do { try finishProcessingContentKeyRequest(keyRequest: keyContentRequest, license: responseData) } catch { keyContentRequest.processContentKeyResponseError(error) } - + _pendingLicenses.removeValue(forKey: licenseUrl) } - - public func setJSLicenseError(error: String, licenseUrl: String) { + + func setJSLicenseError(error: String, licenseUrl: String) { if let onVideoError, let reactTag { let err = RCTVideoErrorHandler.fromJSPart(error) onVideoError([ @@ -65,13 +67,13 @@ extension DRMManager { "target": reactTag, ]) } - + // Check if the loading request exists in _loadingRequests based on licenseUrl - guard _pendingLicenses.contains(where: { url, _ in url == licenseUrl}) else { - print("Loading request for licenseUrl \(licenseUrl) not found") - return + guard _pendingLicenses.contains(where: { url, _ in url == licenseUrl }) else { + print("Loading request for licenseUrl \(licenseUrl) not found") + return } - + _pendingLicenses.removeValue(forKey: licenseUrl) } } diff --git a/ios/Video/Features/DRMManager+Persitable.swift b/ios/Video/Features/DRMManager+Persitable.swift index 0cc9f2e49e..1b6a66f880 100644 --- a/ios/Video/Features/DRMManager+Persitable.swift +++ b/ios/Video/Features/DRMManager+Persitable.swift @@ -13,7 +13,7 @@ extension DRMManager { if localSourceEncryptionKeyScheme != nil { try handleEmbemedKey(keyRequest: keyRequest) } - + // Offline DRM is not supported yet - if you need it please checkout below issue // https://github.com/TheWidlarzGroup/react-native-video/issues/3539 throw NSError() @@ -21,22 +21,22 @@ extension DRMManager { keyRequest.processContentKeyResponseError(error) } } - + func handleEmbemedKey(keyRequest: AVPersistableContentKeyRequest) throws { guard let localSourceEncryptionKeyScheme else { throw RCTVideoErrorHandler.noDRMData } - + guard let uri = keyRequest.identifier as? String, let url = URL(string: uri) else { throw RCTVideoErrorHandler.noDRMData } - + guard let persistentKeyData = RCTVideoUtils.extractDataFromCustomSchemeUrl(from: url, scheme: localSourceEncryptionKeyScheme) else { throw RCTVideoErrorHandler.noDataFromLicenseRequest } - + let persistentKey = try keyRequest.persistableContentKey(fromKeyVendorResponse: persistentKeyData) - + try finishProcessingContentKeyRequest(keyRequest: keyRequest, license: persistentKey) } } diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index e3d756f30a..31e40cdc0a 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -10,32 +10,38 @@ import AVFoundation class DRMManager: NSObject { static let queue = DispatchQueue(label: "RNVideoContentKeyDelegateQueue") let contentKeySession: AVContentKeySession - + var drmParams: DRMParams? var localSourceEncryptionKeyScheme: String? var reactTag: NSNumber? var onVideoError: RCTDirectEventBlock? var onGetLicense: RCTDirectEventBlock? - + // Licneses handled by onGetLicense var _pendingLicenses: [String: AVContentKeyRequest] = [:] - + override init() { contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming) super.init() - + contentKeySession.setDelegate(self, queue: DRMManager.queue) } - - public func createContentKeyRequest(asset: AVContentKeyRecipient, drmParams: DRMParams?, reactTag: NSNumber?, onVideoError: RCTDirectEventBlock?, onGetLicense: RCTDirectEventBlock?) { + + public func createContentKeyRequest( + asset: AVContentKeyRecipient, + drmParams: DRMParams?, + reactTag: NSNumber?, + onVideoError: RCTDirectEventBlock?, + onGetLicense: RCTDirectEventBlock? + ) { self.reactTag = reactTag self.onVideoError = onVideoError self.onGetLicense = onGetLicense self.drmParams = drmParams - + contentKeySession.addContentKeyRecipient(asset) } - + func handleContentKeyRequest(keyRequest: AVContentKeyRequest) { Task { do { @@ -44,27 +50,27 @@ class DRMManager: NSObject { try keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError() return } - + try await processContentKeyRequest(keyRequest: keyRequest) } catch { keyRequest.processContentKeyResponseError(error) } } } - + func finishProcessingContentKeyRequest(keyRequest: AVContentKeyRequest, license: Data) throws { let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: license) keyRequest.processContentKeyResponse(keyResponse) } - + private func processContentKeyRequest(keyRequest: AVContentKeyRequest) async throws { guard let assetId = getAssetId(keyRequest: keyRequest), let assetIdData = assetId.data(using: .utf8) else { throw RCTVideoErrorHandler.invalidContentId } - + let appCertificte = try await self.requestApplicationCertificate(keyRequest: keyRequest) let spcData = try await keyRequest.makeStreamingContentKeyRequestData(forApp: appCertificte, contentIdentifier: assetIdData) - + if onGetLicense == nil { // try get license on native part let license = try await self.requestlicense(spcData: spcData, keyRequest: keyRequest) @@ -75,39 +81,37 @@ class DRMManager: NSObject { try requestLicneseFromJS(spcData: spcData, assetId: assetId, keyRequest: keyRequest) } } - - private func requestApplicationCertificate(keyRequest: AVContentKeyRequest) async throws -> Data { + + private func requestApplicationCertificate(keyRequest _: AVContentKeyRequest) async throws -> Data { guard let urlString = drmParams?.certificateUrl, let url = URL(string: urlString) else { throw RCTVideoErrorHandler.noCertificateURL } - + let urlRequest = URLRequest(url: url) let (data, response) = try await URLSession.shared.data(from: urlRequest) - + if let httpsResponse = response as? HTTPURLResponse { if httpsResponse.statusCode != 200 { throw RCTVideoErrorHandler.noCertificateData } } - + guard let certData = (drmParams?.base64Certificate != nil ? Data(base64Encoded: data) : data) else { throw RCTVideoErrorHandler.noCertificateData } - - return data + + return certData } - - private func requestlicense(spcData: Data, keyRequest: AVContentKeyRequest) async throws -> Data { - let license: Data? = nil - + + private func requestlicense(spcData: Data, keyRequest _: AVContentKeyRequest) async throws -> Data { guard let licenseSeverUrlString = drmParams?.licenseServer else { throw RCTVideoErrorHandler.noLicenseServerURL } - + guard let licenseSeverUrl = URL(string: licenseSeverUrlString) else { throw RCTVideoErrorHandler.noLicenseServerURL } - + var urlRequest = URLRequest(url: licenseSeverUrl) urlRequest.httpMethod = "POST" @@ -119,36 +123,36 @@ class DRMManager: NSObject { urlRequest.setValue(value, forHTTPHeaderField: item.key) } } - + urlRequest.httpBody = spcData - + let (data, response) = try await URLSession.shared.data(from: urlRequest) - + if let httpsResponse = response as? HTTPURLResponse { if httpsResponse.statusCode != 200 { throw RCTVideoErrorHandler.licenseRequestNotOk(httpsResponse.statusCode) } } - + if data.isEmpty { throw RCTVideoErrorHandler.noDataFromLicenseRequest } - + return data } - + private func getAssetId(keyRequest: AVContentKeyRequest) -> String? { guard let assetIdString = drmParams?.contentId else { let url = keyRequest.identifier as? String - + if let url { let assetId = url.replacingOccurrences(of: "skd://", with: "") return assetId } - + return nil } - + return assetIdString } } diff --git a/ios/Video/Features/RCTVideoErrorHandling.swift b/ios/Video/Features/RCTVideoErrorHandling.swift index 4b420bd34a..de506a881e 100644 --- a/ios/Video/Features/RCTVideoErrorHandling.swift +++ b/ios/Video/Features/RCTVideoErrorHandling.swift @@ -112,7 +112,7 @@ enum RCTVideoErrorHandler { NSLocalizedRecoverySuggestionErrorKey: "Is the contentId and url ok?", ] ) - + static let invalidAppCert = NSError( domain: "RCTVideo", code: RCTVideoError.invalidAppCert.rawValue, diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 5694c10684..5345fb296b 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -462,8 +462,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH if _drmManager == nil { _drmManager = DRMManager() } - - _drmManager?.createContentKeyRequest(asset: asset, drmParams: source.drm, reactTag: reactTag, onVideoError: onVideoError, onGetLicense: onGetLicense) + + _drmManager?.createContentKeyRequest( + asset: asset, + drmParams: source.drm, + reactTag: reactTag, + onVideoError: onVideoError, + onGetLicense: onGetLicense + ) } return await playerItemPrepareText(source: source, asset: asset, assetOptions: assetOptions, uri: source.uri ?? "") From 31b2ddda7e9ccc2bdd2f3f1b0db2784a96d2555e Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Mon, 2 Sep 2024 13:17:24 +0200 Subject: [PATCH 08/22] code clean --- ...MManager+AVContentKeySessionDelegate.swift | 39 +-- .../Features/DRMManager+OnGetLicense.swift | 65 ++--- .../Features/DRMManager+Persitable.swift | 32 +-- ios/Video/Features/DRMManager.swift | 146 ++++++---- .../Features/RCTVideoErrorHandling.swift | 253 +++++++++++------- 5 files changed, 290 insertions(+), 245 deletions(-) diff --git a/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift index 06482e5451..5d4cb2c84a 100644 --- a/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift +++ b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift @@ -17,36 +17,25 @@ extension DRMManager: AVContentKeySessionDelegate { } func contentKeySession(_: AVContentKeySession, shouldRetry _: AVContentKeyRequest, reason retryReason: AVContentKeyRequest.RetryReason) -> Bool { - let reasons = [ - AVContentKeyRequest.RetryReason.timedOut, - AVContentKeyRequest.RetryReason.receivedResponseWithExpiredLease, - AVContentKeyRequest.RetryReason.receivedObsoleteContentKey, + let retryReasons: [AVContentKeyRequest.RetryReason] = [ + .timedOut, + .receivedResponseWithExpiredLease, + .receivedObsoleteContentKey, ] - - // Check if we should retry - return reasons.contains(where: { r in r == retryReason }) + return retryReasons.contains(retryReason) } func contentKeySession(_: AVContentKeySession, didProvide keyRequest: AVPersistableContentKeyRequest) { - handlePersistableKeyRequest(keyRequest: keyRequest) - } - - func contentKeySession(_: AVContentKeySession, contentKeyRequest _: AVContentKeyRequest, didFailWithError err: any Error) { - guard let onVideoError, let reactTag else { - return + Task { + do { + try await handlePersistableKeyRequest(keyRequest: keyRequest) + } catch { + handleError(error, for: keyRequest) + } } + } - let error = err as NSError - - onVideoError([ - "error": [ - "code": NSNumber(value: error.code), - "localizedDescription": error.localizedDescription, - "localizedFailureReason": error.localizedFailureReason ?? "", - "localizedRecoverySuggestion": error.localizedRecoverySuggestion ?? "", - "domain": error.domain, - ], - "target": reactTag, - ]) + func contentKeySession(_: AVContentKeySession, contentKeyRequest keyRequest: AVContentKeyRequest, didFailWithError error: Error) { + handleError(error, for: keyRequest) } } diff --git a/ios/Video/Features/DRMManager+OnGetLicense.swift b/ios/Video/Features/DRMManager+OnGetLicense.swift index c8972bdfcd..90b1c38cb1 100644 --- a/ios/Video/Features/DRMManager+OnGetLicense.swift +++ b/ios/Video/Features/DRMManager+OnGetLicense.swift @@ -7,73 +7,62 @@ import AVFoundation -public extension DRMManager { - internal func requestLicneseFromJS(spcData: Data, assetId: String, keyRequest: AVContentKeyRequest) throws { +extension DRMManager { + func requestLicenseFromJS(spcData: Data, assetId: String, keyRequest: AVContentKeyRequest) async throws { guard let onGetLicense else { - throw RCTVideoErrorHandler.noDataFromLicenseRequest + throw RCTVideoError.noDataFromLicenseRequest } - guard let licenseSeverUrl = drmParams?.licenseServer, !licenseSeverUrl.isEmpty else { - throw RCTVideoErrorHandler.noLicenseServerURL + guard let licenseServerUrl = drmParams?.licenseServer, !licenseServerUrl.isEmpty else { + throw RCTVideoError.noLicenseServerURL } guard let loadedLicenseUrl = keyRequest.identifier as? String else { - throw RCTVideoErrorHandler.invalidContentId + throw RCTVideoError.invalidContentId } - _pendingLicenses[loadedLicenseUrl] = keyRequest + pendingLicenses[loadedLicenseUrl] = keyRequest - onGetLicense([ - "licenseUrl": licenseSeverUrl, - "loadedLicenseUrl": loadedLicenseUrl, - "contentId": assetId, - "spcBase64": spcData.base64EncodedString(), - "target": reactTag as Any, - ]) + DispatchQueue.main.async { [weak self] in + onGetLicense([ + "licenseUrl": licenseServerUrl, + "loadedLicenseUrl": loadedLicenseUrl, + "contentId": assetId, + "spcBase64": spcData.base64EncodedString(), + "target": self?.reactTag as Any, + ]) + } } func setJSLicenseResult(license: String, licenseUrl: String) { - // Check if the loading request exists in _loadingRequests based on licenseUrl - guard let keyContentRequest = _pendingLicenses[licenseUrl] else { + guard let keyContentRequest = pendingLicenses[licenseUrl] else { setJSLicenseError(error: "Loading request for licenseUrl \(licenseUrl) not found", licenseUrl: licenseUrl) return } guard let responseData = Data(base64Encoded: license) else { - setJSLicenseError(error: "No data from JS license response", licenseUrl: licenseUrl) + setJSLicenseError(error: "Invalid license data", licenseUrl: licenseUrl) return } do { try finishProcessingContentKeyRequest(keyRequest: keyContentRequest, license: responseData) + pendingLicenses.removeValue(forKey: licenseUrl) } catch { - keyContentRequest.processContentKeyResponseError(error) + handleError(error, for: keyContentRequest) } - - _pendingLicenses.removeValue(forKey: licenseUrl) } func setJSLicenseError(error: String, licenseUrl: String) { - if let onVideoError, let reactTag { - let err = RCTVideoErrorHandler.fromJSPart(error) - onVideoError([ - "error": [ - "code": NSNumber(value: err.code), - "localizedDescription": err.localizedDescription, - "localizedFailureReason": err.localizedFailureReason ?? "", - "localizedRecoverySuggestion": err.localizedRecoverySuggestion ?? "", - "domain": err.domain, - ], - "target": reactTag, - ]) - } + let rctError = RCTVideoError.fromJSPart(error) - // Check if the loading request exists in _loadingRequests based on licenseUrl - guard _pendingLicenses.contains(where: { url, _ in url == licenseUrl }) else { - print("Loading request for licenseUrl \(licenseUrl) not found") - return + DispatchQueue.main.async { [weak self] in + self?.onVideoError?([ + "error": RCTVideoErrorHandler.createError(from: rctError), + "target": self?.reactTag as Any, + ]) } - _pendingLicenses.removeValue(forKey: licenseUrl) + pendingLicenses.removeValue(forKey: licenseUrl) } } diff --git a/ios/Video/Features/DRMManager+Persitable.swift b/ios/Video/Features/DRMManager+Persitable.swift index 1b6a66f880..e94bd80e96 100644 --- a/ios/Video/Features/DRMManager+Persitable.swift +++ b/ios/Video/Features/DRMManager+Persitable.swift @@ -8,35 +8,27 @@ import AVFoundation extension DRMManager { - func handlePersistableKeyRequest(keyRequest: AVPersistableContentKeyRequest) { - do { - if localSourceEncryptionKeyScheme != nil { - try handleEmbemedKey(keyRequest: keyRequest) - } - - // Offline DRM is not supported yet - if you need it please checkout below issue + func handlePersistableKeyRequest(keyRequest: AVPersistableContentKeyRequest) async throws { + if let localSourceEncryptionKeyScheme { + try handleEmbeddedKey(keyRequest: keyRequest, scheme: localSourceEncryptionKeyScheme) + } else { + // Offline DRM is not supported yet - if you need it please check out the following issue: // https://github.com/TheWidlarzGroup/react-native-video/issues/3539 - throw NSError() - } catch { - keyRequest.processContentKeyResponseError(error) + throw RCTVideoError.offlineDRMNotSuported } } - func handleEmbemedKey(keyRequest: AVPersistableContentKeyRequest) throws { - guard let localSourceEncryptionKeyScheme else { - throw RCTVideoErrorHandler.noDRMData - } - - guard let uri = keyRequest.identifier as? String, let url = URL(string: uri) else { - throw RCTVideoErrorHandler.noDRMData + private func handleEmbeddedKey(keyRequest: AVPersistableContentKeyRequest, scheme: String) throws { + guard let uri = keyRequest.identifier as? String, + let url = URL(string: uri) else { + throw RCTVideoError.invalidContentId } - guard let persistentKeyData = RCTVideoUtils.extractDataFromCustomSchemeUrl(from: url, scheme: localSourceEncryptionKeyScheme) else { - throw RCTVideoErrorHandler.noDataFromLicenseRequest + guard let persistentKeyData = RCTVideoUtils.extractDataFromCustomSchemeUrl(from: url, scheme: scheme) else { + throw RCTVideoError.embeddedKeyExtractionFailed } let persistentKey = try keyRequest.persistableContentKey(fromKeyVendorResponse: persistentKeyData) - try finishProcessingContentKeyRequest(keyRequest: keyRequest, license: persistentKey) } } diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index 31e40cdc0a..8a435af450 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -17,8 +17,8 @@ class DRMManager: NSObject { var onVideoError: RCTDirectEventBlock? var onGetLicense: RCTDirectEventBlock? - // Licneses handled by onGetLicense - var _pendingLicenses: [String: AVContentKeyRequest] = [:] + // Licenses handled by onGetLicense (from JS side) + var pendingLicenses: [String: AVContentKeyRequest] = [:] override init() { contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming) @@ -27,7 +27,7 @@ class DRMManager: NSObject { contentKeySession.setDelegate(self, queue: DRMManager.queue) } - public func createContentKeyRequest( + func createContentKeyRequest( asset: AVContentKeyRecipient, drmParams: DRMParams?, reactTag: NSNumber?, @@ -42,10 +42,11 @@ class DRMManager: NSObject { contentKeySession.addContentKeyRecipient(asset) } + // MARK: - Internal + func handleContentKeyRequest(keyRequest: AVContentKeyRequest) { Task { do { - // If localSourceEncryptionKeyScheme we will handle it in PersistableContentKeyRequest if localSourceEncryptionKeyScheme != nil { try keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError() return @@ -53,7 +54,7 @@ class DRMManager: NSObject { try await processContentKeyRequest(keyRequest: keyRequest) } catch { - keyRequest.processContentKeyResponseError(error) + handleError(error, for: keyRequest) } } } @@ -63,96 +64,125 @@ class DRMManager: NSObject { 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) + } + + // MARK: - Private + private func processContentKeyRequest(keyRequest: AVContentKeyRequest) async throws { - guard let assetId = getAssetId(keyRequest: keyRequest), let assetIdData = assetId.data(using: .utf8) else { - throw RCTVideoErrorHandler.invalidContentId + guard let assetId = getAssetId(keyRequest: keyRequest), + let assetIdData = assetId.data(using: .utf8) else { + throw RCTVideoError.invalidContentId } - let appCertificte = try await self.requestApplicationCertificate(keyRequest: keyRequest) - let spcData = try await keyRequest.makeStreamingContentKeyRequestData(forApp: appCertificte, contentIdentifier: assetIdData) + let appCertificate = try await requestApplicationCertificate() + let spcData = try await keyRequest.makeStreamingContentKeyRequestData(forApp: appCertificate, contentIdentifier: assetIdData) - if onGetLicense == nil { - // try get license on native part - let license = try await self.requestlicense(spcData: spcData, keyRequest: keyRequest) - try finishProcessingContentKeyRequest(keyRequest: keyRequest, license: license) + if let onGetLicense { + try await requestLicenseFromJS(spcData: spcData, assetId: assetId, keyRequest: keyRequest) } else { - // try get license from JS (method provided by user) - // We will set loading request, and after callback we will set result to keyRequest - try requestLicneseFromJS(spcData: spcData, assetId: assetId, keyRequest: keyRequest) + let license = try await requestLicense(spcData: spcData) + try finishProcessingContentKeyRequest(keyRequest: keyRequest, license: license) } } - private func requestApplicationCertificate(keyRequest _: AVContentKeyRequest) async throws -> Data { - guard let urlString = drmParams?.certificateUrl, let url = URL(string: urlString) else { - throw RCTVideoErrorHandler.noCertificateURL + private func requestApplicationCertificate() async throws -> Data { + guard let urlString = drmParams?.certificateUrl, + let url = URL(string: urlString) else { + throw RCTVideoError.noCertificateURL } - let urlRequest = URLRequest(url: url) - let (data, response) = try await URLSession.shared.data(from: urlRequest) + let (data, response) = try await URLSession.shared.data(from: url) - if let httpsResponse = response as? HTTPURLResponse { - if httpsResponse.statusCode != 200 { - throw RCTVideoErrorHandler.noCertificateData - } + guard let httpResponse = response as? HTTPURLResponse, + httpResponse.statusCode == 200 else { + throw RCTVideoError.noCertificateData } - guard let certData = (drmParams?.base64Certificate != nil ? Data(base64Encoded: data) : data) else { - throw RCTVideoErrorHandler.noCertificateData + if drmParams?.base64Certificate == true { + guard let certData = Data(base64Encoded: data) else { + throw RCTVideoError.noCertificateData + } + return certData } - return certData + return data } - private func requestlicense(spcData: Data, keyRequest _: AVContentKeyRequest) async throws -> Data { - guard let licenseSeverUrlString = drmParams?.licenseServer else { - throw RCTVideoErrorHandler.noLicenseServerURL - } - - guard let licenseSeverUrl = URL(string: licenseSeverUrlString) else { - throw RCTVideoErrorHandler.noLicenseServerURL + private func requestLicense(spcData: Data) async throws -> Data { + guard let licenseServerUrlString = drmParams?.licenseServer, + let licenseServerUrl = URL(string: licenseServerUrlString) else { + throw RCTVideoError.noLicenseServerURL } - var urlRequest = URLRequest(url: licenseSeverUrl) - urlRequest.httpMethod = "POST" + var request = URLRequest(url: licenseServerUrl) + request.httpMethod = "POST" + request.httpBody = spcData if let headers = drmParams?.headers { - for item in headers { - guard let value = item.value as? String else { - continue + for (key, value) in headers { + if let stringValue = value as? String { + request.setValue(stringValue, forHTTPHeaderField: key) } - urlRequest.setValue(value, forHTTPHeaderField: item.key) } } - urlRequest.httpBody = spcData + let (data, response) = try await URLSession.shared.data(for: request) - let (data, response) = try await URLSession.shared.data(from: urlRequest) + guard let httpResponse = response as? HTTPURLResponse else { + throw RCTVideoError.licenseRequestFailed(0) + } - if let httpsResponse = response as? HTTPURLResponse { - if httpsResponse.statusCode != 200 { - throw RCTVideoErrorHandler.licenseRequestNotOk(httpsResponse.statusCode) - } + guard httpResponse.statusCode == 200 else { + throw RCTVideoError.licenseRequestFailed(httpResponse.statusCode) } - if data.isEmpty { - throw RCTVideoErrorHandler.noDataFromLicenseRequest + guard !data.isEmpty else { + throw RCTVideoError.noDataFromLicenseRequest } return data } private func getAssetId(keyRequest: AVContentKeyRequest) -> String? { - guard let assetIdString = drmParams?.contentId else { - let url = keyRequest.identifier as? String - - if let url { - let assetId = url.replacingOccurrences(of: "skd://", with: "") - return assetId - } + if let assetId = drmParams?.contentId { + return assetId + } - return nil + if let url = keyRequest.identifier as? String { + return url.replacingOccurrences(of: "skd://", with: "") } - return assetIdString + return nil } } diff --git a/ios/Video/Features/RCTVideoErrorHandling.swift b/ios/Video/Features/RCTVideoErrorHandling.swift index de506a881e..ec18351fe2 100644 --- a/ios/Video/Features/RCTVideoErrorHandling.swift +++ b/ios/Video/Features/RCTVideoErrorHandling.swift @@ -1,125 +1,170 @@ +import Foundation + // MARK: - RCTVideoError -enum RCTVideoError: Int { - case fromJSPart +enum RCTVideoError: Error, Hashable { + case fromJSPart(String) case noLicenseServerURL - case licenseRequestNotOk + case licenseRequestFailed(Int) case noDataFromLicenseRequest case noSPC - case noDataRequest case noCertificateData case noCertificateURL - case noFairplayDRM case noDRMData case invalidContentId case invalidAppCert -} - -// MARK: - RCTVideoErrorHandler - -enum RCTVideoErrorHandler { - static let noDRMData = NSError( - domain: "RCTVideo", - code: RCTVideoError.noDRMData.rawValue, - userInfo: [ - NSLocalizedDescriptionKey: "Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: "No drm object found.", - NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?", - ] - ) - - static let noCertificateURL = NSError( - domain: "RCTVideo", - code: RCTVideoError.noCertificateURL.rawValue, - userInfo: [ - NSLocalizedDescriptionKey: "Error obtaining DRM License.", - NSLocalizedFailureReasonErrorKey: "No certificate URL has been found.", - NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop certificateUrl?", - ] - ) - - static let noCertificateData = NSError( - domain: "RCTVideo", - code: RCTVideoError.noCertificateData.rawValue, - userInfo: [ - NSLocalizedDescriptionKey: "Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: "No certificate data obtained from the specificied url.", - NSLocalizedRecoverySuggestionErrorKey: "Have you specified a valid 'certificateUrl'?", - ] - ) + case keyRequestCreationFailed + case persistableKeyRequestFailed + case embeddedKeyExtractionFailed + case offlineDRMNotSuported - static let noSPC = NSError( - domain: "RCTVideo", - code: RCTVideoError.noSPC.rawValue, - userInfo: [ - NSLocalizedDescriptionKey: "Error obtaining license.", - NSLocalizedFailureReasonErrorKey: "No spc received.", - NSLocalizedRecoverySuggestionErrorKey: "Check your DRM config.", - ] - ) + var errorCode: Int { + switch self { + case .fromJSPart: + return 1000 + case .noLicenseServerURL: + return 1001 + case .licenseRequestFailed: + return 1002 + case .noDataFromLicenseRequest: + return 1003 + case .noSPC: + return 1004 + case .noCertificateData: + return 1005 + case .noCertificateURL: + return 1006 + case .noDRMData: + return 1007 + case .invalidContentId: + return 1008 + case .invalidAppCert: + return 1009 + case .keyRequestCreationFailed: + return 1010 + case .persistableKeyRequestFailed: + return 1011 + case .embeddedKeyExtractionFailed: + return 1012 + case .offlineDRMNotSuported: + return 1013 + } + } +} - static let noLicenseServerURL = NSError( - domain: "RCTVideo", - code: RCTVideoError.noLicenseServerURL.rawValue, - userInfo: [ - NSLocalizedDescriptionKey: "Error obtaining DRM License.", - NSLocalizedFailureReasonErrorKey: "No license server URL has been found.", - NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop licenseServer?", - ] - ) +// MARK: LocalizedError - static let noDataFromLicenseRequest = NSError( - domain: "RCTVideo", - code: RCTVideoError.noDataFromLicenseRequest.rawValue, - userInfo: [ - NSLocalizedDescriptionKey: "Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: "No data received from the license server.", - NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?", - ] - ) +extension RCTVideoError: LocalizedError { + var errorDescription: String? { + switch self { + case let .fromJSPart(error): + return NSLocalizedString("Error from JavaScript: \(error)", comment: "") + case .noLicenseServerURL: + return NSLocalizedString("No license server URL provided", comment: "") + case let .licenseRequestFailed(statusCode): + return NSLocalizedString("License request failed with status code: \(statusCode)", comment: "") + case .noDataFromLicenseRequest: + return NSLocalizedString("No data received from license server", comment: "") + case .noSPC: + return NSLocalizedString("Failed to create Server Playback Context (SPC)", comment: "") + case .noCertificateData: + return NSLocalizedString("No certificate data obtained", comment: "") + case .noCertificateURL: + return NSLocalizedString("No certificate URL provided", comment: "") + case .noDRMData: + return NSLocalizedString("No DRM data available", comment: "") + case .invalidContentId: + return NSLocalizedString("Invalid content ID", comment: "") + case .invalidAppCert: + return NSLocalizedString("Invalid application certificate", comment: "") + case .keyRequestCreationFailed: + return NSLocalizedString("Failed to create content key request", comment: "") + case .persistableKeyRequestFailed: + return NSLocalizedString("Failed to create persistable content key request", comment: "") + case .embeddedKeyExtractionFailed: + return NSLocalizedString("Failed to extract embedded key", comment: "") + case .offlineDRMNotSuported: + return NSLocalizedString("Offline DRM is not supported, see https://github.com/TheWidlarzGroup/react-native-video/issues/3539", comment: "") + } + } - static func licenseRequestNotOk(_ statusCode: Int) -> NSError { - return NSError( - domain: "RCTVideo", - code: RCTVideoError.licenseRequestNotOk.rawValue, - userInfo: [ - NSLocalizedDescriptionKey: "Error obtaining license.", - NSLocalizedFailureReasonErrorKey: String( - format: "License server responded with status code %li", - statusCode - ), - NSLocalizedRecoverySuggestionErrorKey: "Did you send the correct data to the license Server? Is the server ok?", - ] - ) + var failureReason: String? { + switch self { + case .fromJSPart: + return NSLocalizedString("An error occurred in the JavaScript part of the application.", comment: "") + case .noLicenseServerURL: + return NSLocalizedString("The license server URL is missing in the DRM configuration.", comment: "") + case .licenseRequestFailed: + return NSLocalizedString("The license server responded with an error status code.", comment: "") + case .noDataFromLicenseRequest: + return NSLocalizedString("The license server did not return any data.", comment: "") + case .noSPC: + return NSLocalizedString("Failed to generate the Server Playback Context (SPC) for the content.", comment: "") + case .noCertificateData: + return NSLocalizedString("Unable to retrieve certificate data from the specified URL.", comment: "") + case .noCertificateURL: + return NSLocalizedString("The certificate URL is missing in the DRM configuration.", comment: "") + case .noDRMData: + return NSLocalizedString("The required DRM data is not available or is invalid.", comment: "") + case .invalidContentId: + return NSLocalizedString("The content ID provided is not valid or recognized.", comment: "") + case .invalidAppCert: + return NSLocalizedString("The application certificate is invalid or not recognized.", comment: "") + case .keyRequestCreationFailed: + return NSLocalizedString("Unable to create a content key request for DRM.", comment: "") + case .persistableKeyRequestFailed: + return NSLocalizedString("Failed to create a persistable content key request for offline playback.", comment: "") + case .embeddedKeyExtractionFailed: + return NSLocalizedString("Unable to extract the embedded key from the custom scheme URL.", comment: "") + case .offlineDRMNotSuported: + return NSLocalizedString("You tried to use Offline DRM but it is not supported yet", comment: "") + } } - static func fromJSPart(_ error: String) -> NSError { - return NSError(domain: "RCTVideo", - code: RCTVideoError.fromJSPart.rawValue, - userInfo: [ - NSLocalizedDescriptionKey: error, - NSLocalizedFailureReasonErrorKey: error, - NSLocalizedRecoverySuggestionErrorKey: error, - ]) + var recoverySuggestion: String? { + switch self { + case .fromJSPart: + return NSLocalizedString("Check the JavaScript logs for more details and fix any issues in the JS code.", comment: "") + case .noLicenseServerURL: + return NSLocalizedString("Ensure that you have specified the 'licenseServer' property in the DRM configuration.", comment: "") + case .licenseRequestFailed: + return NSLocalizedString("Verify that the license server is functioning correctly and that you're sending the correct data.", comment: "") + case .noDataFromLicenseRequest: + return NSLocalizedString("Check if the license server is operational and responding with the expected data.", comment: "") + case .noSPC: + return NSLocalizedString("Verify that the content key request is properly configured and that the DRM setup is correct.", comment: "") + case .noCertificateData: + return NSLocalizedString("Check if the certificate URL is correct and accessible, and that it returns valid certificate data.", comment: "") + case .noCertificateURL: + return NSLocalizedString("Make sure you have specified the 'certificateUrl' property in the DRM configuration.", comment: "") + case .noDRMData: + return NSLocalizedString("Ensure that you have provided all necessary DRM-related data in the configuration.", comment: "") + case .invalidContentId: + return NSLocalizedString("Verify that the content ID is correct and matches the expected format for your DRM system.", comment: "") + case .invalidAppCert: + return NSLocalizedString("Check if the application certificate is valid and properly formatted for your DRM system.", comment: "") + case .keyRequestCreationFailed: + return NSLocalizedString("Review your DRM configuration and ensure all required parameters are correctly set.", comment: "") + case .persistableKeyRequestFailed: + return NSLocalizedString("Verify that offline playback is supported and properly configured for your content.", comment: "") + case .embeddedKeyExtractionFailed: + return NSLocalizedString("Check if the embedded key is present in the URL and the custom scheme is correctly implemented.", comment: "") + case .offlineDRMNotSuported: + return NSLocalizedString("Check if localSourceEncryptionKeyScheme is setted", comment: "") + } } +} - static let invalidContentId = NSError( - domain: "RCTVideo", - code: RCTVideoError.invalidContentId.rawValue, - userInfo: [ - NSLocalizedDescriptionKey: "Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: "No valide content Id received", - NSLocalizedRecoverySuggestionErrorKey: "Is the contentId and url ok?", - ] - ) +// MARK: - RCTVideoErrorHandler - static let invalidAppCert = NSError( - domain: "RCTVideo", - code: RCTVideoError.invalidAppCert.rawValue, - userInfo: [ - NSLocalizedDescriptionKey: "Error obtaining Application Certificate license.", - NSLocalizedFailureReasonErrorKey: "No valide Application Certificate received", - NSLocalizedRecoverySuggestionErrorKey: "Is the Application Certificate valid?", +enum RCTVideoErrorHandler { + static func createError(from error: RCTVideoError) -> [String: Any] { + return [ + "code": error.errorCode, + "localizedDescription": error.localizedDescription, + "localizedFailureReason": error.failureReason ?? "", + "localizedRecoverySuggestion": error.recoverySuggestion ?? "", + "domain": "RCTVideo", ] - ) + } } From cf74c1b7ca908d56564ca7ca5c4f8b093740a24f Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Mon, 2 Sep 2024 13:28:06 +0200 Subject: [PATCH 09/22] code clean --- ios/Video/Features/DRMManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index 8a435af450..d08098620d 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -108,7 +108,7 @@ class DRMManager: NSObject { let appCertificate = try await requestApplicationCertificate() let spcData = try await keyRequest.makeStreamingContentKeyRequestData(forApp: appCertificate, contentIdentifier: assetIdData) - if let onGetLicense { + if onGetLicense != nil { try await requestLicenseFromJS(spcData: spcData, assetId: assetId, keyRequest: keyRequest) } else { let license = try await requestLicense(spcData: spcData) From ad950f4ebae50e92c65925a4fb8732b4e8a9fe44 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Mon, 2 Sep 2024 15:38:31 +0200 Subject: [PATCH 10/22] remove old files --- .../Features/RCTResourceLoaderDelegate.swift | 186 ------------------ ios/Video/Features/RCTVideoDRM.swift | 161 --------------- 2 files changed, 347 deletions(-) delete mode 100644 ios/Video/Features/RCTResourceLoaderDelegate.swift delete mode 100644 ios/Video/Features/RCTVideoDRM.swift diff --git a/ios/Video/Features/RCTResourceLoaderDelegate.swift b/ios/Video/Features/RCTResourceLoaderDelegate.swift deleted file mode 100644 index f7ab10314a..0000000000 --- a/ios/Video/Features/RCTResourceLoaderDelegate.swift +++ /dev/null @@ -1,186 +0,0 @@ -import AVFoundation - -class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate { - private var _loadingRequests: [String: AVAssetResourceLoadingRequest?] = [:] - private var _requestingCertificate = false - private var _requestingCertificateErrored = false - private var _drm: DRMParams? - private var _localSourceEncryptionKeyScheme: String? - private var _reactTag: NSNumber? - private var _onVideoError: RCTDirectEventBlock? - private var _onGetLicense: RCTDirectEventBlock? - - init( - asset: AVURLAsset, - drm: DRMParams?, - localSourceEncryptionKeyScheme: String?, - onVideoError: RCTDirectEventBlock?, - onGetLicense: RCTDirectEventBlock?, - reactTag: NSNumber - ) { - super.init() - let queue = DispatchQueue(label: "assetQueue") - asset.resourceLoader.setDelegate(self, queue: queue) - _reactTag = reactTag - _onVideoError = onVideoError - _onGetLicense = onGetLicense - _drm = drm - _localSourceEncryptionKeyScheme = localSourceEncryptionKeyScheme - } - - deinit { - for request in _loadingRequests.values { - request?.finishLoading() - } - } - - func resourceLoader(_: AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest: AVAssetResourceRenewalRequest) -> Bool { - return loadingRequestHandling(renewalRequest) - } - - func resourceLoader(_: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { - return loadingRequestHandling(loadingRequest) - } - - func resourceLoader(_: AVAssetResourceLoader, didCancel _: AVAssetResourceLoadingRequest) { - RCTLog("didCancelLoadingRequest") - } - - func setLicenseResult(_ license: String!, _ licenseUrl: String!) { - // Check if the loading request exists in _loadingRequests based on licenseUrl - guard let loadingRequest = _loadingRequests[licenseUrl] else { - setLicenseResultError("Loading request for licenseUrl \(String(describing: licenseUrl)) not found", licenseUrl) - return - } - - // Check if the license data is valid - guard let respondData = RCTVideoUtils.base64DataFromBase64String(base64String: license) else { - setLicenseResultError("No data from JS license response", licenseUrl) - return - } - - let dataRequest: AVAssetResourceLoadingDataRequest! = loadingRequest?.dataRequest - dataRequest.respond(with: respondData) - loadingRequest!.finishLoading() - _loadingRequests.removeValue(forKey: licenseUrl) - } - - func setLicenseResultError(_ error: String!, _ licenseUrl: String!) { - // Check if the loading request exists in _loadingRequests based on licenseUrl - guard let loadingRequest = _loadingRequests[licenseUrl] else { - print("Loading request for licenseUrl \(licenseUrl) not found. Error: \(error)") - return - } - - self.finishLoadingWithError(error: RCTVideoErrorHandler.fromJSPart(error), licenseUrl: licenseUrl) - } - - func finishLoadingWithError(error: Error!, licenseUrl: String!) -> Bool { - // Check if the loading request exists in _loadingRequests based on licenseUrl - guard let loadingRequest = _loadingRequests[licenseUrl], let error = error as NSError? else { - // Handle the case where the loading request is not found or error is nil - return false - } - - loadingRequest!.finishLoading(with: error) - _loadingRequests.removeValue(forKey: licenseUrl) - _onVideoError?([ - "error": [ - "code": NSNumber(value: error.code), - "localizedDescription": error.localizedDescription, - "localizedFailureReason": error.localizedFailureReason ?? "", - "localizedRecoverySuggestion": error.localizedRecoverySuggestion ?? "", - "domain": error.domain, - ], - "target": _reactTag as Any, - ]) - - return false - } - - func loadingRequestHandling(_ loadingRequest: AVAssetResourceLoadingRequest!) -> Bool { - if handleEmbeddedKey(loadingRequest) { - return true - } - - if _drm != nil { - return handleDrm(loadingRequest) - } - - return false - } - - func handleEmbeddedKey(_ loadingRequest: AVAssetResourceLoadingRequest!) -> Bool { - guard let url = loadingRequest.request.url, - let _localSourceEncryptionKeyScheme, - let persistentKeyData = RCTVideoUtils.extractDataFromCustomSchemeUrl(from: url, scheme: _localSourceEncryptionKeyScheme) - else { - return false - } - - loadingRequest.contentInformationRequest?.contentType = AVStreamingKeyDeliveryPersistentContentKeyType - loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true - loadingRequest.contentInformationRequest?.contentLength = Int64(persistentKeyData.count) - loadingRequest.dataRequest?.respond(with: persistentKeyData) - loadingRequest.finishLoading() - - return true - } - - func handleDrm(_ loadingRequest: AVAssetResourceLoadingRequest!) -> Bool { - if _requestingCertificate { - return true - } else if _requestingCertificateErrored { - return false - } - - let requestKey: String = loadingRequest.request.url?.absoluteString ?? "" - - _loadingRequests[requestKey] = loadingRequest - - guard let _drm, let drmType = _drm.type, drmType == "fairplay" else { - return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData, licenseUrl: requestKey) - } - - Task { - do { - if _onGetLicense != nil { - let contentId = _drm.contentId ?? loadingRequest.request.url?.host - let spcData = try await RCTVideoDRM.handleWithOnGetLicense( - loadingRequest: loadingRequest, - contentId: contentId, - certificateUrl: _drm.certificateUrl, - base64Certificate: _drm.base64Certificate - ) - - self._requestingCertificate = true - self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? "", - "loadedLicenseUrl": loadingRequest.request.url?.absoluteString ?? "", - "contentId": contentId ?? "", - "spcBase64": spcData.base64EncodedString(options: []), - "target": self._reactTag as Any]) - } else { - let data = try await RCTVideoDRM.handleInternalGetLicense( - loadingRequest: loadingRequest, - contentId: _drm.contentId, - licenseServer: _drm.licenseServer, - certificateUrl: _drm.certificateUrl, - base64Certificate: _drm.base64Certificate, - headers: _drm.headers - ) - - guard let dataRequest = loadingRequest.dataRequest else { - throw RCTVideoErrorHandler.noCertificateData - } - dataRequest.respond(with: data) - loadingRequest.finishLoading() - } - } catch { - self.finishLoadingWithError(error: error, licenseUrl: requestKey) - self._requestingCertificateErrored = true - } - } - - return true - } -} diff --git a/ios/Video/Features/RCTVideoDRM.swift b/ios/Video/Features/RCTVideoDRM.swift deleted file mode 100644 index bc73d48df3..0000000000 --- a/ios/Video/Features/RCTVideoDRM.swift +++ /dev/null @@ -1,161 +0,0 @@ -import AVFoundation - -enum RCTVideoDRM { - static func fetchLicense( - licenseServer: String, - spcData: Data?, - contentId: String, - headers: [String: Any]? - ) async throws -> Data { - let request = createLicenseRequest(licenseServer: licenseServer, spcData: spcData, contentId: contentId, headers: headers) - - let (data, response) = try await URLSession.shared.data(from: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw RCTVideoErrorHandler.noDataFromLicenseRequest - } - - if httpResponse.statusCode != 200 { - print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)") - throw RCTVideoErrorHandler.licenseRequestNotOk(httpResponse.statusCode) - } - - guard let decodedData = Data(base64Encoded: data, options: []) else { - throw RCTVideoErrorHandler.noDataFromLicenseRequest - } - - return decodedData - } - - 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 { - 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 - ) throws -> Data { - #if os(visionOS) - // TODO: DRM is not supported yet on visionOS. See #3467 - throw NSError(domain: "DRM is not supported yet on visionOS", code: 0, userInfo: nil) - #else - guard let spcData = try? loadingRequest.streamingContentKeyRequestData( - forApp: certificateData, - contentIdentifier: contentIdData as Data, - options: nil - ) else { - throw RCTVideoErrorHandler.noSPC - } - - return spcData - #endif - } - - static func createCertificateData(certificateStringUrl: String?, base64Certificate: Bool?) throws -> Data { - guard let certificateStringUrl, - let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else { - throw RCTVideoErrorHandler.noCertificateURL - } - - var certificateData: Data? - do { - certificateData = try Data(contentsOf: certificateURL) - if base64Certificate != nil { - certificateData = Data(base64Encoded: certificateData! as Data, options: .ignoreUnknownCharacters) - } - } catch {} - - guard let certificateData else { - throw RCTVideoErrorHandler.noCertificateData - } - - return certificateData - } - - static func handleWithOnGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId: String?, certificateUrl: String?, - base64Certificate: Bool?) throws -> Data { - let contentIdData = contentId?.data(using: .utf8) - - let certificateData = try? RCTVideoDRM.createCertificateData(certificateStringUrl: certificateUrl, base64Certificate: base64Certificate) - - guard let contentIdData else { - throw RCTVideoError.invalidContentId as! Error - } - - guard let certificateData else { - throw RCTVideoError.noCertificateData as! Error - } - - return try RCTVideoDRM.fetchSpcData( - loadingRequest: loadingRequest, - certificateData: certificateData, - contentIdData: contentIdData - ) - } - - static func handleInternalGetLicense( - loadingRequest: AVAssetResourceLoadingRequest, - contentId: String?, - licenseServer: String?, - certificateUrl: String?, - base64Certificate: Bool?, - headers: [String: Any]? - ) async throws -> Data { - let url = loadingRequest.request.url - - let parsedContentId = contentId != nil && !contentId!.isEmpty ? contentId : nil - - guard let contentId = parsedContentId ?? url?.absoluteString.replacingOccurrences(of: "skd://", with: "") else { - throw RCTVideoError.invalidContentId as! Error - } - - let contentIdData = NSData(bytes: contentId.cString(using: String.Encoding.utf8), length: contentId.lengthOfBytes(using: String.Encoding.utf8)) as Data - let certificateData = try RCTVideoDRM.createCertificateData(certificateStringUrl: certificateUrl, base64Certificate: base64Certificate) - let spcData = try RCTVideoDRM.fetchSpcData( - loadingRequest: loadingRequest, - certificateData: certificateData, - contentIdData: contentIdData - ) - - guard let licenseServer else { - throw RCTVideoError.noLicenseServerURL as! Error - } - - return try await RCTVideoDRM.fetchLicense( - licenseServer: licenseServer, - spcData: spcData, - contentId: contentId, - headers: headers - ) - } -} From 7dcdb48d4f5882f1baab70ed72adda12462bbf8b Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Tue, 3 Sep 2024 16:35:29 +0200 Subject: [PATCH 11/22] fix tvOS build --- ios/Video/Features/DRMManager.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index d08098620d..bacd1a3873 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -48,8 +48,12 @@ class DRMManager: NSObject { Task { do { if localSourceEncryptionKeyScheme != nil { - try keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError() - return + #if os(iOS) + try keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError() + return + #else + throw RCTVideoError.offlineDRMNotSuported + #endif } try await processContentKeyRequest(keyRequest: keyRequest) From a7f2f28e6d7cfe2c2e9483f1e70d6ea434de00e3 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Wed, 4 Sep 2024 12:21:55 +0200 Subject: [PATCH 12/22] fix errors loop --- .../Features/DRMManager+AVContentKeySessionDelegate.swift | 4 ++-- ios/Video/Features/DRMManager.swift | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift index 5d4cb2c84a..c662b49b92 100644 --- a/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift +++ b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift @@ -35,7 +35,7 @@ extension DRMManager: AVContentKeySessionDelegate { } } - func contentKeySession(_: AVContentKeySession, contentKeyRequest keyRequest: AVContentKeyRequest, didFailWithError error: Error) { - handleError(error, for: keyRequest) + func contentKeySession(_: AVContentKeySession, contentKeyRequest _: AVContentKeyRequest, didFailWithError error: Error) { + print(error) } } diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index bacd1a3873..0a927dc58a 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -99,6 +99,7 @@ class DRMManager: NSObject { } keyRequest.processContentKeyResponseError(error) + contentKeySession.expire() } // MARK: - Private From 07836fd85e338537a5fa34ac6c3c88d1c831aa72 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Fri, 6 Sep 2024 11:50:15 +0200 Subject: [PATCH 13/22] move `localSourceEncryptionKeyScheme` into drm params --- ios/Video/DataStructures/DRMParams.swift | 3 +++ ios/Video/Features/DRMManager+Persitable.swift | 2 +- ios/Video/Features/DRMManager.swift | 3 +-- ios/Video/RCTVideo.swift | 7 +++---- src/Video.tsx | 6 +++++- src/specs/VideoNativeComponent.ts | 2 +- src/types/video.ts | 2 ++ 7 files changed, 16 insertions(+), 9 deletions(-) diff --git a/ios/Video/DataStructures/DRMParams.swift b/ios/Video/DataStructures/DRMParams.swift index ce91d4dc34..bf8a4d2a49 100644 --- a/ios/Video/DataStructures/DRMParams.swift +++ b/ios/Video/DataStructures/DRMParams.swift @@ -5,6 +5,7 @@ struct DRMParams { let contentId: String? let certificateUrl: String? let base64Certificate: Bool? + let localSourceEncryptionKeyScheme: String? let json: NSDictionary? @@ -17,6 +18,7 @@ struct DRMParams { self.certificateUrl = nil self.base64Certificate = nil self.headers = nil + self.localSourceEncryptionKeyScheme = nil return } self.json = json @@ -36,5 +38,6 @@ struct DRMParams { } else { self.headers = nil } + localSourceEncryptionKeyScheme = json["localSourceEncryptionKeyScheme"] as? String } } diff --git a/ios/Video/Features/DRMManager+Persitable.swift b/ios/Video/Features/DRMManager+Persitable.swift index e94bd80e96..bd4480791e 100644 --- a/ios/Video/Features/DRMManager+Persitable.swift +++ b/ios/Video/Features/DRMManager+Persitable.swift @@ -9,7 +9,7 @@ import AVFoundation extension DRMManager { func handlePersistableKeyRequest(keyRequest: AVPersistableContentKeyRequest) async throws { - if let localSourceEncryptionKeyScheme { + if let localSourceEncryptionKeyScheme = drmParams?.localSourceEncryptionKeyScheme { try handleEmbeddedKey(keyRequest: keyRequest, scheme: localSourceEncryptionKeyScheme) } else { // Offline DRM is not supported yet - if you need it please check out the following issue: diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index 0a927dc58a..6a3e98ba2c 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -12,7 +12,6 @@ class DRMManager: NSObject { let contentKeySession: AVContentKeySession var drmParams: DRMParams? - var localSourceEncryptionKeyScheme: String? var reactTag: NSNumber? var onVideoError: RCTDirectEventBlock? var onGetLicense: RCTDirectEventBlock? @@ -47,7 +46,7 @@ class DRMManager: NSObject { func handleContentKeyRequest(keyRequest: AVContentKeyRequest) { Task { do { - if localSourceEncryptionKeyScheme != nil { + if drmParams?.localSourceEncryptionKeyScheme != nil { #if os(iOS) try keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError() return diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 5345fb296b..45d8e0d775 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -17,7 +17,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH private var _playerViewController: RCTVideoPlayerViewController? private var _videoURL: NSURL? - private var _localSourceEncryptionKeyScheme: String? /* Required to publish events */ private var _eventDispatcher: RCTEventDispatcher? @@ -458,7 +457,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } #endif - if source.drm != nil || _localSourceEncryptionKeyScheme != nil { + if source.drm != nil { if _drmManager == nil { _drmManager = DRMManager() } @@ -602,8 +601,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _localSourceEncryptionKeyScheme = keyScheme } - func playerItemPrepareText(source: VideoSource, asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem { - if source.textTracks.isEmpty != true || uri.hasSuffix(".m3u8") { + func playerItemPrepareText(asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem { + if self._textTracks.isEmpty == true || (uri.hasSuffix(".m3u8")) { return await self.playerItemPropegateMetadata(AVPlayerItem(asset: asset)) } diff --git a/src/Video.tsx b/src/Video.tsx index c1f251a187..afe39f49f5 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -119,6 +119,7 @@ const Video = forwardRef( onTextTrackDataChanged, onVideoTracks, onAspectRatio, + localSourceEncryptionKeyScheme, ...rest }, ref, @@ -189,6 +190,9 @@ const Video = forwardRef( base64Certificate: selectedDrm.base64Certificate, useExternalGetLicense: !!selectedDrm.getLicense, multiDrm: selectedDrm.multiDrm, + localSourceEncryptionKeyScheme: + selectedDrm.localSourceEncryptionKeyScheme || + localSourceEncryptionKeyScheme, }; let _cmcd: NativeCmcdConfiguration | undefined; @@ -238,7 +242,7 @@ const Video = forwardRef( textTracksAllowChunklessPreparation: resolvedSource.textTracksAllowChunklessPreparation, }; - }, [drm, source, textTracks, contentStartTime]); + }, [drm, source, textTracks, contentStartTime, localSourceEncryptionKeyScheme]); const _selectedTextTrack = useMemo(() => { if (!selectedTextTrack) { diff --git a/src/specs/VideoNativeComponent.ts b/src/specs/VideoNativeComponent.ts index f210cfccfb..34a50438ad 100644 --- a/src/specs/VideoNativeComponent.ts +++ b/src/specs/VideoNativeComponent.ts @@ -62,6 +62,7 @@ type Drm = Readonly<{ base64Certificate?: boolean; // ios default: false useExternalGetLicense?: boolean; // ios multiDrm?: WithDefault; // android + localSourceEncryptionKeyScheme?: string; // ios }>; type CmcdMode = WithDefault; @@ -341,7 +342,6 @@ export interface VideoNativeProps extends ViewProps { fullscreenOrientation?: WithDefault; progressUpdateInterval?: Float; restoreUserInterfaceForPIPStopCompletionHandler?: boolean; - localSourceEncryptionKeyScheme?: string; debug?: DebugConfig; showNotificationControls?: WithDefault; // Android, iOS bufferConfig?: BufferConfig; // Android diff --git a/src/types/video.ts b/src/types/video.ts index 71304080d6..9e25e25ea5 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -81,6 +81,7 @@ export type Drm = Readonly<{ certificateUrl?: string; // ios base64Certificate?: boolean; // ios default: false multiDrm?: boolean; // android + localSourceEncryptionKeyScheme?: string; // ios /* eslint-disable @typescript-eslint/no-unused-vars */ getLicense?: ( spcBase64: string, @@ -321,6 +322,7 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps { /** @deprecated Use viewType*/ useSecureView?: boolean; // Android volume?: number; + /** @deprecated use **localSourceEncryptionKeyScheme** key in **drm** props instead */ localSourceEncryptionKeyScheme?: string; debug?: DebugConfig; allowsExternalPlayback?: boolean; // iOS From a7957e49b0236524cfccb3adb2e1724fd614b4de Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Fri, 6 Sep 2024 11:51:11 +0200 Subject: [PATCH 14/22] add check for drm type --- ios/Video/Features/DRMManager.swift | 10 +++++++++- ios/Video/Features/RCTVideoErrorHandling.swift | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index 6a3e98ba2c..cad95d17e9 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -37,7 +37,15 @@ class DRMManager: NSObject { 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 + } + contentKeySession.addContentKeyRecipient(asset) } diff --git a/ios/Video/Features/RCTVideoErrorHandling.swift b/ios/Video/Features/RCTVideoErrorHandling.swift index ec18351fe2..db6078307f 100644 --- a/ios/Video/Features/RCTVideoErrorHandling.swift +++ b/ios/Video/Features/RCTVideoErrorHandling.swift @@ -17,6 +17,7 @@ enum RCTVideoError: Error, Hashable { case persistableKeyRequestFailed case embeddedKeyExtractionFailed case offlineDRMNotSuported + case unsupportedDRMType var errorCode: Int { switch self { @@ -48,6 +49,8 @@ enum RCTVideoError: Error, Hashable { return 1012 case .offlineDRMNotSuported: return 1013 + case .unsupportedDRMType: + return 1014 } } } @@ -85,6 +88,8 @@ extension RCTVideoError: LocalizedError { return NSLocalizedString("Failed to extract embedded key", comment: "") case .offlineDRMNotSuported: return NSLocalizedString("Offline DRM is not supported, see https://github.com/TheWidlarzGroup/react-native-video/issues/3539", comment: "") + case .unsupportedDRMType: + return NSLocalizedString("Unsupporeted DRM type", comment: "") } } @@ -118,6 +123,8 @@ extension RCTVideoError: LocalizedError { return NSLocalizedString("Unable to extract the embedded key from the custom scheme URL.", comment: "") case .offlineDRMNotSuported: return NSLocalizedString("You tried to use Offline DRM but it is not supported yet", comment: "") + case .unsupportedDRMType: + return NSLocalizedString("You tried to use unsupporeted DRM type", comment: "") } } @@ -151,6 +158,8 @@ extension RCTVideoError: LocalizedError { return NSLocalizedString("Check if the embedded key is present in the URL and the custom scheme is correctly implemented.", comment: "") case .offlineDRMNotSuported: return NSLocalizedString("Check if localSourceEncryptionKeyScheme is setted", comment: "") + case .unsupportedDRMType: + return NSLocalizedString("Verifiy that you are using fairplay (on Apple devices)", comment: "") } } } From f74e2a63bec50274d3eeabeef837988f87e9de7c Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Fri, 6 Sep 2024 11:51:19 +0200 Subject: [PATCH 15/22] use DebugLog --- ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift index c662b49b92..f8d31fd017 100644 --- a/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift +++ b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift @@ -36,6 +36,6 @@ extension DRMManager: AVContentKeySessionDelegate { } func contentKeySession(_: AVContentKeySession, contentKeyRequest _: AVContentKeyRequest, didFailWithError error: Error) { - print(error) + DebugLog(String(describing: error)) } } From b915f1353d7aa3f9a7c972364f39155b981808d4 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Fri, 6 Sep 2024 11:51:38 +0200 Subject: [PATCH 16/22] lint --- ios/Video/Features/DRMManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index cad95d17e9..782a9a404f 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -37,7 +37,7 @@ class DRMManager: NSObject { self.onVideoError = onVideoError self.onGetLicense = onGetLicense self.drmParams = drmParams - + if drmParams?.type != "fairplay" { self.onVideoError?([ "error": RCTVideoErrorHandler.createError(from: RCTVideoError.unsupportedDRMType), @@ -45,7 +45,7 @@ class DRMManager: NSObject { ]) return } - + contentKeySession.addContentKeyRecipient(asset) } From 059b09ac72c2ea9188a111c888c2d85b33854d06 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Fri, 6 Sep 2024 15:00:39 +0200 Subject: [PATCH 17/22] update docs --- docs/pages/component/props.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/pages/component/props.mdx b/docs/pages/component/props.mdx index 2129886814..b1f3f3f36a 100644 --- a/docs/pages/component/props.mdx +++ b/docs/pages/component/props.mdx @@ -343,6 +343,8 @@ Controls the iOS silent switch behavior +**This is deprecated, prop moved to `drm`** + Set the url scheme for stream encryption key for local assets Type: String @@ -789,7 +791,7 @@ The following other types are supported on some platforms, but aren't fully docu #### Using DRM content - + To setup DRM please follow [this guide](/component/drm) @@ -807,8 +809,6 @@ Example: }, ``` -> ⚠️ DRM is not supported on visionOS yet - #### Start playback at a specific point in time From 2dedcd2bd91f27de4bd5960a2e1f49daea6325c3 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Wed, 18 Sep 2024 08:00:14 +0200 Subject: [PATCH 18/22] lint code --- src/Video.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Video.tsx b/src/Video.tsx index afe39f49f5..8f8dd6c04b 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -242,7 +242,13 @@ const Video = forwardRef( textTracksAllowChunklessPreparation: resolvedSource.textTracksAllowChunklessPreparation, }; - }, [drm, source, textTracks, contentStartTime, localSourceEncryptionKeyScheme]); + }, [ + drm, + source, + textTracks, + contentStartTime, + localSourceEncryptionKeyScheme, + ]); const _selectedTextTrack = useMemo(() => { if (!selectedTextTrack) { From e41a1788db58387eca6f6226259324d568a7aa5f Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Wed, 18 Sep 2024 10:34:30 +0200 Subject: [PATCH 19/22] fix bad rebase --- ios/Video/RCTVideo.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 45d8e0d775..8bbaa2e8f3 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -596,13 +596,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH DispatchQueue.global(qos: .default).async(execute: initializeSource) } - @objc - func setLocalSourceEncryptionKeyScheme(_ keyScheme: String) { - _localSourceEncryptionKeyScheme = keyScheme - } - - func playerItemPrepareText(asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem { - if self._textTracks.isEmpty == true || (uri.hasSuffix(".m3u8")) { + func playerItemPrepareText(source: VideoSource, asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem { + if source.textTracks.isEmpty == true || uri.hasSuffix(".m3u8") { return await self.playerItemPropegateMetadata(AVPlayerItem(asset: asset)) } From c1afb61acce1b3f3e143f4f5a6b5fa82d5a29884 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Wed, 18 Sep 2024 11:52:28 +0200 Subject: [PATCH 20/22] update docs --- docs/pages/component/drm.mdx | 14 ++++++++++++++ docs/pages/component/props.mdx | 15 --------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/pages/component/drm.mdx b/docs/pages/component/drm.mdx index 6e7fa0aaf3..da9074d5ea 100644 --- a/docs/pages/component/drm.mdx +++ b/docs/pages/component/drm.mdx @@ -137,6 +137,20 @@ You can specify the DRM type, either by string or using the exported DRMType enu Valid values are, for Android: DRMType.WIDEVINE / DRMType.PLAYREADY / DRMType.CLEARKEY. for iOS: DRMType.FAIRPLAY +### `localSourceEncryptionKeyScheme` + + + +Set the url scheme for stream encryption key for local assets + +Type: String + +Example: + +``` +localSourceEncryptionKeyScheme="my-offline-key" +``` + ## Common Usage Scenarios ### Send cookies to license server diff --git a/docs/pages/component/props.mdx b/docs/pages/component/props.mdx index b1f3f3f36a..f976dfbc99 100644 --- a/docs/pages/component/props.mdx +++ b/docs/pages/component/props.mdx @@ -339,21 +339,6 @@ Controls the iOS silent switch behavior - **"ignore"** - Play audio even if the silent switch is set - **"obey"** - Don't play audio if the silent switch is set -### `localSourceEncryptionKeyScheme` - - - -**This is deprecated, prop moved to `drm`** - -Set the url scheme for stream encryption key for local assets - -Type: String - -Example: - -``` -localSourceEncryptionKeyScheme="my-offline-key" -``` ### `maxBitRate` From c60756c8baf825ee8e886ba67965dad69d318c86 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Wed, 18 Sep 2024 17:35:34 +0200 Subject: [PATCH 21/22] fix crashes on simulators --- ios/Video/RCTVideo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 8bbaa2e8f3..3d1da714ca 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -457,7 +457,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } #endif - if source.drm != nil { + if source.drm?.type != nil { if _drmManager == nil { _drmManager = DRMManager() } From 85a027ca1da4ce118fa56914043dabbb352d34a5 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Wed, 18 Sep 2024 22:05:51 +0200 Subject: [PATCH 22/22] show error on simulator when using DRM --- ios/Video/DataStructures/VideoSource.swift | 4 +-- ios/Video/Features/DRMManager.swift | 27 ++++++++++++++----- .../Features/RCTVideoErrorHandling.swift | 9 +++++++ ios/Video/RCTVideo.swift | 4 +-- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/ios/Video/DataStructures/VideoSource.swift b/ios/Video/DataStructures/VideoSource.swift index e672929fe7..45e5b2d300 100644 --- a/ios/Video/DataStructures/VideoSource.swift +++ b/ios/Video/DataStructures/VideoSource.swift @@ -10,7 +10,7 @@ struct VideoSource { let cropEnd: Int64? let customMetadata: CustomMetadata? /* DRM */ - let drm: DRMParams? + let drm: DRMParams var textTracks: [TextTrack] = [] let json: NSDictionary? @@ -28,7 +28,7 @@ struct VideoSource { self.cropStart = nil self.cropEnd = nil self.customMetadata = nil - self.drm = nil + self.drm = DRMParams(nil) return } self.json = json diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift index 782a9a404f..8978fd5885 100644 --- a/ios/Video/Features/DRMManager.swift +++ b/ios/Video/Features/DRMManager.swift @@ -9,7 +9,7 @@ import AVFoundation class DRMManager: NSObject { static let queue = DispatchQueue(label: "RNVideoContentKeyDelegateQueue") - let contentKeySession: AVContentKeySession + let contentKeySession: AVContentKeySession? var drmParams: DRMParams? var reactTag: NSNumber? @@ -20,10 +20,15 @@ class DRMManager: NSObject { var pendingLicenses: [String: AVContentKeyRequest] = [:] override init() { - contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming) - super.init() - - contentKeySession.setDelegate(self, queue: DRMManager.queue) + #if targetEnvironment(simulator) + contentKeySession = nil + super.init() + #else + contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming) + super.init() + + contentKeySession?.setDelegate(self, queue: DRMManager.queue) + #endif } func createContentKeyRequest( @@ -46,7 +51,15 @@ class DRMManager: NSObject { return } - contentKeySession.addContentKeyRecipient(asset) + #if targetEnvironment(simulator) + DebugLog("Simulator is not supported for FairPlay DRM.") + self.onVideoError?([ + "error": RCTVideoErrorHandler.createError(from: RCTVideoError.simulatorDRMNotSuported), + "target": self.reactTag as Any, + ]) + #endif + + contentKeySession?.addContentKeyRecipient(asset) } // MARK: - Internal @@ -106,7 +119,7 @@ class DRMManager: NSObject { } keyRequest.processContentKeyResponseError(error) - contentKeySession.expire() + contentKeySession?.expire() } // MARK: - Private diff --git a/ios/Video/Features/RCTVideoErrorHandling.swift b/ios/Video/Features/RCTVideoErrorHandling.swift index db6078307f..7e446c04db 100644 --- a/ios/Video/Features/RCTVideoErrorHandling.swift +++ b/ios/Video/Features/RCTVideoErrorHandling.swift @@ -18,6 +18,7 @@ enum RCTVideoError: Error, Hashable { case embeddedKeyExtractionFailed case offlineDRMNotSuported case unsupportedDRMType + case simulatorDRMNotSuported var errorCode: Int { switch self { @@ -51,6 +52,8 @@ enum RCTVideoError: Error, Hashable { return 1013 case .unsupportedDRMType: return 1014 + case .simulatorDRMNotSuported: + return 1015 } } } @@ -90,6 +93,8 @@ extension RCTVideoError: LocalizedError { return NSLocalizedString("Offline DRM is not supported, see https://github.com/TheWidlarzGroup/react-native-video/issues/3539", comment: "") case .unsupportedDRMType: return NSLocalizedString("Unsupporeted DRM type", comment: "") + case .simulatorDRMNotSuported: + return NSLocalizedString("DRM on simulators is not supported", comment: "") } } @@ -125,6 +130,8 @@ extension RCTVideoError: LocalizedError { return NSLocalizedString("You tried to use Offline DRM but it is not supported yet", comment: "") case .unsupportedDRMType: return NSLocalizedString("You tried to use unsupporeted DRM type", comment: "") + case .simulatorDRMNotSuported: + return NSLocalizedString("You tried to DRM on a simulator", comment: "") } } @@ -160,6 +167,8 @@ extension RCTVideoError: LocalizedError { return NSLocalizedString("Check if localSourceEncryptionKeyScheme is setted", comment: "") case .unsupportedDRMType: return NSLocalizedString("Verifiy that you are using fairplay (on Apple devices)", comment: "") + case .simulatorDRMNotSuported: + return NSLocalizedString("You need to test DRM conent on real device", comment: "") } } } diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 3d1da714ca..9c6ad63777 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -420,7 +420,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH "type": _source?.type ?? NSNull(), "isNetwork": NSNumber(value: _source?.isNetwork ?? false), ], - "drm": source.drm?.json ?? NSNull(), + "drm": source.drm.json ?? NSNull(), "target": reactTag as Any, ]) @@ -457,7 +457,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } #endif - if source.drm?.type != nil { + if source.drm.json != nil { if _drmManager == nil { _drmManager = DRMManager() }