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:
		| @@ -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) { | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -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(); | ||||||
|             try { |         // need to realign UUID in DRM Props from source | ||||||
|                 drmSessionManager = self.buildDrmSessionManager(self.drmProps.getDrmUUID(), |         if (drmProps != null && drmProps.getDrmType() != null) { | ||||||
|                         self.drmProps.getDrmLicenseServer(), |             UUID uuid = Util.getDrmUuid(drmProps.getDrmType()); | ||||||
|                         self.drmProps.getDrmLicenseHeader()); |             if (uuid != null) { | ||||||
|             } catch (UnsupportedDrmException e) { |                 try { | ||||||
|                 int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported |                     DebugLog.w(TAG, "drm buildDrmSessionManager"); | ||||||
|                         : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME |                     drmSessionManager = buildDrmSessionManager(uuid, drmProps); | ||||||
|                         ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); |                 } catch (UnsupportedDrmException e) { | ||||||
|                 eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003"); |                     int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported | ||||||
|                 return null; |                             : (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; |         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,21 +944,21 @@ 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) { |  | ||||||
|                 for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { |             String[] keyRequestPropertiesArray = drmProps.getDrmLicenseHeader(); | ||||||
|                     drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]); |             for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { | ||||||
|                 } |                 drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]); | ||||||
|             } |             } | ||||||
|             FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid); |             FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid); | ||||||
|             if (hasDrmFailed) { |             if (hasDrmFailed) { | ||||||
| @@ -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,7 +1817,9 @@ public class ReactExoplayerView extends FrameLayout implements | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void setResizeModeModifier(@ResizeMode.Mode int resizeMode) { |     public void setResizeModeModifier(@ResizeMode.Mode int resizeMode) { | ||||||
|         exoPlayerView.setResizeMode(resizeMode); |         if (exoPlayerView != null) { | ||||||
|  |             exoPlayerView.setResizeMode(resizeMode); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void applyModifiers() { |     private void applyModifiers() { | ||||||
| @@ -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"); | ||||||
|   | |||||||
| @@ -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(); | ||||||
|   | |||||||
| @@ -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']} /> | ||||||
|   | |||||||
| @@ -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']} /> | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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> = ({}) => { | ||||||
|   | |||||||
| @@ -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) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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} | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user