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 a1251810ad6..4c7b5629ba2 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 @@ -125,9 +125,13 @@ public BundledHlsMediaChunkExtractor createExtractor( if (sniffQuietly(extractor, extractorInput)) { return new BundledHlsMediaChunkExtractor(extractor, format, timestampAdjuster); } - if (fileType == FileTypes.TS) { - // Fall back on TsExtractor to handle TS streams with an EXT-X-MAP tag. See - // https://github.com/google/ExoPlayer/issues/8219. + if (fallBackExtractor == null + && (fileType == formatInferredFileType + || fileType == responseHeadersInferredFileType + || fileType == uriInferredFileType + || fileType == FileTypes.TS)) { + // If sniffing fails, fallback to the file types inferred from context. If all else fails, + // fallback to Transport Stream. See https://github.com/google/ExoPlayer/issues/8219. fallBackExtractor = extractor; } } 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 d51a800b888..2bbc5bb0f9a 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 @@ -24,12 +24,14 @@ import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; 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; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TimestampAdjuster; +import com.google.common.collect.ImmutableMap; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -42,14 +44,15 @@ @RunWith(AndroidJUnit4.class) public class DefaultHlsExtractorFactoryTest { - private Uri tsUri; + private static final Uri URI_WITH_TS_EXTENSION = Uri.parse("http://path/filename.ts"); + private static final Uri URI_WITH_MP4_EXTENSION = Uri.parse("http://path/filename.mp4"); + private Format webVttFormat; private TimestampAdjuster timestampAdjuster; private Map> ac3ResponseHeaders; @Before public void setUp() { - tsUri = Uri.parse("http://path/filename.ts"); webVttFormat = new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build(); timestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); ac3ResponseHeaders = new HashMap<>(); @@ -69,7 +72,7 @@ public void createExtractor_withFileTypeInFormat_returnsExtractorMatchingFormat( BundledHlsMediaChunkExtractor result = new DefaultHlsExtractorFactory() .createExtractor( - tsUri, + URI_WITH_TS_EXTENSION, webVttFormat, /* muxedCaptionFormats= */ null, timestampAdjuster, @@ -93,7 +96,7 @@ public void createExtractor_withFileTypeInFormat_returnsExtractorMatchingFormat( BundledHlsMediaChunkExtractor result = new DefaultHlsExtractorFactory() .createExtractor( - tsUri, + URI_WITH_TS_EXTENSION, webVttFormat, /* muxedCaptionFormats= */ null, timestampAdjuster, @@ -115,7 +118,7 @@ public void createExtractor_withFileTypeInUri_returnsExtractorMatchingUri() thro BundledHlsMediaChunkExtractor result = new DefaultHlsExtractorFactory() .createExtractor( - tsUri, + URI_WITH_TS_EXTENSION, webVttFormat, /* muxedCaptionFormats= */ null, timestampAdjuster, @@ -138,7 +141,7 @@ public void createExtractor_withFileTypeNotInMediaInfo_returnsExpectedExtractor( BundledHlsMediaChunkExtractor result = new DefaultHlsExtractorFactory() .createExtractor( - tsUri, + URI_WITH_TS_EXTENSION, webVttFormat, /* muxedCaptionFormats= */ null, timestampAdjuster, @@ -149,19 +152,75 @@ public void createExtractor_withFileTypeNotInMediaInfo_returnsExpectedExtractor( } @Test - public void createExtractor_withNoMatchingExtractor_fallsBackOnTsExtractor() throws Exception { + public void createExtractor_onFailedSniff_fallsBackOnFormatInferred() throws Exception { ExtractorInput emptyExtractorInput = new FakeExtractorInput.Builder().build(); BundledHlsMediaChunkExtractor result = new DefaultHlsExtractorFactory() .createExtractor( - tsUri, + URI_WITH_MP4_EXTENSION, webVttFormat, /* muxedCaptionFormats= */ null, timestampAdjuster, ac3ResponseHeaders, emptyExtractorInput); + // The format indicates WebVTT so we expect a WebVTT extractor. + assertThat(result.extractor.getClass()).isEqualTo(WebvttExtractor.class); + } + + @Test + public void createExtractor_onFailedSniff_fallsBackOnHttpContentType() throws Exception { + ExtractorInput emptyExtractorInput = new FakeExtractorInput.Builder().build(); + + BundledHlsMediaChunkExtractor result = + new DefaultHlsExtractorFactory() + .createExtractor( + URI_WITH_MP4_EXTENSION, + new Format.Builder().build(), + /* muxedCaptionFormats= */ null, + timestampAdjuster, + ac3ResponseHeaders, + emptyExtractorInput); + + // No format info, so we expect an AC-3 Extractor, as per HTTP Content-Type header. + assertThat(result.extractor.getClass()).isEqualTo(Ac3Extractor.class); + } + + @Test + public void createExtractor_onFailedSniff_fallsBackOnFileExtension() throws Exception { + ExtractorInput emptyExtractorInput = new FakeExtractorInput.Builder().build(); + + BundledHlsMediaChunkExtractor result = + new DefaultHlsExtractorFactory() + .createExtractor( + URI_WITH_MP4_EXTENSION, + new Format.Builder().build(), + /* muxedCaptionFormats= */ null, + timestampAdjuster, + /* responseHeaders= */ ImmutableMap.of(), + emptyExtractorInput); + + // No format info, and no HTTP headers, so we expect an fMP4 extractor, as per file extension. + assertThat(result.extractor.getClass()).isEqualTo(FragmentedMp4Extractor.class); + } + + @Test + public void createExtractor_onFailedSniff_fallsBackOnTsExtractor() throws Exception { + ExtractorInput emptyExtractorInput = new FakeExtractorInput.Builder().build(); + + BundledHlsMediaChunkExtractor result = + new DefaultHlsExtractorFactory() + .createExtractor( + Uri.parse("http://path/no_extension"), + new Format.Builder().build(), + /* muxedCaptionFormats= */ null, + timestampAdjuster, + /* responseHeaders= */ ImmutableMap.of(), + emptyExtractorInput); + + // There's no information for inferring the file type, we expect the factory to fall back on + // Transport Stream. assertThat(result.extractor.getClass()).isEqualTo(TsExtractor.class); } }