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
This commit is contained in:
aquilescanta 2016-12-07 03:58:59 -08:00 committed by Oliver Woodman
parent 8765b1981c
commit 1cbc0fc678
8 changed files with 220 additions and 140 deletions

View file

@ -51,9 +51,9 @@ public final class ChunkedTrackBlacklistUtil {
/**
* Blacklists {@code trackSelectionIndex} in {@code trackSelection} for
* {@code blacklistDurationMs} if {@code e} is an {@link InvalidResponseCodeException} with
* {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. Else does nothing. Note
* that blacklisting will fail if the track is the only non-blacklisted track in the selection.
* {@code blacklistDurationMs} if calling {@link #shouldBlacklist(Exception)} for {@code e}
* returns true. Else does nothing. Note that blacklisting will fail if the track is the only
* non-blacklisted track in the selection.
*
* @param trackSelection The track selection.
* @param trackSelectionIndex The index in the selection to consider blacklisting.
@ -63,24 +63,33 @@ public final class ChunkedTrackBlacklistUtil {
*/
public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex,
Exception e, long blacklistDurationMs) {
if (trackSelection.length() == 1) {
// Blacklisting won't ever work if there's only one track in the selection.
return false;
}
if (e instanceof InvalidResponseCodeException) {
InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e;
int responseCode = responseCodeException.responseCode;
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;
if (shouldBlacklist(e)) {
boolean blacklisted = trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs);
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) {
int responseCode = ((InvalidResponseCodeException) e).responseCode;
return responseCode == 404 || responseCode == 410;
}
return false;
}

View file

@ -278,9 +278,10 @@ import java.util.Locale;
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
null);
out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex],
trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs,
startTimeUs + segment.durationUs, chunkMediaSequence, segment.discontinuitySequenceNumber,
isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv);
trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence,
segment.discontinuitySequenceNumber, isTimestampMaster, timestampAdjuster, previous,
encryptionKey, encryptionIv);
}
/**
@ -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 error The error.
* @param url The url that references the blacklisted playlist.
* @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);
if (trackGroupIndex == C.INDEX_UNSET) {
// The url is not handled by this chunk source.
return;
if (trackGroupIndex != C.INDEX_UNSET) {
int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex);
if (trackSelectionIndex != C.INDEX_UNSET) {
trackSelection.blacklist(trackSelectionIndex, blacklistMs);
}
}
ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection,
trackSelection.indexOf(trackGroupIndex), error);
}
// Private methods.

View file

@ -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.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.util.ArrayList;
@ -42,7 +41,7 @@ import java.util.List;
* A {@link MediaPeriod} that loads an HLS stream.
*/
public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback,
HlsPlaylistTracker.PlaylistRefreshCallback {
HlsPlaylistTracker.PlaylistEventListener {
private final HlsPlaylistTracker playlistTracker;
private final DataSource.Factory dataSourceFactory;
@ -52,7 +51,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private final IdentityHashMap<SampleStream, Integer> streamWrapperIndices;
private final TimestampAdjusterProvider timestampAdjusterProvider;
private final Handler continueLoadingHandler;
private final Loader manifestFetcher;
private final long preparePositionUs;
private Callback callback;
@ -74,13 +72,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
streamWrapperIndices = new IdentityHashMap<>();
timestampAdjusterProvider = new TimestampAdjusterProvider();
continueLoadingHandler = new Handler();
manifestFetcher = new Loader("Loader:ManifestFetcher");
preparePositionUs = positionUs;
}
public void release() {
playlistTracker.removeListener(this);
continueLoadingHandler.removeCallbacksAndMessages(null);
manifestFetcher.release();
if (sampleStreamWrappers != null) {
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.release();
@ -90,15 +87,14 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
@Override
public void prepare(Callback callback) {
playlistTracker.addListener(this);
this.callback = callback;
buildAndPrepareSampleStreamWrappers();
}
@Override
public void maybeThrowPrepareError() throws IOException {
if (sampleStreamWrappers == null) {
manifestFetcher.maybeThrowError();
} else {
if (sampleStreamWrappers != null) {
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.maybeThrowPrepareError();
}
@ -255,7 +251,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
@Override
public void onPlaylistRefreshRequired(HlsUrl url) {
playlistTracker.refreshPlaylist(url, this);
playlistTracker.refreshPlaylist(url);
}
@Override
@ -271,22 +267,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
@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();
}
}
continuePreparingOrLoading();
}
@Override
public void onPlaylistLoadError(HlsUrl url, IOException error) {
for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) {
sampleStreamWrapper.onPlaylistLoadError(url, error);
public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) {
for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) {
streamWrapper.onPlaylistBlacklisted(url, blacklistMs);
}
callback.onContinueLoadingRequested(this);
continuePreparingOrLoading();
}
// Internal methods.
@ -363,6 +352,17 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
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) {
String codecs = variant.format.codecs;
if (TextUtils.isEmpty(codecs)) {

View file

@ -77,7 +77,7 @@ public final class HlsMediaSource implements MediaSource,
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
playlistTracker.maybeThrowPrimaryPlaylistRefreshError();
playlistTracker.maybeThrowPlaylistRefreshError();
}
@Override

View file

@ -279,8 +279,8 @@ import java.util.LinkedList;
chunkSource.setIsTimestampMaster(isTimestampMaster);
}
public void onPlaylistLoadError(HlsUrl url, IOException error) {
chunkSource.onPlaylistLoadError(url, error);
public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) {
chunkSource.onPlaylistBlacklisted(url, blacklistMs);
}
// SampleStream implementation.

View file

@ -68,19 +68,21 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final long startTimeUs;
public final int mediaSequence;
public final int version;
public final Segment initializationSegment;
public final List<Segment> segments;
public final long targetDurationUs;
public final boolean hasEndTag;
public final boolean hasProgramDateTime;
public final Segment initializationSegment;
public final List<Segment> segments;
public final long durationUs;
public HlsMediaPlaylist(String baseUri, long startTimeUs, int mediaSequence, int version,
boolean hasEndTag, boolean hasProgramDateTime, Segment initializationSegment,
List<Segment> segments) {
public HlsMediaPlaylist(String baseUri, long startTimeUs, int mediaSequence,
int version, long targetDurationUs, boolean hasEndTag, boolean hasProgramDateTime,
Segment initializationSegment, List<Segment> segments) {
super(baseUri, HlsPlaylist.TYPE_MEDIA);
this.startTimeUs = startTimeUs;
this.mediaSequence = mediaSequence;
this.version = version;
this.targetDurationUs = targetDurationUs;
this.hasEndTag = hasEndTag;
this.hasProgramDateTime = hasProgramDateTime;
this.initializationSegment = initializationSegment;
@ -105,8 +107,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
}
public HlsMediaPlaylist copyWithStartTimeUs(long startTimeUs) {
return new HlsMediaPlaylist(baseUri, startTimeUs, mediaSequence, version, hasEndTag,
hasProgramDateTime, initializationSegment, segments);
return new HlsMediaPlaylist(baseUri, startTimeUs, mediaSequence, version, targetDurationUs,
hasEndTag, hasProgramDateTime, initializationSegment, segments);
}
}

View file

@ -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_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
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_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE
+ ":(\\d+)\\b");
@ -207,6 +209,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
throws IOException {
int mediaSequence = 0;
int version = 1; // Default version == 1.
long targetDurationUs = C.TIME_UNSET;
boolean hasEndTag = false;
Segment initializationSegment = null;
List<Segment> segments = new ArrayList<>();
@ -239,6 +242,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
initializationSegment = new Segment(uri, segmentByteRangeOffset, segmentByteRangeLength);
segmentByteRangeOffset = 0;
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)) {
mediaSequence = parseIntAttr(line, REGEX_MEDIA_SEQUENCE);
segmentMediaSequence = mediaSequence;
@ -300,8 +305,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
hasEndTag = true;
}
}
return new HlsMediaPlaylist(baseUri, playlistStartTimeUs, mediaSequence, version, hasEndTag,
playlistStartTimeUs != 0, initializationSegment, segments);
return new HlsMediaPlaylist(baseUri, playlistStartTimeUs, mediaSequence, version,
targetDurationUs, hasEndTag, playlistStartTimeUs != 0, initializationSegment, segments);
}
private static String parseStringAttr(String line, Pattern pattern) throws ParserException {

View file

@ -17,9 +17,11 @@ package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
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.chunk.ChunkedTrackBlacklistUtil;
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.upstream.DataSource;
@ -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();
/**
* 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 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
* drift from the actual start time of the chunk it refers to for it to be adjusted.
* The minimum number of milliseconds by which a media playlist segment's start time has to drift
* 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;
/**
* 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 DataSource.Factory dataSourceFactory;
@ -89,11 +93,13 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
private final IdentityHashMap<HlsUrl, MediaPlaylistBundle> playlistBundles;
private final Handler playlistRefreshHandler;
private final PrimaryPlaylistListener primaryPlaylistListener;
private final List<PlaylistEventListener> listeners;
private final Loader initialPlaylistLoader;
private final EventDispatcher eventDispatcher;
private HlsMasterPlaylist masterPlaylist;
private HlsUrl primaryHlsUrl;
private HlsMediaPlaylist primaryUrlSnapshot;
private boolean isLive;
/**
@ -113,12 +119,31 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
this.eventDispatcher = eventDispatcher;
this.minRetryCount = minRetryCount;
this.primaryPlaylistListener = primaryPlaylistListener;
listeners = new ArrayList<>();
initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist");
playlistParser = new HlsPlaylistParser();
playlistBundles = new IdentityHashMap<>();
playlistRefreshHandler = new Handler();
}
/**
* 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.
*/
@ -147,7 +172,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
* be null if no snapshot has been loaded yet.
*/
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
}
/**
* If the tracker is having trouble refreshing the primary playlist, this method throws the
* underlying error. Otherwise, does nothing.
* If the tracker is having trouble refreshing the primary playlist or loading an irreplaceable
* playlist, this method throws the underlying error. Otherwise, does nothing.
*
* @throws IOException The underlying error.
*/
public void maybeThrowPrimaryPlaylistRefreshError() throws IOException {
public void maybeThrowPlaylistRefreshError() throws IOException {
initialPlaylistLoader.maybeThrowError();
if (primaryHlsUrl != null) {
playlistBundles.get(primaryHlsUrl).mediaPlaylistLoader.maybeThrowError();
@ -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
* the provided {@link HlsUrl} changes.
* Triggers a playlist refresh and whitelists it.
*
* @param key The {@link HlsUrl} of the playlist to be refreshed.
* @param callback The callback.
* @param url The {@link HlsUrl} of the playlist to be refreshed.
*/
public void refreshPlaylist(HlsUrl key, PlaylistRefreshCallback callback) {
MediaPlaylistBundle bundle = playlistBundles.get(key);
bundle.setCallback(callback);
bundle.loadPlaylist();
public void refreshPlaylist(HlsUrl url) {
playlistBundles.get(url).loadPlaylist();
}
/**
@ -206,6 +227,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
*/
public void onChunkLoaded(HlsUrl hlsUrl, int chunkMediaSequence, long adjustedStartTimeUs) {
playlistBundles.get(hlsUrl).adjustTimestampsOfPlaylist(chunkMediaSequence, adjustedStartTimeUs);
maybeSetPrimaryUrl(hlsUrl);
}
// Loader.Callback implementation.
@ -257,11 +279,41 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
// 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) {
int listSize = urls.size();
long currentTimeMs = SystemClock.elapsedRealtime();
for (int i = 0; i < listSize; i++) {
HlsUrl url = urls.get(i);
MediaPlaylistBundle bundle = new MediaPlaylistBundle(url);
MediaPlaylistBundle bundle = new MediaPlaylistBundle(url, currentTimeMs);
playlistBundles.put(urls.get(i), bundle);
}
}
@ -271,20 +323,30 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
*
* @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) {
private boolean onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) {
if (url == primaryHlsUrl) {
if (isFirstSnapshot) {
if (primaryUrlSnapshot == null) {
// This is the first primary url snapshot.
isLive = !newSnapshot.hasEndTag;
}
primaryUrlSnapshot = 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
return oldPlaylist;
}
}
HlsMediaPlaylist primaryPlaylistSnapshot =
playlistBundles.get(primaryHlsUrl).latestPlaylistSnapshot;
// TODO: Once playlist type support is added, the snapshot's age can be added by using the
// target duration.
long primarySnapshotStartTimeUs = primaryUrlSnapshot != null
? primaryUrlSnapshot.startTimeUs : 0;
if (oldPlaylist == null) {
if (primaryPlaylistSnapshot == null
|| primaryPlaylistSnapshot.startTimeUs == newPlaylist.startTimeUs) {
if (newPlaylist.startTimeUs == primarySnapshotStartTimeUs) {
// Playback has just started or is VOD so no adjustment is needed.
return newPlaylist;
} else {
return newPlaylist.copyWithStartTimeUs(primaryPlaylistSnapshot.startTimeUs);
return newPlaylist.copyWithStartTimeUs(primarySnapshotStartTimeUs);
}
}
List<Segment> oldSegments = oldPlaylist.segments;
@ -324,7 +387,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
return newPlaylist.copyWithStartTimeUs(adjustedNewPlaylistStartTimeUs);
}
// 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
private final Loader mediaPlaylistLoader;
private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable;
private PlaylistRefreshCallback callback;
private HlsMediaPlaylist latestPlaylistSnapshot;
private HlsMediaPlaylist playlistSnapshot;
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;
latestPlaylistSnapshot = initialSnapshot;
lastSnapshotAccessTimeMs = initialLastSnapshotAccessTimeMs;
mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist");
mediaPlaylistLoadable = new ParsingLoadable<>(dataSourceFactory.createDataSource(),
UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST,
playlistParser);
}
public HlsMediaPlaylist getPlaylistSnapshot() {
lastSnapshotAccessTimeMs = SystemClock.elapsedRealtime();
return playlistSnapshot;
}
public void release() {
mediaPlaylistLoader.release();
}
public void loadPlaylist() {
blacklistUntilMs = 0;
if (!mediaPlaylistLoader.isLoading()) {
mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount);
}
}
public void setCallback(PlaylistRefreshCallback callback) {
this.callback = callback;
}
public void adjustTimestampsOfPlaylist(int chunkMediaSequence, long adjustedStartTimeUs) {
int indexOfChunk = chunkMediaSequence - latestPlaylistSnapshot.mediaSequence;
if (latestPlaylistSnapshot.hasProgramDateTime || indexOfChunk < 0) {
public void adjustTimestampsOfPlaylist(int chunkMediaSequence, long adjustedChunkStartTimeUs) {
int indexOfChunk = chunkMediaSequence - playlistSnapshot.mediaSequence;
if (playlistSnapshot.hasProgramDateTime || indexOfChunk < 0) {
return;
}
Segment actualSegment = latestPlaylistSnapshot.segments.get(indexOfChunk);
Segment actualSegment = playlistSnapshot.segments.get(indexOfChunk);
long segmentAbsoluteStartTimeUs =
actualSegment.relativeStartTimeUs + latestPlaylistSnapshot.startTimeUs;
long timestampDriftUs = Math.abs(segmentAbsoluteStartTimeUs - adjustedStartTimeUs);
actualSegment.relativeStartTimeUs + playlistSnapshot.startTimeUs;
long timestampDriftUs = Math.abs(segmentAbsoluteStartTimeUs - adjustedChunkStartTimeUs);
if (timestampDriftUs < TIMESTAMP_ADJUSTMENT_THRESHOLD_US) {
return;
}
latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithStartTimeUs(
adjustedStartTimeUs - actualSegment.relativeStartTimeUs);
playlistSnapshot = playlistSnapshot.copyWithStartTimeUs(
adjustedChunkStartTimeUs - actualSegment.relativeStartTimeUs);
}
// Loader.Callback implementation.
@ -403,18 +465,21 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
@Override
public int onLoadError(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs, IOException error) {
// TODO: Change primary playlist if this is the primary playlist bundle.
boolean isFatal = error instanceof ParserException;
eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded(), error, isFatal);
if (callback != null) {
callback.onPlaylistLoadError(playlistUrl, error);
}
if (isFatal) {
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.
@ -427,21 +492,19 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
// 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;
HlsMediaPlaylist oldPlaylist = playlistSnapshot;
playlistSnapshot = adjustPlaylistTimestamps(oldPlaylist, loadedMediaPlaylist);
long refreshDelayUs = C.TIME_UNSET;
if (oldPlaylist != playlistSnapshot) {
if (onPlaylistUpdated(playlistUrl, playlistSnapshot)) {
refreshDelayUs = playlistSnapshot.targetDurationUs;
}
shouldScheduleRefresh = onPlaylistUpdated(playlistUrl, latestPlaylistSnapshot,
oldPlaylist == null);
} else {
shouldScheduleRefresh = !loadedMediaPlaylist.hasEndTag;
} else if (!loadedMediaPlaylist.hasEndTag) {
refreshDelayUs = playlistSnapshot.targetDurationUs / 2;
}
if (shouldScheduleRefresh) {
playlistRefreshHandler.postDelayed(this, PLAYLIST_REFRESH_PERIOD_MS);
if (refreshDelayUs != C.TIME_UNSET) {
// See HLS spec v20, section 6.3.4 for more information on media playlist refreshing.
playlistRefreshHandler.postDelayed(this, C.usToMs(refreshDelayUs));
}
}