diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d654ab9522..0f9a4b855d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -156,12 +156,17 @@ * HLS: * Add support for upstream discard including cancelation of ongoing load ([#6322](https://github.com/google/ExoPlayer/issues/6322)). -* MP3: - * Add `IndexSeeker` for accurate seeks in VBR streams +* Extractors: + * Add `IndexSeeker` for accurate seeks in VBR MP3 streams ([#6787](https://github.com/google/ExoPlayer/issues/6787)). This seeker is enabled by passing `FLAG_ENABLE_INDEX_SEEKING` to the `Mp3Extractor`. It may require to scan a significant portion of the file for seeking, which may be costly on large files. + * Change the order of extractors for sniffing to reduce start-up latency + in `DefaultExtractorsFactory` and `DefaultHlsExtractorsFactory` + ([#6410](https://github.com/google/ExoPlayer/issues/6410)). + * Select first extractors based on the filename extension in + `DefaultExtractorsFactory`. * Testing * Add `TestExoPlayer`, a utility class with APIs to create `SimpleExoPlayer` instances with fake components for testing. @@ -179,9 +184,6 @@ * Cast extension: Implement playlist API and deprecate the old queue manipulation API. * Demo app: Retain previous position in list of samples. -* Change the order of extractors for sniffing to reduce start-up latency in - `DefaultExtractorsFactory` and `DefaultHlsExtractorsFactory` - ([#6410](https://github.com/google/ExoPlayer/issues/6410)). * Add Guava dependency. ### 2.11.5 (2020-06-03) ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index 6c1d26cb07..fbb657a4e4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -276,7 +276,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource return new ProgressiveMediaPeriod( playbackProperties.uri, dataSource, - extractorsFactory.createExtractors(), + extractorsFactory.createExtractors(playbackProperties.uri), drmSessionManager, loadableLoadErrorHandlingPolicy, createEventDispatcher(id), diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 9306a146d5..6f620c319e 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -15,6 +15,23 @@ */ package com.google.android.exoplayer2.extractor; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_AC3; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_AC4; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_ADTS; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_AMR; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_FLAC; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_FLV; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_MATROSKA; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_MP3; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_MP4; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_OGG; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_PS; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_TS; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_UNKNOWN; +import static com.google.android.exoplayer2.util.FilenameUtil.FILE_FORMAT_WAV; +import static com.google.android.exoplayer2.util.FilenameUtil.getFormatFromExtension; + +import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.extractor.amr.AmrExtractor; import com.google.android.exoplayer2.extractor.flac.FlacExtractor; @@ -32,8 +49,11 @@ import com.google.android.exoplayer2.extractor.ts.PsExtractor; import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader; import com.google.android.exoplayer2.extractor.wav.WavExtractor; +import com.google.android.exoplayer2.util.FilenameUtil; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; /** * An {@link ExtractorsFactory} that provides an array of extractors for the following formats: @@ -63,6 +83,25 @@ import java.lang.reflect.Constructor; */ public final class DefaultExtractorsFactory implements ExtractorsFactory { + // Extractors order is optimized according to + // https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ. + private static final int[] DEFAULT_EXTRACTOR_ORDER = + new int[] { + FILE_FORMAT_FLV, + FILE_FORMAT_FLAC, + FILE_FORMAT_WAV, + FILE_FORMAT_MP4, + FILE_FORMAT_AMR, + FILE_FORMAT_PS, + FILE_FORMAT_OGG, + FILE_FORMAT_TS, + FILE_FORMAT_MATROSKA, + FILE_FORMAT_ADTS, + FILE_FORMAT_AC3, + FILE_FORMAT_AC4, + FILE_FORMAT_MP3, + }; + @Nullable private static final Constructor FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR; @@ -240,48 +279,96 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @Override public synchronized Extractor[] createExtractors() { - Extractor[] extractors = new Extractor[14]; - // Extractors order is optimized according to - // https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ. - extractors[0] = new FlvExtractor(); - if (FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR != null) { - try { - extractors[1] = FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance(); - } catch (Exception e) { - // Should never happen. - throw new IllegalStateException("Unexpected error creating FLAC extractor", e); - } - } else { - extractors[1] = new FlacExtractor(coreFlacFlags); - } - extractors[2] = new WavExtractor(); - extractors[3] = new FragmentedMp4Extractor(fragmentedMp4Flags); - extractors[4] = new Mp4Extractor(mp4Flags); - extractors[5] = - new AmrExtractor( - amrFlags - | (constantBitrateSeekingEnabled - ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING - : 0)); - extractors[6] = new PsExtractor(); - extractors[7] = new OggExtractor(); - extractors[8] = new TsExtractor(tsMode, tsFlags); - extractors[9] = new MatroskaExtractor(matroskaFlags); - extractors[10] = - new AdtsExtractor( - adtsFlags - | (constantBitrateSeekingEnabled - ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING - : 0)); - extractors[11] = new Ac3Extractor(); - extractors[12] = new Ac4Extractor(); - extractors[13] = - new Mp3Extractor( - mp3Flags - | (constantBitrateSeekingEnabled - ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING - : 0)); - return extractors; + return createExtractors(Uri.EMPTY); } + @Override + public synchronized Extractor[] createExtractors(Uri uri) { + List extractors = new ArrayList<>(/* initialCapacity= */ 14); + + String filename = uri.getLastPathSegment(); + @FilenameUtil.FileFormat + int extensionFormat = filename == null ? FILE_FORMAT_UNKNOWN : getFormatFromExtension(filename); + addExtractorsForFormat(extensionFormat, extractors); + + for (int format : DEFAULT_EXTRACTOR_ORDER) { + if (format != extensionFormat) { + addExtractorsForFormat(format, extractors); + } + } + + return extractors.toArray(new Extractor[extractors.size()]); + } + + private void addExtractorsForFormat( + @FilenameUtil.FileFormat int fileFormat, List extractors) { + switch (fileFormat) { + case FILE_FORMAT_AC3: + extractors.add(new Ac3Extractor()); + break; + case FILE_FORMAT_AC4: + extractors.add(new Ac4Extractor()); + break; + case FILE_FORMAT_ADTS: + extractors.add( + new AdtsExtractor( + adtsFlags + | (constantBitrateSeekingEnabled + ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0))); + break; + case FILE_FORMAT_AMR: + extractors.add( + new AmrExtractor( + amrFlags + | (constantBitrateSeekingEnabled + ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0))); + break; + case FILE_FORMAT_FLAC: + if (FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR != null) { + try { + extractors.add(FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance()); + } catch (Exception e) { + // Should never happen. + throw new IllegalStateException("Unexpected error creating FLAC extractor", e); + } + } else { + extractors.add(new FlacExtractor(coreFlacFlags)); + } + break; + case FILE_FORMAT_FLV: + extractors.add(new FlvExtractor()); + break; + case FILE_FORMAT_MATROSKA: + extractors.add(new MatroskaExtractor(matroskaFlags)); + break; + case FILE_FORMAT_MP3: + extractors.add( + new Mp3Extractor( + mp3Flags + | (constantBitrateSeekingEnabled + ? Mp3Extractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING + : 0))); + break; + case FILE_FORMAT_MP4: + extractors.add(new FragmentedMp4Extractor(fragmentedMp4Flags)); + extractors.add(new Mp4Extractor(mp4Flags)); + break; + case FILE_FORMAT_OGG: + extractors.add(new OggExtractor()); + break; + case FILE_FORMAT_PS: + extractors.add(new PsExtractor()); + break; + case FILE_FORMAT_TS: + extractors.add(new TsExtractor(tsMode, tsFlags)); + break; + case FILE_FORMAT_WAV: + extractors.add(new WavExtractor()); + break; + default: + break; + } + } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java index ee29f376a1..bbd43d8c5a 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java @@ -15,9 +15,19 @@ */ package com.google.android.exoplayer2.extractor; +import android.net.Uri; + /** Factory for arrays of {@link Extractor} instances. */ public interface ExtractorsFactory { /** Returns an array of new {@link Extractor} instances. */ Extractor[] createExtractors(); + + /** + * Returns an array of new {@link Extractor} instances to extract the stream corresponding to the + * provided {@link Uri}. + */ + default Extractor[] createExtractors(Uri uri) { + return createExtractors(); + } } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java index b24c76d262..030c45e5b1 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactoryTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor; import static com.google.common.truth.Truth.assertThat; +import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.extractor.amr.AmrExtractor; import com.google.android.exoplayer2.extractor.flac.FlacExtractor; @@ -42,34 +43,71 @@ import org.junit.runner.RunWith; public final class DefaultExtractorsFactoryTest { @Test - public void createExtractors_returnExpectedClasses() { + public void createExtractors_withoutUri_optimizesSniffingOrder() { DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); Extractor[] extractors = defaultExtractorsFactory.createExtractors(); - List> listCreatedExtractorClasses = new ArrayList<>(); + + List> extractorClasses = getExtractorClasses(extractors); + assertThat(extractorClasses.subList(0, 3)) + .containsExactly(FlvExtractor.class, FlacExtractor.class, WavExtractor.class) + .inOrder(); + assertThat(extractorClasses.subList(3, 5)) + .containsExactly(Mp4Extractor.class, FragmentedMp4Extractor.class); + assertThat(extractorClasses.subList(5, extractors.length)) + .containsExactly( + AmrExtractor.class, + PsExtractor.class, + OggExtractor.class, + TsExtractor.class, + MatroskaExtractor.class, + AdtsExtractor.class, + Ac3Extractor.class, + Ac4Extractor.class, + Mp3Extractor.class) + .inOrder(); + } + + @Test + public void createExtractors_withUri_startsWithExtractorsMatchingExtension() { + DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); + + Extractor[] extractors = defaultExtractorsFactory.createExtractors(Uri.parse("test.mp4")); + + List> extractorClasses = getExtractorClasses(extractors); + assertThat(extractorClasses.subList(0, 2)) + .containsExactly(Mp4Extractor.class, FragmentedMp4Extractor.class); + } + + @Test + public void createExtractors_withUri_optimizesSniffingOrder() { + DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); + + Extractor[] extractors = defaultExtractorsFactory.createExtractors(Uri.parse("test.mp4")); + + List> extractorClasses = getExtractorClasses(extractors); + assertThat(extractorClasses.subList(2, extractors.length)) + .containsExactly( + FlvExtractor.class, + FlacExtractor.class, + WavExtractor.class, + AmrExtractor.class, + PsExtractor.class, + OggExtractor.class, + TsExtractor.class, + MatroskaExtractor.class, + AdtsExtractor.class, + Ac3Extractor.class, + Ac4Extractor.class, + Mp3Extractor.class) + .inOrder(); + } + + private static List> getExtractorClasses(Extractor[] extractors) { + List> extractorClasses = new ArrayList<>(); for (Extractor extractor : extractors) { - listCreatedExtractorClasses.add(extractor.getClass()); + extractorClasses.add(extractor.getClass()); } - - Class[] expectedExtractorClassses = - new Class[] { - MatroskaExtractor.class, - FragmentedMp4Extractor.class, - Mp4Extractor.class, - Mp3Extractor.class, - AdtsExtractor.class, - Ac3Extractor.class, - TsExtractor.class, - FlvExtractor.class, - OggExtractor.class, - PsExtractor.class, - WavExtractor.class, - AmrExtractor.class, - Ac4Extractor.class, - FlacExtractor.class - }; - - assertThat(listCreatedExtractorClasses).containsNoDuplicates(); - assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses); + return extractorClasses; } }