Commit aaf38adc by aquilescanta Committed by Oliver Woodman

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
parent 7b3690a0
...@@ -74,7 +74,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -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());
......
...@@ -23,21 +23,26 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.Eve ...@@ -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 { ...@@ -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 { ...@@ -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);
}
} }
...@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.SequenceableLoader; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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;
} }
......
...@@ -73,4 +73,10 @@ public final class HlsMasterPlaylist extends HlsPlaylist { ...@@ -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);
}
} }
...@@ -16,6 +16,8 @@ ...@@ -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 { ...@@ -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 { ...@@ -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);
}
} }
...@@ -27,7 +27,6 @@ import java.io.IOException; ...@@ -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 ...@@ -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 ...@@ -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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment