diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 6fa8e0b3fa..11af4a629c 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -6,11 +6,9 @@ assignees: [] 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! + 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 + - type: input id: version attributes: label: Version 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/common/api/SideLoadedTextTrack.kt b/android/src/main/java/com/brentvatne/common/api/SideLoadedTextTrack.kt new file mode 100644 index 0000000000..59408c78c4 --- /dev/null +++ b/android/src/main/java/com/brentvatne/common/api/SideLoadedTextTrack.kt @@ -0,0 +1,34 @@ +package com.brentvatne.common.api + +import android.net.Uri +import com.brentvatne.common.toolbox.ReactBridgeUtils +import com.facebook.react.bridge.ReadableMap + +/** + * Class representing a sideLoaded text track from application + * Do you use player import in this class + */ +class SideLoadedTextTrack { + var language: String? = null + var title: String? = null + var uri: Uri = Uri.EMPTY + var type: String? = null + companion object { + val SIDELOAD_TEXT_TRACK_LANGUAGE = "language" + val SIDELOAD_TEXT_TRACK_TITLE = "title" + val SIDELOAD_TEXT_TRACK_URI = "uri" + val SIDELOAD_TEXT_TRACK_TYPE = "type" + + fun parse(src: ReadableMap?): SideLoadedTextTrack { + val sideLoadedTextTrack = SideLoadedTextTrack() + if (src == null) { + return sideLoadedTextTrack + } + sideLoadedTextTrack.language = ReactBridgeUtils.safeGetString(src, SIDELOAD_TEXT_TRACK_LANGUAGE) + sideLoadedTextTrack.title = ReactBridgeUtils.safeGetString(src, SIDELOAD_TEXT_TRACK_TITLE, "") + sideLoadedTextTrack.uri = Uri.parse(ReactBridgeUtils.safeGetString(src, SIDELOAD_TEXT_TRACK_URI, "")) + sideLoadedTextTrack.type = ReactBridgeUtils.safeGetString(src, SIDELOAD_TEXT_TRACK_TYPE, "") + return sideLoadedTextTrack + } + } +} diff --git a/android/src/main/java/com/brentvatne/common/api/SideLoadedTextTrackList.kt b/android/src/main/java/com/brentvatne/common/api/SideLoadedTextTrackList.kt new file mode 100644 index 0000000000..e3d519c408 --- /dev/null +++ b/android/src/main/java/com/brentvatne/common/api/SideLoadedTextTrackList.kt @@ -0,0 +1,27 @@ +package com.brentvatne.common.api + +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap + +/** + * Class representing a list of sideLoaded text track from application + * Do you use player import in this class + */ + +class SideLoadedTextTrackList { + var tracks = ArrayList() + + companion object { + fun parse(src: ReadableArray?): SideLoadedTextTrackList? { + if (src == null) { + return null + } + var sideLoadedTextTrackList = SideLoadedTextTrackList() + for (i in 0 until src.size()) { + val textTrack: ReadableMap = src.getMap(i) + sideLoadedTextTrackList.tracks.add(SideLoadedTextTrack.parse(textTrack)) + } + return sideLoadedTextTrackList + } + } +} diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 5ddc2bd8d4..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,7 +104,10 @@ 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; import com.brentvatne.common.api.SubtitleStyle; import com.brentvatne.common.api.TimedMetadata; import com.brentvatne.common.api.Track; @@ -117,8 +119,6 @@ import com.brentvatne.receiver.AudioBecomingNoisyReceiver; import com.brentvatne.receiver.BecomingNoisyListener; import com.facebook.react.bridge.LifecycleEventListener; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.uimanager.ThemedReactContext; import com.google.ads.interactivemedia.v3.api.AdError; @@ -220,10 +220,10 @@ public class ReactExoplayerView extends FrameLayout implements private String videoTrackValue; private String textTrackType; private String textTrackValue; - private ReadableArray textTracks; + 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); } } @@ -588,13 +592,13 @@ private void initializePlayer() { DrmSessionManager drmSessionManager = initializePlayerDrm(self); if (drmSessionManager == null && self.drmUUID != null) { // Failed to intialize DRM session manager - cannot continue - DebugLog.e("ExoPlayer Exception", "Failed to initialize DRM Session Manager Framework!"); + 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; } if (activity == null) { - DebugLog.e("ExoPlayer Exception", "Failed to initialize Player!"); + DebugLog.e(TAG, "Failed to initialize Player!"); eventEmitter.error("Failed to initialize Player!", new Exception("Current Activity is null!"), "1001"); return; } @@ -606,8 +610,8 @@ private void initializePlayer() { initializePlayerSource(self, drmSessionManager); } catch (Exception ex) { self.playerNeedsSource = true; - DebugLog.e("ExoPlayer Exception", "Failed to initialize Player!"); - DebugLog.e("ExoPlayer Exception", ex.toString()); + DebugLog.e(TAG, "Failed to initialize Player!"); + DebugLog.e(TAG, ex.toString()); self.eventEmitter.error(ex.toString(), ex, "1001"); } }); @@ -617,8 +621,8 @@ private void initializePlayer() { } } catch (Exception ex) { self.playerNeedsSource = true; - DebugLog.e("ExoPlayer Exception", "Failed to initialize Player!"); - DebugLog.e("ExoPlayer Exception", ex.toString()); + DebugLog.e(TAG, "Failed to initialize Player!"); + DebugLog.e(TAG, ex.toString()); eventEmitter.error(ex.toString(), ex, "1001"); } }; @@ -734,7 +738,7 @@ private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager d wait(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); - DebugLog.e("ExoPlayer Exception", ex.toString()); + DebugLog.e(TAG, ex.toString()); } } @@ -995,17 +999,12 @@ private ArrayList buildTextSources() { return textSources; } - for (int i = 0; i < textTracks.size(); ++i) { - ReadableMap textTrack = textTracks.getMap(i); - String language = textTrack.getString("language"); - String title = textTrack.hasKey("title") - ? textTrack.getString("title") : language + " " + i; - Uri uri = Uri.parse(textTrack.getString("uri")); - MediaSource textSource = buildTextSource(title, uri, textTrack.getString("type"), - language); - if (textSource != null) { - textSources.add(textSource); - } + for (SideLoadedTextTrack track : textTracks.getTracks()) { + MediaSource textSource = buildTextSource(track.getTitle(), + track.getUri(), + track.getType(), + track.getLanguage()); + textSources.add(textSource); } return textSources; } @@ -1718,7 +1717,7 @@ public void setRawSrc(final Uri uri, final String extension) { } } - public void setTextTracks(ReadableArray textTracks) { + public void setTextTracks(SideLoadedTextTrackList textTracks) { this.textTracks = textTracks; reloadSource(); } @@ -2082,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 6b5b0d3070..997a137e93 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -10,10 +10,11 @@ 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; import com.brentvatne.common.react.VideoEventEmitter; import com.brentvatne.common.toolbox.DebugLog; @@ -34,6 +35,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager { + private static final String TAG = "ExoViewManager"; private static final String REACT_CLASS = "RCTVideo"; private static final String PROP_SRC = "src"; private static final String PROP_SRC_URI = "uri"; @@ -71,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/Video.tsx b/src/Video.tsx index f85524764a..7cdbc211cd 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -549,7 +549,7 @@ const Video = forwardRef( onVideoEnd={onEnd} onVideoBuffer={onBuffer ? onVideoBuffer : undefined} onVideoPlaybackStateChanged={ - onPlaybackRateChange ? onVideoPlaybackStateChanged : undefined + onPlaybackStateChanged ? onVideoPlaybackStateChanged : undefined } onVideoBandwidthUpdate={ onBandwidthUpdate ? _onBandwidthUpdate : undefined diff --git a/src/specs/VideoNativeComponent.ts b/src/specs/VideoNativeComponent.ts index fb299be26f..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; @@ -102,6 +104,7 @@ type BufferConfig = Readonly<{ backBufferDurationMs?: Float; // Android minBackBufferMemoryReservePercent?: Float; minBufferMemoryReservePercent?: Float; + cacheSizeMB?: Float; }>; type SubtitleStyle = Readonly<{ @@ -316,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;