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:
parent
8d83d491f1
commit
b570c72588
@ -7,6 +7,8 @@
|
|||||||
resource URIs where `package` is different to the package of the current
|
resource URIs where `package` is different to the package of the current
|
||||||
application. This has always been documented to work, but wasn't
|
application. This has always been documented to work, but wasn't
|
||||||
correctly implemented until now.
|
correctly implemented until now.
|
||||||
|
* Normalize MIME types set by app code or read from media to be fully
|
||||||
|
lower-case.
|
||||||
* ExoPlayer:
|
* ExoPlayer:
|
||||||
* Add `PreloadMediaSource` and `PreloadMediaPeriod` that allows apps to
|
* Add `PreloadMediaSource` and `PreloadMediaPeriod` that allows apps to
|
||||||
preload the media source at a specific start position before playback,
|
preload the media source at a specific start position before playback,
|
||||||
|
@ -296,7 +296,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
|
|||||||
UUID uuid, @Nullable String licenseServerUrl, String mimeType, @Nullable byte[] data) {
|
UUID uuid, @Nullable String licenseServerUrl, String mimeType, @Nullable byte[] data) {
|
||||||
this.uuid = Assertions.checkNotNull(uuid);
|
this.uuid = Assertions.checkNotNull(uuid);
|
||||||
this.licenseServerUrl = licenseServerUrl;
|
this.licenseServerUrl = licenseServerUrl;
|
||||||
this.mimeType = Assertions.checkNotNull(mimeType);
|
this.mimeType = MimeTypes.normalizeMimeType(Assertions.checkNotNull(mimeType));
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,7 +396,7 @@ public final class Format implements Bundleable {
|
|||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public Builder setContainerMimeType(@Nullable String containerMimeType) {
|
public Builder setContainerMimeType(@Nullable String containerMimeType) {
|
||||||
this.containerMimeType = containerMimeType;
|
this.containerMimeType = MimeTypes.normalizeMimeType(containerMimeType);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -410,7 +410,7 @@ public final class Format implements Bundleable {
|
|||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public Builder setSampleMimeType(@Nullable String sampleMimeType) {
|
public Builder setSampleMimeType(@Nullable String sampleMimeType) {
|
||||||
this.sampleMimeType = sampleMimeType;
|
this.sampleMimeType = MimeTypes.normalizeMimeType(sampleMimeType);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1179,7 +1179,7 @@ public final class MediaItem implements Bundleable {
|
|||||||
@Nullable Object tag,
|
@Nullable Object tag,
|
||||||
long imageDurationMs) {
|
long imageDurationMs) {
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.mimeType = mimeType;
|
this.mimeType = MimeTypes.normalizeMimeType(mimeType);
|
||||||
this.drmConfiguration = drmConfiguration;
|
this.drmConfiguration = drmConfiguration;
|
||||||
this.adsConfiguration = adsConfiguration;
|
this.adsConfiguration = adsConfiguration;
|
||||||
this.streamKeys = streamKeys;
|
this.streamKeys = streamKeys;
|
||||||
@ -1612,7 +1612,7 @@ public final class MediaItem implements Bundleable {
|
|||||||
/** Sets the MIME type. */
|
/** Sets the MIME type. */
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public Builder setMimeType(@Nullable String mimeType) {
|
public Builder setMimeType(@Nullable String mimeType) {
|
||||||
this.mimeType = mimeType;
|
this.mimeType = MimeTypes.normalizeMimeType(mimeType);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1694,7 +1694,7 @@ public final class MediaItem implements Bundleable {
|
|||||||
@Nullable String label,
|
@Nullable String label,
|
||||||
@Nullable String id) {
|
@Nullable String id) {
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.mimeType = mimeType;
|
this.mimeType = MimeTypes.normalizeMimeType(mimeType);
|
||||||
this.language = language;
|
this.language = language;
|
||||||
this.selectionFlags = selectionFlags;
|
this.selectionFlags = selectionFlags;
|
||||||
this.roleFlags = roleFlags;
|
this.roleFlags = roleFlags;
|
||||||
|
@ -25,6 +25,7 @@ import com.google.common.base.Ascii;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import org.checkerframework.checker.nullness.qual.PolyNull;
|
||||||
import org.checkerframework.dataflow.qual.Pure;
|
import org.checkerframework.dataflow.qual.Pure;
|
||||||
|
|
||||||
/** Defines common MIME types and helper methods. */
|
/** 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.
|
* 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.
|
* @return The normalized MIME type, or the argument MIME type if its normalized form is unknown.
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@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) {
|
switch (mimeType) {
|
||||||
|
// Normalize uncommon versions of some audio MIME types to their standard equivalent.
|
||||||
case BASE_TYPE_AUDIO + "/x-flac":
|
case BASE_TYPE_AUDIO + "/x-flac":
|
||||||
return AUDIO_FLAC;
|
return AUDIO_FLAC;
|
||||||
case BASE_TYPE_AUDIO + "/mp3":
|
case BASE_TYPE_AUDIO + "/mp3":
|
||||||
return AUDIO_MPEG;
|
return AUDIO_MPEG;
|
||||||
case BASE_TYPE_AUDIO + "/x-wav":
|
case BASE_TYPE_AUDIO + "/x-wav":
|
||||||
return AUDIO_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:
|
default:
|
||||||
return mimeType;
|
return mimeType;
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import android.os.Parcelable;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.StreamKey;
|
import androidx.media3.common.StreamKey;
|
||||||
import androidx.media3.common.util.Assertions;
|
import androidx.media3.common.util.Assertions;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
@ -61,7 +62,7 @@ public final class DownloadRequest implements Parcelable {
|
|||||||
/** Sets the {@link DownloadRequest#mimeType}. */
|
/** Sets the {@link DownloadRequest#mimeType}. */
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public Builder setMimeType(@Nullable String mimeType) {
|
public Builder setMimeType(@Nullable String mimeType) {
|
||||||
this.mimeType = mimeType;
|
this.mimeType = MimeTypes.normalizeMimeType(mimeType);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +51,18 @@ public class DefaultMediaSourceFactoryTest {
|
|||||||
assertThat(mediaSource).isInstanceOf(HlsMediaSource.class);
|
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
|
@Test
|
||||||
public void createMediaSource_withTag_tagInSource() {
|
public void createMediaSource_withTag_tagInSource() {
|
||||||
Object tag = new Object();
|
Object tag = new Object();
|
||||||
|
@ -22,6 +22,7 @@ import android.os.Parcelable;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.MediaMetadata;
|
import androidx.media3.common.MediaMetadata;
|
||||||
import androidx.media3.common.Metadata;
|
import androidx.media3.common.Metadata;
|
||||||
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.ParsableByteArray;
|
import androidx.media3.common.util.ParsableByteArray;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import com.google.common.base.Charsets;
|
import com.google.common.base.Charsets;
|
||||||
@ -159,7 +160,8 @@ public final class PictureFrame implements Metadata.Entry {
|
|||||||
public static PictureFrame fromPictureBlock(ParsableByteArray pictureBlock) {
|
public static PictureFrame fromPictureBlock(ParsableByteArray pictureBlock) {
|
||||||
int pictureType = pictureBlock.readInt();
|
int pictureType = pictureBlock.readInt();
|
||||||
int mimeTypeLength = 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();
|
int descriptionLength = pictureBlock.readInt();
|
||||||
String description = pictureBlock.readString(descriptionLength);
|
String description = pictureBlock.readString(descriptionLength);
|
||||||
int width = pictureBlock.readInt();
|
int width = pictureBlock.readInt();
|
||||||
|
@ -18,6 +18,7 @@ package androidx.media3.extractor.metadata.id3;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Metadata;
|
import androidx.media3.common.Metadata;
|
||||||
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.Log;
|
import androidx.media3.common.util.Log;
|
||||||
import androidx.media3.common.util.ParsableBitArray;
|
import androidx.media3.common.util.ParsableBitArray;
|
||||||
import androidx.media3.common.util.ParsableByteArray;
|
import androidx.media3.common.util.ParsableByteArray;
|
||||||
@ -557,7 +558,8 @@ public final class Id3Decoder extends SimpleMetadataDecoder {
|
|||||||
id3Data.readBytes(data, 0, frameSize - 1);
|
id3Data.readBytes(data, 0, frameSize - 1);
|
||||||
|
|
||||||
int mimeTypeEndIndex = indexOfZeroByte(data, 0);
|
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 filenameStartIndex = mimeTypeEndIndex + 1;
|
||||||
int filenameEndIndex = indexOfTerminator(data, filenameStartIndex, encoding);
|
int filenameEndIndex = indexOfTerminator(data, filenameStartIndex, encoding);
|
||||||
|
@ -74,6 +74,7 @@ public final class TransformationRequest {
|
|||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public Builder setVideoMimeType(@Nullable String videoMimeType) {
|
public Builder setVideoMimeType(@Nullable String videoMimeType) {
|
||||||
|
videoMimeType = MimeTypes.normalizeMimeType(videoMimeType);
|
||||||
checkArgument(
|
checkArgument(
|
||||||
videoMimeType == null || MimeTypes.isVideo(videoMimeType),
|
videoMimeType == null || MimeTypes.isVideo(videoMimeType),
|
||||||
"Not a video MIME type: " + videoMimeType);
|
"Not a video MIME type: " + videoMimeType);
|
||||||
@ -100,6 +101,7 @@ public final class TransformationRequest {
|
|||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public Builder setAudioMimeType(@Nullable String audioMimeType) {
|
public Builder setAudioMimeType(@Nullable String audioMimeType) {
|
||||||
|
audioMimeType = MimeTypes.normalizeMimeType(audioMimeType);
|
||||||
checkArgument(
|
checkArgument(
|
||||||
audioMimeType == null || MimeTypes.isAudio(audioMimeType),
|
audioMimeType == null || MimeTypes.isAudio(audioMimeType),
|
||||||
"Not an audio MIME type: " + audioMimeType);
|
"Not an audio MIME type: " + audioMimeType);
|
||||||
|
@ -174,6 +174,7 @@ public final class Transformer {
|
|||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public Builder setAudioMimeType(String audioMimeType) {
|
public Builder setAudioMimeType(String audioMimeType) {
|
||||||
|
audioMimeType = MimeTypes.normalizeMimeType(audioMimeType);
|
||||||
checkArgument(MimeTypes.isAudio(audioMimeType), "Not an audio MIME type: " + audioMimeType);
|
checkArgument(MimeTypes.isAudio(audioMimeType), "Not an audio MIME type: " + audioMimeType);
|
||||||
this.audioMimeType = audioMimeType;
|
this.audioMimeType = audioMimeType;
|
||||||
return this;
|
return this;
|
||||||
@ -205,6 +206,7 @@ public final class Transformer {
|
|||||||
*/
|
*/
|
||||||
@CanIgnoreReturnValue
|
@CanIgnoreReturnValue
|
||||||
public Builder setVideoMimeType(String videoMimeType) {
|
public Builder setVideoMimeType(String videoMimeType) {
|
||||||
|
videoMimeType = MimeTypes.normalizeMimeType(videoMimeType);
|
||||||
checkArgument(MimeTypes.isVideo(videoMimeType), "Not a video MIME type: " + videoMimeType);
|
checkArgument(MimeTypes.isVideo(videoMimeType), "Not a video MIME type: " + videoMimeType);
|
||||||
this.videoMimeType = videoMimeType;
|
this.videoMimeType = videoMimeType;
|
||||||
return this;
|
return this;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user