diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index 3bfb8356a5..546d56bccf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -480,8 +480,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } maybeNotifyDownstreamFormat(sampleQueueIndex); int result = - sampleQueues[sampleQueueIndex].read( - formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + sampleQueues[sampleQueueIndex].read(formatHolder, buffer, formatRequired, loadingFinished); if (result == C.RESULT_NOTHING_READ) { maybeStartDeferredRetry(sampleQueueIndex); } @@ -815,6 +814,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; loadable.setLoadPosition( Assertions.checkNotNull(seekMap).getSeekPoints(pendingResetPositionUs).first.position, pendingResetPositionUs); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.setStartTimeUs(pendingResetPositionUs); + } pendingResetPositionUs = C.TIME_UNSET; } extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index f2c9a11246..b65d86279a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source; import android.os.Looper; +import android.util.Log; import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -52,6 +53,7 @@ public class SampleQueue implements TrackOutput { } @VisibleForTesting /* package */ static final int SAMPLE_CAPACITY_INCREMENT = 1000; + private static final String TAG = "SampleQueue"; private final SampleDataQueue sampleDataQueue; private final SampleExtrasHolder extrasHolder; @@ -77,6 +79,7 @@ public class SampleQueue implements TrackOutput { private int relativeFirstIndex; private int readPosition; + private long startTimeUs; private long largestDiscardedTimestampUs; private long largestQueuedTimestampUs; private boolean isLastSampleQueued; @@ -87,6 +90,8 @@ public class SampleQueue implements TrackOutput { @Nullable private Format upstreamFormat; @Nullable private Format upstreamCommittedFormat; private int upstreamSourceId; + private boolean upstreamAllSamplesAreSyncSamples; + private boolean loggedUnexpectedNonSyncSample; private long sampleOffsetUs; private boolean pendingSplice; @@ -119,6 +124,7 @@ public class SampleQueue implements TrackOutput { sizes = new int[capacity]; cryptoDatas = new CryptoData[capacity]; formats = new Format[capacity]; + startTimeUs = Long.MIN_VALUE; largestDiscardedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; upstreamFormatRequired = true; @@ -155,6 +161,7 @@ public class SampleQueue implements TrackOutput { relativeFirstIndex = 0; readPosition = 0; upstreamKeyframeRequired = true; + startTimeUs = Long.MIN_VALUE; largestDiscardedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; isLastSampleQueued = false; @@ -166,6 +173,16 @@ public class SampleQueue implements TrackOutput { } } + /** + * Sets the start time for the queue. Samples with earlier timestamps will be discarded or have + * the {@link C#BUFFER_FLAG_DECODE_ONLY} flag set when read. + * + * @param startTimeUs The start time, in microseconds. + */ + public final void setStartTimeUs(long startTimeUs) { + this.startTimeUs = startTimeUs; + } + /** * Sets a source identifier for subsequent samples. * @@ -325,8 +342,6 @@ public class SampleQueue implements TrackOutput { * it's not changing. A sample will never be read if set to true, however it is still possible * for the end of stream or nothing to be read. * @param loadingFinished True if an empty queue should be considered the end of the stream. - * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will - * be set if the buffer's timestamp is less than this value. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ @@ -335,11 +350,9 @@ public class SampleQueue implements TrackOutput { FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, - boolean loadingFinished, - long decodeOnlyUntilUs) { + boolean loadingFinished) { int result = - readSampleMetadata( - formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilUs, extrasHolder); + readSampleMetadata(formatHolder, buffer, formatRequired, loadingFinished, extrasHolder); if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream() && !buffer.isFlagsOnly()) { sampleDataQueue.readToBuffer(buffer, extrasHolder); } @@ -357,6 +370,7 @@ public class SampleQueue implements TrackOutput { if (sampleIndex < absoluteFirstIndex || sampleIndex > absoluteFirstIndex + length) { return false; } + startTimeUs = Long.MIN_VALUE; readPosition = sampleIndex - absoluteFirstIndex; return true; } @@ -382,6 +396,7 @@ public class SampleQueue implements TrackOutput { if (offset == -1) { return false; } + startTimeUs = timeUs; readPosition += offset; return true; } @@ -513,6 +528,22 @@ public class SampleQueue implements TrackOutput { } timeUs += sampleOffsetUs; + if (upstreamAllSamplesAreSyncSamples) { + if (timeUs < startTimeUs) { + // If we know that all samples are sync samples, we can discard those that come before the + // start time on the write side of the queue. + return; + } + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) { + // The flag should always be set unless the source content has incorrect sample metadata. + // Log a warning (once per format change, to avoid log spam) and override the flag. + if (!loggedUnexpectedNonSyncSample) { + Log.w(TAG, "Overriding unexpected non-sync sample for format: " + upstreamFormat); + loggedUnexpectedNonSyncSample = true; + } + flags |= C.BUFFER_FLAG_KEY_FRAME; + } + } if (pendingSplice) { if (!isKeyframe || !attemptSplice(timeUs)) { return; @@ -568,25 +599,9 @@ public class SampleQueue implements TrackOutput { DecoderInputBuffer buffer, boolean formatRequired, boolean loadingFinished, - long decodeOnlyUntilUs, SampleExtrasHolder extrasHolder) { buffer.waitingForKeys = false; - // This is a temporary fix for https://github.com/google/ExoPlayer/issues/6155. - // TODO: Remove it and replace it with a fix that discards samples when writing to the queue. - boolean hasNextSample; - int relativeReadIndex = C.INDEX_UNSET; - while ((hasNextSample = hasNextSample())) { - relativeReadIndex = getRelativeIndex(readPosition); - long timeUs = timesUs[relativeReadIndex]; - if (timeUs < decodeOnlyUntilUs - && MimeTypes.allSamplesAreSyncSamples(formats[relativeReadIndex].sampleMimeType)) { - readPosition++; - } else { - break; - } - } - - if (!hasNextSample) { + if (!hasNextSample()) { if (loadingFinished || isLastSampleQueued) { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; @@ -598,6 +613,7 @@ public class SampleQueue implements TrackOutput { } } + int relativeReadIndex = getRelativeIndex(readPosition); if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { onFormatResult(formats[relativeReadIndex], formatHolder); return C.RESULT_FORMAT_READ; @@ -610,7 +626,7 @@ public class SampleQueue implements TrackOutput { buffer.setFlags(flags[relativeReadIndex]); buffer.timeUs = timesUs[relativeReadIndex]; - if (buffer.timeUs < decodeOnlyUntilUs) { + if (buffer.timeUs < startTimeUs) { buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); } if (buffer.isFlagsOnly()) { @@ -631,16 +647,19 @@ public class SampleQueue implements TrackOutput { // current upstreamFormat so we can detect format changes on the read side using cheap // referential quality. return false; - } else if (Util.areEqual(format, upstreamCommittedFormat)) { + } + if (Util.areEqual(format, upstreamCommittedFormat)) { // The format has changed back to the format of the last committed sample. If they are // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat // so we can detect format changes on the read side using cheap referential equality. upstreamFormat = upstreamCommittedFormat; - return true; } else { upstreamFormat = format; - return true; } + upstreamAllSamplesAreSyncSamples = + MimeTypes.allSamplesAreSyncSamples(upstreamFormat.sampleMimeType); + loggedUnexpectedNonSyncSample = false; + return true; } private synchronized long discardSampleMetadataTo( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 6efe25420c..0e193a1a2a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -87,7 +87,6 @@ public class ChunkSampleStream implements SampleStream, S private long lastSeekPositionUs; private int nextNotifyPrimaryFormatMediaChunkIndex; - /* package */ long decodeOnlyUntilPositionUs; /* package */ boolean loadingFinished; /** @@ -282,14 +281,12 @@ public class ChunkSampleStream implements SampleStream, S if (seekToMediaChunk != null) { // When seeking to the start of a chunk we use the index of the first sample in the chunk // rather than the seek position. This ensures we seek to the keyframe at the start of the - // chunk even if the sample timestamps are slightly offset from the chunk start times. + // chunk even if its timestamp is slightly earlier than the advertised chunk start time. seekInsideBuffer = primarySampleQueue.seekTo(seekToMediaChunk.getFirstSampleIndex(0)); - decodeOnlyUntilPositionUs = 0; } else { seekInsideBuffer = primarySampleQueue.seekTo( positionUs, /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs()); - decodeOnlyUntilPositionUs = lastSeekPositionUs; } if (seekInsideBuffer) { @@ -383,8 +380,7 @@ public class ChunkSampleStream implements SampleStream, S } maybeNotifyPrimaryTrackFormatChanged(); - return primarySampleQueue.read( - formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); + return primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished); } @Override @@ -577,9 +573,16 @@ public class ChunkSampleStream implements SampleStream, S if (isMediaChunk(loadable)) { BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable; if (pendingReset) { - boolean resetToMediaChunk = mediaChunk.startTimeUs == pendingResetPositionUs; - // Only enable setting of the decode only flag if we're not resetting to a chunk boundary. - decodeOnlyUntilPositionUs = resetToMediaChunk ? 0 : pendingResetPositionUs; + // Only set the queue start times if we're not seeking to a chunk boundary. If we are + // seeking to a chunk boundary then we want the queue to pass through all of the samples in + // the chunk. Doing this ensures we'll always output the keyframe at the start of the chunk, + // even if its timestamp is slightly earlier than the advertised chunk start time. + if (mediaChunk.startTimeUs != pendingResetPositionUs) { + primarySampleQueue.setStartTimeUs(pendingResetPositionUs); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.setStartTimeUs(pendingResetPositionUs); + } + } pendingResetPositionUs = C.TIME_UNSET; } mediaChunk.init(chunkOutput); @@ -799,12 +802,7 @@ public class ChunkSampleStream implements SampleStream, S return C.RESULT_NOTHING_READ; } maybeNotifyDownstreamFormat(); - return sampleQueue.read( - formatHolder, - buffer, - formatRequired, - loadingFinished, - decodeOnlyUntilPositionUs); + return sampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished); } public void release() { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 480735a689..bc8ed07167 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.common.primitives.Bytes; import java.io.IOException; @@ -180,6 +181,7 @@ public final class SampleQueueTest { assertReadSample( /* timeUs= */ i * 1000, /* isKeyFrame= */ true, + /* isDecodeOnly= */ false, /* isEncrypted= */ false, /* sampleData= */ new byte[1], /* offset= */ 0, @@ -226,9 +228,23 @@ public final class SampleQueueTest { sampleQueue.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); assertReadFormat(false, FORMAT_1); - assertReadSample(0, true, /* isEncrypted= */ false, DATA, 0, ALLOCATION_SIZE); + assertReadSample( + 0, + /* isKeyFrame= */ true, + /* isDecodeOnly= */ false, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + ALLOCATION_SIZE); // Assert the second sample is read without a format change. - assertReadSample(1000, true, /* isEncrypted= */ false, DATA, 0, ALLOCATION_SIZE); + assertReadSample( + 1000, + /* isKeyFrame= */ true, + /* isDecodeOnly= */ false, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + ALLOCATION_SIZE); // The same applies if the queue is empty when the formats are written. sampleQueue.format(FORMAT_2); @@ -237,7 +253,14 @@ public final class SampleQueueTest { sampleQueue.sampleMetadata(2000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); // Assert the third sample is read without a format change. - assertReadSample(2000, true, /* isEncrypted= */ false, DATA, 0, ALLOCATION_SIZE); + assertReadSample( + 2000, + /* isKeyFrame= */ true, + /* isDecodeOnly= */ false, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + ALLOCATION_SIZE); } @Test @@ -260,7 +283,14 @@ public final class SampleQueueTest { // If formatRequired, should read the format rather than the sample. assertReadFormat(true, FORMAT_1); // Otherwise should read the sample. - assertReadSample(1000, true, /* isEncrypted= */ false, DATA, 0, ALLOCATION_SIZE); + assertReadSample( + 1000, + /* isKeyFrame= */ true, + /* isDecodeOnly= */ false, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + ALLOCATION_SIZE); // Allocation should still be held. assertAllocationCount(1); sampleQueue.discardToRead(); @@ -277,7 +307,14 @@ public final class SampleQueueTest { // If formatRequired, should read the format rather than the sample. assertReadFormat(true, FORMAT_1); // Read the sample. - assertReadSample(2000, false, /* isEncrypted= */ false, DATA, 0, ALLOCATION_SIZE - 1); + assertReadSample( + 2000, + /* isKeyFrame= */ false, + /* isDecodeOnly= */ false, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + ALLOCATION_SIZE - 1); // Allocation should still be held. assertAllocationCount(1); sampleQueue.discardToRead(); @@ -291,7 +328,14 @@ public final class SampleQueueTest { // If formatRequired, should read the format rather than the sample. assertReadFormat(true, FORMAT_1); // Read the sample. - assertReadSample(3000, false, /* isEncrypted= */ false, DATA, ALLOCATION_SIZE - 1, 1); + assertReadSample( + 3000, + /* isKeyFrame= */ false, + /* isDecodeOnly= */ false, + /* isEncrypted= */ false, + DATA, + ALLOCATION_SIZE - 1, + 1); // Allocation should still be held. assertAllocationCount(1); sampleQueue.discardToRead(); @@ -394,11 +438,7 @@ public final class SampleQueueTest { int result = sampleQueue.read( - formatHolder, - inputBuffer, - /* formatRequired= */ false, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); + formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false); assertThat(result).isEqualTo(RESULT_FORMAT_READ); assertThat(formatHolder.drmSession).isSameInstanceAs(mockDrmSession); assertReadEncryptedSample(/* sampleIndex= */ 0); @@ -407,21 +447,13 @@ public final class SampleQueueTest { assertThat(formatHolder.drmSession).isNull(); result = sampleQueue.read( - formatHolder, - inputBuffer, - /* formatRequired= */ false, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); + formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false); assertThat(result).isEqualTo(RESULT_FORMAT_READ); assertThat(formatHolder.drmSession).isNull(); assertReadEncryptedSample(/* sampleIndex= */ 2); result = sampleQueue.read( - formatHolder, - inputBuffer, - /* formatRequired= */ false, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); + formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false); assertThat(result).isEqualTo(RESULT_FORMAT_READ); assertThat(formatHolder.drmSession).isSameInstanceAs(mockDrmSession); } @@ -438,11 +470,7 @@ public final class SampleQueueTest { int result = sampleQueue.read( - formatHolder, - inputBuffer, - /* formatRequired= */ false, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); + formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false); assertThat(result).isEqualTo(RESULT_FORMAT_READ); assertThat(formatHolder.drmSession).isSameInstanceAs(mockDrmSession); assertReadEncryptedSample(/* sampleIndex= */ 0); @@ -451,21 +479,13 @@ public final class SampleQueueTest { assertThat(formatHolder.drmSession).isNull(); result = sampleQueue.read( - formatHolder, - inputBuffer, - /* formatRequired= */ false, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); + formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false); assertThat(result).isEqualTo(RESULT_FORMAT_READ); assertThat(formatHolder.drmSession).isSameInstanceAs(mockPlaceholderDrmSession); assertReadEncryptedSample(/* sampleIndex= */ 2); result = sampleQueue.read( - formatHolder, - inputBuffer, - /* formatRequired= */ false, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); + formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false); assertThat(result).isEqualTo(RESULT_FORMAT_READ); assertThat(formatHolder.drmSession).isSameInstanceAs(mockDrmSession); assertReadEncryptedSample(/* sampleIndex= */ 3); @@ -495,11 +515,7 @@ public final class SampleQueueTest { int result = sampleQueue.read( - formatHolder, - inputBuffer, - /* formatRequired= */ false, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); + formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false); assertThat(result).isEqualTo(RESULT_FORMAT_READ); // Fill cryptoInfo.iv with non-zero data. When the 8 byte initialization vector is written into @@ -509,11 +525,7 @@ public final class SampleQueueTest { result = sampleQueue.read( - formatHolder, - inputBuffer, - /* formatRequired= */ false, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); + formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false); assertThat(result).isEqualTo(RESULT_BUFFER_READ); // Assert cryptoInfo.iv contains the 8-byte initialization vector and that the trailing 8 bytes @@ -608,7 +620,14 @@ public final class SampleQueueTest { sampleQueue.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); // Once the metadata has been written, check the sample can be read as expected. - assertReadSample(0, true, /* isEncrypted= */ false, DATA, 0, ALLOCATION_SIZE); + assertReadSample( + /* timeUs= */ 0, + /* isKeyFrame= */ true, + /* isDecodeOnly= */ false, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + ALLOCATION_SIZE); assertNoSamplesToRead(FORMAT_1); assertAllocationCount(1); sampleQueue.discardToRead(); @@ -641,7 +660,7 @@ public final class SampleQueueTest { int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP); // Should advance to 2nd keyframe (the 4th frame). assertThat(skipCount).isEqualTo(4); - assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX); + assertReadTestData(/* startFormat= */ null, DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(FORMAT_2); } @@ -651,7 +670,7 @@ public final class SampleQueueTest { int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1); // Should advance to 2nd keyframe (the 4th frame). assertThat(skipCount).isEqualTo(4); - assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX); + assertReadTestData(/* startFormat= */ null, DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(FORMAT_2); } @@ -681,7 +700,12 @@ public final class SampleQueueTest { boolean success = sampleQueue.seekTo(LAST_SAMPLE_TIMESTAMP, false); assertThat(success).isTrue(); assertThat(sampleQueue.getReadIndex()).isEqualTo(4); - assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX); + assertReadTestData( + /* startFormat= */ null, + DATA_SECOND_KEYFRAME_INDEX, + /* sampleCount= */ SAMPLE_TIMESTAMPS.length - DATA_SECOND_KEYFRAME_INDEX, + /* sampleOffsetUs= */ 0, + /* decodeOnlyUntilUs= */ LAST_SAMPLE_TIMESTAMP); assertNoSamplesToRead(FORMAT_2); } @@ -701,7 +725,12 @@ public final class SampleQueueTest { boolean success = sampleQueue.seekTo(LAST_SAMPLE_TIMESTAMP + 1, true); assertThat(success).isTrue(); assertThat(sampleQueue.getReadIndex()).isEqualTo(4); - assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX); + assertReadTestData( + /* startFormat= */ null, + DATA_SECOND_KEYFRAME_INDEX, + /* sampleCount= */ SAMPLE_TIMESTAMPS.length - DATA_SECOND_KEYFRAME_INDEX, + /* sampleOffsetUs= */ 0, + /* decodeOnlyUntilUs= */ LAST_SAMPLE_TIMESTAMP + 1); assertNoSamplesToRead(FORMAT_2); } @@ -711,7 +740,13 @@ public final class SampleQueueTest { boolean success = sampleQueue.seekTo(LAST_SAMPLE_TIMESTAMP, false); assertThat(success).isTrue(); assertThat(sampleQueue.getReadIndex()).isEqualTo(4); - assertReadTestData(null, DATA_SECOND_KEYFRAME_INDEX); + assertReadTestData( + /* startFormat= */ null, + DATA_SECOND_KEYFRAME_INDEX, + /* sampleCount= */ SAMPLE_TIMESTAMPS.length - DATA_SECOND_KEYFRAME_INDEX, + /* sampleOffsetUs= */ 0, + /* decodeOnlyUntilUs= */ LAST_SAMPLE_TIMESTAMP); + assertNoSamplesToRead(FORMAT_2); // Seek back to the start. success = sampleQueue.seekTo(SAMPLE_TIMESTAMPS[0], false); @@ -721,6 +756,51 @@ public final class SampleQueueTest { assertNoSamplesToRead(FORMAT_2); } + @Test + public void setStartTimeUs_allSamplesAreSyncSamples_discardsOnWriteSide() { + // The format uses a MIME type for which MimeTypes.allSamplesAreSyncSamples() is true. + Format format = new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_RAW).build(); + Format[] sampleFormats = new Format[SAMPLE_SIZES.length]; + Arrays.fill(sampleFormats, format); + int[] sampleFlags = new int[SAMPLE_SIZES.length]; + Arrays.fill(sampleFlags, BUFFER_FLAG_KEY_FRAME); + + sampleQueue.setStartTimeUs(LAST_SAMPLE_TIMESTAMP); + writeTestData( + DATA, SAMPLE_SIZES, SAMPLE_OFFSETS, SAMPLE_TIMESTAMPS, sampleFormats, sampleFlags); + + assertThat(sampleQueue.getReadIndex()).isEqualTo(0); + + assertReadFormat(/* formatRequired= */ false, format); + assertReadSample( + SAMPLE_TIMESTAMPS[7], + /* isKeyFrame= */ true, + /* isDecodeOnly= */ false, + /* isEncrypted= */ false, + DATA, + DATA.length - SAMPLE_OFFSETS[7] - SAMPLE_SIZES[7], + SAMPLE_SIZES[7]); + } + + @Test + public void setStartTimeUs_notAllSamplesAreSyncSamples_discardsOnReadSide() { + // The format uses a MIME type for which MimeTypes.allSamplesAreSyncSamples() is false. + Format format = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build(); + Format[] sampleFormats = new Format[SAMPLE_SIZES.length]; + Arrays.fill(sampleFormats, format); + + sampleQueue.setStartTimeUs(LAST_SAMPLE_TIMESTAMP); + writeTestData(); + + assertThat(sampleQueue.getReadIndex()).isEqualTo(0); + assertReadTestData( + /* startFormat= */ null, + /* firstSampleIndex= */ 0, + /* sampleCount= */ SAMPLE_TIMESTAMPS.length, + /* sampleOffsetUs= */ 0, + /* decodeOnlyUntilUs= */ LAST_SAMPLE_TIMESTAMP); + } + @Test public void discardToEnd() { writeTestData(); @@ -745,7 +825,7 @@ public final class SampleQueueTest { assertThat(sampleQueue.getReadIndex()).isEqualTo(0); assertAllocationCount(10); // Read the first sample. - assertReadTestData(null, 0, 1); + assertReadTestData(/* startFormat= */ null, 0, 1); // Shouldn't discard anything. sampleQueue.discardTo(SAMPLE_TIMESTAMPS[1] - 1, false, true); assertThat(sampleQueue.getFirstIndex()).isEqualTo(0); @@ -835,7 +915,7 @@ public final class SampleQueueTest { writeTestData(); sampleQueue.discardUpstreamSamples(4); assertAllocationCount(4); - assertReadTestData(null, 0, 4); + assertReadTestData(/* startFormat= */ null, 0, 4); assertReadFormat(false, FORMAT_2); assertNoSamplesToRead(FORMAT_2); } @@ -843,7 +923,7 @@ public final class SampleQueueTest { @Test public void discardUpstreamAfterRead() { writeTestData(); - assertReadTestData(null, 0, 3); + assertReadTestData(/* startFormat= */ null, 0, 3); sampleQueue.discardUpstreamSamples(8); assertAllocationCount(10); sampleQueue.discardToRead(); @@ -908,7 +988,11 @@ public final class SampleQueueTest { sampleQueue.setSampleOffsetUs(sampleOffsetUs); writeTestData(); assertReadTestData( - /* startFormat= */ null, /* firstSampleIndex= */ 0, /* sampleCount= */ 8, sampleOffsetUs); + /* startFormat= */ null, + /* firstSampleIndex= */ 0, + /* sampleCount= */ 8, + sampleOffsetUs, + /* decodeOnlyUntilUs= */ 0); assertReadEndOfStream(/* formatRequired= */ false); } @@ -931,6 +1015,7 @@ public final class SampleQueueTest { assertReadSample( unadjustedTimestampUs + sampleOffsetUs, /* isKeyFrame= */ false, + /* isDecodeOnly= */ false, /* isEncrypted= */ false, DATA, /* offset= */ 0, @@ -986,6 +1071,7 @@ public final class SampleQueueTest { assertReadSample( /* timeUs= */ 0, /* isKeyFrame= */ true, + /* isDecodeOnly= */ false, /* isEncrypted= */ false, DATA, /* offset= */ 0, @@ -994,6 +1080,7 @@ public final class SampleQueueTest { assertReadSample( /* timeUs= */ 1, /* isKeyFrame= */ false, + /* isDecodeOnly= */ false, /* isEncrypted= */ false, DATA, /* offset= */ 0, @@ -1009,16 +1096,23 @@ public final class SampleQueueTest { long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[4]; writeFormat(FORMAT_SPLICED); writeSample(DATA, spliceSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME); - assertReadTestData(null, 0, 4); + assertReadTestData(/* startFormat= */ null, 0, 4); assertReadFormat(false, FORMAT_SPLICED); - assertReadSample(spliceSampleTimeUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length); + assertReadSample( + spliceSampleTimeUs, + /* isKeyFrame= */ true, + /* isDecodeOnly= */ false, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + DATA.length); assertReadEndOfStream(false); } @Test public void spliceAfterRead() { writeTestData(); - assertReadTestData(null, 0, 4); + assertReadTestData(/* startFormat= */ null, 0, 4); sampleQueue.splice(); // Splice should fail, leaving the last 4 samples unchanged. long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3]; @@ -1028,14 +1122,21 @@ public final class SampleQueueTest { assertReadEndOfStream(false); sampleQueue.seekTo(0); - assertReadTestData(null, 0, 4); + assertReadTestData(/* startFormat= */ null, 0, 4); sampleQueue.splice(); // Splice should succeed, replacing the last 4 samples with the sample being written spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3] + 1; writeFormat(FORMAT_SPLICED); writeSample(DATA, spliceSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME); assertReadFormat(false, FORMAT_SPLICED); - assertReadSample(spliceSampleTimeUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length); + assertReadSample( + spliceSampleTimeUs, + /* isKeyFrame= */ true, + /* isDecodeOnly= */ false, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + DATA.length); assertReadEndOfStream(false); } @@ -1049,14 +1150,23 @@ public final class SampleQueueTest { long spliceSampleTimeUs = SAMPLE_TIMESTAMPS[4]; writeFormat(FORMAT_SPLICED); writeSample(DATA, spliceSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME); - assertReadTestData(null, 0, 4, sampleOffsetUs); + assertReadTestData(/* startFormat= */ null, 0, 4, sampleOffsetUs, /* decodeOnlyUntilUs= */ 0); assertReadFormat( false, FORMAT_SPLICED.buildUpon().setSubsampleOffsetUs(sampleOffsetUs).build()); assertReadSample( - spliceSampleTimeUs + sampleOffsetUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length); + spliceSampleTimeUs + sampleOffsetUs, + /* isKeyFrame= */ true, + /* isDecodeOnly= */ false, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + DATA.length); assertReadEndOfStream(false); } + @Test + public void setStartTime() {} + // Internal methods. /** @@ -1119,7 +1229,7 @@ public final class SampleQueueTest { * Asserts correct reading of standard test data from {@code sampleQueue}. */ private void assertReadTestData() { - assertReadTestData(null, 0); + assertReadTestData(/* startFormat= */ null, 0); } /** @@ -1149,7 +1259,12 @@ public final class SampleQueueTest { * @param sampleCount The number of samples to read. */ private void assertReadTestData(Format startFormat, int firstSampleIndex, int sampleCount) { - assertReadTestData(startFormat, firstSampleIndex, sampleCount, 0); + assertReadTestData( + startFormat, + firstSampleIndex, + sampleCount, + /* sampleOffsetUs= */ 0, + /* decodeOnlyUntilUs= */ 0); } /** @@ -1161,7 +1276,11 @@ public final class SampleQueueTest { * @param sampleOffsetUs The expected sample offset. */ private void assertReadTestData( - Format startFormat, int firstSampleIndex, int sampleCount, long sampleOffsetUs) { + Format startFormat, + int firstSampleIndex, + int sampleCount, + long sampleOffsetUs, + long decodeOnlyUntilUs) { Format format = adjustFormat(startFormat, sampleOffsetUs); for (int i = firstSampleIndex; i < firstSampleIndex + sampleCount; i++) { // Use equals() on the read side despite using referential equality on the write side, since @@ -1175,9 +1294,11 @@ public final class SampleQueueTest { // If we require the format, we should always read it. assertReadFormat(true, testSampleFormat); // Assert the sample is as expected. + long expectedTimeUs = SAMPLE_TIMESTAMPS[i] + sampleOffsetUs; assertReadSample( - SAMPLE_TIMESTAMPS[i] + sampleOffsetUs, + expectedTimeUs, (SAMPLE_FLAGS[i] & C.BUFFER_FLAG_KEY_FRAME) != 0, + /* isDecodeOnly= */ expectedTimeUs < decodeOnlyUntilUs, /* isEncrypted= */ false, DATA, DATA.length - SAMPLE_OFFSETS[i] - SAMPLE_SIZES[i], @@ -1221,12 +1342,7 @@ public final class SampleQueueTest { private void assertReadNothing(boolean formatRequired) { clearFormatHolderAndInputBuffer(); int result = - sampleQueue.read( - formatHolder, - inputBuffer, - formatRequired, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); + sampleQueue.read(formatHolder, inputBuffer, formatRequired, /* loadingFinished= */ false); assertThat(result).isEqualTo(RESULT_NOTHING_READ); // formatHolder should not be populated. assertThat(formatHolder.format).isNull(); @@ -1244,12 +1360,7 @@ public final class SampleQueueTest { private void assertReadEndOfStream(boolean formatRequired) { clearFormatHolderAndInputBuffer(); int result = - sampleQueue.read( - formatHolder, - inputBuffer, - formatRequired, - /* loadingFinished= */ true, - /* decodeOnlyUntilUs= */ 0); + sampleQueue.read(formatHolder, inputBuffer, formatRequired, /* loadingFinished= */ true); assertThat(result).isEqualTo(RESULT_BUFFER_READ); // formatHolder should not be populated. assertThat(formatHolder.format).isNull(); @@ -1270,12 +1381,7 @@ public final class SampleQueueTest { private void assertReadFormat(boolean formatRequired, Format format) { clearFormatHolderAndInputBuffer(); int result = - sampleQueue.read( - formatHolder, - inputBuffer, - formatRequired, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); + sampleQueue.read(formatHolder, inputBuffer, formatRequired, /* loadingFinished= */ false); assertThat(result).isEqualTo(RESULT_FORMAT_READ); // formatHolder should be populated. assertThat(formatHolder.format).isEqualTo(format); @@ -1292,6 +1398,7 @@ public final class SampleQueueTest { assertReadSample( ENCRYPTED_SAMPLE_TIMESTAMPS[sampleIndex], isKeyFrame, + /* isDecodeOnly= */ false, isEncrypted, sampleData, /* offset= */ 0, @@ -1304,6 +1411,7 @@ public final class SampleQueueTest { * * @param timeUs The expected buffer timestamp. * @param isKeyFrame The expected keyframe flag. + * @param isDecodeOnly The expected decodeOnly flag. * @param isEncrypted The expected encrypted flag. * @param sampleData An array containing the expected sample data. * @param offset The offset in {@code sampleData} of the expected sample data. @@ -1312,6 +1420,7 @@ public final class SampleQueueTest { private void assertReadSample( long timeUs, boolean isKeyFrame, + boolean isDecodeOnly, boolean isEncrypted, byte[] sampleData, int offset, @@ -1319,18 +1428,14 @@ public final class SampleQueueTest { clearFormatHolderAndInputBuffer(); int result = sampleQueue.read( - formatHolder, - inputBuffer, - /* formatRequired= */ false, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); + formatHolder, inputBuffer, /* formatRequired= */ false, /* loadingFinished= */ false); assertThat(result).isEqualTo(RESULT_BUFFER_READ); // formatHolder should not be populated. assertThat(formatHolder.format).isNull(); // inputBuffer should be populated. assertThat(inputBuffer.timeUs).isEqualTo(timeUs); assertThat(inputBuffer.isKeyFrame()).isEqualTo(isKeyFrame); - assertThat(inputBuffer.isDecodeOnly()).isFalse(); + assertThat(inputBuffer.isDecodeOnly()).isEqualTo(isDecodeOnly); assertThat(inputBuffer.isEncrypted()).isEqualTo(isEncrypted); inputBuffer.flip(); assertThat(inputBuffer.data.limit()).isEqualTo(length); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index 58783ad745..02b0dd3b52 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -380,11 +380,7 @@ public final class PlayerEmsgHandler implements Handler.Callback { buffer.clear(); int result = sampleQueue.read( - formatHolder, - buffer, - /* formatRequired= */ false, - /* loadingFinished= */ false, - /* decodeOnlyUntilUs= */ 0); + formatHolder, buffer, /* formatRequired= */ false, /* loadingFinished= */ false); if (result == C.RESULT_BUFFER_READ) { buffer.flip(); return buffer; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 5f0aff46cc..9fb625313a 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -560,8 +560,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } int result = - sampleQueues[sampleQueueIndex].read( - formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); + sampleQueues[sampleQueueIndex].read(formatHolder, buffer, requireFormat, loadingFinished); if (result == C.RESULT_FORMAT_READ) { Format format = Assertions.checkNotNull(formatHolder.format); if (sampleQueueIndex == primarySampleQueueIndex) { @@ -641,6 +640,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; if (isPendingReset()) { chunkQueue = Collections.emptyList(); loadPositionUs = pendingResetPositionUs; + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.setStartTimeUs(pendingResetPositionUs); + } } else { chunkQueue = readOnlyMediaChunks; HlsMediaChunk lastMediaChunk = getLastMediaChunk();