diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json
index b063b68e140..7afc3ab8eb4 100644
--- a/demos/main/src/main/assets/media.exolist.json
+++ b/demos/main/src/main/assets/media.exolist.json
@@ -787,6 +787,10 @@
{
"name": "MPEG-H HD (MP4, H265)",
"uri": "https://media.githubusercontent.com/media/Fraunhofer-IIS/mpegh-test-content/main/TRI_Fileset_17_514H_D1_D2_D3_O1_24bit1080p50.mp4"
+ },
+ {
+ "name": "xHE-AAC Test (MP4)",
+ "uri": "https://www2.iis.fraunhofer.de/AAC/Test_PRL-20.mp4"
}
]
},
diff --git a/libraries/common/src/main/java/androidx/media3/common/CodecParameter.java b/libraries/common/src/main/java/androidx/media3/common/CodecParameter.java
new file mode 100644
index 00000000000..2adabcfdd0f
--- /dev/null
+++ b/libraries/common/src/main/java/androidx/media3/common/CodecParameter.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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 androidx.media3.common;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
+
+import android.media.MediaFormat;
+import android.os.Build;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.media3.common.util.UnstableApi;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A parameter for configuring an underlying {@link android.media.MediaCodec}.
+ *
+ *
The key must be a key that is understood by the underlying decoder instance.
+ */
+@UnstableApi
+public final class CodecParameter {
+
+ /**
+ * Value types for a {@link CodecParameter}. One of {@link #TYPE_NULL}, {@link #TYPE_INT}, {@link
+ * #TYPE_LONG}, {@link #TYPE_FLOAT}, {@link #TYPE_STRING} or {@link #TYPE_BYTE_BUFFER}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @Target(TYPE_USE)
+ @IntDef({TYPE_NULL, TYPE_INT, TYPE_LONG, TYPE_FLOAT, TYPE_STRING, TYPE_BYTE_BUFFER})
+ public @interface ValueType {}
+
+ /**
+ * @see MediaFormat#TYPE_NULL
+ */
+ public static final int TYPE_NULL = 0;
+
+ /**
+ * @see MediaFormat#TYPE_INTEGER
+ */
+ public static final int TYPE_INT = 1;
+
+ /**
+ * @see MediaFormat#TYPE_LONG
+ */
+ public static final int TYPE_LONG = 2;
+
+ /**
+ * @see MediaFormat#TYPE_FLOAT
+ */
+ public static final int TYPE_FLOAT = 3;
+
+ /**
+ * @see MediaFormat#TYPE_STRING
+ */
+ public static final int TYPE_STRING = 4;
+
+ /**
+ * @see MediaFormat#TYPE_BYTE_BUFFER
+ */
+ public static final int TYPE_BYTE_BUFFER = 5;
+
+ /**
+ * @see MediaFormat#KEY_AAC_DRC_ALBUM_MODE
+ */
+ @RequiresApi(api = Build.VERSION_CODES.R)
+ public static final String KEY_AAC_DRC_ALBUM_MODE = MediaFormat.KEY_AAC_DRC_ALBUM_MODE;
+
+ /**
+ * @see MediaFormat#KEY_AAC_DRC_ATTENUATION_FACTOR
+ */
+ public static final String KEY_AAC_DRC_ATTENUATION_FACTOR =
+ MediaFormat.KEY_AAC_DRC_ATTENUATION_FACTOR;
+
+ /**
+ * @see MediaFormat#KEY_AAC_DRC_BOOST_FACTOR
+ */
+ public static final String KEY_AAC_DRC_BOOST_FACTOR = MediaFormat.KEY_AAC_DRC_BOOST_FACTOR;
+
+ /**
+ * @see MediaFormat#KEY_AAC_DRC_EFFECT_TYPE
+ */
+ @RequiresApi(api = Build.VERSION_CODES.P)
+ public static final String KEY_AAC_DRC_EFFECT_TYPE = MediaFormat.KEY_AAC_DRC_EFFECT_TYPE;
+
+ /**
+ * @see MediaFormat#KEY_AAC_DRC_TARGET_REFERENCE_LEVEL
+ */
+ public static final String KEY_AAC_DRC_TARGET_REFERENCE_LEVEL =
+ MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL;
+
+ /**
+ * @see MediaFormat#KEY_AAC_DRC_OUTPUT_LOUDNESS
+ */
+ @RequiresApi(api = Build.VERSION_CODES.R)
+ public static final String KEY_AAC_DRC_OUTPUT_LOUDNESS = MediaFormat.KEY_AAC_DRC_OUTPUT_LOUDNESS;
+
+ /**
+ * Key to set the MPEG-H output mode. The corresponding value must be of value type {@link
+ * ValueType#TYPE_INT}.
+ *
+ *
Possible values are:
+ *
+ *
+ * - 0 for PCM output (decoding the MPEG-H bitstream with a certain MPEG-H target layout CICP
+ * index)
+ *
- 1 for MPEG-H bitstream bypass (using IEC61937-13 with a sample rate factor of 4)
+ *
- 2 for MPEG-H bitstream bypass (using IEC61937-13 with a sample rate factor of 16)
+ *
+ */
+ public static final String KEY_MPEGH_OUTPUT_MODE = "mpegh-output-mode";
+
+ /**
+ * Key to set the MPEG-H target layout CICP index. The corresponding value must be of value type
+ * {@link ValueType#TYPE_INT}. It must be set before decoder initialization. A change during
+ * runtime does not have any effect.
+ *
+ * Supported values are: 0, 1, 2, 6, 8, 10, 12. A value of 0 tells the decoder to create
+ * binauralized output.
+ */
+ public static final String KEY_MPEGH_TARGET_LAYOUT = "mpegh-target-layout";
+
+ /**
+ * Key to set the MPEG-H UI configuration. The corresponding value must be of value type {@link
+ * ValueType#TYPE_STRING}. This key is returned from the MPEG-H UI manager.
+ */
+ public static final String KEY_MPEGH_UI_CONFIG = "mpegh-ui-config";
+
+ /**
+ * Key to set the MPEG-H UI command. The corresponding value must be of value type {@link
+ * ValueType#TYPE_STRING}. This key is passed to the MPEG-H UI manager.
+ */
+ public static final String KEY_MPEGH_UI_COMMAND = "mpegh-ui-command";
+
+ /**
+ * Key to set the MPEG-H UI persistence storage path. The corresponding value must be of value
+ * type {@link ValueType#TYPE_STRING}. This key is passed to the MPEG-H UI manager.
+ */
+ public static final String KEY_MPEGH_UI_PERSISTENCESTORAGE_PATH =
+ "mpegh-ui-persistencestorage-path";
+
+ /** The key of the codec parameter. */
+ public final String key;
+
+ /** The value of the codec parameter. */
+ public final @Nullable Object value;
+
+ /** The {@link ValueType} of the value object. */
+ public final @ValueType int valueType;
+
+ /**
+ * Creates an instance.
+ *
+ * @param key The key of the codec parameter.
+ * @param value The value of the codec parameter.
+ * @param valueType The {@link ValueType} of the value object.
+ */
+ public CodecParameter(String key, @Nullable Object value, @ValueType int valueType) {
+ this.key = key;
+ this.value = value;
+ this.valueType = valueType;
+ }
+}
diff --git a/libraries/common/src/main/java/androidx/media3/common/CodecParameters.java b/libraries/common/src/main/java/androidx/media3/common/CodecParameters.java
new file mode 100644
index 00000000000..84bc70ef5fb
--- /dev/null
+++ b/libraries/common/src/main/java/androidx/media3/common/CodecParameters.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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 androidx.media3.common;
+
+import android.media.MediaFormat;
+import android.os.Build;
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+import androidx.media3.common.util.UnstableApi;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** A collection of {@link CodecParameter} objects. */
+@UnstableApi
+public final class CodecParameters {
+
+ private final Map codecParameters;
+
+ /** Creates an instance. */
+ public CodecParameters() {
+ codecParameters = new HashMap<>();
+ }
+
+ /**
+ * Sets, replaces, or removes a codec parameter in the collection.
+ *
+ * If the {@link CodecParameter#valueType} is {@link CodecParameter#TYPE_NULL}, the parameter
+ * with the given key will be removed.
+ *
+ * @param param The {@link CodecParameter} to set, replace, or remove.
+ */
+ public void set(CodecParameter param) {
+ if (param.valueType == CodecParameter.TYPE_NULL) {
+ codecParameters.remove(param.key);
+ } else {
+ codecParameters.put(param.key, param);
+ }
+ }
+
+ /** Returns the map of codec parameters in this collection. */
+ public Map get() {
+ return codecParameters;
+ }
+
+ /**
+ * Returns a codec parameter from the collection by its key.
+ *
+ * @param key A string representing the key of the codec parameter.
+ * @return The requested {@link CodecParameter}, or {@code null} if not found.
+ */
+ @Nullable
+ public CodecParameter get(String key) {
+ return codecParameters.get(key);
+ }
+
+ /** Removes all codec parameters from the collection. */
+ public void clear() {
+ codecParameters.clear();
+ }
+
+ /**
+ * Converts the collection of codec parameters to a {@link Bundle}.
+ *
+ * @return A {@link Bundle} containing the codec parameters.
+ */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ for (Map.Entry entry : codecParameters.entrySet()) {
+ String key = entry.getKey();
+ CodecParameter param = entry.getValue();
+
+ switch (param.valueType) {
+ case CodecParameter.TYPE_INT:
+ bundle.putInt(key, (int) param.value);
+ break;
+ case CodecParameter.TYPE_LONG:
+ bundle.putLong(key, (long) param.value);
+ break;
+ case CodecParameter.TYPE_FLOAT:
+ bundle.putFloat(key, (float) param.value);
+ break;
+ case CodecParameter.TYPE_STRING:
+ bundle.putString(key, (String) param.value);
+ break;
+ case CodecParameter.TYPE_BYTE_BUFFER:
+ bundle.putByteArray(key, ((ByteBuffer) param.value).array());
+ break;
+ case CodecParameter.TYPE_NULL:
+ default:
+ break;
+ }
+ }
+ return bundle;
+ }
+
+ /**
+ * Populates this collection from the entries of a {@link MediaFormat}.
+ *
+ * @param mediaFormat The {@link MediaFormat} from which to populate this collection.
+ * @param filterKeys A list of {@link MediaFormat} entry keys that should be included. If {@code
+ * null}, all entries in the media format will be included (requires API 29+).
+ */
+ public void setFromMediaFormat(MediaFormat mediaFormat, @Nullable List filterKeys) {
+ if (filterKeys == null) {
+ if (Build.VERSION.SDK_INT >= 29) {
+ Set keys = mediaFormat.getKeys();
+ for (String key : keys) {
+ int type = mediaFormat.getValueTypeForKey(key);
+ @Nullable Object value;
+ int valueType;
+ switch (type) {
+ case MediaFormat.TYPE_INTEGER:
+ value = mediaFormat.getInteger(key);
+ valueType = CodecParameter.TYPE_INT;
+ break;
+ case MediaFormat.TYPE_LONG:
+ value = mediaFormat.getLong(key);
+ valueType = CodecParameter.TYPE_LONG;
+ break;
+ case MediaFormat.TYPE_FLOAT:
+ value = mediaFormat.getFloat(key);
+ valueType = CodecParameter.TYPE_FLOAT;
+ break;
+ case MediaFormat.TYPE_STRING:
+ value = mediaFormat.getString(key);
+ valueType = CodecParameter.TYPE_STRING;
+ break;
+ case MediaFormat.TYPE_BYTE_BUFFER:
+ value = mediaFormat.getByteBuffer(key);
+ valueType = CodecParameter.TYPE_BYTE_BUFFER;
+ break;
+ case MediaFormat.TYPE_NULL:
+ default:
+ continue;
+ }
+ codecParameters.put(key, new CodecParameter(key, value, valueType));
+ }
+ }
+ } else {
+ for (String key : filterKeys) {
+ if (mediaFormat.containsKey(key)) {
+ @Nullable Object value = null;
+ @CodecParameter.ValueType int type = CodecParameter.TYPE_NULL;
+ boolean success = false;
+ try {
+ value = mediaFormat.getInteger(key);
+ type = CodecParameter.TYPE_INT;
+ success = true;
+ } catch (Exception e) {
+ // Do nothing.
+ }
+
+ if (!success) {
+ try {
+ value = mediaFormat.getLong(key);
+ type = CodecParameter.TYPE_LONG;
+ success = true;
+ } catch (Exception e) {
+ // Do nothing.
+ }
+ }
+
+ if (!success) {
+ try {
+ value = mediaFormat.getFloat(key);
+ type = CodecParameter.TYPE_FLOAT;
+ success = true;
+ } catch (Exception e) {
+ // Do nothing.
+ }
+ }
+
+ if (!success) {
+ try {
+ value = mediaFormat.getString(key);
+ type = CodecParameter.TYPE_STRING;
+ success = true;
+ } catch (Exception e) {
+ // Do nothing.
+ }
+ }
+
+ if (!success) {
+ try {
+ value = mediaFormat.getByteBuffer(key);
+ type = CodecParameter.TYPE_BYTE_BUFFER;
+ success = true;
+ } catch (Exception e) {
+ // Do nothing.
+ }
+ }
+
+ if (success) {
+ codecParameters.put(key, new CodecParameter(key, value, type));
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds all parameters from this collection to a {@link MediaFormat}. Existing keys in the {@link
+ * MediaFormat} will be overwritten.
+ *
+ * @param mediaFormat The {@link MediaFormat} to which codec parameters should be added.
+ */
+ public void addToMediaFormat(MediaFormat mediaFormat) {
+ for (Map.Entry entry : codecParameters.entrySet()) {
+ String key = entry.getKey();
+ CodecParameter param = entry.getValue();
+
+ switch (param.valueType) {
+ case CodecParameter.TYPE_INT:
+ mediaFormat.setInteger(key, (int) param.value);
+ break;
+ case CodecParameter.TYPE_LONG:
+ mediaFormat.setLong(key, (long) param.value);
+ break;
+ case CodecParameter.TYPE_FLOAT:
+ mediaFormat.setFloat(key, (float) param.value);
+ break;
+ case CodecParameter.TYPE_STRING:
+ mediaFormat.setString(key, (String) param.value);
+ break;
+ case CodecParameter.TYPE_BYTE_BUFFER:
+ mediaFormat.setByteBuffer(key, (ByteBuffer) param.value);
+ break;
+ case CodecParameter.TYPE_NULL:
+ default:
+ break;
+ }
+ }
+ }
+}
diff --git a/libraries/common/src/main/java/androidx/media3/common/CodecParametersChangeListener.java b/libraries/common/src/main/java/androidx/media3/common/CodecParametersChangeListener.java
new file mode 100644
index 00000000000..449ea361efc
--- /dev/null
+++ b/libraries/common/src/main/java/androidx/media3/common/CodecParametersChangeListener.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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 androidx.media3.common;
+
+import androidx.media3.common.util.UnstableApi;
+import java.util.List;
+
+/** A listener for changes to codec parameters. */
+@UnstableApi
+public interface CodecParametersChangeListener {
+
+ /**
+ * Called when one or more codec parameters have changed.
+ *
+ * @param codecParameters A {@link CodecParameters} instance containing the set of changed
+ * parameters.
+ */
+ void onCodecParametersChanged(CodecParameters codecParameters);
+
+ /**
+ * Returns a list of parameter keys that the listener is interested in.
+ *
+ * The player will only call {@link #onCodecParametersChanged} with parameters whose keys are
+ * included in this list. This allows the player to avoid unnecessary work querying parameters
+ * that the listener does not care about. If the list is empty, the listener will not be called.
+ *
+ * @return An {@link List} of the requested keys.
+ */
+ List getFilterKeys();
+}
diff --git a/libraries/common/src/test/java/androidx/media3/common/CodecParametersTest.java b/libraries/common/src/test/java/androidx/media3/common/CodecParametersTest.java
new file mode 100644
index 00000000000..b33d295fc08
--- /dev/null
+++ b/libraries/common/src/test/java/androidx/media3/common/CodecParametersTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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 androidx.media3.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.media.MediaFormat;
+import android.os.Bundle;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link CodecParameters}. */
+@RunWith(AndroidJUnit4.class)
+public class CodecParametersTest {
+
+ @Test
+ public void setAndGet_singleParameter_retrievesCorrectly() {
+ CodecParameters codecParameters = new CodecParameters();
+ CodecParameter param = new CodecParameter("test-key", 123, CodecParameter.TYPE_INT);
+ codecParameters.set(param);
+
+ assertThat(codecParameters.get("test-key")).isSameInstanceAs(param);
+ }
+
+ @Test
+ public void set_nullValueType_removesParameter() {
+ CodecParameters codecParameters = new CodecParameters();
+ codecParameters.set(new CodecParameter("test-key", 123, CodecParameter.TYPE_INT));
+
+ assertThat(codecParameters.get("test-key")).isNotNull();
+
+ codecParameters.set(new CodecParameter("test-key", null, CodecParameter.TYPE_NULL));
+
+ assertThat(codecParameters.get("test-key")).isNull();
+ }
+
+ @Test
+ public void clear_removesAllParameters() {
+ CodecParameters codecParameters = new CodecParameters();
+ codecParameters.set(new CodecParameter("key1", 1, CodecParameter.TYPE_INT));
+ codecParameters.set(new CodecParameter("key2", "value2", CodecParameter.TYPE_STRING));
+
+ assertThat(codecParameters.get().isEmpty()).isFalse();
+
+ codecParameters.clear();
+
+ assertThat(codecParameters.get().isEmpty()).isTrue();
+ }
+
+ @Test
+ public void toBundle_withAllValueTypes_createsCorrectBundle() {
+ CodecParameters codecParameters = new CodecParameters();
+ byte[] testBytes = new byte[] {1, 2, 3};
+ codecParameters.set(new CodecParameter("key-int", 10, CodecParameter.TYPE_INT));
+ codecParameters.set(new CodecParameter("key-long", 20L, CodecParameter.TYPE_LONG));
+ codecParameters.set(new CodecParameter("key-float", 30.0f, CodecParameter.TYPE_FLOAT));
+ codecParameters.set(new CodecParameter("key-string", "forty", CodecParameter.TYPE_STRING));
+ codecParameters.set(
+ new CodecParameter(
+ "key-byte-buffer", ByteBuffer.wrap(testBytes), CodecParameter.TYPE_BYTE_BUFFER));
+
+ Bundle bundle = codecParameters.toBundle();
+
+ assertThat(bundle.getInt("key-int")).isEqualTo(10);
+ assertThat(bundle.getLong("key-long")).isEqualTo(20L);
+ assertThat(bundle.getFloat("key-float")).isEqualTo(30.0f);
+ assertThat(bundle.getString("key-string")).isEqualTo("forty");
+ assertThat(bundle.getByteArray("key-byte-buffer")).isEqualTo(testBytes);
+ }
+
+ @Test
+ public void addToMediaFormat_withAllValueTypes_addsToFormat() {
+ CodecParameters codecParameters = new CodecParameters();
+ byte[] testBytes = new byte[] {1, 2, 3};
+ codecParameters.set(new CodecParameter("key-int", 10, CodecParameter.TYPE_INT));
+ codecParameters.set(new CodecParameter("key-long", 20L, CodecParameter.TYPE_LONG));
+ codecParameters.set(new CodecParameter("key-float", 30.0f, CodecParameter.TYPE_FLOAT));
+ codecParameters.set(new CodecParameter("key-string", "forty", CodecParameter.TYPE_STRING));
+ codecParameters.set(
+ new CodecParameter(
+ "key-byte-buffer", ByteBuffer.wrap(testBytes), CodecParameter.TYPE_BYTE_BUFFER));
+ MediaFormat mediaFormat = new MediaFormat();
+
+ codecParameters.addToMediaFormat(mediaFormat);
+
+ assertThat(mediaFormat.getInteger("key-int")).isEqualTo(10);
+ assertThat(mediaFormat.getLong("key-long")).isEqualTo(20L);
+ assertThat(mediaFormat.getFloat("key-float")).isEqualTo(30.0f);
+ assertThat(mediaFormat.getString("key-string")).isEqualTo("forty");
+ assertThat(mediaFormat.getByteBuffer("key-byte-buffer")).isEqualTo(ByteBuffer.wrap(testBytes));
+ }
+
+ @Test
+ public void setFromMediaFormat_withFilter_extractsCorrectValues() {
+ MediaFormat mediaFormat = new MediaFormat();
+ mediaFormat.setInteger("key-int", 10);
+ mediaFormat.setString("key-string", "hello");
+ mediaFormat.setLong("key-long-ignored", 99L);
+ ArrayList filterKeys = new ArrayList<>();
+ filterKeys.add("key-int");
+ filterKeys.add("key-string");
+ CodecParameters codecParameters = new CodecParameters();
+
+ codecParameters.setFromMediaFormat(mediaFormat, filterKeys);
+
+ assertThat(codecParameters.get().size()).isEqualTo(2);
+ CodecParameter intParam = codecParameters.get("key-int");
+ assertThat(intParam.value).isEqualTo(10);
+ assertThat(intParam.valueType).isEqualTo(CodecParameter.TYPE_INT);
+ CodecParameter stringParam = codecParameters.get("key-string");
+ assertThat(stringParam.value).isEqualTo("hello");
+ assertThat(stringParam.valueType).isEqualTo(CodecParameter.TYPE_STRING);
+ assertThat(codecParameters.get("key-long-ignored")).isNull();
+ }
+}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java
index 525400c5921..de4a925d4f1 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java
@@ -38,6 +38,8 @@
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.AuxEffectInfo;
import androidx.media3.common.C;
+import androidx.media3.common.CodecParameter;
+import androidx.media3.common.CodecParametersChangeListener;
import androidx.media3.common.Effect;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
@@ -1961,4 +1963,29 @@ void setVideoChangeFrameRateStrategy(
*/
@UnstableApi
void setImageOutput(@Nullable ImageOutput imageOutput);
+
+ /**
+ * Sets a parameter on the underlying {@link android.media.MediaCodec} instances, or clears all
+ * previously set parameters.
+ *
+ * This method is asynchronous. The parameter will be applied to the renderers on the playback
+ * thread. If the underlying decoder does not support the parameter, it will be ignored.
+ *
+ * @param codecParameter The {@link CodecParameter} to set, or {@code null} to clear all
+ * previously set parameters.
+ */
+ @UnstableApi
+ void setCodecParameter(@Nullable CodecParameter codecParameter);
+
+ /**
+ * Sets a listener for codec parameter changes.
+ *
+ *
This method is asynchronous. The listener will be set on the renderers on the playback
+ * thread.
+ *
+ * @param listener The {@link CodecParametersChangeListener} to set, or {@code null} to remove a
+ * previously set listener.
+ */
+ @UnstableApi
+ void setCodecParametersChangeListener(@Nullable CodecParametersChangeListener listener);
}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java
index a2c7889b290..dabcecf7ba5 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java
@@ -27,6 +27,8 @@
import static androidx.media3.exoplayer.Renderer.MSG_SET_AUX_EFFECT_INFO;
import static androidx.media3.exoplayer.Renderer.MSG_SET_CAMERA_MOTION_LISTENER;
import static androidx.media3.exoplayer.Renderer.MSG_SET_CHANGE_FRAME_RATE_STRATEGY;
+import static androidx.media3.exoplayer.Renderer.MSG_SET_CODEC_PARAMETER;
+import static androidx.media3.exoplayer.Renderer.MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER;
import static androidx.media3.exoplayer.Renderer.MSG_SET_IMAGE_OUTPUT;
import static androidx.media3.exoplayer.Renderer.MSG_SET_PREFERRED_AUDIO_DEVICE;
import static androidx.media3.exoplayer.Renderer.MSG_SET_PRIORITY;
@@ -60,6 +62,8 @@
import androidx.media3.common.BasePlayer;
import androidx.media3.common.C;
import androidx.media3.common.C.TrackType;
+import androidx.media3.common.CodecParameter;
+import androidx.media3.common.CodecParametersChangeListener;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.Effect;
import androidx.media3.common.Format;
@@ -1980,6 +1984,26 @@ public void setImageOutput(@Nullable ImageOutput imageOutput) {
sendRendererMessage(TRACK_TYPE_IMAGE, MSG_SET_IMAGE_OUTPUT, imageOutput);
}
+ @Override
+ public void setCodecParameter(@Nullable CodecParameter codecParameter) {
+ verifyApplicationThread();
+ for (Renderer renderer : renderers) {
+ createMessage(renderer).setType(MSG_SET_CODEC_PARAMETER).setPayload(codecParameter).send();
+ }
+ }
+
+ @Override
+ public void setCodecParametersChangeListener(
+ @Nullable CodecParametersChangeListener codecParametersChangeListener) {
+ verifyApplicationThread();
+ for (Renderer renderer : renderers) {
+ createMessage(renderer)
+ .setType(MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER)
+ .setPayload(codecParametersChangeListener)
+ .send();
+ }
+ }
+
@SuppressWarnings("deprecation") // Calling deprecated methods.
/* package */ void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) {
this.throwsWhenUsingWrongThread = throwsWhenUsingWrongThread;
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java
index 0e77cf99fc1..aef96280b8f 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java
@@ -24,6 +24,8 @@
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.AuxEffectInfo;
import androidx.media3.common.C;
+import androidx.media3.common.CodecParameter;
+import androidx.media3.common.CodecParametersChangeListener;
import androidx.media3.common.Effect;
import androidx.media3.common.Format;
import androidx.media3.common.Player;
@@ -195,7 +197,8 @@ interface WakeupListener {
* #MSG_SET_AUX_EFFECT_INFO}, {@link #MSG_SET_VIDEO_FRAME_METADATA_LISTENER}, {@link
* #MSG_SET_CAMERA_MOTION_LISTENER}, {@link #MSG_SET_SKIP_SILENCE_ENABLED}, {@link
* #MSG_SET_AUDIO_SESSION_ID}, {@link #MSG_SET_WAKEUP_LISTENER}, {@link #MSG_SET_VIDEO_EFFECTS},
- * {@link #MSG_SET_VIDEO_OUTPUT_RESOLUTION} or {@link #MSG_SET_IMAGE_OUTPUT}. May also be an
+ * {@link #MSG_SET_VIDEO_OUTPUT_RESOLUTION}, {@link #MSG_SET_IMAGE_OUTPUT}, {@link
+ * #MSG_SET_CODEC_PARAMETER} or {@link #MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER}. May also be an
* app-defined value (see {@link #MSG_CUSTOM_BASE}).
*/
@Documented
@@ -220,7 +223,9 @@ interface WakeupListener {
MSG_SET_IMAGE_OUTPUT,
MSG_SET_PRIORITY,
MSG_TRANSFER_RESOURCES,
- MSG_SET_SCRUBBING_MODE
+ MSG_SET_SCRUBBING_MODE,
+ MSG_SET_CODEC_PARAMETER,
+ MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER
})
public @interface MessageType {}
@@ -372,6 +377,22 @@ interface WakeupListener {
*/
int MSG_SET_SCRUBBING_MODE = 18;
+ /**
+ * The type of a message that can be passed to renderers via {@link
+ * ExoPlayer#createMessage(PlayerMessage.Target)}. The message payload is a {@link
+ * CodecParameter}.
+ *
+ *
If the receiving renderer does not support the codec parameter, then it should ignore it
+ */
+ int MSG_SET_CODEC_PARAMETER = 19;
+
+ /**
+ * The type of a message that can be passed to renderers via {@link
+ * ExoPlayer#createMessage(PlayerMessage.Target)}. The message payload should be a {@link
+ * CodecParametersChangeListener} instance, or null.
+ */
+ int MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER = 20;
+
/**
* Applications or extensions may define custom {@code MSG_*} constants that can be passed to
* renderers. These custom constants must be greater than or equal to this value.
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java
index 115b4f8ccdf..c267c196236 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java
@@ -29,6 +29,8 @@
import androidx.media3.common.AuxEffectInfo;
import androidx.media3.common.BasePlayer;
import androidx.media3.common.C;
+import androidx.media3.common.CodecParameter;
+import androidx.media3.common.CodecParametersChangeListener;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.Effect;
import androidx.media3.common.Format;
@@ -1349,6 +1351,19 @@ public void setImageOutput(@Nullable ImageOutput imageOutput) {
player.setImageOutput(imageOutput);
}
+ @Override
+ public void setCodecParameter(@Nullable CodecParameter codecParameter) {
+ blockUntilConstructorFinished();
+ player.setCodecParameter(codecParameter);
+ }
+
+ @Override
+ public void setCodecParametersChangeListener(
+ @Nullable CodecParametersChangeListener codecParametersChangeListener) {
+ blockUntilConstructorFinished();
+ player.setCodecParametersChangeListener(codecParametersChangeListener);
+ }
+
/* package */ void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) {
blockUntilConstructorFinished();
player.setThrowsWhenUsingWrongThread(throwsWhenUsingWrongThread);
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java
index 551106338a3..484fd23325c 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java
@@ -39,6 +39,9 @@
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.AuxEffectInfo;
import androidx.media3.common.C;
+import androidx.media3.common.CodecParameter;
+import androidx.media3.common.CodecParameters;
+import androidx.media3.common.CodecParametersChangeListener;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
@@ -113,6 +116,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private final AudioSink audioSink;
@Nullable private final LoudnessCodecController loudnessCodecController;
+ /**
+ * A holder for codec parameters that are persistently applied to the underlying {@link
+ * MediaCodec}.
+ *
+ * @see ExoPlayer#setCodecParameter(CodecParameter)
+ */
+ private final CodecParameters codecParameters;
+
private int codecMaxInputSize;
private boolean codecNeedsDiscardChannelsWorkaround;
private boolean codecNeedsVorbisToAndroidChannelMappingWorkaround;
@@ -129,6 +140,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private boolean isStarted;
private long nextBufferToWritePresentationTimeUs;
+ /**
+ * A listener for codec parameter changes.
+ *
+ * @see ExoPlayer#setCodecParametersChangeListener(CodecParametersChangeListener)
+ */
+ @Nullable private CodecParametersChangeListener codecParametersChangeListener;
+
/**
* @param context A context.
* @param mediaCodecSelector A decoder selector.
@@ -308,6 +326,7 @@ public MediaCodecAudioRenderer(
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
nextBufferToWritePresentationTimeUs = C.TIME_UNSET;
audioSink.setListener(new AudioSinkListener());
+ codecParameters = new CodecParameters();
}
@Override
@@ -598,6 +617,11 @@ protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder)
@Override
protected void onOutputFormatChanged(Format format, @Nullable MediaFormat mediaFormat)
throws ExoPlaybackException {
+ if (codecParametersChangeListener != null && mediaFormat != null) {
+ CodecParameters params = new CodecParameters();
+ params.setFromMediaFormat(mediaFormat, codecParametersChangeListener.getFilterKeys());
+ codecParametersChangeListener.onCodecParametersChanged(params);
+ }
Format audioSinkInputFormat;
@Nullable int[] channelMap = null;
if (decryptOnlyCodecFormat != null) { // Direct playback with a codec for decryption.
@@ -918,6 +942,20 @@ public void handleMessage(@MessageType int messageType, @Nullable Object message
rendererPriority = (int) checkNotNull(message);
updateCodecImportance();
break;
+ case MSG_SET_CODEC_PARAMETER:
+ if (message == null) {
+ this.codecParameters.clear();
+ } else {
+ this.codecParameters.set((CodecParameter) message);
+ }
+ @Nullable MediaCodecAdapter codec = getCodec();
+ if (codec != null) {
+ codec.setParameters(codecParameters.toBundle());
+ }
+ break;
+ case MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER:
+ this.codecParametersChangeListener = (CodecParametersChangeListener) message;
+ break;
default:
super.handleMessage(messageType, message);
break;
@@ -1037,6 +1075,8 @@ protected MediaFormat getMediaFormat(
if (SDK_INT >= 35) {
mediaFormat.setInteger(MediaFormat.KEY_IMPORTANCE, max(0, -rendererPriority));
}
+
+ codecParameters.addToMediaFormat(mediaFormat);
return mediaFormat;
}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
index 9354ce96b67..401c99527a7 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
@@ -104,6 +104,9 @@
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
+import androidx.media3.common.CodecParameter;
+import androidx.media3.common.CodecParameters;
+import androidx.media3.common.CodecParametersChangeListener;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
@@ -15991,6 +15994,69 @@ public void settingMediaItems_withEmptyListAfterReady_transitionsToEnded() throw
assertThat(stateAfterClearingPlaylist).isEqualTo(Player.STATE_ENDED);
}
+ @Test
+ public void setCodecParameter_whenReady_notifiesListener() throws Exception {
+ RenderersFactory renderersFactory =
+ new DefaultRenderersFactory(context) {
+ @Override
+ protected void buildAudioRenderers(
+ Context context,
+ @ExtensionRendererMode int extensionRendererMode,
+ MediaCodecSelector mediaCodecSelector,
+ boolean enableDecoderFallback,
+ AudioSink audioSink,
+ Handler eventHandler,
+ AudioRendererEventListener eventListener,
+ ArrayList out) {
+ out.add(
+ new FakeRenderer(C.TRACK_TYPE_AUDIO) {
+ @Nullable private CodecParametersChangeListener listener;
+
+ @Override
+ public void handleMessage(@MessageType int messageType, @Nullable Object message)
+ throws ExoPlaybackException {
+ if (messageType == Renderer.MSG_SET_CODEC_PARAMETER) {
+ if (listener != null) {
+ CodecParameters params = new CodecParameters();
+ if (message instanceof CodecParameter) {
+ params.set((CodecParameter) message);
+ }
+ eventHandler.post(() -> listener.onCodecParametersChanged(params));
+ }
+ } else if (messageType == Renderer.MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER) {
+ this.listener = (CodecParametersChangeListener) message;
+ }
+ super.handleMessage(messageType, message);
+ }
+ });
+ }
+ };
+ ExoPlayer player =
+ new TestExoPlayerBuilder(context).setRenderersFactory(renderersFactory).build();
+ player.setMediaSource(new FakeMediaSource());
+ player.prepare();
+ advance(player).untilState(Player.STATE_READY);
+ CodecParametersChangeListener mockListener = mock(CodecParametersChangeListener.class);
+ player.setCodecParametersChangeListener(mockListener);
+ CodecParameter testParameter = new CodecParameter("test-key", 123, CodecParameter.TYPE_INT);
+
+ player.setCodecParameter(testParameter);
+ advance(player).untilPendingCommandsAreFullyHandled();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(CodecParameters.class);
+ verify(mockListener).onCodecParametersChanged(paramsCaptor.capture());
+ assertThat(paramsCaptor.getValue().get("test-key")).isSameInstanceAs(testParameter);
+
+ player.setCodecParameter(null);
+ advance(player).untilPendingCommandsAreFullyHandled();
+
+ verify(mockListener, times(2)).onCodecParametersChanged(paramsCaptor.capture());
+ assertThat(paramsCaptor.getValue().get()).isEmpty();
+
+ player.release();
+ }
+
// Internal methods.
private void addWatchAsSystemFeature() {
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/MediaCodecAudioRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/MediaCodecAudioRendererTest.java
index e814477a84e..6b663f11ca6 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/MediaCodecAudioRendererTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/MediaCodecAudioRendererTest.java
@@ -27,23 +27,31 @@
import static org.mockito.ArgumentMatchers.longThat;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
+import android.media.MediaCodec;
import android.media.MediaFormat;
+import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.SystemClock;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
+import androidx.media3.common.CodecParameter;
+import androidx.media3.common.CodecParameters;
+import androidx.media3.common.CodecParametersChangeListener;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.util.Clock;
import androidx.media3.exoplayer.ExoPlaybackException;
+import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.RendererCapabilities;
import androidx.media3.exoplayer.RendererCapabilities.Capabilities;
import androidx.media3.exoplayer.RendererConfiguration;
@@ -51,6 +59,7 @@
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.mediacodec.DefaultMediaCodecAdapterFactory;
+import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter;
import androidx.media3.exoplayer.mediacodec.MediaCodecInfo;
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
import androidx.media3.exoplayer.source.MediaSource;
@@ -61,7 +70,9 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Before;
import org.junit.Rule;
@@ -1379,6 +1390,158 @@ public void isReady_returnsTrueOnceAudioSinkHasPendingData() throws Exception {
assertThat(isReadyAfterPendingData).isTrue();
}
+ @Test
+ public void setCodecParameter_onEnabledRenderer_appliesParameterToLiveCodec() throws Exception {
+ MediaCodecAdapter mockCodecAdapter = mock(MediaCodecAdapter.class);
+ when(mockCodecAdapter.dequeueInputBufferIndex()).thenReturn(MediaCodec.INFO_TRY_AGAIN_LATER);
+ when(mockCodecAdapter.dequeueOutputBufferIndex(any()))
+ .thenReturn(MediaCodec.INFO_TRY_AGAIN_LATER);
+ MediaCodecAdapter.Factory mockCodecAdapterFactory = configuration -> mockCodecAdapter;
+ mediaCodecAudioRenderer =
+ new MediaCodecAudioRenderer(
+ ApplicationProvider.getApplicationContext(),
+ mockCodecAdapterFactory,
+ mediaCodecSelector,
+ /* enableDecoderFallback= */ false,
+ new Handler(Looper.getMainLooper()),
+ audioRendererEventListener,
+ audioSink);
+ mediaCodecAudioRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT);
+ FakeSampleStream fakeSampleStream =
+ new FakeSampleStream(
+ new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
+ /* mediaSourceEventDispatcher= */ null,
+ DrmSessionManager.DRM_UNSUPPORTED,
+ new DrmSessionEventListener.EventDispatcher(),
+ AUDIO_AAC,
+ ImmutableList.of(
+ oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM));
+ fakeSampleStream.writeData(/* startPositionUs= */ 0);
+ mediaCodecAudioRenderer.enable(
+ RendererConfiguration.DEFAULT,
+ new Format[] {AUDIO_AAC},
+ fakeSampleStream,
+ /* positionUs= */ 0,
+ /* joining= */ false,
+ /* mayRenderStartOfStream= */ false,
+ /* startPositionUs= */ 0,
+ /* offsetUs= */ 0,
+ new MediaSource.MediaPeriodId(new Object()));
+ mediaCodecAudioRenderer.start();
+ mediaCodecAudioRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000);
+ CodecParameter testParameter = new CodecParameter("test-key", 42, CodecParameter.TYPE_INT);
+
+ mediaCodecAudioRenderer.handleMessage(Renderer.MSG_SET_CODEC_PARAMETER, testParameter);
+
+ ArgumentCaptor bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+ verify(mockCodecAdapter).setParameters(bundleCaptor.capture());
+ assertThat(bundleCaptor.getValue().getInt("test-key")).isEqualTo(42);
+
+ mediaCodecAudioRenderer.handleMessage(Renderer.MSG_SET_CODEC_PARAMETER, /* message= */ null);
+
+ verify(mockCodecAdapter, times(2)).setParameters(bundleCaptor.capture());
+ assertThat(bundleCaptor.getValue().isEmpty()).isTrue();
+ }
+
+ @Test
+ public void setCodecParameter_beforeCodecInitialized_parameterIsAppliedDuringConfiguration()
+ throws Exception {
+ MediaCodecAdapter mockCodecAdapter = mock(MediaCodecAdapter.class);
+ when(mockCodecAdapter.dequeueInputBufferIndex()).thenReturn(MediaCodec.INFO_TRY_AGAIN_LATER);
+ when(mockCodecAdapter.dequeueOutputBufferIndex(any()))
+ .thenReturn(MediaCodec.INFO_TRY_AGAIN_LATER);
+ MediaCodecAdapter.Factory mockCodecAdapterFactory = mock(MediaCodecAdapter.Factory.class);
+ when(mockCodecAdapterFactory.createAdapter(any())).thenReturn(mockCodecAdapter);
+ mediaCodecAudioRenderer =
+ new MediaCodecAudioRenderer(
+ ApplicationProvider.getApplicationContext(),
+ mockCodecAdapterFactory,
+ mediaCodecSelector,
+ /* enableDecoderFallback= */ false,
+ new Handler(Looper.getMainLooper()),
+ audioRendererEventListener,
+ audioSink);
+ mediaCodecAudioRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT);
+ CodecParameter testParameter = new CodecParameter("test-key", 42, CodecParameter.TYPE_INT);
+ mediaCodecAudioRenderer.handleMessage(Renderer.MSG_SET_CODEC_PARAMETER, testParameter);
+ FakeSampleStream fakeSampleStream =
+ new FakeSampleStream(
+ new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
+ /* mediaSourceEventDispatcher= */ null,
+ DrmSessionManager.DRM_UNSUPPORTED,
+ new DrmSessionEventListener.EventDispatcher(),
+ AUDIO_AAC,
+ ImmutableList.of(
+ oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM));
+ fakeSampleStream.writeData(/* startPositionUs= */ 0);
+ mediaCodecAudioRenderer.enable(
+ RendererConfiguration.DEFAULT,
+ new Format[] {AUDIO_AAC},
+ fakeSampleStream,
+ /* positionUs= */ 0,
+ /* joining= */ false,
+ /* mayRenderStartOfStream= */ false,
+ /* startPositionUs= */ 0,
+ /* offsetUs= */ 0,
+ new MediaSource.MediaPeriodId(new Object()));
+
+ mediaCodecAudioRenderer.render(/* positionUs= */ 0, /* elapsedRealTimeUs= */ 0);
+
+ ArgumentCaptor configurationCaptor =
+ ArgumentCaptor.forClass(MediaCodecAdapter.Configuration.class);
+ verify(mockCodecAdapterFactory).createAdapter(configurationCaptor.capture());
+ MediaFormat mediaFormat = configurationCaptor.getValue().mediaFormat;
+ assertThat(mediaFormat.getInteger("test-key")).isEqualTo(42);
+ }
+
+ @Test
+ public void
+ onOutputFormatChanged_whenFormatContainsParameter_notifiesListenerWithCorrectParameters()
+ throws Exception {
+ CodecParametersChangeListener mockListener = mock(CodecParametersChangeListener.class);
+ ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(CodecParameters.class);
+ ArrayList filterKeys = new ArrayList<>();
+ filterKeys.add("test-key");
+ when(mockListener.getFilterKeys()).thenReturn(filterKeys);
+ mediaCodecAudioRenderer.handleMessage(
+ Renderer.MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER, mockListener);
+ FakeSampleStream fakeSampleStream =
+ new FakeSampleStream(
+ new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
+ /* mediaSourceEventDispatcher= */ null,
+ DrmSessionManager.DRM_UNSUPPORTED,
+ new DrmSessionEventListener.EventDispatcher(),
+ AUDIO_AAC,
+ ImmutableList.of(
+ oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM));
+ fakeSampleStream.writeData(/* startPositionUs= */ 0);
+ mediaCodecAudioRenderer.enable(
+ RendererConfiguration.DEFAULT,
+ new Format[] {AUDIO_AAC},
+ fakeSampleStream,
+ /* positionUs= */ 0,
+ /* joining= */ false,
+ /* mayRenderStartOfStream= */ false,
+ /* startPositionUs= */ 0,
+ /* offsetUs= */ 0,
+ new MediaSource.MediaPeriodId(new Object()));
+ mediaCodecAudioRenderer.start();
+ mediaCodecAudioRenderer.render(/* positionUs= */ 0, SystemClock.elapsedRealtime() * 1000);
+ MediaFormat mediaFormat = new MediaFormat();
+ mediaFormat.setInteger("test-key", 123);
+ mediaFormat.setString("ignored-key", "some-value");
+ mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 2);
+ mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);
+
+ mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ verify(mockListener).onCodecParametersChanged(paramsCaptor.capture());
+ CodecParameters capturedParams = paramsCaptor.getValue();
+ assertThat(Objects.requireNonNull(capturedParams.get("test-key")).value).isEqualTo(123);
+ assertThat(capturedParams.get("ignored-key")).isNull();
+ }
+
private void maybeIdleAsynchronousMediaCodecAdapterThreads() {
if (queueingThread != null) {
shadowOf(queueingThread.getLooper()).idle();
diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java
index 6b22d5d95c9..47d0feb7f89 100644
--- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java
+++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java
@@ -21,6 +21,8 @@
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.AuxEffectInfo;
import androidx.media3.common.C;
+import androidx.media3.common.CodecParameter;
+import androidx.media3.common.CodecParametersChangeListener;
import androidx.media3.common.Effect;
import androidx.media3.common.Format;
import androidx.media3.common.PriorityTaskManager;
@@ -418,4 +420,15 @@ public boolean isReleased() {
public void setImageOutput(@Nullable ImageOutput imageOutput) {
throw new UnsupportedOperationException();
}
+
+ @Override
+ public void setCodecParameter(@Nullable CodecParameter codecParameter) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setCodecParametersChangeListener(
+ CodecParametersChangeListener codecParametersChangeListener) {
+ throw new UnsupportedOperationException();
+ }
}