From b1e56304a1fda8075fc637074927c0886f49fdf1 Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 8 Jun 2020 16:55:41 +0100 Subject: [PATCH] Add support for inferring file format from MIME type PiperOrigin-RevId: 315283926 --- .../android/exoplayer2/util/FileTypes.java | 75 ++++++++++++++++++- .../android/exoplayer2/util/MimeTypes.java | 23 ++++++ .../google/android/exoplayer2/util/Util.java | 1 + .../exoplayer2/util/FileTypesTest.java | 62 ++++++++++++--- .../extractor/DefaultExtractorsFactory.java | 8 +- .../hls/DefaultHlsExtractorFactory.java | 44 ++++++----- 6 files changed, 174 insertions(+), 39 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/FileTypes.java b/library/common/src/main/java/com/google/android/exoplayer2/util/FileTypes.java index 62fdb48e01..d4b87abfdd 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/FileTypes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/FileTypes.java @@ -15,11 +15,17 @@ */ package com.google.android.exoplayer2.util; +import static com.google.android.exoplayer2.util.MimeTypes.normalizeMimeType; + import android.net.Uri; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Map; /** Defines common file type constants and helper methods. */ public final class FileTypes { @@ -64,6 +70,8 @@ public final class FileTypes { /** File type for the WebVTT format. */ public static final int WEBVTT = 13; + @VisibleForTesting /* package */ static final String HEADER_CONTENT_TYPE = "Content-Type"; + private static final String EXTENSION_AC3 = ".ac3"; private static final String EXTENSION_EC3 = ".ec3"; private static final String EXTENSION_AC4 = ".ac4"; @@ -94,13 +102,72 @@ public final class FileTypes { private FileTypes() {} + /** Returns the {@link Type} corresponding to the response headers provided. */ + @FileTypes.Type + public static int inferFileTypeFromResponseHeaders(Map> responseHeaders) { + @Nullable List contentTypes = responseHeaders.get(HEADER_CONTENT_TYPE); + @Nullable + String mimeType = contentTypes == null || contentTypes.isEmpty() ? null : contentTypes.get(0); + return inferFileTypeFromMimeType(mimeType); + } + /** - * Returns the {@link Type} corresponding to the filename extension of the provided {@link Uri}. - * The filename is considered to be the last segment of the {@link Uri} path. + * Returns the {@link Type} corresponding to the MIME type provided. + * + *

Returns {@link #UNKNOWN} if the mime type is {@code null}. */ @FileTypes.Type - public static int getFormatFromExtension(Uri uri) { - String filename = uri.getLastPathSegment(); + public static int inferFileTypeFromMimeType(@Nullable String mimeType) { + if (mimeType == null) { + return FileTypes.UNKNOWN; + } + mimeType = normalizeMimeType(mimeType); + switch (mimeType) { + case MimeTypes.AUDIO_AC3: + case MimeTypes.AUDIO_E_AC3: + case MimeTypes.AUDIO_E_AC3_JOC: + return FileTypes.AC3; + case MimeTypes.AUDIO_AC4: + return FileTypes.AC4; + case MimeTypes.AUDIO_AMR: + case MimeTypes.AUDIO_AMR_NB: + case MimeTypes.AUDIO_AMR_WB: + return FileTypes.AMR; + case MimeTypes.AUDIO_FLAC: + return FileTypes.FLAC; + case MimeTypes.VIDEO_FLV: + return FileTypes.FLV; + case MimeTypes.VIDEO_MATROSKA: + case MimeTypes.AUDIO_MATROSKA: + case MimeTypes.VIDEO_WEBM: + case MimeTypes.AUDIO_WEBM: + case MimeTypes.APPLICATION_WEBM: + return FileTypes.MATROSKA; + case MimeTypes.AUDIO_MPEG: + return FileTypes.MP3; + case MimeTypes.VIDEO_MP4: + case MimeTypes.AUDIO_MP4: + case MimeTypes.APPLICATION_MP4: + return FileTypes.MP4; + case MimeTypes.AUDIO_OGG: + return FileTypes.OGG; + case MimeTypes.VIDEO_PS: + return FileTypes.PS; + case MimeTypes.VIDEO_MP2T: + return FileTypes.TS; + case MimeTypes.AUDIO_WAV: + return FileTypes.WAV; + case MimeTypes.TEXT_VTT: + return FileTypes.WEBVTT; + default: + return FileTypes.UNKNOWN; + } + } + + /** Returns the {@link Type} corresponding to the {@link Uri} provided. */ + @FileTypes.Type + public static int inferFileTypeFromUri(Uri uri) { + @Nullable String filename = uri.getLastPathSegment(); if (filename == null) { return FileTypes.UNKNOWN; } else if (filename.endsWith(EXTENSION_AC3) || filename.endsWith(EXTENSION_EC3)) { diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index e2055a24f0..a3bc395574 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -47,6 +47,7 @@ public final class MimeTypes { public static final String BASE_TYPE_APPLICATION = "application"; public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4"; + public static final String VIDEO_MATROSKA = BASE_TYPE_VIDEO + "/x-matroska"; public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm"; public static final String VIDEO_H263 = BASE_TYPE_VIDEO + "/3gpp"; public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc"; @@ -67,6 +68,7 @@ public final class MimeTypes { public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4"; public static final String AUDIO_AAC = BASE_TYPE_AUDIO + "/mp4a-latm"; + public static final String AUDIO_MATROSKA = BASE_TYPE_AUDIO + "/x-matroska"; public static final String AUDIO_WEBM = BASE_TYPE_AUDIO + "/webm"; public static final String AUDIO_MPEG = BASE_TYPE_AUDIO + "/mpeg"; public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1"; @@ -91,6 +93,7 @@ public final class MimeTypes { public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm"; public static final String AUDIO_OGG = BASE_TYPE_AUDIO + "/ogg"; + public static final String AUDIO_WAV = BASE_TYPE_AUDIO + "/wav"; public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; @@ -502,6 +505,26 @@ public final class MimeTypes { return new Mp4aObjectType(objectTypeIndication, audioObjectTypeIndication); } + /** + * Normalizes the MIME type provided so that equivalent MIME types are uniquely represented. + * + * @param mimeType The MIME type to normalize. The MIME type provided is returned if its + * normalized form is unknown. + * @return The normalized MIME type. + */ + public static String normalizeMimeType(String mimeType) { + switch (mimeType) { + case BASE_TYPE_AUDIO + "/x-flac": + return AUDIO_FLAC; + case BASE_TYPE_AUDIO + "/mp3": + return AUDIO_MPEG; + case BASE_TYPE_AUDIO + "/x-wav": + return AUDIO_WAV; + default: + return mimeType; + } + } + /** * Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not * contain a forward slash character ({@code '/'}). diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java index 888f0afa16..09303c4a9c 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -1676,6 +1676,7 @@ public final class Util { * @param mimeType If not null, used to infer the type. * @return The content type. */ + @C.ContentType public static int inferContentTypeWithMimeType(Uri uri, @Nullable String mimeType) { if (mimeType == null) { return Util.inferContentType(uri); diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/FileTypesTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/FileTypesTest.java index ed7c17055d..aee23f9c17 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/FileTypesTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/FileTypesTest.java @@ -15,11 +15,17 @@ */ package com.google.android.exoplayer2.util; -import static com.google.android.exoplayer2.util.FileTypes.getFormatFromExtension; +import static com.google.android.exoplayer2.util.FileTypes.HEADER_CONTENT_TYPE; +import static com.google.android.exoplayer2.util.FileTypes.inferFileTypeFromMimeType; +import static com.google.android.exoplayer2.util.FileTypes.inferFileTypeFromUri; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,30 +34,64 @@ import org.junit.runner.RunWith; public class FileTypesTest { @Test - public void getFormatFromExtension_withExtension_returnsExpectedFormat() { - assertThat(getFormatFromExtension(Uri.parse("filename.mp3"))).isEqualTo(FileTypes.MP3); + public void inferFileFormat_fromResponseHeaders_returnsExpectedFormat() { + Map> responseHeaders = new HashMap<>(); + responseHeaders.put(HEADER_CONTENT_TYPE, Collections.singletonList(MimeTypes.VIDEO_MP4)); + + assertThat(FileTypes.inferFileTypeFromResponseHeaders(responseHeaders)) + .isEqualTo(FileTypes.MP4); } @Test - public void getFormatFromExtension_withExtensionPrefix_returnsExpectedFormat() { - assertThat(getFormatFromExtension(Uri.parse("filename.mka"))).isEqualTo(FileTypes.MATROSKA); + public void inferFileFormat_fromResponseHeadersWithUnknownContentType_returnsUnknownFormat() { + Map> responseHeaders = new HashMap<>(); + responseHeaders.put(HEADER_CONTENT_TYPE, Collections.singletonList("unknown")); + + assertThat(FileTypes.inferFileTypeFromResponseHeaders(responseHeaders)) + .isEqualTo(FileTypes.UNKNOWN); } @Test - public void getFormatFromExtension_withUnknownExtension_returnsUnknownFormat() { - assertThat(getFormatFromExtension(Uri.parse("filename.unknown"))).isEqualTo(FileTypes.UNKNOWN); + public void inferFileFormat_fromResponseHeadersWithoutContentType_returnsUnknownFormat() { + assertThat(FileTypes.inferFileTypeFromResponseHeaders(new HashMap<>())) + .isEqualTo(FileTypes.UNKNOWN); } @Test - public void getFormatFromExtension_withUriNotEndingWithFilename_returnsExpectedFormat() { + public void inferFileFormat_fromMimeType_returnsExpectedFormat() { + assertThat(FileTypes.inferFileTypeFromMimeType("audio/x-flac")).isEqualTo(FileTypes.FLAC); + } + + @Test + public void inferFileFormat_fromUnknownMimeType_returnsUnknownFormat() { + assertThat(inferFileTypeFromMimeType(/* mimeType= */ "unknown")).isEqualTo(FileTypes.UNKNOWN); + } + + @Test + public void inferFileFormat_fromNullMimeType_returnsUnknownFormat() { + assertThat(inferFileTypeFromMimeType(/* mimeType= */ null)).isEqualTo(FileTypes.UNKNOWN); + } + + @Test + public void inferFileFormat_fromUri_returnsExpectedFormat() { assertThat( - getFormatFromExtension( + inferFileTypeFromUri( Uri.parse("http://www.example.com/filename.mp3?query=myquery#fragment"))) .isEqualTo(FileTypes.MP3); } @Test - public void getFormatFromExtension_withNullFilename_returnsUnknownFormat() { - assertThat(getFormatFromExtension(Uri.EMPTY)).isEqualTo(FileTypes.UNKNOWN); + public void inferFileFormat_fromUriWithExtensionPrefix_returnsExpectedFormat() { + assertThat(inferFileTypeFromUri(Uri.parse("filename.mka"))).isEqualTo(FileTypes.MATROSKA); + } + + @Test + public void inferFileFormat_fromUriWithUnknownExtension_returnsUnknownFormat() { + assertThat(inferFileTypeFromUri(Uri.parse("filename.unknown"))).isEqualTo(FileTypes.UNKNOWN); + } + + @Test + public void inferFileFormat_fromEmptyUri_returnsUnknownFormat() { + assertThat(inferFileTypeFromUri(Uri.EMPTY)).isEqualTo(FileTypes.UNKNOWN); } } 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 3e329573ba..585871635c 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,7 +15,7 @@ */ package com.google.android.exoplayer2.extractor; -import static com.google.android.exoplayer2.util.FileTypes.getFormatFromExtension; +import static com.google.android.exoplayer2.util.FileTypes.inferFileTypeFromUri; import android.net.Uri; import androidx.annotation.Nullable; @@ -272,11 +272,11 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { public synchronized Extractor[] createExtractors(Uri uri) { List extractors = new ArrayList<>(/* initialCapacity= */ 14); - @FileTypes.Type int extensionFormat = getFormatFromExtension(uri); - addExtractorsForFormat(extensionFormat, extractors); + @FileTypes.Type int inferredFileType = inferFileTypeFromUri(uri); + addExtractorsForFormat(inferredFileType, extractors); for (int format : DEFAULT_EXTRACTOR_ORDER) { - if (format != extensionFormat) { + if (format != inferredFileType) { addExtractorsForFormat(format, extractors); } } 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 32a156f66d..52d9e359cd 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 @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.source.hls; -import static com.google.android.exoplayer2.util.FileTypes.getFormatFromExtension; +import static com.google.android.exoplayer2.util.FileTypes.inferFileTypeFromUri; import android.net.Uri; import android.text.TextUtils; @@ -101,12 +101,12 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { // Try selecting the extractor by the file extension. @Nullable - Extractor extractorByFileExtension = - createExtractorByFileExtension(uri, format, muxedCaptionFormats, timestampAdjuster); + Extractor inferredExtractor = + createInferredExtractor( + uri, format, muxedCaptionFormats, timestampAdjuster, responseHeaders); extractorInput.resetPeekPosition(); - if (extractorByFileExtension != null - && sniffQuietly(extractorByFileExtension, extractorInput)) { - return buildResult(extractorByFileExtension); + if (inferredExtractor != null && sniffQuietly(inferredExtractor, extractorInput)) { + return buildResult(inferredExtractor); } // We need to manually sniff each known type, without retrying the one selected by file @@ -114,9 +114,9 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { // https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ. // Extractor to be used if the type is not recognized. - @Nullable Extractor fallBackExtractor = extractorByFileExtension; + @Nullable Extractor fallBackExtractor = inferredExtractor; - if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) { + if (!(inferredExtractor instanceof FragmentedMp4Extractor)) { FragmentedMp4Extractor fragmentedMp4Extractor = createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats); if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) { @@ -124,14 +124,14 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { } } - if (!(extractorByFileExtension instanceof WebvttExtractor)) { + if (!(inferredExtractor instanceof WebvttExtractor)) { WebvttExtractor webvttExtractor = new WebvttExtractor(format.language, timestampAdjuster); if (sniffQuietly(webvttExtractor, extractorInput)) { return buildResult(webvttExtractor); } } - if (!(extractorByFileExtension instanceof TsExtractor)) { + if (!(inferredExtractor instanceof TsExtractor)) { TsExtractor tsExtractor = createTsExtractor( payloadReaderFactoryFlags, @@ -147,28 +147,28 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { } } - if (!(extractorByFileExtension instanceof AdtsExtractor)) { + if (!(inferredExtractor instanceof AdtsExtractor)) { AdtsExtractor adtsExtractor = new AdtsExtractor(); if (sniffQuietly(adtsExtractor, extractorInput)) { return buildResult(adtsExtractor); } } - if (!(extractorByFileExtension instanceof Ac3Extractor)) { + if (!(inferredExtractor instanceof Ac3Extractor)) { Ac3Extractor ac3Extractor = new Ac3Extractor(); if (sniffQuietly(ac3Extractor, extractorInput)) { return buildResult(ac3Extractor); } } - if (!(extractorByFileExtension instanceof Ac4Extractor)) { + if (!(inferredExtractor instanceof Ac4Extractor)) { Ac4Extractor ac4Extractor = new Ac4Extractor(); if (sniffQuietly(ac4Extractor, extractorInput)) { return buildResult(ac4Extractor); } } - if (!(extractorByFileExtension instanceof Mp3Extractor)) { + if (!(inferredExtractor instanceof Mp3Extractor)) { Mp3Extractor mp3Extractor = new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0); if (sniffQuietly(mp3Extractor, extractorInput)) { @@ -180,16 +180,20 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { } @Nullable - private Extractor createExtractorByFileExtension( + private Extractor createInferredExtractor( Uri uri, Format format, @Nullable List muxedCaptionFormats, - TimestampAdjuster timestampAdjuster) { - if (MimeTypes.TEXT_VTT.equals(format.sampleMimeType)) { - return new WebvttExtractor(format.language, timestampAdjuster); + TimestampAdjuster timestampAdjuster, + Map> responseHeaders) { + @FileTypes.Type int fileType = FileTypes.inferFileTypeFromMimeType(format.sampleMimeType); + if (fileType == FileTypes.UNKNOWN) { + fileType = FileTypes.inferFileTypeFromResponseHeaders(responseHeaders); } - @FileTypes.Type int fileFormat = getFormatFromExtension(uri); - switch (fileFormat) { + if (fileType == FileTypes.UNKNOWN) { + fileType = inferFileTypeFromUri(uri); + } + switch (fileType) { case FileTypes.WEBVTT: return new WebvttExtractor(format.language, timestampAdjuster); case FileTypes.ADTS: