Skip to content

Commit

Permalink
feat(iOS): rewrite DRM Module (#4136)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
KrzysztofMoch authored Sep 20, 2024
1 parent c96f7d4 commit 0e4c95d
Show file tree
Hide file tree
Showing 15 changed files with 576 additions and 482 deletions.
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
// 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.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)
}
}
Loading

0 comments on commit 0e4c95d

Please sign in to comment.