From d3fc4fdbce981e7aa680a4f3d16251b1957798c8 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sun, 5 May 2024 21:06:29 +0200 Subject: [PATCH 01/30] perf: ensure we do not provide callback to native if no callback provided from app --- src/Video.tsx | 66 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/src/Video.tsx b/src/Video.tsx index cc01733890..afba2fd298 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -542,38 +542,60 @@ const Video = forwardRef( selectedAudioTrack={_selectedAudioTrack} selectedVideoTrack={_selectedVideoTrack} onGetLicense={useExternalGetLicense ? onGetLicense : undefined} - onVideoLoad={onVideoLoad as (e: NativeSyntheticEvent) => void} - onVideoLoadStart={onVideoLoadStart} - onVideoError={onVideoError} - onVideoProgress={onVideoProgress} - onVideoSeek={onVideoSeek} + onVideoLoad={ + onLoad + ? (onVideoLoad as (e: NativeSyntheticEvent) => void) + : undefined + } + onVideoLoadStart={onLoadStart ? onVideoLoadStart : undefined} + onVideoError={onError ? onVideoError : undefined} + onVideoProgress={onProgress ? onVideoProgress : undefined} + onVideoSeek={onSeek ? onVideoSeek : undefined} onVideoEnd={onEnd} - onVideoBuffer={onVideoBuffer} - onVideoPlaybackStateChanged={onVideoPlaybackStateChanged} - onVideoBandwidthUpdate={_onBandwidthUpdate} - onTimedMetadata={_onTimedMetadata} - onAudioTracks={_onAudioTracks} - onTextTracks={_onTextTracks} - onTextTrackDataChanged={_onTextTrackDataChanged} - onVideoTracks={_onVideoTracks} + onVideoBuffer={onBuffer ? onVideoBuffer : undefined} + onVideoPlaybackStateChanged={ + onPlaybackRateChange ? onVideoPlaybackStateChanged : undefined + } + onVideoBandwidthUpdate={ + onBandwidthUpdate ? _onBandwidthUpdate : undefined + } + onTimedMetadata={onTimedMetadata ? _onTimedMetadata : undefined} + onAudioTracks={onAudioTracks ? _onAudioTracks : undefined} + onTextTracks={onTextTracks ? _onTextTracks : undefined} + onTextTrackDataChanged={ + onTextTrackDataChanged ? _onTextTrackDataChanged : undefined + } + onVideoTracks={onVideoTracks ? _onVideoTracks : undefined} onVideoFullscreenPlayerDidDismiss={onFullscreenPlayerDidDismiss} onVideoFullscreenPlayerDidPresent={onFullscreenPlayerDidPresent} onVideoFullscreenPlayerWillDismiss={onFullscreenPlayerWillDismiss} onVideoFullscreenPlayerWillPresent={onFullscreenPlayerWillPresent} - onVideoExternalPlaybackChange={onVideoExternalPlaybackChange} - onVideoIdle={onVideoIdle} - onAudioFocusChanged={_onAudioFocusChanged} - onReadyForDisplay={_onReadyForDisplay} - onPlaybackRateChange={_onPlaybackRateChange} - onVolumeChange={_onVolumeChange} + onVideoExternalPlaybackChange={ + onExternalPlaybackChange ? onVideoExternalPlaybackChange : undefined + } + onVideoIdle={onIdle ? onVideoIdle : undefined} + onAudioFocusChanged={ + onAudioFocusChanged ? _onAudioFocusChanged : undefined + } + onReadyForDisplay={onReadyForDisplay ? _onReadyForDisplay : undefined} + onPlaybackRateChange={ + onPlaybackRateChange ? _onPlaybackRateChange : undefined + } + onVolumeChange={onVolumeChange ? _onVolumeChange : undefined} onVideoAudioBecomingNoisy={onAudioBecomingNoisy} - onPictureInPictureStatusChanged={_onPictureInPictureStatusChanged} + onPictureInPictureStatusChanged={ + onPictureInPictureStatusChanged + ? _onPictureInPictureStatusChanged + : undefined + } onRestoreUserInterfaceForPictureInPictureStop={ onRestoreUserInterfaceForPictureInPictureStop } - onVideoAspectRatio={_onVideoAspectRatio} + onVideoAspectRatio={onAspectRatio ? _onVideoAspectRatio : undefined} onReceiveAdEvent={ - _onReceiveAdEvent as (e: NativeSyntheticEvent) => void + onReceiveAdEvent + ? (_onReceiveAdEvent as (e: NativeSyntheticEvent) => void) + : undefined } /> {hasPoster && showPoster ? ( From e1da32d6d47fa8461020f11ad3e8bfffe59b7eae Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Mon, 6 May 2024 21:36:40 +0200 Subject: [PATCH 02/30] chore: rework bufferConfig to make it more generic and reduce ReactExoplayerView code size --- .../com/brentvatne/common/api/BufferConfig.kt | 66 +++++++++++++++++ .../exoplayer/ReactExoplayerSimpleCache.kt | 2 +- .../exoplayer/ReactExoplayerView.java | 71 +++++++++---------- .../exoplayer/ReactExoplayerViewManager.java | 35 ++------- .../brentvatne/react/VideoManagerModule.kt | 4 +- 5 files changed, 105 insertions(+), 73 deletions(-) create mode 100644 android/src/main/java/com/brentvatne/common/api/BufferConfig.kt diff --git a/android/src/main/java/com/brentvatne/common/api/BufferConfig.kt b/android/src/main/java/com/brentvatne/common/api/BufferConfig.kt new file mode 100644 index 0000000000..297a1bf65a --- /dev/null +++ b/android/src/main/java/com/brentvatne/common/api/BufferConfig.kt @@ -0,0 +1,66 @@ +package com.brentvatne.common.api + +import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetDouble +import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetInt +import com.facebook.react.bridge.ReadableMap + +/** + * Class representing bufferConfig for host. + * Only generic code here, no reference to the player. + * By default, if application don't provide input field, -1 is set instead + */ +class BufferConfig { + var cacheSize = BufferConfigPropUnsetInt + var minBufferMs = BufferConfigPropUnsetInt + var maxBufferMs = BufferConfigPropUnsetInt + var bufferForPlaybackMs = BufferConfigPropUnsetInt + var bufferForPlaybackAfterRebufferMs = BufferConfigPropUnsetInt + var backBufferDurationMs = BufferConfigPropUnsetInt + var maxHeapAllocationPercent = BufferConfigPropUnsetDouble + var minBackBufferMemoryReservePercent = BufferConfigPropUnsetDouble + var minBufferMemoryReservePercent = BufferConfigPropUnsetDouble + + companion object { + val BufferConfigPropUnsetInt = -1 + val BufferConfigPropUnsetDouble = -1.0 + + private val PROP_BUFFER_CONFIG_CACHE_SIZE = "cacheSizeMB" + private val PROP_BUFFER_CONFIG_MIN_BUFFER_MS = "minBufferMs" + private val PROP_BUFFER_CONFIG_MAX_BUFFER_MS = "maxBufferMs" + private val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS = "bufferForPlaybackMs" + private val PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = "bufferForPlaybackAfterRebufferMs" + private val PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT = "maxHeapAllocationPercent" + private val PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT = "minBackBufferMemoryReservePercent" + private val PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT = "minBufferMemoryReservePercent" + private val PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS = "backBufferDurationMs" + + @JvmStatic + fun parse(src: ReadableMap?): BufferConfig { + val bufferConfig = BufferConfig() + + if (src != null) { + bufferConfig.cacheSize = safeGetInt(src, PROP_BUFFER_CONFIG_CACHE_SIZE, BufferConfigPropUnsetInt) + bufferConfig.minBufferMs = safeGetInt(src, PROP_BUFFER_CONFIG_MIN_BUFFER_MS, BufferConfigPropUnsetInt) + bufferConfig.maxBufferMs = safeGetInt(src, PROP_BUFFER_CONFIG_MAX_BUFFER_MS, BufferConfigPropUnsetInt) + bufferConfig.bufferForPlaybackMs = safeGetInt(src, PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_MS, BufferConfigPropUnsetInt) + bufferConfig.bufferForPlaybackAfterRebufferMs = + safeGetInt(src, PROP_BUFFER_CONFIG_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, BufferConfigPropUnsetInt) + bufferConfig.maxHeapAllocationPercent = + safeGetDouble(src, PROP_BUFFER_CONFIG_MAX_HEAP_ALLOCATION_PERCENT, BufferConfigPropUnsetDouble) + bufferConfig.minBackBufferMemoryReservePercent = safeGetDouble( + src, + PROP_BUFFER_CONFIG_MIN_BACK_BUFFER_MEMORY_RESERVE_PERCENT, + BufferConfigPropUnsetDouble + ) + bufferConfig.minBufferMemoryReservePercent = + safeGetDouble( + src, + PROP_BUFFER_CONFIG_MIN_BUFFER_MEMORY_RESERVE_PERCENT, + BufferConfigPropUnsetDouble + ) + bufferConfig.backBufferDurationMs = safeGetInt(src, PROP_BUFFER_CONFIG_BACK_BUFFER_DURATION_MS, BufferConfigPropUnsetInt) + } + return bufferConfig + } + } +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerSimpleCache.kt b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerSimpleCache.kt index cec4b898ff..23d51f7793 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerSimpleCache.kt +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerSimpleCache.kt @@ -15,7 +15,7 @@ object RNVSimpleCache { var cacheDataSourceFactory: DataSource.Factory? = null fun setSimpleCache(context: Context, cacheSize: Int, factory: HttpDataSource.Factory) { - if (cacheDataSourceFactory != null || cacheSize == 0) return + if (cacheDataSourceFactory != null || cacheSize <= 0) return simpleCache = SimpleCache( File(context.cacheDir, "RNVCache"), LeastRecentlyUsedCacheEvictor( diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index ba4142454f..1b2623885a 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -94,6 +94,7 @@ import androidx.media3.extractor.metadata.id3.TextInformationFrame; import androidx.media3.ui.LegacyPlayerControlView; +import com.brentvatne.common.api.BufferConfig; import com.brentvatne.common.api.ResizeMode; import com.brentvatne.common.api.SubtitleStyle; import com.brentvatne.common.api.TimedMetadata; @@ -183,18 +184,11 @@ public class ReactExoplayerView extends FrameLayout implements private AudioOutput audioOutput = AudioOutput.SPEAKER; private float audioVolume = 1f; private int minLoadRetryCount = 3; + private BufferConfig bufferConfig = new BufferConfig(); private int maxBitRate = 0; private boolean hasDrmFailed = false; private boolean isUsingContentResolution = false; private boolean selectTrackWhenReady = false; - private int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS; - private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS; - private int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; - private int bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - private int backBufferDurationMs = DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS; - private double maxHeapAllocationPercent = ReactExoplayerView.DEFAULT_MAX_HEAP_ALLOCATION_PERCENT; - private double minBackBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BACK_BUFFER_MEMORY_RESERVE; - private double minBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE; private Handler mainHandler; private Runnable mainRunnable; private DataSource.Factory cacheDataSourceFactory; @@ -495,19 +489,32 @@ private void reLayoutControls() { private class RNVLoadControl extends DefaultLoadControl { private final int availableHeapInBytes; private final Runtime runtime; - public RNVLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, int bufferForPlaybackMs, int bufferForPlaybackAfterRebufferMs, int targetBufferBytes, boolean prioritizeTimeOverSizeThresholds, int backBufferDurationMs, boolean retainBackBufferFromKeyframe) { + public RNVLoadControl(DefaultAllocator allocator, BufferConfig config) { super(allocator, - minBufferMs, - maxBufferMs, - bufferForPlaybackMs, - bufferForPlaybackAfterRebufferMs, - targetBufferBytes, - prioritizeTimeOverSizeThresholds, - backBufferDurationMs, - retainBackBufferFromKeyframe); + config.getMinBufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt() + ? config.getMinBufferMs() + : DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, + config.getMaxBufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt() + ? config.getMaxBufferMs() + : DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, + config.getBufferForPlaybackMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt() + ? config.getBufferForPlaybackMs() + : DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS , + config.getBufferForPlaybackAfterRebufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt() + ? config.getBufferForPlaybackAfterRebufferMs() + : DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + -1, + true, + config.getBackBufferDurationMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt() + ? config.getBackBufferDurationMs() + : DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS, + DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME); runtime = Runtime.getRuntime(); ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(ThemedReactContext.ACTIVITY_SERVICE); - availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeapAllocationPercent * 1024 * 1024); + double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble() + ? bufferConfig.getMaxHeapAllocationPercent() + : DEFAULT_MAX_HEAP_ALLOCATION_PERCENT; + availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeap * 1024 * 1024); } @Override @@ -522,6 +529,9 @@ public boolean shouldContinueLoading(long playbackPositionUs, long bufferedDurat } long usedMemory = runtime.totalMemory() - runtime.freeMemory(); long freeMemory = runtime.maxMemory() - usedMemory; + double minBufferMemoryReservePercent = bufferConfig.getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble() + ? bufferConfig.getMinBufferMemoryReservePercent() + : ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE; long reserveMemory = (long)minBufferMemoryReservePercent * runtime.maxMemory(); long bufferedMs = bufferedDurationUs / (long)1000; if (reserveMemory > freeMemory && bufferedMs > 2000) { @@ -602,14 +612,7 @@ private void initializePlayerCore(ReactExoplayerView self) { DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); RNVLoadControl loadControl = new RNVLoadControl( allocator, - minBufferMs, - maxBufferMs, - bufferForPlaybackMs, - bufferForPlaybackAfterRebufferMs, - -1, - true, - backBufferDurationMs, - DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME + bufferConfig ); DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext()) @@ -2025,26 +2028,18 @@ public void setHideShutterView(boolean hideShutterView) { exoPlayerView.setHideShutterView(hideShutterView); } - public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBufferForPlaybackMs, int newBufferForPlaybackAfterRebufferMs, double newMaxHeapAllocationPercent, double newMinBackBufferMemoryReservePercent, double newMinBufferMemoryReservePercent, int newBackBufferDurationMs, int cacheSize) { - minBufferMs = newMinBufferMs; - maxBufferMs = newMaxBufferMs; - bufferForPlaybackMs = newBufferForPlaybackMs; - bufferForPlaybackAfterRebufferMs = newBufferForPlaybackAfterRebufferMs; - maxHeapAllocationPercent = newMaxHeapAllocationPercent; - minBackBufferMemoryReservePercent = newMinBackBufferMemoryReservePercent; - minBufferMemoryReservePercent = newMinBufferMemoryReservePercent; - if (cacheSize > 0) { + public void setBufferConfig(BufferConfig config) { + bufferConfig = config; + if (bufferConfig.getCacheSize() > 0) { RNVSimpleCache.INSTANCE.setSimpleCache( this.getContext(), - cacheSize, + bufferConfig.getCacheSize(), buildHttpDataSourceFactory(false) ); cacheDataSourceFactory = RNVSimpleCache.INSTANCE.getCacheDataSourceFactory(); } else { cacheDataSourceFactory = null; } - - backBufferDurationMs = newBackBufferDurationMs; releasePlayer(); initializePlayer(); } diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 1e9ffd606b..1f50faa1bd 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -11,6 +11,7 @@ import androidx.media3.datasource.RawResourceDataSource; import androidx.media3.exoplayer.DefaultLoadControl; +import com.brentvatne.common.api.BufferConfig; import com.brentvatne.common.api.ResizeMode; import com.brentvatne.common.api.SubtitleStyle; import com.brentvatne.common.react.VideoEventEmitter; @@ -59,15 +60,6 @@ public class ReactExoplayerViewManager extends ViewGroupManager Unit) { UiThreadUtil.runOnUiThread { From 0f33ad1a1035400599476e5c7d13ab0a584dc750 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Wed, 8 May 2024 15:52:30 +0200 Subject: [PATCH 03/30] chore: improve issue template --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 575ecdc7a3..6fa8e0b3fa 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -7,6 +7,8 @@ body: - type: markdown attributes: value: Thanks for taking the time to fill out this bug report! + Please do not report issue on 5.2.1 version, this version is not maintained anymore. + Only issues on version > V6 will be handled. Please also ensure your issue is reproduced with the last release! - type: textarea id: version From 1066898f0af9ee624367b7ad56fe3d6af5db4ac4 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Fri, 10 May 2024 16:26:28 +0200 Subject: [PATCH 04/30] fix(android): avoid video view flickering at playback startup --- .../brentvatne/exoplayer/ExoPlayerView.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index db56f65659..9b7904c5c3 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -4,16 +4,13 @@ import androidx.core.content.ContextCompat; import androidx.media3.common.AdViewProvider; import androidx.media3.common.C; -import androidx.media3.common.PlaybackException; -import androidx.media3.common.PlaybackParameters; +import androidx.media3.common.Format; import androidx.media3.common.Player; -import androidx.media3.common.Timeline; import androidx.media3.common.Tracks; import androidx.media3.common.VideoSize; import androidx.media3.common.text.Cue; import androidx.media3.common.util.Assertions; import androidx.media3.exoplayer.ExoPlayer; -import androidx.media3.exoplayer.trackselection.TrackSelectionArray; import androidx.media3.ui.SubtitleView; import android.util.AttributeSet; @@ -27,6 +24,7 @@ import com.brentvatne.common.api.ResizeMode; import com.brentvatne.common.api.SubtitleStyle; +import com.google.common.collect.ImmutableList; import java.util.List; @@ -247,19 +245,22 @@ public void setHideShutterView(boolean hideShutterView) { layout(getLeft(), getTop(), getRight(), getBottom()); }; - private void updateForCurrentTrackSelections() { - if (player == null) { + private void updateForCurrentTrackSelections(Tracks tracks) { + if (tracks == null) { return; } - TrackSelectionArray selections = player.getCurrentTrackSelections(); - for (int i = 0; i < selections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { - // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in - // onRenderedFirstFrame(). + ImmutableList groups = tracks.getGroups(); + for (Tracks.Group group: groups) { + if (group.getType() == C.TRACK_TYPE_VIDEO && group.length > 0) { + // get the first track of the group to identify aspect ratio + Format format = group.getTrackFormat(0); + + // update aspect ratio ! + layout.setAspectRatio(format.height == 0 ? 1 : (format.width * format.pixelWidthHeightRatio) / format.height); return; } } - // Video disabled so the shutter must be closed. + // no video tracks, in that case refresh shutterView visibility shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE); } @@ -293,8 +294,7 @@ public void onRenderedFirstFrame() { @Override public void onTracksChanged(Tracks tracks) { - updateForCurrentTrackSelections(); + updateForCurrentTrackSelections(tracks); } } - } From 41e257742546a4b78aa220440ec7be16f60eb321 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Tue, 28 May 2024 11:26:37 +0200 Subject: [PATCH 05/30] chore(android): refactor DRM props into a dedicated class --- .../com/brentvatne/common/api/DRMProps.kt | 64 +++++++++++++++++++ .../exoplayer/ReactExoplayerView.java | 32 ++++------ .../exoplayer/ReactExoplayerViewManager.java | 29 ++------- 3 files changed, 83 insertions(+), 42 deletions(-) create mode 100644 android/src/main/java/com/brentvatne/common/api/DRMProps.kt diff --git a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt new file mode 100644 index 0000000000..c1abadca2e --- /dev/null +++ b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt @@ -0,0 +1,64 @@ +package com.brentvatne.common.api + +import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetArray +import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetString +import com.facebook.react.bridge.ReadableMap +import java.util.UUID + +/** + * Class representing DRM props for host. + * Only generic code here, no reference to the player. + */ +class DRMProps { + /** + * string version of configured UUID for drm prop + */ + var drmType: String? = null + + /** + * Configured UUID for drm prop + */ + var drmUUID: UUID? = null + /** + * DRM license server to be used + */ + var drmLicenseServer: String? = null + /** + * DRM Http Header to access to license server + */ + var drmLicenseHeader: Array = emptyArray() + 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" + + /** parse the source ReadableMap received from app */ + @JvmStatic + fun parse(src: ReadableMap?): DRMProps? { + var drm : DRMProps? = null + if (src != null && src.hasKey(PROP_DRM_TYPE)) { + drm = DRMProps() + drm.drmType = safeGetString(src, PROP_DRM_TYPE) + drm.drmLicenseServer = safeGetString(src, PROP_DRM_LICENSE_SERVER) + val drmHeadersArray = safeGetArray(src, PROP_DRM_HEADERS) + if (drm.drmType != null && drm.drmLicenseServer != null) { + if (drmHeadersArray != null) { + val drmKeyRequestPropertiesList = ArrayList() + for (i in 0 until drmHeadersArray.size()) { + val current = drmHeadersArray.getMap(i) + drmKeyRequestPropertiesList.add(safeGetString(current, PROP_DRM_HEADERS_KEY)) + drmKeyRequestPropertiesList.add(safeGetString(current, PROP_DRM_HEADERS_VALUE)) + } + val array = emptyArray() + drm.drmLicenseHeader = drmKeyRequestPropertiesList.toArray(array) + } + } else { + return null + } + } + return drm + } + } +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 957f7c28ae..a0fdb76d1f 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -110,6 +110,7 @@ import com.brentvatne.common.api.BufferConfig; import com.brentvatne.common.api.BufferingStrategy; import com.brentvatne.common.api.ControlsConfig; +import com.brentvatne.common.api.DRMProps; import com.brentvatne.common.api.ResizeMode; import com.brentvatne.common.api.SideLoadedTextTrack; import com.brentvatne.common.api.SideLoadedTextTrackList; @@ -243,9 +244,8 @@ public class ReactExoplayerView extends FrameLayout implements private boolean playInBackground = false; private Map requestHeaders; private boolean mReportBandwidth = false; - private UUID drmUUID = null; - private String drmLicenseUrl = null; - private String[] drmLicenseHeader = null; + + private DRMProps drmProps; private boolean controls; private Uri adTagUrl; @@ -659,7 +659,7 @@ private void initializePlayer() { es.execute(() -> { // DRM initialization must run on a different thread DrmSessionManager drmSessionManager = initializePlayerDrm(self); - if (drmSessionManager == null && self.drmUUID != null) { + if (drmSessionManager == null && self.drmProps != null) { // Failed to intialize DRM session manager - cannot continue DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!"); eventEmitter.error("Failed to initialize DRM Session Manager Framework!", new Exception("DRM Session Manager Framework failure!"), "3003"); @@ -767,10 +767,11 @@ private void initializePlayerCore(ReactExoplayerView self) { private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) { DrmSessionManager drmSessionManager = null; - if (self.drmUUID != null) { + if (self.drmProps != null) { try { - drmSessionManager = self.buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl, - self.drmLicenseHeader); + 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 @@ -2245,7 +2246,7 @@ public void setFullscreen(boolean fullscreen) { } public void setUseTextureView(boolean useTextureView) { - boolean finallyUseTextureView = useTextureView && this.drmUUID == null; + boolean finallyUseTextureView = useTextureView && drmProps == null; exoPlayerView.setUseTextureView(finallyUseTextureView); } @@ -2272,16 +2273,11 @@ public void setBufferConfig(BufferConfig config) { initializePlayer(); } - public void setDrmType(UUID drmType) { - this.drmUUID = drmType; - } - - public void setDrmLicenseUrl(String licenseUrl){ - this.drmLicenseUrl = licenseUrl; - } - - public void setDrmLicenseHeader(String[] header){ - this.drmLicenseHeader = header; + public void setDrm(DRMProps drmProps) { + this.drmProps = drmProps; + if (drmProps != null && drmProps.getDrmType() != null) { + this.drmProps.setDrmUUID(Util.getDrmUuid(drmProps.getDrmType())); + } } @Override diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index d92b15a8fc..fc45a9f0c7 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -16,6 +16,7 @@ import com.brentvatne.common.api.BufferConfig; import com.brentvatne.common.api.BufferingStrategy; import com.brentvatne.common.api.ControlsConfig; +import com.brentvatne.common.api.DRMProps; import com.brentvatne.common.api.ResizeMode; import com.brentvatne.common.api.SideLoadedTextTrackList; import com.brentvatne.common.api.SubtitleStyle; @@ -49,9 +50,6 @@ public class ReactExoplayerViewManager extends ViewGroupManager drmKeyRequestPropertiesList = new ArrayList<>(); - for (int i = 0; i < drmHeadersArray.size(); i++) { - ReadableMap current = drmHeadersArray.getMap(i); - String key = current.hasKey("key") ? current.getString("key") : null; - String value = current.hasKey("value") ? current.getString("value") : null; - drmKeyRequestPropertiesList.add(key); - drmKeyRequestPropertiesList.add(value); - } - videoView.setDrmLicenseHeader(drmKeyRequestPropertiesList.toArray(new String[0])); - } - videoView.setUseTextureView(false); - } + DRMProps drmProps = DRMProps.parse(drm); + if (drmProps != null) { + videoView.setDrm(drmProps); + videoView.setUseTextureView(false); } } From 755b8347fdc92bbc3f0fc80c54312c6e8b067e62 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet <62574056+freeboub@users.noreply.github.com> Date: Tue, 28 May 2024 11:28:30 +0200 Subject: [PATCH 06/30] Update android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 1 - 1 file changed, 1 deletion(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index a0fdb76d1f..d9d5e2be7f 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -244,7 +244,6 @@ public class ReactExoplayerView extends FrameLayout implements private boolean playInBackground = false; private Map requestHeaders; private boolean mReportBandwidth = false; - private DRMProps drmProps; private boolean controls; private Uri adTagUrl; From dc12ff8fa6d8f38151afbfc221daf33171fbdef6 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Tue, 28 May 2024 11:29:27 +0200 Subject: [PATCH 07/30] chore: fix linter --- android/src/main/java/com/brentvatne/common/api/DRMProps.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt index c1abadca2e..a26d048ac2 100644 --- a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt +++ b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt @@ -19,10 +19,12 @@ class DRMProps { * Configured UUID for drm prop */ var drmUUID: UUID? = null + /** * DRM license server to be used */ var drmLicenseServer: String? = null + /** * DRM Http Header to access to license server */ @@ -37,7 +39,7 @@ class DRMProps { /** parse the source ReadableMap received from app */ @JvmStatic fun parse(src: ReadableMap?): DRMProps? { - var drm : DRMProps? = null + var drm: DRMProps? = null if (src != null && src.hasKey(PROP_DRM_TYPE)) { drm = DRMProps() drm.drmType = safeGetString(src, PROP_DRM_TYPE) From 1e155fc778d306b6f4517aabb2e16207504a443b Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Thu, 30 May 2024 10:42:07 +0200 Subject: [PATCH 08/30] fix: ensure drm prop is correctly cleaned --- .../com/brentvatne/exoplayer/ReactExoplayerViewManager.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index d97dffd737..c43349b9c6 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -117,10 +117,8 @@ public void onDropViewInstance(ReactExoplayerView view) { @ReactProp(name = PROP_DRM) public void setDRM(final ReactExoplayerView videoView, @Nullable ReadableMap drm) { DRMProps drmProps = DRMProps.parse(drm); - if (drmProps != null) { - videoView.setDrm(drmProps); - videoView.setUseTextureView(false); - } + videoView.setDrm(drmProps); + videoView.setUseTextureView(false); } @ReactProp(name = PROP_SRC) From 5d69e28a323efba69df17178488ce6b25d3adcb0 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Fri, 31 May 2024 18:46:47 +0200 Subject: [PATCH 09/30] feat(android): move viewType (secure texture) & drm inside the source The origianl behavior has been kept for interoperability, but marked as deprecated in doc --- .../com/brentvatne/common/api/DRMProps.kt | 27 +++++-- .../java/com/brentvatne/common/api/Source.kt | 20 ++++- .../com/brentvatne/common/api/ViewType.kt | 19 +++++ .../brentvatne/exoplayer/ExoPlayerView.java | 77 +++++++++---------- .../exoplayer/ReactExoplayerView.java | 70 +++++++---------- .../exoplayer/ReactExoplayerViewManager.java | 25 +----- docs/pages/component/props.mdx | 47 +++++++++-- examples/basic/src/VideoPlayer.tsx | 2 - examples/basic/src/components/Seeker.tsx | 1 - src/Video.tsx | 54 +++++++------ src/specs/VideoNativeComponent.ts | 5 +- src/types/ViewType.ts | 11 +++ src/types/index.ts | 1 + src/types/video.ts | 9 ++- 14 files changed, 218 insertions(+), 150 deletions(-) create mode 100644 android/src/main/java/com/brentvatne/common/api/ViewType.kt create mode 100644 src/types/ViewType.ts diff --git a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt index a26d048ac2..c262241498 100644 --- a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt +++ b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt @@ -15,11 +15,6 @@ class DRMProps { */ var drmType: String? = null - /** - * Configured UUID for drm prop - */ - var drmUUID: UUID? = null - /** * DRM license server to be used */ @@ -29,6 +24,28 @@ class DRMProps { * DRM Http Header to access to license server */ var drmLicenseHeader: Array = emptyArray() + + /** return true if this and src are equals */ + override fun equals(other: Any?): Boolean { + if (other == null || other !is DRMProps) return false + val seemsEqual = ( + drmType == other.drmType && + drmLicenseServer == other.drmLicenseServer && + drmLicenseHeader.size == other.drmLicenseHeader.size + ) + if (!seemsEqual) { + return false + } + var i = 0 + while (i < drmLicenseHeader.size) { + if (drmLicenseHeader[i] != other.drmLicenseHeader[i]) { + return false + } + i++ + } + return true + } + companion object { private const val PROP_DRM_TYPE = "type" private const val PROP_DRM_LICENSE_SERVER = "licenseServer" diff --git a/android/src/main/java/com/brentvatne/common/api/Source.kt b/android/src/main/java/com/brentvatne/common/api/Source.kt index 60c2711bd2..1d94187480 100644 --- a/android/src/main/java/com/brentvatne/common/api/Source.kt +++ b/android/src/main/java/com/brentvatne/common/api/Source.kt @@ -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 @@ -44,6 +45,17 @@ class Source { /** http header list */ val headers: MutableMap = HashMap() + /** + * DRM properties linked to the source + */ + var drmProps: DRMProps? = null + + /** + * Type of view to be used + */ + @ViewType.ViewType + var viewType = ViewType.VIEW_TYPE_SURFACE + /** return true if this and src are equals */ override fun equals(other: Any?): Boolean { if (other == null || other !is Source) return false @@ -52,7 +64,9 @@ class Source { cropStartMs == other.cropStartMs && cropEndMs == other.cropEndMs && startPositionMs == other.startPositionMs && - extension == other.extension + extension == other.extension && + viewType == other.viewType && + drmProps == other.drmProps ) } @@ -116,6 +130,8 @@ 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_VIEW_TYPE = "viewType" + private const val PROP_SRC_DRM = "drm" @SuppressLint("DiscouragedApi") private fun getUriFromAssetId(context: Context, uriString: String): Uri? { @@ -172,6 +188,8 @@ 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.viewType = safeGetInt(src, PROP_SRC_VIEW_TYPE, ViewType.VIEW_TYPE_SURFACE) + source.drmProps = parse(safeGetMap(src, PROP_SRC_DRM)); val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS) if (propSrcHeadersArray != null) { diff --git a/android/src/main/java/com/brentvatne/common/api/ViewType.kt b/android/src/main/java/com/brentvatne/common/api/ViewType.kt new file mode 100644 index 0000000000..c171a231a2 --- /dev/null +++ b/android/src/main/java/com/brentvatne/common/api/ViewType.kt @@ -0,0 +1,19 @@ +package com.brentvatne.common.api + +internal object ViewType { + /** + * View used will be a TextureView. + */ + const val VIEW_TYPE_TEXTURE = 0 + + /** + * View used will be a SurfaceView. + */ + const val VIEW_TYPE_SURFACE = 1 + + /** + * View used will be a SurfaceView with secure flag set. + */ + const val VIEW_TYPE_SURFACE_SECURE = 2 + annotation class ViewType +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index e254975f40..1243fba454 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -26,12 +26,15 @@ import com.brentvatne.common.api.ResizeMode; import com.brentvatne.common.api.SubtitleStyle; +import com.brentvatne.common.api.ViewType; +import com.brentvatne.common.toolbox.DebugLog; import com.google.common.collect.ImmutableList; import java.util.List; public final class ExoPlayerView extends FrameLayout implements AdViewProvider { + private final static String TAG = "ExoPlayerView"; private View surfaceView; private final View shutterView; private final SubtitleView subtitleLayout; @@ -42,19 +45,17 @@ public final class ExoPlayerView extends FrameLayout implements AdViewProvider { private final ViewGroup.LayoutParams layoutParams; private final FrameLayout adOverlayFrameLayout; - private boolean useTextureView = true; - private boolean useSecureView = false; private boolean hideShutterView = false; - public ExoPlayerView(Context context) { - this(context, null); + public ExoPlayerView(Context context, @ViewType.ViewType int viewType) { + this(context, null, viewType); } - public ExoPlayerView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + public ExoPlayerView(Context context, AttributeSet attrs, @ViewType.ViewType int viewType) { + this(context, attrs, 0, viewType); } - public ExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) { + public ExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr, @ViewType.ViewType int viewType) { super(context, attrs, defStyleAttr); this.context = context; @@ -81,7 +82,7 @@ public ExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) { subtitleLayout.setUserDefaultStyle(); subtitleLayout.setUserDefaultTextSize(); - updateSurfaceView(); + updateSurfaceView(viewType); adOverlayFrameLayout = new FrameLayout(context); @@ -134,28 +135,38 @@ public void setShutterColor(Integer color) { shutterView.setBackgroundColor(color); } - private void updateSurfaceView() { - View view; - if (!useTextureView || useSecureView) { - view = new SurfaceView(context); - if (useSecureView) { - ((SurfaceView)view).setSecure(true); + public void updateSurfaceView(@ViewType.ViewType int viewType) { + DebugLog.w(TAG, "allocate surface type " + viewType); + boolean viewNeedRefresh = false; + if (viewType == ViewType.VIEW_TYPE_SURFACE || viewType == ViewType.VIEW_TYPE_SURFACE_SECURE) { + if (!(surfaceView instanceof SurfaceView)) { + surfaceView = new SurfaceView(context); + viewNeedRefresh = true; + } + if (viewType == ViewType.VIEW_TYPE_SURFACE_SECURE) { + ((SurfaceView)surfaceView).setSecure(true); + } + } else if (viewType == ViewType.VIEW_TYPE_TEXTURE) { + if (!(surfaceView instanceof TextureView)) { + surfaceView = new TextureView(context); + viewNeedRefresh = true; } - } else { - view = new TextureView(context); // Support opacity properly: - ((TextureView) view).setOpaque(false); + ((TextureView) surfaceView).setOpaque(false); + } else { + DebugLog.wtf(TAG, "wtf is this texture " + viewType); } - view.setLayoutParams(layoutParams); + if (viewNeedRefresh) { + surfaceView.setLayoutParams(layoutParams); - surfaceView = view; - if (layout.getChildAt(0) != null) { - layout.removeViewAt(0); - } - layout.addView(surfaceView, 0, layoutParams); + if (layout.getChildAt(0) != null) { + layout.removeViewAt(0); + } + layout.addView(surfaceView, 0, layoutParams); - if (this.player != null) { - setVideoView(); + if (this.player != null) { + setVideoView(); + } } } @@ -205,26 +216,12 @@ 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); } } - public void setUseTextureView(boolean useTextureView) { - if (useTextureView != this.useTextureView) { - this.useTextureView = useTextureView; - updateSurfaceView(); - } - } - - public void useSecureView(boolean useSecureView) { - if (useSecureView != this.useSecureView) { - this.useSecureView = useSecureView; - updateSurfaceView(); - } - } - public void setHideShutterView(boolean hideShutterView) { this.hideShutterView = hideShutterView; updateShutterViewVisibility(); diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 149e1ffa8c..417afb3661 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -215,7 +215,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(); @@ -239,7 +239,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; @@ -298,7 +297,7 @@ public ReactExoplayerView(ThemedReactContext context, ReactExoplayerConfig confi this.eventEmitter = new VideoEventEmitter(context); this.config = config; this.bandwidthMeter = config.getBandwidthMeter(); - + mainHandler = new Handler(); createViews(); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); @@ -325,14 +324,11 @@ private void createViews() { LayoutParams layoutParams = new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - exoPlayerView = new ExoPlayerView(getContext()); + exoPlayerView = new ExoPlayerView(getContext(), source.getViewType()); exoPlayerView.setLayoutParams(layoutParams); - addView(exoPlayerView, 0, layoutParams); exoPlayerView.setFocusable(this.focusable); - - mainHandler = new Handler(); } // LifecycleEventListener implementation @@ -641,6 +637,7 @@ private void initializePlayer() { mainRunnable = () -> { try { if (player == null) { + exoPlayerView.updateSurfaceView(source.getViewType()); // Initialize core configuration and listeners initializePlayerCore(self); } @@ -750,19 +747,24 @@ 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.error(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.getDrmLicenseServer(), + 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.error(getResources().getString(errorStringId), e, "3003"); + } } } return drmSessionManager; @@ -772,14 +774,10 @@ 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 - DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!"); - eventEmitter.error("Failed to initialize DRM Session Manager Framework!", new Exception("DRM Session Manager Framework failure!"), "3003"); - return; - } + /// init DRM + DrmSessionManager drmSessionManager = initializePlayerDrm(); + // init source to manage ads and external text tracks ArrayList mediaSourceList = buildTextSources(); MediaSource videoSource = buildMediaSource(source.getUri(), source.getExtension(), drmSessionManager, source.getCropStartMs(), source.getCropEndMs()); MediaSource mediaSourceWithAds = null; @@ -1796,7 +1794,9 @@ private void reloadSource() { } public void setResizeModeModifier(@ResizeMode.Mode int resizeMode) { - exoPlayerView.setResizeMode(resizeMode); + if (exoPlayerView != null) { + exoPlayerView.setResizeMode(resizeMode); + } } private void applyModifiers() { @@ -2223,15 +2223,6 @@ public void setFullscreen(boolean fullscreen) { updateFullScreenButtonVisbility(); } - public void setUseTextureView(boolean useTextureView) { - boolean finallyUseTextureView = useTextureView && drmProps == null; - exoPlayerView.setUseTextureView(finallyUseTextureView); - } - - public void useSecureView(boolean useSecureView) { - exoPlayerView.useSecureView(useSecureView); - } - public void setHideShutterView(boolean hideShutterView) { exoPlayerView.setHideShutterView(hideShutterView); } @@ -2251,13 +2242,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"); diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index c43349b9c6..8295023d59 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -17,6 +17,7 @@ import com.brentvatne.common.api.SideLoadedTextTrackList; import com.brentvatne.common.api.Source; import com.brentvatne.common.api.SubtitleStyle; +import com.brentvatne.common.api.ViewType; import com.brentvatne.common.react.VideoEventEmitter; import com.brentvatne.common.toolbox.DebugLog; import com.brentvatne.common.toolbox.ReactBridgeUtils; @@ -27,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; @@ -39,8 +38,6 @@ public class ReactExoplayerViewManager extends ViewGroupManager - -To setup DRM please follow [this guide](/component/drm) - -> ⚠️ DRM is not supported on visionOS yet +> [!WARNING] +> deprecated, use source.drm instead ### `filter` @@ -728,6 +725,29 @@ type: 'mpd' }} The following other types are supported on some platforms, but aren't fully documented yet: `content://, ms-appx://, ms-appdata://, assets-library://` +#### Using DRM content + + + +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 @@ -772,6 +792,17 @@ source={{ } }} ``` +#### viewType + + + +Allow to explicitly specify view type. +This flag replace `useSecureView` and `useTextureView` fields. +There are 3 available values: +- 'textureView': The video is rendered in a texture view. it allows mapping the view on a texture (usefull for 3D). +DRM playback is not supported on textureView, if drm prop is provided, the suface will be transformed to a SurfaceView. +- 'surfaceView' (default): The video is rendered in a surface. take less resources to be rendered. +- 'secureView': The video is rendered in a surface which disallow taking screenshot of the video ### `subtitleStyle` @@ -873,6 +904,9 @@ To customize the notification controls you can use `metadata` property in the `s ### `useSecureView` +> [!WARNING] +> deprecated, use source.viewType instead + Force the output to a SurfaceView and enables the secure surface. @@ -886,6 +920,9 @@ SurfaceView is is the only one that can be labeled as secure. ### `useTextureView` +> [!WARNING] +> deprecated, use source.viewType instead + Controls whether to output to a TextureView or SurfaceView. diff --git a/examples/basic/src/VideoPlayer.tsx b/examples/basic/src/VideoPlayer.tsx index f712e0134f..822c60e406 100644 --- a/examples/basic/src/VideoPlayer.tsx +++ b/examples/basic/src/VideoPlayer.tsx @@ -57,7 +57,6 @@ type AdditionnalSourceInfo = { textTracks: TextTracks; adTagUrl: string; description: string; - drm: Drm; noView: boolean; }; @@ -769,7 +768,6 @@ class VideoPlayer extends Component { source={currentSrc as ReactVideoSource} textTracks={additionnal?.textTracks} adTagUrl={additionnal?.adTagUrl} - drm={additionnal?.drm} style={viewStyle} rate={this.state.rate} paused={this.state.paused} diff --git a/examples/basic/src/components/Seeker.tsx b/examples/basic/src/components/Seeker.tsx index 2ca7a28055..a74122b0c8 100644 --- a/examples/basic/src/components/Seeker.tsx +++ b/examples/basic/src/components/Seeker.tsx @@ -99,7 +99,6 @@ const Seeker = ({ useEffect(() => { if (!isLoading && !seeking && !isUISeeking) { - console.log('update position from currentTime'); const percent = currentTime / duration; const position = seekerWidth * percent; updateSeekerPosition(position); diff --git a/src/Video.tsx b/src/Video.tsx index c2dba261c4..45c1a535fa 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -43,11 +43,12 @@ import { resolveAssetSourceForVideo, } from './utils'; import {VideoManager} from './specs/VideoNativeComponent'; -import type { - OnLoadData, - OnTextTracksData, - OnReceiveAdEventData, - ReactVideoProps, +import { + type OnLoadData, + type OnTextTracksData, + type OnReceiveAdEventData, + type ReactVideoProps, + ViewType, } from './types'; export type VideoSaveData = { @@ -82,6 +83,8 @@ const Video = forwardRef( selectedVideoTrack, selectedAudioTrack, selectedTextTrack, + useTextureView, + useSecureView, onLoadStart, onLoad, onError, @@ -156,6 +159,26 @@ const Video = forwardRef( ) ); + 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, + }; + + const viewType = useSecureView + ? ViewType.SURFACE_SECURE + : useTextureView && + (_drm === undefined || Object.keys(_drm).length === 0) + ? ViewType.TEXTURE + : ViewType.SURFACE; + return { uri, isNetwork, @@ -169,24 +192,10 @@ const Video = forwardRef( cropStart: resolvedSource.cropStart || 0, cropEnd: resolvedSource.cropEnd, metadata: resolvedSource.metadata, + viewType, + drm: _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]); + }, [drm, source, useSecureView, useTextureView]); const _selectedTextTrack = useMemo(() => { if (!selectedTextTrack) { @@ -538,7 +547,6 @@ const Video = forwardRef( ref={nativeRef} {...rest} src={src} - drm={_drm} style={StyleSheet.absoluteFill} resizeMode={resizeMode} fullscreen={isFullscreen} diff --git a/src/specs/VideoNativeComponent.ts b/src/specs/VideoNativeComponent.ts index d776956c03..349c21de37 100644 --- a/src/specs/VideoNativeComponent.ts +++ b/src/specs/VideoNativeComponent.ts @@ -39,6 +39,8 @@ export type VideoSrc = Readonly<{ cropStart?: Float; cropEnd?: Float; metadata?: VideoMetadata; + viewType?: Int32; // Android + drm?: Drm; }>; type DRMType = WithDefault; @@ -290,7 +292,6 @@ type ControlsStyles = Readonly<{ export interface VideoNativeProps extends ViewProps { src?: VideoSrc; - drm?: Drm; adTagUrl?: string; allowsExternalPlayback?: boolean; // ios, true maxBitRate?: Float; @@ -332,8 +333,6 @@ export interface VideoNativeProps extends ViewProps { minLoadRetryCount?: Int32; // Android reportBandwidth?: boolean; //Android subtitleStyle?: SubtitleStyle; // android - useTextureView?: boolean; // Android - useSecureView?: boolean; // Android bufferingStrategy?: BufferingStrategyType; // Android controlsStyles?: ControlsStyles; // Android onVideoLoad?: DirectEventHandler; diff --git a/src/types/ViewType.ts b/src/types/ViewType.ts new file mode 100644 index 0000000000..34e6a419b4 --- /dev/null +++ b/src/types/ViewType.ts @@ -0,0 +1,11 @@ +/** + * Define Available view type for android + * these values shall match android spec, see ViewType.kt + */ +enum ResizeMode { + TEXTURE = 0, + SURFACE = 1, + SURFACE_SECURE = 2, +} + +export default ResizeMode; diff --git a/src/types/index.ts b/src/types/index.ts index 5742eac029..ab7cf42db9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,5 +5,6 @@ export * from './language'; export {default as Orientation} from './Orientation'; export {default as ResizeMode} from './ResizeMode'; export {default as TextTrackType} from './TextTrackType'; +export {default as ViewType} from './ViewType'; export * from './video'; export * from '../specs/VideoNativeComponent'; diff --git a/src/types/video.ts b/src/types/video.ts index 7c6001c5de..ac34f62537 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -3,6 +3,7 @@ import type {ReactVideoEvents} from './events'; import type {StyleProp, ViewProps, ViewStyle} from 'react-native'; import type VideoResizeMode from './ResizeMode'; import type FilterType from './FilterType'; +import type ViewType from './ViewType'; export type Headers = Record; @@ -23,6 +24,8 @@ export type ReactVideoSourceProperties = { cropStart?: number; cropEnd?: number; metadata?: VideoMetadata; + drm?: Drm; + viewType?: ViewType; // Android }; export type ReactVideoSource = Readonly< @@ -209,7 +212,7 @@ export type ControlsStyles = { export interface ReactVideoProps extends ReactVideoEvents, ViewProps { source?: ReactVideoSource; - drm?: Drm; + drm?: Drm; // deprecated style?: StyleProp; adTagUrl?: string; audioOutput?: AudioOutput; // Mobile @@ -255,8 +258,8 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps { shutterColor?: string; // Android textTracks?: TextTracks; testID?: string; - useTextureView?: boolean; // Android - useSecureView?: boolean; // Android + useTextureView?: boolean; // Android // deprecated + useSecureView?: boolean; // Android // deprecated volume?: number; localSourceEncryptionKeyScheme?: string; debug?: DebugConfig; From cc5aceb28e5cac075f01c0579674a554c4a9cd04 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Fri, 31 May 2024 18:52:34 +0200 Subject: [PATCH 10/30] chore: fix linter --- android/src/main/java/com/brentvatne/common/api/DRMProps.kt | 3 +-- android/src/main/java/com/brentvatne/common/api/Source.kt | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt index c262241498..b5b00206f8 100644 --- a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt +++ b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt @@ -3,7 +3,6 @@ package com.brentvatne.common.api import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetArray import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetString import com.facebook.react.bridge.ReadableMap -import java.util.UUID /** * Class representing DRM props for host. @@ -29,7 +28,7 @@ class DRMProps { override fun equals(other: Any?): Boolean { if (other == null || other !is DRMProps) return false val seemsEqual = ( - drmType == other.drmType && + drmType == other.drmType && drmLicenseServer == other.drmLicenseServer && drmLicenseHeader.size == other.drmLicenseHeader.size ) diff --git a/android/src/main/java/com/brentvatne/common/api/Source.kt b/android/src/main/java/com/brentvatne/common/api/Source.kt index 1d94187480..ce59a6a4a7 100644 --- a/android/src/main/java/com/brentvatne/common/api/Source.kt +++ b/android/src/main/java/com/brentvatne/common/api/Source.kt @@ -71,9 +71,7 @@ class Source { } /** return true if this and src are equals */ - fun isEquals(source: Source): Boolean { - return this == source - } + fun isEquals(source: Source): Boolean = this == source /** Metadata to display in notification */ class Metadata { @@ -189,7 +187,7 @@ class Source { source.cropEndMs = safeGetInt(src, PROP_SRC_CROP_END, -1) source.extension = safeGetString(src, PROP_SRC_TYPE, null) source.viewType = safeGetInt(src, PROP_SRC_VIEW_TYPE, ViewType.VIEW_TYPE_SURFACE) - source.drmProps = parse(safeGetMap(src, PROP_SRC_DRM)); + source.drmProps = parse(safeGetMap(src, PROP_SRC_DRM)) val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS) if (propSrcHeadersArray != null) { From df474efe7cfb8ae99114a110535b828dcd96ed06 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Fri, 31 May 2024 23:05:37 +0200 Subject: [PATCH 11/30] chore(ios): move drm prop in source like on android --- examples/basic/ios/Podfile.lock | 4 ++-- ios/Video/DataStructures/VideoSource.swift | 4 ++++ ios/Video/RCTVideo.swift | 15 +++------------ 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/examples/basic/ios/Podfile.lock b/examples/basic/ios/Podfile.lock index a92a0b4867..1f97b5fb76 100644 --- a/examples/basic/ios/Podfile.lock +++ b/examples/basic/ios/Podfile.lock @@ -1398,7 +1398,7 @@ SPEC CHECKSUMS: fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 hermes-engine: 16b8530de1b383cdada1476cf52d1b52f0692cbc - RCT-Folly: 02617c592a293bd6d418e0a88ff4ee1f88329b47 + RCT-Folly: 045d6ecaa59d826c5736dfba0b2f4083ff8d79df RCTDeprecation: efb313d8126259e9294dc4ee0002f44a6f676aba RCTRequired: f49ea29cece52aee20db633ae7edc4b271435562 RCTTypeSafety: a11979ff0570d230d74de9f604f7d19692157bc4 @@ -1452,4 +1452,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: bbe1bd5f521b6b9366288dbaf8af1cec85c0a95a -COCOAPODS: 1.13.0 +COCOAPODS: 1.15.2 diff --git a/ios/Video/DataStructures/VideoSource.swift b/ios/Video/DataStructures/VideoSource.swift index ab011d9894..e1304d93f1 100644 --- a/ios/Video/DataStructures/VideoSource.swift +++ b/ios/Video/DataStructures/VideoSource.swift @@ -9,6 +9,8 @@ struct VideoSource { let cropStart: Int64? let cropEnd: Int64? let customMetadata: CustomMetadata? + /* DRM */ + let drm: DRMParams? let json: NSDictionary? @@ -25,6 +27,7 @@ struct VideoSource { self.cropStart = nil self.cropEnd = nil self.customMetadata = nil + self.drm = nil return } self.json = json @@ -48,5 +51,6 @@ struct VideoSource { self.cropStart = (json["cropStart"] as? Float64).flatMap { Int64(round($0)) } self.cropEnd = (json["cropEnd"] as? Float64).flatMap { Int64(round($0)) } self.customMetadata = CustomMetadata(json["metadata"] as? NSDictionary) + self.drm = DRMParams(json["drm"] as? NSDictionary ) } } diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index cfa3554ad6..301e9908b0 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -17,10 +17,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH private var _playerViewController: RCTVideoPlayerViewController? private var _videoURL: NSURL? - - /* DRM */ - private var _drm: DRMParams? - private var _localSourceEncryptionKeyScheme: String? /* Required to publish events */ @@ -388,7 +384,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH "type": _source?.type ?? NSNull(), "isNetwork": NSNumber(value: _source?.isNetwork ?? false), ], - "drm": _drm?.json ?? NSNull(), + "drm": source.drm?.json ?? NSNull(), "target": reactTag, ]) @@ -425,10 +421,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } #endif - if _drm != nil || _localSourceEncryptionKeyScheme != nil { + if source.drm != nil || _localSourceEncryptionKeyScheme != nil { _resouceLoaderDelegate = RCTResourceLoaderDelegate( asset: asset, - drm: _drm, + drm: source.drm, localSourceEncryptionKeyScheme: _localSourceEncryptionKeyScheme, onVideoError: onVideoError, onGetLicense: onGetLicense, @@ -546,11 +542,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH DispatchQueue.global(qos: .default).async(execute: initializeSource) } - @objc - func setDrm(_ drm: NSDictionary) { - _drm = DRMParams(drm) - } - @objc func setLocalSourceEncryptionKeyScheme(_ keyScheme: String) { _localSourceEncryptionKeyScheme = keyScheme From 18360318ec981351a8576074dca01eb3afbe5734 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Fri, 31 May 2024 23:30:58 +0200 Subject: [PATCH 12/30] chore: fix linter --- ios/Video/DataStructures/VideoSource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Video/DataStructures/VideoSource.swift b/ios/Video/DataStructures/VideoSource.swift index e1304d93f1..b23d8ede49 100644 --- a/ios/Video/DataStructures/VideoSource.swift +++ b/ios/Video/DataStructures/VideoSource.swift @@ -51,6 +51,6 @@ struct VideoSource { self.cropStart = (json["cropStart"] as? Float64).flatMap { Int64(round($0)) } self.cropEnd = (json["cropEnd"] as? Float64).flatMap { Int64(round($0)) } self.customMetadata = CustomMetadata(json["metadata"] as? NSDictionary) - self.drm = DRMParams(json["drm"] as? NSDictionary ) + self.drm = DRMParams(json["drm"] as? NSDictionary) } } From 3f87ee9a0204f157f89886a432ca448a9a05b936 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sun, 2 Jun 2024 15:49:56 +0200 Subject: [PATCH 13/30] chore: clean log --- .../src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java | 1 - 1 file changed, 1 deletion(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index 1243fba454..2ae78a3a73 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -136,7 +136,6 @@ public void setShutterColor(Integer color) { } public void updateSurfaceView(@ViewType.ViewType int viewType) { - DebugLog.w(TAG, "allocate surface type " + viewType); boolean viewNeedRefresh = false; if (viewType == ViewType.VIEW_TYPE_SURFACE || viewType == ViewType.VIEW_TYPE_SURFACE_SECURE) { if (!(surfaceView instanceof SurfaceView)) { From 22fa2a81d35d741453c61128f948d27c304f5c4b Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sun, 2 Jun 2024 15:51:43 +0200 Subject: [PATCH 14/30] fix: allow to disable secure View --- .../src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index 2ae78a3a73..ae9effd8b4 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -142,9 +142,7 @@ public void updateSurfaceView(@ViewType.ViewType int viewType) { surfaceView = new SurfaceView(context); viewNeedRefresh = true; } - if (viewType == ViewType.VIEW_TYPE_SURFACE_SECURE) { - ((SurfaceView)surfaceView).setSecure(true); - } + ((SurfaceView)surfaceView).setSecure(viewType == ViewType.VIEW_TYPE_SURFACE_SECURE); } else if (viewType == ViewType.VIEW_TYPE_TEXTURE) { if (!(surfaceView instanceof TextureView)) { surfaceView = new TextureView(context); From 9557fc09d0439e83c761d8a7dfffa6314d893605 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sun, 2 Jun 2024 16:00:05 +0200 Subject: [PATCH 15/30] chore: fix viewType resolution (source value was not handled) --- src/Video.tsx | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Video.tsx b/src/Video.tsx index 45c1a535fa..2a25286ad1 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -172,12 +172,20 @@ const Video = forwardRef( useExternalGetLicense: !!selectedDrm.getLicense, }; - const viewType = useSecureView - ? ViewType.SURFACE_SECURE - : useTextureView && - (_drm === undefined || Object.keys(_drm).length === 0) - ? ViewType.TEXTURE - : ViewType.SURFACE; + const hasValidDrmProp = + _drm === undefined || Object.keys(_drm).length === 0; + + const viewType = + hasValidDrmProp && + (source.viewType === ViewType.TEXTURE || useTextureView) + ? ViewType.SURFACE // check if we should force the type to Surface due to DRM + : source.viewType + ? source.viewType // else use ViewType from source + : useSecureView // else infer view type from useSecureView and useTextureView + ? ViewType.SURFACE_SECURE + : useTextureView + ? ViewType.TEXTURE + : ViewType.SURFACE; return { uri, From 96d38cb9b536bd65f38a04f5f4dc2fe999602cc9 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sun, 2 Jun 2024 21:29:14 +0200 Subject: [PATCH 16/30] chore: use contentDeepEquals instead of manual checks --- .../java/com/brentvatne/common/api/DRMProps.kt | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt index b5b00206f8..0649948a48 100644 --- a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt +++ b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt @@ -27,22 +27,9 @@ class DRMProps { /** return true if this and src are equals */ override fun equals(other: Any?): Boolean { if (other == null || other !is DRMProps) return false - val seemsEqual = ( - drmType == other.drmType && + return drmType == other.drmType && drmLicenseServer == other.drmLicenseServer && - drmLicenseHeader.size == other.drmLicenseHeader.size - ) - if (!seemsEqual) { - return false - } - var i = 0 - while (i < drmLicenseHeader.size) { - if (drmLicenseHeader[i] != other.drmLicenseHeader[i]) { - return false - } - i++ - } - return true + drmLicenseHeader.contentDeepEquals(other.drmLicenseHeader) // drmLicenseHeader is never null } companion object { From 784c2ae8abace35ff4caea261dfabe3933c191e6 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sun, 2 Jun 2024 21:43:25 +0200 Subject: [PATCH 17/30] chore: fix linter --- android/src/main/java/com/brentvatne/common/api/DRMProps.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt index 0649948a48..ad5a7e6eb5 100644 --- a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt +++ b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt @@ -28,8 +28,8 @@ class DRMProps { override fun equals(other: Any?): Boolean { if (other == null || other !is DRMProps) return false return drmType == other.drmType && - drmLicenseServer == other.drmLicenseServer && - drmLicenseHeader.contentDeepEquals(other.drmLicenseHeader) // drmLicenseHeader is never null + drmLicenseServer == other.drmLicenseServer && + drmLicenseHeader.contentDeepEquals(other.drmLicenseHeader) // drmLicenseHeader is never null } companion object { From 0eba6daaee6c5202bb35f824414e85166a50f1bb Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Tue, 4 Jun 2024 18:13:51 +0200 Subject: [PATCH 18/30] fix: ensure player doesn't start when view is unmounted --- .../exoplayer/ReactExoplayerView.java | 17 ++++++++++++++--- ios/Video/NowPlayingInfoCenterManager.swift | 4 ++-- ios/Video/RCTVideo.swift | 10 ++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index a417489125..6b54a9a4c4 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -258,6 +258,7 @@ public class ReactExoplayerView extends FrameLayout implements private long lastBufferDuration = -1; private long lastDuration = -1; + private boolean viewHasDropped = false; private void updateProgress() { if (player != null) { if (playerControlView != null && isPlayingAd() && controls) { @@ -375,6 +376,8 @@ protected void onDetachedFromWindow() { public void cleanUpResources() { stopPlayback(); themedReactContext.removeLifecycleEventListener(this); + releasePlayer(); + viewHasDropped = true; } //BandwidthMeter.EventListener implementation @@ -647,6 +650,9 @@ private void initializePlayer() { Activity activity = themedReactContext.getCurrentActivity(); // This ensures all props have been settled, to avoid async racing conditions. mainRunnable = () -> { + if (viewHasDropped) { + return; + } try { if (player == null) { // Initialize core configuration and listeners @@ -658,7 +664,9 @@ private void initializePlayer() { ExecutorService es = Executors.newSingleThreadExecutor(); es.execute(() -> { // DRM initialization must run on a different thread - + if (viewHasDropped) { + return; + } if (activity == null) { DebugLog.e(TAG, "Failed to initialize Player!, null activity"); eventEmitter.error("Failed to initialize Player!", new Exception("Current Activity is null!"), "1001"); @@ -667,12 +675,15 @@ private void initializePlayer() { // Initialize handler to run on the main thread activity.runOnUiThread(() -> { + if (viewHasDropped) { + return; + } try { // Source initialization must run on the main thread initializePlayerSource(); } catch (Exception ex) { self.playerNeedsSource = true; - DebugLog.e(TAG, "Failed to initialize Player!"); + DebugLog.e(TAG, "Failed to initialize Player! 1"); DebugLog.e(TAG, ex.toString()); ex.printStackTrace(); self.eventEmitter.error(ex.toString(), ex, "1001"); @@ -684,7 +695,7 @@ private void initializePlayer() { } } catch (Exception ex) { self.playerNeedsSource = true; - DebugLog.e(TAG, "Failed to initialize Player!"); + DebugLog.e(TAG, "Failed to initialize Player! 2"); DebugLog.e(TAG, ex.toString()); ex.printStackTrace(); eventEmitter.error(ex.toString(), ex, "1001"); diff --git a/ios/Video/NowPlayingInfoCenterManager.swift b/ios/Video/NowPlayingInfoCenterManager.swift index 2307e10b02..6c416c72a1 100644 --- a/ios/Video/NowPlayingInfoCenterManager.swift +++ b/ios/Video/NowPlayingInfoCenterManager.swift @@ -61,11 +61,11 @@ class NowPlayingInfoCenterManager { return } - if let observer = observers[players.hashValue] { + if let observer = observers[player.hashValue] { observer.invalidate() } - observers.removeValue(forKey: players.hashValue) + observers.removeValue(forKey: player.hashValue) players.remove(player) if currentPlayer == player { diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index bf58d0c1b8..724f33c412 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -1241,10 +1241,19 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // MARK: - Lifecycle override func removeFromSuperview() { + self._player?.replaceCurrentItem(with: nil) if let player = _player { player.pause() NowPlayingInfoCenterManager.shared.removePlayer(player: player) } + _playerItem = nil + _source = nil + _chapters = nil + _drm = nil + _textTracks = nil + _selectedTextTrackCriteria = nil + _selectedAudioTrackCriteria = nil + _presentingViewController = nil _player = nil _resouceLoaderDelegate = nil @@ -1252,6 +1261,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH #if USE_GOOGLE_IMA _imaAdsManager.releaseAds() + _imaAdsManager = nil #endif self.removePlayerLayer() From 8693dbc8fae8810ea8def3de91364d9f73a3a729 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet <62574056+freeboub@users.noreply.github.com> Date: Thu, 6 Jun 2024 22:46:52 +0200 Subject: [PATCH 19/30] Fix/ensure view drop stop playback startup (#3875) * fix: ensure player doesn't start when view is unmounted --- .../exoplayer/ReactExoplayerView.java | 17 ++++++++++++++--- ios/Video/NowPlayingInfoCenterManager.swift | 4 ++-- ios/Video/RCTVideo.swift | 10 ++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index a417489125..6b54a9a4c4 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -258,6 +258,7 @@ public class ReactExoplayerView extends FrameLayout implements private long lastBufferDuration = -1; private long lastDuration = -1; + private boolean viewHasDropped = false; private void updateProgress() { if (player != null) { if (playerControlView != null && isPlayingAd() && controls) { @@ -375,6 +376,8 @@ protected void onDetachedFromWindow() { public void cleanUpResources() { stopPlayback(); themedReactContext.removeLifecycleEventListener(this); + releasePlayer(); + viewHasDropped = true; } //BandwidthMeter.EventListener implementation @@ -647,6 +650,9 @@ private void initializePlayer() { Activity activity = themedReactContext.getCurrentActivity(); // This ensures all props have been settled, to avoid async racing conditions. mainRunnable = () -> { + if (viewHasDropped) { + return; + } try { if (player == null) { // Initialize core configuration and listeners @@ -658,7 +664,9 @@ private void initializePlayer() { ExecutorService es = Executors.newSingleThreadExecutor(); es.execute(() -> { // DRM initialization must run on a different thread - + if (viewHasDropped) { + return; + } if (activity == null) { DebugLog.e(TAG, "Failed to initialize Player!, null activity"); eventEmitter.error("Failed to initialize Player!", new Exception("Current Activity is null!"), "1001"); @@ -667,12 +675,15 @@ private void initializePlayer() { // Initialize handler to run on the main thread activity.runOnUiThread(() -> { + if (viewHasDropped) { + return; + } try { // Source initialization must run on the main thread initializePlayerSource(); } catch (Exception ex) { self.playerNeedsSource = true; - DebugLog.e(TAG, "Failed to initialize Player!"); + DebugLog.e(TAG, "Failed to initialize Player! 1"); DebugLog.e(TAG, ex.toString()); ex.printStackTrace(); self.eventEmitter.error(ex.toString(), ex, "1001"); @@ -684,7 +695,7 @@ private void initializePlayer() { } } catch (Exception ex) { self.playerNeedsSource = true; - DebugLog.e(TAG, "Failed to initialize Player!"); + DebugLog.e(TAG, "Failed to initialize Player! 2"); DebugLog.e(TAG, ex.toString()); ex.printStackTrace(); eventEmitter.error(ex.toString(), ex, "1001"); diff --git a/ios/Video/NowPlayingInfoCenterManager.swift b/ios/Video/NowPlayingInfoCenterManager.swift index 2307e10b02..6c416c72a1 100644 --- a/ios/Video/NowPlayingInfoCenterManager.swift +++ b/ios/Video/NowPlayingInfoCenterManager.swift @@ -61,11 +61,11 @@ class NowPlayingInfoCenterManager { return } - if let observer = observers[players.hashValue] { + if let observer = observers[player.hashValue] { observer.invalidate() } - observers.removeValue(forKey: players.hashValue) + observers.removeValue(forKey: player.hashValue) players.remove(player) if currentPlayer == player { diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index bf58d0c1b8..724f33c412 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -1241,10 +1241,19 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // MARK: - Lifecycle override func removeFromSuperview() { + self._player?.replaceCurrentItem(with: nil) if let player = _player { player.pause() NowPlayingInfoCenterManager.shared.removePlayer(player: player) } + _playerItem = nil + _source = nil + _chapters = nil + _drm = nil + _textTracks = nil + _selectedTextTrackCriteria = nil + _selectedAudioTrackCriteria = nil + _presentingViewController = nil _player = nil _resouceLoaderDelegate = nil @@ -1252,6 +1261,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH #if USE_GOOGLE_IMA _imaAdsManager.releaseAds() + _imaAdsManager = nil #endif self.removePlayerLayer() From 80ec434590648f1060144c48c42be295f543725d Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Fri, 7 Jun 2024 15:14:57 +0200 Subject: [PATCH 20/30] chore: revert change --- .../exoplayer/ReactExoplayerView.java | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index e422b43d3c..e983292748 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -867,25 +867,6 @@ private void finishPlayerInitialization() { initializePlayerControl(); setControls(controls); applyModifiers(); - refreshMetadata(); - } - - /** - * refresh custom Metadata from notification bar - */ - private void refreshMetadata() { - // refresh custom Metadata - MediaMetadata newCustomMetadata = ConfigurationUtils.buildCustomMetadata(source.getMetadata()); - // Apply custom metadata is possible - if (player != null && !Util.areEqual(newCustomMetadata, customMetadata)) { - customMetadata = newCustomMetadata; - MediaItem currentMediaItem = player.getCurrentMediaItem(); - if (currentMediaItem != null && customMetadata != null) { - MediaItem newMediaItem = currentMediaItem.buildUpon().setMediaMetadata(customMetadata).build(); - // This will cause video blink/reload but won't louse progress - player.setMediaItem(newMediaItem, false); - } - } } private void setupPlaybackService() { From a64366ebe33b26e4a38892f8429729121914cefa Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Fri, 7 Jun 2024 15:18:39 +0200 Subject: [PATCH 21/30] chore: add warning in case of invalid Surface configuration --- src/Video.tsx | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Video.tsx b/src/Video.tsx index 2a25286ad1..a73d0e6c7f 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -175,17 +175,30 @@ const Video = forwardRef( const hasValidDrmProp = _drm === undefined || Object.keys(_drm).length === 0; - const viewType = + const shallForceViewType = hasValidDrmProp && - (source.viewType === ViewType.TEXTURE || useTextureView) - ? ViewType.SURFACE // check if we should force the type to Surface due to DRM - : source.viewType - ? source.viewType // else use ViewType from source - : useSecureView // else infer view type from useSecureView and useTextureView - ? ViewType.SURFACE_SECURE - : useTextureView - ? ViewType.TEXTURE - : ViewType.SURFACE; + (source.viewType === ViewType.TEXTURE || useTextureView); + + if (shallForceViewType) { + console.warn( + 'cannot use DRM on texture view. please set useTextureView={false}', + ); + } + if (useSecureView && useTextureView) { + console.warn( + 'cannot use SecureView on texture view. please set useTextureView={false}', + ); + } + + const viewType = shallForceViewType + ? ViewType.SURFACE // check if we should force the type to Surface due to DRM + : source.viewType + ? source.viewType // else use ViewType from source + : useSecureView // else infer view type from useSecureView and useTextureView + ? ViewType.SURFACE_SECURE + : useTextureView + ? ViewType.TEXTURE + : ViewType.SURFACE; return { uri, From f7cc6cdf2b897085c75d340fd45bbd6bbeed0976 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sun, 9 Jun 2024 21:54:55 +0200 Subject: [PATCH 22/30] chore: code clean --- .../com/brentvatne/exoplayer/ExoPlayerView.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index ae9effd8b4..a5a1587df1 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -1,5 +1,6 @@ package com.brentvatne.exoplayer; +import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.NonNull; @@ -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; @@ -32,6 +32,7 @@ import java.util.List; +@SuppressLint("ViewConstructor") public final class ExoPlayerView extends FrameLayout implements AdViewProvider { private final static String TAG = "ExoPlayerView"; @@ -48,15 +49,7 @@ public final class ExoPlayerView extends FrameLayout implements AdViewProvider { private boolean hideShutterView = false; public ExoPlayerView(Context context, @ViewType.ViewType int viewType) { - this(context, null, viewType); - } - - public ExoPlayerView(Context context, AttributeSet attrs, @ViewType.ViewType int viewType) { - this(context, attrs, 0, viewType); - } - - public ExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr, @ViewType.ViewType int viewType) { - super(context, attrs, defStyleAttr); + super(context, null, 0); this.context = context; From e0a1815e2f9e5f26156ee09b56010df0bbe6b840 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sun, 9 Jun 2024 22:04:45 +0200 Subject: [PATCH 23/30] fix: simplify surface management --- .../src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java | 4 ++-- .../java/com/brentvatne/exoplayer/ReactExoplayerView.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index a5a1587df1..07637e20f4 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -48,7 +48,7 @@ public final class ExoPlayerView extends FrameLayout implements AdViewProvider { private boolean hideShutterView = false; - public ExoPlayerView(Context context, @ViewType.ViewType int viewType) { + public ExoPlayerView(Context context) { super(context, null, 0); this.context = context; @@ -75,7 +75,7 @@ public ExoPlayerView(Context context, @ViewType.ViewType int viewType) { subtitleLayout.setUserDefaultStyle(); subtitleLayout.setUserDefaultTextSize(); - updateSurfaceView(viewType); + updateSurfaceView(ViewType.VIEW_TYPE_SURFACE); adOverlayFrameLayout = new FrameLayout(context); diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index e983292748..12841493cb 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -332,7 +332,7 @@ private void createViews() { LayoutParams layoutParams = new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - exoPlayerView = new ExoPlayerView(getContext(), source.getViewType()); + exoPlayerView = new ExoPlayerView(getContext()); exoPlayerView.setLayoutParams(layoutParams); addView(exoPlayerView, 0, layoutParams); From 777201f914394a304628e10d7c08e702f3a077bf Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sun, 9 Jun 2024 22:13:47 +0200 Subject: [PATCH 24/30] chore: restore previous code --- .../java/com/brentvatne/exoplayer/ReactExoplayerView.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 12841493cb..9d344bab6c 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -797,7 +797,11 @@ private void initializePlayerSource() { } /// 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!"); + return; + } // init source to manage ads and external text tracks ArrayList mediaSourceList = buildTextSources(); MediaSource videoSource = buildMediaSource(source.getUri(), source.getExtension(), drmSessionManager, source.getCropStartMs(), source.getCropEndMs()); From 3d03f29f32cd5f79f4624c712b37f1410c1c8841 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Mon, 10 Jun 2024 22:52:01 +0200 Subject: [PATCH 25/30] chore: fix typo --- docs/pages/component/props.mdx | 2 +- docs/pages/other/debug.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/pages/component/props.mdx b/docs/pages/component/props.mdx index 8e912d438d..06870b25c8 100644 --- a/docs/pages/component/props.mdx +++ b/docs/pages/component/props.mdx @@ -799,7 +799,7 @@ source={{ Allow to explicitly specify view type. This flag replace `useSecureView` and `useTextureView` fields. There are 3 available values: -- 'textureView': The video is rendered in a texture view. it allows mapping the view on a texture (usefull for 3D). +- 'textureView': The video is rendered in a texture view. it allows mapping the view on a texture (useful for 3D). DRM playback is not supported on textureView, if drm prop is provided, the suface will be transformed to a SurfaceView. - 'surfaceView' (default): The video is rendered in a surface. take less resources to be rendered. - 'secureView': The video is rendered in a surface which disallow taking screenshot of the video diff --git a/docs/pages/other/debug.md b/docs/pages/other/debug.md index cd8605ae78..0cbaebf245 100644 --- a/docs/pages/other/debug.md +++ b/docs/pages/other/debug.md @@ -1,6 +1,6 @@ # Debugging -This page describe usefull tips for debugging and investigating issue in the package or in your application. +This page describe useful tips for debugging and investigating issue in the package or in your application. ## Using the sample app This repository contains multiple a sample implementation in example folder. From f9fa8fd88c457b090fb8175113ae21b181a01148 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Thu, 27 Jun 2024 12:08:35 +0200 Subject: [PATCH 26/30] chore: code cleanup --- docs/pages/component/props.mdx | 11 ----------- src/Video.tsx | 2 +- src/specs/VideoNativeComponent.ts | 1 - src/types/video.ts | 1 - 4 files changed, 1 insertion(+), 14 deletions(-) diff --git a/docs/pages/component/props.mdx b/docs/pages/component/props.mdx index 2a7c145890..d132941e9c 100644 --- a/docs/pages/component/props.mdx +++ b/docs/pages/component/props.mdx @@ -792,17 +792,6 @@ source={{ } }} ``` -#### viewType - - - -Allow to explicitly specify view type. -This flag replace `useSecureView` and `useTextureView` fields. -There are 3 available values: -- 'textureView': The video is rendered in a texture view. it allows mapping the view on a texture (useful for 3D). -DRM playback is not supported on textureView, if drm prop is provided, the suface will be transformed to a SurfaceView. -- 'surfaceView' (default): The video is rendered in a surface. take less resources to be rendered. -- 'secureView': The video is rendered in a surface which disallow taking screenshot of the video #### `textTracksAllowChunklessPreparation` diff --git a/src/Video.tsx b/src/Video.tsx index b25c8dc8d1..2de6dcda2b 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -193,7 +193,7 @@ const Video = forwardRef( textTracksAllowChunklessPreparation: resolvedSource.textTracksAllowChunklessPreparation, }; - }, [drm, source, useSecureView, useTextureView]); + }, [drm, source]); const _selectedTextTrack = useMemo(() => { if (!selectedTextTrack) { diff --git a/src/specs/VideoNativeComponent.ts b/src/specs/VideoNativeComponent.ts index fd3c4ddd84..9aa561c9a7 100644 --- a/src/specs/VideoNativeComponent.ts +++ b/src/specs/VideoNativeComponent.ts @@ -39,7 +39,6 @@ export type VideoSrc = Readonly<{ cropStart?: Float; cropEnd?: Float; metadata?: VideoMetadata; - viewType?: Int32; // Android drm?: Drm; textTracksAllowChunklessPreparation?: boolean; // android }>; diff --git a/src/types/video.ts b/src/types/video.ts index 8f98f663c6..15f4423e3f 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -25,7 +25,6 @@ export type ReactVideoSourceProperties = { cropEnd?: number; metadata?: VideoMetadata; drm?: Drm; - viewType?: ViewType; // Android textTracksAllowChunklessPreparation?: boolean; }; From 62e9e9dfc962786573519dd2b69fcf41250b6dd8 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Thu, 27 Jun 2024 12:49:42 +0200 Subject: [PATCH 27/30] feat(android): add multiDrm flag support --- .../com/brentvatne/common/api/DRMProps.kt | 9 +++++++ .../exoplayer/ReactExoplayerView.java | 24 +++++++++---------- docs/pages/component/drm.mdx | 7 ++++++ src/Video.tsx | 1 + src/specs/VideoNativeComponent.ts | 1 + src/types/video.ts | 1 + 6 files changed, 30 insertions(+), 13 deletions(-) diff --git a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt index 29060a7ff4..5c093dfdac 100644 --- a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt +++ b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt @@ -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 @@ -30,11 +31,17 @@ class DRMProps { */ var drmLicenseHeader: Array = emptyArray() + /** + * + */ + 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 } @@ -44,6 +51,7 @@ class DRMProps { 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 @@ -53,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) { diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 2632911a82..a4a495b575 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -791,9 +791,7 @@ private DrmSessionManager initializePlayerDrm() { if (uuid != null) { try { DebugLog.w(TAG, "drm buildDrmSessionManager"); - drmSessionManager = buildDrmSessionManager(uuid, - drmProps.getDrmLicenseServer(), - drmProps.getDrmLicenseHeader()); + 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 @@ -951,21 +949,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) { @@ -975,7 +973,7 @@ 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 @@ -983,7 +981,7 @@ private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, S } 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.error(ex.toString(), ex, "3006"); diff --git a/docs/pages/component/drm.mdx b/docs/pages/component/drm.mdx index ae0ceec379..6e7fa0aaf3 100644 --- a/docs/pages/component/drm.mdx +++ b/docs/pages/component/drm.mdx @@ -119,6 +119,13 @@ Default: false The URL pointing to the licenseServer that will provide the authorization to play the protected stream. +### `multiDrm` + +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` diff --git a/src/Video.tsx b/src/Video.tsx index 2de6dcda2b..1fd8e73ba2 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -174,6 +174,7 @@ const Video = forwardRef( certificateUrl: selectedDrm.certificateUrl, base64Certificate: selectedDrm.base64Certificate, useExternalGetLicense: !!selectedDrm.getLicense, + multiDrm: selectedDrm.multiDrm, }; return { diff --git a/src/specs/VideoNativeComponent.ts b/src/specs/VideoNativeComponent.ts index 9aa561c9a7..b55dabd5cf 100644 --- a/src/specs/VideoNativeComponent.ts +++ b/src/specs/VideoNativeComponent.ts @@ -58,6 +58,7 @@ type Drm = Readonly<{ certificateUrl?: string; // ios base64Certificate?: boolean; // ios default: false useExternalGetLicense?: boolean; // ios + multiDrm?: boolean; // android }>; type TextTracks = ReadonlyArray< diff --git a/src/types/video.ts b/src/types/video.ts index 15f4423e3f..efce783e54 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -61,6 +61,7 @@ export type Drm = Readonly<{ contentId?: string; // ios certificateUrl?: string; // ios base64Certificate?: boolean; // ios default: false + multiDrm: boolean; // android /* eslint-disable @typescript-eslint/no-unused-vars */ getLicense?: ( spcBase64: string, From 79215412a4729ed16b24d4ae03ddda5afbfed0d7 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Thu, 27 Jun 2024 12:58:12 +0200 Subject: [PATCH 28/30] docs: update docs --- android/src/main/java/com/brentvatne/common/api/DRMProps.kt | 2 +- docs/pages/component/props.mdx | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt index 5c093dfdac..0c495e1177 100644 --- a/android/src/main/java/com/brentvatne/common/api/DRMProps.kt +++ b/android/src/main/java/com/brentvatne/common/api/DRMProps.kt @@ -32,7 +32,7 @@ class DRMProps { var drmLicenseHeader: Array = emptyArray() /** - * + * Flag to enable key rotation support */ var multiDrm: Boolean = false diff --git a/docs/pages/component/props.mdx b/docs/pages/component/props.mdx index d132941e9c..ef99920273 100644 --- a/docs/pages/component/props.mdx +++ b/docs/pages/component/props.mdx @@ -213,6 +213,12 @@ Determines if the player needs to throw an error when connection is lost or not > [!WARNING] > deprecated, use source.drm instead + + +To setup DRM please follow [this guide](/component/drm) + +> ⚠️ DRM is not supported on visionOS yet + ### `filter` From e07659045b83f7568b9146d8d0e763bb2673150f Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Fri, 5 Jul 2024 23:17:32 +0200 Subject: [PATCH 29/30] chore: fix ios build --- ios/Video/RCTVideo.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index ad72aca800..1bb64ac903 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -1253,7 +1253,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _playerItem = nil _source = nil _chapters = nil - _drm = nil _textTracks = nil _selectedTextTrackCriteria = nil _selectedAudioTrackCriteria = nil From 731d1e77b18a388c12823eab711b117a6fce5e1e Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Fri, 5 Jul 2024 23:21:27 +0200 Subject: [PATCH 30/30] chore: fix deprecated declaration --- src/types/video.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/types/video.ts b/src/types/video.ts index efce783e54..463f30b518 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -213,7 +213,8 @@ export type ControlsStyles = { export interface ReactVideoProps extends ReactVideoEvents, ViewProps { source?: ReactVideoSource; - drm?: Drm; // deprecated + /** @deprecated */ + drm?: Drm; style?: StyleProp; adTagUrl?: string; audioOutput?: AudioOutput; // Mobile @@ -260,8 +261,10 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps { textTracks?: TextTracks; testID?: string; viewType?: ViewType; - useTextureView?: boolean; // Android // deprecated - useSecureView?: boolean; // Android // deprecated + /** @deprecated */ + useTextureView?: boolean; // Android + /** @deprecated */ + useSecureView?: boolean; // Android volume?: number; localSourceEncryptionKeyScheme?: string; debug?: DebugConfig;