Skip to content

Commit

Permalink
Add support for HLS live seeking
Browse files Browse the repository at this point in the history
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
  • Loading branch information
AquilesCanta authored and ojw28 committed Nov 10, 2016
1 parent 7b3690a commit aaf38ad
Show file tree
Hide file tree
Showing 9 changed files with 557 additions and 331 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments;
assertNotNull(segments);
assertEquals(5, segments.size());
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -48,55 +40,41 @@
/**
* A {@link MediaPeriod} that loads an HLS stream.
*/
/* package */ final class HlsMediaPeriod implements MediaPeriod,
Loader.Callback<ParsingLoadable<HlsPlaylist>>, 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<SampleStream, Integer> 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() {
Expand All @@ -112,10 +90,7 @@ public void release() {
@Override
public void prepare(Callback callback) {
this.callback = callback;
ParsingLoadable<HlsPlaylist> 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
Expand Down Expand Up @@ -234,42 +209,13 @@ 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);
}
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.

@Override
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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<HlsMasterPlaylist.HlsUrl> selectedVariants = new ArrayList<>(masterPlaylist.variants);
ArrayList<HlsMasterPlaylist.HlsUrl> definiteVideoVariants = new ArrayList<>();
Expand Down Expand Up @@ -367,15 +306,15 @@ 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();
}

// 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();
}
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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<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);
}

}
Loading

0 comments on commit aaf38ad

Please sign in to comment.