Skip to content

Commit

Permalink
refactor: move view type and drm in source (#3867)
Browse files Browse the repository at this point in the history
* perf: ensure we do not provide callback to native if no callback provided from app

* chore: rework bufferConfig to make it more generic and reduce ReactExoplayerView code size

* chore: improve issue template

* fix(android): avoid video view flickering at playback startup

* chore(android): refactor DRM props into a dedicated class

* Update android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java

* chore: fix linter

* fix: ensure drm prop is correctly cleaned

* feat(android): move viewType (secure texture) & drm inside the source

The origianl behavior has been kept for interoperability, but marked as deprecated in doc

* chore: fix linter

* chore(ios): move drm prop in source like on android

* chore: fix linter

* chore: clean log

* fix: allow to disable secure View

* chore: fix viewType resolution (source value was not handled)

* chore: use contentDeepEquals instead of manual checks

* chore: fix linter

* fix: ensure player doesn't start when view is unmounted

* Fix/ensure view drop stop playback startup (#3875)

* fix: ensure player doesn't start when view is unmounted

* chore: revert change

* chore: add warning in case of invalid Surface configuration

* chore: code clean

* fix: simplify surface management

* chore: restore previous code

* chore: fix typo

* chore: code cleanup

* feat(android): add multiDrm flag support

* docs: update docs

* chore: fix ios build

* chore: fix deprecated declaration

---------

Co-authored-by: Krzysztof Moch <[email protected]>
  • Loading branch information
freeboub and KrzysztofMoch authored Jul 10, 2024
1 parent 08f6caa commit 66dcf32
Show file tree
Hide file tree
Showing 14 changed files with 141 additions and 99 deletions.
18 changes: 18 additions & 0 deletions android/src/main/java/com/brentvatne/common/api/DRMProps.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.brentvatne.common.api

import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetArray
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetBool
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetString
import com.facebook.react.bridge.ReadableMap
import java.util.UUID
Expand Down Expand Up @@ -29,12 +30,28 @@ class DRMProps {
* DRM Http Header to access to license server
*/
var drmLicenseHeader: Array<String> = emptyArray<String>()

/**
* Flag to enable key rotation support
*/
var multiDrm: Boolean = false

/** return true if this and src are equals */
override fun equals(other: Any?): Boolean {
if (other == null || other !is DRMProps) return false
return drmType == other.drmType &&
drmLicenseServer == other.drmLicenseServer &&
multiDrm == other.multiDrm &&
drmLicenseHeader.contentDeepEquals(other.drmLicenseHeader) // drmLicenseHeader is never null
}

companion object {
private const val PROP_DRM_TYPE = "type"
private const val PROP_DRM_LICENSE_SERVER = "licenseServer"
private const val PROP_DRM_HEADERS = "headers"
private const val PROP_DRM_HEADERS_KEY = "key"
private const val PROP_DRM_HEADERS_VALUE = "value"
private const val PROP_DRM_MULTI_DRM = "multiDrm"

/** parse the source ReadableMap received from app */
@JvmStatic
Expand All @@ -44,6 +61,7 @@ class DRMProps {
drm = DRMProps()
drm.drmType = safeGetString(src, PROP_DRM_TYPE)
drm.drmLicenseServer = safeGetString(src, PROP_DRM_LICENSE_SERVER)
drm.multiDrm = safeGetBool(src, PROP_DRM_MULTI_DRM, false)
val drmHeadersArray = safeGetArray(src, PROP_DRM_HEADERS)
if (drm.drmType != null && drm.drmLicenseServer != null) {
if (drmHeadersArray != null) {
Expand Down
11 changes: 10 additions & 1 deletion android/src/main/java/com/brentvatne/common/api/Source.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.content.Context
import android.content.res.Resources
import android.net.Uri
import android.text.TextUtils
import com.brentvatne.common.api.DRMProps.Companion.parse
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.common.toolbox.DebugLog.e
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetArray
Expand Down Expand Up @@ -46,6 +47,11 @@ class Source {
/** http header list */
val headers: MutableMap<String, String> = HashMap()

/**
* DRM properties linked to the source
*/
var drmProps: DRMProps? = null

/** enable chunkless preparation for HLS
* see:
*/
Expand All @@ -61,7 +67,8 @@ class Source {
cropStartMs == other.cropStartMs &&
cropEndMs == other.cropEndMs &&
startPositionMs == other.startPositionMs &&
extension == other.extension
extension == other.extension &&
drmProps == other.drmProps
)
}

Expand Down Expand Up @@ -123,6 +130,7 @@ class Source {
private const val PROP_SRC_TYPE = "type"
private const val PROP_SRC_METADATA = "metadata"
private const val PROP_SRC_HEADERS = "requestHeaders"
private const val PROP_SRC_DRM = "drm"
private const val PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION = "textTracksAllowChunklessPreparation"

@SuppressLint("DiscouragedApi")
Expand Down Expand Up @@ -180,6 +188,7 @@ class Source {
source.cropStartMs = safeGetInt(src, PROP_SRC_CROP_START, -1)
source.cropEndMs = safeGetInt(src, PROP_SRC_CROP_END, -1)
source.extension = safeGetString(src, PROP_SRC_TYPE, null)
source.drmProps = parse(safeGetMap(src, PROP_SRC_DRM))
source.textTracksAllowChunklessPreparation = safeGetBool(src, PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION, true)

val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS)
Expand Down
15 changes: 4 additions & 11 deletions android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.brentvatne.exoplayer;

import android.annotation.SuppressLint;
import android.content.Context;

import androidx.annotation.NonNull;
Expand All @@ -15,7 +16,6 @@
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.ui.SubtitleView;

import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.SurfaceView;
Expand All @@ -32,6 +32,7 @@

import java.util.List;

@SuppressLint("ViewConstructor")
public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
private final static String TAG = "ExoPlayerView";
private View surfaceView;
Expand All @@ -48,15 +49,7 @@ public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
private boolean hideShutterView = false;

public ExoPlayerView(Context context) {
this(context, null);
}

public ExoPlayerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public ExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
super(context, null, 0);

this.context = context;

Expand Down Expand Up @@ -214,7 +207,7 @@ public void setPlayer(ExoPlayer player) {
* @param resizeMode The resize mode.
*/
public void setResizeMode(@ResizeMode.Mode int resizeMode) {
if (layout.getResizeMode() != resizeMode) {
if (layout != null && layout.getResizeMode() != resizeMode) {
layout.setResizeMode(resizeMode);
post(measureAndLayout);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ public class ReactExoplayerView extends FrameLayout implements
private boolean hasDrmFailed = false;
private boolean isUsingContentResolution = false;
private boolean selectTrackWhenReady = false;
private Handler mainHandler;
private final Handler mainHandler;
private Runnable mainRunnable;
private boolean useCache = false;
private ControlsConfig controlsConfig = new ControlsConfig();
Expand All @@ -241,7 +241,6 @@ public class ReactExoplayerView extends FrameLayout implements
private float mProgressUpdateInterval = 250.0f;
private boolean playInBackground = false;
private boolean mReportBandwidth = false;
private DRMProps drmProps;
private boolean controls;
private Uri adTagUrl;

Expand Down Expand Up @@ -311,7 +310,7 @@ public ReactExoplayerView(ThemedReactContext context, ReactExoplayerConfig confi
this.eventEmitter = new VideoEventEmitter();
this.config = config;
this.bandwidthMeter = config.getBandwidthMeter();

mainHandler = new Handler();
createViews();

audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
Expand All @@ -334,12 +333,9 @@ private void createViews() {
LayoutParams.MATCH_PARENT);
exoPlayerView = new ExoPlayerView(getContext());
exoPlayerView.setLayoutParams(layoutParams);

addView(exoPlayerView, 0, layoutParams);

exoPlayerView.setFocusable(this.focusable);

mainHandler = new Handler();
}

// LifecycleEventListener implementation
Expand Down Expand Up @@ -781,19 +777,22 @@ private void initializePlayerCore(ReactExoplayerView self) {
}
}

private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) {
private DrmSessionManager initializePlayerDrm() {
DrmSessionManager drmSessionManager = null;
if (self.drmProps != null) {
try {
drmSessionManager = self.buildDrmSessionManager(self.drmProps.getDrmUUID(),
self.drmProps.getDrmLicenseServer(),
self.drmProps.getDrmLicenseHeader());
} 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.onVideoError.invoke(getResources().getString(errorStringId), e, "3003");
return null;
DRMProps drmProps = source.getDrmProps();
// need to realign UUID in DRM Props from source
if (drmProps != null && drmProps.getDrmType() != null) {
UUID uuid = Util.getDrmUuid(drmProps.getDrmType());
if (uuid != null) {
try {
DebugLog.w(TAG, "drm buildDrmSessionManager");
drmSessionManager = buildDrmSessionManager(uuid, drmProps);
} 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.onVideoError.invoke(getResources().getString(errorStringId), e, "3003");
}
}
}
return drmSessionManager;
Expand All @@ -803,14 +802,14 @@ private void initializePlayerSource() {
if (source.getUri() == null) {
return;
}
DrmSessionManager drmSessionManager = initializePlayerDrm(this);
if (drmSessionManager == null && drmProps != null && drmProps.getDrmUUID() != null) {
// Failed to intialize DRM session manager - cannot continue
/// init DRM
DrmSessionManager drmSessionManager = initializePlayerDrm();
if (drmSessionManager == null && source.getDrmProps() != null && source.getDrmProps().getDrmType() != null) {
// Failed to initialize DRM session manager - cannot continue
DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!");
eventEmitter.onVideoError.invoke("Failed to initialize DRM Session Manager Framework!", new Exception("DRM Session Manager Framework failure!"), "3003");
return;
}

// init source to manage ads and external text tracks
ArrayList<MediaSource> mediaSourceList = buildTextSources();
MediaSource videoSource = buildMediaSource(source.getUri(), source.getExtension(), drmSessionManager, source.getCropStartMs(), source.getCropEndMs());
MediaSource mediaSourceWithAds = null;
Expand Down Expand Up @@ -945,21 +944,21 @@ private void cleanupPlaybackService() {
}
}

private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException {
return buildDrmSessionManager(uuid, licenseUrl, keyRequestPropertiesArray, 0);
private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps) throws UnsupportedDrmException {
return buildDrmSessionManager(uuid, drmProps, 0);
}

private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, int retryCount) throws UnsupportedDrmException {
private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps, int retryCount) throws UnsupportedDrmException {
if (Util.SDK_INT < 18) {
return null;
}
try {
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmProps.getDrmLicenseServer(),
buildHttpDataSourceFactory(false));
if (keyRequestPropertiesArray != null) {
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
}

String[] keyRequestPropertiesArray = drmProps.getDrmLicenseHeader();
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
}
FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid);
if (hasDrmFailed) {
Expand All @@ -969,15 +968,15 @@ private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, S
return new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(uuid, (_uuid) -> mediaDrm)
.setKeyRequestParameters(null)
.setMultiSession(false)
.setMultiSession(drmProps.getMultiDrm())
.build(drmCallback);
} catch (UnsupportedDrmException ex) {
// Unsupported DRM exceptions are handled by the calling method
throw ex;
} catch (Exception ex) {
if (retryCount < 3) {
// Attempt retry 3 times in case where the OS Media DRM Framework fails for whatever reason
return buildDrmSessionManager(uuid, licenseUrl, keyRequestPropertiesArray, ++retryCount);
return buildDrmSessionManager(uuid, drmProps, ++retryCount);
}
// Handle the unknow exception and emit to JS
eventEmitter.onVideoError.invoke(ex.toString(), ex, "3006");
Expand Down Expand Up @@ -1818,7 +1817,9 @@ private void reloadSource() {
}

public void setResizeModeModifier(@ResizeMode.Mode int resizeMode) {
exoPlayerView.setResizeMode(resizeMode);
if (exoPlayerView != null) {
exoPlayerView.setResizeMode(resizeMode);
}
}

private void applyModifiers() {
Expand Down Expand Up @@ -2261,13 +2262,6 @@ public void setBufferConfig(BufferConfig config) {
initializePlayer();
}

public void setDrm(DRMProps drmProps) {
this.drmProps = drmProps;
if (drmProps != null && drmProps.getDrmType() != null) {
this.drmProps.setDrmUUID(Util.getDrmUuid(drmProps.getDrmType()));
}
}

@Override
public void onDrmKeysLoaded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
DebugLog.d("DRM Info", "onDrmKeysLoaded");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;

import java.util.ArrayList;
import java.util.Map;
import java.util.UUID;

import javax.annotation.Nullable;

Expand All @@ -40,8 +38,6 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String REACT_CLASS = "RCTVideo";
private static final String PROP_SRC = "src";
private static final String PROP_AD_TAG_URL = "adTagUrl";
private static final String PROP_DRM = "drm";
private static final String PROP_SRC_HEADERS = "requestHeaders";
private static final String PROP_RESIZE_MODE = "resizeMode";
private static final String PROP_REPEAT = "repeat";
private static final String PROP_SELECTED_AUDIO_TRACK = "selectedAudioTrack";
Expand Down Expand Up @@ -81,7 +77,6 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_DEBUG = "debug";
private static final String PROP_CONTROLS_STYLES = "controlsStyles";


private final ReactExoplayerConfig config;

public ReactExoplayerViewManager(ReactExoplayerConfig config) {
Expand Down Expand Up @@ -118,12 +113,6 @@ public void addEventEmitters(@NonNull ThemedReactContext reactContext, @NonNull
view.eventEmitter.addEventEmitters(reactContext, view);
}

@ReactProp(name = PROP_DRM)
public void setDRM(final ReactExoplayerView videoView, @Nullable ReadableMap drm) {
DRMProps drmProps = DRMProps.parse(drm);
videoView.setDrm(drmProps);
}

@ReactProp(name = PROP_SRC)
public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) {
Context context = videoView.getContext().getApplicationContext();
Expand Down
7 changes: 7 additions & 0 deletions docs/pages/component/drm.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ Default: false

The URL pointing to the licenseServer that will provide the authorization to play the protected stream.

### `multiDrm`
<PlatformsList types={['Android']} />
Type: boolean\
Default: false

Indicates that drm system shall support key rotation, see: https://developer.android.google.cn/media/media3/exoplayer/drm?hl=en#key-rotation

### `type`

<PlatformsList types={['Android', 'iOS']} />
Expand Down
Loading

0 comments on commit 66dcf32

Please sign in to comment.