Skip to content

Commit 0e4c95d

Browse files
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
1 parent c96f7d4 commit 0e4c95d

15 files changed

+576
-482
lines changed

docs/pages/component/drm.mdx

+14
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,20 @@ You can specify the DRM type, either by string or using the exported DRMType enu
137137
Valid values are, for Android: DRMType.WIDEVINE / DRMType.PLAYREADY / DRMType.CLEARKEY.
138138
for iOS: DRMType.FAIRPLAY
139139

140+
### `localSourceEncryptionKeyScheme`
141+
142+
<PlatformsList types={['iOS']} />
143+
144+
Set the url scheme for stream encryption key for local assets
145+
146+
Type: String
147+
148+
Example:
149+
150+
```
151+
localSourceEncryptionKeyScheme="my-offline-key"
152+
```
153+
140154
## Common Usage Scenarios
141155

142156
### Send cookies to license server

docs/pages/component/props.mdx

+1-16
Original file line numberDiff line numberDiff line change
@@ -339,19 +339,6 @@ Controls the iOS silent switch behavior
339339
- **"ignore"** - Play audio even if the silent switch is set
340340
- **"obey"** - Don't play audio if the silent switch is set
341341

342-
### `localSourceEncryptionKeyScheme`
343-
344-
<PlatformsList types={['iOS']} />
345-
346-
Set the url scheme for stream encryption key for local assets
347-
348-
Type: String
349-
350-
Example:
351-
352-
```
353-
localSourceEncryptionKeyScheme="my-offline-key"
354-
```
355342

356343
### `maxBitRate`
357344

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

790777
#### Using DRM content
791778

792-
<PlatformsList types={['Android', 'iOS']} />
779+
<PlatformsList types={['Android', 'iOS', 'visionOS', 'tvOS']} />
793780

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

@@ -807,8 +794,6 @@ Example:
807794
},
808795
```
809796

810-
> ⚠️ DRM is not supported on visionOS yet
811-
812797

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

ios/Video/DataStructures/DRMParams.swift

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ struct DRMParams {
55
let contentId: String?
66
let certificateUrl: String?
77
let base64Certificate: Bool?
8+
let localSourceEncryptionKeyScheme: String?
89

910
let json: NSDictionary?
1011

@@ -17,6 +18,7 @@ struct DRMParams {
1718
self.certificateUrl = nil
1819
self.base64Certificate = nil
1920
self.headers = nil
21+
self.localSourceEncryptionKeyScheme = nil
2022
return
2123
}
2224
self.json = json
@@ -36,5 +38,6 @@ struct DRMParams {
3638
} else {
3739
self.headers = nil
3840
}
41+
localSourceEncryptionKeyScheme = json["localSourceEncryptionKeyScheme"] as? String
3942
}
4043
}

ios/Video/DataStructures/VideoSource.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ struct VideoSource {
1010
let cropEnd: Int64?
1111
let customMetadata: CustomMetadata?
1212
/* DRM */
13-
let drm: DRMParams?
13+
let drm: DRMParams
1414
var textTracks: [TextTrack] = []
1515

1616
let json: NSDictionary?
@@ -28,7 +28,7 @@ struct VideoSource {
2828
self.cropStart = nil
2929
self.cropEnd = nil
3030
self.customMetadata = nil
31-
self.drm = nil
31+
self.drm = DRMParams(nil)
3232
return
3333
}
3434
self.json = json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// DRMManager+AVContentKeySessionDelegate.swift
3+
// react-native-video
4+
//
5+
// Created by Krzysztof Moch on 14/08/2024.
6+
//
7+
8+
import AVFoundation
9+
10+
extension DRMManager: AVContentKeySessionDelegate {
11+
func contentKeySession(_: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) {
12+
handleContentKeyRequest(keyRequest: keyRequest)
13+
}
14+
15+
func contentKeySession(_: AVContentKeySession, didProvideRenewingContentKeyRequest keyRequest: AVContentKeyRequest) {
16+
handleContentKeyRequest(keyRequest: keyRequest)
17+
}
18+
19+
func contentKeySession(_: AVContentKeySession, shouldRetry _: AVContentKeyRequest, reason retryReason: AVContentKeyRequest.RetryReason) -> Bool {
20+
let retryReasons: [AVContentKeyRequest.RetryReason] = [
21+
.timedOut,
22+
.receivedResponseWithExpiredLease,
23+
.receivedObsoleteContentKey,
24+
]
25+
return retryReasons.contains(retryReason)
26+
}
27+
28+
func contentKeySession(_: AVContentKeySession, didProvide keyRequest: AVPersistableContentKeyRequest) {
29+
Task {
30+
do {
31+
try await handlePersistableKeyRequest(keyRequest: keyRequest)
32+
} catch {
33+
handleError(error, for: keyRequest)
34+
}
35+
}
36+
}
37+
38+
func contentKeySession(_: AVContentKeySession, contentKeyRequest _: AVContentKeyRequest, didFailWithError error: Error) {
39+
DebugLog(String(describing: error))
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// DRMManager+OnGetLicense.swift
3+
// react-native-video
4+
//
5+
// Created by Krzysztof Moch on 14/08/2024.
6+
//
7+
8+
import AVFoundation
9+
10+
extension DRMManager {
11+
func requestLicenseFromJS(spcData: Data, assetId: String, keyRequest: AVContentKeyRequest) async throws {
12+
guard let onGetLicense else {
13+
throw RCTVideoError.noDataFromLicenseRequest
14+
}
15+
16+
guard let licenseServerUrl = drmParams?.licenseServer, !licenseServerUrl.isEmpty else {
17+
throw RCTVideoError.noLicenseServerURL
18+
}
19+
20+
guard let loadedLicenseUrl = keyRequest.identifier as? String else {
21+
throw RCTVideoError.invalidContentId
22+
}
23+
24+
pendingLicenses[loadedLicenseUrl] = keyRequest
25+
26+
DispatchQueue.main.async { [weak self] in
27+
onGetLicense([
28+
"licenseUrl": licenseServerUrl,
29+
"loadedLicenseUrl": loadedLicenseUrl,
30+
"contentId": assetId,
31+
"spcBase64": spcData.base64EncodedString(),
32+
"target": self?.reactTag as Any,
33+
])
34+
}
35+
}
36+
37+
func setJSLicenseResult(license: String, licenseUrl: String) {
38+
guard let keyContentRequest = pendingLicenses[licenseUrl] else {
39+
setJSLicenseError(error: "Loading request for licenseUrl \(licenseUrl) not found", licenseUrl: licenseUrl)
40+
return
41+
}
42+
43+
guard let responseData = Data(base64Encoded: license) else {
44+
setJSLicenseError(error: "Invalid license data", licenseUrl: licenseUrl)
45+
return
46+
}
47+
48+
do {
49+
try finishProcessingContentKeyRequest(keyRequest: keyContentRequest, license: responseData)
50+
pendingLicenses.removeValue(forKey: licenseUrl)
51+
} catch {
52+
handleError(error, for: keyContentRequest)
53+
}
54+
}
55+
56+
func setJSLicenseError(error: String, licenseUrl: String) {
57+
let rctError = RCTVideoError.fromJSPart(error)
58+
59+
DispatchQueue.main.async { [weak self] in
60+
self?.onVideoError?([
61+
"error": RCTVideoErrorHandler.createError(from: rctError),
62+
"target": self?.reactTag as Any,
63+
])
64+
}
65+
66+
pendingLicenses.removeValue(forKey: licenseUrl)
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// DRMManager+Persitable.swift
3+
// react-native-video
4+
//
5+
// Created by Krzysztof Moch on 19/08/2024.
6+
//
7+
8+
import AVFoundation
9+
10+
extension DRMManager {
11+
func handlePersistableKeyRequest(keyRequest: AVPersistableContentKeyRequest) async throws {
12+
if let localSourceEncryptionKeyScheme = drmParams?.localSourceEncryptionKeyScheme {
13+
try handleEmbeddedKey(keyRequest: keyRequest, scheme: localSourceEncryptionKeyScheme)
14+
} else {
15+
// Offline DRM is not supported yet - if you need it please check out the following issue:
16+
// https://github.com/TheWidlarzGroup/react-native-video/issues/3539
17+
throw RCTVideoError.offlineDRMNotSupported
18+
}
19+
}
20+
21+
private func handleEmbeddedKey(keyRequest: AVPersistableContentKeyRequest, scheme: String) throws {
22+
guard let uri = keyRequest.identifier as? String,
23+
let url = URL(string: uri) else {
24+
throw RCTVideoError.invalidContentId
25+
}
26+
27+
guard let persistentKeyData = RCTVideoUtils.extractDataFromCustomSchemeUrl(from: url, scheme: scheme) else {
28+
throw RCTVideoError.embeddedKeyExtractionFailed
29+
}
30+
31+
let persistentKey = try keyRequest.persistableContentKey(fromKeyVendorResponse: persistentKeyData)
32+
try finishProcessingContentKeyRequest(keyRequest: keyRequest, license: persistentKey)
33+
}
34+
}

0 commit comments

Comments
 (0)