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 ea8b362b86..e2055a24f0 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 @@ -18,13 +18,29 @@ package com.google.android.exoplayer2.util; import android.text.TextUtils; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.AacUtil; import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Defines common MIME types and helper methods. */ public final class MimeTypes { + /** An mp4a Object Type Indication (OTI) and its optional audio OTI is defined by RFC 6381. */ + public static final class Mp4aObjectType { + /** The Object Type Indication of the mp4a codec. */ + public final int objectTypeIndication; + /** The Audio Object Type Indication of the mp4a codec, or 0 if it is absent. */ + @AacUtil.AacAudioObjectType public final int audioObjectTypeIndication; + + private Mp4aObjectType(int objectTypeIndication, int audioObjectTypeIndication) { + this.objectTypeIndication = objectTypeIndication; + this.audioObjectTypeIndication = audioObjectTypeIndication; + } + } + public static final String BASE_TYPE_VIDEO = "video"; public static final String BASE_TYPE_AUDIO = "audio"; public static final String BASE_TYPE_TEXT = "text"; @@ -106,6 +122,9 @@ public final class MimeTypes { private static final ArrayList customMimeTypes = new ArrayList<>(); + private static final Pattern MP4A_RFC_6381_CODEC_PATTERN = + Pattern.compile("^mp4a\\.([a-zA-Z0-9]{2})(?:\\.([0-9]{1,2}))?$"); + /** * Registers a custom MIME type. Most applications do not need to call this method, as handling of * standard MIME types is built in. These built-in MIME types take precedence over any registered @@ -275,15 +294,9 @@ public final class MimeTypes { } else if (codec.startsWith("mp4a")) { @Nullable String mimeType = null; if (codec.startsWith("mp4a.")) { - String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix - if (objectTypeString.length() >= 2) { - try { - String objectTypeHexString = Util.toUpperInvariant(objectTypeString.substring(0, 2)); - int objectTypeInt = Integer.parseInt(objectTypeHexString, 16); - mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt); - } catch (NumberFormatException ignored) { - // Ignored. - } + @Nullable Mp4aObjectType objectType = getObjectTypeFromMp4aRFC6381CodecString(codec); + if (objectType != null) { + mimeType = getMimeTypeFromMp4ObjectType(objectType.objectTypeIndication); } } return mimeType == null ? MimeTypes.AUDIO_AAC : mimeType; @@ -407,13 +420,25 @@ public final class MimeTypes { * it is an encoded (non-PCM) audio format, or {@link C#ENCODING_INVALID} otherwise. * * @param mimeType The MIME type. - * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or + * @param codecs Codecs of the format as described in RFC 6381, or null if unknown or not + * applicable. + * @return One of {@link C.Encoding} constants that corresponds to a specified MIME type, or * {@link C#ENCODING_INVALID}. */ - public static @C.Encoding int getEncoding(String mimeType) { + @C.Encoding + public static int getEncoding(String mimeType, @Nullable String codecs) { switch (mimeType) { case MimeTypes.AUDIO_MPEG: return C.ENCODING_MP3; + case MimeTypes.AUDIO_AAC: + if (codecs == null) { + return C.ENCODING_INVALID; + } + @Nullable Mp4aObjectType objectType = getObjectTypeFromMp4aRFC6381CodecString(codecs); + if (objectType == null) { + return C.ENCODING_INVALID; + } + return AacUtil.getEncodingForAudioObjectType(objectType.audioObjectTypeIndication); case MimeTypes.AUDIO_AC3: return C.ENCODING_AC3; case MimeTypes.AUDIO_E_AC3: @@ -443,6 +468,40 @@ public final class MimeTypes { return getTrackType(getMediaMimeType(codec)); } + /** + * Retrieves the object type of an mp4 audio codec from its string as defined in RFC 6381. + * + *

Per https://mp4ra.org/#/object_types and https://tools.ietf.org/html/rfc6381#section-3.3, an + * mp4 codec string has the form: + * ~~~~~~~~~~~~~~ Object Type Indication (OTI) byte in hex + * mp4a.[a-zA-Z0-9]{2}(.[0-9]{1,2})? + * ~~~~~~~~~~ audio OTI, decimal. Only for certain OTI. + * For example: mp4a.40.2, has an OTI of 0x40 and an audio OTI of 2. + * + * @param codec The string as defined in RFC 6381 describing an mp4 audio codec. + * @return The {@link Mp4aObjectType} or {@code null} if the input is invalid. + */ + @Nullable + public static Mp4aObjectType getObjectTypeFromMp4aRFC6381CodecString(String codec) { + Matcher matcher = MP4A_RFC_6381_CODEC_PATTERN.matcher(codec); + if (!matcher.matches()) { + return null; + } + String objectTypeIndicationHex = Assertions.checkNotNull(matcher.group(1)); + @Nullable String audioObjectTypeIndicationDec = matcher.group(2); + int objectTypeIndication; + int audioObjectTypeIndication = 0; + try { + objectTypeIndication = Integer.parseInt(objectTypeIndicationHex, 16); + if (audioObjectTypeIndicationDec != null) { + audioObjectTypeIndication = Integer.parseInt(audioObjectTypeIndicationDec); + } + } catch (NumberFormatException e) { + return null; + } + return new Mp4aObjectType(objectTypeIndication, audioObjectTypeIndication); + } + /** * 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/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java index e88385bbca..34ad0a5946 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util; import static com.google.common.truth.Truth.assertThat; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; @@ -133,4 +134,41 @@ public final class MimeTypesTest { assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(0x01)).isNull(); assertThat(MimeTypes.getMimeTypeFromMp4ObjectType(-1)).isNull(); } + + @Test + public void getObjectTypeFromMp4aRFC6381CodecString_onInvalidInput_returnsNull() { + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("abc")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("mp4a.")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("mp4a.1")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("mp4a.a")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("mp4a.1g")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("mp4v.20.9")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("mp4a.100.1")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("mp4a.10.")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("mp4a.a.1")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("mp4a.10,01")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("mp4a.1f.f1")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("mp4a.1a.a")).isNull(); + assertThat(MimeTypes.getObjectTypeFromMp4aRFC6381CodecString("mp4a.01.110")).isNull(); + } + + @Test + public void getObjectTypeFromMp4aRFC6381CodecString_onValidInput_returnsCorrectObjectType() { + assert_getObjectTypeFromMp4aRFC6381CodecString_for_returns("mp4a.00.0", 0x00, 0); + assert_getObjectTypeFromMp4aRFC6381CodecString_for_returns("mp4a.01.01", 0x01, 1); + assert_getObjectTypeFromMp4aRFC6381CodecString_for_returns("mp4a.10.10", 0x10, 10); + assert_getObjectTypeFromMp4aRFC6381CodecString_for_returns("mp4a.a0.90", 0xa0, 90); + assert_getObjectTypeFromMp4aRFC6381CodecString_for_returns("mp4a.Ff.99", 0xff, 99); + assert_getObjectTypeFromMp4aRFC6381CodecString_for_returns("mp4a.D0.9", 0xd0, 9); + } + + private static void assert_getObjectTypeFromMp4aRFC6381CodecString_for_returns( + String codec, int expectedObjectTypeIndicator, int expectedAudioObjectTypeIndicator) { + @Nullable + MimeTypes.Mp4aObjectType objectType = MimeTypes.getObjectTypeFromMp4aRFC6381CodecString(codec); + assertThat(objectType).isNotNull(); + assertThat(objectType.objectTypeIndication).isEqualTo(expectedObjectTypeIndicator); + assertThat(objectType.audioObjectTypeIndication).isEqualTo(expectedAudioObjectTypeIndicator); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index df53291883..c69383ffe2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -463,13 +463,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media // E-AC3 JOC is object-based so the output channel count is arbitrary. if (audioSink.supportsOutput( /* channelCount= */ Format.NO_VALUE, format.sampleRate, C.ENCODING_E_AC3_JOC)) { - return MimeTypes.getEncoding(MimeTypes.AUDIO_E_AC3_JOC); + return MimeTypes.getEncoding(MimeTypes.AUDIO_E_AC3_JOC, format.codecs); } // E-AC3 receivers can decode JOC streams, but in 2-D rather than 3-D, so try to fall back. mimeType = MimeTypes.AUDIO_E_AC3; } - @C.Encoding int encoding = MimeTypes.getEncoding(mimeType); + @C.Encoding int encoding = MimeTypes.getEncoding(mimeType, format.codecs); if (audioSink.supportsOutput(format.channelCount, format.sampleRate, encoding)) { return encoding; } else {