Skip to content

Commit

Permalink
VEX-7861: Port previous fixes to current code (#41)
Browse files Browse the repository at this point in the history
This PR ports previous fixes to the latest react-native-video

Jira: VEX-7861

Velocity PR: crunchyroll/velocity#2680

Previous improvements were not merged to the upstream and were lost when we upgraded the version, this PR ports the fixes to the current code.
Specifically, the fixes are, to disable backbuffer when free memory is too low, and restrict the maximum video quality based on the device's capabilities

Reviews
Major reviewer (domain expert): @jctorresM
Minor reviewer: @rogercrunchyroll
  • Loading branch information
gabriel-rivero authored Sep 29, 2022
1 parent ea14600 commit ddca85d
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 8 deletions.
12 changes: 12 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ bufferForPlaybackAfterRebufferMs | number | The default duration of media that m
maxHeapAllocationPercent | number | The percentage of available heap that the video can use to buffer, between 0 and 1
minBackBufferMemoryReservePercent | number | The percentage of available app memory at which during startup the back buffer will be disabled, between 0 and 1
minBufferMemoryReservePercent | number | The percentage of available app memory to keep in reserve that prevents buffer from using it, between 0 and 1
minAvailableMemoryToEnableBackBuffer | number | The amount of memory the device must have to allow the usage of back-buffer

This prop should only be set when you are setting the source, changing it after the media is loaded will cause it to be reloaded.

Expand Down Expand Up @@ -566,6 +567,17 @@ Controls the iOS silent switch behavior

Platforms: iOS

#### limitMaxResolution
Allows the player to ignore resolutions that are higher of the current device resolution
Default: false. Do not limit the max resolution

Example:
```
limitMaxResolution={true}
```

Platforms: Android

#### maxBitRate
Sets the desired limit, in bits per second, of network bandwidth consumption when multiple video streams are available for a playlist.

Expand Down
125 changes: 118 additions & 7 deletions android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
import android.content.Context;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.view.Display;
import android.view.View;
import android.view.Window;
import android.view.accessibility.CaptioningManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.util.DisplayMetrics;

import com.brentvatne.react.R;
import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
Expand Down Expand Up @@ -109,6 +112,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.lang.Integer;
import java.lang.reflect.Method;

@SuppressLint("ViewConstructor")
class ReactExoplayerView extends FrameLayout implements
Expand All @@ -135,7 +139,7 @@ class ReactExoplayerView extends FrameLayout implements

private final VideoEventEmitter eventEmitter;
private final ReactExoplayerConfig config;
private final DefaultBandwidthMeter bandwidthMeter;
private DefaultBandwidthMeter bandwidthMeter;
private PlayerControlView playerControlView;
private View playPauseControlContainer;
private Player.Listener eventListener;
Expand Down Expand Up @@ -164,6 +168,7 @@ class ReactExoplayerView extends FrameLayout implements
private boolean hasDrmFailed = false;
private boolean isUsingContentResolution = false;
private boolean selectTrackWhenReady = false;
private boolean limitMaxResolution = false;

private int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
Expand All @@ -172,6 +177,7 @@ class ReactExoplayerView extends FrameLayout implements
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 double enableBackBufferAvailableMemory = -1f;
private Handler mainHandler;

// Props from React
Expand Down Expand Up @@ -337,7 +343,7 @@ public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
* Toggling the visibility of the player control view
*/
private void togglePlayerControlVisibility() {
if(player == null) return;
if(player == null || playerControlView == null) return;
reLayout(playerControlView);
if (playerControlView.isVisible()) {
playerControlView.hide();
Expand All @@ -350,6 +356,9 @@ private void togglePlayerControlVisibility() {
* Initializing Player control
*/
private void initializePlayerControl() {
if (!controls) {
return;
}
if (playerControlView == null) {
playerControlView = new PlayerControlView(getContext());
}
Expand Down Expand Up @@ -411,7 +420,7 @@ public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
* Adding Player control to the frame layout
*/
private void addPlayerControl() {
if(player == null) return;
if(player == null || playerControlView == null) return;
LayoutParams layoutParams = new LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
Expand Down Expand Up @@ -505,6 +514,7 @@ public void run() {
// DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread
ExecutorService es = Executors.newSingleThreadExecutor();
es.execute(new Runnable() {
ExecutorService parentEs = es;
@Override
public void run() {
// DRM initialization must run on a different thread
Expand All @@ -518,6 +528,7 @@ public void run() {

// Initialize handler to run on the main thread
activity.runOnUiThread(new Runnable() {
ExecutorService es = parentEs;
public void run() {
try {
// Source initialization must run on the main thread
Expand All @@ -528,6 +539,7 @@ public void run() {
Log.e("ExoPlayer Exception", ex.toString());
self.eventEmitter.error(ex.toString(), ex, "1001");
}
es.shutdown();
}
});
}
Expand All @@ -552,6 +564,15 @@ private void initializePlayerCore(ReactExoplayerView self) {
self.trackSelector.setParameters(trackSelector.buildUponParameters()
.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate));

Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long freeMemory = runtime.maxMemory() - usedMemory;
int backBufferMs = backBufferDurationMs;
if (freeMemory < (self.enableBackBufferAvailableMemory * 1000 * 1000)) {
Log.w("LoadControl", "Available memory is less than required to enable back buffer, setting to 0ms!");
backBufferMs = 0;
}

DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
RNVLoadControl loadControl = new RNVLoadControl(
allocator,
Expand All @@ -561,7 +582,7 @@ private void initializePlayerCore(ReactExoplayerView self) {
bufferForPlaybackAfterRebufferMs,
-1,
true,
backBufferDurationMs,
backBufferMs,
DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME
);
DefaultRenderersFactory renderersFactory =
Expand All @@ -575,7 +596,6 @@ private void initializePlayerCore(ReactExoplayerView self) {
player.addListener(self);
exoPlayerView.setPlayer(player);
audioBecomingNoisyReceiver.setListener(self);
bandwidthMeter.addEventListener(new Handler(), self);
setPlayWhenReady(!isPaused);
playerNeedsSource = true;

Expand Down Expand Up @@ -774,15 +794,29 @@ private MediaSource buildTextSource(String title, Uri uri, String mimeType, Stri
private void releasePlayer() {
if (player != null) {
updateResumePosition();
player.setPlayWhenReady(false);
player.stop(true);
player.seekTo(0);
player.release();
player.removeListener(this);
trackSelector = null;
player = null;
exoPlayerView.setPlayer(null);
if (playerControlView != null) {
playerControlView.setPlayer(null);
}
}
progressHandler.removeMessages(SHOW_PROGRESS);
themedReactContext.removeLifecycleEventListener(this);
audioBecomingNoisyReceiver.removeListener();
bandwidthMeter.removeEventListener(this);
if (bandwidthMeter != null) {
bandwidthMeter.removeEventListener(this);
bandwidthMeter = null;
}
Runtime runtime = Runtime.getRuntime();
if (runtime != null) {
runtime.gc();
}
}

private boolean requestAudioFocus() {
Expand Down Expand Up @@ -1024,11 +1058,13 @@ private void videoLoaded() {

ExecutorService es = Executors.newSingleThreadExecutor();
es.execute(new Runnable() {
ExecutorService parentEs = es;
@Override
public void run() {
// To prevent ANRs caused by getVideoTrackInfo we run this on a different thread and notify the player only when we're done
eventEmitter.load(duration, currentPosition, width, height,
audioTrackInfo, textTrackInfo, getVideoTrackInfo(trackRendererIndex), trackId);
parentEs.shutdown();
}
});
}
Expand Down Expand Up @@ -1090,6 +1126,13 @@ private WritableArray getVideoTrackInfo(int trackRendererIndex) {

for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
Format format = group.getFormat(trackIndex);

int shortestFormatSide = format.height < format.width ? format.height : format.width;
int shortestScreenSize = this.getScreenShortestSide(this.themedReactContext);
if (this.limitMaxResolution && shortestFormatSide > shortestScreenSize) {
// This video track is larger than screen resolution so we do not include it in the list of video tracks
continue;
}
if (isFormatSupported(format)) {
WritableMap videoTrack = Arguments.createMap();
videoTrack.putInt("width", format.width == Format.NO_VALUE ? 0 : format.width);
Expand All @@ -1105,21 +1148,68 @@ private WritableArray getVideoTrackInfo(int trackRendererIndex) {
return videoTracks;
}

private int getScreenShortestSide(ThemedReactContext context) {
if (context == null) {
// No context so we fallback to max int
return 2147483647;
}
Display display = context.getCurrentActivity().getWindowManager().getDefaultDisplay();
int realWidth;
int realHeight;

if (Build.VERSION.SDK_INT >= 17){
//new pleasant way to get real metrics
DisplayMetrics realMetrics = new DisplayMetrics();
display.getRealMetrics(realMetrics);
realWidth = realMetrics.widthPixels;
realHeight = realMetrics.heightPixels;

} else if (Build.VERSION.SDK_INT >= 14) {
//Reflection for this weird in-between time
try {
Method mGetRawH = Display.class.getMethod("getRawHeight");
Method mGetRawW = Display.class.getMethod("getRawWidth");
realWidth = (Integer) mGetRawW.invoke(display);
realHeight = (Integer) mGetRawH.invoke(display);

} catch (Exception e) {
//This may not be 100% accurate, but it's all we've got
realWidth = display.getWidth();
realHeight = display.getHeight();
Log.e("Display Info", "Couldn't use reflection to get the real display metrics.");
}

} else {
//This should be close, as lower API devices should not have window navigation bars
realWidth = display.getWidth();
realHeight = display.getHeight();
}
return realHeight < realWidth ? realHeight : realWidth;
}

private WritableArray getVideoTrackInfoFromManifest() {
return this.getVideoTrackInfoFromManifest(0);
}

// We need retry count to in case where minefest request fails from poor network conditions
private WritableArray getVideoTrackInfoFromManifest(int retryCount) {
ExecutorService es = Executors.newSingleThreadExecutor();
if (this.mediaDataSourceFactory == null) {
// Data source factory was not yet initialised - we can't proceed without it!
return null;
}
final DataSource dataSource = this.mediaDataSourceFactory.createDataSource();
final Uri sourceUri = this.srcUri;
final long startTime = this.contentStartTime * 1000 - 100; // s -> ms with 100ms offset
int shortestScreenSide = this.getScreenShortestSide(this.themedReactContext);
boolean limitMaxRes = this.limitMaxResolution;

Future<WritableArray> result = es.submit(new Callable<WritableArray>() {
DataSource ds = dataSource;
Uri uri = sourceUri;
long startTimeUs = startTime * 1000; // ms -> us
int shortestScreenSize = shortestScreenSide;
boolean limitMaxResolution = limitMaxRes;

public WritableArray call() throws Exception {
WritableArray videoTracks = Arguments.createArray();
Expand All @@ -1142,6 +1232,13 @@ public WritableArray call() throws Exception {
}
hasFoundContentPeriod = true;
WritableMap videoTrack = Arguments.createMap();

int shortestFormatSide = format.height < format.width ? format.height : format.width;
if (limitMaxResolution && shortestFormatSide > shortestScreenSize) {
// This video track is larger than screen resolution so we do not include it in the list of video tracks
continue;
}

videoTrack.putInt("width", format.width == Format.NO_VALUE ? 0 : format.width);
videoTrack.putInt("height",format.height == Format.NO_VALUE ? 0 : format.height);
videoTrack.putInt("bitrate", format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
Expand Down Expand Up @@ -1176,6 +1273,10 @@ public WritableArray call() throws Exception {

private WritableArray getTextTrackInfo() {
WritableArray textTracks = Arguments.createArray();
if (trackSelector == null) {
// Likely player is unmounting so no text tracks are available anymore
return textTracks;
}

MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
int index = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
Expand Down Expand Up @@ -1343,6 +1444,10 @@ public void setSrc(final Uri uri, final String extension, Map<String, String> he
this.srcUri = uri;
this.extension = extension;
this.requestHeaders = headers;
if (this.bandwidthMeter == null) {
this.bandwidthMeter = config.getBandwidthMeter();
this.bandwidthMeter.addEventListener(new Handler(), this);
}
this.mediaDataSourceFactory =
DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter,
this.requestHeaders);
Expand All @@ -1363,6 +1468,7 @@ public void clearSrc() {
this.mediaDataSourceFactory = null;
clearResumePosition();
}
releasePlayer();
}

public void setProgressUpdateInterval(final float progressUpdateInterval) {
Expand Down Expand Up @@ -1400,6 +1506,10 @@ public void setResizeModeModifier(@ResizeMode.Mode int resizeMode) {
exoPlayerView.setResizeMode(resizeMode);
}

public void setLimitMaxResolution(boolean limitMaxResolution) {
this.limitMaxResolution = limitMaxResolution;
}

private void applyModifiers() {
setRepeatModifier(repeat);
setMutedModifier(muted);
Expand Down Expand Up @@ -1769,14 +1879,15 @@ 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) {
public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBufferForPlaybackMs, int newBufferForPlaybackAfterRebufferMs, double newMaxHeapAllocationPercent, double newMinBackBufferMemoryReservePercent, double newMinBufferMemoryReservePercent, double minAvailableMemoryToEnableBackBuffer) {
minBufferMs = newMinBufferMs;
maxBufferMs = newMaxBufferMs;
bufferForPlaybackMs = newBufferForPlaybackMs;
bufferForPlaybackAfterRebufferMs = newBufferForPlaybackAfterRebufferMs;
maxHeapAllocationPercent = newMaxHeapAllocationPercent;
minBackBufferMemoryReservePercent = newMinBackBufferMemoryReservePercent;
minBufferMemoryReservePercent = newMinBufferMemoryReservePercent;
enableBackBufferAvailableMemory = minAvailableMemoryToEnableBackBuffer;
releasePlayer();
initializePlayer();
}
Expand Down
Loading

0 comments on commit ddca85d

Please sign in to comment.