diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index 12f5952dd0..b999164f00 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -40,6 +40,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final int maxDurationForQualityDecreaseMs; private final int minDurationToRetainAfterDiscardMs; private final float bandwidthFraction; + private final float bufferedFractionToLiveEdgeForQualityIncrease; /** * @param bandwidthMeter Provides an estimate of the currently available bandwidth. @@ -48,7 +49,9 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, - DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION); + DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + DEFAULT_BANDWIDTH_FRACTION, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); } /** @@ -70,19 +73,53 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate, int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, int minDurationToRetainAfterDiscardMs, float bandwidthFraction) { + this (bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, + bandwidthFraction, DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); + } + + /** + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed + * when a bandwidth estimate is unavailable. + * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for + * the selected track to switch to one of higher quality. + * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for + * the selected track to switch to one of lower quality. + * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher + * quality, the selection may indicate that media already buffered at the lower quality can + * be discarded to speed up the switch. This is the minimum duration of media that must be + * retained at the lower quality. + * @param bandwidthFraction The fraction of the available bandwidth that the selection should + * consider available for use. Setting to a value less than 1 is recommended to account + * for inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of + * the duration from current playback position to the live edge that has to be buffered + * before the selected track can be switched to one of higher quality. This parameter is + * only applied when the playback position is closer to the live edge than + * {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a + * higher quality from happening. + */ + public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate, + int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease) { this.bandwidthMeter = bandwidthMeter; this.maxInitialBitrate = maxInitialBitrate; this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs; this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs; this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs; this.bandwidthFraction = bandwidthFraction; + this.bufferedFractionToLiveEdgeForQualityIncrease = + bufferedFractionToLiveEdgeForQualityIncrease; } @Override public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) { return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, - minDurationToRetainAfterDiscardMs, bandwidthFraction); + minDurationToRetainAfterDiscardMs, bandwidthFraction, + bufferedFractionToLiveEdgeForQualityIncrease); } } @@ -92,6 +129,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000; public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000; public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; + public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f; private final BandwidthMeter bandwidthMeter; private final int maxInitialBitrate; @@ -99,6 +137,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final long maxDurationForQualityDecreaseUs; private final long minDurationToRetainAfterDiscardUs; private final float bandwidthFraction; + private final float bufferedFractionToLiveEdgeForQualityIncrease; private int selectedIndex; private int reason; @@ -114,7 +153,9 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, - DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION); + DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + DEFAULT_BANDWIDTH_FRACTION, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); } /** @@ -135,11 +176,17 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * @param bandwidthFraction The fraction of the available bandwidth that the selection should * consider available for use. Setting to a value less than 1 is recommended to account * for inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of + * the duration from current playback position to the live edge that has to be buffered + * before the selected track can be switched to one of higher quality. This parameter is + * only applied when the playback position is closer to the live edge than + * {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a + * higher quality from happening. */ public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, int maxInitialBitrate, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, - float bandwidthFraction) { + float bandwidthFraction, float bufferedFractionToLiveEdgeForQualityIncrease) { super(group, tracks); this.bandwidthMeter = bandwidthMeter; this.maxInitialBitrate = maxInitialBitrate; @@ -147,12 +194,14 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { 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 bufferedDurationUs) { + public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { long nowMs = SystemClock.elapsedRealtime(); // Stash the current selection, then make a new one. int currentSelectedIndex = selectedIndex; @@ -160,12 +209,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { 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) { + && 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; @@ -251,4 +301,12 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { return lowestBitrateNonBlacklistedIndex; } + private long minDurationForQualityIncreaseUs(long availableDurationUs) { + boolean isAvailableDurationTooShort = availableDurationUs != C.TIME_UNSET + && availableDurationUs <= minDurationForQualityIncreaseUs; + return isAvailableDurationTooShort + ? (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease) + : minDurationForQualityIncreaseUs; + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java index de1b500c61..ca43258e3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java @@ -78,7 +78,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { // Do nothing. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java index 5c7625d6b4..b70cc8e0d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java @@ -88,7 +88,7 @@ public final class RandomTrackSelection extends BaseTrackSelection { } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { // Count the number of non-blacklisted formats. long nowMs = SystemClock.elapsedRealtime(); int nonBlacklistedFormatCount = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index fe66946a65..aeb1d1d6e3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -26,7 +26,7 @@ import java.util.List; * {@link TrackGroup}, and a possibly varying individual selected track from the subset. *

* Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual selected - * track may change as a result of calling {@link #updateSelectedTrack(long)}. + * track may change as a result of calling {@link #updateSelectedTrack(long, long)}. */ public interface TrackSelection { @@ -126,8 +126,11 @@ public interface TrackSelection { * Updates the selected track. * * @param bufferedDurationUs The duration of media currently buffered in microseconds. + * @param availableDurationUs The duration of media available for buffering from the current + * playback position, in microseconds, or {@link C#TIME_UNSET} if media can be buffered + * to the end of the current period. */ - void updateSelectedTrack(long bufferedDurationUs); + void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs); /** * May be called periodically by sources that load media in discrete {@link MediaChunk}s and @@ -148,10 +151,10 @@ public interface TrackSelection { /** * Attempts to blacklist the track at the specified index in the selection, making it ineligible - * for selection by calls to {@link #updateSelectedTrack(long)} for the specified period of time. - * Blacklisting will fail if all other tracks are currently blacklisted. If blacklisting the + * for selection by calls to {@link #updateSelectedTrack(long, long)} for the specified period of + * time. Blacklisting will fail if all other tracks are currently blacklisted. If blacklisting the * currently selected track, note that it will remain selected until the next call to - * {@link #updateSelectedTrack(long)}. + * {@link #updateSelectedTrack(long, long)}. * * @param index The index of the track in the selection. * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index c6c1461001..cd7ef6a2bf 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -95,6 +95,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private int periodIndex; private IOException fatalError; private boolean missingLastSegment; + private long liveEdgeTimeUs; /** * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. @@ -130,6 +131,7 @@ public class DefaultDashChunkSource implements DashChunkSource { this.maxSegmentsPerLoad = maxSegmentsPerLoad; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); + liveEdgeTimeUs = C.TIME_UNSET; List representations = getRepresentations(); representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { @@ -179,7 +181,8 @@ public class DefaultDashChunkSource implements DashChunkSource { } long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - trackSelection.updateSelectedTrack(bufferedDurationUs); + long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs, previous == null); + trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); RepresentationHolder representationHolder = representationHolders[trackSelection.getSelectedIndex()]; @@ -203,7 +206,6 @@ public class DefaultDashChunkSource implements DashChunkSource { } } - long nowUs = getNowUnixTimeUs(); int availableSegmentCount = representationHolder.getSegmentCount(); if (availableSegmentCount == 0) { // The index doesn't define any segments. @@ -216,21 +218,23 @@ public class DefaultDashChunkSource implements DashChunkSource { if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { // The index is itself unbounded. We need to use the current time to calculate the range of // available segments. - long liveEdgeTimeUs = nowUs - manifest.availabilityStartTime * 1000; - long periodStartUs = manifest.getPeriod(periodIndex).startMs * 1000; + long liveEdgeTimeUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTime); + long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs); long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs; if (manifest.timeShiftBufferDepth != C.TIME_UNSET) { - long bufferDepthUs = manifest.timeShiftBufferDepth * 1000; + long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepth); firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs)); } - // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the + // getSegmentNum(liveEdgeTimeInPeriodUs) will not be completed yet, so subtract one to get the // index of the last completed segment. lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs) - 1; } else { lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1; } + updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum); + int segmentNum; if (previous == null) { segmentNum = Util.constrainValue(representationHolder.getSegmentNum(playbackPositionUs), @@ -311,6 +315,19 @@ public class DefaultDashChunkSource implements DashChunkSource { return representations; } + private void updateLiveEdgeTimeUs(RepresentationHolder representationHolder, + int lastAvailableSegmentNum) { + if (manifest.dynamic) { + DashSegmentIndex segmentIndex = representationHolder.representation.getIndex(); + long lastSegmentDurationUs = segmentIndex.getDurationUs(lastAvailableSegmentNum, + manifest.getPeriodDurationUs(periodIndex)); + liveEdgeTimeUs = segmentIndex.getTimeUs(lastAvailableSegmentNum) + + lastSegmentDurationUs; + } else { + liveEdgeTimeUs = C.TIME_UNSET; + } + } + private long getNowUnixTimeUs() { if (elapsedRealtimeOffsetMs != 0) { return (SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs) * 1000; @@ -375,6 +392,12 @@ public class DefaultDashChunkSource implements DashChunkSource { } } + private long resolveTimeToLiveEdgeUs(long playbackPositionUs, boolean isAfterPositionReset) { + boolean resolveTimeToLiveEdgePossible = manifest.dynamic + && !isAfterPositionReset && liveEdgeTimeUs != C.TIME_UNSET; + return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; + } + // Protected classes. /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index d0161d839c..0ad9dd1a6e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -103,6 +103,7 @@ import java.util.List; // the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods // in TrackSelection to avoid unexpected behavior. private TrackSelection trackSelection; + private long liveEdgeTimeUs; /** * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. @@ -122,6 +123,7 @@ import java.util.List; this.variants = variants; this.timestampAdjusterProvider = timestampAdjusterProvider; this.muxedCaptionFormats = muxedCaptionFormats; + liveEdgeTimeUs = C.TIME_UNSET; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; for (int i = 0; i < variants.length; i++) { @@ -214,7 +216,8 @@ import java.util.List; (independentSegments ? previous.endTimeUs : previous.startTimeUs) - playbackPositionUs); // Select the variant. - trackSelection.updateSelectedTrack(bufferedDurationUs); + long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs, previous == null); + trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); int selectedVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); boolean switchingVariant = oldVariantIndex != selectedVariantIndex; @@ -228,6 +231,8 @@ import java.util.List; HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); independentSegments = mediaPlaylist.hasIndependentSegmentsTag; + updateLiveEdgeTimeUs(mediaPlaylist); + // Select the chunk. int chunkMediaSequence; if (previous == null || switchingVariant) { @@ -360,6 +365,16 @@ import java.util.List; // Private methods. + private long resolveTimeToLiveEdgeUs(long playbackPositionUs, boolean isAfterPositionReset) { + final boolean resolveTimeToLiveEdgePossible = !isAfterPositionReset + && liveEdgeTimeUs != C.TIME_UNSET; + return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; + } + + private void updateLiveEdgeTimeUs(HlsMediaPlaylist mediaPlaylist) { + liveEdgeTimeUs = mediaPlaylist.hasEndTag ? C.TIME_UNSET : mediaPlaylist.getEndTimeUs(); + } + private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex, int trackSelectionReason, Object trackSelectionData) { DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); @@ -409,7 +424,7 @@ import java.util.List; } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { long nowMs = SystemClock.elapsedRealtime(); if (!isBlacklisted(selectedIndex, nowMs)) { return; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 00a3cd4a85..b844988588 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -92,7 +92,6 @@ import java.util.LinkedList; private boolean prepared; private int enabledTrackCount; private Format downstreamTrackFormat; - private int upstreamChunkUid; private boolean released; // Tracks are complicated in HLS. See documentation of buildTracks for details. @@ -255,7 +254,7 @@ import java.util.LinkedList; // may need to be discarded. boolean primarySampleQueueDirty = false; if (!seenFirstTrackSelection) { - primaryTrackSelection.updateSelectedTrack(0); + primaryTrackSelection.updateSelectedTrack(0, C.TIME_UNSET); int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { // This is the first selection and the chunk loaded during preparation does not match @@ -553,7 +552,6 @@ import java.util.LinkedList; * samples already queued to the wrapper. */ public void init(int chunkUid, boolean shouldSpliceIn) { - upstreamChunkUid = chunkUid; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.sourceId(chunkUid); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index 1069527989..ae7dfc32e8 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -155,7 +155,8 @@ public class DefaultSsChunkSource implements SsChunkSource { } long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - trackSelection.updateSelectedTrack(bufferedDurationUs); + long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); + trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); StreamElement streamElement = manifest.streamElements[elementIndex]; if (streamElement.chunkCount == 0) { @@ -222,4 +223,20 @@ public class DefaultSsChunkSource implements SsChunkSource { extractorWrapper); } + private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { + if (!manifest.isLive) { + return C.TIME_UNSET; + } + + StreamElement currentElement = manifest.streamElements[elementIndex]; + if (currentElement.chunkCount == 0) { + return C.TIME_UNSET; + } + + int lastChunkIndex = currentElement.chunkCount - 1; + long lastChunkEndTimeUs = currentElement.getStartTimeUs(lastChunkIndex) + + currentElement.getChunkDurationUs(lastChunkIndex); + return lastChunkEndTimeUs - playbackPositionUs; + } + } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index 41fda178d7..40c91a5a81 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import android.net.Uri; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.ChunkHolder; @@ -83,7 +84,7 @@ public final class FakeChunkSource implements ChunkSource { @Override public void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - trackSelection.updateSelectedTrack(bufferedDurationUs); + trackSelection.updateSelectedTrack(bufferedDurationUs, C.TIME_UNSET); int chunkIndex = previous == null ? dataSet.getChunkIndexByPosition(playbackPositionUs) : previous.getNextChunkIndex(); if (chunkIndex >= dataSet.getChunkCount()) {