refactor: move view type and drm in source (#3867)

* 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 <krzysmoch.programs@gmail.com>
This commit is contained in:
Olivier Bouillet 2024-07-10 12:17:22 +02:00 committed by GitHub
parent 08f6caa645
commit 66dcf32b56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 141 additions and 99 deletions

View File

@ -1,6 +1,7 @@
package com.brentvatne.common.api package com.brentvatne.common.api
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetArray import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetArray
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetBool
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetString import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetString
import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableMap
import java.util.UUID import java.util.UUID
@ -29,12 +30,28 @@ class DRMProps {
* DRM Http Header to access to license server * DRM Http Header to access to license server
*/ */
var drmLicenseHeader: Array<String> = emptyArray<String>() 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 { companion object {
private const val PROP_DRM_TYPE = "type" private const val PROP_DRM_TYPE = "type"
private const val PROP_DRM_LICENSE_SERVER = "licenseServer" private const val PROP_DRM_LICENSE_SERVER = "licenseServer"
private const val PROP_DRM_HEADERS = "headers" private const val PROP_DRM_HEADERS = "headers"
private const val PROP_DRM_HEADERS_KEY = "key" private const val PROP_DRM_HEADERS_KEY = "key"
private const val PROP_DRM_HEADERS_VALUE = "value" private const val PROP_DRM_HEADERS_VALUE = "value"
private const val PROP_DRM_MULTI_DRM = "multiDrm"
/** parse the source ReadableMap received from app */ /** parse the source ReadableMap received from app */
@JvmStatic @JvmStatic
@ -44,6 +61,7 @@ class DRMProps {
drm = DRMProps() drm = DRMProps()
drm.drmType = safeGetString(src, PROP_DRM_TYPE) drm.drmType = safeGetString(src, PROP_DRM_TYPE)
drm.drmLicenseServer = safeGetString(src, PROP_DRM_LICENSE_SERVER) drm.drmLicenseServer = safeGetString(src, PROP_DRM_LICENSE_SERVER)
drm.multiDrm = safeGetBool(src, PROP_DRM_MULTI_DRM, false)
val drmHeadersArray = safeGetArray(src, PROP_DRM_HEADERS) val drmHeadersArray = safeGetArray(src, PROP_DRM_HEADERS)
if (drm.drmType != null && drm.drmLicenseServer != null) { if (drm.drmType != null && drm.drmLicenseServer != null) {
if (drmHeadersArray != null) { if (drmHeadersArray != null) {

View File

@ -6,6 +6,7 @@ import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.net.Uri import android.net.Uri
import android.text.TextUtils import android.text.TextUtils
import com.brentvatne.common.api.DRMProps.Companion.parse
import com.brentvatne.common.toolbox.DebugLog import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.common.toolbox.DebugLog.e import com.brentvatne.common.toolbox.DebugLog.e
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetArray import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetArray
@ -46,6 +47,11 @@ class Source {
/** http header list */ /** http header list */
val headers: MutableMap<String, String> = HashMap() val headers: MutableMap<String, String> = HashMap()
/**
* DRM properties linked to the source
*/
var drmProps: DRMProps? = null
/** enable chunkless preparation for HLS /** enable chunkless preparation for HLS
* see: * see:
*/ */
@ -61,7 +67,8 @@ class Source {
cropStartMs == other.cropStartMs && cropStartMs == other.cropStartMs &&
cropEndMs == other.cropEndMs && cropEndMs == other.cropEndMs &&
startPositionMs == other.startPositionMs && startPositionMs == other.startPositionMs &&
extension == other.extension extension == other.extension &&
drmProps == other.drmProps
) )
} }
@ -123,6 +130,7 @@ class Source {
private const val PROP_SRC_TYPE = "type" private const val PROP_SRC_TYPE = "type"
private const val PROP_SRC_METADATA = "metadata" private const val PROP_SRC_METADATA = "metadata"
private const val PROP_SRC_HEADERS = "requestHeaders" 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" private const val PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION = "textTracksAllowChunklessPreparation"
@SuppressLint("DiscouragedApi") @SuppressLint("DiscouragedApi")
@ -180,6 +188,7 @@ class Source {
source.cropStartMs = safeGetInt(src, PROP_SRC_CROP_START, -1) source.cropStartMs = safeGetInt(src, PROP_SRC_CROP_START, -1)
source.cropEndMs = safeGetInt(src, PROP_SRC_CROP_END, -1) source.cropEndMs = safeGetInt(src, PROP_SRC_CROP_END, -1)
source.extension = safeGetString(src, PROP_SRC_TYPE, null) 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) source.textTracksAllowChunklessPreparation = safeGetBool(src, PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION, true)
val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS) val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS)

View File

@ -1,5 +1,6 @@
package com.brentvatne.exoplayer; package com.brentvatne.exoplayer;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -15,7 +16,6 @@ import androidx.media3.common.util.Assertions;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.ui.SubtitleView; import androidx.media3.ui.SubtitleView;
import android.util.AttributeSet;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.Gravity; import android.view.Gravity;
import android.view.SurfaceView; import android.view.SurfaceView;
@ -32,6 +32,7 @@ import com.google.common.collect.ImmutableList;
import java.util.List; import java.util.List;
@SuppressLint("ViewConstructor")
public final class ExoPlayerView extends FrameLayout implements AdViewProvider { public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
private final static String TAG = "ExoPlayerView"; private final static String TAG = "ExoPlayerView";
private View surfaceView; private View surfaceView;
@ -48,15 +49,7 @@ public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
private boolean hideShutterView = false; private boolean hideShutterView = false;
public ExoPlayerView(Context context) { public ExoPlayerView(Context context) {
this(context, null); super(context, null, 0);
}
public ExoPlayerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context; this.context = context;
@ -214,7 +207,7 @@ public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
* @param resizeMode The resize mode. * @param resizeMode The resize mode.
*/ */
public void setResizeMode(@ResizeMode.Mode int resizeMode) { public void setResizeMode(@ResizeMode.Mode int resizeMode) {
if (layout.getResizeMode() != resizeMode) { if (layout != null && layout.getResizeMode() != resizeMode) {
layout.setResizeMode(resizeMode); layout.setResizeMode(resizeMode);
post(measureAndLayout); post(measureAndLayout);
} }

View File

@ -217,7 +217,7 @@ public class ReactExoplayerView extends FrameLayout implements
private boolean hasDrmFailed = false; private boolean hasDrmFailed = false;
private boolean isUsingContentResolution = false; private boolean isUsingContentResolution = false;
private boolean selectTrackWhenReady = false; private boolean selectTrackWhenReady = false;
private Handler mainHandler; private final Handler mainHandler;
private Runnable mainRunnable; private Runnable mainRunnable;
private boolean useCache = false; private boolean useCache = false;
private ControlsConfig controlsConfig = new ControlsConfig(); private ControlsConfig controlsConfig = new ControlsConfig();
@ -241,7 +241,6 @@ public class ReactExoplayerView extends FrameLayout implements
private float mProgressUpdateInterval = 250.0f; private float mProgressUpdateInterval = 250.0f;
private boolean playInBackground = false; private boolean playInBackground = false;
private boolean mReportBandwidth = false; private boolean mReportBandwidth = false;
private DRMProps drmProps;
private boolean controls; private boolean controls;
private Uri adTagUrl; private Uri adTagUrl;
@ -311,7 +310,7 @@ public class ReactExoplayerView extends FrameLayout implements
this.eventEmitter = new VideoEventEmitter(); this.eventEmitter = new VideoEventEmitter();
this.config = config; this.config = config;
this.bandwidthMeter = config.getBandwidthMeter(); this.bandwidthMeter = config.getBandwidthMeter();
mainHandler = new Handler();
createViews(); createViews();
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@ -334,12 +333,9 @@ public class ReactExoplayerView extends FrameLayout implements
LayoutParams.MATCH_PARENT); LayoutParams.MATCH_PARENT);
exoPlayerView = new ExoPlayerView(getContext()); exoPlayerView = new ExoPlayerView(getContext());
exoPlayerView.setLayoutParams(layoutParams); exoPlayerView.setLayoutParams(layoutParams);
addView(exoPlayerView, 0, layoutParams); addView(exoPlayerView, 0, layoutParams);
exoPlayerView.setFocusable(this.focusable); exoPlayerView.setFocusable(this.focusable);
mainHandler = new Handler();
} }
// LifecycleEventListener implementation // LifecycleEventListener implementation
@ -781,19 +777,22 @@ public class ReactExoplayerView extends FrameLayout implements
} }
} }
private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) { private DrmSessionManager initializePlayerDrm() {
DrmSessionManager drmSessionManager = null; DrmSessionManager drmSessionManager = null;
if (self.drmProps != 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 { try {
drmSessionManager = self.buildDrmSessionManager(self.drmProps.getDrmUUID(), DebugLog.w(TAG, "drm buildDrmSessionManager");
self.drmProps.getDrmLicenseServer(), drmSessionManager = buildDrmSessionManager(uuid, drmProps);
self.drmProps.getDrmLicenseHeader());
} catch (UnsupportedDrmException e) { } catch (UnsupportedDrmException e) {
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003"); eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003");
return null; }
} }
} }
return drmSessionManager; return drmSessionManager;
@ -803,14 +802,14 @@ public class ReactExoplayerView extends FrameLayout implements
if (source.getUri() == null) { if (source.getUri() == null) {
return; return;
} }
DrmSessionManager drmSessionManager = initializePlayerDrm(this); /// init DRM
if (drmSessionManager == null && drmProps != null && drmProps.getDrmUUID() != null) { DrmSessionManager drmSessionManager = initializePlayerDrm();
// Failed to intialize DRM session manager - cannot continue 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!"); 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; return;
} }
// init source to manage ads and external text tracks
ArrayList<MediaSource> mediaSourceList = buildTextSources(); ArrayList<MediaSource> mediaSourceList = buildTextSources();
MediaSource videoSource = buildMediaSource(source.getUri(), source.getExtension(), drmSessionManager, source.getCropStartMs(), source.getCropEndMs()); MediaSource videoSource = buildMediaSource(source.getUri(), source.getExtension(), drmSessionManager, source.getCropStartMs(), source.getCropEndMs());
MediaSource mediaSourceWithAds = null; MediaSource mediaSourceWithAds = null;
@ -945,22 +944,22 @@ public class ReactExoplayerView extends FrameLayout implements
} }
} }
private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { private DrmSessionManager buildDrmSessionManager(UUID uuid, DRMProps drmProps) throws UnsupportedDrmException {
return buildDrmSessionManager(uuid, licenseUrl, keyRequestPropertiesArray, 0); 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) { if (Util.SDK_INT < 18) {
return null; return null;
} }
try { try {
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmProps.getDrmLicenseServer(),
buildHttpDataSourceFactory(false)); buildHttpDataSourceFactory(false));
if (keyRequestPropertiesArray != null) {
String[] keyRequestPropertiesArray = drmProps.getDrmLicenseHeader();
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]); drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
} }
}
FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid); FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid);
if (hasDrmFailed) { if (hasDrmFailed) {
// When DRM fails using L1 we want to switch to L3 // When DRM fails using L1 we want to switch to L3
@ -969,7 +968,7 @@ public class ReactExoplayerView extends FrameLayout implements
return new DefaultDrmSessionManager.Builder() return new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(uuid, (_uuid) -> mediaDrm) .setUuidAndExoMediaDrmProvider(uuid, (_uuid) -> mediaDrm)
.setKeyRequestParameters(null) .setKeyRequestParameters(null)
.setMultiSession(false) .setMultiSession(drmProps.getMultiDrm())
.build(drmCallback); .build(drmCallback);
} catch (UnsupportedDrmException ex) { } catch (UnsupportedDrmException ex) {
// Unsupported DRM exceptions are handled by the calling method // Unsupported DRM exceptions are handled by the calling method
@ -977,7 +976,7 @@ public class ReactExoplayerView extends FrameLayout implements
} catch (Exception ex) { } catch (Exception ex) {
if (retryCount < 3) { if (retryCount < 3) {
// Attempt retry 3 times in case where the OS Media DRM Framework fails for whatever reason // 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 // Handle the unknow exception and emit to JS
eventEmitter.onVideoError.invoke(ex.toString(), ex, "3006"); eventEmitter.onVideoError.invoke(ex.toString(), ex, "3006");
@ -1818,8 +1817,10 @@ public class ReactExoplayerView extends FrameLayout implements
} }
public void setResizeModeModifier(@ResizeMode.Mode int resizeMode) { public void setResizeModeModifier(@ResizeMode.Mode int resizeMode) {
if (exoPlayerView != null) {
exoPlayerView.setResizeMode(resizeMode); exoPlayerView.setResizeMode(resizeMode);
} }
}
private void applyModifiers() { private void applyModifiers() {
setRepeatModifier(repeat); setRepeatModifier(repeat);
@ -2261,13 +2262,6 @@ public class ReactExoplayerView extends FrameLayout implements
initializePlayer(); initializePlayer();
} }
public void setDrm(DRMProps drmProps) {
this.drmProps = drmProps;
if (drmProps != null && drmProps.getDrmType() != null) {
this.drmProps.setDrmUUID(Util.getDrmUuid(drmProps.getDrmType()));
}
}
@Override @Override
public void onDrmKeysLoaded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { public void onDrmKeysLoaded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
DebugLog.d("DRM Info", "onDrmKeysLoaded"); DebugLog.d("DRM Info", "onDrmKeysLoaded");

View File

@ -28,9 +28,7 @@ import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.annotations.ReactProp;
import java.util.ArrayList;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -40,8 +38,6 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String REACT_CLASS = "RCTVideo"; private static final String REACT_CLASS = "RCTVideo";
private static final String PROP_SRC = "src"; private static final String PROP_SRC = "src";
private static final String PROP_AD_TAG_URL = "adTagUrl"; 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_RESIZE_MODE = "resizeMode";
private static final String PROP_REPEAT = "repeat"; private static final String PROP_REPEAT = "repeat";
private static final String PROP_SELECTED_AUDIO_TRACK = "selectedAudioTrack"; private static final String PROP_SELECTED_AUDIO_TRACK = "selectedAudioTrack";
@ -81,7 +77,6 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_DEBUG = "debug"; private static final String PROP_DEBUG = "debug";
private static final String PROP_CONTROLS_STYLES = "controlsStyles"; private static final String PROP_CONTROLS_STYLES = "controlsStyles";
private final ReactExoplayerConfig config; private final ReactExoplayerConfig config;
public ReactExoplayerViewManager(ReactExoplayerConfig config) { public ReactExoplayerViewManager(ReactExoplayerConfig config) {
@ -118,12 +113,6 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
view.eventEmitter.addEventEmitters(reactContext, view); 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) @ReactProp(name = PROP_SRC)
public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) { public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) {
Context context = videoView.getContext().getApplicationContext(); Context context = videoView.getContext().getApplicationContext();

View File

@ -119,6 +119,13 @@ Default: false
The URL pointing to the licenseServer that will provide the authorization to play the protected stream. 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` ### `type`
<PlatformsList types={['Android', 'iOS']} /> <PlatformsList types={['Android', 'iOS']} />

View File

@ -210,6 +210,9 @@ Determines if the player needs to throw an error when connection is lost or not
### `drm` ### `drm`
> [!WARNING]
> deprecated, use source.drm instead
<PlatformsList types={['Android', 'iOS']} /> <PlatformsList types={['Android', 'iOS']} />
To setup DRM please follow [this guide](/component/drm) To setup DRM please follow [this guide](/component/drm)
@ -728,6 +731,29 @@ type: 'mpd' }}
The following other types are supported on some platforms, but aren't fully documented yet: The following other types are supported on some platforms, but aren't fully documented yet:
`content://, ms-appx://, ms-appdata://, assets-library://` `content://, ms-appx://, ms-appdata://, assets-library://`
#### Using DRM content
<PlatformsList types={['Android', 'iOS']} />
To setup DRM please follow [this guide](/component/drm)
Example:
```javascript
{
description: 'WV: Secure SD & HD (cbcs,MP4,H264)',
uri: 'https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd',
drm: {
type: DRMType.WIDEVINE,
licenseServer:
'https://proxy.uat.widevine.com/proxy?provider=widevine_test',
},
},
```
> ⚠️ DRM is not supported on visionOS yet
#### Start playback at a specific point in time #### Start playback at a specific point in time
<PlatformsList types={['Android', 'iOS']} /> <PlatformsList types={['Android', 'iOS']} />

View File

@ -1590,7 +1590,7 @@ SPEC CHECKSUMS:
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Yoga: bd92064a0d558be92786820514d74fc4dddd1233 Yoga: eed50599a85bd9f6882a9938d118aed6a397db9c
PODFILE CHECKSUM: a73d485df51877001f2b04a5a4379cfa5a3ba8fa PODFILE CHECKSUM: a73d485df51877001f2b04a5a4379cfa5a3ba8fa

View File

@ -25,6 +25,7 @@ import Video, {
BufferingStrategyType, BufferingStrategyType,
ReactVideoSource, ReactVideoSource,
SelectedTrackType, SelectedTrackType,
TextTracks,
ResizeMode, ResizeMode,
VideoTrack, VideoTrack,
SelectedTrack, SelectedTrack,
@ -35,6 +36,13 @@ import {AdditionalSourceInfo} from './types';
import {bufferConfig, srcList, textTracksSelectionBy} from './constants'; import {bufferConfig, srcList, textTracksSelectionBy} from './constants';
import {Overlay, toast} from './components'; import {Overlay, toast} from './components';
type AdditionnalSourceInfo = {
textTracks: TextTracks;
adTagUrl: string;
description: string;
noView: boolean;
};
type Props = NonNullable<unknown>; type Props = NonNullable<unknown>;
const VideoPlayer: FC<Props> = ({}) => { const VideoPlayer: FC<Props> = ({}) => {

View File

@ -9,6 +9,8 @@ struct VideoSource {
let cropStart: Int64? let cropStart: Int64?
let cropEnd: Int64? let cropEnd: Int64?
let customMetadata: CustomMetadata? let customMetadata: CustomMetadata?
/* DRM */
let drm: DRMParams?
let json: NSDictionary? let json: NSDictionary?
@ -25,6 +27,7 @@ struct VideoSource {
self.cropStart = nil self.cropStart = nil
self.cropEnd = nil self.cropEnd = nil
self.customMetadata = nil self.customMetadata = nil
self.drm = nil
return return
} }
self.json = json self.json = json
@ -48,5 +51,6 @@ struct VideoSource {
self.cropStart = (json["cropStart"] as? Float64).flatMap { Int64(round($0)) } self.cropStart = (json["cropStart"] as? Float64).flatMap { Int64(round($0)) }
self.cropEnd = (json["cropEnd"] as? Float64).flatMap { Int64(round($0)) } self.cropEnd = (json["cropEnd"] as? Float64).flatMap { Int64(round($0)) }
self.customMetadata = CustomMetadata(json["metadata"] as? NSDictionary) self.customMetadata = CustomMetadata(json["metadata"] as? NSDictionary)
self.drm = DRMParams(json["drm"] as? NSDictionary)
} }
} }

View File

@ -17,10 +17,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
private var _playerViewController: RCTVideoPlayerViewController? private var _playerViewController: RCTVideoPlayerViewController?
private var _videoURL: NSURL? private var _videoURL: NSURL?
/* DRM */
private var _drm: DRMParams?
private var _localSourceEncryptionKeyScheme: String? private var _localSourceEncryptionKeyScheme: String?
/* Required to publish events */ /* Required to publish events */
@ -406,7 +402,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
"type": _source?.type ?? NSNull(), "type": _source?.type ?? NSNull(),
"isNetwork": NSNumber(value: _source?.isNetwork ?? false), "isNetwork": NSNumber(value: _source?.isNetwork ?? false),
], ],
"drm": _drm?.json ?? NSNull(), "drm": source.drm?.json ?? NSNull(),
"target": reactTag, "target": reactTag,
]) ])
@ -443,10 +439,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
} }
#endif #endif
if _drm != nil || _localSourceEncryptionKeyScheme != nil { if source.drm != nil || _localSourceEncryptionKeyScheme != nil {
_resouceLoaderDelegate = RCTResourceLoaderDelegate( _resouceLoaderDelegate = RCTResourceLoaderDelegate(
asset: asset, asset: asset,
drm: _drm, drm: source.drm,
localSourceEncryptionKeyScheme: _localSourceEncryptionKeyScheme, localSourceEncryptionKeyScheme: _localSourceEncryptionKeyScheme,
onVideoError: onVideoError, onVideoError: onVideoError,
onGetLicense: onGetLicense, onGetLicense: onGetLicense,
@ -568,11 +564,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
DispatchQueue.global(qos: .default).async(execute: initializeSource) DispatchQueue.global(qos: .default).async(execute: initializeSource)
} }
@objc
func setDrm(_ drm: NSDictionary) {
_drm = DRMParams(drm)
}
@objc @objc
func setLocalSourceEncryptionKeyScheme(_ keyScheme: String) { func setLocalSourceEncryptionKeyScheme(_ keyScheme: String) {
_localSourceEncryptionKeyScheme = keyScheme _localSourceEncryptionKeyScheme = keyScheme
@ -1271,7 +1262,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
_playerItem = nil _playerItem = nil
_source = nil _source = nil
_chapters = nil _chapters = nil
_drm = nil
_textTracks = nil _textTracks = nil
_selectedTextTrackCriteria = nil _selectedTextTrackCriteria = nil
_selectedAudioTrackCriteria = nil _selectedAudioTrackCriteria = nil

View File

@ -163,6 +163,20 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
) )
); );
const selectedDrm = source.drm || drm;
const _drm = !selectedDrm
? undefined
: {
type: selectedDrm.type,
licenseServer: selectedDrm.licenseServer,
headers: generateHeaderForNative(selectedDrm.headers),
contentId: selectedDrm.contentId,
certificateUrl: selectedDrm.certificateUrl,
base64Certificate: selectedDrm.base64Certificate,
useExternalGetLicense: !!selectedDrm.getLicense,
multiDrm: selectedDrm.multiDrm,
};
return { return {
uri, uri,
isNetwork, isNetwork,
@ -176,26 +190,11 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
cropStart: resolvedSource.cropStart || 0, cropStart: resolvedSource.cropStart || 0,
cropEnd: resolvedSource.cropEnd, cropEnd: resolvedSource.cropEnd,
metadata: resolvedSource.metadata, metadata: resolvedSource.metadata,
drm: _drm,
textTracksAllowChunklessPreparation: textTracksAllowChunklessPreparation:
resolvedSource.textTracksAllowChunklessPreparation, resolvedSource.textTracksAllowChunklessPreparation,
}; };
}, [source]); }, [drm, source]);
const _drm = useMemo(() => {
if (!drm) {
return;
}
return {
type: drm.type,
licenseServer: drm.licenseServer,
headers: generateHeaderForNative(drm.headers),
contentId: drm.contentId,
certificateUrl: drm.certificateUrl,
base64Certificate: drm.base64Certificate,
useExternalGetLicense: !!drm.getLicense,
};
}, [drm]);
const _selectedTextTrack = useMemo(() => { const _selectedTextTrack = useMemo(() => {
if (!selectedTextTrack) { if (!selectedTextTrack) {
@ -612,7 +611,6 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
ref={nativeRef} ref={nativeRef}
{...rest} {...rest}
src={src} src={src}
drm={_drm}
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
resizeMode={resizeMode} resizeMode={resizeMode}
fullscreen={isFullscreen} fullscreen={isFullscreen}

View File

@ -39,6 +39,7 @@ export type VideoSrc = Readonly<{
cropStart?: Float; cropStart?: Float;
cropEnd?: Float; cropEnd?: Float;
metadata?: VideoMetadata; metadata?: VideoMetadata;
drm?: Drm;
textTracksAllowChunklessPreparation?: boolean; // android textTracksAllowChunklessPreparation?: boolean; // android
}>; }>;
@ -57,6 +58,7 @@ type Drm = Readonly<{
certificateUrl?: string; // ios certificateUrl?: string; // ios
base64Certificate?: boolean; // ios default: false base64Certificate?: boolean; // ios default: false
useExternalGetLicense?: boolean; // ios useExternalGetLicense?: boolean; // ios
multiDrm?: boolean; // android
}>; }>;
type TextTracks = ReadonlyArray< type TextTracks = ReadonlyArray<
@ -295,7 +297,6 @@ export type OnControlsVisibilityChange = Readonly<{
export interface VideoNativeProps extends ViewProps { export interface VideoNativeProps extends ViewProps {
src?: VideoSrc; src?: VideoSrc;
drm?: Drm;
adTagUrl?: string; adTagUrl?: string;
allowsExternalPlayback?: boolean; // ios, true allowsExternalPlayback?: boolean; // ios, true
maxBitRate?: Float; maxBitRate?: Float;

View File

@ -24,6 +24,7 @@ export type ReactVideoSourceProperties = {
cropStart?: number; cropStart?: number;
cropEnd?: number; cropEnd?: number;
metadata?: VideoMetadata; metadata?: VideoMetadata;
drm?: Drm;
textTracksAllowChunklessPreparation?: boolean; textTracksAllowChunklessPreparation?: boolean;
}; };
@ -60,6 +61,7 @@ export type Drm = Readonly<{
contentId?: string; // ios contentId?: string; // ios
certificateUrl?: string; // ios certificateUrl?: string; // ios
base64Certificate?: boolean; // ios default: false base64Certificate?: boolean; // ios default: false
multiDrm: boolean; // android
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
getLicense?: ( getLicense?: (
spcBase64: string, spcBase64: string,
@ -211,6 +213,7 @@ export type ControlsStyles = {
export interface ReactVideoProps extends ReactVideoEvents, ViewProps { export interface ReactVideoProps extends ReactVideoEvents, ViewProps {
source?: ReactVideoSource; source?: ReactVideoSource;
/** @deprecated */
drm?: Drm; drm?: Drm;
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
adTagUrl?: string; adTagUrl?: string;
@ -258,8 +261,10 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps {
textTracks?: TextTracks; textTracks?: TextTracks;
testID?: string; testID?: string;
viewType?: ViewType; viewType?: ViewType;
useTextureView?: boolean; // Android // deprecated /** @deprecated */
useSecureView?: boolean; // Android // deprecated useTextureView?: boolean; // Android
/** @deprecated */
useSecureView?: boolean; // Android
volume?: number; volume?: number;
localSourceEncryptionKeyScheme?: string; localSourceEncryptionKeyScheme?: string;
debug?: DebugConfig; debug?: DebugConfig;