diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 80e3daad41..6f73258fc1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -29,7 +29,6 @@ import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.Chunk; -import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; import com.google.android.exoplayer2.source.chunk.DataChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; @@ -49,9 +48,44 @@ import java.util.Arrays; import java.util.Locale; /** - * A temporary test source of HLS chunks. + * Source of Hls(possibly adaptive) chunks. */ -public class HlsChunkSource { +/* package */ class HlsChunkSource { + + /** + * Chunk holder that allows the scheduling of retries. + */ + public static final class HlsChunkHolder { + + public HlsChunkHolder() { + clear(); + } + + /** + * The chunk. + */ + public Chunk chunk; + + /** + * Indicates that the end of the stream has been reached. + */ + public boolean endOfStream; + + /** + * Milliseconds to wait before retrying. + */ + public long retryInMs; + + /** + * Clears the holder. + */ + public void clear() { + chunk = null; + endOfStream = false; + retryInMs = C.TIME_UNSET; + } + + } /** * The default time for which a media playlist should be blacklisted. @@ -173,9 +207,10 @@ public class HlsChunkSource { /** * Returns the next chunk to load. *

- * If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has - * been reached then {@link ChunkHolder#endOfStream} is set. If a chunk is not available but the - * end of the stream has not been reached, the {@link ChunkHolder} is not modified. + * If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream has + * been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available but + * the end of the stream has not been reached, {@link HlsChunkHolder#retryInMs} is set to contain + * the amount of milliseconds to wait before retrying. * * @param previous The most recently loaded media chunk. * @param playbackPositionUs The current playback position. If {@code previous} is null then this @@ -183,7 +218,7 @@ public class HlsChunkSource { * should be interpreted as a seek position. * @param out A holder to populate. */ - public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, ChunkHolder out) { + public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) { int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); @@ -206,8 +241,13 @@ public class HlsChunkSource { int chunkMediaSequence; if (live) { if (previous == null) { - chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, - true, true) + mediaPlaylist.mediaSequence; + // When playling a live stream, the starting chunk will be the third counting from the live + // edge. + chunkMediaSequence = Math.max(0, mediaPlaylist.segments.size() - 3) + + mediaPlaylist.mediaSequence; + // TODO: Bring this back for live window seeking. + // chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, + // true, true) + mediaPlaylist.mediaSequence; } else { chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex, newVariantIndex); @@ -233,9 +273,16 @@ public class HlsChunkSource { if (chunkIndex >= mediaPlaylist.segments.size()) { if (!mediaPlaylist.live) { out.endOfStream = true; - } else if (shouldRerequestLiveMediaPlaylist(newVariantIndex)) { - out.chunk = newMediaPlaylistChunk(newVariantIndex, - trackSelection.getSelectionReason(), trackSelection.getSelectionData()); + } else /* Live */ { + long msToRerequestLiveMediaPlaylist = msToRerequestLiveMediaPlaylist(newVariantIndex); + if (msToRerequestLiveMediaPlaylist <= 0) { + out.chunk = newMediaPlaylistChunk(newVariantIndex, + trackSelection.getSelectionReason(), trackSelection.getSelectionData()); + } else { + // 10 milliseconds are added to the wait to make sure the playlist is refreshed when + // getNextChunk() is called. + out.retryInMs = msToRerequestLiveMediaPlaylist + 10; + } } return; } @@ -417,12 +464,12 @@ public class HlsChunkSource { // Private methods. - private boolean shouldRerequestLiveMediaPlaylist(int variantIndex) { + private long msToRerequestLiveMediaPlaylist(int variantIndex) { HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex]; long timeSinceLastMediaPlaylistLoadMs = SystemClock.elapsedRealtime() - variantLastPlaylistLoadTimesMs[variantIndex]; // Don't re-request media playlist more often than one-half of the target duration. - return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2; + return (mediaPlaylist.targetDurationSecs * 1000) / 2 - timeSinceLastMediaPlaylistLoadMs; } private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex, int trackSelectionReason, diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 73876a1a8c..071d1f94ab 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; +import android.os.Handler; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -60,6 +61,7 @@ import java.util.List; private final IdentityHashMap streamWrapperIndices; private final PtsTimestampAdjusterProvider timestampAdjusterProvider; private final HlsPlaylistParser manifestParser; + private final Handler continueLoadingHandler; private final Loader manifestFetcher; private final long preparePositionUs; @@ -72,10 +74,11 @@ import java.util.List; private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private CompositeSequenceableLoader sequenceableLoader; + private Runnable continueLoadingRunnable; public HlsMediaPeriod(Uri manifestUri, DataSource.Factory dataSourceFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, - MediaSource.Listener sourceListener, Callback callback, Allocator allocator, + MediaSource.Listener sourceListener, final Callback callback, Allocator allocator, long positionUs) { this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; @@ -86,8 +89,15 @@ import java.util.List; streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); manifestParser = new HlsPlaylistParser(); + continueLoadingHandler = new Handler(); manifestFetcher = new Loader("Loader:ManifestFetcher"); preparePositionUs = positionUs; + continueLoadingRunnable = new Runnable() { + @Override + public void run() { + callback.onContinueLoadingRequested(HlsMediaPeriod.this); + } + }; ParsingLoadable loadable = new ParsingLoadable<>( dataSourceFactory.createDataSource(), manifestUri, C.DATA_TYPE_MANIFEST, manifestParser); @@ -96,6 +106,7 @@ import java.util.List; } public void release() { + continueLoadingHandler.removeCallbacksAndMessages(null); manifestFetcher.release(); for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { sampleStreamWrapper.release(); @@ -286,6 +297,12 @@ import java.util.List; sourceListener.onSourceInfoRefreshed(timeline, playlist); } + @Override + public void onContinueLoadingRequiredInMs(final HlsSampleStreamWrapper sampleStreamWrapper, + long delayMs) { + continueLoadingHandler.postDelayed(continueLoadingRunnable, delayMs); + } + @Override public void onContinueLoadingRequested(HlsSampleStreamWrapper sampleStreamWrapper) { if (trackGroups == null) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index cc9b2ecc50..4d7f026498 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -30,7 +30,6 @@ import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.Chunk; -import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Loader; @@ -56,6 +55,12 @@ import java.util.LinkedList; */ void onPrepared(); + /** + * Called to schedule a {@link #continueLoading(long)} call. + */ + void onContinueLoadingRequiredInMs(HlsSampleStreamWrapper sampleStreamSource, + long delayMs); + } private static final int PRIMARY_TYPE_NONE = 0; @@ -72,7 +77,7 @@ import java.util.LinkedList; private final int minLoadableRetryCount; private final Loader loader; private final EventDispatcher eventDispatcher; - private final ChunkHolder nextChunkHolder; + private final HlsChunkSource.HlsChunkHolder nextChunkHolder; private final SparseArray sampleQueues; private final LinkedList mediaChunks; @@ -121,7 +126,7 @@ import java.util.LinkedList; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; loader = new Loader("Loader:HlsSampleStreamWrapper"); - nextChunkHolder = new ChunkHolder(); + nextChunkHolder = new HlsChunkSource.HlsChunkHolder(); sampleQueues = new SparseArray<>(); mediaChunks = new LinkedList<>(); lastSeekPositionUs = positionUs; @@ -310,6 +315,7 @@ import java.util.LinkedList; nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; Chunk loadable = nextChunkHolder.chunk; + long retryInMs = nextChunkHolder.retryInMs; nextChunkHolder.clear(); if (endOfStream) { @@ -318,6 +324,8 @@ import java.util.LinkedList; } if (loadable == null) { + Assertions.checkState(retryInMs != C.TIME_UNSET && chunkSource.isLive()); + callback.onContinueLoadingRequiredInMs(this, retryInMs); return false; }