diff --git a/docs/pages/component/drm.mdx b/docs/pages/component/drm.mdx
index 6e7fa0aa..da9074d5 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 21298868..f976dfbc 100644
--- a/docs/pages/component/props.mdx
+++ b/docs/pages/component/props.mdx
@@ -339,19 +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`
-
-
-
-Set the url scheme for stream encryption key for local assets
-
-Type: String
-
-Example:
-
-```
-localSourceEncryptionKeyScheme="my-offline-key"
-```
### `maxBitRate`
@@ -789,7 +776,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 +794,6 @@ Example:
},
```
-> ⚠️ DRM is not supported on visionOS yet
-
#### Start playback at a specific point in time
diff --git a/ios/Video/DataStructures/DRMParams.swift b/ios/Video/DataStructures/DRMParams.swift
index ce91d4dc..bf8a4d2a 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/DataStructures/VideoSource.swift b/ios/Video/DataStructures/VideoSource.swift
index e672929f..45e5b2d3 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+AVContentKeySessionDelegate.swift b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift
new file mode 100644
index 00000000..f8d31fd0
--- /dev/null
+++ b/ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift
@@ -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))
+ }
+}
diff --git a/ios/Video/Features/DRMManager+OnGetLicense.swift b/ios/Video/Features/DRMManager+OnGetLicense.swift
new file mode 100644
index 00000000..90b1c38c
--- /dev/null
+++ b/ios/Video/Features/DRMManager+OnGetLicense.swift
@@ -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)
+ }
+}
diff --git a/ios/Video/Features/DRMManager+Persitable.swift b/ios/Video/Features/DRMManager+Persitable.swift
new file mode 100644
index 00000000..022743d8
--- /dev/null
+++ b/ios/Video/Features/DRMManager+Persitable.swift
@@ -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)
+ }
+}
diff --git a/ios/Video/Features/DRMManager.swift b/ios/Video/Features/DRMManager.swift
new file mode 100644
index 00000000..d62df8b3
--- /dev/null
+++ b/ios/Video/Features/DRMManager.swift
@@ -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
+ }
+}
diff --git a/ios/Video/Features/RCTResourceLoaderDelegate.swift b/ios/Video/Features/RCTResourceLoaderDelegate.swift
deleted file mode 100644
index f7ab1031..00000000
--- 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 bc73d48d..00000000
--- 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
- )
- }
-}
diff --git a/ios/Video/Features/RCTVideoErrorHandling.swift b/ios/Video/Features/RCTVideoErrorHandling.swift
index 7dc68783..b06031cf 100644
--- a/ios/Video/Features/RCTVideoErrorHandling.swift
+++ b/ios/Video/Features/RCTVideoErrorHandling.swift
@@ -1,114 +1,188 @@
+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
+ 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
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 func createError(from error: RCTVideoError) -> [String: Any] {
+ return [
+ "code": error.errorCode,
+ "localizedDescription": error.localizedDescription,
+ "localizedFailureReason": error.failureReason ?? "",
+ "localizedRecoverySuggestion": error.recoverySuggestion ?? "",
+ "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?",
- ]
- )
}
diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift
index 64304a2f..21027ca8 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?
@@ -97,7 +96,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
@@ -421,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,
])
@@ -458,14 +457,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
}
#endif
- if source.drm != nil || _localSourceEncryptionKeyScheme != nil {
- _resouceLoaderDelegate = RCTResourceLoaderDelegate(
+ if source.drm.json != nil {
+ if _drmManager == nil {
+ _drmManager = DRMManager()
+ }
+
+ _drmManager?.createContentKeyRequest(
asset: asset,
- drm: source.drm,
- localSourceEncryptionKeyScheme: _localSourceEncryptionKeyScheme,
+ drmParams: source.drm,
+ reactTag: reactTag,
onVideoError: onVideoError,
- onGetLicense: onGetLicense,
- reactTag: reactTag
+ onGetLicense: onGetLicense
)
}
@@ -562,7 +564,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
@@ -594,11 +596,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
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 {
if source.textTracks.isEmpty == true || uri.hasSuffix(".m3u8") {
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)
_player = nil
- _resouceLoaderDelegate = nil
+ _drmManager = nil
_playerObserver.clearPlayer()
self.removePlayerLayer()
@@ -1328,12 +1325,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
)
}
- func setLicenseResult(_ license: String!, _ licenseUrl: String!) {
- _resouceLoaderDelegate?.setLicenseResult(license, licenseUrl)
+ func setLicenseResult(_ license: String, _ licenseUrl: String) {
+ _drmManager?.setJSLicenseResult(license: license, licenseUrl: licenseUrl)
}
- func setLicenseResultError(_ error: String!, _ licenseUrl: String!) {
- _resouceLoaderDelegate?.setLicenseResultError(error, licenseUrl)
+ func setLicenseResultError(_ error: String, _ licenseUrl: String) {
+ _drmManager?.setJSLicenseError(error: error, licenseUrl: licenseUrl)
}
// MARK: - RCTPlayerObserverHandler
diff --git a/src/Video.tsx b/src/Video.tsx
index c1f251a1..8f8dd6c0 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,13 @@ 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 f210cfcc..34a50438 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 71304080..9e25e25e 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