Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(iOS): rewrite DRM Module #4136

Merged
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/pages/component/drm.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`

<PlatformsList types={['iOS']} />

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
Expand Down
17 changes: 1 addition & 16 deletions docs/pages/component/props.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`

<PlatformsList types={['iOS']} />

Set the url scheme for stream encryption key for local assets

Type: String

Example:

```
localSourceEncryptionKeyScheme="my-offline-key"
```

### `maxBitRate`

Expand Down Expand Up @@ -789,7 +776,7 @@ The following other types are supported on some platforms, but aren't fully docu

#### Using DRM content

<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'visionOS', 'tvOS']} />

To setup DRM please follow [this guide](/component/drm)

Expand All @@ -807,8 +794,6 @@ Example:
},
```

> ⚠️ DRM is not supported on visionOS yet


#### Start playback at a specific point in time

Expand Down
3 changes: 3 additions & 0 deletions ios/Video/DataStructures/DRMParams.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ struct DRMParams {
let contentId: String?
let certificateUrl: String?
let base64Certificate: Bool?
let localSourceEncryptionKeyScheme: String?

let json: NSDictionary?

Expand All @@ -17,6 +18,7 @@ struct DRMParams {
self.certificateUrl = nil
self.base64Certificate = nil
self.headers = nil
self.localSourceEncryptionKeyScheme = nil
return
}
self.json = json
Expand All @@ -36,5 +38,6 @@ struct DRMParams {
} else {
self.headers = nil
}
localSourceEncryptionKeyScheme = json["localSourceEncryptionKeyScheme"] as? String
}
}
4 changes: 2 additions & 2 deletions ios/Video/DataStructures/VideoSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
Expand Down
41 changes: 41 additions & 0 deletions ios/Video/Features/DRMManager+AVContentKeySessionDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// DRMManager+AVContentKeySessionDelegate.swift
freeboub marked this conversation as resolved.
Show resolved Hide resolved
// 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 changes: 68 additions & 0 deletions ios/Video/Features/DRMManager+OnGetLicense.swift
Original file line number Diff line number Diff line change
@@ -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 changes: 34 additions & 0 deletions ios/Video/Features/DRMManager+Persitable.swift
Original file line number Diff line number Diff line change
@@ -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.offlineDRMNotSuported
}
}

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)
}
}
Loading
Loading