Add support for HLS live seeking

In order to expose the live window, it is necessary (unlike before) to refresh
the live playlists being played periodically so as to know where the user can
seek to. For this, the HlsPlaylistTracker is added, which is basically a map
from HlsUrl's to playlist. One of the playlists involved in the playback will
be chosen to define the live window. The playlist tracker it periodically.
The rest of the playilst will be loaded lazily. N.B: This means that for VOD,
playlists are not refreshed at all. There are three important features missing
in this CL(that will be added in later CLs):

* Blacklisting HlsUrls that point to resources that return 4xx response codes.
    As per [Internal: b/18948961].
* Allow loaded chunks to feed timestamps back to the tracker, to fix any
    drifting in live playlists.
* Dinamically choose the HlsUrl that points to the playlist that defines the
    live window.

Other features:
--------------

The tracker can also be used for keeping track of discontinuities. In the case
of single variant playlists, this is particularly useful. Might also work if
there is a that the live playlists are aligned (but this is more like working
around the issue, than actually solving it). For this, see [Internal: b/32166568]
and [Internal: b/28985320].

Issue:#87

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=138054302
This commit is contained in:
aquilescanta 2016-11-03 03:55:12 -07:00 committed by Oliver Woodman
parent 7b3690a0a6
commit aaf38adc26
9 changed files with 558 additions and 332 deletions

View File

@ -74,7 +74,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(2679, mediaPlaylist.mediaSequence); assertEquals(2679, mediaPlaylist.mediaSequence);
assertEquals(8, mediaPlaylist.targetDurationSecs); assertEquals(8, mediaPlaylist.targetDurationSecs);
assertEquals(3, mediaPlaylist.version); assertEquals(3, mediaPlaylist.version);
assertEquals(false, mediaPlaylist.live); assertEquals(true, mediaPlaylist.hasEndTag);
List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments; List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments;
assertNotNull(segments); assertNotNull(segments);
assertEquals(5, segments.size()); assertEquals(5, segments.size());

View File

@ -36,7 +36,7 @@ import com.google.android.exoplayer2.source.chunk.DataChunk;
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.HlsMediaPlaylist; 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.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.BaseTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
@ -44,7 +44,6 @@ import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.UriUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Arrays; import java.util.Arrays;
@ -65,7 +64,7 @@ import java.util.Locale;
} }
/** /**
* The chunk. * The chunk to be loaded next.
*/ */
public Chunk chunk; public Chunk chunk;
@ -75,9 +74,9 @@ import java.util.Locale;
public boolean endOfStream; 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. * Clears the holder.
@ -85,20 +84,11 @@ import java.util.Locale;
public void clear() { public void clear() {
chunk = null; chunk = null;
endOfStream = false; 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 AAC_FILE_EXTENSION = ".aac";
private static final String AC3_FILE_EXTENSION = ".ac3"; private static final String AC3_FILE_EXTENSION = ".ac3";
private static final String EC3_FILE_EXTENSION = ".ec3"; private static final String EC3_FILE_EXTENSION = ".ec3";
@ -107,18 +97,13 @@ import java.util.Locale;
private static final String VTT_FILE_EXTENSION = ".vtt"; private static final String VTT_FILE_EXTENSION = ".vtt";
private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; private static final String WEBVTT_FILE_EXTENSION = ".webvtt";
private final String baseUri;
private final DataSource dataSource; private final DataSource dataSource;
private final HlsPlaylistParser playlistParser;
private final TimestampAdjusterProvider timestampAdjusterProvider; private final TimestampAdjusterProvider timestampAdjusterProvider;
private final HlsMasterPlaylist.HlsUrl[] variants; private final HlsMasterPlaylist.HlsUrl[] variants;
private final HlsMediaPlaylist[] variantPlaylists; private final HlsPlaylistTracker playlistTracker;
private final TrackGroup trackGroup; private final TrackGroup trackGroup;
private final long[] variantLastPlaylistLoadTimesMs;
private byte[] scratchSpace; private byte[] scratchSpace;
private boolean live;
private long durationUs;
private IOException fatalError; private IOException fatalError;
private HlsInitializationChunk lastLoadedInitializationChunk; private HlsInitializationChunk lastLoadedInitializationChunk;
@ -133,22 +118,19 @@ import java.util.Locale;
private TrackSelection trackSelection; 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 variants The available variants.
* @param dataSource A {@link DataSource} suitable for loading the media data. * @param dataSource A {@link DataSource} suitable for loading the media data.
* @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If
* multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the
* same provider. * same provider.
*/ */
public HlsChunkSource(String baseUri, HlsMasterPlaylist.HlsUrl[] variants, DataSource dataSource, public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsMasterPlaylist.HlsUrl[] variants,
TimestampAdjusterProvider timestampAdjusterProvider) { DataSource dataSource, TimestampAdjusterProvider timestampAdjusterProvider) {
this.baseUri = baseUri; this.playlistTracker = playlistTracker;
this.variants = variants; this.variants = variants;
this.dataSource = dataSource; this.dataSource = dataSource;
this.timestampAdjusterProvider = timestampAdjusterProvider; this.timestampAdjusterProvider = timestampAdjusterProvider;
playlistParser = new HlsPlaylistParser();
variantPlaylists = new HlsMediaPlaylist[variants.length];
variantLastPlaylistLoadTimesMs = new long[variants.length];
Format[] variantFormats = new Format[variants.length]; Format[] variantFormats = new Format[variants.length];
int[] initialTrackSelection = new int[variants.length]; int[] initialTrackSelection = new int[variants.length];
@ -172,20 +154,6 @@ import java.util.Locale;
} }
} }
/**
* 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. * Returns the track group exposed by the source.
*/ */
@ -214,8 +182,8 @@ import java.util.Locale;
* <p> * <p>
* If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream has * 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 * 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 end of the stream has not been reached, {@link HlsChunkHolder#playlist} is set to
* the amount of milliseconds to wait before retrying. * contain the {@link HlsMasterPlaylist.HlsUrl} that refers to the playlist that needs refreshing.
* *
* @param previous The most recently loaded media chunk. * @param previous The most recently loaded media chunk.
* @param playbackPositionUs The current playback position. If {@code previous} is null then this * @param playbackPositionUs The current playback position. If {@code previous} is null then this
@ -226,7 +194,6 @@ import java.util.Locale;
public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) { public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) {
int oldVariantIndex = previous == null ? C.INDEX_UNSET int oldVariantIndex = previous == null ? C.INDEX_UNSET
: trackGroup.indexOf(previous.trackFormat); : trackGroup.indexOf(previous.trackFormat);
// Use start time of the previous chunk rather than its end time because switching format will // Use start time of the previous chunk rather than its end time because switching format will
// require downloading overlapping segments. // require downloading overlapping segments.
long bufferedDurationUs = previous == null ? 0 long bufferedDurationUs = previous == null ? 0
@ -235,67 +202,44 @@ import java.util.Locale;
int newVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); int newVariantIndex = trackSelection.getSelectedIndexInTrackGroup();
boolean switchingVariant = oldVariantIndex != newVariantIndex; boolean switchingVariant = oldVariantIndex != newVariantIndex;
HlsMediaPlaylist mediaPlaylist = variantPlaylists[newVariantIndex]; HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(variants[newVariantIndex]);
if (mediaPlaylist == null) { if (mediaPlaylist == null) {
// We don't have the media playlist for the next variant. Request it now. out.playlist = variants[newVariantIndex];
out.chunk = newMediaPlaylistChunk(newVariantIndex, trackSelection.getSelectionReason(), // Retry when playlist is refreshed.
trackSelection.getSelectionData());
return; return;
} }
int chunkMediaSequence; int chunkMediaSequence;
if (live) { if (previous == null || switchingVariant) {
if (previous == null) { long targetPositionUs = previous == null ? playbackPositionUs : previous.startTimeUs;
// When playing a live stream, the starting chunk will be the third counting from the live if (targetPositionUs > mediaPlaylist.getEndTimeUs()) {
// edge. // If the playlist is too old to contain the chunk, we need to refresh it.
chunkMediaSequence = Math.max(0, mediaPlaylist.segments.size() - 3) chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
+ mediaPlaylist.mediaSequence;
// TODO: Bring this back for live window seeking.
// chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs,
// true, true) + mediaPlaylist.mediaSequence;
} else { } else {
chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex, chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, targetPositionUs, true,
newVariantIndex); !playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence;
if (chunkMediaSequence < mediaPlaylist.mediaSequence) { if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) {
// We try getting the next chunk without adapting in case that's the reason for falling // We try getting the next chunk without adapting in case that's the reason for falling
// behind the live window. // behind the live window.
newVariantIndex = oldVariantIndex; newVariantIndex = oldVariantIndex;
mediaPlaylist = variantPlaylists[newVariantIndex]; mediaPlaylist = playlistTracker.getPlaylistSnapshot(variants[newVariantIndex]);
chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex, chunkMediaSequence = previous.getNextChunkIndex();
newVariantIndex); }
}
} else {
chunkMediaSequence = previous.getNextChunkIndex();
}
if (chunkMediaSequence < mediaPlaylist.mediaSequence) { if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
fatalError = new BehindLiveWindowException(); fatalError = new BehindLiveWindowException();
return; return;
} }
}
}
} 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();
}
}
int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
if (chunkIndex >= mediaPlaylist.segments.size()) { if (chunkIndex >= mediaPlaylist.segments.size()) {
if (!mediaPlaylist.live) { if (mediaPlaylist.hasEndTag) {
out.endOfStream = true; out.endOfStream = true;
} else /* Live */ { } else /* Live */ {
long msToRerequestLiveMediaPlaylist = msToRerequestLiveMediaPlaylist(newVariantIndex); out.playlist = variants[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; return;
} }
@ -319,18 +263,10 @@ import java.util.Locale;
} }
// Compute start and end times, and the sequence number of the next chunk. // Compute start and end times, and the sequence number of the next chunk.
long startTimeUs; long startTimeUs = segment.startTimeUs;
if (live) { if (previous != null && !switchingVariant) {
if (previous == null) {
startTimeUs = 0;
} else if (switchingVariant) {
startTimeUs = previous.getAdjustedStartTimeUs();
} else {
startTimeUs = previous.getAdjustedEndTimeUs(); startTimeUs = previous.getAdjustedEndTimeUs();
} }
} else /* Not live */ {
startTimeUs = segment.startTimeUs;
}
long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND); long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND);
Format format = variants[newVariantIndex].format; Format format = variants[newVariantIndex].format;
@ -424,52 +360,6 @@ import java.util.Locale;
encryptionKey, encryptionIv); 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 * Called when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this
* source. * source.
@ -479,10 +369,6 @@ import java.util.Locale;
public void onChunkLoadCompleted(Chunk chunk) { public void onChunkLoadCompleted(Chunk chunk) {
if (chunk instanceof HlsInitializationChunk) { if (chunk instanceof HlsInitializationChunk) {
lastLoadedInitializationChunk = (HlsInitializationChunk) chunk; 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) { } else if (chunk instanceof EncryptionKeyChunk) {
EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk; EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk;
scratchSpace = encryptionKeyChunk.getDataHolder(); scratchSpace = encryptionKeyChunk.getDataHolder();
@ -519,24 +405,6 @@ import java.util.Locale;
format); 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, private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex,
int trackSelectionReason, Object trackSelectionData) { int trackSelectionReason, Object trackSelectionData) {
DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP);
@ -571,13 +439,6 @@ import java.util.Locale;
encryptionIv = null; 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. // Private classes.
/** /**
@ -626,38 +487,6 @@ import java.util.Locale;
} }
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 { private static final class EncryptionKeyChunk extends DataChunk {
public final String iv; public final String iv;

View File

@ -15,30 +15,22 @@
*/ */
package com.google.android.exoplayer2.source.hls; package com.google.android.exoplayer2.source.hls;
import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.text.TextUtils; import android.text.TextUtils;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; 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.AdaptiveMediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.CompositeSequenceableLoader;
import com.google.android.exoplayer2.source.MediaPeriod; 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.SampleStream;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
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.hls.playlist.HlsMasterPlaylist; 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.HlsPlaylistTracker;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -48,55 +40,41 @@ import java.util.List;
/** /**
* A {@link MediaPeriod} that loads an HLS stream. * A {@link MediaPeriod} that loads an HLS stream.
*/ */
/* package */ final class HlsMediaPeriod implements MediaPeriod, public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback,
Loader.Callback<ParsingLoadable<HlsPlaylist>>, HlsSampleStreamWrapper.Callback { HlsPlaylistTracker.PlaylistRefreshCallback {
private final Uri manifestUri; private final HlsPlaylistTracker playlistTracker;
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
private final int minLoadableRetryCount; private final int minLoadableRetryCount;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final MediaSource.Listener sourceListener;
private final Allocator allocator; private final Allocator allocator;
private final IdentityHashMap<SampleStream, Integer> streamWrapperIndices; private final IdentityHashMap<SampleStream, Integer> streamWrapperIndices;
private final TimestampAdjusterProvider timestampAdjusterProvider; private final TimestampAdjusterProvider timestampAdjusterProvider;
private final HlsPlaylistParser manifestParser;
private final Handler continueLoadingHandler; private final Handler continueLoadingHandler;
private final Loader manifestFetcher; private final Loader manifestFetcher;
private final long preparePositionUs; private final long preparePositionUs;
private final Runnable continueLoadingRunnable;
private Callback callback; private Callback callback;
private int pendingPrepareCount; private int pendingPrepareCount;
private HlsPlaylist playlist;
private boolean seenFirstTrackSelection; private boolean seenFirstTrackSelection;
private boolean isLive;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] sampleStreamWrappers;
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
private CompositeSequenceableLoader sequenceableLoader; private CompositeSequenceableLoader sequenceableLoader;
public HlsMediaPeriod(Uri manifestUri, DataSource.Factory dataSourceFactory, public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, DataSource.Factory dataSourceFactory,
int minLoadableRetryCount, EventDispatcher eventDispatcher, int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator,
MediaSource.Listener sourceListener, Allocator allocator,
long positionUs) { long positionUs) {
this.manifestUri = manifestUri; this.playlistTracker = playlistTracker;
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
this.eventDispatcher = eventDispatcher; this.eventDispatcher = eventDispatcher;
this.sourceListener = sourceListener;
this.allocator = allocator; this.allocator = allocator;
streamWrapperIndices = new IdentityHashMap<>(); streamWrapperIndices = new IdentityHashMap<>();
timestampAdjusterProvider = new TimestampAdjusterProvider(); timestampAdjusterProvider = new TimestampAdjusterProvider();
manifestParser = new HlsPlaylistParser();
continueLoadingHandler = new Handler(); continueLoadingHandler = new Handler();
manifestFetcher = new Loader("Loader:ManifestFetcher"); manifestFetcher = new Loader("Loader:ManifestFetcher");
preparePositionUs = positionUs; preparePositionUs = positionUs;
continueLoadingRunnable = new Runnable() {
@Override
public void run() {
callback.onContinueLoadingRequested(HlsMediaPeriod.this);
}
};
} }
public void release() { public void release() {
@ -112,10 +90,7 @@ import java.util.List;
@Override @Override
public void prepare(Callback callback) { public void prepare(Callback callback) {
this.callback = callback; this.callback = callback;
ParsingLoadable<HlsPlaylist> loadable = new ParsingLoadable<>( buildAndPrepareSampleStreamWrappers();
dataSourceFactory.createDataSource(), manifestUri, C.DATA_TYPE_MANIFEST, manifestParser);
long elapsedRealtimeMs = manifestFetcher.startLoading(loadable, this, minLoadableRetryCount);
eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);
} }
@Override @Override
@ -234,8 +209,6 @@ import java.util.List;
@Override @Override
public long seekToUs(long positionUs) { public long seekToUs(long positionUs) {
// Treat all seeks into non-seekable media as being to t=0.
positionUs = isLive ? 0 : positionUs;
timestampAdjusterProvider.reset(); timestampAdjusterProvider.reset();
for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) {
sampleStreamWrapper.seekTo(positionUs); sampleStreamWrapper.seekTo(positionUs);
@ -243,33 +216,6 @@ import java.util.List;
return positionUs; return positionUs;
} }
// Loader.Callback implementation.
@Override
public void onLoadCompleted(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs) {
eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
playlist = loadable.getResult();
buildAndPrepareSampleStreamWrappers();
}
@Override
public void onLoadCanceled(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) {
eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
}
@Override
public int onLoadError(ParsingLoadable<HlsPlaylist> 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. // HlsSampleStreamWrapper.Callback implementation.
@Override @Override
@ -278,10 +224,6 @@ import java.util.List;
return; 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; int totalTrackGroupCount = 0;
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
totalTrackGroupCount += sampleStreamWrapper.getTrackGroups().length; totalTrackGroupCount += sampleStreamWrapper.getTrackGroups().length;
@ -296,16 +238,11 @@ import java.util.List;
} }
trackGroups = new TrackGroupArray(trackGroupArray); trackGroups = new TrackGroupArray(trackGroupArray);
callback.onPrepared(this); callback.onPrepared(this);
// TODO[playlists]: Calculate the window.
Timeline timeline = new SinglePeriodTimeline(durationUs, durationUs, 0, 0, !isLive, isLive);
sourceListener.onSourceInfoRefreshed(timeline, playlist);
} }
@Override @Override
public void onContinueLoadingRequiredInMs(final HlsSampleStreamWrapper sampleStreamWrapper, public void onPlaylistRefreshRequired(HlsMasterPlaylist.HlsUrl url) {
long delayMs) { playlistTracker.refreshPlaylist(url, this);
continueLoadingHandler.postDelayed(continueLoadingRunnable, delayMs);
} }
@Override @Override
@ -317,22 +254,24 @@ import java.util.List;
callback.onContinueLoadingRequested(this); callback.onContinueLoadingRequested(this);
} }
// PlaylistListener implementation.
@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();
}
}
}
// Internal methods. // Internal methods.
private void buildAndPrepareSampleStreamWrappers() { private void buildAndPrepareSampleStreamWrappers() {
String baseUri = playlist.baseUri; HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist();
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;
}
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
// Build the default stream wrapper. // Build the default stream wrapper.
List<HlsMasterPlaylist.HlsUrl> selectedVariants = new ArrayList<>(masterPlaylist.variants); List<HlsMasterPlaylist.HlsUrl> selectedVariants = new ArrayList<>(masterPlaylist.variants);
ArrayList<HlsMasterPlaylist.HlsUrl> definiteVideoVariants = new ArrayList<>(); ArrayList<HlsMasterPlaylist.HlsUrl> definiteVideoVariants = new ArrayList<>();
@ -367,7 +306,7 @@ import java.util.List;
HlsMasterPlaylist.HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; HlsMasterPlaylist.HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()];
selectedVariants.toArray(variants); selectedVariants.toArray(variants);
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT,
baseUri, variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat);
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
sampleStreamWrapper.continuePreparing(); sampleStreamWrapper.continuePreparing();
} }
@ -375,7 +314,7 @@ import java.util.List;
// Build audio stream wrappers. // Build audio stream wrappers.
for (int i = 0; i < audioVariants.size(); i++) { for (int i = 0; i < audioVariants.size(); i++) {
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, 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; sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
sampleStreamWrapper.continuePreparing(); sampleStreamWrapper.continuePreparing();
} }
@ -384,16 +323,16 @@ import java.util.List;
for (int i = 0; i < subtitleVariants.size(); i++) { for (int i = 0; i < subtitleVariants.size(); i++) {
HlsMasterPlaylist.HlsUrl url = subtitleVariants.get(i); HlsMasterPlaylist.HlsUrl url = subtitleVariants.get(i);
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT,
baseUri, new HlsMasterPlaylist.HlsUrl[] {url}, null, null); new HlsMasterPlaylist.HlsUrl[] {url}, null, null);
sampleStreamWrapper.prepareSingleTrack(url.format); sampleStreamWrapper.prepareSingleTrack(url.format);
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
} }
} }
private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, String baseUri, private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType,
HlsMasterPlaylist.HlsUrl[] variants, Format muxedAudioFormat, Format muxedCaptionFormat) { HlsMasterPlaylist.HlsUrl[] variants, Format muxedAudioFormat, Format muxedCaptionFormat) {
DataSource dataSource = dataSourceFactory.createDataSource(); DataSource dataSource = dataSourceFactory.createDataSource();
HlsChunkSource defaultChunkSource = new HlsChunkSource(baseUri, variants, dataSource, HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSource,
timestampAdjusterProvider); timestampAdjusterProvider);
return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator,
preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount, preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount,

View File

@ -23,21 +23,26 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.Eve
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SinglePeriodTimeline; 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.Allocator;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.util.List;
/** /**
* An HLS {@link MediaSource}. * 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. * The default minimum number of times to retry loading data prior to failing.
*/ */
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; 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 DataSource.Factory dataSourceFactory;
private final int minLoadableRetryCount; private final int minLoadableRetryCount;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
@ -53,29 +58,29 @@ public final class HlsMediaSource implements MediaSource {
public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory,
int minLoadableRetryCount, Handler eventHandler, int minLoadableRetryCount, Handler eventHandler,
AdaptiveMediaSourceEventListener eventListener) { AdaptiveMediaSourceEventListener eventListener) {
this.manifestUri = manifestUri;
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher,
minLoadableRetryCount, this);
} }
@Override @Override
public void prepareSource(MediaSource.Listener listener) { public void prepareSource(MediaSource.Listener listener) {
sourceListener = listener; sourceListener = listener;
// TODO: Defer until the playlist has been loaded. playlistTracker.start();
listener.onSourceInfoRefreshed(new SinglePeriodTimeline(C.TIME_UNSET, false), null);
} }
@Override @Override
public void maybeThrowSourceInfoRefreshError() { public void maybeThrowSourceInfoRefreshError() throws IOException {
// Do nothing. playlistTracker.maybeThrowPrimaryPlaylistRefreshError();
} }
@Override @Override
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
Assertions.checkArgument(index == 0); Assertions.checkArgument(index == 0);
return new HlsMediaPeriod(manifestUri, dataSourceFactory, minLoadableRetryCount, return new HlsMediaPeriod(playlistTracker, dataSourceFactory, minLoadableRetryCount,
eventDispatcher, sourceListener, allocator, positionUs); eventDispatcher, allocator, positionUs);
} }
@Override @Override
@ -85,7 +90,26 @@ public final class HlsMediaSource implements MediaSource {
@Override @Override
public void releaseSource() { public void releaseSource() {
playlistTracker.release();
sourceListener = null; 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<HlsMediaPlaylist.Segment> 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);
}
} }

View File

@ -32,6 +32,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.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader;
@ -58,10 +59,10 @@ import java.util.LinkedList;
void onPrepared(); void onPrepared();
/** /**
* Called to schedule a {@link #continueLoading(long)} call. * Called to schedule a {@link #continueLoading(long)} call when the playlist referred by the
* given url changes.
*/ */
void onContinueLoadingRequiredInMs(HlsSampleStreamWrapper sampleStreamSource, void onPlaylistRefreshRequired(HlsMasterPlaylist.HlsUrl playlistUrl);
long delayMs);
} }
@ -164,14 +165,6 @@ import java.util.LinkedList;
maybeThrowError(); maybeThrowError();
} }
public long getDurationUs() {
return chunkSource.getDurationUs();
}
public boolean isLive() {
return chunkSource.isLive();
}
public TrackGroupArray getTrackGroups() { public TrackGroupArray getTrackGroups() {
return trackGroups; return trackGroups;
} }
@ -340,7 +333,7 @@ import java.util.LinkedList;
nextChunkHolder); nextChunkHolder);
boolean endOfStream = nextChunkHolder.endOfStream; boolean endOfStream = nextChunkHolder.endOfStream;
Chunk loadable = nextChunkHolder.chunk; Chunk loadable = nextChunkHolder.chunk;
long retryInMs = nextChunkHolder.retryInMs; HlsMasterPlaylist.HlsUrl playlistToLoad = nextChunkHolder.playlist;
nextChunkHolder.clear(); nextChunkHolder.clear();
if (endOfStream) { if (endOfStream) {
@ -349,9 +342,8 @@ import java.util.LinkedList;
} }
if (loadable == null) { if (loadable == null) {
if (retryInMs != C.TIME_UNSET) { if (playlistToLoad != null) {
Assertions.checkState(chunkSource.isLive()); callback.onPlaylistRefreshRequired(playlistToLoad);
callback.onContinueLoadingRequiredInMs(this, retryInMs);
} }
return false; return false;
} }

View File

@ -73,4 +73,10 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
this.muxedCaptionFormat = muxedCaptionFormat; this.muxedCaptionFormat = muxedCaptionFormat;
} }
public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) {
List<HlsUrl> variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri));
List<HlsUrl> emptyList = Collections.emptyList();
return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null);
}
} }

View File

@ -16,6 +16,8 @@
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
@ -60,6 +62,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public int compareTo(Long startTimeUs) { public int compareTo(Long startTimeUs) {
return this.startTimeUs > startTimeUs ? 1 : (this.startTimeUs < startTimeUs ? -1 : 0); 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"; public static final String ENCRYPTION_METHOD_NONE = "NONE";
@ -70,25 +78,51 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final int version; public final int version;
public final Segment initializationSegment; public final Segment initializationSegment;
public final List<Segment> segments; public final List<Segment> segments;
public final boolean live; public final boolean hasEndTag;
public final long durationUs; public final long durationUs;
public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version, public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version,
boolean live, Segment initializationSegment, List<Segment> segments) { boolean hasEndTag, Segment initializationSegment, List<Segment> segments) {
super(baseUri, HlsPlaylist.TYPE_MEDIA); super(baseUri, HlsPlaylist.TYPE_MEDIA);
this.mediaSequence = mediaSequence; this.mediaSequence = mediaSequence;
this.targetDurationSecs = targetDurationSecs; this.targetDurationSecs = targetDurationSecs;
this.version = version; this.version = version;
this.live = live; this.hasEndTag = hasEndTag;
this.initializationSegment = initializationSegment; this.initializationSegment = initializationSegment;
this.segments = segments; this.segments = Collections.unmodifiableList(segments);
if (!segments.isEmpty()) { if (!segments.isEmpty()) {
Segment first = segments.get(0);
Segment last = segments.get(segments.size() - 1); 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 { } else {
durationUs = 0; 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<Segment> 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<Segment> segments) {
return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, hasEndTag,
initializationSegment, segments);
}
} }

View File

@ -27,7 +27,6 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
@ -214,7 +213,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
int mediaSequence = 0; int mediaSequence = 0;
int targetDurationSecs = 0; int targetDurationSecs = 0;
int version = 1; // Default version == 1. int version = 1; // Default version == 1.
boolean live = true; boolean hasEndTag = false;
Segment initializationSegment = null; Segment initializationSegment = null;
List<Segment> segments = new ArrayList<>(); List<Segment> segments = new ArrayList<>();
@ -298,11 +297,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
segmentByteRangeLength = C.LENGTH_UNSET; segmentByteRangeLength = C.LENGTH_UNSET;
} else if (line.equals(TAG_ENDLIST)) { } else if (line.equals(TAG_ENDLIST)) {
live = false; hasEndTag = true;
} }
} }
return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, live, return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, hasEndTag,
initializationSegment, Collections.unmodifiableList(segments)); initializationSegment, segments);
} }
private static String parseStringAttr(String line, Pattern pattern) throws ParserException { private static String parseStringAttr(String line, Pattern pattern) throws ParserException {

View File

@ -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<ParsingLoadable<HlsPlaylist>> {
/**
* 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<HlsUrl, MediaPlaylistBundle> 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<HlsPlaylist> 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<HlsPlaylist> 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<HlsUrl> 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<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) {
eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
}
@Override
public int onLoadError(ParsingLoadable<HlsPlaylist> 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<HlsUrl> 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<HlsMediaPlaylist.Segment> oldSegments = oldPlaylist.segments;
int oldPlaylistSize = oldSegments.size();
if (newSegmentsCount <= oldPlaylistSize) {
ArrayList<HlsMediaPlaylist.Segment> 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<ParsingLoadable<HlsPlaylist>>,
Runnable {
private final HlsUrl playlistUrl;
private final Loader mediaPlaylistLoader;
private final ParsingLoadable<HlsPlaylist> 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<HlsPlaylist> 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<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) {
eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
}
@Override
public int onLoadError(ParsingLoadable<HlsPlaylist> 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);
}
}
}
}