Skip to content

Instantly share code, notes, and snippets.

@abd3lraouf
Created October 25, 2022 10:00
Show Gist options
  • Save abd3lraouf/17f79744ba2dbfa88c6dd1fce26f1733 to your computer and use it in GitHub Desktop.
Save abd3lraouf/17f79744ba2dbfa88c6dd1fce26f1733 to your computer and use it in GitHub Desktop.
exo player custom load control with drip-feeding

Exo Player Drip-feeding

TBD

/**
* The default {@link LoadControl} implementation.
*/
public final class CustomLoadControl implements LoadControl {
/**
* The default minimum duration of media that the player will attempt to ensure is buffered at all
* times, in milliseconds.
*/
public static final int DEFAULT_MIN_BUFFER_MS = 5_000;
/**
* The default maximum duration of media that the player will attempt to buffer, in milliseconds.
*/
public static final int DEFAULT_MAX_BUFFER_MS = 10_000;
/**
* The default duration of media that must be buffered for playback to start or resume following a
* user action such as a seek, in milliseconds.
*/
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 50;
/**
* The default duration of media that must be buffered for playback to resume after a rebuffer,
* in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user
* action.
*/
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 50;
/**
* To increase buffer time and size.
* Added by Sri
*/
public static int VIDEO_BUFFER_SCALE_UP_FACTOR = 4;
/**
* Priority for media loading.
*/
public static final int LOADING_PRIORITY = 0;
private EventListener bufferedDurationListener;
private Handler eventHandler;
private static final int ABOVE_HIGH_WATERMARK = 0;
private static final int BETWEEN_WATERMARKS = 1;
private static final int BELOW_LOW_WATERMARK = 2;
private final DefaultAllocator allocator;
private final long minBufferUs;
private final long maxBufferUs;
private final long bufferForPlaybackUs;
private final long bufferForPlaybackAfterRebufferUs;
private final PriorityTaskManager priorityTaskManager;
private int targetBufferSize;
private boolean isBuffering;
/**
* Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
*/
public CustomLoadControl() {
this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE));
}
/**
* Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
*/
public CustomLoadControl(EventListener listener, Handler handler) {
this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE));
bufferedDurationListener = listener;
eventHandler = handler;
}
/**
* Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
*
* @param allocator The {@link DefaultAllocator} used by the loader.
*/
public CustomLoadControl(DefaultAllocator allocator) {
this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
}
/**
* Constructs a new instance.
*
* @param allocator The {@link DefaultAllocator} used by the loader.
* @param minBufferMs The minimum duration of media that the player will attempt to ensure is
* buffered at all times, in milliseconds.
* @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
* milliseconds.
* @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
* resume following a user action such as a seek, in milliseconds.
* @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
* playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
* buffer depletion rather than a user action.
*/
public CustomLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) {
this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs,
null);
}
/**
* Constructs a new instance.
*
* @param allocator The {@link DefaultAllocator} used by the loader.
* @param minBufferMs The minimum duration of media that the player will attempt to ensure is
* buffered at all times, in milliseconds.
* @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
* milliseconds.
* @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
* resume following a user action such as a seek, in milliseconds.
* @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
* playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
* buffer depletion rather than a user action.
* @param priorityTaskManager If not null, registers itself as a task with priority
* {@link #LOADING_PRIORITY} during loading periods, and unregisters itself during draining
* periods.
*/
public CustomLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs,
PriorityTaskManager priorityTaskManager) {
this.allocator = allocator;
minBufferUs = VIDEO_BUFFER_SCALE_UP_FACTOR/*Added by Sri to control buffer size */ * minBufferMs * 1000L;
maxBufferUs = VIDEO_BUFFER_SCALE_UP_FACTOR/*Added by Sri to control buffer size */ * maxBufferMs * 1000L;
bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
this.priorityTaskManager = priorityTaskManager;
}
@Override
public void onPrepared() {
reset(false);
}
@Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, ExoTrackSelection[] trackSelections) {
targetBufferSize = 0;
for (int i = 0; i < renderers.length; i++) {
if (trackSelections[i] != null) {
targetBufferSize += getDefaultBufferSize(renderers[i].getTrackType());
if(renderers[i].getTrackType() == C.TRACK_TYPE_VIDEO)
targetBufferSize *= VIDEO_BUFFER_SCALE_UP_FACTOR; /*Added by Sri to control buffer size */
}
}
allocator.setTargetBufferSize(targetBufferSize);
}
@Override
public void onStopped() {
reset(true);
}
@Override
public void onReleased() {
reset(true);
}
@Override
public Allocator getAllocator() {
return allocator;
}
@Override
public long getBackBufferDurationUs() {
return 0;
}
@Override
public boolean retainBackBufferFromKeyframe() {
return false;
}
@Override
public boolean shouldContinueLoading(long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
int bufferTimeState = getBufferTimeState(bufferedDurationUs);
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
boolean wasBuffering = isBuffering;
isBuffering = bufferTimeState == BELOW_LOW_WATERMARK
|| (bufferTimeState == BETWEEN_WATERMARKS
/*
* commented below line to achieve drip-feeding method for better caching. once you are below maxBufferUs, do fetch immediately.
* Added by Sri
*/
/* && isBuffering */
&& !targetBufferSizeReached);
if (priorityTaskManager != null && isBuffering != wasBuffering) {
if (isBuffering) {
priorityTaskManager.add(LOADING_PRIORITY);
} else {
priorityTaskManager.remove(LOADING_PRIORITY);
}
}
if (null != bufferedDurationListener && null != eventHandler)
eventHandler.post(new Runnable() {
@Override
public void run() {
bufferedDurationListener.onBufferedDurationSample(bufferedDurationUs);
}
});
// Log.e("DLC","current buff Dur: "+bufferedDurationUs+",max buff:" + maxBufferUs +" shouldContinueLoading: "+isBuffering);
return isBuffering;
}
@Override
public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering, long targetLiveOffsetUs) {
long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs;
return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs;
}
private int getBufferTimeState(long bufferedDurationUs) {
return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK
: (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS);
}
private void reset(boolean resetAllocator) {
targetBufferSize = 0;
if (priorityTaskManager != null && isBuffering) {
priorityTaskManager.remove(LOADING_PRIORITY);
}
isBuffering = false;
if (resetAllocator) {
allocator.reset();
}
}
private static int getDefaultBufferSize(@C.TrackType int trackType) {
switch (trackType) {
case C.TRACK_TYPE_DEFAULT:
return DEFAULT_MUXED_BUFFER_SIZE;
case C.TRACK_TYPE_AUDIO:
return DEFAULT_AUDIO_BUFFER_SIZE;
case C.TRACK_TYPE_VIDEO:
return DEFAULT_VIDEO_BUFFER_SIZE;
case C.TRACK_TYPE_TEXT:
return DEFAULT_TEXT_BUFFER_SIZE;
case C.TRACK_TYPE_METADATA:
return DEFAULT_METADATA_BUFFER_SIZE;
case C.TRACK_TYPE_CAMERA_MOTION:
return DEFAULT_CAMERA_MOTION_BUFFER_SIZE;
case C.TRACK_TYPE_IMAGE:
return DEFAULT_IMAGE_BUFFER_SIZE;
case C.TRACK_TYPE_NONE:
return 0;
case C.TRACK_TYPE_UNKNOWN:
default:
throw new IllegalArgumentException();
}
}
public interface EventListener {
void onBufferedDurationSample(long bufferedDurationUs);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment