diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f586a3..41630b09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Changelog +### Version 5.1.0-alpha7 + +- Basic support for DRM on iOS and Android [#1445](https://github.com/react-native-community/react-native-video/pull/1445) + ### Version 5.1.0-alpha6 [WIP] - Fix iOS bug which would break size of views when video is displayed with controls on a non full-screen React view. [#1931](https://github.com/react-native-community/react-native-video/pull/1931) diff --git a/DRM.md b/DRM.md new file mode 100644 index 00000000..d0ce88bf --- /dev/null +++ b/DRM.md @@ -0,0 +1,139 @@ +# DRM + +## Provide DRM data (only tested with http/https assets) + +You can provide some configuration to allow DRM playback. +This feature will disable the use of `TextureView` on Android. + +DRM object allows this members: + +| Property | Type | Default | Platform | Description | +| --- | --- | --- | --- | --- | +| [`type`](#type) | DRMType | undefined | iOS/Android | Specifies which type of DRM you are going to use, DRMType is an enum exposed on the JS module ('fairplay', 'playready', ...) | +| [`licenseServer`](#licenseserver) | string | undefined | iOS/Android | Specifies the license server URL | +| [`headers`](#headers) | Object | undefined | iOS/Android | Specifies the headers send to the license server URL on license acquisition | +| [`contentId`](#contentid) | string | undefined | iOS | Specify the content id of the stream, otherwise it will take the host value from `loadingRequest.request.URL.host` (f.e: `skd://testAsset` -> will take `testAsset`) | +| [`certificateUrl`](#certificateurl) | string | undefined | iOS | Specifies the url to obtain your ios certificate for fairplay, Url to the .cer file | +| [`base64Certificate`](#base64certificate) | bool | false | iOS | Specifies whether or not the certificate returned by the `certificateUrl` is on base64 | +| [`getLicense`](#getlicense)| function | undefined | iOS | Rather than setting the `licenseServer` url to get the license, you can manually get the license on the JS part, and send the result to the native part to configure FairplayDRM for the stream | + +### `base64Certificate` + +Whether or not the certificate url returns it on base64. + +Platforms: iOS + +### `certificateUrl` + +URL to fetch a valid certificate for FairPlay. + +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`. + +With this prop you can override the license acquisition flow, as an example: + +```js +getLicense: (spcString) => { + const base64spc = Base64.encode(spcString); + const formData = new FormData(); + formData.append('spc', base64spc); + return fetch(`https://license.pallycon.com/ri/licenseManager.do`, { + method: 'POST', + headers: { + 'pallycon-customdata-v2': 'd2VpcmRiYXNlNjRzdHJpbmcgOlAgRGFuaWVsIE1hcmnxbyB3YXMgaGVyZQ==', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData + }).then(response => response.text()).then((response) => { + return response; + }).catch((error) => { + console.error('Error', error); + }); +} +``` + +Platforms: iOS + +### `headers` + +You can customize headers send to the licenseServer. + +Example: + +```js +source={{ + uri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd', +}} +drm={{ + type: DRMType.WIDEVINE, + licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', + headers: { + 'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU' + }, +}} +``` + +### `licenseServer` + +The URL pointing to the licenseServer that will provide the authorization to play the protected stream. + +### `type` + +You can specify the DRM type, either by string or using the exported DRMType enum. +Valid values are, for Android: DRMType.WIDEVINE / DRMType.PLAYREADY / DRMType.CLEARKEY. +for iOS: DRMType.FAIRPLAY + +## Common Usage Scenarios + +### Send cookies to license server + +You can send Cookies to the license server via `headers` prop. Example: + +```js +drm: { + type: DRMType.WIDEVINE + licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', + headers: { + 'Cookie': 'PHPSESSID=etcetc; csrftoken=mytoken; _gat=1; foo=bar' + }, +} +``` + +### Custom License Acquisition (only iOS for now) + +```js +drm: { + type: DRMType.FAIRPLAY, + getLicense: (spcString) => { + const base64spc = Base64.encode(spcString); + return fetch('YOUR LICENSE SERVER HERE', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + getFairplayLicense: { + foo: 'bar', + spcMessage: base64spc, + } + }) + }) + .then(response => response.json()) + .then((response) => { + if (response && response.getFairplayLicenseResponse + && response.getFairplayLicenseResponse.ckcResponse) { + return response.getFairplayLicenseResponse.ckcResponse; + } + throw new Error('No correct response'); + }) + .catch((error) => { + console.error('CKC error', error); + }); + } +} +``` diff --git a/DRMType.js b/DRMType.js new file mode 100644 index 00000000..473536b2 --- /dev/null +++ b/DRMType.js @@ -0,0 +1,6 @@ +export default { + WIDEVINE: 'widevine', + PLAYREADY: 'playready', + CLEARKEY: 'clearkey', + FAIRPLAY: 'fairplay' +}; diff --git a/README.md b/README.md index 00fc7932..644185c0 100644 --- a/README.md +++ b/README.md @@ -415,6 +415,11 @@ Determines whether video audio should override background music/audio in Android Platforms: Android Exoplayer +### DRM +To setup DRM please follow [this guide](./DRM.md) + +Platforms: Android Exoplayer, iOS + #### filter Add video filter * **FilterType.NONE (default)** - No Filter @@ -799,6 +804,17 @@ Note: Using this feature adding an entry for NSAppleMusicUsageDescription to you Platforms: iOS +##### Explicit mimetype for the stream + +Provide a member `type` with value (`mpd`/`m3u8`/`ism`) inside the source object. +Sometimes is needed when URL extension does not match with the mimetype that you are expecting, as seen on the next example. (Extension is .ism -smooth streaming- but file served is on format mpd -mpeg dash-) + +Example: +``` +source={{ uri: 'http://host-serving-a-type-different-than-the-extension.ism/manifest(format=mpd-time-csf)', +type: 'mpd' }} +``` + ###### Other protocols The following other types are supported on some platforms, but aren't fully documented yet: diff --git a/Video.js b/Video.js index 450a7796..6b84021f 100644 --- a/Video.js +++ b/Video.js @@ -4,6 +4,7 @@ import { StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; import TextTrackType from './TextTrackType'; import FilterType from './FilterType'; +import DRMType from './DRMType'; import VideoResizeMode from './VideoResizeMode.js'; const styles = StyleSheet.create({ @@ -12,7 +13,7 @@ const styles = StyleSheet.create({ }, }); -export { TextTrackType, FilterType }; +export { TextTrackType, FilterType, DRMType }; export default class Video extends Component { @@ -232,6 +233,26 @@ export default class Video extends Component { } }; + _onGetLicense = (event) => { + if (this.props.drm && this.props.drm.getLicense instanceof Function) { + const data = event.nativeEvent; + if (data && data.spc) { + const getLicenseOverride = this.props.drm.getLicense(data.spc, data.contentId, data.spcBase64, this.props); + 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)); + } else { + NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('Empty license result', findNodeHandle(this._root)); + } + })).catch((error) => { + NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError(error, findNodeHandle(this._root)); + }); + } else { + NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError("No spc received", findNodeHandle(this._root)); + } + } + } getViewManagerConfig = viewManagerName => { if (!NativeModules.UIManager.getViewManagerConfig) { return NativeModules.UIManager[viewManagerName]; @@ -304,6 +325,7 @@ export default class Video extends Component { onPlaybackRateChange: this._onPlaybackRateChange, onAudioFocusChanged: this._onAudioFocusChanged, onAudioBecomingNoisy: this._onAudioBecomingNoisy, + onGetLicense: nativeProps.drm && nativeProps.drm.getLicense && this._onGetLicense, onPictureInPictureStatusChanged: this._onPictureInPictureStatusChanged, onRestoreUserInterfaceForPictureInPictureStop: this._onRestoreUserInterfaceForPictureInPictureStop, }); @@ -379,6 +401,16 @@ Video.propTypes = { // Opaque type returned by require('./video.mp4') PropTypes.number, ]), + drm: PropTypes.shape({ + type: PropTypes.oneOf([ + DRMType.CLEARKEY, DRMType.FAIRPLAY, DRMType.WIDEVINE, DRMType.PLAYREADY + ]), + licenseServer: PropTypes.string, + headers: PropTypes.shape({}), + base64Certificate: PropTypes.bool, + certificateUrl: PropTypes.string, + getLicense: PropTypes.func, + }), minLoadRetryCount: PropTypes.number, maxBitRate: PropTypes.number, resizeMode: PropTypes.string, diff --git a/android-exoplayer/build.gradle b/android-exoplayer/build.gradle index 44918610..296e21cb 100644 --- a/android-exoplayer/build.gradle +++ b/android-exoplayer/build.gradle @@ -19,6 +19,11 @@ android { versionCode 1 versionName "1.0" } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.java index 487efeb0..19dda002 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.java @@ -22,6 +22,7 @@ public class DataSourceUtil { private static DataSource.Factory rawDataSourceFactory = null; private static DataSource.Factory defaultDataSourceFactory = null; + private static HttpDataSource.Factory defaultHttpDataSourceFactory = null; private static String userAgent = null; public static void setUserAgent(String userAgent) { @@ -58,6 +59,17 @@ public class DataSourceUtil { DataSourceUtil.defaultDataSourceFactory = factory; } + public static HttpDataSource.Factory getDefaultHttpDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map requestHeaders) { + if (defaultHttpDataSourceFactory == null || (requestHeaders != null && !requestHeaders.isEmpty())) { + defaultHttpDataSourceFactory = buildHttpDataSourceFactory(context, bandwidthMeter, requestHeaders); + } + return defaultHttpDataSourceFactory; + } + + public static void setDefaultHttpDataSourceFactory(HttpDataSource.Factory factory) { + DataSourceUtil.defaultHttpDataSourceFactory = factory; + } + private static DataSource.Factory buildRawDataSourceFactory(ReactContext context) { return new RawResourceDataSourceFactory(context.getApplicationContext()); } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 4e6fea58..b41b2768 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -36,6 +36,13 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.metadata.Metadata; @@ -70,6 +77,7 @@ import java.net.CookieManager; import java.net.CookiePolicy; import java.util.ArrayList; import java.util.Locale; +import java.util.UUID; import java.util.Map; @SuppressLint("ViewConstructor") @@ -79,7 +87,8 @@ class ReactExoplayerView extends FrameLayout implements BandwidthMeter.EventListener, BecomingNoisyListener, AudioManager.OnAudioFocusChangeListener, - MetadataOutput { + MetadataOutput, + DefaultDrmSessionEventListener { private static final String TAG = "ReactExoplayerView"; @@ -124,6 +133,8 @@ class ReactExoplayerView extends FrameLayout implements private int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; private int bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; + private Handler mainHandler; + // Props from React private Uri srcUri; private String extension; @@ -141,6 +152,9 @@ class ReactExoplayerView extends FrameLayout implements private boolean playInBackground = false; private Map requestHeaders; private boolean mReportBandwidth = false; + private UUID drmUUID = null; + private String drmLicenseUrl = null; + private String[] drmLicenseHeader = null; private boolean controls; // \ End props @@ -189,8 +203,6 @@ class ReactExoplayerView extends FrameLayout implements audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); themedReactContext.addLifecycleEventListener(this); audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext); - - initializePlayer(); } @@ -214,6 +226,8 @@ class ReactExoplayerView extends FrameLayout implements exoPlayerView.setLayoutParams(layoutParams); addView(exoPlayerView, 0, layoutParams); + + mainHandler = new Handler(); } @Override @@ -395,9 +409,23 @@ class ReactExoplayerView extends FrameLayout implements DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext()) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); - // TODO: Add drmSessionManager to 5th param from: https://github.com/react-native-community/react-native-video/pull/1445 + // DRM + DrmSessionManager drmSessionManager = null; + if (self.drmUUID != null) { + try { + drmSessionManager = buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl, + self.drmLicenseHeader); + } catch (UnsupportedDrmException e) { + int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported + : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME + ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); + eventEmitter.error(getResources().getString(errorStringId), e); + return; + } + } + // End DRM player = ExoPlayerFactory.newSimpleInstance(getContext(), renderersFactory, - trackSelector, defaultLoadControl, null, bandwidthMeter); + trackSelector, defaultLoadControl, drmSessionManager, bandwidthMeter); player.addListener(self); player.addMetadataOutput(self); exoPlayerView.setPlayer(player); @@ -444,6 +472,23 @@ class ReactExoplayerView extends FrameLayout implements }, 1); } + private DrmSessionManager buildDrmSessionManager(UUID uuid, + String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { + if (Util.SDK_INT < 18) { + return null; + } + HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, + buildHttpDataSourceFactory(false)); + if (keyRequestPropertiesArray != null) { + for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { + drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], + keyRequestPropertiesArray[i + 1]); + } + } + return new DefaultDrmSessionManager<>(uuid, + FrameworkMediaDrm.newInstance(uuid), drmCallback, null, false, 3); + } + private MediaSource buildMediaSource(Uri uri, String overrideExtension) { int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension : uri.getLastPathSegment()); @@ -615,6 +660,18 @@ class ReactExoplayerView extends FrameLayout implements useBandwidthMeter ? bandwidthMeter : null, requestHeaders); } + /** + * Returns a new HttpDataSource factory. + * + * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new + * DataSource factory. + * @return A new HttpDataSource factory. + */ + private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) { + return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext, useBandwidthMeter ? bandwidthMeter : null, requestHeaders); + } + + // AudioManager.OnAudioFocusChangeListener implementation @Override @@ -924,10 +981,12 @@ class ReactExoplayerView extends FrameLayout implements } public int getTrackRendererIndex(int trackType) { - int rendererCount = player.getRendererCount(); - for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { - if (player.getRendererType(rendererIndex) == trackType) { - return rendererIndex; + if (player != null) { + int rendererCount = player.getRendererCount(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + if (player.getRendererType(rendererIndex) == trackType) { + return rendererIndex; + } } } return C.INDEX_UNSET; @@ -1182,12 +1241,12 @@ class ReactExoplayerView extends FrameLayout implements } public void setRateModifier(float newRate) { - rate = newRate; + rate = newRate; - if (player != null) { - PlaybackParameters params = new PlaybackParameters(rate, 1f); - player.setPlaybackParameters(params); - } + if (player != null) { + PlaybackParameters params = new PlaybackParameters(rate, 1f); + player.setPlaybackParameters(params); + } } public void setMaxBitRateModifier(int newMaxBitRate) { @@ -1246,7 +1305,8 @@ class ReactExoplayerView extends FrameLayout implements } public void setUseTextureView(boolean useTextureView) { - exoPlayerView.setUseTextureView(useTextureView); + boolean finallyUseTextureView = useTextureView && this.drmUUID == null; + exoPlayerView.setUseTextureView(finallyUseTextureView); } public void setHideShutterView(boolean hideShutterView) { @@ -1262,6 +1322,40 @@ class ReactExoplayerView extends FrameLayout implements initializePlayer(); } + public void setDrmType(UUID drmType) { + this.drmUUID = drmType; + } + + public void setDrmLicenseUrl(String licenseUrl){ + this.drmLicenseUrl = licenseUrl; + } + + public void setDrmLicenseHeader(String[] header){ + this.drmLicenseHeader = header; + } + + + @Override + public void onDrmKeysLoaded() { + Log.d("DRM Info", "onDrmKeysLoaded"); + } + + @Override + public void onDrmSessionManagerError(Exception e) { + Log.d("DRM Info", "onDrmSessionManagerError"); + eventEmitter.error("onDrmSessionManagerError", e); + } + + @Override + public void onDrmKeysRestored() { + Log.d("DRM Info", "onDrmKeysRestored"); + } + + @Override + public void onDrmKeysRemoved() { + Log.d("DRM Info", "onDrmKeysRemoved"); + } + /** * Handling controls prop * diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index cf50fdae..0d81e0b2 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -3,19 +3,25 @@ package com.brentvatne.exoplayer; import android.content.Context; import android.net.Uri; import android.text.TextUtils; +import android.util.Log; import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.bridge.ReactMethod; +import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.upstream.RawResourceDataSource; import java.util.HashMap; +import java.util.ArrayList; import java.util.Map; +import java.util.UUID; import javax.annotation.Nullable; @@ -26,6 +32,10 @@ public class ReactExoplayerViewManager extends ViewGroupManager drmKeyRequestPropertiesList = new ArrayList<>(); + ReadableMapKeySetIterator itr = drmHeaders.keySetIterator(); + while (itr.hasNextKey()) { + String key = itr.nextKey(); + drmKeyRequestPropertiesList.add(key); + drmKeyRequestPropertiesList.add(drmHeaders.getString(key)); + } + videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); + } + videoView.setUseTextureView(false); + } + } + } + @ReactProp(name = PROP_SRC) public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) { Context context = videoView.getContext().getApplicationContext(); @@ -108,7 +143,6 @@ public class ReactExoplayerViewManager extends ViewGroupManager headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null; - if (TextUtils.isEmpty(uriString)) { return; } diff --git a/android-exoplayer/src/main/res/values/strings.xml b/android-exoplayer/src/main/res/values/strings.xml index 4f69ec34..1f037779 100644 --- a/android-exoplayer/src/main/res/values/strings.xml +++ b/android-exoplayer/src/main/res/values/strings.xml @@ -8,7 +8,12 @@ Unable to query device decoders Unable to instantiate decoder %1$s + + Protected content not supported on API levels below 18 Unrecognized media format + This device does not support the required DRM scheme + + An unknown DRM error occurred diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index 26d436c2..6fee2996 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -14,11 +14,11 @@ @class RCTEventDispatcher; #if __has_include() -@interface RCTVideo : UIView +@interface RCTVideo : UIView #elif TARGET_OS_TV -@interface RCTVideo : UIView +@interface RCTVideo : UIView #else -@interface RCTVideo : UIView +@interface RCTVideo : UIView #endif @property (nonatomic, copy) RCTDirectEventBlock onVideoLoadStart; @@ -42,11 +42,26 @@ @property (nonatomic, copy) RCTDirectEventBlock onVideoExternalPlaybackChange; @property (nonatomic, copy) RCTDirectEventBlock onPictureInPictureStatusChanged; @property (nonatomic, copy) RCTDirectEventBlock onRestoreUserInterfaceForPictureInPictureStop; +@property (nonatomic, copy) RCTDirectEventBlock onGetLicense; + +typedef NS_ENUM(NSInteger, RCTVideoError) { + RCTVideoErrorFromJSPart, + RCTVideoErrorLicenseRequestNotOk, + RCTVideoErrorNoDataFromLicenseRequest, + RCTVideoErrorNoSPC, + RCTVideoErrorNoDataRequest, + RCTVideoErrorNoCertificateData, + RCTVideoErrorNoCertificateURL, + RCTVideoErrorNoFairplayDRM, + RCTVideoErrorNoDRMData +}; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem; - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +- (void)setLicenseResult:(NSString * )license; +- (BOOL)setLicenseResultError:(NSString * )error; @end diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 01e1b1c6..8780f48f 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -33,6 +33,12 @@ static int const RCTVideoUnset = -1; BOOL _playerLayerObserverSet; RCTVideoPlayerViewController *_playerViewController; NSURL *_videoURL; + BOOL _requestingCertificate; + BOOL _requestingCertificateErrored; + + /* DRM */ + NSDictionary *_drm; + AVAssetResourceLoadingRequest *_loadingRequest; /* Required to publish events */ RCTEventDispatcher *_eventDispatcher; @@ -146,14 +152,14 @@ static int const RCTVideoUnset = -1; - (RCTVideoPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem { - RCTVideoPlayerViewController* viewController = [[RCTVideoPlayerViewController alloc] init]; - viewController.showsPlaybackControls = YES; - viewController.rctDelegate = self; - viewController.preferredOrientation = _fullscreenOrientation; - - viewController.view.frame = self.bounds; - viewController.player = player; - return viewController; + RCTVideoPlayerViewController* viewController = [[RCTVideoPlayerViewController alloc] init]; + viewController.showsPlaybackControls = YES; + viewController.rctDelegate = self; + viewController.preferredOrientation = _fullscreenOrientation; + + viewController.view.frame = self.bounds; + viewController.player = player; + return viewController; } /* --------------------------------------------------------- @@ -247,11 +253,11 @@ static int const RCTVideoUnset = -1; - (void)audioRouteChanged:(NSNotification *)notification { - NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; - NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; - if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { - self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); - } + NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; + NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; + if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { + self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); + } } #pragma mark - Progress @@ -354,16 +360,16 @@ static int const RCTVideoUnset = -1; [self removePlayerLayer]; [self removePlayerTimeObserver]; [self removePlayerItemObservers]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) 0), dispatch_get_main_queue(), ^{ - + // perform on next run loop, otherwise other passed react-props may not be set - [self playerItemForSource:source withCallback:^(AVPlayerItem * playerItem) { + [self playerItemForSource:self->_source withCallback:^(AVPlayerItem * playerItem) { + self->_playerItem = playerItem; _playerItem = playerItem; [self setPreferredForwardBufferDuration:_preferredForwardBufferDuration]; [self addPlayerItemObservers]; - [self setFilter:_filterName]; - [self setMaxBitRate:_maxBitRate]; + [self setFilter:self->_filterName]; + [self setMaxBitRate:self->_maxBitRate]; [_player pause]; @@ -371,20 +377,20 @@ static int const RCTVideoUnset = -1; [_player removeObserver:self forKeyPath:playbackRate context:nil]; _playbackRateObserverRegistered = NO; } - if (_isExternalPlaybackActiveObserverRegistered) { - [_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; - _isExternalPlaybackActiveObserverRegistered = NO; + if (self->_isExternalPlaybackActiveObserverRegistered) { + [self->_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; + self->_isExternalPlaybackActiveObserverRegistered = NO; } - - _player = [AVPlayer playerWithPlayerItem:_playerItem]; - _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; - - [_player addObserver:self forKeyPath:playbackRate options:0 context:nil]; - _playbackRateObserverRegistered = YES; - [_player addObserver:self forKeyPath:externalPlaybackActive options:0 context:nil]; - _isExternalPlaybackActiveObserverRegistered = YES; - + self->_player = [AVPlayer playerWithPlayerItem:self->_playerItem]; + self->_player.actionAtItemEnd = AVPlayerActionAtItemEndNone; + + [self->_player addObserver:self forKeyPath:playbackRate options:0 context:nil]; + self->_playbackRateObserverRegistered = YES; + + [self->_player addObserver:self forKeyPath:externalPlaybackActive options:0 context:nil]; + self->_isExternalPlaybackActiveObserverRegistered = YES; + [self addPlayerTimeObserver]; if (@available(iOS 10.0, *)) { [self setAutomaticallyWaitsToMinimizeStalling:_automaticallyWaitsToMinimizeStalling]; @@ -392,13 +398,14 @@ static int const RCTVideoUnset = -1; //Perform on next run loop, otherwise onVideoLoadStart is nil if (self.onVideoLoadStart) { - id uri = [source objectForKey:@"uri"]; - id type = [source objectForKey:@"type"]; + id uri = [self->_source objectForKey:@"uri"]; + id type = [self->_source objectForKey:@"type"]; self.onVideoLoadStart(@{@"src": @{ - @"uri": uri ? uri : [NSNull null], - @"type": type ? type : [NSNull null], - @"isNetwork": [NSNumber numberWithBool:(bool)[source objectForKey:@"isNetwork"]]}, - @"target": self.reactTag + @"uri": uri ? uri : [NSNull null], + @"type": type ? type : [NSNull null], + @"isNetwork": [NSNumber numberWithBool:(bool)[self->_source objectForKey:@"isNetwork"]]}, + @"drm": self->_drm ? self->_drm : [NSNull null], + @"target": self.reactTag }); } }]; @@ -406,6 +413,10 @@ static int const RCTVideoUnset = -1; _videoLoadStarted = YES; } +- (void)setDrm:(NSDictionary *)drm { + _drm = drm; +} + - (NSURL*) urlFilePath:(NSString*) filepath { if ([filepath containsString:@"file://"]) { return [NSURL URLWithString:filepath]; @@ -436,7 +447,7 @@ static int const RCTVideoUnset = -1; // AVPlayer can't airplay AVMutableCompositions _allowsExternalPlayback = NO; - + // sideload text tracks AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init]; @@ -477,7 +488,7 @@ static int const RCTVideoUnset = -1; if (validTextTracks.count != _textTracks.count) { [self setTextTracks:validTextTracks]; } - + handler([AVPlayerItem playerItemWithAsset:mixComposition]); } @@ -488,11 +499,12 @@ static int const RCTVideoUnset = -1; bool shouldCache = [RCTConvert BOOL:[source objectForKey:@"shouldCache"]]; NSString *uri = [source objectForKey:@"uri"]; NSString *type = [source objectForKey:@"type"]; + AVURLAsset *asset; if (!uri || [uri isEqualToString:@""]) { DebugLog(@"Could not find video URL in source '%@'", source); return; } - + NSURL *url = isNetwork || isAsset ? [NSURL URLWithString:uri] : [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]]; @@ -505,7 +517,7 @@ static int const RCTVideoUnset = -1; } NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; [assetOptions setObject:cookies forKey:AVURLAssetHTTPCookiesKey]; - + #if __has_include() if (shouldCache && (!_textTracks || !_textTracks.count)) { /* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying @@ -517,61 +529,69 @@ static int const RCTVideoUnset = -1; return; } #endif - - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:assetOptions]; - [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; - return; + + asset = [AVURLAsset URLAssetWithURL:url options:assetOptions]; } else if (isAsset) { - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil]; - [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; - return; + asset = [AVURLAsset URLAssetWithURL:url options:nil]; + } else { + asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]] options:nil]; } - - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]] options:nil]; + // Reset _loadingRequest + if (_loadingRequest != nil) { + [_loadingRequest finishLoading]; + } + _requestingCertificate = NO; + _requestingCertificateErrored = NO; + // End Reset _loadingRequest + if (self->_drm != nil) { + dispatch_queue_t queue = dispatch_queue_create("assetQueue", nil); + [asset.resourceLoader setDelegate:self queue:queue]; + } + [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; } #if __has_include() - (void)playerItemForSourceUsingCache:(NSString *)uri assetOptions:(NSDictionary *)options withCallback:(void(^)(AVPlayerItem *))handler { - NSURL *url = [NSURL URLWithString:uri]; - [_videoCache getItemForUri:uri withCallback:^(RCTVideoCacheStatus videoCacheStatus, AVAsset * _Nullable cachedAsset) { - switch (videoCacheStatus) { - case RCTVideoCacheStatusMissingFileExtension: { - DebugLog(@"Could not generate cache key for uri '%@'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; - [self playerItemPrepareText:asset assetOptions:options withCallback:handler]; - return; - } - case RCTVideoCacheStatusUnsupportedFileExtension: { - DebugLog(@"Could not generate cache key for uri '%@'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; - [self playerItemPrepareText:asset assetOptions:options withCallback:handler]; - return; - } - default: - if (cachedAsset) { - DebugLog(@"Playing back uri '%@' from cache", uri); - // See note in playerItemForSource about not being able to support text tracks & caching - handler([AVPlayerItem playerItemWithAsset:cachedAsset]); - return; - } + NSURL *url = [NSURL URLWithString:uri]; + [_videoCache getItemForUri:uri withCallback:^(RCTVideoCacheStatus videoCacheStatus, AVAsset * _Nullable cachedAsset) { + switch (videoCacheStatus) { + case RCTVideoCacheStatusMissingFileExtension: { + DebugLog(@"Could not generate cache key for uri '%@'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; + [self playerItemPrepareText:asset assetOptions:options withCallback:handler]; + return; + } + case RCTVideoCacheStatusUnsupportedFileExtension: { + DebugLog(@"Could not generate cache key for uri '%@'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); + AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; + [self playerItemPrepareText:asset assetOptions:options withCallback:handler]; + return; + } + default: + if (cachedAsset) { + DebugLog(@"Playing back uri '%@' from cache", uri); + // See note in playerItemForSource about not being able to support text tracks & caching + handler([AVPlayerItem playerItemWithAsset:cachedAsset]); + return; } - - DVURLAsset *asset = [[DVURLAsset alloc] initWithURL:url options:options networkTimeout:10000]; - asset.loaderDelegate = self; - - /* More granular code to have control over the DVURLAsset - DVAssetLoaderDelegate *resourceLoaderDelegate = [[DVAssetLoaderDelegate alloc] initWithURL:url]; - resourceLoaderDelegate.delegate = self; - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO]; - components.scheme = [DVAssetLoaderDelegate scheme]; - AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[components URL] options:options]; - [asset.resourceLoader setDelegate:resourceLoaderDelegate queue:dispatch_get_main_queue()]; - */ - - handler([AVPlayerItem playerItemWithAsset:asset]); - }]; + } + + DVURLAsset *asset = [[DVURLAsset alloc] initWithURL:url options:options networkTimeout:10000]; + asset.loaderDelegate = self; + + /* More granular code to have control over the DVURLAsset + DVAssetLoaderDelegate *resourceLoaderDelegate = [[DVAssetLoaderDelegate alloc] initWithURL:url]; + resourceLoaderDelegate.delegate = self; + NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO]; + components.scheme = [DVAssetLoaderDelegate scheme]; + AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[components URL] options:options]; + [asset.resourceLoader setDelegate:resourceLoaderDelegate queue:dispatch_get_main_queue()]; + */ + + handler([AVPlayerItem playerItemWithAsset:asset]); + }]; } #pragma mark - DVAssetLoaderDelegate @@ -579,9 +599,9 @@ static int const RCTVideoUnset = -1; - (void)dvAssetLoaderDelegate:(DVAssetLoaderDelegate *)loaderDelegate didLoadData:(NSData *)data forURL:(NSURL *)url { - [_videoCache storeItem:data forUri:[url absoluteString] withCallback:^(BOOL success) { - DebugLog(@"Cache data stored successfully 🎉"); - }]; + [_videoCache storeItem:data forUri:[url absoluteString] withCallback:^(BOOL success) { + DebugLog(@"Cache data stored successfully 🎉"); + }]; } #endif @@ -679,7 +699,10 @@ static int const RCTVideoUnset = -1; [self applyModifiers]; } else if (_playerItem.status == AVPlayerItemStatusFailed && self.onVideoError) { self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: _playerItem.error.code], - @"domain": _playerItem.error.domain}, + @"localizedDescription": [_playerItem.error localizedDescription] == nil ? @"" : [_playerItem.error localizedDescription], + @"localizedFailureReason": [_playerItem.error localizedFailureReason] == nil ? @"" : [_playerItem.error localizedFailureReason], + @"localizedRecoverySuggestion": [_playerItem.error localizedRecoverySuggestion] == nil ? @"" : [_playerItem.error localizedRecoverySuggestion], + @"domain": _playerItem != nil && _playerItem.error != nil ? _playerItem.error.domain : @"RTCVideo"}, @"target": self.reactTag}); } } else if ([keyPath isEqualToString:playbackBufferEmptyKeyPath]) { @@ -708,10 +731,10 @@ static int const RCTVideoUnset = -1; } } else if([keyPath isEqualToString:externalPlaybackActive]) { - if(self.onVideoExternalPlaybackChange) { - self.onVideoExternalPlaybackChange(@{@"isExternalPlaybackActive": [NSNumber numberWithBool:_player.isExternalPlaybackActive], - @"target": self.reactTag}); - } + if(self.onVideoExternalPlaybackChange) { + self.onVideoExternalPlaybackChange(@{@"isExternalPlaybackActive": [NSNumber numberWithBool:_player.isExternalPlaybackActive], + @"target": self.reactTag}); + } } } else if (object == _playerViewController.contentOverlayView) { // when controls==true, this is a hack to reset the rootview when rotation happens in fullscreen @@ -752,7 +775,7 @@ static int const RCTVideoUnset = -1; selector:@selector(playbackStalled:) name:AVPlayerItemPlaybackStalledNotification object:nil]; - + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemNewAccessLogEntryNotification object:nil]; @@ -760,18 +783,35 @@ static int const RCTVideoUnset = -1; selector:@selector(handleAVPlayerAccess:) name:AVPlayerItemNewAccessLogEntryNotification object:nil]; - + [[NSNotificationCenter defaultCenter] removeObserver:self + name: AVPlayerItemFailedToPlayToEndTimeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didFailToFinishPlaying:) + name: AVPlayerItemFailedToPlayToEndTimeNotification + object:nil]; + } - (void)handleAVPlayerAccess:(NSNotification *)notification { - AVPlayerItemAccessLog *accessLog = [((AVPlayerItem *)notification.object) accessLog]; - AVPlayerItemAccessLogEvent *lastEvent = accessLog.events.lastObject; - - /* TODO: get this working - if (self.onBandwidthUpdate) { - self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]}); - } - */ + AVPlayerItemAccessLog *accessLog = [((AVPlayerItem *)notification.object) accessLog]; + AVPlayerItemAccessLogEvent *lastEvent = accessLog.events.lastObject; + + /* TODO: get this working + if (self.onBandwidthUpdate) { + self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]}); + } + */ +} + +- (void)didFailToFinishPlaying:(NSNotification *)notification { + NSError *error = notification.userInfo[AVPlayerItemFailedToPlayToEndTimeErrorKey]; + self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: error.code], + @"localizedDescription": [error localizedDescription] == nil ? @"" : [error localizedDescription], + @"localizedFailureReason": [error localizedFailureReason] == nil ? @"" : [error localizedFailureReason], + @"localizedRecoverySuggestion": [error localizedRecoverySuggestion] == nil ? @"" : [error localizedRecoverySuggestion], + @"domain": error.domain}, + @"target": self.reactTag}); } - (void)playbackStalled:(NSNotification *)notification @@ -825,8 +865,8 @@ static int const RCTVideoUnset = -1; - (void)setAllowsExternalPlayback:(BOOL)allowsExternalPlayback { - _allowsExternalPlayback = allowsExternalPlayback; - _player.allowsExternalPlayback = _allowsExternalPlayback; + _allowsExternalPlayback = allowsExternalPlayback; + _player.allowsExternalPlayback = _allowsExternalPlayback; } - (void)setPlayWhenInactive:(BOOL)playWhenInactive @@ -840,7 +880,7 @@ static int const RCTVideoUnset = -1; if (_pictureInPicture == pictureInPicture) { return; } - + _pictureInPicture = pictureInPicture; if (_pipController && _pictureInPicture && ![_pipController isPictureInPictureActive]) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -849,7 +889,7 @@ static int const RCTVideoUnset = -1; } else if (_pipController && !_pictureInPicture && [_pipController isPictureInPictureActive]) { dispatch_async(dispatch_get_main_queue(), ^{ [_pipController stopPictureInPicture]; - }); + }); } #endif } @@ -1053,52 +1093,52 @@ static int const RCTVideoUnset = -1; - (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)characteristic withCriteria:(NSDictionary *)criteria { - NSString *type = criteria[@"type"]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:characteristic]; - AVMediaSelectionOption *mediaOption; + NSString *type = criteria[@"type"]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:characteristic]; + AVMediaSelectionOption *mediaOption; - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { - NSString *value = criteria[@"value"]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *optionValue; - if ([type isEqualToString:@"language"]) { - optionValue = [currentOption extendedLanguageTag]; - } else { - optionValue = [[[currentOption commonMetadata] - valueForKey:@"value"] - objectAtIndex:0]; - } - if ([value isEqualToString:optionValue]) { - mediaOption = currentOption; - break; - } + if ([type isEqualToString:@"disabled"]) { + // Do nothing. We want to ensure option is nil + } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { + NSString *value = criteria[@"value"]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *optionValue; + if ([type isEqualToString:@"language"]) { + optionValue = [currentOption extendedLanguageTag]; + } else { + optionValue = [[[currentOption commonMetadata] + valueForKey:@"value"] + objectAtIndex:0]; + } + if ([value isEqualToString:optionValue]) { + mediaOption = currentOption; + break; } - //} else if ([type isEqualToString:@"default"]) { - // option = group.defaultOption; */ - } else if ([type isEqualToString:@"index"]) { - if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [criteria[@"value"] intValue]; - if (group.options.count > index) { - mediaOption = [group.options objectAtIndex:index]; - } - } - } else { // default. invalid type or "system" - [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; - return; } + //} else if ([type isEqualToString:@"default"]) { + // option = group.defaultOption; */ + } else if ([type isEqualToString:@"index"]) { + if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { + int index = [criteria[@"value"] intValue]; + if (group.options.count > index) { + mediaOption = [group.options objectAtIndex:index]; + } + } + } else { // default. invalid type or "system" + [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; + return; + } - // If a match isn't found, option will be nil and text tracks will be disabled - [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; + // If a match isn't found, option will be nil and text tracks will be disabled + [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; } - (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack { - _selectedAudioTrack = selectedAudioTrack; - [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible - withCriteria:_selectedAudioTrack]; + _selectedAudioTrack = selectedAudioTrack; + [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible + withCriteria:_selectedAudioTrack]; } - (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack { @@ -1233,25 +1273,25 @@ static int const RCTVideoUnset = -1; - (NSArray *)getAudioTrackInfo { - NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *title = @""; - NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; - if (values.count > 0) { - title = [values objectAtIndex:0]; - } - NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; - NSDictionary *audioTrack = @{ - @"index": [NSNumber numberWithInt:i], - @"title": title, - @"language": language - }; - [audioTracks addObject:audioTrack]; + NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; + AVMediaSelectionGroup *group = [_player.currentItem.asset + mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; + for (int i = 0; i < group.options.count; ++i) { + AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *title = @""; + NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; + if (values.count > 0) { + title = [values objectAtIndex:0]; } - return audioTracks; + NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; + NSDictionary *audioTrack = @{ + @"index": [NSNumber numberWithInt:i], + @"title": title, + @"language": language + }; + [audioTracks addObject:audioTrack]; + } + return audioTracks; } - (NSArray *)getTextTrackInfo @@ -1423,6 +1463,11 @@ static int const RCTVideoUnset = -1; - (void)removePlayerLayer { + if (_loadingRequest != nil) { + [_loadingRequest finishLoading]; + } + _requestingCertificate = NO; + _requestingCertificateErrored = NO; [_playerLayer removeFromSuperlayer]; if (_playerLayerObserverSet) { [_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath]; @@ -1461,29 +1506,29 @@ static int const RCTVideoUnset = -1; } - (void)setFilter:(NSString *)filterName { - _filterName = filterName; - - if (!_filterEnabled) { - return; - } else if ([[_source objectForKey:@"uri"] rangeOfString:@"m3u8"].location != NSNotFound) { - return; // filters don't work for HLS... return - } else if (!_playerItem.asset) { - return; - } - - CIFilter *filter = [CIFilter filterWithName:filterName]; - _playerItem.videoComposition = [AVVideoComposition - videoCompositionWithAsset:_playerItem.asset - applyingCIFiltersWithHandler:^(AVAsynchronousCIImageFilteringRequest *_Nonnull request) { - if (filter == nil) { - [request finishWithImage:request.sourceImage context:nil]; - } else { - CIImage *image = request.sourceImage.imageByClampingToExtent; - [filter setValue:image forKey:kCIInputImageKey]; - CIImage *output = [filter.outputImage imageByCroppingToRect:request.sourceImage.extent]; - [request finishWithImage:output context:nil]; - } - }]; + _filterName = filterName; + + if (!_filterEnabled) { + return; + } else if ([[_source objectForKey:@"uri"] rangeOfString:@"m3u8"].location != NSNotFound) { + return; // filters don't work for HLS... return + } else if (!_playerItem.asset) { + return; + } + + CIFilter *filter = [CIFilter filterWithName:filterName]; + _playerItem.videoComposition = [AVVideoComposition + videoCompositionWithAsset:_playerItem.asset + applyingCIFiltersWithHandler:^(AVAsynchronousCIImageFilteringRequest *_Nonnull request) { + if (filter == nil) { + [request finishWithImage:request.sourceImage context:nil]; + } else { + CIImage *image = request.sourceImage.imageByClampingToExtent; + [filter setValue:image forKey:kCIInputImageKey]; + CIImage *output = [filter.outputImage imageByCroppingToRect:request.sourceImage.extent]; + [request finishWithImage:output context:nil]; + } + }]; } - (void)setFilterEnabled:(BOOL)filterEnabled { @@ -1583,106 +1628,351 @@ static int const RCTVideoUnset = -1; #pragma mark - Export - (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - - AVAsset *asset = _playerItem.asset; - - if (asset != nil) { - - AVAssetExportSession *exportSession = [AVAssetExportSession - exportSessionWithAsset:asset presetName:AVAssetExportPresetHighestQuality]; - - if (exportSession != nil) { - NSString *path = nil; - NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - path = [self generatePathInDirectory:[[self cacheDirectoryPath] stringByAppendingPathComponent:@"Videos"] - withExtension:@".mp4"]; - NSURL *url = [NSURL fileURLWithPath:path]; - exportSession.outputFileType = AVFileTypeMPEG4; - exportSession.outputURL = url; - exportSession.videoComposition = _playerItem.videoComposition; - exportSession.shouldOptimizeForNetworkUse = true; - [exportSession exportAsynchronouslyWithCompletionHandler:^{ - - switch ([exportSession status]) { - case AVAssetExportSessionStatusFailed: - reject(@"ERROR_COULD_NOT_EXPORT_VIDEO", @"Could not export video", exportSession.error); - break; - case AVAssetExportSessionStatusCancelled: - reject(@"ERROR_EXPORT_SESSION_CANCELLED", @"Export session was cancelled", exportSession.error); - break; - default: - resolve(@{@"uri": url.absoluteString}); - break; - } - - }]; - - } else { - - reject(@"ERROR_COULD_NOT_CREATE_EXPORT_SESSION", @"Could not create export session", nil); - + + AVAsset *asset = _playerItem.asset; + + if (asset != nil) { + + AVAssetExportSession *exportSession = [AVAssetExportSession + exportSessionWithAsset:asset presetName:AVAssetExportPresetHighestQuality]; + + if (exportSession != nil) { + NSString *path = nil; + NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + path = [self generatePathInDirectory:[[self cacheDirectoryPath] stringByAppendingPathComponent:@"Videos"] + withExtension:@".mp4"]; + NSURL *url = [NSURL fileURLWithPath:path]; + exportSession.outputFileType = AVFileTypeMPEG4; + exportSession.outputURL = url; + exportSession.videoComposition = _playerItem.videoComposition; + exportSession.shouldOptimizeForNetworkUse = true; + [exportSession exportAsynchronouslyWithCompletionHandler:^{ + + switch ([exportSession status]) { + case AVAssetExportSessionStatusFailed: + reject(@"ERROR_COULD_NOT_EXPORT_VIDEO", @"Could not export video", exportSession.error); + break; + case AVAssetExportSessionStatusCancelled: + reject(@"ERROR_EXPORT_SESSION_CANCELLED", @"Export session was cancelled", exportSession.error); + break; + default: + resolve(@{@"uri": url.absoluteString}); + break; } - + + }]; + } else { - - reject(@"ERROR_ASSET_NIL", @"Asset is nil", nil); - + + reject(@"ERROR_COULD_NOT_CREATE_EXPORT_SESSION", @"Could not create export session", nil); + } + + } else { + + reject(@"ERROR_ASSET_NIL", @"Asset is nil", nil); + + } +} + +- (void)setLicenseResult:(NSString *)license { + NSData *respondData = [self base64DataFromBase64String:license]; + if (_loadingRequest != nil && respondData != nil) { + AVAssetResourceLoadingDataRequest *dataRequest = [_loadingRequest dataRequest]; + [dataRequest respondWithData:respondData]; + [_loadingRequest finishLoading]; + } else { + [self setLicenseResultError:@"No data from JS license response"]; + } +} + +- (BOOL)setLicenseResultError:(NSString *)error { + if (_loadingRequest != nil) { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorFromJSPart + userInfo: @{ + NSLocalizedDescriptionKey: error, + NSLocalizedFailureReasonErrorKey: error, + NSLocalizedRecoverySuggestionErrorKey: error + } + ]; + [self finishLoadingWithError:licenseError]; + } + return NO; +} + +- (BOOL)finishLoadingWithError:(NSError *)error { + if (_loadingRequest && error != nil) { + NSError *licenseError = error; + [_loadingRequest finishLoadingWithError:licenseError]; + if (self.onVideoError) { + self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: error.code], + @"localizedDescription": [error localizedDescription] == nil ? @"" : [error localizedDescription], + @"localizedFailureReason": [error localizedFailureReason] == nil ? @"" : [error localizedFailureReason], + @"localizedRecoverySuggestion": [error localizedRecoverySuggestion] == nil ? @"" : [error localizedRecoverySuggestion], + @"domain": _playerItem.error == nil ? @"RCTVideo" : _playerItem.error.domain}, + @"target": self.reactTag}); + } + } + return NO; } - (BOOL)ensureDirExistsWithPath:(NSString *)path { - BOOL isDir = NO; - NSError *error; - BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; - if (!(exists && isDir)) { - [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error]; - if (error) { - return NO; - } + BOOL isDir = NO; + NSError *error; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; + if (!(exists && isDir)) { + [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error]; + if (error) { + return NO; } - return YES; + } + return YES; } - (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension { - NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension]; - [self ensureDirExistsWithPath:directory]; - return [directory stringByAppendingPathComponent:fileName]; + NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension]; + [self ensureDirExistsWithPath:directory]; + return [directory stringByAppendingPathComponent:fileName]; } - (NSString *)cacheDirectoryPath { - NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - return array[0]; + NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + return array[0]; } +#pragma mark - AVAssetResourceLoaderDelegate + +- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForRenewalOfRequestedResource:(AVAssetResourceRenewalRequest *)renewalRequest { + return [self loadingRequestHandling:renewalRequest]; +} + +- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { + return [self loadingRequestHandling:loadingRequest]; +} + +- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader +didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest { + NSLog(@"didCancelLoadingRequest"); +} + +- (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { + if (self->_requestingCertificate) { + return YES; + } else if (self->_requestingCertificateErrored) { + return NO; + } + _loadingRequest = loadingRequest; + NSURL *url = loadingRequest.request.URL; + NSString *contentId = url.host; + if (self->_drm != nil) { + NSString *contentIdOverride = (NSString *)[self->_drm objectForKey:@"contentId"]; + if (contentIdOverride != nil) { + contentId = contentIdOverride; + } + NSString *drmType = (NSString *)[self->_drm objectForKey:@"type"]; + if ([drmType isEqualToString:@"fairplay"]) { + NSString *certificateStringUrl = (NSString *)[self->_drm objectForKey:@"certificateUrl"]; + if (certificateStringUrl != nil) { + NSURL *certificateURL = [NSURL URLWithString:[certificateStringUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL]; + if ([self->_drm objectForKey:@"base64Certificate"]) { + certificateData = [[NSData alloc] initWithBase64EncodedData:certificateData options:NSDataBase64DecodingIgnoreUnknownCharacters]; + } + + if (certificateData != nil) { + NSData *contentIdData = [contentId dataUsingEncoding:NSUTF8StringEncoding]; + AVAssetResourceLoadingDataRequest *dataRequest = [loadingRequest dataRequest]; + if (dataRequest != nil) { + NSError *spcError = nil; + NSData *spcData = [loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError]; + // Request CKC to the server + NSString *licenseServer = (NSString *)[self->_drm objectForKey:@"licenseServer"]; + if (spcError != nil) { + [self finishLoadingWithError:spcError]; + self->_requestingCertificateErrored = YES; + } + if (spcData != nil) { + if(self.onGetLicense) { + NSString *spcStr = [[NSString alloc] initWithData:spcData encoding:NSASCIIStringEncoding]; + self->_requestingCertificate = YES; + self.onGetLicense(@{@"spc": spcStr, + @"contentId": contentId, + @"spcBase64": [[[NSData alloc] initWithBase64EncodedData:certificateData options:NSDataBase64DecodingIgnoreUnknownCharacters] base64EncodedStringWithOptions:0], + @"target": self.reactTag}); + } else if(licenseServer != nil) { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setHTTPMethod:@"POST"]; + [request setURL:[NSURL URLWithString:licenseServer]]; + // HEADERS + NSDictionary *headers = (NSDictionary *)[self->_drm objectForKey:@"headers"]; + if (headers != nil) { + for (NSString *key in headers) { + NSString *value = headers[key]; + [request setValue:value forHTTPHeaderField:key]; + } + } + // + + [request setHTTPBody: spcData]; + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; + NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response; + if (error != nil) { + NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); + [self finishLoadingWithError:error]; + self->_requestingCertificateErrored = YES; + } else { + if([httpResponse statusCode] != 200){ + NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorLicenseRequestNotOk + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining license.", + NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"License server responded with status code %li", (long)[httpResponse statusCode]], + NSLocalizedRecoverySuggestionErrorKey: @"Did you send the correct data to the license Server? Is the server ok?" + } + ]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; + } else if (data != nil) { + [dataRequest respondWithData:data]; + [loadingRequest finishLoading]; + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoDataFromLicenseRequest + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"No data received from the license server.", + NSLocalizedRecoverySuggestionErrorKey: @"Is the licenseServer ok?." + } + ]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; + } + + } + }]; + [postDataTask resume]; + } + + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoSPC + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining license.", + NSLocalizedFailureReasonErrorKey: @"No spc received.", + NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM config." + } + ]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; + } + + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoDataRequest + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"No dataRequest found.", + NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM configuration." + } + ]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; + } + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoCertificateData + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"No certificate data obtained from the specificied url.", + NSLocalizedRecoverySuggestionErrorKey: @"Have you specified a valid 'certificateUrl'?" + } + ]; + [self finishLoadingWithError:licenseError]; + self->_requestingCertificateErrored = YES; + } + }); + return YES; + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoCertificateURL + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM License.", + NSLocalizedFailureReasonErrorKey: @"No certificate URL has been found.", + NSLocalizedRecoverySuggestionErrorKey: @"Did you specified the prop certificateUrl?" + } + ]; + return [self finishLoadingWithError:licenseError]; + } + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoFairplayDRM + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"Not a valid DRM Scheme has found", + NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' 'type' as fairplay?" + } + ]; + return [self finishLoadingWithError:licenseError]; + } + + } else { + NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" + code: RCTVideoErrorNoDRMData + userInfo: @{ + NSLocalizedDescriptionKey: @"Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: @"No drm object found.", + NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' prop?" + } + ]; + return [self finishLoadingWithError:licenseError]; + } + + + return NO; +} + +- (NSData *)base64DataFromBase64String: (NSString *)base64String { + if (base64String != nil) { + // NSData from the Base64 encoded str + NSData *base64Data = [[NSData alloc] initWithBase64EncodedString:base64String options:NSASCIIStringEncoding]; + return base64Data; + } + return nil; +} #pragma mark - Picture in Picture #if TARGET_OS_IOS - (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { if (self.onPictureInPictureStatusChanged) { self.onPictureInPictureStatusChanged(@{ - @"isActive": [NSNumber numberWithBool:false] - }); + @"isActive": [NSNumber numberWithBool:false] + }); } } - (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { if (self.onPictureInPictureStatusChanged) { self.onPictureInPictureStatusChanged(@{ - @"isActive": [NSNumber numberWithBool:true] - }); + @"isActive": [NSNumber numberWithBool:true] + }); } } - (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - + } - (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - + } - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error { - + } - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL))completionHandler { diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 62c8b821..000a9e83 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -19,6 +19,7 @@ RCT_EXPORT_MODULE(); } RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary); +RCT_EXPORT_VIEW_PROPERTY(drm, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(maxBitRate, float); RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString); RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL); @@ -68,6 +69,7 @@ RCT_EXPORT_VIEW_PROPERTY(onPlaybackStalled, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoExternalPlaybackChange, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onGetLicense, RCTDirectEventBlock); RCT_REMAP_METHOD(save, options:(NSDictionary *)options reactTag:(nonnull NSNumber *)reactTag @@ -82,7 +84,34 @@ RCT_REMAP_METHOD(save, [view save:options resolve:resolve reject:reject]; } }]; -} +}; +RCT_REMAP_METHOD(setLicenseResult, + license:(NSString *)license + reactTag:(nonnull NSNumber *)reactTag) +{ + [self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTVideo *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTVideo class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view); + } else { + [view setLicenseResult:license]; + } + }]; +}; + +RCT_REMAP_METHOD(setLicenseResultError, + error:(NSString *)error + reactTag:(nonnull NSNumber *)reactTag) +{ + [self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTVideo *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTVideo class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view); + } else { + [view setLicenseResultError:error]; + } + }]; +}; RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTDirectEventBlock); diff --git a/package.json b/package.json index 1ae4fb25..a26b8cc2 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "ios", "windows", "FilterType.js", + "DRMType.js", "TextTrackType.js", "VideoResizeMode.js", "react-native-video.podspec" diff --git a/react-native-video.podspec b/react-native-video.podspec index 98ba5537..1650cebb 100644 --- a/react-native-video.podspec +++ b/react-native-video.podspec @@ -32,4 +32,8 @@ Pod::Spec.new do |s| s.dependency "React" s.default_subspec = "Video" + + s.xcconfig = { + 'OTHER_LDFLAGS': '-ObjC', + } end