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:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							c6ee294403
						
					
				
				
					commit
					f4acaccd80
				
			
							
								
								
									
										8
									
								
								Video.js
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								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));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Data>
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 
 | 
			
		||||
@@ -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!) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user