diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java index 9e9f153cd9..de830a3807 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java @@ -88,6 +88,15 @@ public abstract class Chunk implements Loadable { this.endTimeUs = endTimeUs; } + /** + * Gets the duration of the chunk in microseconds. + * + * @return The duration of the chunk in microseconds. + */ + public final long getDurationUs() { + return endTimeUs - startTimeUs; + } + /** * Gets the number of bytes that have been loaded. * diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 2f25e8a9bc..c91d90ccb9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -59,7 +59,11 @@ public class HlsChunkSource { * The default time for which a media playlist should be blacklisted. */ public static final long DEFAULT_PLAYLIST_BLACKLIST_MS = 60000; - + /** + * Subtracted value to lookup position when switching between variants in live streams to avoid + * gaps in playback in case playlist drift apart. + */ + private static final double LIVE_VARIANT_SWITCH_SAFETY_EXTRA_SECS = 2.0; private static final String TAG = "HlsChunkSource"; private static final String AAC_FILE_EXTENSION = ".aac"; private static final String MP3_FILE_EXTENSION = ".mp3"; @@ -220,33 +224,41 @@ public class HlsChunkSource { * @param out A holder to populate. */ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, ChunkHolder out) { + int previousChunkVariantIndex = + previous != null ? getVariantIndex(previous.format) : -1; updateFormatEvaluation(previous, playbackPositionUs); - int variantIndex = 0; - for (int i = 0; i < variants.length; i++) { - if (variants[i].format == evaluation.format) { - variantIndex = i; - break; - } - } - boolean switchingVariant = previous != null - && variants[variantIndex].format != previous.format; - - HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex]; + int newVariantIndex = getVariantIndex(evaluation.format); + boolean switchingVariant = previousChunkVariantIndex != newVariantIndex; + HlsMediaPlaylist mediaPlaylist = variantPlaylists[newVariantIndex]; if (mediaPlaylist == null) { // We don't have the media playlist for the next variant. Request it now. - out.chunk = newMediaPlaylistChunk(variantIndex, evaluation.trigger, evaluation.data); + out.chunk = newMediaPlaylistChunk(newVariantIndex, evaluation.trigger, evaluation.data); return; } int chunkMediaSequence; - if (previous == null) { - chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, - true, true) + mediaPlaylist.mediaSequence; + if (live) { + if (previous == null) { + chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, + true, true) + mediaPlaylist.mediaSequence; + } else { + chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, + previousChunkVariantIndex, newVariantIndex); + if (chunkMediaSequence < mediaPlaylist.mediaSequence) { + fatalError = new BehindLiveWindowException(); + return; + } + } } else { - chunkMediaSequence = switchingVariant ? previous.chunkIndex : previous.chunkIndex + 1; - if (chunkMediaSequence < mediaPlaylist.mediaSequence) { - fatalError = new BehindLiveWindowException(); - return; + // Not live. + if (previous == null) { + chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, + true, true) + mediaPlaylist.mediaSequence; + } else if (switchingVariant) { + chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, + previous.startTimeUs, true, true) + mediaPlaylist.mediaSequence; + } else { + chunkMediaSequence = previous.getNextChunkIndex(); } } @@ -254,8 +266,8 @@ public class HlsChunkSource { if (chunkIndex >= mediaPlaylist.segments.size()) { if (!mediaPlaylist.live) { out.endOfStream = true; - } else if (shouldRerequestLiveMediaPlaylist(variantIndex)) { - out.chunk = newMediaPlaylistChunk(variantIndex, evaluation.trigger, evaluation.data); + } else if (shouldRerequestLiveMediaPlaylist(newVariantIndex)) { + out.chunk = newMediaPlaylistChunk(newVariantIndex, evaluation.trigger, evaluation.data); } return; } @@ -268,7 +280,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, variantIndex, + out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, newVariantIndex, evaluation.trigger, evaluation.data); return; } @@ -289,15 +301,15 @@ public class HlsChunkSource { if (previous == null) { startTimeUs = 0; } else if (switchingVariant) { - startTimeUs = previous.startTimeUs; + startTimeUs = previous.getAdjustedStartTimeUs(); } else { - startTimeUs = previous.endTimeUs; + startTimeUs = previous.getAdjustedEndTimeUs(); } } else /* Not live */ { startTimeUs = segment.startTimeUs; } long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND); - Format format = variants[variantIndex].format; + Format format = variants[newVariantIndex].format; // Configure the extractor that will read the chunk. Extractor extractor; @@ -332,7 +344,7 @@ public class HlsChunkSource { return; } int workaroundFlags = 0; - String codecs = variants[variantIndex].codecs; + String codecs = variants[newVariantIndex].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 @@ -356,6 +368,48 @@ public class HlsChunkSource { extractorNeedsInit, switchingVariant, encryptionKey, encryptionIv); } + /** + * Returns the media sequence number of a chunk in a new variant for a live stream variant switch. + * + * @param previousChunkIndex The index of the last chunk in the old variant. + * @param oldVariantIndex The index of the old variant. + * @param newVariantIndex The index of the new variant. + * @return Media sequence number of the chunk to switch to in a live stream in the variant that + * corresponds to the given {@code newVariantIndex}. + */ + private int getLiveNextChunkSequenceNumber(int previousChunkIndex, int oldVariantIndex, + int newVariantIndex) { + if (oldVariantIndex == newVariantIndex) { + return previousChunkIndex + 1; + } + HlsMediaPlaylist oldMediaPlaylist = variantPlaylists[oldVariantIndex]; + HlsMediaPlaylist newMediaPlaylist = variantPlaylists[newVariantIndex]; + double offsetToLiveInstantSecs = 0; + for (int i = previousChunkIndex - oldMediaPlaylist.mediaSequence; + i < oldMediaPlaylist.segments.size(); i++) { + offsetToLiveInstantSecs += oldMediaPlaylist.segments.get(i).durationSecs; + } + long currentTimeMs = SystemClock.elapsedRealtime(); + offsetToLiveInstantSecs += + (double) (currentTimeMs - variantLastPlaylistLoadTimesMs[oldVariantIndex]) / 1000; + offsetToLiveInstantSecs += LIVE_VARIANT_SWITCH_SAFETY_EXTRA_SECS; + offsetToLiveInstantSecs -= + (double) (currentTimeMs - variantLastPlaylistLoadTimesMs[newVariantIndex]) / 1000; + if (offsetToLiveInstantSecs < 0) { + // The instant we are looking for is not contained in the playlist, we need it to be + // refreshed. + return newMediaPlaylist.mediaSequence + newMediaPlaylist.segments.size() + 1; + } + for (int i = newMediaPlaylist.segments.size() - 1; i >= 0; i--) { + offsetToLiveInstantSecs -= newMediaPlaylist.segments.get(i).durationSecs; + if (offsetToLiveInstantSecs < 0) { + return newMediaPlaylist.mediaSequence + i; + } + } + // We have fallen behind the live window. + return newMediaPlaylist.mediaSequence - 1; + } + /** * Invoked when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this * source. @@ -484,7 +538,7 @@ public class HlsChunkSource { if (previous != null) { // Use start time of the previous chunk rather than its end time because switching format // will require downloading overlapping segments. - bufferedDurationUs = Math.max(0, previous.startTimeUs - playbackPositionUs); + bufferedDurationUs = Math.max(0, previous.getAdjustedStartTimeUs() - playbackPositionUs); } else { bufferedDurationUs = 0; } @@ -580,6 +634,16 @@ public class HlsChunkSource { throw new IllegalStateException("Invalid format: " + format); } + private int getVariantIndex(Format format) { + for (int i = 0; i < variants.length; i++) { + if (variants[i].format == format) { + return i; + } + } + // Should never happen. + throw new IllegalStateException("Invalid format: " + format); + } + // Private classes. private static final class MediaPlaylistChunk extends DataChunk { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 094104d76a..e80c8e47d1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -54,6 +54,8 @@ import java.util.concurrent.atomic.AtomicInteger; private final boolean shouldSpliceIn; private int bytesLoaded; + private HlsSampleStreamWrapper extractorOutput; + private long adjustedEndTimeUs; private volatile boolean loadCanceled; private volatile boolean loadCompleted; @@ -65,7 +67,7 @@ import java.util.concurrent.atomic.AtomicInteger; * @param formatEvaluatorData See {@link #formatEvaluatorData}. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. - * @param chunkIndex The index of the chunk. + * @param chunkIndex The media sequence number of the chunk. * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk. * @param extractor The extractor to decode samples from the data. * @param extractorNeedsInit Whether the extractor needs initializing with the target @@ -87,6 +89,7 @@ import java.util.concurrent.atomic.AtomicInteger; this.extractorNeedsInit = extractorNeedsInit; this.shouldSpliceIn = shouldSpliceIn; // Note: this.dataSource and dataSource may be different. + adjustedEndTimeUs = startTimeUs; this.isEncrypted = this.dataSource instanceof Aes128DataSource; uid = UID_SOURCE.getAndIncrement(); } @@ -98,12 +101,31 @@ import java.util.concurrent.atomic.AtomicInteger; * @param output The output that will receive the loaded samples. */ public void init(HlsSampleStreamWrapper output) { + extractorOutput = output; output.init(uid, shouldSpliceIn); if (extractorNeedsInit) { extractor.init(output); } } + /** + * Gets the start time in microseconds by subtracting the duration from the adjusted end time. + * + * @return The start time in microseconds. + */ + public long getAdjustedStartTimeUs() { + return adjustedEndTimeUs - getDurationUs(); + } + + /** + * Gets the presentation time in microseconds of the last sample contained in the chunk + * + * @return The presentation time in microseconds of the last sample contained in the chunk. + */ + public long getAdjustedEndTimeUs() { + return adjustedEndTimeUs; + } + @Override public boolean isLoadCompleted() { return loadCompleted; @@ -152,6 +174,10 @@ import java.util.concurrent.atomic.AtomicInteger; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { result = extractor.read(input, null); } + long adjustedEndTimeUs = extractorOutput.getLargestQueuedTimestampUs(); + if (adjustedEndTimeUs != Long.MIN_VALUE) { + this.adjustedEndTimeUs = adjustedEndTimeUs; + } } finally { bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 6fe27c6fbc..871e12a2fc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -239,6 +239,15 @@ import java.util.List; loader.release(); } + public long getLargestQueuedTimestampUs() { + long largestQueuedTimestampUs = Long.MIN_VALUE; + for (int i = 0; i < sampleQueues.size(); i++) { + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, + sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); + } + return largestQueuedTimestampUs; + } + // SampleStream implementation. /* package */ boolean isReady(int group) {