fix(ios): fairplay different key per asset (#3261)

* [Fix] Replace _loadingRequest instance with _loadingRequests dictionary to support multiple concurrent requests
* Remove stored finished requests from dictionary
* Keep contentId as is, and send loadingRequest.url in licenseUrl.
* Update DRM.md

---------

Co-authored-by: Facundo Gutierrez <facundo.gutierrez@tcc.com.uy>
This commit is contained in:
Facundo Gutierrez 2023-10-05 16:37:28 -03:00 committed by GitHub
parent c6ee294403
commit f4acaccd80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 80 additions and 52 deletions

View File

@ -279,15 +279,15 @@ export default class Video extends Component {
const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not. const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not.
getLicensePromise.then((result => { getLicensePromise.then((result => {
if (result !== undefined) { if (result !== undefined) {
NativeModules.VideoManager.setLicenseResult(result, findNodeHandle(this._root)); NativeModules.VideoManager.setLicenseResult(result, data.licenseUrl, findNodeHandle(this._root));
} else { } else {
NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('Empty license result', findNodeHandle(this._root)); NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('Empty license result', data.licenseUrl, findNodeHandle(this._root));
} }
})).catch((error) => { })).catch((error) => {
NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError(error, findNodeHandle(this._root)); NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError(error, data.licenseUrl, findNodeHandle(this._root));
}); });
} else { } else {
NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('No spc received', findNodeHandle(this._root)); NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('No spc received', data.licenseUrl, findNodeHandle(this._root));
} }
} }
} }

View File

@ -32,12 +32,15 @@ Platforms: iOS
### `getLicense` ### `getLicense`
`licenseServer` and `headers` will be ignored. You will obtain as argument the `SPC` (as ASCII string, you will probably need to convert it to base 64) obtained from your `contentId` + the provided certificate via `[loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError];`. `licenseServer` and `headers` will be ignored. You will obtain as argument the `SPC` (as ASCII string, you will probably need to convert it to base 64) obtained from your `contentId` + the provided certificate via `[loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError];`.
You should return on this method a `CKC` in Base64, either by just returning it or returning a `Promise` that resolves with the `CKC`.
Also, you will receive the `contentId` and a `licenseUrl` URL defined as `loadingRequest.request.URL.absoluteString ` or as the `licenseServer` prop if it's passed.
You should return on this method a `CKC` in Base64, either by just returning it or returning a `Promise` that resolves with the `CKC`.
With this prop you can override the license acquisition flow, as an example: With this prop you can override the license acquisition flow, as an example:
```js ```js
getLicense: (spcString) => { getLicense: (spcString, contentId, licenseUrl) => {
const base64spc = Base64.encode(spcString); const base64spc = Base64.encode(spcString);
const formData = new FormData(); const formData = new FormData();
formData.append('spc', base64spc); formData.append('spc', base64spc);

View File

@ -3,7 +3,7 @@ import Promises
class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate { class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate {
private var _loadingRequest:AVAssetResourceLoadingRequest? private var _loadingRequests: [String: AVAssetResourceLoadingRequest?] = [:]
private var _requestingCertificate:Bool = false private var _requestingCertificate:Bool = false
private var _requestingCertificateErrored:Bool = false private var _requestingCertificateErrored:Bool = false
private var _drm: DRMParams? private var _drm: DRMParams?
@ -32,7 +32,9 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
} }
deinit { deinit {
_loadingRequest?.finishLoading() for request in _loadingRequests.values {
request?.finishLoading()
}
} }
func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest:AVAssetResourceRenewalRequest) -> Bool { func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest:AVAssetResourceRenewalRequest) -> Bool {
@ -47,42 +49,60 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
RCTLog("didCancelLoadingRequest") RCTLog("didCancelLoadingRequest")
} }
func setLicenseResult(_ license:String!) { func setLicenseResult(_ license:String!,_ licenseUrl: String!) {
guard let respondData = RCTVideoUtils.base64DataFromBase64String(base64String: license),
let _loadingRequest = _loadingRequest else { // Check if the loading request exists in _loadingRequests based on licenseUrl
setLicenseResultError("No data from JS license response") guard let loadingRequest = _loadingRequests[licenseUrl] else {
setLicenseResultError("Loading request for licenseUrl \(licenseUrl) not found", licenseUrl)
return return
} }
let dataRequest:AVAssetResourceLoadingDataRequest! = _loadingRequest.dataRequest
// 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) dataRequest.respond(with: respondData)
_loadingRequest.finishLoading() loadingRequest!.finishLoading()
_loadingRequests.removeValue(forKey: licenseUrl)
} }
func setLicenseResultError(_ error:String!) { func setLicenseResultError(_ error:String!,_ licenseUrl: String!) {
if _loadingRequest != nil { // Check if the loading request exists in _loadingRequests based on licenseUrl
self.finishLoadingWithError(error: RCTVideoErrorHandler.fromJSPart(error)) guard let loadingRequest = _loadingRequests[licenseUrl] else {
} print("Loading request for licenseUrl \(licenseUrl) not found. Error: \(error)")
return
} }
func finishLoadingWithError(error:Error!) -> Bool { self.finishLoadingWithError(error: RCTVideoErrorHandler.fromJSPart(error), licenseUrl: licenseUrl)
if let _loadingRequest = _loadingRequest, let error = error { }
_loadingRequest.finishLoading(with: error as! NSError)
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?([ _onVideoError?([
"error": [ "error": [
"code": NSNumber(value: (error as NSError).code), "code": NSNumber(value: error.code),
"localizedDescription": error.localizedDescription == nil ? "" : error.localizedDescription, "localizedDescription": error.localizedDescription ?? "",
"localizedFailureReason": ((error as NSError).localizedFailureReason == nil ? "" : (error as NSError).localizedFailureReason) ?? "", "localizedFailureReason": error.localizedFailureReason ?? "",
"localizedRecoverySuggestion": ((error as NSError).localizedRecoverySuggestion == nil ? "" : (error as NSError).localizedRecoverySuggestion) ?? "", "localizedRecoverySuggestion": error.localizedRecoverySuggestion ?? "",
"domain": (error as NSError).domain "domain": error.domain
], ],
"target": _reactTag "target": _reactTag
]) ])
}
return false return false
} }
func loadingRequestHandling(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool { func loadingRequestHandling(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool {
if handleEmbeddedKey(loadingRequest) { if handleEmbeddedKey(loadingRequest) {
return true return true
@ -118,10 +138,13 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
} else if _requestingCertificateErrored { } else if _requestingCertificateErrored {
return false return false
} }
_loadingRequest = loadingRequest
var requestKey: String = loadingRequest.request.url?.absoluteString ?? ""
_loadingRequests[requestKey] = loadingRequest
guard let _drm = _drm, let drmType = _drm.type, drmType == "fairplay" else { guard let _drm = _drm, let drmType = _drm.type, drmType == "fairplay" else {
return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData) return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData, licenseUrl: requestKey)
} }
var promise: Promise<Data> var promise: Promise<Data>
@ -134,8 +157,8 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
base64Certificate:_drm.base64Certificate base64Certificate:_drm.base64Certificate
) .then{ spcData -> Void in ) .then{ spcData -> Void in
self._requestingCertificate = true self._requestingCertificate = true
self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? "", self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? loadingRequest.request.url?.absoluteString ?? "",
"contentId": contentId, "contentId": contentId ?? "",
"spcBase64": spcData.base64EncodedString(options: []), "spcBase64": spcData.base64EncodedString(options: []),
"target": self._reactTag]) "target": self._reactTag])
} }
@ -158,7 +181,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes
promise.catch{ error in promise.catch{ error in
self.finishLoadingWithError(error:error) self.finishLoadingWithError(error:error, licenseUrl: requestKey)
self._requestingCertificateErrored = true self._requestingCertificateErrored = true
} }

View File

@ -1030,12 +1030,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
) )
} }
func setLicenseResult(_ license:String!) { func setLicenseResult(_ license:String!, _ licenseUrl: String!) {
_resouceLoaderDelegate?.setLicenseResult(license) _resouceLoaderDelegate?.setLicenseResult(license, licenseUrl)
} }
func setLicenseResultError(_ error:String!) { func setLicenseResultError(_ error:String!, _ licenseUrl: String!) {
_resouceLoaderDelegate?.setLicenseResultError(error) _resouceLoaderDelegate?.setLicenseResultError(error, licenseUrl)
} }
func dismissFullscreenPlayer(_ error:String!) { func dismissFullscreenPlayer(_ error:String!) {

View File

@ -69,9 +69,11 @@ RCT_EXTERN_METHOD(save:(NSDictionary *)options
rejecter:(RCTPromiseRejectBlock)reject) rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(setLicenseResult:(NSString *)license RCT_EXTERN_METHOD(setLicenseResult:(NSString *)license
licenseUrl:(NSString *)licenseUrl
reactTag:(nonnull NSNumber *)reactTag) reactTag:(nonnull NSNumber *)reactTag)
RCT_EXTERN_METHOD(setLicenseResultError(NSString *)error RCT_EXTERN_METHOD(setLicenseResultError:(NSString *)error
licenseUrl:(NSString *)licenseUrl
reactTag:(nonnull NSNumber *)reactTag) reactTag:(nonnull NSNumber *)reactTag)
RCT_EXTERN_METHOD(setPlayerPauseState:(nonnull NSNumber *)paused RCT_EXTERN_METHOD(setPlayerPauseState:(nonnull NSNumber *)paused

View File

@ -24,26 +24,26 @@ class RCTVideoManager: RCTViewManager {
}) })
} }
@objc(setLicenseResult:reactTag:) @objc(setLicenseResult:licenseUrl:reactTag:)
func setLicenseResult(license: NSString, reactTag: NSNumber) -> Void { func setLicenseResult(license: NSString, licenseUrl:NSString, reactTag: NSNumber) -> Void {
bridge.uiManager.prependUIBlock({_ , viewRegistry in bridge.uiManager.prependUIBlock({_ , viewRegistry in
let view = viewRegistry?[reactTag] let view = viewRegistry?[reactTag]
if !(view is RCTVideo) { if !(view is RCTVideo) {
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
} else if let view = view as? RCTVideo { } else if let view = view as? RCTVideo {
view.setLicenseResult(license as String) view.setLicenseResult(license as String, licenseUrl as String)
} }
}) })
} }
@objc(setLicenseResultError:reactTag:) @objc(setLicenseResultError:licenseUrl:reactTag:)
func setLicenseResultError(error: NSString, reactTag: NSNumber) -> Void { func setLicenseResultError(error: NSString, licenseUrl:NSString, reactTag: NSNumber) -> Void {
bridge.uiManager.prependUIBlock({_ , viewRegistry in bridge.uiManager.prependUIBlock({_ , viewRegistry in
let view = viewRegistry?[reactTag] let view = viewRegistry?[reactTag]
if !(view is RCTVideo) { if !(view is RCTVideo) {
RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view))
} else if let view = view as? RCTVideo { } else if let view = view as? RCTVideo {
view.setLicenseResultError(error as String) view.setLicenseResultError(error as String, licenseUrl as String)
} }
}) })
} }