From f4acaccd80a3e380940d014d18cda3d03d5720c2 Mon Sep 17 00:00:00 2001 From: Facundo Gutierrez Date: Thu, 5 Oct 2023 16:37:28 -0300 Subject: [PATCH] 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 --- Video.js | 8 +- docs/DRM.md | 9 +- .../Features/RCTResourceLoaderDelegate.swift | 89 ++++++++++++------- ios/Video/RCTVideo.swift | 8 +- ios/Video/RCTVideoManager.m | 6 +- ios/Video/RCTVideoManager.swift | 12 +-- 6 files changed, 80 insertions(+), 52 deletions(-) diff --git a/Video.js b/Video.js index 44616b54..c6abf501 100644 --- a/Video.js +++ b/Video.js @@ -279,15 +279,15 @@ export default class Video extends Component { const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not. getLicensePromise.then((result => { if (result !== undefined) { - NativeModules.VideoManager.setLicenseResult(result, findNodeHandle(this._root)); + NativeModules.VideoManager.setLicenseResult(result, data.licenseUrl, findNodeHandle(this._root)); } 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) => { - NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError(error, findNodeHandle(this._root)); + NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError(error, data.licenseUrl, findNodeHandle(this._root)); }); } 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)); } } } diff --git a/docs/DRM.md b/docs/DRM.md index d0ce88bf..517e23d2 100644 --- a/docs/DRM.md +++ b/docs/DRM.md @@ -31,13 +31,16 @@ Platforms: iOS ### `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];`. - You should return on this method a `CKC` in Base64, either by just returning it or returning a `Promise` that resolves with the `CKC`. +`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];`. + +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: ```js -getLicense: (spcString) => { +getLicense: (spcString, contentId, licenseUrl) => { const base64spc = Base64.encode(spcString); const formData = new FormData(); formData.append('spc', base64spc); diff --git a/ios/Video/Features/RCTResourceLoaderDelegate.swift b/ios/Video/Features/RCTResourceLoaderDelegate.swift index 52228517..07b8e62a 100644 --- a/ios/Video/Features/RCTResourceLoaderDelegate.swift +++ b/ios/Video/Features/RCTResourceLoaderDelegate.swift @@ -3,7 +3,7 @@ import Promises class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate { - private var _loadingRequest:AVAssetResourceLoadingRequest? + private var _loadingRequests: [String: AVAssetResourceLoadingRequest?] = [:] private var _requestingCertificate:Bool = false private var _requestingCertificateErrored:Bool = false private var _drm: DRMParams? @@ -32,7 +32,9 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes } deinit { - _loadingRequest?.finishLoading() + for request in _loadingRequests.values { + request?.finishLoading() + } } func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest:AVAssetResourceRenewalRequest) -> Bool { @@ -47,41 +49,59 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes RCTLog("didCancelLoadingRequest") } - func setLicenseResult(_ license:String!) { - guard let respondData = RCTVideoUtils.base64DataFromBase64String(base64String: license), - let _loadingRequest = _loadingRequest else { - setLicenseResultError("No data from JS license response") - return - } - let dataRequest:AVAssetResourceLoadingDataRequest! = _loadingRequest.dataRequest + 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 \(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() + loadingRequest!.finishLoading() + _loadingRequests.removeValue(forKey: licenseUrl) } - func setLicenseResultError(_ error:String!) { - if _loadingRequest != nil { - self.finishLoadingWithError(error: RCTVideoErrorHandler.fromJSPart(error)) + 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!) -> Bool { - if let _loadingRequest = _loadingRequest, let error = error { - _loadingRequest.finishLoading(with: error as! NSError) - - _onVideoError?([ - "error": [ - "code": NSNumber(value: (error as NSError).code), - "localizedDescription": error.localizedDescription == nil ? "" : error.localizedDescription, - "localizedFailureReason": ((error as NSError).localizedFailureReason == nil ? "" : (error as NSError).localizedFailureReason) ?? "", - "localizedRecoverySuggestion": ((error as NSError).localizedRecoverySuggestion == nil ? "" : (error as NSError).localizedRecoverySuggestion) ?? "", - "domain": (error as NSError).domain - ], - "target": _reactTag - ]) - + 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 + ]) + return false } + func loadingRequestHandling(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool { if handleEmbeddedKey(loadingRequest) { @@ -118,10 +138,13 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes } else if _requestingCertificateErrored { 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 { - return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData) + return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData, licenseUrl: requestKey) } var promise: Promise @@ -134,8 +157,8 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes base64Certificate:_drm.base64Certificate ) .then{ spcData -> Void in self._requestingCertificate = true - self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? "", - "contentId": contentId, + self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? loadingRequest.request.url?.absoluteString ?? "", + "contentId": contentId ?? "", "spcBase64": spcData.base64EncodedString(options: []), "target": self._reactTag]) } @@ -158,7 +181,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes promise.catch{ error in - self.finishLoadingWithError(error:error) + self.finishLoadingWithError(error:error, licenseUrl: requestKey) self._requestingCertificateErrored = true } diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 8f9e9d16..f75ec0dd 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -1030,12 +1030,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH ) } - func setLicenseResult(_ license:String!) { - _resouceLoaderDelegate?.setLicenseResult(license) + func setLicenseResult(_ license:String!, _ licenseUrl: String!) { + _resouceLoaderDelegate?.setLicenseResult(license, licenseUrl) } - func setLicenseResultError(_ error:String!) { - _resouceLoaderDelegate?.setLicenseResultError(error) + func setLicenseResultError(_ error:String!, _ licenseUrl: String!) { + _resouceLoaderDelegate?.setLicenseResultError(error, licenseUrl) } func dismissFullscreenPlayer(_ error:String!) { diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 2810f142..a77ce134 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -69,10 +69,12 @@ RCT_EXTERN_METHOD(save:(NSDictionary *)options rejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(setLicenseResult:(NSString *)license + licenseUrl:(NSString *)licenseUrl reactTag:(nonnull NSNumber *)reactTag) -RCT_EXTERN_METHOD(setLicenseResultError(NSString *)error - reactTag:(nonnull NSNumber *)reactTag) +RCT_EXTERN_METHOD(setLicenseResultError:(NSString *)error + licenseUrl:(NSString *)licenseUrl + reactTag:(nonnull NSNumber *)reactTag) RCT_EXTERN_METHOD(setPlayerPauseState:(nonnull NSNumber *)paused reactTag:(nonnull NSNumber *)reactTag) diff --git a/ios/Video/RCTVideoManager.swift b/ios/Video/RCTVideoManager.swift index 64e5eccd..2b9e68cc 100644 --- a/ios/Video/RCTVideoManager.swift +++ b/ios/Video/RCTVideoManager.swift @@ -24,26 +24,26 @@ class RCTVideoManager: RCTViewManager { }) } - @objc(setLicenseResult:reactTag:) - func setLicenseResult(license: NSString, reactTag: NSNumber) -> Void { + @objc(setLicenseResult:licenseUrl:reactTag:) + func setLicenseResult(license: NSString, licenseUrl:NSString, reactTag: NSNumber) -> Void { bridge.uiManager.prependUIBlock({_ , viewRegistry in let view = viewRegistry?[reactTag] if !(view is RCTVideo) { RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) } else if let view = view as? RCTVideo { - view.setLicenseResult(license as String) + view.setLicenseResult(license as String, licenseUrl as String) } }) } - @objc(setLicenseResultError:reactTag:) - func setLicenseResultError(error: NSString, reactTag: NSNumber) -> Void { + @objc(setLicenseResultError:licenseUrl:reactTag:) + func setLicenseResultError(error: NSString, licenseUrl:NSString, reactTag: NSNumber) -> Void { bridge.uiManager.prependUIBlock({_ , viewRegistry in let view = viewRegistry?[reactTag] if !(view is RCTVideo) { RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) } else if let view = view as? RCTVideo { - view.setLicenseResultError(error as String) + view.setLicenseResultError(error as String, licenseUrl as String) } }) }