diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index cd44a283a24..ac99760d875 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -74,7 +74,7 @@ public void testParseMediaPlaylist() { assertEquals(2679, mediaPlaylist.mediaSequence); assertEquals(8, mediaPlaylist.targetDurationSecs); assertEquals(3, mediaPlaylist.version); - assertEquals(false, mediaPlaylist.live); + assertEquals(true, mediaPlaylist.hasEndTag); List segments = mediaPlaylist.segments; assertNotNull(segments); assertEquals(5, segments.size()); 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 7ef16f361d5..351583a3348 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 @@ -36,7 +36,7 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.trackselection.BaseTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; @@ -44,7 +44,6 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigInteger; import java.util.Arrays; @@ -65,7 +64,7 @@ public HlsChunkHolder() { } /** - * The chunk. + * The chunk to be loaded next. */ public Chunk chunk; @@ -75,9 +74,9 @@ public HlsChunkHolder() { public boolean endOfStream; /** - * Milliseconds to wait before retrying. + * Indicates that the chunk source is waiting for the referred playlist to be refreshed. */ - public long retryInMs; + public HlsMasterPlaylist.HlsUrl playlist; /** * Clears the holder. @@ -85,20 +84,11 @@ public HlsChunkHolder() { public void clear() { chunk = null; endOfStream = false; - retryInMs = C.TIME_UNSET; + playlist = null; } } - /** - * The default time for which a media playlist should be blacklisted. - */ - public static final long DEFAULT_PLAYLIST_BLACKLIST_MS = 60000; - /** - * Subtracted value to lookup position when switching between variants in live streams to avoid - * gaps in playback in case playlist drift apart. - */ - private static final double LIVE_VARIANT_SWITCH_SAFETY_EXTRA_SECS = 2.0; private static final String AAC_FILE_EXTENSION = ".aac"; private static final String AC3_FILE_EXTENSION = ".ac3"; private static final String EC3_FILE_EXTENSION = ".ec3"; @@ -107,18 +97,13 @@ public void clear() { private static final String VTT_FILE_EXTENSION = ".vtt"; private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; - private final String baseUri; private final DataSource dataSource; - private final HlsPlaylistParser playlistParser; private final TimestampAdjusterProvider timestampAdjusterProvider; private final HlsMasterPlaylist.HlsUrl[] variants; - private final HlsMediaPlaylist[] variantPlaylists; + private final HlsPlaylistTracker playlistTracker; private final TrackGroup trackGroup; - private final long[] variantLastPlaylistLoadTimesMs; private byte[] scratchSpace; - private boolean live; - private long durationUs; private IOException fatalError; private HlsInitializationChunk lastLoadedInitializationChunk; @@ -133,22 +118,19 @@ public void clear() { private TrackSelection trackSelection; /** - * @param baseUri The playlist's base uri. + * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. * @param variants The available variants. * @param dataSource A {@link DataSource} suitable for loading the media data. * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. */ - public HlsChunkSource(String baseUri, HlsMasterPlaylist.HlsUrl[] variants, DataSource dataSource, - TimestampAdjusterProvider timestampAdjusterProvider) { - this.baseUri = baseUri; + public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsMasterPlaylist.HlsUrl[] variants, + DataSource dataSource, TimestampAdjusterProvider timestampAdjusterProvider) { + this.playlistTracker = playlistTracker; this.variants = variants; this.dataSource = dataSource; this.timestampAdjusterProvider = timestampAdjusterProvider; - playlistParser = new HlsPlaylistParser(); - variantPlaylists = new HlsMediaPlaylist[variants.length]; - variantLastPlaylistLoadTimesMs = new long[variants.length]; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; @@ -172,20 +154,6 @@ public void maybeThrowError() throws IOException { } } - /** - * Returns whether this is a live playback. - */ - public boolean isLive() { - return live; - } - - /** - * Returns the duration of the source, or {@link C#TIME_UNSET} if the duration is unknown. - */ - public long getDurationUs() { - return durationUs; - } - /** * Returns the track group exposed by the source. */ @@ -214,8 +182,8 @@ public void reset() { *

* 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. + * the end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to + * contain the {@link HlsMasterPlaylist.HlsUrl} that refers to the playlist that needs refreshing. * * @param previous The most recently loaded media chunk. * @param playbackPositionUs The current playback position. If {@code previous} is null then this @@ -226,7 +194,6 @@ public void reset() { public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) { int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); - // Use start time of the previous chunk rather than its end time because switching format will // require downloading overlapping segments. long bufferedDurationUs = previous == null ? 0 @@ -235,67 +202,44 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu int newVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); boolean switchingVariant = oldVariantIndex != newVariantIndex; - HlsMediaPlaylist mediaPlaylist = variantPlaylists[newVariantIndex]; + HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(variants[newVariantIndex]); if (mediaPlaylist == null) { - // We don't have the media playlist for the next variant. Request it now. - out.chunk = newMediaPlaylistChunk(newVariantIndex, trackSelection.getSelectionReason(), - trackSelection.getSelectionData()); + out.playlist = variants[newVariantIndex]; + // Retry when playlist is refreshed. return; } int chunkMediaSequence; - if (live) { - if (previous == null) { - // When playing 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; + if (previous == null || switchingVariant) { + long targetPositionUs = previous == null ? playbackPositionUs : previous.startTimeUs; + if (targetPositionUs > mediaPlaylist.getEndTimeUs()) { + // If the playlist is too old to contain the chunk, we need to refresh it. + chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { - chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex, - newVariantIndex); - if (chunkMediaSequence < mediaPlaylist.mediaSequence) { + chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, targetPositionUs, true, + !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. newVariantIndex = oldVariantIndex; - mediaPlaylist = variantPlaylists[newVariantIndex]; - chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex, - newVariantIndex); - if (chunkMediaSequence < mediaPlaylist.mediaSequence) { - fatalError = new BehindLiveWindowException(); - return; - } + mediaPlaylist = playlistTracker.getPlaylistSnapshot(variants[newVariantIndex]); + chunkMediaSequence = previous.getNextChunkIndex(); } } } else { - // Not live. - if (previous == null) { - chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, - true, true) + mediaPlaylist.mediaSequence; - } else if (switchingVariant) { - chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, - previous.startTimeUs, true, true) + mediaPlaylist.mediaSequence; - } else { - chunkMediaSequence = previous.getNextChunkIndex(); - } + chunkMediaSequence = previous.getNextChunkIndex(); + } + if (chunkMediaSequence < mediaPlaylist.mediaSequence) { + fatalError = new BehindLiveWindowException(); + return; } int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; if (chunkIndex >= mediaPlaylist.segments.size()) { - if (!mediaPlaylist.live) { + if (mediaPlaylist.hasEndTag) { out.endOfStream = true; } 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; - } + out.playlist = variants[newVariantIndex]; } return; } @@ -319,17 +263,9 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu } // Compute start and end times, and the sequence number of the next chunk. - long startTimeUs; - if (live) { - if (previous == null) { - startTimeUs = 0; - } else if (switchingVariant) { - startTimeUs = previous.getAdjustedStartTimeUs(); - } else { - startTimeUs = previous.getAdjustedEndTimeUs(); - } - } else /* Not live */ { - startTimeUs = segment.startTimeUs; + long startTimeUs = segment.startTimeUs; + if (previous != null && !switchingVariant) { + startTimeUs = previous.getAdjustedEndTimeUs(); } long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND); Format format = variants[newVariantIndex].format; @@ -424,52 +360,6 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu encryptionKey, encryptionIv); } - /** - * Returns the media sequence number of a chunk in a new variant for a live stream variant switch. - * - * @param previousChunkIndex The index of the last chunk in the old variant. - * @param oldVariantIndex The index of the old variant. - * @param newVariantIndex The index of the new variant. - * @return Media sequence number of the chunk to switch to in a live stream in the variant that - * corresponds to the given {@code newVariantIndex}. - */ - private int getLiveNextChunkSequenceNumber(int previousChunkIndex, int oldVariantIndex, - int newVariantIndex) { - if (oldVariantIndex == newVariantIndex) { - return previousChunkIndex + 1; - } - HlsMediaPlaylist oldMediaPlaylist = variantPlaylists[oldVariantIndex]; - HlsMediaPlaylist newMediaPlaylist = variantPlaylists[newVariantIndex]; - if (previousChunkIndex < oldMediaPlaylist.mediaSequence) { - // We have fallen behind the live window. - return newMediaPlaylist.mediaSequence - 1; - } - double offsetToLiveInstantSecs = 0; - for (int i = previousChunkIndex - oldMediaPlaylist.mediaSequence; - i < oldMediaPlaylist.segments.size(); i++) { - offsetToLiveInstantSecs += oldMediaPlaylist.segments.get(i).durationSecs; - } - long currentTimeMs = SystemClock.elapsedRealtime(); - offsetToLiveInstantSecs += - (double) (currentTimeMs - variantLastPlaylistLoadTimesMs[oldVariantIndex]) / 1000; - offsetToLiveInstantSecs += LIVE_VARIANT_SWITCH_SAFETY_EXTRA_SECS; - offsetToLiveInstantSecs -= - (double) (currentTimeMs - variantLastPlaylistLoadTimesMs[newVariantIndex]) / 1000; - if (offsetToLiveInstantSecs < 0) { - // The instant we are looking for is not contained in the playlist, we need it to be - // refreshed. - return newMediaPlaylist.mediaSequence + newMediaPlaylist.segments.size() + 1; - } - for (int i = newMediaPlaylist.segments.size() - 1; i >= 0; i--) { - offsetToLiveInstantSecs -= newMediaPlaylist.segments.get(i).durationSecs; - if (offsetToLiveInstantSecs < 0) { - return newMediaPlaylist.mediaSequence + i; - } - } - // We have fallen behind the live window. - return newMediaPlaylist.mediaSequence - 1; - } - /** * Called when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this * source. @@ -479,10 +369,6 @@ private int getLiveNextChunkSequenceNumber(int previousChunkIndex, int oldVarian public void onChunkLoadCompleted(Chunk chunk) { if (chunk instanceof HlsInitializationChunk) { lastLoadedInitializationChunk = (HlsInitializationChunk) chunk; - } else if (chunk instanceof MediaPlaylistChunk) { - MediaPlaylistChunk mediaPlaylistChunk = (MediaPlaylistChunk) chunk; - scratchSpace = mediaPlaylistChunk.getDataHolder(); - setMediaPlaylist(mediaPlaylistChunk.variantIndex, mediaPlaylistChunk.getResult()); } else if (chunk instanceof EncryptionKeyChunk) { EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk; scratchSpace = encryptionKeyChunk.getDataHolder(); @@ -519,24 +405,6 @@ private HlsInitializationChunk buildInitializationChunk(HlsMediaPlaylist mediaPl format); } - 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 (mediaPlaylist.targetDurationSecs * 1000) / 2 - timeSinceLastMediaPlaylistLoadMs; - } - - private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex, int trackSelectionReason, - Object trackSelectionData) { - Uri mediaPlaylistUri = UriUtil.resolveToUri(baseUri, variants[variantIndex].url); - DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNSET, null, - DataSpec.FLAG_ALLOW_GZIP); - return new MediaPlaylistChunk(dataSource, dataSpec, variants[variantIndex].format, - trackSelectionReason, trackSelectionData, scratchSpace, playlistParser, variantIndex, - mediaPlaylistUri); - } - 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); @@ -571,13 +439,6 @@ private void clearEncryptionData() { encryptionIv = null; } - private void setMediaPlaylist(int variantIndex, HlsMediaPlaylist mediaPlaylist) { - variantLastPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime(); - variantPlaylists[variantIndex] = mediaPlaylist; - live |= mediaPlaylist.live; - durationUs = live ? C.TIME_UNSET : mediaPlaylist.durationUs; - } - // Private classes. /** @@ -626,38 +487,6 @@ public Object getSelectionData() { } - private static final class MediaPlaylistChunk extends DataChunk { - - public final int variantIndex; - - private final HlsPlaylistParser playlistParser; - private final Uri playlistUri; - - private HlsMediaPlaylist result; - - public MediaPlaylistChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, byte[] scratchSpace, - HlsPlaylistParser playlistParser, int variantIndex, - Uri playlistUri) { - super(dataSource, dataSpec, C.DATA_TYPE_MANIFEST, trackFormat, trackSelectionReason, - trackSelectionData, scratchSpace); - this.variantIndex = variantIndex; - this.playlistParser = playlistParser; - this.playlistUri = playlistUri; - } - - @Override - protected void consume(byte[] data, int limit) throws IOException { - result = (HlsMediaPlaylist) playlistParser.parse(playlistUri, - new ByteArrayInputStream(data, 0, limit)); - } - - public HlsMediaPlaylist getResult() { - return result; - } - - } - private static final class EncryptionKeyChunk extends DataChunk { public final String iv; 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 598fa9b281b..0c27b3df7d8 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 @@ -15,30 +15,22 @@ */ 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; -import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SampleStream; -import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.Loader; -import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; @@ -48,55 +40,41 @@ /** * A {@link MediaPeriod} that loads an HLS stream. */ -/* package */ final class HlsMediaPeriod implements MediaPeriod, - Loader.Callback>, HlsSampleStreamWrapper.Callback { +public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback, + HlsPlaylistTracker.PlaylistRefreshCallback { - private final Uri manifestUri; + private final HlsPlaylistTracker playlistTracker; private final DataSource.Factory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; - private final MediaSource.Listener sourceListener; private final Allocator allocator; private final IdentityHashMap streamWrapperIndices; private final TimestampAdjusterProvider timestampAdjusterProvider; - private final HlsPlaylistParser manifestParser; private final Handler continueLoadingHandler; private final Loader manifestFetcher; private final long preparePositionUs; - private final Runnable continueLoadingRunnable; private Callback callback; private int pendingPrepareCount; - private HlsPlaylist playlist; private boolean seenFirstTrackSelection; - private boolean isLive; private TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private CompositeSequenceableLoader sequenceableLoader; - public HlsMediaPeriod(Uri manifestUri, DataSource.Factory dataSourceFactory, - int minLoadableRetryCount, EventDispatcher eventDispatcher, - MediaSource.Listener sourceListener, Allocator allocator, + public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, DataSource.Factory dataSourceFactory, + int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator, long positionUs) { - this.manifestUri = manifestUri; + this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; - this.sourceListener = sourceListener; this.allocator = allocator; streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new TimestampAdjusterProvider(); - manifestParser = new HlsPlaylistParser(); continueLoadingHandler = new Handler(); manifestFetcher = new Loader("Loader:ManifestFetcher"); preparePositionUs = positionUs; - continueLoadingRunnable = new Runnable() { - @Override - public void run() { - callback.onContinueLoadingRequested(HlsMediaPeriod.this); - } - }; } public void release() { @@ -112,10 +90,7 @@ public void release() { @Override public void prepare(Callback callback) { this.callback = callback; - ParsingLoadable loadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(), manifestUri, C.DATA_TYPE_MANIFEST, manifestParser); - long elapsedRealtimeMs = manifestFetcher.startLoading(loadable, this, minLoadableRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); + buildAndPrepareSampleStreamWrappers(); } @Override @@ -234,8 +209,6 @@ public long getBufferedPositionUs() { @Override public long seekToUs(long positionUs) { - // Treat all seeks into non-seekable media as being to t=0. - positionUs = isLive ? 0 : positionUs; timestampAdjusterProvider.reset(); for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { sampleStreamWrapper.seekTo(positionUs); @@ -243,33 +216,6 @@ public long seekToUs(long positionUs) { return positionUs; } - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - playlist = loadable.getResult(); - buildAndPrepareSampleStreamWrappers(); - } - - @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { - boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs, - loadable.bytesLoaded(), error, isFatal); - return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; - } - // HlsSampleStreamWrapper.Callback implementation. @Override @@ -278,10 +224,6 @@ public void onPrepared() { return; } - // The wrapper at index 0 is the one of type TRACK_TYPE_DEFAULT. - long durationUs = sampleStreamWrappers[0].getDurationUs(); - isLive = sampleStreamWrappers[0].isLive(); - int totalTrackGroupCount = 0; for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { totalTrackGroupCount += sampleStreamWrapper.getTrackGroups().length; @@ -296,16 +238,11 @@ public void onPrepared() { } trackGroups = new TrackGroupArray(trackGroupArray); callback.onPrepared(this); - - // TODO[playlists]: Calculate the window. - Timeline timeline = new SinglePeriodTimeline(durationUs, durationUs, 0, 0, !isLive, isLive); - sourceListener.onSourceInfoRefreshed(timeline, playlist); } @Override - public void onContinueLoadingRequiredInMs(final HlsSampleStreamWrapper sampleStreamWrapper, - long delayMs) { - continueLoadingHandler.postDelayed(continueLoadingRunnable, delayMs); + public void onPlaylistRefreshRequired(HlsMasterPlaylist.HlsUrl url) { + playlistTracker.refreshPlaylist(url, this); } @Override @@ -317,22 +254,24 @@ public void onContinueLoadingRequested(HlsSampleStreamWrapper sampleStreamWrappe callback.onContinueLoadingRequested(this); } - // Internal methods. + // PlaylistListener implementation. - private void buildAndPrepareSampleStreamWrappers() { - String baseUri = playlist.baseUri; - if (playlist instanceof HlsMediaPlaylist) { - HlsMasterPlaylist.HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[] { - HlsMasterPlaylist.HlsUrl.createMediaPlaylistHlsUrl(playlist.baseUri)}; - sampleStreamWrappers = new HlsSampleStreamWrapper[] { - buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, null, null)}; - pendingPrepareCount = 1; - sampleStreamWrappers[0].continuePreparing(); - return; + @Override + public void onPlaylistChanged() { + if (trackGroups != null) { + callback.onContinueLoadingRequested(this); + } else { + // Some of the wrappers were waiting for their media playlist to prepare. + for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) { + wrapper.continuePreparing(); + } } + } - HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; + // Internal methods. + private void buildAndPrepareSampleStreamWrappers() { + HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist(); // Build the default stream wrapper. List selectedVariants = new ArrayList<>(masterPlaylist.variants); ArrayList definiteVideoVariants = new ArrayList<>(); @@ -367,7 +306,7 @@ private void buildAndPrepareSampleStreamWrappers() { HlsMasterPlaylist.HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; selectedVariants.toArray(variants); HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, - baseUri, variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); + variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.continuePreparing(); } @@ -375,7 +314,7 @@ private void buildAndPrepareSampleStreamWrappers() { // Build audio stream wrappers. for (int i = 0; i < audioVariants.size(); i++) { HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, - baseUri, new HlsMasterPlaylist.HlsUrl[] {audioVariants.get(i)}, null, null); + new HlsMasterPlaylist.HlsUrl[] {audioVariants.get(i)}, null, null); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.continuePreparing(); } @@ -384,16 +323,16 @@ private void buildAndPrepareSampleStreamWrappers() { for (int i = 0; i < subtitleVariants.size(); i++) { HlsMasterPlaylist.HlsUrl url = subtitleVariants.get(i); HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, - baseUri, new HlsMasterPlaylist.HlsUrl[] {url}, null, null); + new HlsMasterPlaylist.HlsUrl[] {url}, null, null); sampleStreamWrapper.prepareSingleTrack(url.format); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; } } - private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, String baseUri, + private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsMasterPlaylist.HlsUrl[] variants, Format muxedAudioFormat, Format muxedCaptionFormat) { DataSource dataSource = dataSourceFactory.createDataSource(); - HlsChunkSource defaultChunkSource = new HlsChunkSource(baseUri, variants, dataSource, + HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSource, timestampAdjusterProvider); return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount, diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index b8b6c033b37..6fd82df3166 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -23,21 +23,26 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.List; /** * An HLS {@link MediaSource}. */ -public final class HlsMediaSource implements MediaSource { +public final class HlsMediaSource implements MediaSource, + HlsPlaylistTracker.PrimaryPlaylistListener { /** * The default minimum number of times to retry loading data prior to failing. */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - private final Uri manifestUri; + private final HlsPlaylistTracker playlistTracker; private final DataSource.Factory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; @@ -53,29 +58,29 @@ public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Han public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; eventDispatcher = new EventDispatcher(eventHandler, eventListener); + playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, + minLoadableRetryCount, this); } @Override public void prepareSource(MediaSource.Listener listener) { sourceListener = listener; - // TODO: Defer until the playlist has been loaded. - listener.onSourceInfoRefreshed(new SinglePeriodTimeline(C.TIME_UNSET, false), null); + playlistTracker.start(); } @Override - public void maybeThrowSourceInfoRefreshError() { - // Do nothing. + public void maybeThrowSourceInfoRefreshError() throws IOException { + playlistTracker.maybeThrowPrimaryPlaylistRefreshError(); } @Override public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { Assertions.checkArgument(index == 0); - return new HlsMediaPeriod(manifestUri, dataSourceFactory, minLoadableRetryCount, - eventDispatcher, sourceListener, allocator, positionUs); + return new HlsMediaPeriod(playlistTracker, dataSourceFactory, minLoadableRetryCount, + eventDispatcher, allocator, positionUs); } @Override @@ -85,7 +90,26 @@ public void releasePeriod(MediaPeriod mediaPeriod) { @Override public void releaseSource() { + playlistTracker.release(); sourceListener = null; } + @Override + public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { + SinglePeriodTimeline timeline; + if (playlistTracker.isLive()) { + // TODO: fix windowPositionInPeriodUs when playlist is empty. + long windowPositionInPeriodUs = playlist.getStartTimeUs(); + List segments = playlist.segments; + long windowDefaultStartPositionUs = segments.isEmpty() ? 0 + : segments.get(Math.max(0, segments.size() - 3)).startTimeUs - windowPositionInPeriodUs; + timeline = new SinglePeriodTimeline(C.TIME_UNSET, playlist.durationUs, + windowPositionInPeriodUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); + } else /* not live */ { + timeline = new SinglePeriodTimeline(playlist.durationUs, playlist.durationUs, 0, 0, true, + false); + } + sourceListener.onSourceInfoRefreshed(timeline, playlist); + } + } 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 fe756da0ef5..c491dc97606 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 @@ -32,6 +32,7 @@ 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.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Loader; @@ -58,10 +59,10 @@ public interface Callback extends SequenceableLoader.Callback variants, List aud this.muxedCaptionFormat = muxedCaptionFormat; } + public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) { + List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri)); + List emptyList = Collections.emptyList(); + return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 5aa0c8a3d86..177546d3012 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.source.hls.playlist; import com.google.android.exoplayer2.C; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -60,6 +62,12 @@ public Segment(String uri, double durationSecs, int discontinuitySequenceNumber, public int compareTo(Long startTimeUs) { return this.startTimeUs > startTimeUs ? 1 : (this.startTimeUs < startTimeUs ? -1 : 0); } + + public Segment copyWithStartTimeUs(long startTimeUs) { + return new Segment(url, durationSecs, discontinuitySequenceNumber, startTimeUs, isEncrypted, + encryptionKeyUri, encryptionIV, byterangeOffset, byterangeLength); + } + } public static final String ENCRYPTION_METHOD_NONE = "NONE"; @@ -70,25 +78,51 @@ public int compareTo(Long startTimeUs) { public final int version; public final Segment initializationSegment; public final List segments; - public final boolean live; + public final boolean hasEndTag; public final long durationUs; public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version, - boolean live, Segment initializationSegment, List segments) { + boolean hasEndTag, Segment initializationSegment, List segments) { super(baseUri, HlsPlaylist.TYPE_MEDIA); this.mediaSequence = mediaSequence; this.targetDurationSecs = targetDurationSecs; this.version = version; - this.live = live; + this.hasEndTag = hasEndTag; this.initializationSegment = initializationSegment; - this.segments = segments; + this.segments = Collections.unmodifiableList(segments); if (!segments.isEmpty()) { + Segment first = segments.get(0); Segment last = segments.get(segments.size() - 1); - durationUs = last.startTimeUs + (long) (last.durationSecs * C.MICROS_PER_SECOND); + durationUs = last.startTimeUs + (long) (last.durationSecs * C.MICROS_PER_SECOND) + - first.startTimeUs; } else { durationUs = 0; } } + public long getStartTimeUs() { + return segments.isEmpty() ? 0 : segments.get(0).startTimeUs; + } + + public long getEndTimeUs() { + return getStartTimeUs() + durationUs; + } + + public HlsMediaPlaylist copyWithStartTimeUs(long newStartTimeUs) { + long startTimeOffsetUs = newStartTimeUs - getStartTimeUs(); + int segmentsSize = segments.size(); + List newSegments = new ArrayList<>(segmentsSize); + for (int i = 0; i < segmentsSize; i++) { + Segment segment = segments.get(i); + newSegments.add(segment.copyWithStartTimeUs(segment.startTimeUs + startTimeOffsetUs)); + } + return copyWithSegments(newSegments); + } + + public HlsMediaPlaylist copyWithSegments(List segments) { + return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, hasEndTag, + initializationSegment, segments); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 21cc75765fd..76606fad173 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -27,7 +27,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Queue; @@ -214,7 +213,7 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String int mediaSequence = 0; int targetDurationSecs = 0; int version = 1; // Default version == 1. - boolean live = true; + boolean hasEndTag = false; Segment initializationSegment = null; List segments = new ArrayList<>(); @@ -298,11 +297,11 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String } segmentByteRangeLength = C.LENGTH_UNSET; } else if (line.equals(TAG_ENDLIST)) { - live = false; + hasEndTag = true; } } - return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, live, - initializationSegment, Collections.unmodifiableList(segments)); + return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, hasEndTag, + initializationSegment, segments); } private static String parseStringAttr(String line, Pattern pattern) throws ParserException { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java new file mode 100644 index 00000000000..b8c1d96a6bc --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -0,0 +1,403 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls.playlist; + +import android.net.Uri; +import android.os.Handler; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.UriUtil; +import java.io.IOException; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; + +/** + * Tracks playlists linked to a provided playlist url. The provided url might reference an HLS + * master playlist or a media playlist. + */ +public final class HlsPlaylistTracker implements Loader.Callback> { + + /** + * Listener for primary playlist changes. + */ + public interface PrimaryPlaylistListener { + + /** + * Called when the primary playlist changes. + * + * @param mediaPlaylist The primary playlist new snapshot. + */ + void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist); + + } + + /** + * Called when the playlist changes. + */ + public interface PlaylistRefreshCallback { + + /** + * Called when the target playlist changes. + */ + void onPlaylistChanged(); + + } + + /** + * Period for refreshing playlists. + */ + private static final long PLAYLIST_REFRESH_PERIOD_MS = 5000; + + private final Uri initialPlaylistUri; + private final DataSource.Factory dataSourceFactory; + private final HlsPlaylistParser playlistParser; + private final int minRetryCount; + private final IdentityHashMap playlistBundles; + private final Handler playlistRefreshHandler; + private final PrimaryPlaylistListener primaryPlaylistListener; + private final Loader initialPlaylistLoader; + private final EventDispatcher eventDispatcher; + + private HlsMasterPlaylist masterPlaylist; + private HlsUrl primaryHlsUrl; + private boolean isLive; + + /** + * @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media + * playlist or a master playlist. + * @param dataSourceFactory A factory for {@link DataSource} instances. + * @param eventDispatcher A dispatcher to notify of events. + * @param minRetryCount The minimum number of times the load must be retried before blacklisting a + * playlist. + * @param primaryPlaylistListener A callback for the primary playlist change events. + */ + public HlsPlaylistTracker(Uri initialPlaylistUri, DataSource.Factory dataSourceFactory, + EventDispatcher eventDispatcher, int minRetryCount, + PrimaryPlaylistListener primaryPlaylistListener) { + this.initialPlaylistUri = initialPlaylistUri; + this.dataSourceFactory = dataSourceFactory; + this.eventDispatcher = eventDispatcher; + this.minRetryCount = minRetryCount; + this.primaryPlaylistListener = primaryPlaylistListener; + initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); + playlistParser = new HlsPlaylistParser(); + playlistBundles = new IdentityHashMap<>(); + playlistRefreshHandler = new Handler(); + } + + /** + * Starts tracking all the playlists related to the provided Uri. + */ + public void start() { + ParsingLoadable masterPlaylistLoadable = new ParsingLoadable<>( + dataSourceFactory.createDataSource(), initialPlaylistUri, C.DATA_TYPE_MANIFEST, + playlistParser); + initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); + } + + /** + * Returns the master playlist. + * + * @return The master playlist. Null if the initial playlist has yet to be loaded. + */ + public HlsMasterPlaylist getMasterPlaylist() { + return masterPlaylist; + } + + /** + * Gets the most recent snapshot available of the playlist referred by the provided + * {@link HlsUrl}. + * + * @param url The {@link HlsUrl} corresponding to the requested media playlist. + * @return The most recent snapshot of the playlist referred by the provided {@link HlsUrl}. May + * be null if no snapshot has been loaded yet. + */ + public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) { + return playlistBundles.get(url).latestPlaylistSnapshot; + } + + /** + * Releases the playlist tracker. + */ + public void release() { + initialPlaylistLoader.release(); + for (MediaPlaylistBundle bundle : playlistBundles.values()) { + bundle.release(); + } + playlistRefreshHandler.removeCallbacksAndMessages(null); + playlistBundles.clear(); + } + + /** + * If the tracker is having trouble refreshing the primary playlist, this method throws the + * underlying error. Otherwise, does nothing. + * + * @throws IOException The underlying error. + */ + public void maybeThrowPrimaryPlaylistRefreshError() throws IOException { + initialPlaylistLoader.maybeThrowError(); + if (primaryHlsUrl != null) { + playlistBundles.get(primaryHlsUrl).mediaPlaylistLoader.maybeThrowError(); + } + } + + /** + * Triggers a playlist refresh and sets the callback to be called once the playlist referred by + * the provided {@link HlsUrl} changes. + * + * @param key The {@link HlsUrl} of the playlist to be refreshed. + * @param callback The callback. + */ + public void refreshPlaylist(HlsUrl key, PlaylistRefreshCallback callback) { + MediaPlaylistBundle bundle = playlistBundles.get(key); + bundle.setCallback(callback); + bundle.loadPlaylist(); + } + + /** + * Returns whether this is live content. + * + * @return True if the content is live. False otherwise. + */ + public boolean isLive() { + return isLive; + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs) { + HlsPlaylist result = loadable.getResult(); + HlsMasterPlaylist masterPlaylist; + boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; + if (isMediaPlaylist) { + masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); + } else /* result instanceof HlsMasterPlaylist */ { + masterPlaylist = (HlsMasterPlaylist) result; + } + this.masterPlaylist = masterPlaylist; + primaryHlsUrl = masterPlaylist.variants.get(0); + ArrayList urls = new ArrayList<>(); + urls.addAll(masterPlaylist.variants); + urls.addAll(masterPlaylist.audios); + urls.addAll(masterPlaylist.subtitles); + createBundles(urls); + MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl); + if (isMediaPlaylist) { + // We don't need to load the playlist again. We can use the same result. + primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result); + } else { + primaryBundle.loadPlaylist(); + } + eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + } + + @Override + public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + } + + @Override + public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + boolean isFatal = error instanceof ParserException; + eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded(), error, isFatal); + return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; + } + + // Internal methods. + + private void createBundles(List urls) { + int listSize = urls.size(); + for (int i = 0; i < listSize; i++) { + HlsUrl url = urls.get(i); + MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); + playlistBundles.put(urls.get(i), bundle); + } + } + + /** + * Called by the bundles when a snapshot changes. + * + * @param url The url of the playlist. + * @param newSnapshot The new snapshot. + * @param isFirstSnapshot Whether this is the first snapshot for the given playlist. + * @return True if a refresh should be scheduled. + */ + private boolean onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot, + boolean isFirstSnapshot) { + if (url == primaryHlsUrl) { + if (isFirstSnapshot) { + isLive = !newSnapshot.hasEndTag; + } + primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); + // If the primary playlist is not the final one, we should schedule a refresh. + return !newSnapshot.hasEndTag; + } + return false; + } + + /** + * TODO: Allow chunks to feed adjusted timestamps back to the playlist tracker. + * TODO: Track discontinuities for media playlists that don't include the discontinuity number. + */ + private HlsMediaPlaylist adjustPlaylistTimestamps(HlsMediaPlaylist oldPlaylist, + HlsMediaPlaylist newPlaylist) { + HlsMediaPlaylist primaryPlaylistSnapshot = + playlistBundles.get(primaryHlsUrl).latestPlaylistSnapshot; + if (oldPlaylist == null) { + if (primaryPlaylistSnapshot == null) { + // Playback has just started so no adjustment is needed. + return newPlaylist; + } else { + return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.getStartTimeUs()); + } + } + int newSegmentsCount = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; + if (newSegmentsCount == 0 && oldPlaylist.hasEndTag == newPlaylist.hasEndTag) { + return oldPlaylist; + } + List oldSegments = oldPlaylist.segments; + int oldPlaylistSize = oldSegments.size(); + if (newSegmentsCount <= oldPlaylistSize) { + ArrayList newSegments = new ArrayList<>(); + // We can extrapolate the start time of new segments from the segments of the old snapshot. + int newPlaylistSize = newPlaylist.segments.size(); + for (int i = newSegmentsCount; i < oldPlaylistSize; i++) { + newSegments.add(oldSegments.get(i)); + } + HlsMediaPlaylist.Segment lastSegment = oldSegments.get(oldPlaylistSize - 1); + for (int i = newPlaylistSize - newSegmentsCount; i < newPlaylistSize; i++) { + lastSegment = newPlaylist.segments.get(i).copyWithStartTimeUs( + lastSegment.startTimeUs + (long) lastSegment.durationSecs * C.MICROS_PER_SECOND); + newSegments.add(lastSegment); + } + return newPlaylist.copyWithSegments(newSegments); + } else { + // No segments overlap, we assume the new playlist start coincides with the primary playlist. + return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.getStartTimeUs()); + } + } + + /** + * Holds all information related to a specific Media Playlist. + */ + private final class MediaPlaylistBundle implements Loader.Callback>, + Runnable { + + private final HlsUrl playlistUrl; + private final Loader mediaPlaylistLoader; + private final ParsingLoadable mediaPlaylistLoadable; + + private PlaylistRefreshCallback callback; + private HlsMediaPlaylist latestPlaylistSnapshot; + + public MediaPlaylistBundle(HlsUrl playlistUrl) { + this(playlistUrl, null); + } + + public MediaPlaylistBundle(HlsUrl playlistUrl, HlsMediaPlaylist initialSnapshot) { + this.playlistUrl = playlistUrl; + latestPlaylistSnapshot = initialSnapshot; + mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist"); + mediaPlaylistLoadable = new ParsingLoadable<>(dataSourceFactory.createDataSource(), + UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, + playlistParser); + } + + public void release() { + mediaPlaylistLoader.release(); + } + + public void loadPlaylist() { + if (!mediaPlaylistLoader.isLoading()) { + mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); + } + } + + public void setCallback(PlaylistRefreshCallback callback) { + this.callback = callback; + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs) { + processLoadedPlaylist((HlsMediaPlaylist) loadable.getResult()); + eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + } + + @Override + public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + } + + @Override + public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + // TODO: Add support for playlist blacklisting in response to server error codes. + boolean isFatal = error instanceof ParserException; + eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded(), error, isFatal); + return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; + } + + // Runnable implementation. + + @Override + public void run() { + loadPlaylist(); + } + + // Internal methods. + + private void processLoadedPlaylist(HlsMediaPlaylist loadedMediaPlaylist) { + HlsMediaPlaylist oldPlaylist = latestPlaylistSnapshot; + latestPlaylistSnapshot = adjustPlaylistTimestamps(oldPlaylist, loadedMediaPlaylist); + boolean shouldScheduleRefresh; + if (oldPlaylist != latestPlaylistSnapshot) { + if (callback != null) { + callback.onPlaylistChanged(); + callback = null; + } + shouldScheduleRefresh = onPlaylistUpdated(playlistUrl, latestPlaylistSnapshot, + oldPlaylist == null); + } else { + shouldScheduleRefresh = !loadedMediaPlaylist.hasEndTag; + } + if (shouldScheduleRefresh) { + playlistRefreshHandler.postDelayed(this, PLAYLIST_REFRESH_PERIOD_MS); + } + } + + } + +}