From d3fc4fdbce981e7aa680a4f3d16251b1957798c8 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sun, 5 May 2024 21:06:29 +0200 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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 0eba6daaee6c5202bb35f824414e85166a50f1bb Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Tue, 4 Jun 2024 18:13:51 +0200 Subject: [PATCH 05/11] 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 06/11] 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 b9f8bf8e024394441bbace9933911022ee76222e Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Thu, 18 Jul 2024 21:30:50 +0200 Subject: [PATCH 07/11] chore: fix app boot due to missing registered events --- .../main/java/com/brentvatne/common/react/VideoEventEmitter.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt index 8f29779a36..baf2c34edd 100644 --- a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt +++ b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt @@ -48,7 +48,8 @@ enum class EventTypes(val eventName: String) { fun toMap() = mutableMapOf().apply { EventTypes.values().toList().forEach { eventType -> - put("top${eventType.eventName.removePrefix("on")}", mapOf("registrationName" to eventType.eventName)) + put(eventType.eventName, MapBuilder.of("registrationName", eventType.eventName)); + put("top${eventType.eventName.removePrefix("on")}", MapBuilder.of("registrationName", eventType.eventName)) } } } From 8e209960496f08fbd0e81e20e3d11c64a4606dfc Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Thu, 18 Jul 2024 21:34:27 +0200 Subject: [PATCH 08/11] chore: fix linter --- .../main/java/com/brentvatne/common/react/VideoEventEmitter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt index baf2c34edd..c0cc3925ae 100644 --- a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt +++ b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt @@ -48,7 +48,7 @@ enum class EventTypes(val eventName: String) { fun toMap() = mutableMapOf().apply { EventTypes.values().toList().forEach { eventType -> - put(eventType.eventName, MapBuilder.of("registrationName", eventType.eventName)); + put(eventType.eventName, MapBuilder.of("registrationName", eventType.eventName)) put("top${eventType.eventName.removePrefix("on")}", MapBuilder.of("registrationName", eventType.eventName)) } } From 9b091a5906070eaa5e0633d28e4ccf2666953bac Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Thu, 18 Jul 2024 22:19:54 +0200 Subject: [PATCH 09/11] chore: fix build --- .../main/java/com/brentvatne/common/react/VideoEventEmitter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt index c0cc3925ae..6e9476d0e2 100644 --- a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt +++ b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt @@ -7,6 +7,7 @@ import com.brentvatne.exoplayer.ReactExoplayerView import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap +import com.facebook.react.common.MapBuilder import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.events.Event From 8250fba49fe1e4008c9d7a4414b058b573ba2156 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Fri, 19 Jul 2024 14:30:35 +0200 Subject: [PATCH 10/11] fix: handle code review feedbacks --- .../java/com/brentvatne/common/react/VideoEventEmitter.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt index 6e9476d0e2..78ef7588b9 100644 --- a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt +++ b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt @@ -7,7 +7,6 @@ import com.brentvatne.exoplayer.ReactExoplayerView import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap -import com.facebook.react.common.MapBuilder import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.events.Event @@ -49,8 +48,7 @@ enum class EventTypes(val eventName: String) { fun toMap() = mutableMapOf().apply { EventTypes.values().toList().forEach { eventType -> - put(eventType.eventName, MapBuilder.of("registrationName", eventType.eventName)) - put("top${eventType.eventName.removePrefix("on")}", MapBuilder.of("registrationName", eventType.eventName)) + put(eventType.eventName, hashMapOf("registrationName" to eventType.eventName)) } } } @@ -282,7 +280,7 @@ class VideoEventEmitter { private class EventBuilder(private val surfaceId: Int, private val viewId: Int, private val dispatcher: EventDispatcher) { fun dispatch(event: EventTypes, paramsSetter: (WritableMap.() -> Unit)? = null) = dispatcher.dispatchEvent(object : Event>(surfaceId, viewId) { - override fun getEventName() = "top${event.eventName.removePrefix("on")}" + override fun getEventName() = event.eventName override fun getEventData() = Arguments.createMap().apply(paramsSetter ?: {}) }) } From 442ba52b8665b4ce5893a510ed6a99a45c72e3c9 Mon Sep 17 00:00:00 2001 From: Olivier Bouillet Date: Sat, 20 Jul 2024 14:12:05 +0200 Subject: [PATCH 11/11] chore: revert previous change to handle only necessary fix --- .../java/com/brentvatne/common/react/VideoEventEmitter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt index 78ef7588b9..b227bfc1c9 100644 --- a/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt +++ b/android/src/main/java/com/brentvatne/common/react/VideoEventEmitter.kt @@ -48,7 +48,7 @@ enum class EventTypes(val eventName: String) { fun toMap() = mutableMapOf().apply { EventTypes.values().toList().forEach { eventType -> - put(eventType.eventName, hashMapOf("registrationName" to eventType.eventName)) + put("top${eventType.eventName.removePrefix("on")}", hashMapOf("registrationName" to eventType.eventName)) } } } @@ -280,7 +280,7 @@ class VideoEventEmitter { private class EventBuilder(private val surfaceId: Int, private val viewId: Int, private val dispatcher: EventDispatcher) { fun dispatch(event: EventTypes, paramsSetter: (WritableMap.() -> Unit)? = null) = dispatcher.dispatchEvent(object : Event>(surfaceId, viewId) { - override fun getEventName() = event.eventName + override fun getEventName() = "top${event.eventName.removePrefix("on")}" override fun getEventData() = Arguments.createMap().apply(paramsSetter ?: {}) }) }