From 0ceff589b33992a98db0e439b24aea75f772e6ef Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 18 Feb 2019 15:33:17 +0000 Subject: [PATCH] HLS Bugfix: Avoid loading initialization segment when reusing extractor This bug affects any playlist that uses initialization segments. In practice, almost exclusively fragmented mp4 segmented playlists are affected. The bottom line is that extractors are chosen for reuse after the initialization segment connection is open. However, reused extractors do not need re-parsing the init segment, so loading the initialization is wasteful. PiperOrigin-RevId: 234479467 --- RELEASENOTES.md | 2 + .../hls/DefaultHlsExtractorFactory.java | 49 ++++++++++++------- .../source/hls/HlsExtractorFactory.java | 34 +++++++++++-- .../exoplayer2/source/hls/HlsMediaChunk.java | 33 +++++++------ 4 files changed, 80 insertions(+), 38 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 08837b4a60..6fd0e7d682 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,8 @@ * Form an adaptive track group out of audio renditions with matching name. * Support encrypted initialization segments ([#5441](https://github.com/google/ExoPlayer/issues/5441)). + * Prevent unnecessary reloads of the initialization segment for reused + extractors. * DASH: * Fix issue handling large `EventStream` presentation timestamps ([#5490](https://github.com/google/ExoPlayer/issues/5490)). diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 8a403c3759..8be0371072 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.text.TextUtils; -import android.util.Pair; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.Extractor; @@ -71,7 +70,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { } @Override - public Pair createExtractor( + public Result createExtractor( Extractor previousExtractor, Uri uri, Format format, @@ -84,21 +83,15 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { if (previousExtractor != null) { // A extractor has already been successfully used. Return one of the same type. - if (previousExtractor instanceof TsExtractor - || previousExtractor instanceof FragmentedMp4Extractor) { - // TS and fMP4 extractors can be reused. + if (isReusable(previousExtractor)) { return buildResult(previousExtractor); - } else if (previousExtractor instanceof WebvttExtractor) { - return buildResult(new WebvttExtractor(format.language, timestampAdjuster)); - } else if (previousExtractor instanceof AdtsExtractor) { - return buildResult(new AdtsExtractor()); - } else if (previousExtractor instanceof Ac3Extractor) { - return buildResult(new Ac3Extractor()); - } else if (previousExtractor instanceof Mp3Extractor) { - return buildResult(new Mp3Extractor()); } else { - throw new IllegalArgumentException( - "Unexpected previousExtractor type: " + previousExtractor.getClass().getSimpleName()); + Result result = + buildResultForSameExtractorType(previousExtractor, format, timestampAdjuster); + if (result == null) { + throw new IllegalArgumentException( + "Unexpected previousExtractor type: " + previousExtractor.getClass().getSimpleName()); + } } } @@ -249,12 +242,28 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { new DefaultTsPayloadReaderFactory(payloadReaderFactoryFlags, muxedCaptionFormats)); } - private static Pair buildResult(Extractor extractor) { - return new Pair<>( + private static Result buildResultForSameExtractorType( + Extractor previousExtractor, Format format, TimestampAdjuster timestampAdjuster) { + if (previousExtractor instanceof WebvttExtractor) { + return buildResult(new WebvttExtractor(format.language, timestampAdjuster)); + } else if (previousExtractor instanceof AdtsExtractor) { + return buildResult(new AdtsExtractor()); + } else if (previousExtractor instanceof Ac3Extractor) { + return buildResult(new Ac3Extractor()); + } else if (previousExtractor instanceof Mp3Extractor) { + return buildResult(new Mp3Extractor()); + } else { + return null; + } + } + + private static Result buildResult(Extractor extractor) { + return new Result( extractor, extractor instanceof AdtsExtractor || extractor instanceof Ac3Extractor - || extractor instanceof Mp3Extractor); + || extractor instanceof Mp3Extractor, + isReusable(extractor)); } private static boolean sniffQuietly(Extractor extractor, ExtractorInput input) @@ -270,4 +279,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { return result; } + private static boolean isReusable(Extractor previousExtractor) { + return previousExtractor instanceof TsExtractor + || previousExtractor instanceof FragmentedMp4Extractor; + } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java index eea64d4dc6..103d89188f 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; -import android.util.Pair; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.Extractor; @@ -32,6 +31,33 @@ import java.util.Map; */ public interface HlsExtractorFactory { + /** Holds an {@link Extractor} and associated parameters. */ + final class Result { + + /** The created extractor; */ + public final Extractor extractor; + /** Whether the segments for which {@link #extractor} is created are packed audio segments. */ + public final boolean isPackedAudioExtractor; + /** + * Whether {@link #extractor} may be reused for following continuous (no immediately preceding + * discontinuities) segments of the same variant. + */ + public final boolean isReusable; + + /** + * Creates a result. + * + * @param extractor See {@link #extractor}. + * @param isPackedAudioExtractor See {@link #isPackedAudioExtractor}. + * @param isReusable See {@link #isReusable}. + */ + public Result(Extractor extractor, boolean isPackedAudioExtractor, boolean isReusable) { + this.extractor = extractor; + this.isPackedAudioExtractor = isPackedAudioExtractor; + this.isReusable = isReusable; + } + } + HlsExtractorFactory DEFAULT = new DefaultHlsExtractorFactory(); /** @@ -51,13 +77,11 @@ public interface HlsExtractorFactory { * @param sniffingExtractorInput The first extractor input that will be passed to the returned * extractor's {@link Extractor#read(ExtractorInput, PositionHolder)}. Must only be used to * call {@link Extractor#sniff(ExtractorInput)}. - * @return A pair containing the {@link Extractor} and a boolean that indicates whether it is a - * packed audio extractor. The first element may be {@code previousExtractor} if the factory - * has determined it can be re-used. + * @return A {@link Result}. * @throws InterruptedException If the thread is interrupted while sniffing. * @throws IOException If an I/O error is encountered while sniffing. */ - Pair createExtractor( + Result createExtractor( Extractor previousExtractor, Uri uri, Format format, 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 d307bbb04d..a7e8fcc292 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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.support.annotation.Nullable; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; @@ -133,9 +132,11 @@ import java.util.concurrent.atomic.AtomicInteger; scratchId3Data = previousChunk.scratchId3Data; shouldSpliceIn = previousChunk.hlsUrl != hlsUrl || !previousChunk.loadCompleted; previousExtractor = - previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber || shouldSpliceIn - ? null - : previousChunk.extractor; + previousChunk.isExtractorReusable + && previousChunk.discontinuitySequenceNumber == discontinuitySequenceNumber + && !shouldSpliceIn + ? previousChunk.extractor + : null; } else { id3Decoder = new Id3Decoder(); scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); @@ -204,6 +205,7 @@ import java.util.concurrent.atomic.AtomicInteger; private final boolean initSegmentEncrypted; private Extractor extractor; + private boolean isExtractorReusable; private HlsSampleStreamWrapper output; private int initSegmentBytesLoaded; private int nextLoadPosition; @@ -287,6 +289,12 @@ import java.util.concurrent.atomic.AtomicInteger; @Override public void load() throws IOException, InterruptedException { + if (extractor == null && previousExtractor != null) { + extractor = previousExtractor; + isExtractorReusable = true; + initLoadCompleted = initDataSpec != null; + output.init(uid, shouldSpliceIn, /* reusingExtractor= */ true); + } maybeLoadInitData(); if (!loadCanceled) { if (!hasGapTag) { @@ -380,7 +388,7 @@ import java.util.concurrent.atomic.AtomicInteger; long id3Timestamp = peekId3PrivTimestamp(extractorInput); extractorInput.resetPeekPosition(); - Pair extractorData = + HlsExtractorFactory.Result result = extractorFactory.createExtractor( previousExtractor, dataSpec.uri, @@ -390,21 +398,16 @@ import java.util.concurrent.atomic.AtomicInteger; timestampAdjuster, dataSource.getResponseHeaders(), extractorInput); - extractor = extractorData.first; - boolean reusingExtractor = extractor == previousExtractor; - boolean isPackedAudioExtractor = extractorData.second; - if (isPackedAudioExtractor) { + extractor = result.extractor; + isExtractorReusable = result.isReusable; + if (result.isPackedAudioExtractor) { output.setSampleOffsetUs( id3Timestamp != C.TIME_UNSET ? timestampAdjuster.adjustTsTimestamp(id3Timestamp) : startTimeUs); } - initLoadCompleted = reusingExtractor && initDataSpec != null; - - output.init(uid, shouldSpliceIn, reusingExtractor); - if (!reusingExtractor) { - extractor.init(output); - } + output.init(uid, shouldSpliceIn, /* reusingExtractor= */ false); + extractor.init(output); } return extractorInput;