From ccf2ba3e1bb8cc8bec5ca9a94c3b99750096189f Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Mon, 16 Mar 2020 13:51:22 -0700 Subject: [PATCH] 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: - * - * - * - * 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();