From b570c72588e9b36c5c504edc21b1ecaa88f6ff9c Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2023 07:37:22 -0800 Subject: [PATCH] Normalize MIME types when accepting user or media input MIME types are case-insensitive, but none of the many existing comparisons across our code base take this into account. The code can be made more robust by normalizing all MIME types at the moment they are first set into a class/builder and adding toLowerCase as part of the normalization. Most concretely, this fixes an issue with playing HLS streams via the IMA SDK where the stream MIME type is indicated with all lower case "application/x-mpegurl", which failed the MIME type comparison in DefaultMediaSourceFactory. PiperOrigin-RevId: 582317261 --- RELEASENOTES.md | 2 ++ .../androidx/media3/common/DrmInitData.java | 2 +- .../java/androidx/media3/common/Format.java | 4 ++-- .../java/androidx/media3/common/MediaItem.java | 6 +++--- .../java/androidx/media3/common/MimeTypes.java | 17 +++++++++++++++-- .../exoplayer/offline/DownloadRequest.java | 3 ++- .../hls/DefaultMediaSourceFactoryTest.java | 12 ++++++++++++ .../extractor/metadata/flac/PictureFrame.java | 4 +++- .../extractor/metadata/id3/Id3Decoder.java | 4 +++- .../transformer/TransformationRequest.java | 2 ++ .../media3/transformer/Transformer.java | 2 ++ 11 files changed, 47 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d7f0901bd2..1ed740f23a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,8 @@ resource URIs where `package` is different to the package of the current application. This has always been documented to work, but wasn't correctly implemented until now. + * Normalize MIME types set by app code or read from media to be fully + lower-case. * ExoPlayer: * Add `PreloadMediaSource` and `PreloadMediaPeriod` that allows apps to preload the media source at a specific start position before playback, diff --git a/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java b/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java index 4f942fa739..32a5d234f7 100644 --- a/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java +++ b/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java @@ -296,7 +296,7 @@ public final class DrmInitData implements Comparator, Parcelable { UUID uuid, @Nullable String licenseServerUrl, String mimeType, @Nullable byte[] data) { this.uuid = Assertions.checkNotNull(uuid); this.licenseServerUrl = licenseServerUrl; - this.mimeType = Assertions.checkNotNull(mimeType); + this.mimeType = MimeTypes.normalizeMimeType(Assertions.checkNotNull(mimeType)); this.data = data; } diff --git a/libraries/common/src/main/java/androidx/media3/common/Format.java b/libraries/common/src/main/java/androidx/media3/common/Format.java index 5795a1437b..e5dba212e6 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Format.java +++ b/libraries/common/src/main/java/androidx/media3/common/Format.java @@ -396,7 +396,7 @@ public final class Format implements Bundleable { */ @CanIgnoreReturnValue public Builder setContainerMimeType(@Nullable String containerMimeType) { - this.containerMimeType = containerMimeType; + this.containerMimeType = MimeTypes.normalizeMimeType(containerMimeType); return this; } @@ -410,7 +410,7 @@ public final class Format implements Bundleable { */ @CanIgnoreReturnValue public Builder setSampleMimeType(@Nullable String sampleMimeType) { - this.sampleMimeType = sampleMimeType; + this.sampleMimeType = MimeTypes.normalizeMimeType(sampleMimeType); return this; } diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java index d03c683d68..bef87eb64e 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java @@ -1179,7 +1179,7 @@ public final class MediaItem implements Bundleable { @Nullable Object tag, long imageDurationMs) { this.uri = uri; - this.mimeType = mimeType; + this.mimeType = MimeTypes.normalizeMimeType(mimeType); this.drmConfiguration = drmConfiguration; this.adsConfiguration = adsConfiguration; this.streamKeys = streamKeys; @@ -1612,7 +1612,7 @@ public final class MediaItem implements Bundleable { /** Sets the MIME type. */ @CanIgnoreReturnValue public Builder setMimeType(@Nullable String mimeType) { - this.mimeType = mimeType; + this.mimeType = MimeTypes.normalizeMimeType(mimeType); return this; } @@ -1694,7 +1694,7 @@ public final class MediaItem implements Bundleable { @Nullable String label, @Nullable String id) { this.uri = uri; - this.mimeType = mimeType; + this.mimeType = MimeTypes.normalizeMimeType(mimeType); this.language = language; this.selectionFlags = selectionFlags; this.roleFlags = roleFlags; diff --git a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java index fdfb10327f..54a08b6f94 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java +++ b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java @@ -25,6 +25,7 @@ import com.google.common.base.Ascii; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.PolyNull; import org.checkerframework.dataflow.qual.Pure; /** Defines common MIME types and helper methods. */ @@ -635,18 +636,30 @@ public final class MimeTypes { /** * Normalizes the MIME type provided so that equivalent MIME types are uniquely represented. * - * @param mimeType A MIME type to normalize. + * @param mimeType A MIME type to normalize, or null. * @return The normalized MIME type, or the argument MIME type if its normalized form is unknown. */ @UnstableApi - public static String normalizeMimeType(String mimeType) { + public static @PolyNull String normalizeMimeType(@PolyNull String mimeType) { + if (mimeType == null) { + return null; + } + mimeType = Ascii.toLowerCase(mimeType); switch (mimeType) { + // Normalize uncommon versions of some audio MIME types to their standard equivalent. 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; + // Normalize MIME types that are often written with upper-case letters to their common form. + case "application/x-mpegurl": + return APPLICATION_M3U8; + case "audio/mpeg-l1": + return AUDIO_MPEG_L1; + case "audio/mpeg-l2": + return AUDIO_MPEG_L2; default: return mimeType; } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadRequest.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadRequest.java index 2f69102775..b3bbd130f0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadRequest.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadRequest.java @@ -23,6 +23,7 @@ import android.os.Parcelable; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.MediaItem; +import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.UnstableApi; @@ -61,7 +62,7 @@ public final class DownloadRequest implements Parcelable { /** Sets the {@link DownloadRequest#mimeType}. */ @CanIgnoreReturnValue public Builder setMimeType(@Nullable String mimeType) { - this.mimeType = mimeType; + this.mimeType = MimeTypes.normalizeMimeType(mimeType); return this; } diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/DefaultMediaSourceFactoryTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/DefaultMediaSourceFactoryTest.java index 8062cff051..1db89cce18 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/DefaultMediaSourceFactoryTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/DefaultMediaSourceFactoryTest.java @@ -51,6 +51,18 @@ public class DefaultMediaSourceFactoryTest { assertThat(mediaSource).isInstanceOf(HlsMediaSource.class); } + @Test + public void createMediaSource_withMimeTypeLowerCaseLetters_hlsSource() { + DefaultMediaSourceFactory defaultMediaSourceFactory = + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); + MediaItem mediaItem = + new MediaItem.Builder().setUri(URI_MEDIA).setMimeType("application/x-mpegurl").build(); + + MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); + + assertThat(mediaSource).isInstanceOf(HlsMediaSource.class); + } + @Test public void createMediaSource_withTag_tagInSource() { Object tag = new Object(); diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/flac/PictureFrame.java b/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/flac/PictureFrame.java index 6189212518..36b6d4207c 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/flac/PictureFrame.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/flac/PictureFrame.java @@ -22,6 +22,7 @@ import android.os.Parcelable; import androidx.annotation.Nullable; import androidx.media3.common.MediaMetadata; import androidx.media3.common.Metadata; +import androidx.media3.common.MimeTypes; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; import com.google.common.base.Charsets; @@ -159,7 +160,8 @@ public final class PictureFrame implements Metadata.Entry { public static PictureFrame fromPictureBlock(ParsableByteArray pictureBlock) { int pictureType = pictureBlock.readInt(); int mimeTypeLength = pictureBlock.readInt(); - String mimeType = pictureBlock.readString(mimeTypeLength, Charsets.US_ASCII); + String mimeType = + MimeTypes.normalizeMimeType(pictureBlock.readString(mimeTypeLength, Charsets.US_ASCII)); int descriptionLength = pictureBlock.readInt(); String description = pictureBlock.readString(descriptionLength); int width = pictureBlock.readInt(); diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/Id3Decoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/Id3Decoder.java index 3358fe0daf..b05717e947 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/Id3Decoder.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/metadata/id3/Id3Decoder.java @@ -18,6 +18,7 @@ package androidx.media3.extractor.metadata.id3; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Metadata; +import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Log; import androidx.media3.common.util.ParsableBitArray; import androidx.media3.common.util.ParsableByteArray; @@ -557,7 +558,8 @@ public final class Id3Decoder extends SimpleMetadataDecoder { id3Data.readBytes(data, 0, frameSize - 1); int mimeTypeEndIndex = indexOfZeroByte(data, 0); - String mimeType = new String(data, 0, mimeTypeEndIndex, Charsets.ISO_8859_1); + String mimeType = + MimeTypes.normalizeMimeType(new String(data, 0, mimeTypeEndIndex, Charsets.ISO_8859_1)); int filenameStartIndex = mimeTypeEndIndex + 1; int filenameEndIndex = indexOfTerminator(data, filenameStartIndex, encoding); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java index a91c5178b0..eae549e010 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java @@ -74,6 +74,7 @@ public final class TransformationRequest { */ @CanIgnoreReturnValue public Builder setVideoMimeType(@Nullable String videoMimeType) { + videoMimeType = MimeTypes.normalizeMimeType(videoMimeType); checkArgument( videoMimeType == null || MimeTypes.isVideo(videoMimeType), "Not a video MIME type: " + videoMimeType); @@ -100,6 +101,7 @@ public final class TransformationRequest { */ @CanIgnoreReturnValue public Builder setAudioMimeType(@Nullable String audioMimeType) { + audioMimeType = MimeTypes.normalizeMimeType(audioMimeType); checkArgument( audioMimeType == null || MimeTypes.isAudio(audioMimeType), "Not an audio MIME type: " + audioMimeType); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index 65f1cbf635..df04e1f85d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -174,6 +174,7 @@ public final class Transformer { */ @CanIgnoreReturnValue public Builder setAudioMimeType(String audioMimeType) { + audioMimeType = MimeTypes.normalizeMimeType(audioMimeType); checkArgument(MimeTypes.isAudio(audioMimeType), "Not an audio MIME type: " + audioMimeType); this.audioMimeType = audioMimeType; return this; @@ -205,6 +206,7 @@ public final class Transformer { */ @CanIgnoreReturnValue public Builder setVideoMimeType(String videoMimeType) { + videoMimeType = MimeTypes.normalizeMimeType(videoMimeType); checkArgument(MimeTypes.isVideo(videoMimeType), "Not a video MIME type: " + videoMimeType); this.videoMimeType = videoMimeType; return this;