Commit b1e56304 by kimvde Committed by Ian Baker

Add support for inferring file format from MIME type

PiperOrigin-RevId: 315283926
parent 99d805f6
...@@ -15,11 +15,17 @@ ...@@ -15,11 +15,17 @@
*/ */
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import static com.google.android.exoplayer2.util.MimeTypes.normalizeMimeType;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Map;
/** Defines common file type constants and helper methods. */ /** Defines common file type constants and helper methods. */
public final class FileTypes { public final class FileTypes {
...@@ -64,6 +70,8 @@ public final class FileTypes { ...@@ -64,6 +70,8 @@ public final class FileTypes {
/** File type for the WebVTT format. */ /** File type for the WebVTT format. */
public static final int WEBVTT = 13; 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_AC3 = ".ac3";
private static final String EXTENSION_EC3 = ".ec3"; private static final String EXTENSION_EC3 = ".ec3";
private static final String EXTENSION_AC4 = ".ac4"; private static final String EXTENSION_AC4 = ".ac4";
...@@ -94,13 +102,72 @@ public final class FileTypes { ...@@ -94,13 +102,72 @@ public final class FileTypes {
private FileTypes() {} private FileTypes() {}
/** Returns the {@link Type} corresponding to the response headers provided. */
@FileTypes.Type
public static int inferFileTypeFromResponseHeaders(Map<String, List<String>> responseHeaders) {
@Nullable List<String> 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}. * Returns the {@link Type} corresponding to the MIME type provided.
* The filename is considered to be the last segment of the {@link Uri} path. *
* <p>Returns {@link #UNKNOWN} if the mime type is {@code null}.
*/ */
@FileTypes.Type @FileTypes.Type
public static int getFormatFromExtension(Uri uri) { public static int inferFileTypeFromMimeType(@Nullable String mimeType) {
String filename = uri.getLastPathSegment(); 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) { if (filename == null) {
return FileTypes.UNKNOWN; return FileTypes.UNKNOWN;
} else if (filename.endsWith(EXTENSION_AC3) || filename.endsWith(EXTENSION_EC3)) { } else if (filename.endsWith(EXTENSION_AC3) || filename.endsWith(EXTENSION_EC3)) {
......
...@@ -47,6 +47,7 @@ public final class MimeTypes { ...@@ -47,6 +47,7 @@ public final class MimeTypes {
public static final String BASE_TYPE_APPLICATION = "application"; public static final String BASE_TYPE_APPLICATION = "application";
public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4"; 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_WEBM = BASE_TYPE_VIDEO + "/webm";
public static final String VIDEO_H263 = BASE_TYPE_VIDEO + "/3gpp"; public static final String VIDEO_H263 = BASE_TYPE_VIDEO + "/3gpp";
public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc"; public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc";
...@@ -67,6 +68,7 @@ public final class MimeTypes { ...@@ -67,6 +68,7 @@ public final class MimeTypes {
public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4"; 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_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_WEBM = BASE_TYPE_AUDIO + "/webm";
public static final String AUDIO_MPEG = BASE_TYPE_AUDIO + "/mpeg"; public static final String AUDIO_MPEG = BASE_TYPE_AUDIO + "/mpeg";
public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1"; public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1";
...@@ -91,6 +93,7 @@ public final class MimeTypes { ...@@ -91,6 +93,7 @@ public final class MimeTypes {
public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; 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_MSGSM = BASE_TYPE_AUDIO + "/gsm";
public static final String AUDIO_OGG = BASE_TYPE_AUDIO + "/ogg"; 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 AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown";
public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
...@@ -503,6 +506,26 @@ public final class MimeTypes { ...@@ -503,6 +506,26 @@ public final class MimeTypes {
} }
/** /**
* 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 * Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not
* contain a forward slash character ({@code '/'}). * contain a forward slash character ({@code '/'}).
*/ */
......
...@@ -1676,6 +1676,7 @@ public final class Util { ...@@ -1676,6 +1676,7 @@ public final class Util {
* @param mimeType If not null, used to infer the type. * @param mimeType If not null, used to infer the type.
* @return The content type. * @return The content type.
*/ */
@C.ContentType
public static int inferContentTypeWithMimeType(Uri uri, @Nullable String mimeType) { public static int inferContentTypeWithMimeType(Uri uri, @Nullable String mimeType) {
if (mimeType == null) { if (mimeType == null) {
return Util.inferContentType(uri); return Util.inferContentType(uri);
......
...@@ -15,11 +15,17 @@ ...@@ -15,11 +15,17 @@
*/ */
package com.google.android.exoplayer2.util; 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 static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4; 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.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -28,30 +34,64 @@ import org.junit.runner.RunWith; ...@@ -28,30 +34,64 @@ import org.junit.runner.RunWith;
public class FileTypesTest { public class FileTypesTest {
@Test @Test
public void getFormatFromExtension_withExtension_returnsExpectedFormat() { public void inferFileFormat_fromResponseHeaders_returnsExpectedFormat() {
assertThat(getFormatFromExtension(Uri.parse("filename.mp3"))).isEqualTo(FileTypes.MP3); Map<String, List<String>> responseHeaders = new HashMap<>();
responseHeaders.put(HEADER_CONTENT_TYPE, Collections.singletonList(MimeTypes.VIDEO_MP4));
assertThat(FileTypes.inferFileTypeFromResponseHeaders(responseHeaders))
.isEqualTo(FileTypes.MP4);
}
@Test
public void inferFileFormat_fromResponseHeadersWithUnknownContentType_returnsUnknownFormat() {
Map<String, List<String>> responseHeaders = new HashMap<>();
responseHeaders.put(HEADER_CONTENT_TYPE, Collections.singletonList("unknown"));
assertThat(FileTypes.inferFileTypeFromResponseHeaders(responseHeaders))
.isEqualTo(FileTypes.UNKNOWN);
} }
@Test @Test
public void getFormatFromExtension_withExtensionPrefix_returnsExpectedFormat() { public void inferFileFormat_fromResponseHeadersWithoutContentType_returnsUnknownFormat() {
assertThat(getFormatFromExtension(Uri.parse("filename.mka"))).isEqualTo(FileTypes.MATROSKA); assertThat(FileTypes.inferFileTypeFromResponseHeaders(new HashMap<>()))
.isEqualTo(FileTypes.UNKNOWN);
} }
@Test @Test
public void getFormatFromExtension_withUnknownExtension_returnsUnknownFormat() { public void inferFileFormat_fromMimeType_returnsExpectedFormat() {
assertThat(getFormatFromExtension(Uri.parse("filename.unknown"))).isEqualTo(FileTypes.UNKNOWN); assertThat(FileTypes.inferFileTypeFromMimeType("audio/x-flac")).isEqualTo(FileTypes.FLAC);
} }
@Test @Test
public void getFormatFromExtension_withUriNotEndingWithFilename_returnsExpectedFormat() { 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( assertThat(
getFormatFromExtension( inferFileTypeFromUri(
Uri.parse("http://www.example.com/filename.mp3?query=myquery#fragment"))) Uri.parse("http://www.example.com/filename.mp3?query=myquery#fragment")))
.isEqualTo(FileTypes.MP3); .isEqualTo(FileTypes.MP3);
} }
@Test @Test
public void getFormatFromExtension_withNullFilename_returnsUnknownFormat() { public void inferFileFormat_fromUriWithExtensionPrefix_returnsExpectedFormat() {
assertThat(getFormatFromExtension(Uri.EMPTY)).isEqualTo(FileTypes.UNKNOWN); 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);
} }
} }
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor; 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 android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -272,11 +272,11 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { ...@@ -272,11 +272,11 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
public synchronized Extractor[] createExtractors(Uri uri) { public synchronized Extractor[] createExtractors(Uri uri) {
List<Extractor> extractors = new ArrayList<>(/* initialCapacity= */ 14); List<Extractor> extractors = new ArrayList<>(/* initialCapacity= */ 14);
@FileTypes.Type int extensionFormat = getFormatFromExtension(uri); @FileTypes.Type int inferredFileType = inferFileTypeFromUri(uri);
addExtractorsForFormat(extensionFormat, extractors); addExtractorsForFormat(inferredFileType, extractors);
for (int format : DEFAULT_EXTRACTOR_ORDER) { for (int format : DEFAULT_EXTRACTOR_ORDER) {
if (format != extensionFormat) { if (format != inferredFileType) {
addExtractorsForFormat(format, extractors); addExtractorsForFormat(format, extractors);
} }
} }
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.hls; 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.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
...@@ -101,12 +101,12 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -101,12 +101,12 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
// Try selecting the extractor by the file extension. // Try selecting the extractor by the file extension.
@Nullable @Nullable
Extractor extractorByFileExtension = Extractor inferredExtractor =
createExtractorByFileExtension(uri, format, muxedCaptionFormats, timestampAdjuster); createInferredExtractor(
uri, format, muxedCaptionFormats, timestampAdjuster, responseHeaders);
extractorInput.resetPeekPosition(); extractorInput.resetPeekPosition();
if (extractorByFileExtension != null if (inferredExtractor != null && sniffQuietly(inferredExtractor, extractorInput)) {
&& sniffQuietly(extractorByFileExtension, extractorInput)) { return buildResult(inferredExtractor);
return buildResult(extractorByFileExtension);
} }
// We need to manually sniff each known type, without retrying the one selected by file // 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 { ...@@ -114,9 +114,9 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
// https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ. // https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ.
// Extractor to be used if the type is not recognized. // 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 = FragmentedMp4Extractor fragmentedMp4Extractor =
createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats); createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats);
if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) { if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) {
...@@ -124,14 +124,14 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -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); WebvttExtractor webvttExtractor = new WebvttExtractor(format.language, timestampAdjuster);
if (sniffQuietly(webvttExtractor, extractorInput)) { if (sniffQuietly(webvttExtractor, extractorInput)) {
return buildResult(webvttExtractor); return buildResult(webvttExtractor);
} }
} }
if (!(extractorByFileExtension instanceof TsExtractor)) { if (!(inferredExtractor instanceof TsExtractor)) {
TsExtractor tsExtractor = TsExtractor tsExtractor =
createTsExtractor( createTsExtractor(
payloadReaderFactoryFlags, payloadReaderFactoryFlags,
...@@ -147,28 +147,28 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -147,28 +147,28 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
} }
} }
if (!(extractorByFileExtension instanceof AdtsExtractor)) { if (!(inferredExtractor instanceof AdtsExtractor)) {
AdtsExtractor adtsExtractor = new AdtsExtractor(); AdtsExtractor adtsExtractor = new AdtsExtractor();
if (sniffQuietly(adtsExtractor, extractorInput)) { if (sniffQuietly(adtsExtractor, extractorInput)) {
return buildResult(adtsExtractor); return buildResult(adtsExtractor);
} }
} }
if (!(extractorByFileExtension instanceof Ac3Extractor)) { if (!(inferredExtractor instanceof Ac3Extractor)) {
Ac3Extractor ac3Extractor = new Ac3Extractor(); Ac3Extractor ac3Extractor = new Ac3Extractor();
if (sniffQuietly(ac3Extractor, extractorInput)) { if (sniffQuietly(ac3Extractor, extractorInput)) {
return buildResult(ac3Extractor); return buildResult(ac3Extractor);
} }
} }
if (!(extractorByFileExtension instanceof Ac4Extractor)) { if (!(inferredExtractor instanceof Ac4Extractor)) {
Ac4Extractor ac4Extractor = new Ac4Extractor(); Ac4Extractor ac4Extractor = new Ac4Extractor();
if (sniffQuietly(ac4Extractor, extractorInput)) { if (sniffQuietly(ac4Extractor, extractorInput)) {
return buildResult(ac4Extractor); return buildResult(ac4Extractor);
} }
} }
if (!(extractorByFileExtension instanceof Mp3Extractor)) { if (!(inferredExtractor instanceof Mp3Extractor)) {
Mp3Extractor mp3Extractor = Mp3Extractor mp3Extractor =
new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0); new Mp3Extractor(/* flags= */ 0, /* forcedFirstSampleTimestampUs= */ 0);
if (sniffQuietly(mp3Extractor, extractorInput)) { if (sniffQuietly(mp3Extractor, extractorInput)) {
...@@ -180,16 +180,20 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -180,16 +180,20 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
} }
@Nullable @Nullable
private Extractor createExtractorByFileExtension( private Extractor createInferredExtractor(
Uri uri, Uri uri,
Format format, Format format,
@Nullable List<Format> muxedCaptionFormats, @Nullable List<Format> muxedCaptionFormats,
TimestampAdjuster timestampAdjuster) { TimestampAdjuster timestampAdjuster,
if (MimeTypes.TEXT_VTT.equals(format.sampleMimeType)) { Map<String, List<String>> responseHeaders) {
return new WebvttExtractor(format.language, timestampAdjuster); @FileTypes.Type int fileType = FileTypes.inferFileTypeFromMimeType(format.sampleMimeType);
if (fileType == FileTypes.UNKNOWN) {
fileType = FileTypes.inferFileTypeFromResponseHeaders(responseHeaders);
}
if (fileType == FileTypes.UNKNOWN) {
fileType = inferFileTypeFromUri(uri);
} }
@FileTypes.Type int fileFormat = getFormatFromExtension(uri); switch (fileType) {
switch (fileFormat) {
case FileTypes.WEBVTT: case FileTypes.WEBVTT:
return new WebvttExtractor(format.language, timestampAdjuster); return new WebvttExtractor(format.language, timestampAdjuster);
case FileTypes.ADTS: case FileTypes.ADTS:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment