From f09b86a1bb20bd6054abd38ad16972897e614952 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 24 Mar 2016 07:16:49 -0700 Subject: [PATCH] Fix SampleSource limbo state - Part II This change optimizes startup and track selection for HLS. Changes in HlsChunkSource avoid unnecessary re-requests for media playlists. Changes in HlsSampleSource optimize exit from the limbo state (i.e. when endTrackSelection is first called). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=118026962 --- .../android/exoplayer/hls/HlsChunkSource.java | 145 +++++++++--------- .../exoplayer/hls/HlsSampleSource.java | 37 ++--- 2 files changed, 92 insertions(+), 90 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index b4d95c40c9..6d179dbef5 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -99,15 +99,14 @@ public class HlsChunkSource { private byte[] encryptionIv; // Properties of exposed tracks. - private Variant[] exposedVariants; + private Variant[] variants; + private HlsMediaPlaylist[] variantPlaylists; + private long[] variantLastPlaylistLoadTimesMs; // Properties of enabled variants. private Variant[] enabledVariants; - private HlsMediaPlaylist[] enabledVariantPlaylists; - private long[] enabledVariantLastPlaylistLoadTimesMs; private long[] enabledVariantBlacklistTimes; private boolean[] enabledVariantBlacklistFlags; - private int selectedVariantIndex; /** * @param manifestFetcher A fetcher for the playlist. @@ -182,8 +181,11 @@ public class HlsChunkSource { Collections.emptyList(), Collections.emptyList(), null, null); } processMasterPlaylist(masterPlaylist); - if (exposedVariants.length > 0) { - // TODO[REFACTOR]: Come up with a sane default here. + if (variants.length > 0) { + if (playlist.type == HlsPlaylist.TYPE_MEDIA) { + setMediaPlaylist(0, (HlsMediaPlaylist) playlist); + } + // Select the first variant listed in the master playlist. selectTracks(new int[] {0}); } } @@ -221,7 +223,7 @@ public class HlsChunkSource { * @return The number of tracks. */ public int getTrackCount() { - return exposedVariants.length; + return variants.length; } /** @@ -233,7 +235,7 @@ public class HlsChunkSource { * @return The format of the track. */ public Format getTrackFormat(int index) { - return exposedVariants[index].format; + return variants[index].format; } /** @@ -264,17 +266,15 @@ public class HlsChunkSource { * This method should only be called after the source has been prepared. * * @param tracks The track indices. + * @return True if one or more tracks was unselected. False otherwise. */ - public void selectTracks(int[] tracks) { - evaluation.clear(); - enabledVariants = new Variant[tracks.length]; - enabledVariantPlaylists = new HlsMediaPlaylist[enabledVariants.length]; - enabledVariantLastPlaylistLoadTimesMs = new long[enabledVariants.length]; - enabledVariantBlacklistTimes = new long[enabledVariants.length]; - enabledVariantBlacklistFlags = new boolean[enabledVariants.length]; + public boolean selectTracks(int[] tracks) { + Variant[] oldEnabledVariants = enabledVariants; + // Construct and sort the enabled variants. + enabledVariants = new Variant[tracks.length]; for (int i = 0; i < tracks.length; i++) { - enabledVariants[i] = exposedVariants[tracks[i]]; + enabledVariants[i] = variants[tracks[i]]; } Arrays.sort(enabledVariants, new Comparator() { private final Comparator formatComparator = @@ -284,14 +284,28 @@ public class HlsChunkSource { return formatComparator.compare(first.format, second.format); } }); + + // Reset the enabled variant blacklist flags. + enabledVariantBlacklistTimes = new long[enabledVariants.length]; + enabledVariantBlacklistFlags = new boolean[enabledVariants.length]; + if (enabledVariants.length > 1) { - // TODO[REFACTOR]: We need to disable this at some point. Format[] formats = new Format[enabledVariants.length]; for (int i = 0; i < formats.length; i++) { formats[i] = enabledVariants[i].format; } + // TODO[REFACTOR]: We need to disable this at some point. adaptiveFormatEvaluator.enable(formats); } + + if (oldEnabledVariants != null) { + for (Variant oldVariant : oldEnabledVariants) { + if (!Util.contains(enabledVariants, oldVariant)) { + return true; + } + } + } + return false; } /** @@ -327,22 +341,21 @@ public class HlsChunkSource { */ public void getChunkOperation(TsChunk previousTsChunk, long playbackPositionUs, ChunkOperationHolder out) { - int nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs); + int variantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs); boolean switchingVariant = previousTsChunk != null - && enabledVariants[nextVariantIndex].format != previousTsChunk.format; + && variants[variantIndex].format != previousTsChunk.format; - HlsMediaPlaylist mediaPlaylist = enabledVariantPlaylists[nextVariantIndex]; + HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex]; if (mediaPlaylist == null) { // We don't have the media playlist for the next variant. Request it now. - out.chunk = newMediaPlaylistChunk(nextVariantIndex); + out.chunk = newMediaPlaylistChunk(variantIndex); return; } - selectedVariantIndex = nextVariantIndex; int chunkMediaSequence = 0; if (live) { if (previousTsChunk == null) { - chunkMediaSequence = getLiveStartChunkMediaSequence(nextVariantIndex); + chunkMediaSequence = getLiveStartChunkMediaSequence(variantIndex); } else { chunkMediaSequence = switchingVariant ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; @@ -366,8 +379,8 @@ public class HlsChunkSource { if (chunkIndex >= mediaPlaylist.segments.size()) { if (!mediaPlaylist.live) { out.endOfStream = true; - } else if (shouldRerequestLiveMediaPlaylist(nextVariantIndex)) { - out.chunk = newMediaPlaylistChunk(nextVariantIndex); + } else if (shouldRerequestLiveMediaPlaylist(variantIndex)) { + out.chunk = newMediaPlaylistChunk(variantIndex); } return; } @@ -380,7 +393,7 @@ public class HlsChunkSource { Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); if (!keyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. - out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex); + out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, variantIndex); return; } if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) { @@ -409,7 +422,7 @@ public class HlsChunkSource { } long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND); int trigger = Chunk.TRIGGER_UNSPECIFIED; - Format format = enabledVariants[selectedVariantIndex].format; + Format format = variants[variantIndex].format; // Configure the extractor that will read the chunk. HlsExtractorWrapper extractorWrapper; @@ -449,7 +462,7 @@ public class HlsChunkSource { return; } int workaroundFlags = 0; - String codecs = enabledVariants[selectedVariantIndex].codecs; + String codecs = variants[variantIndex].codecs; if (!TextUtils.isEmpty(codecs)) { // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really // exist. If we know from the codec attribute that they don't exist, then we can explicitly @@ -509,17 +522,7 @@ public class HlsChunkSource { InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e; int responseCode = responseCodeException.responseCode; if (responseCode == 404 || responseCode == 410) { - int enabledVariantIndex; - if (chunk instanceof TsChunk) { - TsChunk tsChunk = (TsChunk) chunk; - enabledVariantIndex = getEnabledVariantIndex(tsChunk.format); - } else if (chunk instanceof MediaPlaylistChunk) { - MediaPlaylistChunk playlistChunk = (MediaPlaylistChunk) chunk; - enabledVariantIndex = playlistChunk.variantIndex; - } else { - EncryptionKeyChunk encryptionChunk = (EncryptionKeyChunk) chunk; - enabledVariantIndex = encryptionChunk.variantIndex; - } + int enabledVariantIndex = getEnabledVariantIndex(chunk.format); boolean alreadyBlacklisted = enabledVariantBlacklistFlags[enabledVariantIndex]; enabledVariantBlacklistFlags[enabledVariantIndex] = true; enabledVariantBlacklistTimes[enabledVariantIndex] = SystemClock.elapsedRealtime(); @@ -528,7 +531,7 @@ public class HlsChunkSource { Log.w(TAG, "Already blacklisted variant (" + responseCode + "): " + chunk.dataSpec.uri); return false; - } else if (!allVariantsBlacklisted()) { + } else if (!allEnabledVariantsBlacklisted()) { // We've handled the 404/410 by blacklisting the variant. Log.w(TAG, "Blacklisted variant (" + responseCode + "): " + chunk.dataSpec.uri); @@ -549,13 +552,15 @@ public class HlsChunkSource { private void processMasterPlaylist(HlsMasterPlaylist playlist) { if (type == TYPE_SUBTITLE || type == TYPE_AUDIO) { - List variants = type == TYPE_AUDIO ? playlist.audios : playlist.subtitles; - if (variants != null && !variants.isEmpty()) { - exposedVariants = new Variant[variants.size()]; - variants.toArray(exposedVariants); + List variantList = type == TYPE_AUDIO ? playlist.audios : playlist.subtitles; + if (variantList != null && !variantList.isEmpty()) { + variants = new Variant[variantList.size()]; + variantList.toArray(variants); } else { - exposedVariants = new Variant[0]; + variants = new Variant[0]; } + variantPlaylists = new HlsMediaPlaylist[variants.length]; + variantLastPlaylistLoadTimesMs = new long[variants.length]; return; } @@ -585,8 +590,10 @@ public class HlsChunkSource { // Leave the enabled variants unchanged. They're likely either all video or all audio. } - exposedVariants = new Variant[enabledVariantList.size()]; - enabledVariantList.toArray(exposedVariants); + variants = new Variant[enabledVariantList.size()]; + enabledVariantList.toArray(variants); + variantPlaylists = new HlsMediaPlaylist[variants.length]; + variantLastPlaylistLoadTimesMs = new long[variants.length]; } private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) { @@ -621,40 +628,41 @@ public class HlsChunkSource { evaluation.format = enabledVariants[0].format; evaluation.trigger = Chunk.TRIGGER_MANUAL; } - for (int i = 0; i < enabledVariants.length; i++) { - if (enabledVariants[i].format == evaluation.format) { + for (int i = 0; i < variants.length; i++) { + if (variants[i].format == evaluation.format) { return i; } } throw new IllegalStateException(); } - private boolean shouldRerequestLiveMediaPlaylist(int nextVariantIndex) { - // Don't re-request media playlist more often than one-half of the target duration. - HlsMediaPlaylist mediaPlaylist = enabledVariantPlaylists[nextVariantIndex]; + private boolean shouldRerequestLiveMediaPlaylist(int variantIndex) { + HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex]; long timeSinceLastMediaPlaylistLoadMs = - SystemClock.elapsedRealtime() - enabledVariantLastPlaylistLoadTimesMs[nextVariantIndex]; + SystemClock.elapsedRealtime() - variantLastPlaylistLoadTimesMs[variantIndex]; + // Don't re-request media playlist more often than one-half of the target duration. return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2; } private int getLiveStartChunkMediaSequence(int variantIndex) { + HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex]; // For live start playback from the third chunk from the end. - HlsMediaPlaylist mediaPlaylist = enabledVariantPlaylists[variantIndex]; int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0; return chunkIndex + mediaPlaylist.mediaSequence; } private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) { - Uri mediaPlaylistUri = UriUtil.resolveToUri(baseUri, enabledVariants[variantIndex].url); + Uri mediaPlaylistUri = UriUtil.resolveToUri(baseUri, variants[variantIndex].url); DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null, DataSpec.FLAG_ALLOW_GZIP); - return new MediaPlaylistChunk(dataSource, dataSpec, scratchSpace, playlistParser, variantIndex, - mediaPlaylistUri.toString()); + return new MediaPlaylistChunk(dataSource, dataSpec, variants[variantIndex].format, scratchSpace, + playlistParser, variantIndex, mediaPlaylistUri.toString()); } private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex) { DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null, DataSpec.FLAG_ALLOW_GZIP); - return new EncryptionKeyChunk(dataSource, dataSpec, scratchSpace, iv, variantIndex); + return new EncryptionKeyChunk(dataSource, dataSpec, variants[variantIndex].format, scratchSpace, + iv); } private void setEncryptionData(Uri keyUri, String iv, byte[] secretKey) { @@ -685,13 +693,13 @@ public class HlsChunkSource { } private void setMediaPlaylist(int variantIndex, HlsMediaPlaylist mediaPlaylist) { - enabledVariantLastPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime(); - enabledVariantPlaylists[variantIndex] = mediaPlaylist; + variantLastPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime(); + variantPlaylists[variantIndex] = mediaPlaylist; live |= mediaPlaylist.live; durationUs = live ? C.UNKNOWN_TIME_US : mediaPlaylist.durationUs; } - private boolean allVariantsBlacklisted() { + private boolean allEnabledVariantsBlacklisted() { for (int i = 0; i < enabledVariantBlacklistFlags.length; i++) { if (!enabledVariantBlacklistFlags[i]) { return false; @@ -731,9 +739,10 @@ public class HlsChunkSource { private HlsMediaPlaylist result; - public MediaPlaylistChunk(DataSource dataSource, DataSpec dataSpec, byte[] scratchSpace, - HlsPlaylistParser playlistParser, int variantIndex, String playlistUrl) { - super(dataSource, dataSpec, Chunk.TYPE_MANIFEST, Chunk.TRIGGER_UNSPECIFIED, null, + public MediaPlaylistChunk(DataSource dataSource, DataSpec dataSpec, Format format, + byte[] scratchSpace, HlsPlaylistParser playlistParser, int variantIndex, + String playlistUrl) { + super(dataSource, dataSpec, Chunk.TYPE_MANIFEST, Chunk.TRIGGER_UNSPECIFIED, format, Chunk.NO_PARENT_ID, scratchSpace); this.variantIndex = variantIndex; this.playlistParser = playlistParser; @@ -755,16 +764,14 @@ public class HlsChunkSource { private static final class EncryptionKeyChunk extends DataChunk { public final String iv; - public final int variantIndex; private byte[] result; - public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, byte[] scratchSpace, - String iv, int variantIndex) { - super(dataSource, dataSpec, Chunk.TYPE_DRM, Chunk.TRIGGER_UNSPECIFIED, null, + public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, Format format, + byte[] scratchSpace, String iv) { + super(dataSource, dataSpec, Chunk.TYPE_DRM, Chunk.TRIGGER_UNSPECIFIED, format, Chunk.NO_PARENT_ID, scratchSpace); this.iv = iv; - this.variantIndex = variantIndex; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index 1065cfc249..c7259c3385 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -83,8 +83,9 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { // Indexed by track (as exposed by this source). private TrackGroupArray trackGroups; private int primaryTrackGroupIndex; - private int[] primarySelectedTracks; - private boolean primarySelectedTracksChanged; + private boolean isFirstTrackSelection; + private boolean newTracksSelected; + private boolean primaryTracksDeselected; // Indexed by group. private boolean[] groupEnabledStates; private boolean[] pendingResets; @@ -146,6 +147,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { if (chunkSource.getTrackCount() == 0) { trackGroups = new TrackGroupArray(); state = STATE_SELECTING_TRACKS; + isFirstTrackSelection = true; return true; } if (!extractors.isEmpty()) { @@ -155,6 +157,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { if (extractor.isPrepared()) { buildTracks(extractor); state = STATE_SELECTING_TRACKS; + isFirstTrackSelection = true; maybeStartLoading(); // Update the load control. return true; } else if (extractors.size() > 1) { @@ -197,7 +200,9 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { public void startTrackSelection() { Assertions.checkState(state == STATE_READING); state = STATE_SELECTING_TRACKS; - primarySelectedTracksChanged = false; + isFirstTrackSelection = false; + newTracksSelected = false; + primaryTracksDeselected = false; } @Override @@ -208,9 +213,9 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { setTrackGroupEnabledState(group, true); downstreamSampleFormats[group] = null; pendingResets[group] = false; - if (group == primaryTrackGroupIndex && !Arrays.equals(tracks, primarySelectedTracks)) { - primarySelectedTracks = tracks; - primarySelectedTracksChanged = true; + newTracksSelected = true; + if (group == primaryTrackGroupIndex) { + primaryTracksDeselected |= chunkSource.selectTracks(tracks); } return new TrackStreamImpl(group); } @@ -242,24 +247,12 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { loadControl.trimAllocator(); } } - } else { + } else if (primaryTracksDeselected || (!isFirstTrackSelection && newTracksSelected)) { if (!loadControlRegistered) { loadControl.register(this, bufferSizeContribution); loadControlRegistered = true; } - // Treat enabling of a live stream as occurring at t=0 in both of the blocks below. - positionUs = chunkSource.isLive() ? 0 : positionUs; - if (primarySelectedTracksChanged) { - // If the primary tracks change then this will affect other exposed tracks that are enabled - // as well. Hence we implement the restart as a seek so that all downstream renderers - // receive a discontinuity event. - chunkSource.selectTracks(primarySelectedTracks); - seekToInternal(positionUs); - } else { - lastSeekPositionUs = positionUs; - downstreamPositionUs = positionUs; - restartFrom(positionUs); - } + seekToInternal(positionUs); } } @@ -370,7 +363,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { if (enabledTrackCount == 0) { return; } - seekToInternal(chunkSource.isLive() ? 0 : positionUs); + seekToInternal(positionUs); } @Override @@ -595,6 +588,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { * @param positionUs The position to seek to. */ private void seekToInternal(long positionUs) { + // Treat all seeks into non-seekable media as being to t=0. + positionUs = chunkSource.isLive() ? 0 : positionUs; lastSeekPositionUs = positionUs; downstreamPositionUs = positionUs; Arrays.fill(pendingResets, true);