mirror of
https://github.com/samsonjs/media.git
synced 2026-04-07 11:35:46 +00:00
Add support for inferring file format from MIME type
PiperOrigin-RevId: 315283926
This commit is contained in:
parent
99d805f6a8
commit
b1e56304a1
6 changed files with 174 additions and 39 deletions
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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 '/'}).
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue