From 0b8b03e46d1512672ab8ae714c9e0f19bdbc1fb2 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Sun, 7 Feb 2021 22:46:42 +0000 Subject: [PATCH] Add method to create a MediaFormat based on an ExoPlayer Format PiperOrigin-RevId: 356157035 --- .../mediacodec/MediaFormatUtil.java | 130 +++++++++++++- .../mediacodec/MediaFormatUtilTest.java | 159 ++++++++++++++++++ 2 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtilTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java index 0ed58db266..4038025a40 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java @@ -15,27 +15,94 @@ */ package com.google.android.exoplayer2.mediacodec; +import android.annotation.SuppressLint; +import android.media.AudioFormat; import android.media.MediaFormat; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.video.ColorInfo; import java.nio.ByteBuffer; import java.util.List; -/** Helper class for configuring {@link MediaFormat} instances. */ +/** Helper class containing utility methods for managing {@link MediaFormat} instances. */ public final class MediaFormatUtil { - private MediaFormatUtil() {} + /** + * Custom {@link MediaFormat} key associated with a float representing the ratio between a pixel's + * width and height. + */ + public static final String KEY_EXO_PIXEL_WIDTH_HEIGHT_RATIO_FLOAT = + "exo-pixel-width-height-ratio-float"; /** - * Sets a {@link MediaFormat} {@link String} value. + * Custom {@link MediaFormat} key associated with an integer representing the PCM encoding. + * + *

Equivalent to {@link MediaFormat#KEY_PCM_ENCODING}, except it allows additional + * ExoPlayer-specific values including {@link C#ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link + * C#ENCODING_PCM_24BIT}, and {@link C#ENCODING_PCM_32BIT}. + */ + public static final String KEY_EXO_PCM_ENCODING = "exo-pcm-encoding-int"; + + private static final int MAX_POWER_OF_TWO_INT = 1 << 30; + + /** + * Returns a {@link MediaFormat} representing the given ExoPlayer {@link Format}. + * + *

May include the following custom keys: + * + *

+ */ + @SuppressLint("InlinedApi") // Inlined MediaFormat keys. + public static MediaFormat createMediaFormatFromFormat(Format format) { + MediaFormat result = new MediaFormat(); + maybeSetInteger(result, MediaFormat.KEY_BIT_RATE, format.bitrate); + maybeSetInteger(result, MediaFormat.KEY_CHANNEL_COUNT, format.channelCount); + + maybeSetColorInfo(result, format.colorInfo); + + maybeSetString(result, MediaFormat.KEY_MIME, format.sampleMimeType); + maybeSetString(result, MediaFormat.KEY_CODECS_STRING, format.codecs); + maybeSetFloat(result, MediaFormat.KEY_FRAME_RATE, format.frameRate); + maybeSetInteger(result, MediaFormat.KEY_WIDTH, format.width); + maybeSetInteger(result, MediaFormat.KEY_HEIGHT, format.height); + + setCsdBuffers(result, format.initializationData); + maybeSetPcmEncoding(result, format.pcmEncoding); + maybeSetString(result, MediaFormat.KEY_LANGUAGE, format.language); + maybeSetInteger(result, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); + maybeSetInteger(result, MediaFormat.KEY_SAMPLE_RATE, format.sampleRate); + maybeSetInteger(result, MediaFormat.KEY_CAPTION_SERVICE_NUMBER, format.accessibilityChannel); + result.setInteger(MediaFormat.KEY_ROTATION, format.rotationDegrees); + + int selectionFlags = format.selectionFlags; + setBooleanAsInt( + result, MediaFormat.KEY_IS_AUTOSELECT, selectionFlags & C.SELECTION_FLAG_AUTOSELECT); + setBooleanAsInt(result, MediaFormat.KEY_IS_DEFAULT, selectionFlags & C.SELECTION_FLAG_DEFAULT); + setBooleanAsInt( + result, MediaFormat.KEY_IS_FORCED_SUBTITLE, selectionFlags & C.SELECTION_FLAG_FORCED); + + result.setInteger(MediaFormat.KEY_ENCODER_DELAY, format.encoderDelay); + result.setInteger(MediaFormat.KEY_ENCODER_PADDING, format.encoderPadding); + + maybeSetPixelAspectRatio(result, format.pixelWidthHeightRatio); + return result; + } + + /** + * Sets a {@link MediaFormat} {@link String} value. Does nothing if {@code value} is null. * * @param format The {@link MediaFormat} being configured. * @param key The key to set. * @param value The value to set. */ - public static void setString(MediaFormat format, String key, String value) { - format.setString(key, value); + public static void maybeSetString(MediaFormat format, String key, @Nullable String value) { + if (value != null) { + format.setString(key, value); + } } /** @@ -106,4 +173,57 @@ public final class MediaFormatUtil { maybeSetByteBuffer(format, MediaFormat.KEY_HDR_STATIC_INFO, colorInfo.hdrStaticInfo); } } + + // Internal methods. + + private static void setBooleanAsInt(MediaFormat format, String key, int value) { + format.setInteger(key, value != 0 ? 1 : 0); + } + + // Inlined MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH and MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT. + @SuppressLint("InlinedApi") + private static void maybeSetPixelAspectRatio( + MediaFormat mediaFormat, float pixelWidthHeightRatio) { + mediaFormat.setFloat(KEY_EXO_PIXEL_WIDTH_HEIGHT_RATIO_FLOAT, pixelWidthHeightRatio); + int pixelAspectRatioWidth = 1; + int pixelAspectRatioHeight = 1; + // ExoPlayer extractors output the pixel aspect ratio as a float. Do our best to recreate the + // pixel aspect ratio width and height by using a large power of two factor. + if (pixelWidthHeightRatio < 1.0f) { + pixelAspectRatioHeight = MAX_POWER_OF_TWO_INT; + pixelAspectRatioWidth = (int) (pixelWidthHeightRatio * pixelAspectRatioHeight); + } else if (pixelWidthHeightRatio > 1.0f) { + pixelAspectRatioWidth = MAX_POWER_OF_TWO_INT; + pixelAspectRatioHeight = (int) (pixelAspectRatioWidth / pixelWidthHeightRatio); + } + mediaFormat.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH, pixelAspectRatioWidth); + mediaFormat.setInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT, pixelAspectRatioHeight); + } + + @SuppressLint("InlinedApi") // Inlined KEY_PCM_ENCODING. + private static void maybeSetPcmEncoding( + MediaFormat mediaFormat, @C.PcmEncoding int exoPcmEncoding) { + if (exoPcmEncoding == Format.NO_VALUE) { + return; + } + int mediaFormatPcmEncoding; + maybeSetInteger(mediaFormat, KEY_EXO_PCM_ENCODING, exoPcmEncoding); + switch (exoPcmEncoding) { + case C.ENCODING_PCM_8BIT: + mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_8BIT; + break; + case C.ENCODING_PCM_16BIT: + mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_16BIT; + break; + case C.ENCODING_PCM_FLOAT: + mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_FLOAT; + break; + default: + // No matching value. Do nothing. + return; + } + mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, mediaFormatPcmEncoding); + } + + private MediaFormatUtil() {} } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtilTest.java new file mode 100644 index 0000000000..b0249ab710 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtilTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.mediacodec; + +import static com.google.common.truth.Truth.assertThat; + +import android.media.MediaFormat; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.video.ColorInfo; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** Unit tests for {@link MediaFormatUtil}. */ +@RunWith(AndroidJUnit4.class) +@Config(sdk = 29) // Allows using MediaFormat.getKeys() to make assertions over the expected keys. +public class MediaFormatUtilTest { + + @Test + public void createMediaFormatFromEmptyExoPlayerFormat_generatesExpectedEntries() { + MediaFormat mediaFormat = + MediaFormatUtil.createMediaFormatFromFormat(new Format.Builder().build()); + // Assert that no invalid keys are accidentally being populated. + assertThat(mediaFormat.getKeys()) + .containsExactly( + MediaFormatUtil.KEY_EXO_PIXEL_WIDTH_HEIGHT_RATIO_FLOAT, + MediaFormat.KEY_ENCODER_DELAY, + MediaFormat.KEY_ENCODER_PADDING, + MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH, + MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT, + MediaFormat.KEY_IS_DEFAULT, + MediaFormat.KEY_IS_FORCED_SUBTITLE, + MediaFormat.KEY_IS_AUTOSELECT, + MediaFormat.KEY_ROTATION); + assertThat(mediaFormat.getFloat(MediaFormatUtil.KEY_EXO_PIXEL_WIDTH_HEIGHT_RATIO_FLOAT)) + .isEqualTo(1.f); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_ENCODER_DELAY)).isEqualTo(0); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_ENCODER_PADDING)).isEqualTo(0); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH)).isEqualTo(1); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT)).isEqualTo(1); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_IS_DEFAULT)).isEqualTo(0); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE)).isEqualTo(0); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT)).isEqualTo(0); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_ROTATION)).isEqualTo(0); + } + + @Test + public void createMediaFormatFromPopulatedExoPlayerFormat_generatesExpectedMediaFormatEntries() { + Format format = + new Format.Builder() + .setAverageBitrate(1) + .setChannelCount(2) + .setColorInfo( + new ColorInfo( + /* colorSpace= */ C.COLOR_SPACE_BT601, + /* colorRange= */ C.COLOR_RANGE_FULL, + /* colorTransfer= */ C.COLOR_TRANSFER_HLG, + new byte[] {3})) + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setCodecs("avc.123") + .setFrameRate(4) + .setWidth(5) + .setHeight(6) + .setInitializationData(ImmutableList.of(new byte[] {7}, new byte[] {8})) + .setPcmEncoding(C.ENCODING_PCM_8BIT) + .setLanguage("en") + .setMaxInputSize(9) + .setRotationDegrees(10) + .setSampleRate(11) + .setAccessibilityChannel(12) + .setSelectionFlags( + C.SELECTION_FLAG_AUTOSELECT | C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED) + .setEncoderDelay(13) + .setEncoderPadding(14) + .setPixelWidthHeightRatio(.5f) + .build(); + MediaFormat mediaFormat = MediaFormatUtil.createMediaFormatFromFormat(format); + + assertThat(mediaFormat.getInteger(MediaFormat.KEY_BIT_RATE)).isEqualTo(format.bitrate); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)) + .isEqualTo(format.channelCount); + + ColorInfo colorInfo = Assertions.checkNotNull(format.colorInfo); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_COLOR_TRANSFER)) + .isEqualTo(colorInfo.colorTransfer); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_COLOR_RANGE)).isEqualTo(colorInfo.colorRange); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_COLOR_STANDARD)) + .isEqualTo(colorInfo.colorSpace); + assertThat(mediaFormat.getByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO).array()) + .isEqualTo(colorInfo.hdrStaticInfo); + + assertThat(mediaFormat.getString(MediaFormat.KEY_MIME)).isEqualTo(format.sampleMimeType); + assertThat(mediaFormat.getString(MediaFormat.KEY_CODECS_STRING)).isEqualTo(format.codecs); + assertThat(mediaFormat.getFloat(MediaFormat.KEY_FRAME_RATE)).isEqualTo(format.frameRate); + + assertThat(mediaFormat.getInteger(MediaFormat.KEY_WIDTH)).isEqualTo(format.width); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_HEIGHT)).isEqualTo(format.height); + + assertThat(mediaFormat.getByteBuffer("csd-0").array()) + .isEqualTo(format.initializationData.get(0)); + assertThat(mediaFormat.getByteBuffer("csd-1").array()) + .isEqualTo(format.initializationData.get(1)); + + assertThat(mediaFormat.getInteger(MediaFormat.KEY_PCM_ENCODING)).isEqualTo(format.pcmEncoding); + assertThat(mediaFormat.getInteger(MediaFormatUtil.KEY_EXO_PCM_ENCODING)) + .isEqualTo(format.pcmEncoding); + + assertThat(mediaFormat.getString(MediaFormat.KEY_LANGUAGE)).isEqualTo(format.language); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)) + .isEqualTo(format.maxInputSize); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_ROTATION)).isEqualTo(format.rotationDegrees); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)).isEqualTo(format.sampleRate); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_CAPTION_SERVICE_NUMBER)) + .isEqualTo(format.accessibilityChannel); + + assertThat(mediaFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT)).isNotEqualTo(0); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_IS_DEFAULT)).isNotEqualTo(0); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE)).isNotEqualTo(0); + + assertThat(mediaFormat.getInteger(MediaFormat.KEY_ENCODER_DELAY)) + .isEqualTo(format.encoderDelay); + assertThat(mediaFormat.getInteger(MediaFormat.KEY_ENCODER_PADDING)) + .isEqualTo(format.encoderPadding); + + float calculatedPixelAspectRatio = + (float) mediaFormat.getInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_WIDTH) + / mediaFormat.getInteger(MediaFormat.KEY_PIXEL_ASPECT_RATIO_HEIGHT); + assertThat(calculatedPixelAspectRatio).isWithin(.0001f).of(format.pixelWidthHeightRatio); + assertThat(mediaFormat.getFloat(MediaFormatUtil.KEY_EXO_PIXEL_WIDTH_HEIGHT_RATIO_FLOAT)) + .isEqualTo(format.pixelWidthHeightRatio); + } + + @Test + public void createMediaFormatWithExoPlayerPcmEncoding_containsExoPlayerSpecificEncoding() { + Format format = new Format.Builder().setPcmEncoding(C.ENCODING_PCM_32BIT).build(); + MediaFormat mediaFormat = MediaFormatUtil.createMediaFormatFromFormat(format); + assertThat(mediaFormat.getInteger(MediaFormatUtil.KEY_EXO_PCM_ENCODING)) + .isEqualTo(C.ENCODING_PCM_32BIT); + assertThat(mediaFormat.containsKey(MediaFormat.KEY_PCM_ENCODING)).isFalse(); + } +}