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:
parent
c96f7d41f3
commit
0e4c95def9
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user