From f78cbd2c9e348db5818136c0b74fea1c97d34a56 Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Mon, 9 Mar 2020 13:30:06 -0700 Subject: [PATCH 1/5] Add HlsCheckedSampleQueue to check timstamp range Add a SampleQueue subclass that checks the timestamp range of media samples queued to it and reports an exception on load if the timestamp is outside of spec bounds. (Smashed to a single commit prior to rebase) --- .../exoplayer2/source/SampleQueue.java | 2 +- .../UnreportedDiscontinuityException.java | 26 ++++++++ .../source/hls/HlsCheckedSampleQueue.java | 65 +++++++++++++++++++ .../exoplayer2/source/hls/HlsMediaChunk.java | 9 ++- .../source/hls/HlsSampleStreamWrapper.java | 9 +-- 5 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/UnreportedDiscontinuityException.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsCheckedSampleQueue.java 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; From e690979d92e05e90ed8971e9bf733a7ec97e5245 Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Fri, 13 Mar 2020 09:59:27 -0700 Subject: [PATCH 2/5] Update with first round of code review suggestions Update with code review suggestions: 1. rename to HlsCheckedSampleQueue to HlsSampleQueue and combine with format adjusting class. 2. Copywrite in added classes 3. Capture additional items useful for recovery and reporting in the exception class 4. Remove extraneous logging 5. eliminate magic number (50 seconds) and use percentage of duration WIP: - eliminate null check for chunk (chunkless prepare starts load before sampleQueue are created) - work out recovery strategy --- .../UnexpectedDiscontinuityException.java | 68 ++++++++ .../UnreportedDiscontinuityException.java | 26 --- .../source/hls/HlsCheckedSampleQueue.java | 65 ------- .../exoplayer2/source/hls/HlsMediaChunk.java | 9 +- .../exoplayer2/source/hls/HlsSampleQueue.java | 159 ++++++++++++++++++ .../source/hls/HlsSampleStreamWrapper.java | 86 +--------- 6 files changed, 236 insertions(+), 177 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/UnexpectedDiscontinuityException.java delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/UnreportedDiscontinuityException.java delete mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsCheckedSampleQueue.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleQueue.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/UnexpectedDiscontinuityException.java b/library/core/src/main/java/com/google/android/exoplayer2/source/UnexpectedDiscontinuityException.java new file mode 100644 index 0000000000..d6033fef2d --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/UnexpectedDiscontinuityException.java @@ -0,0 +1,68 @@ +/* + * 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; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.upstream.DataSpec; + +/** + * 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 an intra-chunk timestamp discontinuity that was not handled by the + * chunk source (the origin server). + * + * For HLS, the origin server is required to break segments at continuity boundaries by the HLS Pantos spec + * (EXT-X-DISCONTINUITY {@see https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.3}) + * + */ +public final class UnexpectedDiscontinuityException extends RuntimeException { + + /** the last in-bounds timestamp committed to the {@link SampleQueue}, or + * {@link C#TIME_UNSET} if this was for the first committed sample + */ + public final long lastValidTimeUs; + + /** The errant timestamp + */ + public final long deviantSampleTimeUs; + + /** The source of the samples that resulted in this error + */ + public final DataSpec dataSpec; + + /** The timeUs that the source of the samples starts (from HLS metadata) + */ + public final long startTimeUs; + + /** + * Construct an UnexpectedDiscontinuityException for a {@link MediaChunk} where an + * unexpected timestamp discontinuity is detected within its sample source (e.g. segment for HLS) + * + * @param mediaChunk the {@link MediaChunk} with the unexpected timestamp value + * @param lastValidTimeUs the last in-bounds timestamp committed to the {@link SampleQueue}, or + * {@link C#TIME_UNSET} if this was for the first committed sample + * @param deviantSampleTimeUs the timestamp that is out of bounds. + */ + public UnexpectedDiscontinuityException(MediaChunk mediaChunk, long lastValidTimeUs, long deviantSampleTimeUs) { + super("Unexpected discontinuity, timeMs: " + C.usToMs(deviantSampleTimeUs) + " loaded from dataSpec: " + mediaChunk.dataSpec); + this.dataSpec = mediaChunk.dataSpec; + this.startTimeUs = mediaChunk.startTimeUs; + this.lastValidTimeUs = lastValidTimeUs; + this.deviantSampleTimeUs = deviantSampleTimeUs; + } +} 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 deleted file mode 100644 index 9ed2177e1e..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/UnreportedDiscontinuityException.java +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 7630d40680..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsCheckedSampleQueue.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 a2e4840402..e700068321 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,7 +27,7 @@ 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.UnexpectedDiscontinuityException; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.upstream.DataSource; @@ -379,10 +379,9 @@ 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); - + } catch (UnexpectedDiscontinuityException e) { + Log.d(TAG, "UnexpectedDiscontinuityException - recovering by discarding balance of segment", e); + throw new IOException("load aborted for segment - " + e.dataSpec + " unexpected discontinuity", e); } finally { nextLoadPosition = (int) (input.getPosition() - dataSpec.position); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleQueue.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleQueue.java new file mode 100644 index 0000000000..c2e22b8344 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleQueue.java @@ -0,0 +1,159 @@ +/* + * 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.Format; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.PrivFrame; +import com.google.android.exoplayer2.source.SampleQueue; +import com.google.android.exoplayer2.source.UnexpectedDiscontinuityException; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; +import com.google.android.exoplayer2.util.TimestampAdjuster; +import java.util.Map; + +/** + * Extend base SampleQueue to add HLS specific processing of the samples, including: + * + *
    + *
  • segment time boundary checks on timestamps of committed samples
  • + *
  • cleaning the {@link Format#metadata} to avoid excessive format changes
  • + *
+ * + * The timestamp check verifies that the adjusted sample time (via {@link TimestampAdjuster}) does not + * fall outside of a set percentage ({@link #MAX_TIMESTAMP_DEVIATION_PERCENTAGE}) of the time + * boundaries of the segment as expressed by the segment duration ((@link HlsMediaChunk#endTimeUs} - + * {@link HlsMediaChunk#startTimeUs}). This is loosely mandated by the Pantos spec and checked by + * Apple's mediastreamvalidator. + * + */ +public class HlsSampleQueue extends SampleQueue { + + private static final String TAG = "HlsSampleQueue"; + + /** + * largest timestamp deviation from the segment time bounds expressed as a percentage of + * the segment duration. + */ + public static double MAX_TIMESTAMP_DEVIATION_PERCENTAGE = 0.50; + + private long lowestTimeUs = C.TIME_UNSET; + private long highestTimeUs = C.TIME_UNSET; + + @Nullable private HlsMediaChunk chunk; + private long lastValidTimeUs; + + private final Map overridingDrmInitData; + @Nullable private DrmInitData drmInitData; + + public HlsSampleQueue(Allocator allocator, + DrmSessionManager drmSessionManager, + MediaSourceEventDispatcher eventDispatcher, + Map overridingDrmInitData) { + super(allocator, drmSessionManager, eventDispatcher); + this.overridingDrmInitData = overridingDrmInitData; + } + + void setCurrentLoadingChunk(HlsMediaChunk chunk) { + double tolerance = (chunk.endTimeUs - chunk.startTimeUs) * MAX_TIMESTAMP_DEVIATION_PERCENTAGE; + this.lowestTimeUs = (long) (chunk.startTimeUs - tolerance); + this.highestTimeUs = (long) (chunk.endTimeUs + tolerance); + this.chunk = chunk; + lastValidTimeUs = C.TIME_UNSET; + } + + public void setDrmInitData(@Nullable DrmInitData drmInitData) { + this.drmInitData = drmInitData; + invalidateUpstreamFormatAdjustment(); + } + + @SuppressWarnings("ReferenceEquality") + @Override + public Format getAdjustedUpstreamFormat(Format format) { + @Nullable + DrmInitData drmInitData = this.drmInitData != null ? this.drmInitData : format.drmInitData; + if (drmInitData != null) { + @Nullable + DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType); + if (overridingDrmInitData != null) { + drmInitData = overridingDrmInitData; + } + } + @Nullable Metadata metadata = getAdjustedMetadata(format.metadata); + if (drmInitData != format.drmInitData || metadata != format.metadata) { + format = format.buildUpon().setDrmInitData(drmInitData).setMetadata(metadata).build(); + } + return super.getAdjustedUpstreamFormat(format); + } + + /** + * Strips the private timestamp frame from metadata, if present. See: + * https://github.com/google/ExoPlayer/issues/5063 + */ + @Nullable + private Metadata getAdjustedMetadata(@Nullable Metadata metadata) { + if (metadata == null) { + return null; + } + int length = metadata.length(); + int transportStreamTimestampMetadataIndex = C.INDEX_UNSET; + for (int i = 0; i < length; i++) { + Metadata.Entry metadataEntry = metadata.get(i); + if (metadataEntry instanceof PrivFrame) { + PrivFrame privFrame = (PrivFrame) metadataEntry; + if (HlsMediaChunk.PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { + transportStreamTimestampMetadataIndex = i; + break; + } + } + } + if (transportStreamTimestampMetadataIndex == C.INDEX_UNSET) { + return metadata; + } + if (length == 1) { + return null; + } + Metadata.Entry[] newMetadataEntries = new Metadata.Entry[length - 1]; + for (int i = 0; i < length; i++) { + if (i != transportStreamTimestampMetadataIndex) { + int newIndex = i < transportStreamTimestampMetadataIndex ? i : i - 1; + newMetadataEntries[newIndex] = metadata.get(i); + } + } + return new Metadata(newMetadataEntries); + } + + @Override + public void sampleMetadata(long timeUs, int flags, int size, int offset, @Nullable CryptoData cryptoData) { + // TODO - chunkless prepare, sampleQueue list is not yet initialized for first chunk +// Assertions.checkNotNull(chunk, "sampleMetadata without a MediaChunk?"); + if (chunk == null) { + super.sampleMetadata(timeUs, flags, size, offset, cryptoData); + } else if (timeUs > highestTimeUs || timeUs < lowestTimeUs) { + throw new UnexpectedDiscontinuityException(chunk, lastValidTimeUs, timeUs); + } else { + lastValidTimeUs = timeUs; + super.sampleMetadata(timeUs, flags, size, offset, cryptoData); + } + } + +} 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 434d30448b..9c7d5f889e 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 @@ -129,7 +129,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final ArrayList hlsSampleStreams; private final Map overridingDrmInitData; - private FormatAdjustingSampleQueue[] sampleQueues; + private HlsSampleQueue[] sampleQueues; private int[] sampleQueueTrackIds; private Set sampleQueueMappingDoneByType; private SparseIntArray sampleQueueIndicesByType; @@ -209,7 +209,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; sampleQueueTrackIds = new int[0]; sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size()); sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size()); - sampleQueues = new FormatAdjustingSampleQueue[0]; + sampleQueues = new HlsSampleQueue[0]; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; mediaChunks = new ArrayList<>(); @@ -827,7 +827,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; */ public void init(int chunkUid, HlsMediaChunk loadingChunk, boolean shouldSpliceIn) { this.chunkUid = chunkUid; - for (HlsCheckedSampleQueue sampleQueue : sampleQueues) { + for (HlsSampleQueue sampleQueue : sampleQueues) { sampleQueue.sourceId(chunkUid); sampleQueue.setCurrentLoadingChunk(loadingChunk); } @@ -908,8 +908,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; int trackCount = sampleQueues.length; boolean isAudioVideo = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; - FormatAdjustingSampleQueue trackOutput = - new FormatAdjustingSampleQueue( + HlsSampleQueue trackOutput = + new HlsSampleQueue( allocator, drmSessionManager, eventDispatcher, overridingDrmInitData); if (isAudioVideo) { trackOutput.setDrmInitData(drmInitData); @@ -1343,82 +1343,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return new DummyTrackOutput(); } - private static final class FormatAdjustingSampleQueue extends HlsCheckedSampleQueue { - - private final Map overridingDrmInitData; - @Nullable private DrmInitData drmInitData; - - public FormatAdjustingSampleQueue( - Allocator allocator, - DrmSessionManager drmSessionManager, - MediaSourceEventDispatcher eventDispatcher, - Map overridingDrmInitData) { - super(allocator, drmSessionManager, eventDispatcher); - this.overridingDrmInitData = overridingDrmInitData; - } - - public void setDrmInitData(@Nullable DrmInitData drmInitData) { - this.drmInitData = drmInitData; - invalidateUpstreamFormatAdjustment(); - } - - @SuppressWarnings("ReferenceEquality") - @Override - public Format getAdjustedUpstreamFormat(Format format) { - @Nullable - DrmInitData drmInitData = this.drmInitData != null ? this.drmInitData : format.drmInitData; - if (drmInitData != null) { - @Nullable - DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType); - if (overridingDrmInitData != null) { - drmInitData = overridingDrmInitData; - } - } - @Nullable Metadata metadata = getAdjustedMetadata(format.metadata); - if (drmInitData != format.drmInitData || metadata != format.metadata) { - format = format.buildUpon().setDrmInitData(drmInitData).setMetadata(metadata).build(); - } - return super.getAdjustedUpstreamFormat(format); - } - - /** - * Strips the private timestamp frame from metadata, if present. See: - * https://github.com/google/ExoPlayer/issues/5063 - */ - @Nullable - private Metadata getAdjustedMetadata(@Nullable Metadata metadata) { - if (metadata == null) { - return null; - } - int length = metadata.length(); - int transportStreamTimestampMetadataIndex = C.INDEX_UNSET; - for (int i = 0; i < length; i++) { - Metadata.Entry metadataEntry = metadata.get(i); - if (metadataEntry instanceof PrivFrame) { - PrivFrame privFrame = (PrivFrame) metadataEntry; - if (HlsMediaChunk.PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { - transportStreamTimestampMetadataIndex = i; - break; - } - } - } - if (transportStreamTimestampMetadataIndex == C.INDEX_UNSET) { - return metadata; - } - if (length == 1) { - return null; - } - Metadata.Entry[] newMetadataEntries = new Metadata.Entry[length - 1]; - for (int i = 0; i < length; i++) { - if (i != transportStreamTimestampMetadataIndex) { - int newIndex = i < transportStreamTimestampMetadataIndex ? i : i - 1; - newMetadataEntries[newIndex] = metadata.get(i); - } - } - return new Metadata(newMetadataEntries); - } - } - private static class EmsgUnwrappingTrackOutput implements TrackOutput { private static final String TAG = "EmsgUnwrappingTrackOutput"; From 0d46e24f944e41c838b826dc05e9e2ee47c75d68 Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Mon, 16 Mar 2020 11:53:49 -0700 Subject: [PATCH 3/5] Update comments for DASH Update the comment to indicate the DASH equivalent use case for the exception. --- .../exoplayer2/source/UnexpectedDiscontinuityException.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/UnexpectedDiscontinuityException.java b/library/core/src/main/java/com/google/android/exoplayer2/source/UnexpectedDiscontinuityException.java index d6033fef2d..2d1a784c10 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/UnexpectedDiscontinuityException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/UnexpectedDiscontinuityException.java @@ -27,7 +27,9 @@ import com.google.android.exoplayer2.upstream.DataSpec; * chunk source (the origin server). * * For HLS, the origin server is required to break segments at continuity boundaries by the HLS Pantos spec - * (EXT-X-DISCONTINUITY {@see https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.3}) + * (EXT-X-DISCONTINUITY {@see https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.3}). + * In DASH, segments must be divided into periods when there are timestamp discontinuities + * {@see https://www.w3.org/2018/12/webmediaguidelines.html#server-side-ad-insertion} * */ public final class UnexpectedDiscontinuityException extends RuntimeException { From ccf2ba3e1bb8cc8bec5ca9a94c3b99750096189f Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Mon, 16 Mar 2020 13:51:22 -0700 Subject: [PATCH 4/5] Move HlsSampleQueue to be static inner-class Backout making the original `FormatAdjustingSampleQueue` an outer class and combining the new timestamp checking logic. This way the diff from `HlsSampleStreamWrapper` to dev-v2 are easier to see. --- .../exoplayer2/source/hls/HlsSampleQueue.java | 159 ------------------ .../source/hls/HlsSampleStreamWrapper.java | 130 ++++++++++++++ 2 files changed, 130 insertions(+), 159 deletions(-) delete mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleQueue.java diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleQueue.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleQueue.java deleted file mode 100644 index c2e22b8344..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleQueue.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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.Format; -import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.PrivFrame; -import com.google.android.exoplayer2.source.SampleQueue; -import com.google.android.exoplayer2.source.UnexpectedDiscontinuityException; -import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; -import com.google.android.exoplayer2.util.TimestampAdjuster; -import java.util.Map; - -/** - * Extend base SampleQueue to add HLS specific processing of the samples, including: - * - *
    - *
  • segment time boundary checks on timestamps of committed samples
  • - *
  • cleaning the {@link Format#metadata} to avoid excessive format changes
  • - *
- * - * The timestamp check verifies that the adjusted sample time (via {@link TimestampAdjuster}) does not - * fall outside of a set percentage ({@link #MAX_TIMESTAMP_DEVIATION_PERCENTAGE}) of the time - * boundaries of the segment as expressed by the segment duration ((@link HlsMediaChunk#endTimeUs} - - * {@link HlsMediaChunk#startTimeUs}). This is loosely mandated by the Pantos spec and checked by - * Apple's mediastreamvalidator. - * - */ -public class HlsSampleQueue extends SampleQueue { - - private static final String TAG = "HlsSampleQueue"; - - /** - * largest timestamp deviation from the segment time bounds expressed as a percentage of - * the segment duration. - */ - public static double MAX_TIMESTAMP_DEVIATION_PERCENTAGE = 0.50; - - private long lowestTimeUs = C.TIME_UNSET; - private long highestTimeUs = C.TIME_UNSET; - - @Nullable private HlsMediaChunk chunk; - private long lastValidTimeUs; - - private final Map overridingDrmInitData; - @Nullable private DrmInitData drmInitData; - - public HlsSampleQueue(Allocator allocator, - DrmSessionManager drmSessionManager, - MediaSourceEventDispatcher eventDispatcher, - Map overridingDrmInitData) { - super(allocator, drmSessionManager, eventDispatcher); - this.overridingDrmInitData = overridingDrmInitData; - } - - void setCurrentLoadingChunk(HlsMediaChunk chunk) { - double tolerance = (chunk.endTimeUs - chunk.startTimeUs) * MAX_TIMESTAMP_DEVIATION_PERCENTAGE; - this.lowestTimeUs = (long) (chunk.startTimeUs - tolerance); - this.highestTimeUs = (long) (chunk.endTimeUs + tolerance); - this.chunk = chunk; - lastValidTimeUs = C.TIME_UNSET; - } - - public void setDrmInitData(@Nullable DrmInitData drmInitData) { - this.drmInitData = drmInitData; - invalidateUpstreamFormatAdjustment(); - } - - @SuppressWarnings("ReferenceEquality") - @Override - public Format getAdjustedUpstreamFormat(Format format) { - @Nullable - DrmInitData drmInitData = this.drmInitData != null ? this.drmInitData : format.drmInitData; - if (drmInitData != null) { - @Nullable - DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType); - if (overridingDrmInitData != null) { - drmInitData = overridingDrmInitData; - } - } - @Nullable Metadata metadata = getAdjustedMetadata(format.metadata); - if (drmInitData != format.drmInitData || metadata != format.metadata) { - format = format.buildUpon().setDrmInitData(drmInitData).setMetadata(metadata).build(); - } - return super.getAdjustedUpstreamFormat(format); - } - - /** - * Strips the private timestamp frame from metadata, if present. See: - * https://github.com/google/ExoPlayer/issues/5063 - */ - @Nullable - private Metadata getAdjustedMetadata(@Nullable Metadata metadata) { - if (metadata == null) { - return null; - } - int length = metadata.length(); - int transportStreamTimestampMetadataIndex = C.INDEX_UNSET; - for (int i = 0; i < length; i++) { - Metadata.Entry metadataEntry = metadata.get(i); - if (metadataEntry instanceof PrivFrame) { - PrivFrame privFrame = (PrivFrame) metadataEntry; - if (HlsMediaChunk.PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { - transportStreamTimestampMetadataIndex = i; - break; - } - } - } - if (transportStreamTimestampMetadataIndex == C.INDEX_UNSET) { - return metadata; - } - if (length == 1) { - return null; - } - Metadata.Entry[] newMetadataEntries = new Metadata.Entry[length - 1]; - for (int i = 0; i < length; i++) { - if (i != transportStreamTimestampMetadataIndex) { - int newIndex = i < transportStreamTimestampMetadataIndex ? i : i - 1; - newMetadataEntries[newIndex] = metadata.get(i); - } - } - return new Metadata(newMetadataEntries); - } - - @Override - public void sampleMetadata(long timeUs, int flags, int size, int offset, @Nullable CryptoData cryptoData) { - // TODO - chunkless prepare, sampleQueue list is not yet initialized for first chunk -// Assertions.checkNotNull(chunk, "sampleMetadata without a MediaChunk?"); - if (chunk == null) { - super.sampleMetadata(timeUs, flags, size, offset, cryptoData); - } else if (timeUs > highestTimeUs || timeUs < lowestTimeUs) { - throw new UnexpectedDiscontinuityException(chunk, lastValidTimeUs, timeUs); - } else { - lastValidTimeUs = timeUs; - super.sampleMetadata(timeUs, flags, size, offset, cryptoData); - } - } - -} 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 9c7d5f889e..15b57d708b 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 @@ -43,6 +43,7 @@ import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.UnexpectedDiscontinuityException; import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -56,6 +57,7 @@ import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; @@ -1338,6 +1340,134 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return true; } + /** + * Extend base SampleQueue to add HLS specific processing of the samples, including: + * + *
    + *
  • segment time boundary checks on timestamps of committed samples
  • + *
  • cleaning the {@link Format#metadata} to avoid excessive format changes
  • + *
+ * + * The timestamp check verifies that the adjusted sample time (via {@link TimestampAdjuster}) does not + * fall outside of a set percentage ({@link #MAX_TIMESTAMP_DEVIATION_PERCENTAGE}) of the time + * boundaries of the segment as expressed by the segment duration ((@link HlsMediaChunk#endTimeUs} - + * {@link HlsMediaChunk#startTimeUs}). This is loosely mandated by the Pantos spec and checked by + * Apple's mediastreamvalidator. + * + */ + private static final class HlsSampleQueue extends SampleQueue { + + private static final String TAG = "HlsSampleQueue"; + + /** + * largest timestamp deviation from the segment time bounds expressed as a percentage of + * the segment duration. + */ + private static double MAX_TIMESTAMP_DEVIATION_PERCENTAGE = 0.50; + + private long lowestTimeUs = C.TIME_UNSET; + private long highestTimeUs = C.TIME_UNSET; + + @Nullable private HlsMediaChunk chunk; + private long lastValidTimeUs; + + private final Map overridingDrmInitData; + @Nullable private DrmInitData drmInitData; + + private HlsSampleQueue(Allocator allocator, + DrmSessionManager drmSessionManager, + MediaSourceEventDispatcher eventDispatcher, + Map overridingDrmInitData) { + super(allocator, drmSessionManager, eventDispatcher); + this.overridingDrmInitData = overridingDrmInitData; + } + + void setCurrentLoadingChunk(HlsMediaChunk chunk) { + double tolerance = (chunk.endTimeUs - chunk.startTimeUs) * MAX_TIMESTAMP_DEVIATION_PERCENTAGE; + this.lowestTimeUs = (long) (chunk.startTimeUs - tolerance); + this.highestTimeUs = (long) (chunk.endTimeUs + tolerance); + this.chunk = chunk; + lastValidTimeUs = C.TIME_UNSET; + } + + private void setDrmInitData(@Nullable DrmInitData drmInitData) { + this.drmInitData = drmInitData; + invalidateUpstreamFormatAdjustment(); + } + + @SuppressWarnings("ReferenceEquality") + @Override + public Format getAdjustedUpstreamFormat(Format format) { + @Nullable + DrmInitData drmInitData = this.drmInitData != null ? this.drmInitData : format.drmInitData; + if (drmInitData != null) { + @Nullable + DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType); + if (overridingDrmInitData != null) { + drmInitData = overridingDrmInitData; + } + } + @Nullable Metadata metadata = getAdjustedMetadata(format.metadata); + if (drmInitData != format.drmInitData || metadata != format.metadata) { + format = format.buildUpon().setDrmInitData(drmInitData).setMetadata(metadata).build(); + } + return super.getAdjustedUpstreamFormat(format); + } + + /** + * Strips the private timestamp frame from metadata, if present. See: + * https://github.com/google/ExoPlayer/issues/5063 + */ + @Nullable + private Metadata getAdjustedMetadata(@Nullable Metadata metadata) { + if (metadata == null) { + return null; + } + int length = metadata.length(); + int transportStreamTimestampMetadataIndex = C.INDEX_UNSET; + for (int i = 0; i < length; i++) { + Metadata.Entry metadataEntry = metadata.get(i); + if (metadataEntry instanceof PrivFrame) { + PrivFrame privFrame = (PrivFrame) metadataEntry; + if (HlsMediaChunk.PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { + transportStreamTimestampMetadataIndex = i; + break; + } + } + } + if (transportStreamTimestampMetadataIndex == C.INDEX_UNSET) { + return metadata; + } + if (length == 1) { + return null; + } + Metadata.Entry[] newMetadataEntries = new Metadata.Entry[length - 1]; + for (int i = 0; i < length; i++) { + if (i != transportStreamTimestampMetadataIndex) { + int newIndex = i < transportStreamTimestampMetadataIndex ? i : i - 1; + newMetadataEntries[newIndex] = metadata.get(i); + } + } + return new Metadata(newMetadataEntries); + } + + @Override + public void sampleMetadata(long timeUs, int flags, int size, int offset, @Nullable CryptoData cryptoData) { + // TODO - chunkless prepare, sampleQueue list is not yet initialized for first chunk + // Assertions.checkNotNull(chunk, "sampleMetadata without a MediaChunk?"); + if (chunk == null) { + super.sampleMetadata(timeUs, flags, size, offset, cryptoData); + } else if (timeUs > highestTimeUs || timeUs < lowestTimeUs) { + throw new UnexpectedDiscontinuityException(chunk, lastValidTimeUs, timeUs); + } else { + lastValidTimeUs = timeUs; + super.sampleMetadata(timeUs, flags, size, offset, cryptoData); + } + } + + } + + private static DummyTrackOutput createDummyTrackOutput(int id, int type) { Log.w(TAG, "Unmapped track with id " + id + " of type " + type); return new DummyTrackOutput(); From 48592071a3bc8f250663fe6362455ade81fff07f Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Mon, 30 Mar 2020 14:40:02 -0700 Subject: [PATCH 5/5] Move back to `UnexpectedDiscontinuityException` extending a `RuntimeException` This avoids massive changes to method signatures to add throws. Also, took suggestion to make it an `IllegalStateException`. Move the catch outside of the finally that sets `nextLoadPosition` (this allows for possible recovery by reseting the `Extractor` and `TimestampAdjuster`). Lastly, took the suggestion to make a minimum value for the tolerance (especially usefull for very short i-Frame only segments). --- .../source/UnexpectedDiscontinuityException.java | 2 +- .../exoplayer2/source/hls/HlsMediaChunk.java | 6 +++--- .../source/hls/HlsSampleStreamWrapper.java | 14 ++++++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/UnexpectedDiscontinuityException.java b/library/core/src/main/java/com/google/android/exoplayer2/source/UnexpectedDiscontinuityException.java index 2d1a784c10..6f810eb0b9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/UnexpectedDiscontinuityException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/UnexpectedDiscontinuityException.java @@ -32,7 +32,7 @@ import com.google.android.exoplayer2.upstream.DataSpec; * {@see https://www.w3.org/2018/12/webmediaguidelines.html#server-side-ad-insertion} * */ -public final class UnexpectedDiscontinuityException extends RuntimeException { +public final class UnexpectedDiscontinuityException extends IllegalStateException { /** the last in-bounds timestamp committed to the {@link SampleQueue}, or * {@link C#TIME_UNSET} if this was for the first committed sample 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 e700068321..11c176bf34 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 @@ -379,12 +379,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { result = extractor.read(input, DUMMY_POSITION_HOLDER); } - } catch (UnexpectedDiscontinuityException e) { - Log.d(TAG, "UnexpectedDiscontinuityException - recovering by discarding balance of segment", e); - throw new IOException("load aborted for segment - " + e.dataSpec + " unexpected discontinuity", e); } finally { nextLoadPosition = (int) (input.getPosition() - dataSpec.position); } + } catch (UnexpectedDiscontinuityException e) { + Log.d(TAG, "UnexpectedDiscontinuityException - recovering by discarding balance of segment", e); + throw new IOException("load aborted for segment - " + e.dataSpec + " unexpected discontinuity", e); } finally { Util.closeQuietly(dataSource); } 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 15b57d708b..e32138d5d1 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 @@ -1360,10 +1360,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private static final String TAG = "HlsSampleQueue"; /** - * largest timestamp deviation from the segment time bounds expressed as a percentage of - * the segment duration. + * Used to compute the "tolerance" value, that is the largest timestamp deviation from + * the segment time bounds. The "tolerance" is set to this percentage of the segment duration. */ - private static double MAX_TIMESTAMP_DEVIATION_PERCENTAGE = 0.50; + private static double MAX_TIMESTAMP_DEVIATION_PERCENTAGE = 0.75; + + /** + * Min time value for "tolerance", for very short segment durations, this overrides + * the percentage + */ + private static double MIN_TOLERANCE_US = 4_000_000; private long lowestTimeUs = C.TIME_UNSET; private long highestTimeUs = C.TIME_UNSET; @@ -1383,7 +1389,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } void setCurrentLoadingChunk(HlsMediaChunk chunk) { - double tolerance = (chunk.endTimeUs - chunk.startTimeUs) * MAX_TIMESTAMP_DEVIATION_PERCENTAGE; + double tolerance = Math.max(MIN_TOLERANCE_US, (chunk.endTimeUs - chunk.startTimeUs) * MAX_TIMESTAMP_DEVIATION_PERCENTAGE); this.lowestTimeUs = (long) (chunk.startTimeUs - tolerance); this.highestTimeUs = (long) (chunk.endTimeUs + tolerance); this.chunk = chunk;