Commit 1cbc0fc6 by aquilescanta Committed by Oliver Woodman

Allow HlsPlaylistTracker to change the primaryHlsUrl

When the primary url is blacklisted (due to a 404, for example) or
the selected variant is different from primary url, allow the tracker
to change the url.

Issue:#87

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=141291435
parent 8765b198
...@@ -51,9 +51,9 @@ public final class ChunkedTrackBlacklistUtil { ...@@ -51,9 +51,9 @@ public final class ChunkedTrackBlacklistUtil {
/** /**
* Blacklists {@code trackSelectionIndex} in {@code trackSelection} for * Blacklists {@code trackSelectionIndex} in {@code trackSelection} for
* {@code blacklistDurationMs} if {@code e} is an {@link InvalidResponseCodeException} with * {@code blacklistDurationMs} if calling {@link #shouldBlacklist(Exception)} for {@code e}
* {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. Else does nothing. Note * returns true. Else does nothing. Note that blacklisting will fail if the track is the only
* that blacklisting will fail if the track is the only non-blacklisted track in the selection. * non-blacklisted track in the selection.
* *
* @param trackSelection The track selection. * @param trackSelection The track selection.
* @param trackSelectionIndex The index in the selection to consider blacklisting. * @param trackSelectionIndex The index in the selection to consider blacklisting.
...@@ -63,24 +63,33 @@ public final class ChunkedTrackBlacklistUtil { ...@@ -63,24 +63,33 @@ public final class ChunkedTrackBlacklistUtil {
*/ */
public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex, public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex,
Exception e, long blacklistDurationMs) { Exception e, long blacklistDurationMs) {
if (trackSelection.length() == 1) { if (shouldBlacklist(e)) {
// Blacklisting won't ever work if there's only one track in the selection. boolean blacklisted = trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs);
return false; int responseCode = ((InvalidResponseCodeException) e).responseCode;
if (blacklisted) {
Log.w(TAG, "Blacklisted: duration=" + blacklistDurationMs + ", responseCode="
+ responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex));
} else {
Log.w(TAG, "Blacklisting failed (cannot blacklist last enabled track): responseCode="
+ responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex));
}
return blacklisted;
} }
return false;
}
/**
* Returns whether a loading error is an {@link InvalidResponseCodeException} with
* {@link InvalidResponseCodeException#responseCode} equal to 404 or 410.
*
* @param e The loading error.
* @return Wheter the loading error is an {@link InvalidResponseCodeException} with
* {@link InvalidResponseCodeException#responseCode} equal to 404 or 410.
*/
public static boolean shouldBlacklist(Exception e) {
if (e instanceof InvalidResponseCodeException) { if (e instanceof InvalidResponseCodeException) {
InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e; int responseCode = ((InvalidResponseCodeException) e).responseCode;
int responseCode = responseCodeException.responseCode; return responseCode == 404 || responseCode == 410;
if (responseCode == 404 || responseCode == 410) {
boolean blacklisted = trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs);
if (blacklisted) {
Log.w(TAG, "Blacklisted: duration=" + blacklistDurationMs + ", responseCode="
+ responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex));
} else {
Log.w(TAG, "Blacklisting failed (cannot blacklist last enabled track): responseCode="
+ responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex));
}
return blacklisted;
}
} }
return false; return false;
} }
......
...@@ -278,9 +278,10 @@ import java.util.Locale; ...@@ -278,9 +278,10 @@ import java.util.Locale;
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
null); null);
out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex], out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex],
trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
startTimeUs + segment.durationUs, chunkMediaSequence, segment.discontinuitySequenceNumber, startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence,
isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv); segment.discontinuitySequenceNumber, isTimestampMaster, timestampAdjuster, previous,
encryptionKey, encryptionIv);
} }
/** /**
...@@ -317,19 +318,19 @@ import java.util.Locale; ...@@ -317,19 +318,19 @@ import java.util.Locale;
} }
/** /**
* Called when an error is encountered while loading a playlist. * Called when a playlist is blacklisted.
* *
* @param url The url that references the playlist whose load encountered the error. * @param url The url that references the blacklisted playlist.
* @param error The error. * @param blacklistMs The amount of milliseconds for which the playlist was blacklisted.
*/ */
public void onPlaylistLoadError(HlsUrl url, IOException error) { public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) {
int trackGroupIndex = trackGroup.indexOf(url.format); int trackGroupIndex = trackGroup.indexOf(url.format);
if (trackGroupIndex == C.INDEX_UNSET) { if (trackGroupIndex != C.INDEX_UNSET) {
// The url is not handled by this chunk source. int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex);
return; if (trackSelectionIndex != C.INDEX_UNSET) {
trackSelection.blacklist(trackSelectionIndex, blacklistMs);
}
} }
ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection,
trackSelection.indexOf(trackGroupIndex), error);
} }
// Private methods. // Private methods.
......
...@@ -31,7 +31,6 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; ...@@ -31,7 +31,6 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
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.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;
...@@ -42,7 +41,7 @@ import java.util.List; ...@@ -42,7 +41,7 @@ import java.util.List;
* A {@link MediaPeriod} that loads an HLS stream. * A {@link MediaPeriod} that loads an HLS stream.
*/ */
public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback, public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback,
HlsPlaylistTracker.PlaylistRefreshCallback { HlsPlaylistTracker.PlaylistEventListener {
private final HlsPlaylistTracker playlistTracker; private final HlsPlaylistTracker playlistTracker;
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
...@@ -52,7 +51,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -52,7 +51,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private final IdentityHashMap<SampleStream, Integer> streamWrapperIndices; private final IdentityHashMap<SampleStream, Integer> streamWrapperIndices;
private final TimestampAdjusterProvider timestampAdjusterProvider; private final TimestampAdjusterProvider timestampAdjusterProvider;
private final Handler continueLoadingHandler; private final Handler continueLoadingHandler;
private final Loader manifestFetcher;
private final long preparePositionUs; private final long preparePositionUs;
private Callback callback; private Callback callback;
...@@ -74,13 +72,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -74,13 +72,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
streamWrapperIndices = new IdentityHashMap<>(); streamWrapperIndices = new IdentityHashMap<>();
timestampAdjusterProvider = new TimestampAdjusterProvider(); timestampAdjusterProvider = new TimestampAdjusterProvider();
continueLoadingHandler = new Handler(); continueLoadingHandler = new Handler();
manifestFetcher = new Loader("Loader:ManifestFetcher");
preparePositionUs = positionUs; preparePositionUs = positionUs;
} }
public void release() { public void release() {
playlistTracker.removeListener(this);
continueLoadingHandler.removeCallbacksAndMessages(null); continueLoadingHandler.removeCallbacksAndMessages(null);
manifestFetcher.release();
if (sampleStreamWrappers != null) { if (sampleStreamWrappers != null) {
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.release(); sampleStreamWrapper.release();
...@@ -90,15 +87,14 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -90,15 +87,14 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
@Override @Override
public void prepare(Callback callback) { public void prepare(Callback callback) {
playlistTracker.addListener(this);
this.callback = callback; this.callback = callback;
buildAndPrepareSampleStreamWrappers(); buildAndPrepareSampleStreamWrappers();
} }
@Override @Override
public void maybeThrowPrepareError() throws IOException { public void maybeThrowPrepareError() throws IOException {
if (sampleStreamWrappers == null) { if (sampleStreamWrappers != null) {
manifestFetcher.maybeThrowError();
} else {
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.maybeThrowPrepareError(); sampleStreamWrapper.maybeThrowPrepareError();
} }
...@@ -255,7 +251,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -255,7 +251,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
@Override @Override
public void onPlaylistRefreshRequired(HlsUrl url) { public void onPlaylistRefreshRequired(HlsUrl url) {
playlistTracker.refreshPlaylist(url, this); playlistTracker.refreshPlaylist(url);
} }
@Override @Override
...@@ -271,22 +267,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -271,22 +267,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
@Override @Override
public void onPlaylistChanged() { public void onPlaylistChanged() {
if (trackGroups != null) { continuePreparingOrLoading();
callback.onContinueLoadingRequested(this);
} else {
// Some of the wrappers were waiting for their media playlist to prepare.
for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) {
wrapper.continuePreparing();
}
}
} }
@Override @Override
public void onPlaylistLoadError(HlsUrl url, IOException error) { public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) {
for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.onPlaylistLoadError(url, error); streamWrapper.onPlaylistBlacklisted(url, blacklistMs);
} }
callback.onContinueLoadingRequested(this); continuePreparingOrLoading();
} }
// Internal methods. // Internal methods.
...@@ -363,6 +352,17 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -363,6 +352,17 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
eventDispatcher); eventDispatcher);
} }
private void continuePreparingOrLoading() {
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();
}
}
}
private static boolean variantHasExplicitCodecWithPrefix(HlsUrl variant, String prefix) { private static boolean variantHasExplicitCodecWithPrefix(HlsUrl variant, String prefix) {
String codecs = variant.format.codecs; String codecs = variant.format.codecs;
if (TextUtils.isEmpty(codecs)) { if (TextUtils.isEmpty(codecs)) {
......
...@@ -77,7 +77,7 @@ public final class HlsMediaSource implements MediaSource, ...@@ -77,7 +77,7 @@ public final class HlsMediaSource implements MediaSource,
@Override @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { public void maybeThrowSourceInfoRefreshError() throws IOException {
playlistTracker.maybeThrowPrimaryPlaylistRefreshError(); playlistTracker.maybeThrowPlaylistRefreshError();
} }
@Override @Override
......
...@@ -279,8 +279,8 @@ import java.util.LinkedList; ...@@ -279,8 +279,8 @@ import java.util.LinkedList;
chunkSource.setIsTimestampMaster(isTimestampMaster); chunkSource.setIsTimestampMaster(isTimestampMaster);
} }
public void onPlaylistLoadError(HlsUrl url, IOException error) { public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) {
chunkSource.onPlaylistLoadError(url, error); chunkSource.onPlaylistBlacklisted(url, blacklistMs);
} }
// SampleStream implementation. // SampleStream implementation.
......
...@@ -68,19 +68,21 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -68,19 +68,21 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final long startTimeUs; public final long startTimeUs;
public final int mediaSequence; public final int mediaSequence;
public final int version; public final int version;
public final Segment initializationSegment; public final long targetDurationUs;
public final List<Segment> segments;
public final boolean hasEndTag; public final boolean hasEndTag;
public final boolean hasProgramDateTime; public final boolean hasProgramDateTime;
public final Segment initializationSegment;
public final List<Segment> segments;
public final long durationUs; public final long durationUs;
public HlsMediaPlaylist(String baseUri, long startTimeUs, int mediaSequence, int version, public HlsMediaPlaylist(String baseUri, long startTimeUs, int mediaSequence,
boolean hasEndTag, boolean hasProgramDateTime, Segment initializationSegment, int version, long targetDurationUs, boolean hasEndTag, boolean hasProgramDateTime,
List<Segment> segments) { Segment initializationSegment, List<Segment> segments) {
super(baseUri, HlsPlaylist.TYPE_MEDIA); super(baseUri, HlsPlaylist.TYPE_MEDIA);
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
this.mediaSequence = mediaSequence; this.mediaSequence = mediaSequence;
this.version = version; this.version = version;
this.targetDurationUs = targetDurationUs;
this.hasEndTag = hasEndTag; this.hasEndTag = hasEndTag;
this.hasProgramDateTime = hasProgramDateTime; this.hasProgramDateTime = hasProgramDateTime;
this.initializationSegment = initializationSegment; this.initializationSegment = initializationSegment;
...@@ -105,8 +107,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -105,8 +107,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
} }
public HlsMediaPlaylist copyWithStartTimeUs(long startTimeUs) { public HlsMediaPlaylist copyWithStartTimeUs(long startTimeUs) {
return new HlsMediaPlaylist(baseUri, startTimeUs, mediaSequence, version, hasEndTag, return new HlsMediaPlaylist(baseUri, startTimeUs, mediaSequence, version, targetDurationUs,
hasProgramDateTime, initializationSegment, segments); hasEndTag, hasProgramDateTime, initializationSegment, segments);
} }
} }
...@@ -67,6 +67,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -67,6 +67,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final Pattern REGEX_BANDWIDTH = Pattern.compile("BANDWIDTH=(\\d+)\\b"); private static final Pattern REGEX_BANDWIDTH = Pattern.compile("BANDWIDTH=(\\d+)\\b");
private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\""); private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)"); private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)");
private static final Pattern REGEX_TARGET_DURATION = Pattern.compile(TAG_TARGET_DURATION
+ ":(\\d+)\\b");
private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + ":(\\d+)\\b"); private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + ":(\\d+)\\b");
private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE
+ ":(\\d+)\\b"); + ":(\\d+)\\b");
...@@ -207,6 +209,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -207,6 +209,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
throws IOException { throws IOException {
int mediaSequence = 0; int mediaSequence = 0;
int version = 1; // Default version == 1. int version = 1; // Default version == 1.
long targetDurationUs = C.TIME_UNSET;
boolean hasEndTag = false; boolean hasEndTag = false;
Segment initializationSegment = null; Segment initializationSegment = null;
List<Segment> segments = new ArrayList<>(); List<Segment> segments = new ArrayList<>();
...@@ -239,6 +242,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -239,6 +242,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
initializationSegment = new Segment(uri, segmentByteRangeOffset, segmentByteRangeLength); initializationSegment = new Segment(uri, segmentByteRangeOffset, segmentByteRangeLength);
segmentByteRangeOffset = 0; segmentByteRangeOffset = 0;
segmentByteRangeLength = C.LENGTH_UNSET; segmentByteRangeLength = C.LENGTH_UNSET;
} else if (line.startsWith(TAG_TARGET_DURATION)) {
targetDurationUs = parseIntAttr(line, REGEX_TARGET_DURATION) * C.MICROS_PER_SECOND;
} else if (line.startsWith(TAG_MEDIA_SEQUENCE)) { } else if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
mediaSequence = parseIntAttr(line, REGEX_MEDIA_SEQUENCE); mediaSequence = parseIntAttr(line, REGEX_MEDIA_SEQUENCE);
segmentMediaSequence = mediaSequence; segmentMediaSequence = mediaSequence;
...@@ -300,8 +305,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -300,8 +305,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
hasEndTag = true; hasEndTag = true;
} }
} }
return new HlsMediaPlaylist(baseUri, playlistStartTimeUs, mediaSequence, version, hasEndTag, return new HlsMediaPlaylist(baseUri, playlistStartTimeUs, mediaSequence, version,
playlistStartTimeUs != 0, initializationSegment, segments); targetDurationUs, hasEndTag, playlistStartTimeUs != 0, initializationSegment, segments);
} }
private static String parseStringAttr(String line, Pattern pattern) throws ParserException { private static String parseStringAttr(String line, Pattern pattern) throws ParserException {
......
...@@ -17,9 +17,11 @@ package com.google.android.exoplayer2.source.hls.playlist; ...@@ -17,9 +17,11 @@ package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
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.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
...@@ -52,35 +54,37 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -52,35 +54,37 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
} }
/** /**
* Called when the playlist changes. * Called on playlist loading events.
*/ */
public interface PlaylistRefreshCallback { public interface PlaylistEventListener {
/** /**
* Called when the target playlist changes. * Called a playlist changes.
*/ */
void onPlaylistChanged(); void onPlaylistChanged();
/** /**
* Called if an error is encountered while loading the target playlist. * Called if an error is encountered while loading a playlist.
* *
* @param url The loaded url that caused the error. * @param url The loaded url that caused the error.
* @param error The loading error. * @param blacklistDurationMs The number of milliseconds for which the playlist has been
* blacklisted.
*/ */
void onPlaylistLoadError(HlsUrl url, IOException error); void onPlaylistBlacklisted(HlsUrl url, long blacklistDurationMs);
} }
/** /**
* Determines the minimum amount of time by which a media playlist segment's start time has to * The minimum number of milliseconds by which a media playlist segment's start time has to drift
* drift from the actual start time of the chunk it refers to for it to be adjusted. * from the actual start time of the chunk it refers to for it to be adjusted.
*/ */
private static final long TIMESTAMP_ADJUSTMENT_THRESHOLD_US = 500000; private static final long TIMESTAMP_ADJUSTMENT_THRESHOLD_US = 500000;
/** /**
* Period for refreshing playlists. * The minimum number of milliseconds that a url is kept as primary url, if no
* {@link #getPlaylistSnapshot} call is made for that url.
*/ */
private static final long PLAYLIST_REFRESH_PERIOD_MS = 5000; private static final long PRIMARY_URL_KEEPALIVE_MS = 15000;
private final Uri initialPlaylistUri; private final Uri initialPlaylistUri;
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
...@@ -89,11 +93,13 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -89,11 +93,13 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
private final IdentityHashMap<HlsUrl, MediaPlaylistBundle> playlistBundles; private final IdentityHashMap<HlsUrl, MediaPlaylistBundle> playlistBundles;
private final Handler playlistRefreshHandler; private final Handler playlistRefreshHandler;
private final PrimaryPlaylistListener primaryPlaylistListener; private final PrimaryPlaylistListener primaryPlaylistListener;
private final List<PlaylistEventListener> listeners;
private final Loader initialPlaylistLoader; private final Loader initialPlaylistLoader;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private HlsMasterPlaylist masterPlaylist; private HlsMasterPlaylist masterPlaylist;
private HlsUrl primaryHlsUrl; private HlsUrl primaryHlsUrl;
private HlsMediaPlaylist primaryUrlSnapshot;
private boolean isLive; private boolean isLive;
/** /**
...@@ -113,6 +119,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -113,6 +119,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
this.eventDispatcher = eventDispatcher; this.eventDispatcher = eventDispatcher;
this.minRetryCount = minRetryCount; this.minRetryCount = minRetryCount;
this.primaryPlaylistListener = primaryPlaylistListener; this.primaryPlaylistListener = primaryPlaylistListener;
listeners = new ArrayList<>();
initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist");
playlistParser = new HlsPlaylistParser(); playlistParser = new HlsPlaylistParser();
playlistBundles = new IdentityHashMap<>(); playlistBundles = new IdentityHashMap<>();
...@@ -120,6 +127,24 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -120,6 +127,24 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
} }
/** /**
* Registers a listener to receive events from the playlist tracker.
*
* @param listener The listener.
*/
public void addListener(PlaylistEventListener listener) {
listeners.add(listener);
}
/**
* Unregisters a listener.
*
* @param listener The listener to unregister.
*/
public void removeListener(PlaylistEventListener listener) {
listeners.remove(listener);
}
/**
* Starts tracking all the playlists related to the provided Uri. * Starts tracking all the playlists related to the provided Uri.
*/ */
public void start() { public void start() {
...@@ -147,7 +172,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -147,7 +172,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
* be null if no snapshot has been loaded yet. * be null if no snapshot has been loaded yet.
*/ */
public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) { public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) {
return playlistBundles.get(url).latestPlaylistSnapshot; return playlistBundles.get(url).getPlaylistSnapshot();
} }
/** /**
...@@ -163,12 +188,12 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -163,12 +188,12 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
} }
/** /**
* If the tracker is having trouble refreshing the primary playlist, this method throws the * If the tracker is having trouble refreshing the primary playlist or loading an irreplaceable
* underlying error. Otherwise, does nothing. * playlist, this method throws the underlying error. Otherwise, does nothing.
* *
* @throws IOException The underlying error. * @throws IOException The underlying error.
*/ */
public void maybeThrowPrimaryPlaylistRefreshError() throws IOException { public void maybeThrowPlaylistRefreshError() throws IOException {
initialPlaylistLoader.maybeThrowError(); initialPlaylistLoader.maybeThrowError();
if (primaryHlsUrl != null) { if (primaryHlsUrl != null) {
playlistBundles.get(primaryHlsUrl).mediaPlaylistLoader.maybeThrowError(); playlistBundles.get(primaryHlsUrl).mediaPlaylistLoader.maybeThrowError();
...@@ -176,16 +201,12 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -176,16 +201,12 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
} }
/** /**
* Triggers a playlist refresh and sets the callback to be called once the playlist referenced by * Triggers a playlist refresh and whitelists it.
* the provided {@link HlsUrl} changes.
* *
* @param key The {@link HlsUrl} of the playlist to be refreshed. * @param url The {@link HlsUrl} of the playlist to be refreshed.
* @param callback The callback.
*/ */
public void refreshPlaylist(HlsUrl key, PlaylistRefreshCallback callback) { public void refreshPlaylist(HlsUrl url) {
MediaPlaylistBundle bundle = playlistBundles.get(key); playlistBundles.get(url).loadPlaylist();
bundle.setCallback(callback);
bundle.loadPlaylist();
} }
/** /**
...@@ -206,6 +227,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -206,6 +227,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
*/ */
public void onChunkLoaded(HlsUrl hlsUrl, int chunkMediaSequence, long adjustedStartTimeUs) { public void onChunkLoaded(HlsUrl hlsUrl, int chunkMediaSequence, long adjustedStartTimeUs) {
playlistBundles.get(hlsUrl).adjustTimestampsOfPlaylist(chunkMediaSequence, adjustedStartTimeUs); playlistBundles.get(hlsUrl).adjustTimestampsOfPlaylist(chunkMediaSequence, adjustedStartTimeUs);
maybeSetPrimaryUrl(hlsUrl);
} }
// Loader.Callback implementation. // Loader.Callback implementation.
...@@ -257,11 +279,41 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -257,11 +279,41 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
// Internal methods. // Internal methods.
private boolean maybeSelectNewPrimaryUrl() {
List<HlsUrl> variants = masterPlaylist.variants;
int variantsSize = variants.size();
long currentTimeMs = SystemClock.elapsedRealtime();
for (int i = 0; i < variantsSize; i++) {
MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i));
if (currentTimeMs > bundle.blacklistUntilMs) {
primaryHlsUrl = bundle.playlistUrl;
bundle.loadPlaylist();
return true;
}
}
return false;
}
private void maybeSetPrimaryUrl(HlsUrl url) {
if (!masterPlaylist.variants.contains(url)) {
// Only allow variant urls to be chosen as primary.
return;
}
MediaPlaylistBundle currentPrimaryBundle = playlistBundles.get(primaryHlsUrl);
long primarySnapshotAccessAgeMs =
currentPrimaryBundle.lastSnapshotAccessTimeMs - SystemClock.elapsedRealtime();
if (primarySnapshotAccessAgeMs > PRIMARY_URL_KEEPALIVE_MS) {
primaryHlsUrl = url;
playlistBundles.get(primaryHlsUrl).loadPlaylist();
}
}
private void createBundles(List<HlsUrl> urls) { private void createBundles(List<HlsUrl> urls) {
int listSize = urls.size(); int listSize = urls.size();
long currentTimeMs = SystemClock.elapsedRealtime();
for (int i = 0; i < listSize; i++) { for (int i = 0; i < listSize; i++) {
HlsUrl url = urls.get(i); HlsUrl url = urls.get(i);
MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); MediaPlaylistBundle bundle = new MediaPlaylistBundle(url, currentTimeMs);
playlistBundles.put(urls.get(i), bundle); playlistBundles.put(urls.get(i), bundle);
} }
} }
...@@ -271,20 +323,30 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -271,20 +323,30 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
* *
* @param url The url of the playlist. * @param url The url of the playlist.
* @param newSnapshot The new snapshot. * @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. * @return True if a refresh should be scheduled.
*/ */
private boolean onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot, private boolean onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) {
boolean isFirstSnapshot) {
if (url == primaryHlsUrl) { if (url == primaryHlsUrl) {
if (isFirstSnapshot) { if (primaryUrlSnapshot == null) {
// This is the first primary url snapshot.
isLive = !newSnapshot.hasEndTag; isLive = !newSnapshot.hasEndTag;
} }
primaryUrlSnapshot = newSnapshot;
primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);
// If the primary playlist is not the final one, we should schedule a refresh.
return !newSnapshot.hasEndTag;
} }
return false; int listenersSize = listeners.size();
for (int i = 0; i < listenersSize; i++) {
listeners.get(i).onPlaylistChanged();
}
// If the primary playlist is not the final one, we should schedule a refresh.
return url == primaryHlsUrl && !newSnapshot.hasEndTag;
}
private void notifyPlaylistBlacklisting(HlsUrl url, long blacklistMs) {
int listenersSize = listeners.size();
for (int i = 0; i < listenersSize; i++) {
listeners.get(i).onPlaylistBlacklisted(url, blacklistMs);
}
} }
/** /**
...@@ -299,15 +361,16 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -299,15 +361,16 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
return oldPlaylist; return oldPlaylist;
} }
} }
HlsMediaPlaylist primaryPlaylistSnapshot = // TODO: Once playlist type support is added, the snapshot's age can be added by using the
playlistBundles.get(primaryHlsUrl).latestPlaylistSnapshot; // target duration.
long primarySnapshotStartTimeUs = primaryUrlSnapshot != null
? primaryUrlSnapshot.startTimeUs : 0;
if (oldPlaylist == null) { if (oldPlaylist == null) {
if (primaryPlaylistSnapshot == null if (newPlaylist.startTimeUs == primarySnapshotStartTimeUs) {
|| primaryPlaylistSnapshot.startTimeUs == newPlaylist.startTimeUs) {
// Playback has just started or is VOD so no adjustment is needed. // Playback has just started or is VOD so no adjustment is needed.
return newPlaylist; return newPlaylist;
} else { } else {
return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.startTimeUs); return newPlaylist.copyWithStartTimeUs(primarySnapshotStartTimeUs);
} }
} }
List<Segment> oldSegments = oldPlaylist.segments; List<Segment> oldSegments = oldPlaylist.segments;
...@@ -324,7 +387,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -324,7 +387,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
return newPlaylist.copyWithStartTimeUs(adjustedNewPlaylistStartTimeUs); return newPlaylist.copyWithStartTimeUs(adjustedNewPlaylistStartTimeUs);
} }
// No segments overlap, we assume the new playlist start coincides with the primary playlist. // No segments overlap, we assume the new playlist start coincides with the primary playlist.
return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.startTimeUs); return newPlaylist.copyWithStartTimeUs(primarySnapshotStartTimeUs);
} }
/** /**
...@@ -337,50 +400,49 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -337,50 +400,49 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
private final Loader mediaPlaylistLoader; private final Loader mediaPlaylistLoader;
private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable; private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable;
private PlaylistRefreshCallback callback; private HlsMediaPlaylist playlistSnapshot;
private HlsMediaPlaylist latestPlaylistSnapshot; private long lastSnapshotAccessTimeMs;
private long blacklistUntilMs;
public MediaPlaylistBundle(HlsUrl playlistUrl) {
this(playlistUrl, null);
}
public MediaPlaylistBundle(HlsUrl playlistUrl, HlsMediaPlaylist initialSnapshot) { public MediaPlaylistBundle(HlsUrl playlistUrl, long initialLastSnapshotAccessTimeMs) {
this.playlistUrl = playlistUrl; this.playlistUrl = playlistUrl;
latestPlaylistSnapshot = initialSnapshot; lastSnapshotAccessTimeMs = initialLastSnapshotAccessTimeMs;
mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist"); mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist");
mediaPlaylistLoadable = new ParsingLoadable<>(dataSourceFactory.createDataSource(), mediaPlaylistLoadable = new ParsingLoadable<>(dataSourceFactory.createDataSource(),
UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST,
playlistParser); playlistParser);
} }
public HlsMediaPlaylist getPlaylistSnapshot() {
lastSnapshotAccessTimeMs = SystemClock.elapsedRealtime();
return playlistSnapshot;
}
public void release() { public void release() {
mediaPlaylistLoader.release(); mediaPlaylistLoader.release();
} }
public void loadPlaylist() { public void loadPlaylist() {
blacklistUntilMs = 0;
if (!mediaPlaylistLoader.isLoading()) { if (!mediaPlaylistLoader.isLoading()) {
mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount);
} }
} }
public void setCallback(PlaylistRefreshCallback callback) { public void adjustTimestampsOfPlaylist(int chunkMediaSequence, long adjustedChunkStartTimeUs) {
this.callback = callback; int indexOfChunk = chunkMediaSequence - playlistSnapshot.mediaSequence;
} if (playlistSnapshot.hasProgramDateTime || indexOfChunk < 0) {
public void adjustTimestampsOfPlaylist(int chunkMediaSequence, long adjustedStartTimeUs) {
int indexOfChunk = chunkMediaSequence - latestPlaylistSnapshot.mediaSequence;
if (latestPlaylistSnapshot.hasProgramDateTime || indexOfChunk < 0) {
return; return;
} }
Segment actualSegment = latestPlaylistSnapshot.segments.get(indexOfChunk); Segment actualSegment = playlistSnapshot.segments.get(indexOfChunk);
long segmentAbsoluteStartTimeUs = long segmentAbsoluteStartTimeUs =
actualSegment.relativeStartTimeUs + latestPlaylistSnapshot.startTimeUs; actualSegment.relativeStartTimeUs + playlistSnapshot.startTimeUs;
long timestampDriftUs = Math.abs(segmentAbsoluteStartTimeUs - adjustedStartTimeUs); long timestampDriftUs = Math.abs(segmentAbsoluteStartTimeUs - adjustedChunkStartTimeUs);
if (timestampDriftUs < TIMESTAMP_ADJUSTMENT_THRESHOLD_US) { if (timestampDriftUs < TIMESTAMP_ADJUSTMENT_THRESHOLD_US) {
return; return;
} }
latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithStartTimeUs( playlistSnapshot = playlistSnapshot.copyWithStartTimeUs(
adjustedStartTimeUs - actualSegment.relativeStartTimeUs); adjustedChunkStartTimeUs - actualSegment.relativeStartTimeUs);
} }
// Loader.Callback implementation. // Loader.Callback implementation.
...@@ -403,18 +465,21 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -403,18 +465,21 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
@Override @Override
public int onLoadError(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs, public int onLoadError(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs, IOException error) { long loadDurationMs, IOException error) {
// TODO: Change primary playlist if this is the primary playlist bundle.
boolean isFatal = error instanceof ParserException; boolean isFatal = error instanceof ParserException;
eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded(), error, isFatal); loadDurationMs, loadable.bytesLoaded(), error, isFatal);
if (callback != null) {
callback.onPlaylistLoadError(playlistUrl, error);
}
if (isFatal) { if (isFatal) {
return Loader.DONT_RETRY_FATAL; return Loader.DONT_RETRY_FATAL;
} else {
return primaryHlsUrl == playlistUrl ? Loader.RETRY : Loader.DONT_RETRY;
} }
boolean shouldRetry = true;
if (ChunkedTrackBlacklistUtil.shouldBlacklist(error)) {
blacklistUntilMs =
SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS;
notifyPlaylistBlacklisting(playlistUrl,
ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS);
shouldRetry = primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl();
}
return shouldRetry ? Loader.RETRY : Loader.DONT_RETRY;
} }
// Runnable implementation. // Runnable implementation.
...@@ -427,21 +492,19 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable ...@@ -427,21 +492,19 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
// Internal methods. // Internal methods.
private void processLoadedPlaylist(HlsMediaPlaylist loadedMediaPlaylist) { private void processLoadedPlaylist(HlsMediaPlaylist loadedMediaPlaylist) {
HlsMediaPlaylist oldPlaylist = latestPlaylistSnapshot; HlsMediaPlaylist oldPlaylist = playlistSnapshot;
latestPlaylistSnapshot = adjustPlaylistTimestamps(oldPlaylist, loadedMediaPlaylist); playlistSnapshot = adjustPlaylistTimestamps(oldPlaylist, loadedMediaPlaylist);
boolean shouldScheduleRefresh; long refreshDelayUs = C.TIME_UNSET;
if (oldPlaylist != latestPlaylistSnapshot) { if (oldPlaylist != playlistSnapshot) {
if (callback != null) { if (onPlaylistUpdated(playlistUrl, playlistSnapshot)) {
callback.onPlaylistChanged(); refreshDelayUs = playlistSnapshot.targetDurationUs;
callback = null;
} }
shouldScheduleRefresh = onPlaylistUpdated(playlistUrl, latestPlaylistSnapshot, } else if (!loadedMediaPlaylist.hasEndTag) {
oldPlaylist == null); refreshDelayUs = playlistSnapshot.targetDurationUs / 2;
} else {
shouldScheduleRefresh = !loadedMediaPlaylist.hasEndTag;
} }
if (shouldScheduleRefresh) { if (refreshDelayUs != C.TIME_UNSET) {
playlistRefreshHandler.postDelayed(this, PLAYLIST_REFRESH_PERIOD_MS); // See HLS spec v20, section 6.3.4 for more information on media playlist refreshing.
playlistRefreshHandler.postDelayed(this, C.usToMs(refreshDelayUs));
} }
} }
......
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