Add support for inferring file format from MIME type

PiperOrigin-RevId: 315283926
This commit is contained in:
kimvde 2020-06-08 16:55:41 +01:00 committed by Ian Baker
parent 99d805f6a8
commit b1e56304a1
6 changed files with 174 additions and 39 deletions

View file

@ -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<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}.
* The filename is considered to be the last segment of the {@link Uri} path.
* Returns the {@link Type} corresponding to the MIME type provided.
*
* <p>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)) {

View file

@ -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 '/'}).

View file

@ -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);

View file

@ -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<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 getFormatFromExtension_withExtensionPrefix_returnsExpectedFormat() {
assertThat(getFormatFromExtension(Uri.parse("filename.mka"))).isEqualTo(FileTypes.MATROSKA);
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
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);
}
}

View file

@ -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<Extractor> 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);
}
}

View file

@ -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<Format> muxedCaptionFormats,
TimestampAdjuster timestampAdjuster) {
if (MimeTypes.TEXT_VTT.equals(format.sampleMimeType)) {
return new WebvttExtractor(format.language, timestampAdjuster);
TimestampAdjuster timestampAdjuster,
Map<String, List<String>> 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: