Search code examples
androidvideohttp-live-streamingexoplayer

ExoPlayer Hls quality


I have ExoPlayer which plays HLS videos, the thing is i need to give user ability to change video quality(auto/1080/720/480).

I figured out that playing around with AdaptiveTrackSelection.Factory does set the quality, but it remains till the object is killed.

I have also tried using MappingTrackSelector, i know that my video has 4 tracks, but i did not get how to select any of it manually. Will this selection make it work?

Thanks for any ideas.

MappingTrackSelector.MappedTrackInfo trackInfo = mDefaultTrackSelector.getCurrentMappedTrackInfo();
                        mDefaultTrackSelector.selectTracks(

//what should go here?

  , trackInfo.getTrackGroups(4));

Solution

  • 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.