diff --git a/android/src/main/java/com/brentvatne/common/api/BufferingStrategy.kt b/android/src/main/java/com/brentvatne/common/api/BufferingStrategy.kt new file mode 100644 index 0000000000..9c48474cd0 --- /dev/null +++ b/android/src/main/java/com/brentvatne/common/api/BufferingStrategy.kt @@ -0,0 +1,47 @@ +package com.brentvatne.common.api + +import com.brentvatne.common.toolbox.DebugLog + +/** + * Define how exoplayer with load data and parsing helper + */ + +class BufferingStrategy { + + /** + * Define how exoplayer with load data + */ + enum class BufferingStrategyEnum { + /** + * default exoplayer strategy + */ + Default, + + /** + * never load more than needed + */ + DisableBuffering, + + /** + * use default strategy but pause loading when available memory is low + */ + DependingOnMemory + } + + companion object { + private const val TAG = "BufferingStrategy" + + /** + * companion function to transform input string to enum + */ + fun parse(src: String?): BufferingStrategyEnum { + if (src == null) return BufferingStrategyEnum.Default + return try { + BufferingStrategyEnum.valueOf(src) + } catch (e: Exception) { + DebugLog.e(TAG, "cannot parse buffering strategy " + src) + BufferingStrategyEnum.Default + } + } + } +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 6dccaee0dc..9eb60e2275 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -32,7 +32,6 @@ import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; @@ -105,6 +104,7 @@ import androidx.media3.ui.LegacyPlayerControlView; import com.brentvatne.common.api.BufferConfig; +import com.brentvatne.common.api.BufferingStrategy; import com.brentvatne.common.api.ResizeMode; import com.brentvatne.common.api.SideLoadedTextTrack; import com.brentvatne.common.api.SideLoadedTextTrackList; @@ -223,7 +223,7 @@ public class ReactExoplayerView extends FrameLayout implements private SideLoadedTextTrackList textTracks; private boolean disableFocus; private boolean focusable = true; - private boolean disableBuffering; + private BufferingStrategy.BufferingStrategyEnum bufferingStrategy; private long contentStartTime = -1L; private boolean disableDisconnectError; private boolean preventsDisplaySleepDuringVideoPlayback = true; @@ -541,30 +541,34 @@ public RNVLoadControl(DefaultAllocator allocator, BufferConfig config) { @Override public boolean shouldContinueLoading(long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) { - if (ReactExoplayerView.this.disableBuffering) { - return false; - } - int loadedBytes = getAllocator().getTotalBytesAllocated(); - boolean isHeapReached = availableHeapInBytes > 0 && loadedBytes >= availableHeapInBytes; - if (isHeapReached) { - return false; - } - 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) { - // We don't have enough memory in reserve so we stop buffering to allow other components to use it instead - return false; - } - if (runtime.freeMemory() == 0) { - DebugLog.w("ExoPlayer Warning", "Free memory reached 0, forcing garbage collection"); - runtime.gc(); + if (bufferingStrategy == BufferingStrategy.BufferingStrategyEnum.DisableBuffering) { return false; + } else if (bufferingStrategy == BufferingStrategy.BufferingStrategyEnum.DependingOnMemory) { + // The goal of this algorithm is to pause video loading (increasing the buffer) + // when available memory on device become low. + int loadedBytes = getAllocator().getTotalBytesAllocated(); + boolean isHeapReached = availableHeapInBytes > 0 && loadedBytes >= availableHeapInBytes; + if (isHeapReached) { + return false; + } + 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) { + // We don't have enough memory in reserve so we stop buffering to allow other components to use it instead + return false; + } + if (runtime.freeMemory() == 0) { + DebugLog.w(TAG, "Free memory reached 0, forcing garbage collection"); + runtime.gc(); + return false; + } } + // "default" case or normal case for "DependingOnMemory" return super.shouldContinueLoading(playbackPositionUs, bufferedDurationUs, playbackSpeed); } } @@ -2077,8 +2081,8 @@ public void setShowNotificationControls(boolean showNotificationControls) { } } - public void setDisableBuffering(boolean disableBuffering) { - this.disableBuffering = disableBuffering; + public void setBufferingStrategy(BufferingStrategy.BufferingStrategyEnum _bufferingStrategy) { + bufferingStrategy = _bufferingStrategy; } public boolean getPreventsDisplaySleepDuringVideoPlayback() { diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 24e9a004c8..997a137e93 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -10,9 +10,9 @@ import androidx.media3.common.MediaMetadata; import androidx.media3.common.util.Util; import androidx.media3.datasource.RawResourceDataSource; -import androidx.media3.exoplayer.DefaultLoadControl; import com.brentvatne.common.api.BufferConfig; +import com.brentvatne.common.api.BufferingStrategy; import com.brentvatne.common.api.ResizeMode; import com.brentvatne.common.api.SideLoadedTextTrackList; import com.brentvatne.common.api.SubtitleStyle; @@ -73,7 +73,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager + +Configure buffering / data loading strategy. + + - **Default (default)**: use exoplayer default loading strategy + - **DisableBuffering**: never try to buffer more than needed. Be carefull using this value will stop playback. To be used with care. + - **DependingOnMemory**: use exoplayer default strategy, but stop buffering and starts gc if available memory is low | + ### `chapters` diff --git a/examples/basic/src/VideoPlayer.tsx b/examples/basic/src/VideoPlayer.tsx index d94baba5b6..663806f5d5 100644 --- a/examples/basic/src/VideoPlayer.tsx +++ b/examples/basic/src/VideoPlayer.tsx @@ -39,6 +39,7 @@ import Video, { OnSeekData, OnPlaybackStateChangedData, OnPlaybackRateChangeData, + BufferingStrategyType, } from 'react-native-video'; import ToggleControl from './ToggleControl'; import MultiValueControl, { @@ -934,6 +935,7 @@ class VideoPlayer extends Component { poster={this.state.poster} onPlaybackRateChange={this.onPlaybackRateChange} onPlaybackStateChanged={this.onPlaybackStateChanged} + bufferingStrategy={BufferingStrategyType.DEFAULT} /> ); diff --git a/src/specs/VideoNativeComponent.ts b/src/specs/VideoNativeComponent.ts index d5dc5079e5..1b383eaeb2 100644 --- a/src/specs/VideoNativeComponent.ts +++ b/src/specs/VideoNativeComponent.ts @@ -93,6 +93,8 @@ export type Seek = Readonly<{ tolerance?: Float; }>; +type BufferingStrategyType = WithDefault; + type BufferConfig = Readonly<{ minBufferMs?: Float; maxBufferMs?: Float; @@ -317,6 +319,7 @@ export interface VideoNativeProps extends ViewProps { subtitleStyle?: SubtitleStyle; // android useTextureView?: boolean; // Android useSecureView?: boolean; // Android + bufferingStrategy?: BufferingStrategyType; // Android onVideoLoad?: DirectEventHandler; onVideoLoadStart?: DirectEventHandler; onVideoAspectRatio?: DirectEventHandler; diff --git a/src/types/video.ts b/src/types/video.ts index f57e2b0ed3..e174bbbf75 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -68,6 +68,12 @@ export type Drm = Readonly<{ /* eslint-enable @typescript-eslint/no-unused-vars */ }>; +export enum BufferingStrategyType { + DEFAULT = 'Default', + DISABLE_BUFFERING = 'DisableBuffering', + DEPENDING_ON_MEMORY = 'DependingOnMemory', +} + export type BufferConfig = { minBufferMs?: number; maxBufferMs?: number; @@ -195,6 +201,7 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps { audioOutput?: AudioOutput; // Mobile automaticallyWaitsToMinimizeStalling?: boolean; // iOS bufferConfig?: BufferConfig; // Android + bufferingStrategy?: BufferingStrategyType; chapters?: Chapters[]; // iOS contentStartTime?: number; // Android controls?: boolean;