Add iOS and Android basic DRM support (#1445)
This PR adds support for DRM streams on iOS (Fairplay) and Android (Playready, Widevine, Clearkey) I am neither Android nor iOS developer, so feel free to provide feedback to improve this PR. **Test stream for ANDROID:** ``` testStream = { uri: 'http://profficialsite.origin.mediaservices.windows.net/c51358ea-9a5e-4322-8951-897d640fdfd7/tearsofsteel_4k.ism/manifest(format=mpd-time-csf)', type: 'mpd', drm: { type: DRMType.PLAYREADY, licenseServer: 'http://test.playready.microsoft.com/service/rightsmanager.asmx?cfg=(persist:false,sl:150)' } }; ``` or ``` { uri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_1080p.mpd', drm: { type: 'widevine', //or DRMType.WIDEVINE licenseServer: 'https://drm-widevine-licensing.axtest.net/AcquireLicense', headers: { 'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImZpcnN0X3BsYXlfZXhwaXJhdGlvbiI6NjAsInBsYXlyZWFkeSI6eyJyZWFsX3RpbWVfZXhwaXJhdGlvbiI6dHJ1ZX0sImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.FAbIiPxX8BHi9RwfzD7Yn-wugU19ghrkBFKsaCPrZmU' }, } } ``` **Test stream for iOS:** Sorry but I can not provide free streams to test. If anyone can provide test streams, or found some we can use, please let me know to also test them. It has been tested with a private provider and they work, at least with the `getLicense` override method. (An example implementation is provided in the README)
This commit is contained in:
@@ -19,6 +19,11 @@ android {
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@@ -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<String, String> 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());
|
||||
}
|
||||
|
@@ -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<String, String> 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<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> 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
|
||||
*
|
||||
|
@@ -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<ReactExoplayerVi
|
||||
private static final String PROP_SRC = "src";
|
||||
private static final String PROP_SRC_URI = "uri";
|
||||
private static final String PROP_SRC_TYPE = "type";
|
||||
private static final String PROP_DRM = "drm";
|
||||
private static final String PROP_DRM_TYPE = "type";
|
||||
private static final String PROP_DRM_LICENSESERVER = "licenseServer";
|
||||
private static final String PROP_DRM_HEADERS = "headers";
|
||||
private static final String PROP_SRC_HEADERS = "requestHeaders";
|
||||
private static final String PROP_RESIZE_MODE = "resizeMode";
|
||||
private static final String PROP_REPEAT = "repeat";
|
||||
@@ -101,6 +111,31 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_DRM)
|
||||
public void setDRM(final ReactExoplayerView videoView, @Nullable ReadableMap drm) {
|
||||
if (drm != null && drm.hasKey(PROP_DRM_TYPE)) {
|
||||
String drmType = drm.hasKey(PROP_DRM_TYPE) ? drm.getString(PROP_DRM_TYPE) : null;
|
||||
String drmLicenseServer = drm.hasKey(PROP_DRM_LICENSESERVER) ? drm.getString(PROP_DRM_LICENSESERVER) : null;
|
||||
ReadableMap drmHeaders = drm.hasKey(PROP_DRM_HEADERS) ? drm.getMap(PROP_DRM_HEADERS) : null;
|
||||
if (drmType != null && drmLicenseServer != null && Util.getDrmUuid(drmType) != null) {
|
||||
UUID drmUUID = Util.getDrmUuid(drmType);
|
||||
videoView.setDrmType(drmUUID);
|
||||
videoView.setDrmLicenseUrl(drmLicenseServer);
|
||||
if (drmHeaders != null) {
|
||||
ArrayList<String> 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<ReactExoplayerVi
|
||||
String extension = src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null;
|
||||
Map<String, String> headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null;
|
||||
|
||||
|
||||
if (TextUtils.isEmpty(uriString)) {
|
||||
return;
|
||||
}
|
||||
|
@@ -8,7 +8,12 @@
|
||||
<string name="error_querying_decoders">Unable to query device decoders</string>
|
||||
|
||||
<string name="error_instantiating_decoder">Unable to instantiate decoder <xliff:g id="decoder_name">%1$s</xliff:g></string>
|
||||
|
||||
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
||||
|
||||
<string name="unrecognized_media_format">Unrecognized media format</string>
|
||||
|
||||
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||
|
||||
<string name="error_drm_unknown">An unknown DRM error occurred</string>
|
||||
</resources>
|
||||
|
Reference in New Issue
Block a user