Commit 6b9e1824 by tonihei Committed by Oliver Woodman

Extend updateSelectedTrack method with additional information.

This provides the list of currently buffered media chunks and iterators over
the potential next chunks to the track selection. Having these two parameters
enables more advanced decision logic based on this data.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=210551812
parent a429f481
...@@ -130,6 +130,9 @@ ...@@ -130,6 +130,9 @@
([#4681](https://github.com/google/ExoPlayer/issues/4681). ([#4681](https://github.com/google/ExoPlayer/issues/4681).
* Fix handling of postrolls with multiple ads * Fix handling of postrolls with multiple ads
([#4710](https://github.com/google/ExoPlayer/issues/4710). ([#4710](https://github.com/google/ExoPlayer/issues/4710).
* Provide additional information for adaptive track selection.
`TrackSelection.updateSelectedTrack` has two new parameters for the current
queue of media chunks and iterators for information about upcoming chunks.
### 2.8.4 ### ### 2.8.4 ###
......
...@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C; ...@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -328,9 +329,14 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { ...@@ -328,9 +329,14 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
} }
@Override @Override
public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, public void updateSelectedTrack(
long availableDurationUs) { long playbackPositionUs,
long bufferedDurationUs,
long availableDurationUs,
List<? extends MediaChunk> queue,
MediaChunkIterator[] mediaChunkIterators) {
long nowMs = clock.elapsedRealtime(); long nowMs = clock.elapsedRealtime();
// Stash the current selection, then make a new one. // Stash the current selection, then make a new one.
int currentSelectedIndex = selectedIndex; int currentSelectedIndex = selectedIndex;
selectedIndex = determineIdealSelectedIndex(nowMs); selectedIndex = determineIdealSelectedIndex(nowMs);
......
...@@ -20,15 +20,17 @@ import com.google.android.exoplayer2.C; ...@@ -20,15 +20,17 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import java.util.List; import java.util.List;
/** /**
* A track selection consisting of a static subset of selected tracks belonging to a * A track selection consisting of a static subset of selected tracks belonging to a {@link
* {@link TrackGroup}, and a possibly varying individual selected track from the subset. * TrackGroup}, and a possibly varying individual selected track from the subset.
* <p> *
* Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual selected * <p>Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual
* track may change as a result of calling {@link #updateSelectedTrack(long, long, long)}. * selected track may change as a result of calling {@link #updateSelectedTrack(long, long, long,
* List, MediaChunkIterator[])}.
*/ */
public interface TrackSelection { public interface TrackSelection {
...@@ -148,25 +150,47 @@ public interface TrackSelection { ...@@ -148,25 +150,47 @@ public interface TrackSelection {
void onPlaybackSpeed(float speed); void onPlaybackSpeed(float speed);
/** /**
* Updates the selected track. * @deprecated Use and implement {@link #updateSelectedTrack(long, long, long, float, List,
* <p> * MediaChunkIterator[])} instead.
* This method may only be called when the selection is enabled. */
@Deprecated
default void updateSelectedTrack(
long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) {
throw new UnsupportedOperationException();
}
/**
* Updates the selected track for sources that load media in discrete {@link MediaChunk}s.
*
* <p>This method may only be called when the selection is enabled.
* *
* @param playbackPositionUs The current playback position in microseconds. If playback of the * @param playbackPositionUs The current playback position in microseconds. If playback of the
* period to which this track selection belongs has not yet started, the value will be the * period to which this track selection belongs has not yet started, the value will be the
* starting position in the period minus the duration of any media in previous periods still * starting position in the period minus the duration of any media in previous periods still
* to be played. * to be played.
* @param bufferedDurationUs The duration of media currently buffered from the current playback * @param bufferedDurationUs The duration of media currently buffered from the current playback
* position, in microseconds. Note that the next load position can be calculated as * position, in microseconds. Note that the next load position can be calculated as {@code
* {@code (playbackPositionUs + bufferedDurationUs)}. * (playbackPositionUs + bufferedDurationUs)}.
* @param availableDurationUs The duration of media available for buffering from the current * @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 * playback position, in microseconds, or {@link C#TIME_UNSET} if media can be buffered to the
* to the end of the current period. Note that if not set to {@link C#TIME_UNSET}, the * end of the current period. Note that if not set to {@link C#TIME_UNSET}, the position up to
* position up to which media is available for buffering can be calculated as * which media is available for buffering can be calculated as {@code (playbackPositionUs +
* {@code (playbackPositionUs + availableDurationUs)}. * availableDurationUs)}.
*/ * @param queue The queue of already buffered {@link MediaChunk}s. Must not be modified.
void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, * @param mediaChunkIterators An array of {@link MediaChunkIterator}s providing information about
long availableDurationUs); * the sequence of upcoming media chunks for each track in the selection. All iterators start
* from the media chunk which will be loaded next if the respective track is selected. Note
* that this information may not be available for all tracks, and so some iterators may be
* empty.
*/
default void updateSelectedTrack(
long playbackPositionUs,
long bufferedDurationUs,
long availableDurationUs,
List<? extends MediaChunk> queue,
MediaChunkIterator[] mediaChunkIterators) {
updateSelectedTrack(playbackPositionUs, bufferedDurationUs, availableDurationUs);
}
/** /**
* May be called periodically by sources that load media in discrete {@link MediaChunk}s and * May be called periodically by sources that load media in discrete {@link MediaChunk}s and
...@@ -190,12 +214,13 @@ public interface TrackSelection { ...@@ -190,12 +214,13 @@ public interface TrackSelection {
/** /**
* Attempts to blacklist the track at the specified index in the selection, making it ineligible * Attempts to blacklist the track at the specified index in the selection, making it ineligible
* for selection by calls to {@link #updateSelectedTrack(long, long, long)} for the specified * for selection by calls to {@link #updateSelectedTrack(long, long, long, List,
* period of time. Blacklisting will fail if all other tracks are currently blacklisted. If * MediaChunkIterator[])} for the specified period of time. Blacklisting will fail if all other
* blacklisting the currently selected track, note that it will remain selected until the next * tracks are currently blacklisted. If blacklisting the currently selected track, note that it
* call to {@link #updateSelectedTrack(long, long, long)}. * will remain selected until the next call to {@link #updateSelectedTrack(long, long, long, List,
* <p> * MediaChunkIterator[])}.
* This method may only be called when the selection is enabled. *
* <p>This method may only be called when the selection is enabled.
* *
* @param index The index of the track in the selection. * @param index The index of the track in the selection.
* @param blacklistDurationMs The duration of time for which the track should be blacklisted, in * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in
...@@ -203,5 +228,4 @@ public interface TrackSelection { ...@@ -203,5 +228,4 @@ public interface TrackSelection {
* @return Whether blacklisting was successful. * @return Whether blacklisting was successful.
*/ */
boolean blacklist(int index, long blacklistDurationMs); boolean blacklist(int index, long blacklistDurationMs);
} }
...@@ -28,6 +28,7 @@ import com.google.android.exoplayer2.C; ...@@ -28,6 +28,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.testutil.FakeClock; import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
...@@ -36,6 +37,7 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; ...@@ -36,6 +37,7 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
...@@ -47,6 +49,11 @@ import org.robolectric.RobolectricTestRunner; ...@@ -47,6 +49,11 @@ import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public final class AdaptiveTrackSelectionTest { public final class AdaptiveTrackSelectionTest {
private static final MediaChunkIterator[] THREE_EMPTY_MEDIA_CHUNK_ITERATORS =
new MediaChunkIterator[] {
MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY, MediaChunkIterator.EMPTY
};
@Mock private BandwidthMeter mockBandwidthMeter; @Mock private BandwidthMeter mockBandwidthMeter;
private FakeClock fakeClock; private FakeClock fakeClock;
...@@ -70,7 +77,9 @@ public final class AdaptiveTrackSelectionTest { ...@@ -70,7 +77,9 @@ public final class AdaptiveTrackSelectionTest {
adaptiveTrackSelection.updateSelectedTrack( adaptiveTrackSelection.updateSelectedTrack(
/* playbackPositionUs= */ 0, /* playbackPositionUs= */ 0,
/* bufferedDurationUs= */ 0, /* bufferedDurationUs= */ 0,
/* availableDurationUs= */ C.TIME_UNSET); /* availableDurationUs= */ C.TIME_UNSET,
/* queue= */ Collections.emptyList(),
/* mediaChunkIterators= */ new MediaChunkIterator[] {MediaChunkIterator.EMPTY});
verify(initialBandwidthMeter, atLeastOnce()).getBitrateEstimate(); verify(initialBandwidthMeter, atLeastOnce()).getBitrateEstimate();
verifyZeroInteractions(injectedBandwidthMeter); verifyZeroInteractions(injectedBandwidthMeter);
...@@ -121,7 +130,9 @@ public final class AdaptiveTrackSelectionTest { ...@@ -121,7 +130,9 @@ public final class AdaptiveTrackSelectionTest {
adaptiveTrackSelection.updateSelectedTrack( adaptiveTrackSelection.updateSelectedTrack(
/* playbackPositionUs= */ 0, /* playbackPositionUs= */ 0,
/* bufferedDurationUs= */ 9_999_000, /* bufferedDurationUs= */ 9_999_000,
/* availableDurationUs= */ C.TIME_UNSET); /* availableDurationUs= */ C.TIME_UNSET,
/* queue= */ Collections.emptyList(),
/* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS);
// When bandwidth estimation is updated to 2000L, we can switch up to use a higher bitrate // When bandwidth estimation is updated to 2000L, we can switch up to use a higher bitrate
// format. However, since we only buffered 9_999_000 us, which is smaller than // format. However, since we only buffered 9_999_000 us, which is smaller than
...@@ -147,7 +158,9 @@ public final class AdaptiveTrackSelectionTest { ...@@ -147,7 +158,9 @@ public final class AdaptiveTrackSelectionTest {
adaptiveTrackSelection.updateSelectedTrack( adaptiveTrackSelection.updateSelectedTrack(
/* playbackPositionUs= */ 0, /* playbackPositionUs= */ 0,
/* bufferedDurationUs= */ 10_000_000, /* bufferedDurationUs= */ 10_000_000,
/* availableDurationUs= */ C.TIME_UNSET); /* availableDurationUs= */ C.TIME_UNSET,
/* queue= */ Collections.emptyList(),
/* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS);
// When bandwidth estimation is updated to 2000L, we can switch up to use a higher bitrate // When bandwidth estimation is updated to 2000L, we can switch up to use a higher bitrate
// format. When we have buffered enough (10_000_000 us, which is equal to // format. When we have buffered enough (10_000_000 us, which is equal to
...@@ -173,7 +186,9 @@ public final class AdaptiveTrackSelectionTest { ...@@ -173,7 +186,9 @@ public final class AdaptiveTrackSelectionTest {
adaptiveTrackSelection.updateSelectedTrack( adaptiveTrackSelection.updateSelectedTrack(
/* playbackPositionUs= */ 0, /* playbackPositionUs= */ 0,
/* bufferedDurationUs= */ 25_000_000, /* bufferedDurationUs= */ 25_000_000,
/* availableDurationUs= */ C.TIME_UNSET); /* availableDurationUs= */ C.TIME_UNSET,
/* queue= */ Collections.emptyList(),
/* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS);
// When bandwidth estimation is updated to 500L, we should switch down to use a lower bitrate // When bandwidth estimation is updated to 500L, we should switch down to use a lower bitrate
// format. However, since we have enough buffer at higher quality (25_000_000 us, which is equal // format. However, since we have enough buffer at higher quality (25_000_000 us, which is equal
...@@ -199,7 +214,9 @@ public final class AdaptiveTrackSelectionTest { ...@@ -199,7 +214,9 @@ public final class AdaptiveTrackSelectionTest {
adaptiveTrackSelection.updateSelectedTrack( adaptiveTrackSelection.updateSelectedTrack(
/* playbackPositionUs= */ 0, /* playbackPositionUs= */ 0,
/* bufferedDurationUs= */ 24_999_000, /* bufferedDurationUs= */ 24_999_000,
/* availableDurationUs= */ C.TIME_UNSET); /* availableDurationUs= */ C.TIME_UNSET,
/* queue= */ Collections.emptyList(),
/* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS);
// When bandwidth estimation is updated to 500L, we should switch down to use a lower bitrate // When bandwidth estimation is updated to 500L, we should switch down to use a lower bitrate
// format. When we don't have enough buffer at higher quality (24_999_000 us is smaller than // format. When we don't have enough buffer at higher quality (24_999_000 us is smaller than
......
...@@ -264,7 +264,37 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -264,7 +264,37 @@ public class DefaultDashChunkSource implements DashChunkSource {
return; return;
} }
trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs); long nowUnixTimeUs = getNowUnixTimeUs();
MediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1);
MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()];
for (int i = 0; i < chunkIterators.length; i++) {
RepresentationHolder representationHolder = representationHolders[i];
if (representationHolder.segmentIndex == null) {
chunkIterators[i] = MediaChunkIterator.EMPTY;
} else {
long firstAvailableSegmentNum =
representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);
long lastAvailableSegmentNum =
representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);
long segmentNum =
getSegmentNum(
representationHolder,
previous,
loadPositionUs,
firstAvailableSegmentNum,
lastAvailableSegmentNum);
if (segmentNum < firstAvailableSegmentNum) {
chunkIterators[i] = MediaChunkIterator.EMPTY;
} else {
chunkIterators[i] =
new RepresentationSegmentIterator(
representationHolder, segmentNum, lastAvailableSegmentNum);
}
}
}
trackSelection.updateSelectedTrack(
playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, chunkIterators);
RepresentationHolder representationHolder = RepresentationHolder representationHolder =
representationHolders[trackSelection.getSelectedIndex()]; representationHolders[trackSelection.getSelectedIndex()];
...@@ -288,48 +318,31 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -288,48 +318,31 @@ public class DefaultDashChunkSource implements DashChunkSource {
} }
} }
int availableSegmentCount = representationHolder.getSegmentCount(); if (representationHolder.getSegmentCount() == 0) {
if (availableSegmentCount == 0) {
// The index doesn't define any segments. // The index doesn't define any segments.
out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1); out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1);
return; return;
} }
long firstAvailableSegmentNum = representationHolder.getFirstSegmentNum(); long firstAvailableSegmentNum =
long lastAvailableSegmentNum; representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);
if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { long lastAvailableSegmentNum =
// The index is itself unbounded. We need to use the current time to calculate the range of representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs);
// available segments.
long liveEdgeTimeUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTimeMs);
long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs);
long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs;
if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) {
long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs);
firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum,
representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs));
}
// 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); updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum);
long segmentNum; long segmentNum =
if (queue.isEmpty()) { getSegmentNum(
segmentNum = Util.constrainValue(representationHolder.getSegmentNum(loadPositionUs), representationHolder,
firstAvailableSegmentNum, lastAvailableSegmentNum); previous,
} else { loadPositionUs,
segmentNum = queue.get(queue.size() - 1).getNextChunkIndex(); firstAvailableSegmentNum,
if (segmentNum < firstAvailableSegmentNum) { lastAvailableSegmentNum);
// This is before the first chunk in the current manifest. if (segmentNum < firstAvailableSegmentNum) {
fatalError = new BehindLiveWindowException(); // This is before the first chunk in the current manifest.
return; fatalError = new BehindLiveWindowException();
} return;
} }
if (segmentNum > lastAvailableSegmentNum if (segmentNum > lastAvailableSegmentNum
|| (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) { || (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) {
// This is beyond the last chunk in the current manifest. // This is beyond the last chunk in the current manifest.
...@@ -409,6 +422,20 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -409,6 +422,20 @@ public class DefaultDashChunkSource implements DashChunkSource {
// Internal methods. // Internal methods.
private long getSegmentNum(
RepresentationHolder representationHolder,
@Nullable MediaChunk previousChunk,
long loadPositionUs,
long firstAvailableSegmentNum,
long lastAvailableSegmentNum) {
return previousChunk != null
? previousChunk.getNextChunkIndex()
: Util.constrainValue(
representationHolder.getSegmentNum(loadPositionUs),
firstAvailableSegmentNum,
lastAvailableSegmentNum);
}
private ArrayList<Representation> getRepresentations() { private ArrayList<Representation> getRepresentations() {
List<AdaptationSet> manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets; List<AdaptationSet> manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets;
ArrayList<Representation> representations = new ArrayList<>(); ArrayList<Representation> representations = new ArrayList<>();
...@@ -683,6 +710,38 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -683,6 +710,38 @@ public class DefaultDashChunkSource implements DashChunkSource {
return segmentIndex.getSegmentUrl(segmentNum - segmentNumShift); return segmentIndex.getSegmentUrl(segmentNum - segmentNumShift);
} }
public long getFirstAvailableSegmentNum(
DashManifest manifest, int periodIndex, long nowUnixTimeUs) {
if (getSegmentCount() == DashSegmentIndex.INDEX_UNBOUNDED
&& manifest.timeShiftBufferDepthMs != C.TIME_UNSET) {
// The index is itself unbounded. We need to use the current time to calculate the range of
// available segments.
long liveEdgeTimeUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs);
long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs);
long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs;
long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs);
return Math.max(
getFirstSegmentNum(), getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs));
}
return getFirstSegmentNum();
}
public long getLastAvailableSegmentNum(
DashManifest manifest, int periodIndex, long nowUnixTimeUs) {
int availableSegmentCount = getSegmentCount();
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 = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs);
long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs);
long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs;
// getSegmentNum(liveEdgeTimeInPeriodUs) will not be completed yet, so subtract one to get
// the index of the last completed segment.
return getSegmentNum(liveEdgeTimeInPeriodUs) - 1;
}
return getFirstSegmentNum() + availableSegmentCount - 1;
}
private static boolean mimeTypeIsWebm(String mimeType) { private static boolean mimeTypeIsWebm(String mimeType) {
return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM) return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM)
|| mimeType.startsWith(MimeTypes.APPLICATION_WEBM); || mimeType.startsWith(MimeTypes.APPLICATION_WEBM);
......
...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.source.TrackGroup; ...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator; import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator;
import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.Chunk;
import com.google.android.exoplayer2.source.chunk.DataChunk; import com.google.android.exoplayer2.source.chunk.DataChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
...@@ -250,7 +251,9 @@ import java.util.List; ...@@ -250,7 +251,9 @@ import java.util.List;
} }
// Select the variant. // Select the variant.
trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs); MediaChunkIterator[] mediaChunkIterators = createMediaChunkIterators(previous, loadPositionUs);
trackSelection.updateSelectedTrack(
playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, mediaChunkIterators);
int selectedVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); int selectedVariantIndex = trackSelection.getSelectedIndexInTrackGroup();
boolean switchingVariant = oldVariantIndex != selectedVariantIndex; boolean switchingVariant = oldVariantIndex != selectedVariantIndex;
...@@ -268,42 +271,25 @@ import java.util.List; ...@@ -268,42 +271,25 @@ import java.util.List;
updateLiveEdgeTimeUs(mediaPlaylist); updateLiveEdgeTimeUs(mediaPlaylist);
// Select the chunk. // Select the chunk.
long chunkMediaSequence;
long startOfPlaylistInPeriodUs = long startOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
if (previous == null || switchingVariant) { long chunkMediaSequence =
long endOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs + mediaPlaylist.durationUs; getChunkMediaSequence(
long targetPositionInPeriodUs = previous, switchingVariant, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs);
(previous == null || independentSegments) ? loadPositionUs : previous.startTimeUs; if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
if (!mediaPlaylist.hasEndTag && targetPositionInPeriodUs >= endOfPlaylistInPeriodUs) { if (previous != null && switchingVariant) {
// If the playlist is too old to contain the chunk, we need to refresh it. // We try getting the next chunk without adapting in case that's the reason for falling
chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); // behind the live window.
selectedVariantIndex = oldVariantIndex;
selectedUrl = variants[selectedVariantIndex];
mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl);
startOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
chunkMediaSequence = previous.getNextChunkIndex();
} else { } else {
long targetPositionInPlaylistUs = targetPositionInPeriodUs - startOfPlaylistInPeriodUs; fatalError = new BehindLiveWindowException();
chunkMediaSequence = return;
Util.binarySearchFloor(
mediaPlaylist.segments,
/* value= */ targetPositionInPlaylistUs,
/* inclusive= */ true,
/* stayInBounds= */ !playlistTracker.isLive() || previous == null)
+ mediaPlaylist.mediaSequence;
if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) {
// We try getting the next chunk without adapting in case that's the reason for falling
// behind the live window.
selectedVariantIndex = oldVariantIndex;
selectedUrl = variants[selectedVariantIndex];
mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl);
startOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
chunkMediaSequence = previous.getNextChunkIndex();
}
} }
} else {
chunkMediaSequence = previous.getNextChunkIndex();
}
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
fatalError = new BehindLiveWindowException();
return;
} }
int chunkIndex = (int) (chunkMediaSequence - mediaPlaylist.mediaSequence); int chunkIndex = (int) (chunkMediaSequence - mediaPlaylist.mediaSequence);
...@@ -433,8 +419,70 @@ import java.util.List; ...@@ -433,8 +419,70 @@ import java.util.List;
|| trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs); || trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs);
} }
/**
* Returns list of {@link MediaChunkIterator}s for upcoming media chunks.
*
* @param previous The previous media chunk. May be null.
* @param loadPositionUs The position at which the iterators will start.
* @return Array of {@link MediaChunkIterator}s for each track.
*/
public MediaChunkIterator[] createMediaChunkIterators(
@Nullable HlsMediaChunk previous, long loadPositionUs) {
int oldVariantIndex =
previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat);
MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()];
for (int i = 0; i < chunkIterators.length; i++) {
int variantIndex = trackSelection.getIndexInTrackGroup(i);
HlsUrl variantUrl = variants[variantIndex];
if (!playlistTracker.isSnapshotValid(variantUrl)) {
chunkIterators[i] = MediaChunkIterator.EMPTY;
continue;
}
HlsMediaPlaylist playlist = playlistTracker.getPlaylistSnapshot(variantUrl);
long startOfPlaylistInPeriodUs =
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
boolean switchingVariant = variantIndex != oldVariantIndex;
long chunkMediaSequence =
getChunkMediaSequence(
previous, switchingVariant, playlist, startOfPlaylistInPeriodUs, loadPositionUs);
if (chunkMediaSequence < playlist.mediaSequence) {
chunkIterators[i] = MediaChunkIterator.EMPTY;
continue;
}
int chunkIndex = (int) (chunkMediaSequence - playlist.mediaSequence);
chunkIterators[i] =
new HlsMediaPlaylistSegmentIterator(playlist, startOfPlaylistInPeriodUs, chunkIndex);
}
return chunkIterators;
}
// Private methods. // Private methods.
private long getChunkMediaSequence(
@Nullable HlsMediaChunk previous,
boolean switchingVariant,
HlsMediaPlaylist mediaPlaylist,
long startOfPlaylistInPeriodUs,
long loadPositionUs) {
if (previous == null || switchingVariant) {
long endOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs + mediaPlaylist.durationUs;
long targetPositionInPeriodUs =
(previous == null || independentSegments) ? loadPositionUs : previous.startTimeUs;
if (!mediaPlaylist.hasEndTag && targetPositionInPeriodUs >= endOfPlaylistInPeriodUs) {
// If the playlist is too old to contain the chunk, we need to refresh it.
return mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
}
long targetPositionInPlaylistUs = targetPositionInPeriodUs - startOfPlaylistInPeriodUs;
return Util.binarySearchFloor(
mediaPlaylist.segments,
/* value= */ targetPositionInPlaylistUs,
/* inclusive= */ true,
/* stayInBounds= */ !playlistTracker.isLive() || previous == null)
+ mediaPlaylist.mediaSequence;
}
return previous.getNextChunkIndex();
}
private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { private long resolveTimeToLiveEdgeUs(long playbackPositionUs) {
final boolean resolveTimeToLiveEdgePossible = liveEdgeInPeriodTimeUs != C.TIME_UNSET; final boolean resolveTimeToLiveEdgePossible = liveEdgeInPeriodTimeUs != C.TIME_UNSET;
return resolveTimeToLiveEdgePossible return resolveTimeToLiveEdgePossible
...@@ -498,8 +546,12 @@ import java.util.List; ...@@ -498,8 +546,12 @@ import java.util.List;
} }
@Override @Override
public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, public void updateSelectedTrack(
long availableDurationUs) { long playbackPositionUs,
long bufferedDurationUs,
long availableDurationUs,
List<? extends MediaChunk> queue,
MediaChunkIterator[] mediaChunkIterators) {
long nowMs = SystemClock.elapsedRealtime(); long nowMs = SystemClock.elapsedRealtime();
if (!isBlacklisted(selectedIndex, nowMs)) { if (!isBlacklisted(selectedIndex, nowMs)) {
return; return;
......
...@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.source.SequenceableLoader; ...@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.source.SequenceableLoader;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.Chunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
...@@ -324,8 +325,16 @@ import java.util.List; ...@@ -324,8 +325,16 @@ import java.util.List;
boolean primarySampleQueueDirty = false; boolean primarySampleQueueDirty = false;
if (!seenFirstTrackSelection) { if (!seenFirstTrackSelection) {
long bufferedDurationUs = positionUs < 0 ? -positionUs : 0; long bufferedDurationUs = positionUs < 0 ? -positionUs : 0;
primaryTrackSelection.updateSelectedTrack(positionUs, bufferedDurationUs, C.TIME_UNSET); HlsMediaChunk lastMediaChunk = getLastMediaChunk();
int chunkIndex = chunkSource.getTrackGroup().indexOf(getLastMediaChunk().trackFormat); MediaChunkIterator[] mediaChunkIterators =
chunkSource.createMediaChunkIterators(lastMediaChunk, positionUs);
primaryTrackSelection.updateSelectedTrack(
positionUs,
bufferedDurationUs,
C.TIME_UNSET,
readOnlyMediaChunks,
mediaChunkIterators);
int chunkIndex = chunkSource.getTrackGroup().indexOf(lastMediaChunk.trackFormat);
if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) {
// This is the first selection and the chunk loaded during preparation does not match // This is the first selection and the chunk loaded during preparation does not match
// the initially selected format. // the initially selected format.
......
...@@ -213,7 +213,14 @@ public class DefaultSsChunkSource implements SsChunkSource { ...@@ -213,7 +213,14 @@ public class DefaultSsChunkSource implements SsChunkSource {
long bufferedDurationUs = loadPositionUs - playbackPositionUs; long bufferedDurationUs = loadPositionUs - playbackPositionUs;
long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);
trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs);
MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()];
for (int i = 0; i < chunkIterators.length; i++) {
int trackIndex = trackSelection.getIndexInTrackGroup(i);
chunkIterators[i] = new StreamElementIterator(streamElement, trackIndex, chunkIndex);
}
trackSelection.updateSelectedTrack(
playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, chunkIterators);
long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex);
long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex);
......
...@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.source.chunk.Chunk; ...@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.source.chunk.Chunk;
import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.source.chunk.ChunkHolder;
import com.google.android.exoplayer2.source.chunk.ChunkSource; import com.google.android.exoplayer2.source.chunk.ChunkSource;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk;
import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment; import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
...@@ -109,11 +110,17 @@ public final class FakeChunkSource implements ChunkSource { ...@@ -109,11 +110,17 @@ public final class FakeChunkSource implements ChunkSource {
List<? extends MediaChunk> queue, List<? extends MediaChunk> queue,
ChunkHolder out) { ChunkHolder out) {
long bufferedDurationUs = loadPositionUs - playbackPositionUs; long bufferedDurationUs = loadPositionUs - playbackPositionUs;
trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, C.TIME_UNSET);
int chunkIndex = int chunkIndex =
queue.isEmpty() queue.isEmpty()
? dataSet.getChunkIndexByPosition(playbackPositionUs) ? dataSet.getChunkIndexByPosition(playbackPositionUs)
: (int) queue.get(queue.size() - 1).getNextChunkIndex(); : (int) queue.get(queue.size() - 1).getNextChunkIndex();
MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()];
for (int i = 0; i < chunkIterators.length; i++) {
int trackGroupIndex = trackSelection.getIndexInTrackGroup(i);
chunkIterators[i] = new FakeAdaptiveDataSet.Iterator(dataSet, trackGroupIndex, chunkIndex);
}
trackSelection.updateSelectedTrack(
playbackPositionUs, bufferedDurationUs, C.TIME_UNSET, queue, chunkIterators);
if (chunkIndex >= dataSet.getChunkCount()) { if (chunkIndex >= dataSet.getChunkCount()) {
out.endOfStream = true; out.endOfStream = true;
} else { } else {
......
...@@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C; ...@@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import java.util.List; import java.util.List;
...@@ -119,7 +120,11 @@ public final class FakeTrackSelection implements TrackSelection { ...@@ -119,7 +120,11 @@ public final class FakeTrackSelection implements TrackSelection {
@Override @Override
public void updateSelectedTrack( public void updateSelectedTrack(
long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) { long playbackPositionUs,
long bufferedDurationUs,
long availableDurationUs,
List<? extends MediaChunk> queue,
MediaChunkIterator[] mediaChunkIterators) {
assertThat(isEnabled).isTrue(); assertThat(isEnabled).isTrue();
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment