diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/BundledHlsMediaChunkExtractor.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/BundledHlsMediaChunkExtractor.java new file mode 100644 index 0000000000..4fd77135ab --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/BundledHlsMediaChunkExtractor.java @@ -0,0 +1,101 @@ +/* + * Copyright 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.VisibleForTesting; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import com.google.android.exoplayer2.extractor.ts.Ac4Extractor; +import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; +import com.google.android.exoplayer2.extractor.ts.TsExtractor; +import com.google.android.exoplayer2.util.TimestampAdjuster; +import java.io.IOException; + +/** + * {@link HlsMediaChunkExtractor} implementation that uses ExoPlayer app-bundled {@link Extractor + * Extractors}. + */ +public final class BundledHlsMediaChunkExtractor implements HlsMediaChunkExtractor { + + private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder(); + + @VisibleForTesting /* package */ final Extractor extractor; + private final Format masterPlaylistFormat; + private final TimestampAdjuster timestampAdjuster; + + /** + * Creates a new instance. + * + * @param extractor The underlying {@link Extractor}. + * @param masterPlaylistFormat The {@link Format} obtained from the master playlist. + * @param timestampAdjuster A {@link TimestampAdjuster} to adjust sample timestamps. + */ + public BundledHlsMediaChunkExtractor( + Extractor extractor, Format masterPlaylistFormat, TimestampAdjuster timestampAdjuster) { + this.extractor = extractor; + this.masterPlaylistFormat = masterPlaylistFormat; + this.timestampAdjuster = timestampAdjuster; + } + + @Override + public void init(ExtractorOutput extractorOutput) { + extractor.init(extractorOutput); + } + + @Override + public boolean read(ExtractorInput extractorInput) throws IOException { + return extractor.read(extractorInput, DUMMY_POSITION_HOLDER) == Extractor.RESULT_CONTINUE; + } + + @Override + public boolean isPackedAudioExtractor() { + return extractor instanceof AdtsExtractor + || extractor instanceof Ac3Extractor + || extractor instanceof Ac4Extractor + || extractor instanceof Mp3Extractor; + } + + @Override + public HlsMediaChunkExtractor reuseOrRecreate() { + if (extractor instanceof TsExtractor || extractor instanceof FragmentedMp4Extractor) { + // We can reuse this instance. + return this; + } + Extractor newExtractorInstance; + if (extractor instanceof WebvttExtractor) { + newExtractorInstance = new WebvttExtractor(masterPlaylistFormat.language, timestampAdjuster); + } else if (extractor instanceof AdtsExtractor) { + newExtractorInstance = new AdtsExtractor(); + } else if (extractor instanceof Ac3Extractor) { + newExtractorInstance = new Ac3Extractor(); + } else if (extractor instanceof Ac4Extractor) { + newExtractorInstance = new Ac4Extractor(); + } else if (extractor instanceof Mp3Extractor) { + newExtractorInstance = new Mp3Extractor(); + } else { + throw new IllegalStateException( + "Unexpected previousExtractor type: " + extractor.getClass().getSimpleName()); + } + return new BundledHlsMediaChunkExtractor( + newExtractorInstance, masterPlaylistFormat, timestampAdjuster); + } +} 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 3176551a45..0fe89d4a4e 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 @@ -88,8 +88,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { } @Override - public Result createExtractor( - @Nullable Extractor previousExtractor, + public BundledHlsMediaChunkExtractor createExtractor( + @Nullable HlsMediaChunkExtractor previousExtractor, Uri uri, Format format, @Nullable List muxedCaptionFormats, @@ -97,22 +97,6 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { Map> responseHeaders, ExtractorInput extractorInput) throws IOException { - - if (previousExtractor != null) { - // An extractor has already been successfully used. Return one of the same type. - if (isReusable(previousExtractor)) { - return buildResult(previousExtractor); - } else { - @Nullable - Result result = - buildResultForSameExtractorType(previousExtractor, format, timestampAdjuster); - if (result == null) { - throw new IllegalArgumentException( - "Unexpected previousExtractor type: " + previousExtractor.getClass().getSimpleName()); - } - } - } - @FileTypes.Type int formatInferredFileType = FileTypes.inferFileTypeFromMimeType(format.sampleMimeType); @FileTypes.Type @@ -139,14 +123,15 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { checkNotNull( createExtractorByFileType(fileType, format, muxedCaptionFormats, timestampAdjuster)); if (sniffQuietly(extractor, extractorInput)) { - return buildResult(extractor); + return new BundledHlsMediaChunkExtractor(extractor, format, timestampAdjuster); } if (fileType == FileTypes.TS) { fallBackExtractor = extractor; } } - return buildResult(checkNotNull(fallBackExtractor)); + return new BundledHlsMediaChunkExtractor( + checkNotNull(fallBackExtractor), format, timestampAdjuster); } private static void addFileTypeIfNotPresent( @@ -257,34 +242,6 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { return false; } - @Nullable - 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 Ac4Extractor) { - return buildResult(new Ac4Extractor()); - } 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 Ac4Extractor - || extractor instanceof Mp3Extractor, - isReusable(extractor)); - } - private static boolean sniffQuietly(Extractor extractor, ExtractorInput input) throws IOException { boolean result = false; @@ -297,9 +254,4 @@ 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 eb3cf8bfcf..de2ecd73b4 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 @@ -31,33 +31,6 @@ 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(); /** @@ -76,11 +49,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 {@link Result}. + * @return An {@link HlsMediaChunkExtractor}. * @throws IOException If an I/O error is encountered while sniffing. */ - Result createExtractor( - @Nullable Extractor previousExtractor, + HlsMediaChunkExtractor createExtractor( + @Nullable HlsMediaChunkExtractor previousExtractor, Uri uri, Format format, @Nullable List muxedCaptionFormats, 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 4e87e717bf..4c3b7655a7 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 @@ -21,9 +21,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; -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; @@ -56,8 +54,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Creates a new instance. * - * @param extractorFactory A {@link HlsExtractorFactory} from which the HLS media chunk extractor - * is obtained. + * @param extractorFactory A {@link HlsExtractorFactory} from which the {@link + * HlsMediaChunkExtractor} is obtained. * @param dataSource The source from which the data should be loaded. * @param format The chunk format. * @param startOfPlaylistInPeriodUs The position of the playlist in the period in microseconds. @@ -130,7 +128,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; int discontinuitySequenceNumber = mediaPlaylist.discontinuitySequence + mediaSegment.relativeDiscontinuitySequence; - @Nullable Extractor previousExtractor = null; + @Nullable HlsMediaChunkExtractor previousExtractor = null; Id3Decoder id3Decoder; ParsableByteArray scratchId3Data; boolean shouldSpliceIn; @@ -147,8 +145,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; sampleQueueDiscardFromIndices = previousChunk.sampleQueueDiscardFromIndices; } previousExtractor = - previousChunk.isExtractorReusable - && previousChunk.discontinuitySequenceNumber == discontinuitySequenceNumber + previousChunk.discontinuitySequenceNumber == discontinuitySequenceNumber && !shouldSpliceIn ? previousChunk.extractor : null; @@ -188,7 +185,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; public static final String PRIV_TIMESTAMP_FRAME_OWNER = "com.apple.streaming.transportStreamTimestamp"; - private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder(); private static final AtomicInteger uidSource = new AtomicInteger(); @@ -207,7 +203,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Nullable private final DataSource initDataSource; @Nullable private final DataSpec initDataSpec; - @Nullable private final Extractor previousExtractor; + @Nullable private final HlsMediaChunkExtractor previousExtractor; private final boolean isMasterTimestampSource; private final boolean hasGapTag; @@ -221,8 +217,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final boolean initSegmentEncrypted; private final boolean shouldSpliceIn; - private @MonotonicNonNull Extractor extractor; - private boolean isExtractorReusable; + private @MonotonicNonNull HlsMediaChunkExtractor extractor; private @MonotonicNonNull HlsSampleStreamWrapper output; // nextLoadPosition refers to the init segment if initDataLoadRequired is true. // Otherwise, nextLoadPosition refers to the media segment. @@ -253,7 +248,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, @Nullable DrmInitData drmInitData, - @Nullable Extractor previousExtractor, + @Nullable HlsMediaChunkExtractor previousExtractor, Id3Decoder id3Decoder, ParsableByteArray scratchId3Data, boolean shouldSpliceIn, @@ -340,9 +335,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // output == null means init() hasn't been called. Assertions.checkNotNull(output); if (extractor == null && previousExtractor != null) { - extractor = previousExtractor; - isExtractorReusable = true; - initDataLoadRequired = false; + extractor = previousExtractor.reuseOrRecreate(); + initDataLoadRequired = extractor != previousExtractor; } maybeLoadInitData(); if (!loadCanceled) { @@ -410,10 +404,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; input.skipFully(nextLoadPosition); } try { - int result = Extractor.RESULT_CONTINUE; - while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractor.read(input, DUMMY_POSITION_HOLDER); - } + while (!loadCanceled && extractor.read(input)) {} } finally { nextLoadPosition = (int) (input.getPosition() - dataSpec.position); } @@ -434,7 +425,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; long id3Timestamp = peekId3PrivTimestamp(extractorInput); extractorInput.resetPeekPosition(); - HlsExtractorFactory.Result result = + extractor = extractorFactory.createExtractor( previousExtractor, dataSpec.uri, @@ -443,9 +434,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; timestampAdjuster, dataSource.getResponseHeaders(), extractorInput); - extractor = result.extractor; - isExtractorReusable = result.isReusable; - if (result.isPackedAudioExtractor) { + if (extractor.isPackedAudioExtractor()) { output.setSampleOffsetUs( id3Timestamp != C.TIME_UNSET ? timestampAdjuster.adjustTsTimestamp(id3Timestamp) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunkExtractor.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunkExtractor.java new file mode 100644 index 0000000000..55f69b7e6c --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunkExtractor.java @@ -0,0 +1,59 @@ +/* + * Copyright 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 com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import java.io.IOException; + +/** Extracts samples and track {@link Format Formats} from {@link HlsMediaChunk HlsMediaChunks}. */ +public interface HlsMediaChunkExtractor { + + /** + * Initializes the extractor with an {@link ExtractorOutput}. Called at most once. + * + * @param extractorOutput An {@link ExtractorOutput} to receive extracted data. + */ + void init(ExtractorOutput extractorOutput); + + /** + * Extracts data read from a provided {@link ExtractorInput}. Must not be called before {@link + * #init(ExtractorOutput)}. + * + *

A single call to this method will block until some progress has been made, but will not + * block for longer than this. Hence each call will consume only a small amount of input data. + * + *

When this method throws an {@link IOException}, extraction may continue by providing an + * {@link ExtractorInput} with an unchanged {@link ExtractorInput#getPosition() read position} to + * a subsequent call to this method. + * + * @param extractorInput The input to read from. + * @return Whether there is any data left to extract. Returns false if the end of input has been + * reached. + * @throws IOException If an error occurred reading from or parsing the input. + */ + boolean read(ExtractorInput extractorInput) throws IOException; + + /** Returns whether this is a packed audio extractor, as defined in RFC 8216, Section 3.4. */ + boolean isPackedAudioExtractor(); + + /** + * If this instance can be used for extracting multiple continuous segments, returns itself. + * Otherwise, returns a new instance for extracting the same type of media. + */ + HlsMediaChunkExtractor reuseOrRecreate(); +} diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactoryTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactoryTest.java index d5f33424ab..b3cd7caac4 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactoryTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactoryTest.java @@ -22,10 +22,8 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; -import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.testutil.FakeExtractorInput; @@ -44,7 +42,6 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class DefaultHlsExtractorFactoryTest { - private Extractor fMp4Extractor; private Uri tsUri; private Format webVttFormat; private TimestampAdjuster timestampAdjuster; @@ -52,7 +49,6 @@ public class DefaultHlsExtractorFactoryTest { @Before public void setUp() { - fMp4Extractor = new FragmentedMp4Extractor(); tsUri = Uri.parse("http://path/filename.ts"); webVttFormat = new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build(); timestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); @@ -60,24 +56,6 @@ public class DefaultHlsExtractorFactoryTest { ac3ResponseHeaders.put("Content-Type", Collections.singletonList(MimeTypes.AUDIO_AC3)); } - @Test - public void createExtractor_withPreviousExtractor_returnsSameExtractorType() throws Exception { - ExtractorInput extractorInput = new FakeExtractorInput.Builder().build(); - - HlsExtractorFactory.Result result = - new DefaultHlsExtractorFactory() - .createExtractor( - /* previousExtractor= */ fMp4Extractor, - tsUri, - webVttFormat, - /* muxedCaptionFormats= */ null, - timestampAdjuster, - ac3ResponseHeaders, - extractorInput); - - assertThat(result.extractor.getClass()).isEqualTo(FragmentedMp4Extractor.class); - } - @Test public void createExtractor_withFileTypeInFormat_returnsExtractorMatchingFormat() throws Exception { @@ -88,7 +66,7 @@ public class DefaultHlsExtractorFactoryTest { ApplicationProvider.getApplicationContext(), "webvtt/typical")) .build(); - HlsExtractorFactory.Result result = + BundledHlsMediaChunkExtractor result = new DefaultHlsExtractorFactory() .createExtractor( /* previousExtractor= */ null, @@ -112,7 +90,7 @@ public class DefaultHlsExtractorFactoryTest { TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), "ts/sample.ac3")) .build(); - HlsExtractorFactory.Result result = + BundledHlsMediaChunkExtractor result = new DefaultHlsExtractorFactory() .createExtractor( /* previousExtractor= */ null, @@ -135,7 +113,7 @@ public class DefaultHlsExtractorFactoryTest { ApplicationProvider.getApplicationContext(), "ts/sample_ac3.ts")) .build(); - HlsExtractorFactory.Result result = + BundledHlsMediaChunkExtractor result = new DefaultHlsExtractorFactory() .createExtractor( /* previousExtractor= */ null, @@ -159,7 +137,7 @@ public class DefaultHlsExtractorFactoryTest { ApplicationProvider.getApplicationContext(), "mp3/bear-id3.mp3")) .build(); - HlsExtractorFactory.Result result = + BundledHlsMediaChunkExtractor result = new DefaultHlsExtractorFactory() .createExtractor( /* previousExtractor= */ null, @@ -177,7 +155,7 @@ public class DefaultHlsExtractorFactoryTest { public void createExtractor_withNoMatchingExtractor_fallsBackOnTsExtractor() throws Exception { ExtractorInput emptyExtractorInput = new FakeExtractorInput.Builder().build(); - HlsExtractorFactory.Result result = + BundledHlsMediaChunkExtractor result = new DefaultHlsExtractorFactory() .createExtractor( /* previousExtractor= */ null,