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 ccbed81124..6b9039eec9 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 @@ -488,7 +488,7 @@ public class SampleQueue implements TrackOutput { } @Override - public final void sampleMetadata( + public void sampleMetadata( long timeUs, @C.BufferFlags int flags, int size, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/UnreportedDiscontinuityException.java b/library/core/src/main/java/com/google/android/exoplayer2/source/UnreportedDiscontinuityException.java new file mode 100644 index 0000000000..9ed2177e1e --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/UnreportedDiscontinuityException.java @@ -0,0 +1,26 @@ +package com.google.android.exoplayer2.source; + +import android.net.Uri; +import com.google.android.exoplayer2.C; + +/** + * Thrown from the loader thread when an attempt is made to commit a sample that is far + * deviant from the expected sequence of timestamps in the SampleStream. This is likely + * caused by a discontinuity in a segment that was not split and reported by metadata in + * an HLS (EXT-X-DISCONTINUITY) or DASH stream. + */ +public class UnreportedDiscontinuityException extends RuntimeException { + + public final long timesUs; + + /** + * Consturct the exception + * + * @param timesUs last timestamp before attempted commit of the deviant sample + * @param uri uri of the segment with the unreported discontinuity + */ + public UnreportedDiscontinuityException(long timesUs, Uri uri) { + super("Unreported discontinuity timeMs: " + C.usToMs(timesUs) + " in URI: " + uri); + this.timesUs = timesUs; + } +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsCheckedSampleQueue.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsCheckedSampleQueue.java new file mode 100644 index 0000000000..7630d40680 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsCheckedSampleQueue.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.source.SampleQueue; +import com.google.android.exoplayer2.source.UnreportedDiscontinuityException; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Log; + +public class HlsCheckedSampleQueue extends SampleQueue { + private static final String TAG = "HlsCheckedSampleQueue"; + + private long lowestTimeUs = C.TIME_UNSET; + private long highestTimeUs = C.TIME_UNSET; + + private HlsMediaChunk chunk; + private boolean loggedFirst = false; + + HlsCheckedSampleQueue(Allocator allocator, DrmSessionManager drmSessionManager) { + super(allocator, drmSessionManager); + } + + void setCurrentLoadingChunk(HlsMediaChunk chunk) { + double tolerance = (chunk.endTimeUs - chunk.startTimeUs) * 0.1; + this.lowestTimeUs = chunk.startTimeUs; + this.highestTimeUs = (long) (chunk.endTimeUs + tolerance); + this.chunk = chunk; + loggedFirst = false; + } + + + @Override + public void sampleMetadata(long timeUs, int flags, int size, int offset, @Nullable CryptoData cryptoData) { + if (lowestTimeUs != C.TIME_UNSET && timeUs < lowestTimeUs && ! loggedFirst) { + Log.d(TAG, "sampleMetadata() - committed timeUs: " + timeUs + " is " + C.usToMs(lowestTimeUs - timeUs) + "ms less then segment start time. chunk: " + chunk.dataSpec.uri); + loggedFirst = true; + } + if (lowestTimeUs != C.TIME_UNSET && timeUs < (lowestTimeUs - C.msToUs(50_000))) { + Log.d(TAG, "sampleMetadata() - committed timeUs: " + timeUs + " is " + C.usToMs(lowestTimeUs - timeUs) + "ms less (MUCH!) then segment start time. chunk: " + chunk.dataSpec.uri); + throw new UnreportedDiscontinuityException(timeUs, chunk.dataSpec.uri); + } + if (highestTimeUs != C.TIME_UNSET && timeUs > highestTimeUs) { + Log.d(TAG, "sampleMetadata() - committed timeUs: " + timeUs + " is " + C.usToMs(lowestTimeUs - timeUs) + "ms greater then segment end time. chunk: " + chunk.dataSpec.uri); + throw new UnreportedDiscontinuityException(timeUs, chunk.dataSpec.uri); + } + super.sampleMetadata(timeUs, flags, size, offset, cryptoData); + } + +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index d3c58a55a0..a2e4840402 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -27,11 +27,13 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.PrivFrame; +import com.google.android.exoplayer2.source.UnreportedDiscontinuityException; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.UriUtil; @@ -50,6 +52,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * An HLS {@link MediaChunk}. */ /* package */ final class HlsMediaChunk extends MediaChunk { + private static final String TAG = "HlsMediaChunk"; /** * Creates a new instance. @@ -282,7 +285,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; */ public void init(HlsSampleStreamWrapper output) { this.output = output; - output.init(uid, shouldSpliceIn); + output.init(uid, this, shouldSpliceIn); } @Override @@ -376,6 +379,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { result = extractor.read(input, DUMMY_POSITION_HOLDER); } + } catch (UnreportedDiscontinuityException e) { + Log.d(TAG, "Unreported discontinuity at timeUs: "+ e.timesUs + " uri: " + dataSpec.uri); + throw new IOException("Timestamp error", e); + } finally { nextLoadPosition = (int) (input.getPosition() - dataSpec.position); } 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 3a717ec2cd..434d30448b 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 @@ -820,15 +820,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Initializes the wrapper for loading a chunk. - * * @param chunkUid The chunk's uid. + * @param loadingChunk the Chunk that is about to start loading. * @param shouldSpliceIn Whether the samples parsed from the chunk should be spliced into any * samples already queued to the wrapper. */ - public void init(int chunkUid, boolean shouldSpliceIn) { + public void init(int chunkUid, HlsMediaChunk loadingChunk, boolean shouldSpliceIn) { this.chunkUid = chunkUid; - for (SampleQueue sampleQueue : sampleQueues) { + for (HlsCheckedSampleQueue sampleQueue : sampleQueues) { sampleQueue.sourceId(chunkUid); + sampleQueue.setCurrentLoadingChunk(loadingChunk); } if (shouldSpliceIn) { for (SampleQueue sampleQueue : sampleQueues) { @@ -1342,7 +1343,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return new DummyTrackOutput(); } - private static final class FormatAdjustingSampleQueue extends SampleQueue { + private static final class FormatAdjustingSampleQueue extends HlsCheckedSampleQueue { private final Map overridingDrmInitData; @Nullable private DrmInitData drmInitData;