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
This commit is contained in:
tonihei 2023-11-14 07:37:22 -08:00 committed by Copybara-Service
parent 8d83d491f1
commit b570c72588
11 changed files with 47 additions and 11 deletions

View File

@ -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,

View File

@ -296,7 +296,7 @@ public final class DrmInitData implements Comparator<SchemeData>, 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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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