From e6ca2df5144185159b4a0a37445cb204f353557f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 10 Sep 2015 18:21:21 +0100 Subject: [PATCH] Fix end-of-stream for live streams. Issue: #764 --- .../exoplayer/chunk/BaseMediaChunk.java | 7 ++-- .../exoplayer/chunk/ChunkOperationHolder.java | 23 ++++++++++-- .../exoplayer/chunk/ChunkSampleSource.java | 31 +++++++++------- .../android/exoplayer/chunk/ChunkSource.java | 19 ++++------ .../exoplayer/chunk/ContainerMediaChunk.java | 5 ++- .../android/exoplayer/chunk/MediaChunk.java | 12 ++----- .../chunk/SingleSampleChunkSource.java | 3 +- .../chunk/SingleSampleMediaChunk.java | 9 +++-- .../exoplayer/dash/DashChunkSource.java | 29 +++++++-------- .../android/exoplayer/hls/HlsChunkSource.java | 35 ++++++++++--------- .../exoplayer/hls/HlsSampleSource.java | 21 +++++++---- .../google/android/exoplayer/hls/TsChunk.java | 7 ++-- .../SmoothStreamingChunkSource.java | 21 ++++++----- 13 files changed, 121 insertions(+), 101 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/BaseMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/BaseMediaChunk.java index 462a9f30ba..b86a2f8fe2 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/BaseMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/BaseMediaChunk.java @@ -46,7 +46,6 @@ public abstract class BaseMediaChunk extends MediaChunk { * @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 isLastChunk True if this is the last chunk in the media. False otherwise. * @param isMediaFormatFinal True if {@link #getMediaFormat()} and {@link #getDrmInitData()} can * be called at any time to obtain the media format and drm initialization data. False if * these methods are only guaranteed to return correct data after the first sample data has @@ -54,10 +53,8 @@ public abstract class BaseMediaChunk extends MediaChunk { * @param parentId Identifier for a parent from which this chunk originates. */ public BaseMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, - long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, - boolean isMediaFormatFinal, int parentId) { - super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk, - parentId); + long startTimeUs, long endTimeUs, int chunkIndex, boolean isMediaFormatFinal, int parentId) { + super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, parentId); this.isMediaFormatFinal = isMediaFormatFinal; } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkOperationHolder.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkOperationHolder.java index c59bce9733..1e2087178b 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkOperationHolder.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkOperationHolder.java @@ -16,8 +16,13 @@ package com.google.android.exoplayer.chunk; /** - * Holds a chunk operation, which consists of a {@link Chunk} to load together with the number of - * {@link MediaChunk}s that should be retained on the queue. + * Holds a chunk operation, which consists of a either: + * */ public final class ChunkOperationHolder { @@ -31,4 +36,18 @@ public final class ChunkOperationHolder { */ public Chunk chunk; + /** + * Indicates that the end of the stream has been reached. + */ + public boolean endOfStream; + + /** + * Clears the holder. + */ + public void clear() { + queueSize = 0; + chunk = null; + endOfStream = false; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index e87f9eca3e..4808b67d86 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -325,10 +325,9 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load Chunk currentLoadable = currentLoadableHolder.chunk; chunkSource.onChunkLoadCompleted(currentLoadable); if (isMediaChunk(currentLoadable)) { - MediaChunk mediaChunk = (MediaChunk) currentLoadable; + BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable; notifyLoadCompleted(currentLoadable.bytesLoaded(), mediaChunk.type, mediaChunk.trigger, mediaChunk.format, mediaChunk.startTimeUs, mediaChunk.endTimeUs, now, loadDurationMs); - loadingFinished = ((BaseMediaChunk) currentLoadable).isLastChunk; } else { notifyLoadCompleted(currentLoadable.bytesLoaded(), currentLoadable.type, currentLoadable.trigger, currentLoadable.format, -1, -1, now, loadDurationMs); @@ -408,9 +407,7 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load || (now - lastPerformedBufferOperation > 2000))) { // Perform the evaluation. lastPerformedBufferOperation = now; - currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, - downstreamPositionUs, currentLoadableHolder); + doChunkOperation(); boolean chunksDiscarded = discardUpstreamMediaChunks(currentLoadableHolder.queueSize); // Update the next load position as appropriate. if (currentLoadableHolder.chunk == null) { @@ -447,8 +444,7 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load if (isPendingReset()) { return pendingResetPositionUs; } else { - BaseMediaChunk lastMediaChunk = mediaChunks.getLast(); - return lastMediaChunk.isLastChunk ? -1 : lastMediaChunk.endTimeUs; + return loadingFinished ? -1 : mediaChunks.getLast().endTimeUs; } } @@ -464,9 +460,7 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load Chunk backedOffChunk = currentLoadableHolder.chunk; if (!isMediaChunk(backedOffChunk)) { - currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, - downstreamPositionUs, currentLoadableHolder); + doChunkOperation(); discardUpstreamMediaChunks(currentLoadableHolder.queueSize); if (currentLoadableHolder.chunk == backedOffChunk) { // Chunk was unchanged. Resume loading. @@ -491,9 +485,7 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load // and add it back again afterwards. BaseMediaChunk removedChunk = mediaChunks.removeLast(); Assertions.checkState(backedOffChunk == removedChunk); - currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, downstreamPositionUs, - currentLoadableHolder); + doChunkOperation(); mediaChunks.add(removedChunk); if (currentLoadableHolder.chunk == backedOffChunk) { @@ -533,6 +525,19 @@ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Load loader.startLoading(currentLoadable, this); } + /** + * Sets up the {@link #currentLoadableHolder}, passes it to the chunk source to cause it to be + * updated with the next operation, and updates {@link #loadingFinished} if the end of the stream + * is reached. + */ + private void doChunkOperation() { + currentLoadableHolder.endOfStream = false; + currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); + chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, downstreamPositionUs, + currentLoadableHolder); + loadingFinished = currentLoadableHolder.endOfStream; + } + /** * Discard upstream media chunks until the queue length is equal to the length specified. * diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java index 00fcb7a628..b90fc5bf15 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java @@ -89,24 +89,19 @@ public interface ChunkSource { * Updates the provided {@link ChunkOperationHolder} to contain the next operation that should * be performed by the calling {@link ChunkSampleSource}. *

- * The next operation comprises of a possibly shortened queue length (shortened if the - * implementation wishes for the caller to discard {@link MediaChunk}s from the queue), together - * with the next {@link Chunk} to load. The next chunk may be a {@link MediaChunk} to be added to - * the queue, or another {@link Chunk} type (e.g. to load initialization data), or null if the - * source is not able to provide a chunk in its current state. - *

* This method should only be called when the source is enabled. * * @param queue A representation of the currently buffered {@link MediaChunk}s. * @param seekPositionUs If the queue is empty, this parameter must specify the seek position. If * the queue is non-empty then this parameter is ignored. * @param playbackPositionUs The current playback position. - * @param out A holder for the next operation, whose {@link ChunkOperationHolder#queueSize} is - * initially equal to the length of the queue, and whose {@link ChunkOperationHolder#chunk} is - * initially equal to null or a {@link Chunk} previously supplied by the {@link ChunkSource} - * that the caller has not yet finished loading. In the latter case the chunk can either be - * replaced or left unchanged. Note that leaving the chunk unchanged is both preferred and - * more efficient than replacing it with a new but identical chunk. + * @param out A holder for the next operation, whose {@link ChunkOperationHolder#endOfStream} is + * initially set to false, whose {@link ChunkOperationHolder#queueSize} is initially equal to + * the length of the queue, and whose {@link ChunkOperationHolder#chunk} is initially equal to + * null or a {@link Chunk} previously supplied by the {@link ChunkSource} that the caller has + * not yet finished loading. In the latter case the chunk can either be replaced or left + * unchanged. Note that leaving the chunk unchanged is both preferred and more efficient than + * replacing it with a new but identical chunk. */ void getChunkOperation(List queue, long seekPositionUs, long playbackPositionUs, ChunkOperationHolder out); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ContainerMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/ContainerMediaChunk.java index 26efddebd5..162bcba1cb 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ContainerMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ContainerMediaChunk.java @@ -53,7 +53,6 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackOu * @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 isLastChunk True if this is the last chunk in the media. False otherwise. * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor. * @param extractorWrapper A wrapped extractor to use for parsing the data. * @param mediaFormat The {@link MediaFormat} of the chunk, if known. May be null if the data is @@ -71,10 +70,10 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackOu * @param parentId Identifier for a parent from which this chunk originates. */ public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, - long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, long sampleOffsetUs, + long startTimeUs, long endTimeUs, int chunkIndex, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper, MediaFormat mediaFormat, int adaptiveMaxWidth, int adaptiveMaxHeight, DrmInitData drmInitData, boolean isMediaFormatFinal, int parentId) { - super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk, + super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isMediaFormatFinal, parentId); this.extractorWrapper = extractorWrapper; this.sampleOffsetUs = sampleOffsetUs; diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java index 865dbef85d..02167d3838 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java @@ -36,14 +36,10 @@ public abstract class MediaChunk extends Chunk { * The chunk index. */ public final int chunkIndex; - /** - * True if this is the last chunk in the media. False otherwise. - */ - public final boolean isLastChunk; public MediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, - long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) { - this(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk, + long startTimeUs, long endTimeUs, int chunkIndex) { + this(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, Chunk.NO_PARENT_ID); } @@ -55,17 +51,15 @@ public abstract class MediaChunk extends Chunk { * @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 isLastChunk True if this is the last chunk in the media. False otherwise. * @param parentId Identifier for a parent from which this chunk originates. */ public MediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, - long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, int parentId) { + long startTimeUs, long endTimeUs, int chunkIndex, int parentId) { super(dataSource, dataSpec, Chunk.TYPE_MEDIA, trigger, format, parentId); Assertions.checkNotNull(format); this.startTimeUs = startTimeUs; this.endTimeUs = endTimeUs; this.chunkIndex = chunkIndex; - this.isLastChunk = isLastChunk; } } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java index c600504350..c8662cc12c 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java @@ -84,6 +84,7 @@ public final class SingleSampleChunkSource implements ChunkSource { long playbackPositionUs, ChunkOperationHolder out) { if (!queue.isEmpty()) { // We've already provided the single sample. + out.endOfStream = true; return; } out.chunk = initChunk(); @@ -111,7 +112,7 @@ public final class SingleSampleChunkSource implements ChunkSource { private SingleSampleMediaChunk initChunk() { return new SingleSampleMediaChunk(dataSource, dataSpec, Chunk.TRIGGER_UNSPECIFIED, format, 0, - durationUs, 0, true, mediaFormat, null, Chunk.NO_PARENT_ID); + durationUs, 0, mediaFormat, null, Chunk.NO_PARENT_ID); } } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java index e0f8a57ed2..d8d35acdc7 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java @@ -43,17 +43,16 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { * @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 isLastChunk True if this is the last chunk in the media. False otherwise. * @param sampleFormat The format of the sample. * @param sampleDrmInitData The {@link DrmInitData} for the sample. Null if the sample is not drm * protected. * @param parentId Identifier for a parent from which this chunk originates. */ public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, - Format format, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, - MediaFormat sampleFormat, DrmInitData sampleDrmInitData, int parentId) { - super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk, - true, parentId); + Format format, long startTimeUs, long endTimeUs, int chunkIndex, MediaFormat sampleFormat, + DrmInitData sampleDrmInitData, int parentId) { + super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, true, + parentId); this.sampleFormat = sampleFormat; this.sampleDrmInitData = sampleDrmInitData; } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 4cb8faef31..f45edfe568 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -400,11 +400,6 @@ public class DashChunkSource implements ChunkSource, Output { } MediaChunk previous = queue.get(out.queueSize - 1); - if (previous.isLastChunk) { - // We've reached the end of the stream. - return; - } - long nextSegmentStartTimeUs = previous.endTimeUs; if (live && nextSegmentStartTimeUs < availableRangeValues[0]) { // This is before the first chunk in the current manifest. @@ -415,6 +410,17 @@ public class DashChunkSource implements ChunkSource, Output { // we'll need to wait until it's refreshed. If it's unbounded we just need to wait for a // while before attempting to load the chunk. return; + } else if (!currentManifest.dynamic) { + // The current manifest isn't dynamic, so check whether we've reached the end of the stream. + PeriodHolder lastPeriodHolder = periodHolders.valueAt(periodHolders.size() - 1); + if (previous.parentId == lastPeriodHolder.localIndex) { + RepresentationHolder representationHolder = + lastPeriodHolder.representationHolders.get(previous.format.id); + if (representationHolder.isLastSegment(previous.chunkIndex)) { + out.endOfStream = true; + return; + } + } } startingNewPeriod = false; @@ -701,13 +707,8 @@ public class DashChunkSource implements ChunkSource, Output { DataSource dataSource, MediaFormat mediaFormat, int segmentNum, int trigger) { Representation representation = representationHolder.representation; Format format = representation.format; - long startTimeUs = representationHolder.getSegmentStartTimeUs(segmentNum); long endTimeUs = representationHolder.getSegmentEndTimeUs(segmentNum); - boolean isLastSegment = !currentManifest.dynamic - && periodHolders.valueAt(periodHolders.size() - 1) == periodHolder - && representationHolder.isLastSegment(segmentNum); - RangedUri segmentUri = representationHolder.getSegmentUrl(segmentNum); DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, representation.getCacheKey()); @@ -715,15 +716,15 @@ public class DashChunkSource implements ChunkSource, Output { long sampleOffsetUs = periodHolder.startTimeUs - representation.presentationTimeOffsetUs; if (mimeTypeIsRawText(format.mimeType)) { return new SingleSampleMediaChunk(dataSource, dataSpec, Chunk.TRIGGER_INITIAL, format, - startTimeUs, endTimeUs, segmentNum, isLastSegment, + startTimeUs, endTimeUs, segmentNum, MediaFormat.createTextFormat(format.mimeType, MediaFormat.NO_VALUE, format.language), null, periodHolder.localIndex); } else { boolean isMediaFormatFinal = (mediaFormat != null); return new ContainerMediaChunk(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, - segmentNum, isLastSegment, sampleOffsetUs, representationHolder.extractorWrapper, - mediaFormat, enabledTrack.adaptiveMaxWidth, enabledTrack.adaptiveMaxHeight, - periodHolder.drmInitData, isMediaFormatFinal, periodHolder.localIndex); + segmentNum, sampleOffsetUs, representationHolder.extractorWrapper, mediaFormat, + enabledTrack.adaptiveMaxWidth, enabledTrack.adaptiveMaxHeight, periodHolder.drmInitData, + isMediaFormatFinal, periodHolder.localIndex); } } 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 c12a1044de..6d78470caa 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 @@ -19,6 +19,7 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.chunk.BaseChunkSampleSourceEventListener; import com.google.android.exoplayer.chunk.Chunk; +import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.DataChunk; import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.extractor.Extractor; @@ -237,16 +238,18 @@ public class HlsChunkSource { } /** - * Returns the next {@link Chunk} that should be loaded. + * Updates the provided {@link ChunkOperationHolder} to contain the next operation that should + * be performed by the calling {@link HlsSampleSource}. * * @param previousTsChunk The previously loaded chunk that the next chunk should follow. * @param seekPositionUs If there is no previous chunk, this parameter must specify the seek * position. If there is a previous chunk then this parameter is ignored. * @param playbackPositionUs The current playback position. - * @return The next chunk to load. + * @param out The holder to populate with the result. {@link ChunkOperationHolder#queueSize} is + * unused. */ - public Chunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs, - long playbackPositionUs) { + public void getChunkOperation(TsChunk previousTsChunk, long seekPositionUs, + long playbackPositionUs, ChunkOperationHolder out) { int nextVariantIndex; boolean switchingVariantSpliced; if (adaptiveMode == ADAPTIVE_MODE_NONE) { @@ -262,7 +265,8 @@ public class HlsChunkSource { HlsMediaPlaylist mediaPlaylist = variantPlaylists[nextVariantIndex]; if (mediaPlaylist == null) { // We don't have the media playlist for the next variant. Request it now. - return newMediaPlaylistChunk(nextVariantIndex); + out.chunk = newMediaPlaylistChunk(nextVariantIndex); + return; } selectedVariantIndex = nextVariantIndex; @@ -299,11 +303,12 @@ public class HlsChunkSource { int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; if (chunkIndex >= mediaPlaylist.segments.size()) { - if (mediaPlaylist.live && shouldRerequestMediaPlaylist(nextVariantIndex)) { - return newMediaPlaylistChunk(nextVariantIndex); - } else { - return null; + if (!mediaPlaylist.live) { + out.endOfStream = true; + } else if (shouldRerequestLiveMediaPlaylist(nextVariantIndex)) { + out.chunk = newMediaPlaylistChunk(nextVariantIndex); } + return; } HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); @@ -314,8 +319,8 @@ public class HlsChunkSource { Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); if (!keyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. - Chunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex); - return toReturn; + out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex); + return; } if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) { setEncryptionData(keyUri, segment.encryptionIV, encryptionKey); @@ -342,7 +347,6 @@ public class HlsChunkSource { startTimeUs = segment.startTimeUs; } long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND); - boolean isLastChunk = !mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1; int trigger = Chunk.TRIGGER_UNSPECIFIED; Format format = variants[selectedVariantIndex].format; @@ -358,9 +362,8 @@ public class HlsChunkSource { extractorWrapper = previousTsChunk.extractorWrapper; } - return new TsChunk(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, - chunkMediaSequence, isLastChunk, extractorWrapper, encryptionKey, - encryptionIv); + out.chunk = new TsChunk(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, + chunkMediaSequence, extractorWrapper, encryptionKey, encryptionIv); } /** @@ -492,7 +495,7 @@ public class HlsChunkSource { return lowestQualityEnabledVariantIndex; } - private boolean shouldRerequestMediaPlaylist(int nextVariantIndex) { + private boolean shouldRerequestLiveMediaPlaylist(int nextVariantIndex) { // Don't re-request media playlist more often than one-half of the target duration. HlsMediaPlaylist mediaPlaylist = variantPlaylists[nextVariantIndex]; long timeSinceLastMediaPlaylistLoadMs = 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 a3cd88fd63..c8a0be37b1 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 @@ -25,6 +25,7 @@ import com.google.android.exoplayer.SampleSource.SampleSourceReader; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.BaseChunkSampleSourceEventListener; import com.google.android.exoplayer.chunk.Chunk; +import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; @@ -58,6 +59,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, private final LinkedList extractors; private final int minLoadableRetryCount; private final int bufferSizeContribution; + private final ChunkOperationHolder chunkOperationHolder; private final int eventSourceId; private final LoadControl loadControl; @@ -114,6 +116,7 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, this.eventSourceId = eventSourceId; this.pendingResetPositionUs = NO_RESET_PENDING; extractors = new LinkedList<>(); + chunkOperationHolder = new ChunkOperationHolder(); } @Override @@ -383,7 +386,6 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, chunkSource.onChunkLoadCompleted(currentLoadable); if (isTsChunk(currentLoadable)) { Assertions.checkState(currentLoadable == currentTsLoadable); - loadingFinished = currentTsLoadable.isLastChunk; previousTsLoadable = currentTsLoadable; notifyLoadCompleted(currentLoadable.bytesLoaded(), currentTsLoadable.type, currentTsLoadable.trigger, currentTsLoadable.format, currentTsLoadable.startTimeUs, @@ -521,8 +523,16 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, return; } - Chunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable, pendingResetPositionUs, - downstreamPositionUs); + chunkSource.getChunkOperation(previousTsLoadable, pendingResetPositionUs, + downstreamPositionUs, chunkOperationHolder); + boolean endOfStream = chunkOperationHolder.endOfStream; + Chunk nextLoadable = chunkOperationHolder.chunk; + chunkOperationHolder.clear(); + + if (endOfStream) { + loadingFinished = true; + return; + } if (nextLoadable == null) { return; } @@ -557,9 +567,8 @@ public final class HlsSampleSource implements SampleSource, SampleSourceReader, if (isPendingReset()) { return pendingResetPositionUs; } else { - return currentTsLoadable != null - ? (currentTsLoadable.isLastChunk ? -1 : currentTsLoadable.endTimeUs) - : (previousTsLoadable.isLastChunk ? -1 : previousTsLoadable.endTimeUs); + return loadingFinished ? -1 + : currentTsLoadable != null ? currentTsLoadable.endTimeUs : previousTsLoadable.endTimeUs; } } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java index 56968c5fe7..520046be36 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -49,16 +49,15 @@ public final class TsChunk extends MediaChunk { * @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 isLastChunk True if this is the last chunk in the media. False otherwise. * @param extractorWrapper A wrapped extractor to parse samples from the data. * @param encryptionKey For AES encryption chunks, the encryption key. * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, - long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, - HlsExtractorWrapper extractorWrapper, byte[] encryptionKey, byte[] encryptionIv) { + long startTimeUs, long endTimeUs, int chunkIndex, HlsExtractorWrapper extractorWrapper, + byte[] encryptionKey, byte[] encryptionIv) { super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, trigger, format, - startTimeUs, endTimeUs, chunkIndex, isLastChunk); + startTimeUs, endTimeUs, chunkIndex); this.extractorWrapper = extractorWrapper; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 26895118d9..6f36d9f4a6 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -270,6 +270,8 @@ public class SmoothStreamingChunkSource implements ChunkSource, if (streamElement.chunkCount == 0) { if (currentManifest.isLive) { needManifestRefresh = true; + } else { + out.endOfStream = true; } return; } @@ -282,7 +284,7 @@ public class SmoothStreamingChunkSource implements ChunkSource, chunkIndex = streamElement.getChunkIndex(seekPositionUs); } else { MediaChunk previous = queue.get(out.queueSize - 1); - chunkIndex = previous.isLastChunk ? -1 : previous.chunkIndex + 1 - currentManifestChunkOffset; + chunkIndex = previous.chunkIndex + 1 - currentManifestChunkOffset; } if (live && chunkIndex < 0) { @@ -299,10 +301,8 @@ public class SmoothStreamingChunkSource implements ChunkSource, // but continue to return the final chunk. needManifestRefresh = true; } - } - - if (chunkIndex == -1) { - // We've reached the end of the stream. + } else if (chunkIndex >= streamElement.chunkCount) { + out.endOfStream = true; return; } @@ -317,9 +317,8 @@ public class SmoothStreamingChunkSource implements ChunkSource, Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, extractorWrappers.get(manifestTrackKey), drmInitData, dataSource, currentAbsoluteChunkIndex, - isLastChunk, chunkStartTimeUs, chunkEndTimeUs, evaluation.trigger, - mediaFormats.get(manifestTrackKey), enabledTrack.adaptiveMaxWidth, - enabledTrack.adaptiveMaxHeight); + chunkStartTimeUs, chunkEndTimeUs, evaluation.trigger, mediaFormats.get(manifestTrackKey), + enabledTrack.adaptiveMaxWidth, enabledTrack.adaptiveMaxHeight); out.chunk = mediaChunk; } @@ -474,14 +473,14 @@ public class SmoothStreamingChunkSource implements ChunkSource, private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey, ChunkExtractorWrapper extractorWrapper, DrmInitData drmInitData, DataSource dataSource, - int chunkIndex, boolean isLast, long chunkStartTimeUs, long chunkEndTimeUs, - int trigger, MediaFormat mediaFormat, int adaptiveMaxWidth, int adaptiveMaxHeight) { + int chunkIndex, long chunkStartTimeUs, long chunkEndTimeUs, int trigger, + MediaFormat mediaFormat, int adaptiveMaxWidth, int adaptiveMaxHeight) { long offset = 0; DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey); // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs. return new ContainerMediaChunk(dataSource, dataSpec, trigger, formatInfo, chunkStartTimeUs, - chunkEndTimeUs, chunkIndex, isLast, chunkStartTimeUs, extractorWrapper, mediaFormat, + chunkEndTimeUs, chunkIndex, chunkStartTimeUs, extractorWrapper, mediaFormat, adaptiveMaxWidth, adaptiveMaxHeight, drmInitData, true, Chunk.NO_PARENT_ID); }