Quality Selector for Exoplayer 2

Quality selector for ExoPlayer 2

Everything you'd like to achieve is viewable in the ExoPlayer2 demo app. More specifically the PlayerActivity class.

You can also check out this good article on the topic.

The core points you'll want to look into are around track selection (via the TrackSelector) as well as the TrackSelectionHelper. I'll include the important code samples below which will hopefully be enough to get you going. But ultimately just following something similar in the demo app will get you where you need to be.

You'll hold onto the track selector you init the player with and use that for just about everything.

Below is just a block of code to ideally cover the gist of what you're trying to do since the demo does appear to over-complicate things a hair. Also I haven't run the code, but it's close enough.

// These two could be fields OR passed around
int videoRendererIndex;
TrackGroupArray trackGroups;

// This is the body of the logic for see if there are even video tracks
// It also does some field setting
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
for (int i = 0; i < mappedTrackInfo.length; i++) {
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
if (trackGroups.length != 0) {
switch (player.getRendererType(i)) {
case C.TRACK_TYPE_VIDEO:
videoRendererIndex = i;
return true;
}
}
}

// This next part is actually about getting the list. It doesn't include
// some additional logic they put in for adaptive tracks (DASH/HLS/SS),
// but you can look at the sample for that (TrackSelectionHelper#buildView())
// Below you'd be building up items in a list. This just does
// views directly, but you could just have a list of track names (with indexes)
for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
TrackGroup group = trackGroups.get(groupIndex);
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
if (trackIndex == 0) {
// Beginning of a new set, the demo app adds a divider
}
CheckedTextView trackView = ...; // The TextView to show in the list
// The below points to a util which extracts the quality from the TrackGroup
trackView.setText(DemoUtil.buildTrackName(group.getFormat(trackIndex)));
}

// Assuming you tagged the view with the groupIndex and trackIndex, you
// can build your override with that info.
Pair<Integer, Integer> tag = (Pair<Integer, Integer>) view.getTag();
int groupIndex = tag.first;
int trackIndex = tag.second;
// This is the override you'd use for something that isn't adaptive.
override = new SelectionOverride(FIXED_FACTORY, groupIndex, trackIndex);
// Otherwise they call their helper for adaptives, which roughly does:
int[] tracks = getTracksAdding(override, trackIndex);
TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY : adaptiveTrackSelectionFactory;
override = new SelectionOverride(factory, groupIndex, tracks);

// Then we actually set our override on the selector to switch the quality/track
selector.setSelectionOverride(rendererIndex, trackGroups, override);

As I mentioned above, this is a slight oversimplification of the process, but the core part is that you're messing around with the TrackSelector, SelectionOverride, and Track/TrackGroups to get this to work.

You could conceivably copy the demo code verbatim and it should work, but I'd highly recommend taking the time to understand what each piece is doing and tailor your solution to your use case.

If I had more time I'd get it to compile and run. But if you can get my sample going then feel free to edit my post.

Hope that helps :)

Customizing exoplayer quality dialog in my app

This might be late but here are the ways to do so,

Here the main Class that does all these awesome stuff is "TrackSelectionView", this class simply extends a LinearLayout. To achieve your desired features you need to make your own class (name is anything) and then just copy paste the entire code of TrackSelectionView in it. Why are we doing so? coz, we need to change some logic of that class and it's a read-only class.

Actually to achieve the first feature (no "none" option) you simply can write dialogPair.second.setShowDisableOption(false); instead of that "true".

Writing our own class and copy-paste code is for the second feature.

In "TrackSelectionView" it uses a 2-D array to store the CheckedTextView. For the first two togglebuttons (Auto and None) it uses CheckedTextView separately but for all other resolution, CheckedTextView is getting stored in that 2-D array.

I won't post the entire codebase here as it will make things messy, I have created a github.gist file, you can get a reference from there...

https://gist.github.com/abhiint16/b473e9b1111bd8bda4833c288ae6a1b4

Don't forget to use your class reference instead of TrackSelectionView.

You will use this above file as shown in this Gist
https://gist.github.com/abhiint16/165449a1a7d1a55a8f69d23718c603c2

The Gist file makes the selection "Single-select" and in addition to that it also performs an awesome stuff for you in case you need it in your ExoPlayer,

Sample Image

Here, the actual video format that you get is in 512 X 288, 0.57 Mbps format in a list, I'm just mapping predefined Low, Medium, High etc with the index of list. You can try your own way.

So when you click on one of the resolution, it transforms the textview of your exoplayer for the selected-resolution ("L" for "Low").

For that you just need to implement an Interface named GetReso in your Class and there you'll get the selected text-initial. Now you can just set that string to a textview.

Sample Image

Enjoy coding.....

ExoPlayer Hls quality

Regarding this thread :https://github.com/google/ExoPlayer/issues/2250, I managed to change exo player video quality while playing previous one, so it does not getting in buffering instantly.

So I have next classes :

public enum HLSQuality {
Auto, Quality1080, Quality720, Quality480, NoValue
}

class HLSUtil {

private HLSUtil() {
}

@NonNull
static HLSQuality getQuality(@NonNull Format format) {
switch (format.height) {
case 1080: {
return HLSQuality.Quality1080;
}
case 720: {
return HLSQuality.Quality720;
}
case 480:
case 486: {
return HLSQuality.Quality480;
}
default: {
return HLSQuality.NoValue;
}
}
}

static boolean isQualityPlayable(@NonNull Format format) {
return format.height <= 1080;
}
}

public class ClassAdaptiveTrackSelection extends BaseTrackSelection {

public static final class Factory implements TrackSelection.Factory {
private final BandwidthMeter bandwidthMeter;
private final int maxInitialBitrate = 2000000;
private final int minDurationForQualityIncreaseMs = 10000;
private final int maxDurationForQualityDecreaseMs = 25000;
private final int minDurationToRetainAfterDiscardMs = 25000;
private final float bandwidthFraction = 0.75f;
private final float bufferedFractionToLiveEdgeForQualityIncrease = 0.75f;

public Factory(BandwidthMeter bandwidthMeter) {
this.bandwidthMeter = bandwidthMeter;
}

@Override
public ClassAdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
Log.d(ClassAdaptiveTrackSelection.class.getSimpleName(), " Video player quality reset to Auto");
sHLSQuality = HLSQuality.Auto;

return new ClassAdaptiveTrackSelection(
group,
tracks,
bandwidthMeter,
maxInitialBitrate,
minDurationForQualityIncreaseMs,
maxDurationForQualityDecreaseMs,
minDurationToRetainAfterDiscardMs,
bandwidthFraction,
bufferedFractionToLiveEdgeForQualityIncrease
);
}
}

private static HLSQuality sHLSQuality = HLSQuality.Auto;
private final BandwidthMeter bandwidthMeter;
private final int maxInitialBitrate;
private final long minDurationForQualityIncreaseUs;
private final long maxDurationForQualityDecreaseUs;
private final long minDurationToRetainAfterDiscardUs;
private final float bandwidthFraction;
private final float bufferedFractionToLiveEdgeForQualityIncrease;

private int selectedIndex;
private int reason;

private ClassAdaptiveTrackSelection(TrackGroup group,
int[] tracks,
BandwidthMeter bandwidthMeter,
int maxInitialBitrate,
long minDurationForQualityIncreaseMs,
long maxDurationForQualityDecreaseMs,
long minDurationToRetainAfterDiscardMs,
float bandwidthFraction,
float bufferedFractionToLiveEdgeForQualityIncrease) {
super(group, tracks);
this.bandwidthMeter = bandwidthMeter;
this.maxInitialBitrate = maxInitialBitrate;
this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
this.bandwidthFraction = bandwidthFraction;
this.bufferedFractionToLiveEdgeForQualityIncrease = bufferedFractionToLiveEdgeForQualityIncrease;
selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE);
reason = C.SELECTION_REASON_INITIAL;
}

@Override
public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) {
long nowMs = SystemClock.elapsedRealtime();
// Stash the current selection, then make a new one.
int currentSelectedIndex = selectedIndex;
selectedIndex = determineIdealSelectedIndex(nowMs);
if (selectedIndex == currentSelectedIndex) {
return;
}

if (!isBlacklisted(currentSelectedIndex, nowMs)) {
// Revert back to the current selection if conditions are not suitable for switching.
Format currentFormat = getFormat(currentSelectedIndex);
Format selectedFormat = getFormat(selectedIndex);
if (selectedFormat.bitrate > currentFormat.bitrate
&& bufferedDurationUs < minDurationForQualityIncreaseUs(availableDurationUs)) {
// The selected track is a higher quality, but we have insufficient buffer to safely switch
// up. Defer switching up for now.
selectedIndex = currentSelectedIndex;
} else if (selectedFormat.bitrate < currentFormat.bitrate
&& bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
// The selected track is a lower quality, but we have sufficient buffer to defer switching
// down for now.
selectedIndex = currentSelectedIndex;
}
}
// If we adapted, update the trigger.
if (selectedIndex != currentSelectedIndex) {
reason = C.SELECTION_REASON_ADAPTIVE;
}
}

@Override
public int getSelectedIndex() {
return selectedIndex;
}

@Override
public int getSelectionReason() {
return reason;
}

@Override
public Object getSelectionData() {
return null;
}

@Override
public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
if (queue.isEmpty()) {
return 0;
}
int queueSize = queue.size();
long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs;
if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) {
return queueSize;
}
int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime());
Format idealFormat = getFormat(idealSelectedIndex);
// If the chunks contain video, discard from the first SD chunk beyond
// minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal
// track.
for (int i = 0; i < queueSize; i++) {
MediaChunk chunk = queue.get(i);
Format format = chunk.trackFormat;
long durationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs;
if (durationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs
&& format.bitrate < idealFormat.bitrate
&& format.height != Format.NO_VALUE && format.height < 720
&& format.width != Format.NO_VALUE && format.width < 1280
&& format.height < idealFormat.height) {
return i;
}
}
return queueSize;
}

private int determineIdealSelectedIndex(long nowMs) {
if (sHLSQuality != HLSQuality.Auto) {
Log.d(ClassAdaptiveTrackSelection.class.getSimpleName(), " Video player quality seeking for " + String.valueOf(sHLSQuality));
for (int i = 0; i < length; i++) {
Format format = getFormat(i);
if (HLSUtil.getQuality(format) == sHLSQuality) {
Log.d(ClassAdaptiveTrackSelection.class.getSimpleName(), " Video player quality set to " + String.valueOf(sHLSQuality));
return i;
}
}
}

Log.d(ClassAdaptiveTrackSelection.class.getSimpleName(), " Video player quality seeking for auto quality " + String.valueOf(sHLSQuality));
long bitrateEstimate = bandwidthMeter.getBitrateEstimate();
long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE
? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
int lowestBitrateNonBlacklistedIndex = 0;
for (int i = 0; i < length; i++) {
if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {
Format format = getFormat(i);
if (format.bitrate <= effectiveBitrate && HLSUtil.isQualityPlayable(format)) {
Log.d(ClassAdaptiveTrackSelection.class.getSimpleName(), " Video player quality auto quality found " + String.valueOf(sHLSQuality));
return i;
} else {
lowestBitrateNonBlacklistedIndex = i;
}
}
}
return lowestBitrateNonBlacklistedIndex;
}

private long minDurationForQualityIncreaseUs(long availableDurationUs) {
boolean isAvailableDurationTooShort = availableDurationUs != C.TIME_UNSET
&& availableDurationUs <= minDurationForQualityIncreaseUs;
return isAvailableDurationTooShort
? (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease)
: minDurationForQualityIncreaseUs;
}

static void setHLSQuality(HLSQuality HLSQuality) {
sHLSQuality = HLSQuality;
}
}

Hope it helps someone.



Related Topics



Leave a reply



Submit