mirror of
https://github.com/samsonjs/media.git
synced 2026-03-26 09:35:47 +00:00
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:
parent
8765b1981c
commit
1cbc0fc678
8 changed files with 220 additions and 140 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ public final class HlsMediaSource implements MediaSource,
|
|||
|
||||
@Override
|
||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||
playlistTracker.maybeThrowPrimaryPlaylistRefreshError();
|
||||
playlistTracker.maybeThrowPlaylistRefreshError();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue