feat(iOS): rewrite DRM Module (#4136)
* minimal api * add suport for `getLicense` * update logic for obtaining `assetId` * add support for localSourceEncryptionKeyScheme * fix typo * fix pendingLicenses key bug * lint code * code clean * code clean * remove old files * fix tvOS build * fix errors loop * move `localSourceEncryptionKeyScheme` into drm params * add check for drm type * use DebugLog * lint * update docs * lint code * fix bad rebase * update docs * fix crashes on simulators * show error on simulator when using DRM * fix typos * code clean
This commit is contained in:
		| @@ -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. | Valid values are, for Android: DRMType.WIDEVINE / DRMType.PLAYREADY / DRMType.CLEARKEY. | ||||||
| for iOS: DRMType.FAIRPLAY | for iOS: DRMType.FAIRPLAY | ||||||
|  |  | ||||||
|  | ### `localSourceEncryptionKeyScheme` | ||||||
|  |  | ||||||
|  | <PlatformsList types={['iOS']} /> | ||||||
|  |  | ||||||
|  | Set the url scheme for stream encryption key for local assets | ||||||
|  |  | ||||||
|  | Type: String | ||||||
|  |  | ||||||
|  | Example: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | localSourceEncryptionKeyScheme="my-offline-key" | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ## Common Usage Scenarios | ## Common Usage Scenarios | ||||||
|  |  | ||||||
| ### Send cookies to license server | ### Send cookies to license server | ||||||
|   | |||||||
| @@ -339,19 +339,6 @@ Controls the iOS silent switch behavior | |||||||
| - **"ignore"** - Play audio even if the silent switch is set | - **"ignore"** - Play audio even if the silent switch is set | ||||||
| - **"obey"** - Don't play audio if the silent switch is set | - **"obey"** - Don't play audio if the silent switch is set | ||||||
|  |  | ||||||
| ### `localSourceEncryptionKeyScheme` |  | ||||||
|  |  | ||||||
| <PlatformsList types={['iOS']} /> |  | ||||||
|  |  | ||||||
| Set the url scheme for stream encryption key for local assets |  | ||||||
|  |  | ||||||
| Type: String |  | ||||||
|  |  | ||||||
| Example: |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| localSourceEncryptionKeyScheme="my-offline-key" |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### `maxBitRate` | ### `maxBitRate` | ||||||
|  |  | ||||||
| @@ -789,7 +776,7 @@ The following other types are supported on some platforms, but aren't fully docu | |||||||
|  |  | ||||||
| #### Using DRM content | #### Using DRM content | ||||||
|  |  | ||||||
| <PlatformsList types={['Android', 'iOS']} /> | <PlatformsList types={['Android', 'iOS', 'visionOS', 'tvOS']} /> | ||||||
|  |  | ||||||
| To setup DRM please follow [this guide](/component/drm) | To setup DRM please follow [this guide](/component/drm) | ||||||
|  |  | ||||||
| @@ -807,8 +794,6 @@ Example: | |||||||
|     }, |     }, | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| > ⚠️ DRM is not supported on visionOS yet |  | ||||||
|  |  | ||||||
|  |  | ||||||
| #### Start playback at a specific point in time | #### Start playback at a specific point in time | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ struct DRMParams { | |||||||
|     let contentId: String? |     let contentId: String? | ||||||
|     let certificateUrl: String? |     let certificateUrl: String? | ||||||
|     let base64Certificate: Bool? |     let base64Certificate: Bool? | ||||||
|  |     let localSourceEncryptionKeyScheme: String? | ||||||
|  |  | ||||||
|     let json: NSDictionary? |     let json: NSDictionary? | ||||||
|  |  | ||||||
| @@ -17,6 +18,7 @@ struct DRMParams { | |||||||
|             self.certificateUrl = nil |             self.certificateUrl = nil | ||||||
|             self.base64Certificate = nil |             self.base64Certificate = nil | ||||||
|             self.headers = nil |             self.headers = nil | ||||||
|  |             self.localSourceEncryptionKeyScheme = nil | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         self.json = json |         self.json = json | ||||||
| @@ -36,5 +38,6 @@ struct DRMParams { | |||||||
|         } else { |         } else { | ||||||
|             self.headers = nil |             self.headers = nil | ||||||
|         } |         } | ||||||
|  |         localSourceEncryptionKeyScheme = json["localSourceEncryptionKeyScheme"] as? String | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ struct VideoSource { | |||||||
|     let cropEnd: Int64? |     let cropEnd: Int64? | ||||||
|     let customMetadata: CustomMetadata? |     let customMetadata: CustomMetadata? | ||||||
|     /* DRM */ |     /* DRM */ | ||||||
|     let drm: DRMParams? |     let drm: DRMParams | ||||||
|     var textTracks: [TextTrack] = [] |     var textTracks: [TextTrack] = [] | ||||||
|  |  | ||||||
|     let json: NSDictionary? |     let json: NSDictionary? | ||||||
| @@ -28,7 +28,7 @@ struct VideoSource { | |||||||
|             self.cropStart = nil |             self.cropStart = nil | ||||||
|             self.cropEnd = nil |             self.cropEnd = nil | ||||||
|             self.customMetadata = nil |             self.customMetadata = nil | ||||||
|             self.drm = nil |             self.drm = DRMParams(nil) | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         self.json = json |         self.json = json | ||||||
|   | |||||||
| @@ -0,0 +1,41 @@ | |||||||
|  | // | ||||||
|  | //  DRMManager+AVContentKeySessionDelegate.swift | ||||||
|  | //  react-native-video | ||||||
|  | // | ||||||
|  | //  Created by Krzysztof Moch on 14/08/2024. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import AVFoundation | ||||||
|  |  | ||||||
|  | extension DRMManager: AVContentKeySessionDelegate { | ||||||
|  |     func contentKeySession(_: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) { | ||||||
|  |         handleContentKeyRequest(keyRequest: keyRequest) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func contentKeySession(_: AVContentKeySession, didProvideRenewingContentKeyRequest keyRequest: AVContentKeyRequest) { | ||||||
|  |         handleContentKeyRequest(keyRequest: keyRequest) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func contentKeySession(_: AVContentKeySession, shouldRetry _: AVContentKeyRequest, reason retryReason: AVContentKeyRequest.RetryReason) -> Bool { | ||||||
|  |         let retryReasons: [AVContentKeyRequest.RetryReason] = [ | ||||||
|  |             .timedOut, | ||||||
|  |             .receivedResponseWithExpiredLease, | ||||||
|  |             .receivedObsoleteContentKey, | ||||||
|  |         ] | ||||||
|  |         return retryReasons.contains(retryReason) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func contentKeySession(_: AVContentKeySession, didProvide keyRequest: AVPersistableContentKeyRequest) { | ||||||
|  |         Task { | ||||||
|  |             do { | ||||||
|  |                 try await handlePersistableKeyRequest(keyRequest: keyRequest) | ||||||
|  |             } catch { | ||||||
|  |                 handleError(error, for: keyRequest) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func contentKeySession(_: AVContentKeySession, contentKeyRequest _: AVContentKeyRequest, didFailWithError error: Error) { | ||||||
|  |         DebugLog(String(describing: error)) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										68
									
								
								ios/Video/Features/DRMManager+OnGetLicense.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								ios/Video/Features/DRMManager+OnGetLicense.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | |||||||
|  | // | ||||||
|  | //  DRMManager+OnGetLicense.swift | ||||||
|  | //  react-native-video | ||||||
|  | // | ||||||
|  | //  Created by Krzysztof Moch on 14/08/2024. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import AVFoundation | ||||||
|  |  | ||||||
|  | extension DRMManager { | ||||||
|  |     func requestLicenseFromJS(spcData: Data, assetId: String, keyRequest: AVContentKeyRequest) async throws { | ||||||
|  |         guard let onGetLicense else { | ||||||
|  |             throw RCTVideoError.noDataFromLicenseRequest | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         guard let licenseServerUrl = drmParams?.licenseServer, !licenseServerUrl.isEmpty else { | ||||||
|  |             throw RCTVideoError.noLicenseServerURL | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         guard let loadedLicenseUrl = keyRequest.identifier as? String else { | ||||||
|  |             throw RCTVideoError.invalidContentId | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         pendingLicenses[loadedLicenseUrl] = keyRequest | ||||||
|  |  | ||||||
|  |         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) { | ||||||
|  |         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: "Invalid license data", licenseUrl: licenseUrl) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             try finishProcessingContentKeyRequest(keyRequest: keyContentRequest, license: responseData) | ||||||
|  |             pendingLicenses.removeValue(forKey: licenseUrl) | ||||||
|  |         } catch { | ||||||
|  |             handleError(error, for: keyContentRequest) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func setJSLicenseError(error: String, licenseUrl: String) { | ||||||
|  |         let rctError = RCTVideoError.fromJSPart(error) | ||||||
|  |  | ||||||
|  |         DispatchQueue.main.async { [weak self] in | ||||||
|  |             self?.onVideoError?([ | ||||||
|  |                 "error": RCTVideoErrorHandler.createError(from: rctError), | ||||||
|  |                 "target": self?.reactTag as Any, | ||||||
|  |             ]) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         pendingLicenses.removeValue(forKey: licenseUrl) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								ios/Video/Features/DRMManager+Persitable.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								ios/Video/Features/DRMManager+Persitable.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | // | ||||||
|  | //  DRMManager+Persitable.swift | ||||||
|  | //  react-native-video | ||||||
|  | // | ||||||
|  | //  Created by Krzysztof Moch on 19/08/2024. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import AVFoundation | ||||||
|  |  | ||||||
|  | extension DRMManager { | ||||||
|  |     func handlePersistableKeyRequest(keyRequest: AVPersistableContentKeyRequest) async throws { | ||||||
|  |         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: | ||||||
|  |             // https://github.com/TheWidlarzGroup/react-native-video/issues/3539 | ||||||
|  |             throw RCTVideoError.offlineDRMNotSupported | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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: scheme) else { | ||||||
|  |             throw RCTVideoError.embeddedKeyExtractionFailed | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let persistentKey = try keyRequest.persistableContentKey(fromKeyVendorResponse: persistentKeyData) | ||||||
|  |         try finishProcessingContentKeyRequest(keyRequest: keyRequest, license: persistentKey) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										213
									
								
								ios/Video/Features/DRMManager.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								ios/Video/Features/DRMManager.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,213 @@ | |||||||
|  | // | ||||||
|  | //  DRMManager.swift | ||||||
|  | //  react-native-video | ||||||
|  | // | ||||||
|  | //  Created by Krzysztof Moch on 13/08/2024. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import AVFoundation | ||||||
|  |  | ||||||
|  | class DRMManager: NSObject { | ||||||
|  |     static let queue = DispatchQueue(label: "RNVideoContentKeyDelegateQueue") | ||||||
|  |     let contentKeySession: AVContentKeySession? | ||||||
|  |  | ||||||
|  |     var drmParams: DRMParams? | ||||||
|  |     var reactTag: NSNumber? | ||||||
|  |     var onVideoError: RCTDirectEventBlock? | ||||||
|  |     var onGetLicense: RCTDirectEventBlock? | ||||||
|  |  | ||||||
|  |     // Licenses handled by onGetLicense (from JS side) | ||||||
|  |     var pendingLicenses: [String: AVContentKeyRequest] = [:] | ||||||
|  |  | ||||||
|  |     override init() { | ||||||
|  |         #if targetEnvironment(simulator) | ||||||
|  |             contentKeySession = nil | ||||||
|  |             super.init() | ||||||
|  |         #else | ||||||
|  |             contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming) | ||||||
|  |             super.init() | ||||||
|  |  | ||||||
|  |             contentKeySession?.setDelegate(self, queue: DRMManager.queue) | ||||||
|  |         #endif | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func createContentKeyRequest( | ||||||
|  |         asset: AVContentKeyRecipient, | ||||||
|  |         drmParams: DRMParams?, | ||||||
|  |         reactTag: NSNumber?, | ||||||
|  |         onVideoError: RCTDirectEventBlock?, | ||||||
|  |         onGetLicense: RCTDirectEventBlock? | ||||||
|  |     ) { | ||||||
|  |         self.reactTag = reactTag | ||||||
|  |         self.onVideoError = onVideoError | ||||||
|  |         self.onGetLicense = onGetLicense | ||||||
|  |         self.drmParams = drmParams | ||||||
|  |  | ||||||
|  |         if drmParams?.type != "fairplay" { | ||||||
|  |             self.onVideoError?([ | ||||||
|  |                 "error": RCTVideoErrorHandler.createError(from: RCTVideoError.unsupportedDRMType), | ||||||
|  |                 "target": self.reactTag as Any, | ||||||
|  |             ]) | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         #if targetEnvironment(simulator) | ||||||
|  |             DebugLog("Simulator is not supported for FairPlay DRM.") | ||||||
|  |             self.onVideoError?([ | ||||||
|  |                 "error": RCTVideoErrorHandler.createError(from: RCTVideoError.simulatorDRMNotSupported), | ||||||
|  |                 "target": self.reactTag as Any, | ||||||
|  |             ]) | ||||||
|  |         #endif | ||||||
|  |  | ||||||
|  |         contentKeySession?.addContentKeyRecipient(asset) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // MARK: - Internal | ||||||
|  |  | ||||||
|  |     func handleContentKeyRequest(keyRequest: AVContentKeyRequest) { | ||||||
|  |         Task { | ||||||
|  |             do { | ||||||
|  |                 if drmParams?.localSourceEncryptionKeyScheme != nil { | ||||||
|  |                     #if os(iOS) | ||||||
|  |                         try keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError() | ||||||
|  |                         return | ||||||
|  |                     #else | ||||||
|  |                         throw RCTVideoError.offlineDRMNotSuported | ||||||
|  |                     #endif | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 try await processContentKeyRequest(keyRequest: keyRequest) | ||||||
|  |             } catch { | ||||||
|  |                 handleError(error, for: keyRequest) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func finishProcessingContentKeyRequest(keyRequest: AVContentKeyRequest, license: Data) throws { | ||||||
|  |         let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: license) | ||||||
|  |         keyRequest.processContentKeyResponse(keyResponse) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     func handleError(_ error: Error, for keyRequest: AVContentKeyRequest) { | ||||||
|  |         let rctError: RCTVideoError | ||||||
|  |         if let videoError = error as? RCTVideoError { | ||||||
|  |             // handle RCTVideoError errors | ||||||
|  |             rctError = videoError | ||||||
|  |  | ||||||
|  |             DispatchQueue.main.async { [weak self] in | ||||||
|  |                 self?.onVideoError?([ | ||||||
|  |                     "error": RCTVideoErrorHandler.createError(from: rctError), | ||||||
|  |                     "target": self?.reactTag as Any, | ||||||
|  |                 ]) | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             let err = error as NSError | ||||||
|  |  | ||||||
|  |             // handle Other errors | ||||||
|  |             DispatchQueue.main.async { [weak self] in | ||||||
|  |                 self?.onVideoError?([ | ||||||
|  |                     "error": [ | ||||||
|  |                         "code": err.code, | ||||||
|  |                         "localizedDescription": err.localizedDescription, | ||||||
|  |                         "localizedFailureReason": err.localizedFailureReason ?? "", | ||||||
|  |                         "localizedRecoverySuggestion": err.localizedRecoverySuggestion ?? "", | ||||||
|  |                         "domain": err.domain, | ||||||
|  |                     ], | ||||||
|  |                     "target": self?.reactTag as Any, | ||||||
|  |                 ]) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         keyRequest.processContentKeyResponseError(error) | ||||||
|  |         contentKeySession?.expire() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // MARK: - Private | ||||||
|  |  | ||||||
|  |     private func processContentKeyRequest(keyRequest: AVContentKeyRequest) async throws { | ||||||
|  |         guard let assetId = getAssetId(keyRequest: keyRequest), | ||||||
|  |               let assetIdData = assetId.data(using: .utf8) else { | ||||||
|  |             throw RCTVideoError.invalidContentId | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let appCertificate = try await requestApplicationCertificate() | ||||||
|  |         let spcData = try await keyRequest.makeStreamingContentKeyRequestData(forApp: appCertificate, contentIdentifier: assetIdData) | ||||||
|  |  | ||||||
|  |         if onGetLicense != nil { | ||||||
|  |             try await requestLicenseFromJS(spcData: spcData, assetId: assetId, keyRequest: keyRequest) | ||||||
|  |         } else { | ||||||
|  |             let license = try await requestLicense(spcData: spcData) | ||||||
|  |             try finishProcessingContentKeyRequest(keyRequest: keyRequest, license: license) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func requestApplicationCertificate() async throws -> Data { | ||||||
|  |         guard let urlString = drmParams?.certificateUrl, | ||||||
|  |               let url = URL(string: urlString) else { | ||||||
|  |             throw RCTVideoError.noCertificateURL | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let (data, response) = try await URLSession.shared.data(from: url) | ||||||
|  |  | ||||||
|  |         guard let httpResponse = response as? HTTPURLResponse, | ||||||
|  |               httpResponse.statusCode == 200 else { | ||||||
|  |             throw RCTVideoError.noCertificateData | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if drmParams?.base64Certificate == true { | ||||||
|  |             guard let certData = Data(base64Encoded: data) else { | ||||||
|  |                 throw RCTVideoError.noCertificateData | ||||||
|  |             } | ||||||
|  |             return certData | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return data | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func requestLicense(spcData: Data) async throws -> Data { | ||||||
|  |         guard let licenseServerUrlString = drmParams?.licenseServer, | ||||||
|  |               let licenseServerUrl = URL(string: licenseServerUrlString) else { | ||||||
|  |             throw RCTVideoError.noLicenseServerURL | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var request = URLRequest(url: licenseServerUrl) | ||||||
|  |         request.httpMethod = "POST" | ||||||
|  |         request.httpBody = spcData | ||||||
|  |  | ||||||
|  |         if let headers = drmParams?.headers { | ||||||
|  |             for (key, value) in headers { | ||||||
|  |                 if let stringValue = value as? String { | ||||||
|  |                     request.setValue(stringValue, forHTTPHeaderField: key) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let (data, response) = try await URLSession.shared.data(for: request) | ||||||
|  |  | ||||||
|  |         guard let httpResponse = response as? HTTPURLResponse else { | ||||||
|  |             throw RCTVideoError.licenseRequestFailed(0) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         guard httpResponse.statusCode == 200 else { | ||||||
|  |             throw RCTVideoError.licenseRequestFailed(httpResponse.statusCode) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         guard !data.isEmpty else { | ||||||
|  |             throw RCTVideoError.noDataFromLicenseRequest | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return data | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private func getAssetId(keyRequest: AVContentKeyRequest) -> String? { | ||||||
|  |         if let assetId = drmParams?.contentId { | ||||||
|  |             return assetId | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if let url = keyRequest.identifier as? String { | ||||||
|  |             return url.replacingOccurrences(of: "skd://", with: "") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return nil | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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 |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -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 |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,114 +1,188 @@ | |||||||
|  | import Foundation | ||||||
|  |  | ||||||
| // MARK: - RCTVideoError | // MARK: - RCTVideoError | ||||||
|  |  | ||||||
| enum RCTVideoError: Int { | enum RCTVideoError: Error, Hashable { | ||||||
|     case fromJSPart |     case fromJSPart(String) | ||||||
|     case noLicenseServerURL |     case noLicenseServerURL | ||||||
|     case licenseRequestNotOk |     case licenseRequestFailed(Int) | ||||||
|     case noDataFromLicenseRequest |     case noDataFromLicenseRequest | ||||||
|     case noSPC |     case noSPC | ||||||
|     case noDataRequest |  | ||||||
|     case noCertificateData |     case noCertificateData | ||||||
|     case noCertificateURL |     case noCertificateURL | ||||||
|     case noFairplayDRM |  | ||||||
|     case noDRMData |     case noDRMData | ||||||
|     case invalidContentId |     case invalidContentId | ||||||
|  |     case invalidAppCert | ||||||
|  |     case keyRequestCreationFailed | ||||||
|  |     case persistableKeyRequestFailed | ||||||
|  |     case embeddedKeyExtractionFailed | ||||||
|  |     case offlineDRMNotSupported | ||||||
|  |     case unsupportedDRMType | ||||||
|  |     case simulatorDRMNotSupported | ||||||
|  |  | ||||||
|  |     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 .offlineDRMNotSupported: | ||||||
|  |             return 1013 | ||||||
|  |         case .unsupportedDRMType: | ||||||
|  |             return 1014 | ||||||
|  |         case .simulatorDRMNotSupported: | ||||||
|  |             return 1015 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MARK: LocalizedError | ||||||
|  |  | ||||||
|  | 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 .offlineDRMNotSupported: | ||||||
|  |             return NSLocalizedString("Offline DRM is not supported, see https://github.com/TheWidlarzGroup/react-native-video/issues/3539", comment: "") | ||||||
|  |         case .unsupportedDRMType: | ||||||
|  |             return NSLocalizedString("Unsupported DRM type", comment: "") | ||||||
|  |         case .simulatorDRMNotSupported: | ||||||
|  |             return NSLocalizedString("DRM on simulators is not supported", comment: "") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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 .offlineDRMNotSupported: | ||||||
|  |             return NSLocalizedString("You tried to use Offline DRM but it is not supported yet", comment: "") | ||||||
|  |         case .unsupportedDRMType: | ||||||
|  |             return NSLocalizedString("You tried to use unsupported DRM type", comment: "") | ||||||
|  |         case .simulatorDRMNotSupported: | ||||||
|  |             return NSLocalizedString("You tried to DRM on a simulator", comment: "") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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 .offlineDRMNotSupported: | ||||||
|  |             return NSLocalizedString("Check if localSourceEncryptionKeyScheme is set", comment: "") | ||||||
|  |         case .unsupportedDRMType: | ||||||
|  |             return NSLocalizedString("Verify that you are using fairplay (on Apple devices)", comment: "") | ||||||
|  |         case .simulatorDRMNotSupported: | ||||||
|  |             return NSLocalizedString("You need to test DRM content on real device", comment: "") | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| // MARK: - RCTVideoErrorHandler | // MARK: - RCTVideoErrorHandler | ||||||
|  |  | ||||||
| enum RCTVideoErrorHandler { | enum RCTVideoErrorHandler { | ||||||
|     static let noDRMData = NSError( |     static func createError(from error: RCTVideoError) -> [String: Any] { | ||||||
|         domain: "RCTVideo", |         return [ | ||||||
|         code: RCTVideoError.noDRMData.rawValue, |             "code": error.errorCode, | ||||||
|         userInfo: [ |             "localizedDescription": error.localizedDescription, | ||||||
|             NSLocalizedDescriptionKey: "Error obtaining DRM license.", |             "localizedFailureReason": error.failureReason ?? "", | ||||||
|             NSLocalizedFailureReasonErrorKey: "No drm object found.", |             "localizedRecoverySuggestion": error.recoverySuggestion ?? "", | ||||||
|             NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?", |             "domain": "RCTVideo", | ||||||
|         ] |         ] | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     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'?", |  | ||||||
|         ] |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     static let noSPC = NSError( |  | ||||||
|         domain: "RCTVideo", |  | ||||||
|         code: RCTVideoError.noSPC.rawValue, |  | ||||||
|         userInfo: [ |  | ||||||
|             NSLocalizedDescriptionKey: "Error obtaining license.", |  | ||||||
|             NSLocalizedFailureReasonErrorKey: "No spc received.", |  | ||||||
|             NSLocalizedRecoverySuggestionErrorKey: "Check your DRM config.", |  | ||||||
|         ] |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     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?", |  | ||||||
|         ] |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     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?", |  | ||||||
|         ] |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     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?", |  | ||||||
|             ] |  | ||||||
|         ) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     static func fromJSPart(_ error: String) -> NSError { |  | ||||||
|         return NSError(domain: "RCTVideo", |  | ||||||
|                        code: RCTVideoError.fromJSPart.rawValue, |  | ||||||
|                        userInfo: [ |  | ||||||
|                            NSLocalizedDescriptionKey: error, |  | ||||||
|                            NSLocalizedFailureReasonErrorKey: error, |  | ||||||
|                            NSLocalizedRecoverySuggestionErrorKey: error, |  | ||||||
|                        ]) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     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?", |  | ||||||
|         ] |  | ||||||
|     ) |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -17,7 +17,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |||||||
|  |  | ||||||
|     private var _playerViewController: RCTVideoPlayerViewController? |     private var _playerViewController: RCTVideoPlayerViewController? | ||||||
|     private var _videoURL: NSURL? |     private var _videoURL: NSURL? | ||||||
|     private var _localSourceEncryptionKeyScheme: String? |  | ||||||
|  |  | ||||||
|     /* Required to publish events */ |     /* Required to publish events */ | ||||||
|     private var _eventDispatcher: RCTEventDispatcher? |     private var _eventDispatcher: RCTEventDispatcher? | ||||||
| @@ -97,7 +96,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |||||||
|     private var _didRequestAds = false |     private var _didRequestAds = false | ||||||
|     private var _adPlaying = false |     private var _adPlaying = false | ||||||
|  |  | ||||||
|     private var _resouceLoaderDelegate: RCTResourceLoaderDelegate? |     private lazy var _drmManager: DRMManager? = DRMManager() | ||||||
|     private var _playerObserver: RCTPlayerObserver = .init() |     private var _playerObserver: RCTPlayerObserver = .init() | ||||||
|  |  | ||||||
|     #if USE_VIDEO_CACHING |     #if USE_VIDEO_CACHING | ||||||
| @@ -421,7 +420,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |||||||
|                 "type": _source?.type ?? NSNull(), |                 "type": _source?.type ?? NSNull(), | ||||||
|                 "isNetwork": NSNumber(value: _source?.isNetwork ?? false), |                 "isNetwork": NSNumber(value: _source?.isNetwork ?? false), | ||||||
|             ], |             ], | ||||||
|             "drm": source.drm?.json ?? NSNull(), |             "drm": source.drm.json ?? NSNull(), | ||||||
|             "target": reactTag as Any, |             "target": reactTag as Any, | ||||||
|         ]) |         ]) | ||||||
|  |  | ||||||
| @@ -458,14 +457,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |||||||
|             } |             } | ||||||
|         #endif |         #endif | ||||||
|  |  | ||||||
|         if source.drm != nil || _localSourceEncryptionKeyScheme != nil { |         if source.drm.json != nil { | ||||||
|             _resouceLoaderDelegate = RCTResourceLoaderDelegate( |             if _drmManager == nil { | ||||||
|  |                 _drmManager = DRMManager() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             _drmManager?.createContentKeyRequest( | ||||||
|                 asset: asset, |                 asset: asset, | ||||||
|                 drm: source.drm, |                 drmParams: source.drm, | ||||||
|                 localSourceEncryptionKeyScheme: _localSourceEncryptionKeyScheme, |                 reactTag: reactTag, | ||||||
|                 onVideoError: onVideoError, |                 onVideoError: onVideoError, | ||||||
|                 onGetLicense: onGetLicense, |                 onGetLicense: onGetLicense | ||||||
|                 reactTag: reactTag |  | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -562,7 +564,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |||||||
|             } |             } | ||||||
|             self.removePlayerLayer() |             self.removePlayerLayer() | ||||||
|             self._playerObserver.player = nil |             self._playerObserver.player = nil | ||||||
|             self._resouceLoaderDelegate = nil |             self._drmManager = nil | ||||||
|             self._playerObserver.playerItem = nil |             self._playerObserver.playerItem = nil | ||||||
|  |  | ||||||
|             // perform on next run loop, otherwise other passed react-props may not be set |             // perform on next run loop, otherwise other passed react-props may not be set | ||||||
| @@ -594,11 +596,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |||||||
|         DispatchQueue.global(qos: .default).async(execute: initializeSource) |         DispatchQueue.global(qos: .default).async(execute: initializeSource) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @objc |  | ||||||
|     func setLocalSourceEncryptionKeyScheme(_ keyScheme: String) { |  | ||||||
|         _localSourceEncryptionKeyScheme = keyScheme |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     func playerItemPrepareText(source: VideoSource, asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem { |     func playerItemPrepareText(source: VideoSource, asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem { | ||||||
|         if source.textTracks.isEmpty == true || uri.hasSuffix(".m3u8") { |         if source.textTracks.isEmpty == true || uri.hasSuffix(".m3u8") { | ||||||
|             return await self.playerItemPropegateMetadata(AVPlayerItem(asset: asset)) |             return await self.playerItemPropegateMetadata(AVPlayerItem(asset: asset)) | ||||||
| @@ -1295,7 +1292,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |||||||
|  |  | ||||||
|         ReactNativeVideoManager.shared.onInstanceRemoved(id: instanceId, player: _player as Any) |         ReactNativeVideoManager.shared.onInstanceRemoved(id: instanceId, player: _player as Any) | ||||||
|         _player = nil |         _player = nil | ||||||
|         _resouceLoaderDelegate = nil |         _drmManager = nil | ||||||
|         _playerObserver.clearPlayer() |         _playerObserver.clearPlayer() | ||||||
|  |  | ||||||
|         self.removePlayerLayer() |         self.removePlayerLayer() | ||||||
| @@ -1328,12 +1325,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     func setLicenseResult(_ license: String!, _ licenseUrl: String!) { |     func setLicenseResult(_ license: String, _ licenseUrl: String) { | ||||||
|         _resouceLoaderDelegate?.setLicenseResult(license, licenseUrl) |         _drmManager?.setJSLicenseResult(license: license, licenseUrl: licenseUrl) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     func setLicenseResultError(_ error: String!, _ licenseUrl: String!) { |     func setLicenseResultError(_ error: String, _ licenseUrl: String) { | ||||||
|         _resouceLoaderDelegate?.setLicenseResultError(error, licenseUrl) |         _drmManager?.setJSLicenseError(error: error, licenseUrl: licenseUrl) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // MARK: - RCTPlayerObserverHandler |     // MARK: - RCTPlayerObserverHandler | ||||||
|   | |||||||
| @@ -119,6 +119,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( | |||||||
|       onTextTrackDataChanged, |       onTextTrackDataChanged, | ||||||
|       onVideoTracks, |       onVideoTracks, | ||||||
|       onAspectRatio, |       onAspectRatio, | ||||||
|  |       localSourceEncryptionKeyScheme, | ||||||
|       ...rest |       ...rest | ||||||
|     }, |     }, | ||||||
|     ref, |     ref, | ||||||
| @@ -189,6 +190,9 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( | |||||||
|             base64Certificate: selectedDrm.base64Certificate, |             base64Certificate: selectedDrm.base64Certificate, | ||||||
|             useExternalGetLicense: !!selectedDrm.getLicense, |             useExternalGetLicense: !!selectedDrm.getLicense, | ||||||
|             multiDrm: selectedDrm.multiDrm, |             multiDrm: selectedDrm.multiDrm, | ||||||
|  |             localSourceEncryptionKeyScheme: | ||||||
|  |               selectedDrm.localSourceEncryptionKeyScheme || | ||||||
|  |               localSourceEncryptionKeyScheme, | ||||||
|           }; |           }; | ||||||
|  |  | ||||||
|       let _cmcd: NativeCmcdConfiguration | undefined; |       let _cmcd: NativeCmcdConfiguration | undefined; | ||||||
| @@ -238,7 +242,13 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( | |||||||
|         textTracksAllowChunklessPreparation: |         textTracksAllowChunklessPreparation: | ||||||
|           resolvedSource.textTracksAllowChunklessPreparation, |           resolvedSource.textTracksAllowChunklessPreparation, | ||||||
|       }; |       }; | ||||||
|     }, [drm, source, textTracks, contentStartTime]); |     }, [ | ||||||
|  |       drm, | ||||||
|  |       source, | ||||||
|  |       textTracks, | ||||||
|  |       contentStartTime, | ||||||
|  |       localSourceEncryptionKeyScheme, | ||||||
|  |     ]); | ||||||
|  |  | ||||||
|     const _selectedTextTrack = useMemo(() => { |     const _selectedTextTrack = useMemo(() => { | ||||||
|       if (!selectedTextTrack) { |       if (!selectedTextTrack) { | ||||||
|   | |||||||
| @@ -62,6 +62,7 @@ type Drm = Readonly<{ | |||||||
|   base64Certificate?: boolean; // ios default: false |   base64Certificate?: boolean; // ios default: false | ||||||
|   useExternalGetLicense?: boolean; // ios |   useExternalGetLicense?: boolean; // ios | ||||||
|   multiDrm?: WithDefault<boolean, false>; // android |   multiDrm?: WithDefault<boolean, false>; // android | ||||||
|  |   localSourceEncryptionKeyScheme?: string; // ios | ||||||
| }>; | }>; | ||||||
|  |  | ||||||
| type CmcdMode = WithDefault<Int32, 1>; | type CmcdMode = WithDefault<Int32, 1>; | ||||||
| @@ -341,7 +342,6 @@ export interface VideoNativeProps extends ViewProps { | |||||||
|   fullscreenOrientation?: WithDefault<string, 'all'>; |   fullscreenOrientation?: WithDefault<string, 'all'>; | ||||||
|   progressUpdateInterval?: Float; |   progressUpdateInterval?: Float; | ||||||
|   restoreUserInterfaceForPIPStopCompletionHandler?: boolean; |   restoreUserInterfaceForPIPStopCompletionHandler?: boolean; | ||||||
|   localSourceEncryptionKeyScheme?: string; |  | ||||||
|   debug?: DebugConfig; |   debug?: DebugConfig; | ||||||
|   showNotificationControls?: WithDefault<boolean, false>; // Android, iOS |   showNotificationControls?: WithDefault<boolean, false>; // Android, iOS | ||||||
|   bufferConfig?: BufferConfig; // Android |   bufferConfig?: BufferConfig; // Android | ||||||
|   | |||||||
| @@ -81,6 +81,7 @@ export type Drm = Readonly<{ | |||||||
|   certificateUrl?: string; // ios |   certificateUrl?: string; // ios | ||||||
|   base64Certificate?: boolean; // ios default: false |   base64Certificate?: boolean; // ios default: false | ||||||
|   multiDrm?: boolean; // android |   multiDrm?: boolean; // android | ||||||
|  |   localSourceEncryptionKeyScheme?: string; // ios | ||||||
|   /* eslint-disable @typescript-eslint/no-unused-vars */ |   /* eslint-disable @typescript-eslint/no-unused-vars */ | ||||||
|   getLicense?: ( |   getLicense?: ( | ||||||
|     spcBase64: string, |     spcBase64: string, | ||||||
| @@ -321,6 +322,7 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps { | |||||||
|   /** @deprecated Use viewType*/ |   /** @deprecated Use viewType*/ | ||||||
|   useSecureView?: boolean; // Android |   useSecureView?: boolean; // Android | ||||||
|   volume?: number; |   volume?: number; | ||||||
|  |   /** @deprecated use **localSourceEncryptionKeyScheme** key in **drm** props instead */ | ||||||
|   localSourceEncryptionKeyScheme?: string; |   localSourceEncryptionKeyScheme?: string; | ||||||
|   debug?: DebugConfig; |   debug?: DebugConfig; | ||||||
|   allowsExternalPlayback?: boolean; // iOS |   allowsExternalPlayback?: boolean; // iOS | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user