From ac062c792b7dd284d947cc91a90b6771ffcb02c8 Mon Sep 17 00:00:00 2001 From: rohitjoins Date: Fri, 8 Dec 2023 12:27:02 +0100 Subject: [PATCH 1/3] - add communication path between player and decoder/renderer - add application example for xHE-AAC --- demos/main/src/main/assets/media.exolist.json | 4 + .../media3/demo/main/PlayerActivity.java | 94 +++++++ .../media3/common/CodecParameter.java | 165 +++++++++++ .../media3/common/CodecParameters.java | 256 ++++++++++++++++++ .../common/CodecParametersChangeListener.java | 24 ++ .../androidx/media3/exoplayer/ExoPlayer.java | 11 + .../media3/exoplayer/ExoPlayerImpl.java | 22 ++ .../androidx/media3/exoplayer/Renderer.java | 25 +- .../media3/exoplayer/SimpleExoPlayer.java | 17 ++ .../audio/MediaCodecAudioRenderer.java | 35 +++ .../media3/test/utils/StubExoPlayer.java | 13 + 11 files changed, 663 insertions(+), 3 deletions(-) create mode 100644 libraries/common/src/main/java/androidx/media3/common/CodecParameter.java create mode 100644 libraries/common/src/main/java/androidx/media3/common/CodecParameters.java create mode 100644 libraries/common/src/main/java/androidx/media3/common/CodecParametersChangeListener.java 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/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java index 449e4312a4d..1ec2c990506 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java @@ -34,12 +34,16 @@ import androidx.appcompat.app.AppCompatActivity; 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.ErrorMessageProvider; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.Tracks; +import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSchemeDataSource; @@ -61,6 +65,7 @@ import androidx.media3.ui.PlayerView; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -261,6 +266,7 @@ protected void setContentView() { /** * @return Whether initialization was successful. */ + @OptIn(markerClass = UnstableApi.class) protected boolean initializePlayer() { Intent intent = getIntent(); if (player == null) { @@ -277,6 +283,56 @@ protected boolean initializePlayer() { setRenderersFactory( playerBuilder, intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false)); player = playerBuilder.build(); + + // --------------------------- TESTING CODE ONLY -- REMOVE AGAIN ---------------------------- + player.setCodecParametersChangeListener(new CodecParametersChangeListener() { + @Override + public void onCodecParametersChanged(CodecParameters codecParameters) { + HashMap parameters = codecParameters.get(); + for (String key : parameters.keySet()) { + Log.e("PlayerActivity", "key = " + key + " value = " + parameters.get(key)); + } + } + + @Override + public ArrayList getFilterKeys() { + ArrayList filterKeys = new ArrayList<>(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + filterKeys.add(CodecParameter.KEY_AAC_DRC_OUTPUT_LOUDNESS); + } + return filterKeys; + } + }); + + CodecParameter codecParameter; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // For testing set initial targetLoudness to -16 LUFS + codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL, 64, + CodecParameter.VALUETYPE_INT); + player.setCodecParameter(codecParameter); + // For testing set initial BoostFactor to 32 + codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_BOOST_FACTOR, 32, + CodecParameter.VALUETYPE_INT); + player.setCodecParameter(codecParameter); + // For testing set initial AttenuationFactor to 16 + codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_ATTENUATION_FACTOR, 16, + CodecParameter.VALUETYPE_INT); + player.setCodecParameter(codecParameter); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // For testing set initial EffectType to NOISY_ENVIRONMENT + codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_EFFECT_TYPE, 2, + CodecParameter.VALUETYPE_INT); + player.setCodecParameter(codecParameter); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // For testing set initial AlbumMode to ENABLED + codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_ALBUM_MODE, 1, + CodecParameter.VALUETYPE_INT); + player.setCodecParameter(codecParameter); + } + // --------------------------- TESTING CODE ONLY -- REMOVE AGAIN ---------------------------- + player.setTrackSelectionParameters(trackSelectionParameters); player.addListener(new PlayerEventListener()); player.addAnalyticsListener(new EventLogger()); @@ -298,6 +354,44 @@ protected boolean initializePlayer() { player.setRepeatMode(IntentUtil.parseRepeatModeExtra(repeatModeExtra)); } updateButtonVisibility(); + + // --------------------------- TESTING CODE ONLY -- REMOVE AGAIN ---------------------------- + // Simulate sleep so the MPEG-D DRC can change during runtime + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + CodecParameter codecParameter; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // Testing a change to the targetLoudness during codec runtime (set to -24 LUFS) + codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL, 96, + CodecParameter.VALUETYPE_INT); + player.setCodecParameter(codecParameter); + // Testing a change to the boost factor during codec runtime (set to 96) + codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_BOOST_FACTOR, 96, + CodecParameter.VALUETYPE_INT); + player.setCodecParameter(codecParameter); + // Testing a change to the attenuation factor during codec runtime (set to 64) + codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_ATTENUATION_FACTOR, 64, + CodecParameter.VALUETYPE_INT); + player.setCodecParameter(codecParameter); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // Testing a change to the EffectType during codec runtime (set to OFF) + codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_EFFECT_TYPE, -1, + CodecParameter.VALUETYPE_INT); + player.setCodecParameter(codecParameter); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Testing a change to the album mode during codec runtime (set to DISABLED) + codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_ALBUM_MODE, 0, + CodecParameter.VALUETYPE_INT); + player.setCodecParameter(codecParameter); + } + // --------------------------- TESTING CODE ONLY -- REMOVE AGAIN ---------------------------- + return true; } 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..5379f59d9bf --- /dev/null +++ b/libraries/common/src/main/java/androidx/media3/common/CodecParameter.java @@ -0,0 +1,165 @@ +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 java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A {@link CodecParameter} holds the key, value and the value type for signalling a parameter change to a + * decoder. The key can be an arbitrary string which must be known by the decoder instance. + */ +public class CodecParameter { + + private static final String TAG = "CodecParameter"; + + /** + * @see MediaFormat#KEY_AAC_DRC_ALBUM_MODE + */ + @RequiresApi(api = Build.VERSION_CODES.R) + public final static String KEY_AAC_DRC_ALBUM_MODE = MediaFormat.KEY_AAC_DRC_ALBUM_MODE; + + /** + * @see MediaFormat#KEY_AAC_DRC_ATTENUATION_FACTOR + */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public final static String KEY_AAC_DRC_ATTENUATION_FACTOR = MediaFormat.KEY_AAC_DRC_ATTENUATION_FACTOR; + + /** + * @see MediaFormat#KEY_AAC_DRC_BOOST_FACTOR + */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public final static 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 final static String KEY_AAC_DRC_EFFECT_TYPE = MediaFormat.KEY_AAC_DRC_EFFECT_TYPE; + + /** + * @see MediaFormat#KEY_AAC_DRC_TARGET_REFERENCE_LEVEL + */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public final static 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 final static 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#VALUETYPE_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 final static 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#VALUETYPE_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 final static 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#VALUETYPE_STRING}. + * This key is returned from the MPEG-H UI manager. + */ + public final static 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#VALUETYPE_STRING}. + * This key is passed to the MPEG-H UI manager. + */ + public final static 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#VALUETYPE_STRING}. + * This key is passed to the MPEG-H UI manager. + */ + public final static String KEY_MPEGH_UI_PERSISTENCESTORAGE_PATH = "mpegh-ui-persistencestorage-path"; + + /** + * @see MediaFormat#TYPE_NULL + */ + public static final int VALUETYPE_NULL = 0; // MediaFormat.TYPE_NULL; + /** + * @see MediaFormat#TYPE_INTEGER + */ + public static final int VALUETYPE_INT = 1; // MediaFormat.TYPE_INTEGER; + /** + * @see MediaFormat#TYPE_LONG + */ + public static final int VALUETYPE_LONG = 2; // MediaFormat.TYPE_LONG; + /** + * @see MediaFormat#TYPE_FLOAT + */ + public static final int VALUETYPE_FLOAT = 3; // MediaFormat.TYPE_FLOAT; + /** + * @see MediaFormat#TYPE_STRING + */ + public static final int VALUETYPE_STRING = 4; // MediaFormat.TYPE_STRING; + /** + * @see MediaFormat#TYPE_BYTE_BUFFER + */ + public static final int VALUETYPE_BYTE_BUFFER = 5; // MediaFormat.TYPE_BYTE_BUFFER; + + /** + * Value types for a {@link CodecParameter}. + * One of {@link #VALUETYPE_NULL}, {@link #VALUETYPE_INT}, {@link #VALUETYPE_LONG}, + * {@link #VALUETYPE_FLOAT}, {@link #VALUETYPE_STRING} or {@link #VALUETYPE_BYTE_BUFFER}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + VALUETYPE_NULL, + VALUETYPE_INT, + VALUETYPE_LONG, + VALUETYPE_FLOAT, + VALUETYPE_STRING, + VALUETYPE_BYTE_BUFFER + }) + public @interface ValueType { + + } + + + public String key; + public @Nullable Object value; + public @ValueType int valueType; + + + /** + * Creates a new codec parameter. + * + * @param key A string holding the key of the codec parameter. + * @param value An object representing the value of the codec parameter. + * @param valueType The value type 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..cf8ca087d47 --- /dev/null +++ b/libraries/common/src/main/java/androidx/media3/common/CodecParameters.java @@ -0,0 +1,256 @@ +package androidx.media3.common; + +import android.media.MediaFormat; +import android.os.Build; +import android.os.Bundle; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * A {@link CodecParameters} instance provides the possibility to store/cache a set of {@link CodecParameter}. + * Furthermore, some conversion functions ({@link CodecParameters#setFromMediaFormat}, {@link CodecParameters#addToMediaFormat} + * and {@link CodecParameters#toBundle}) are given. + */ +public class CodecParameters { + + private static final String TAG = "CodecParameters"; + + /** + * A hashmap for storing several codec parameters. + */ + private final HashMap codecParameters; + + /** + * Creates a new codec parameters instance. + */ + public CodecParameters() { + codecParameters = new HashMap<>(); + } + + + /** + * Set, replace or remove a codec parameter in/from the cache. + * If the value type of the codec parameter is of value type VALUETYPE_NULL, the + * cached codec parameter will be removed. + * + * @param param A {@link CodecParameter} to be set, replaced or removed in/from the cache. + */ + public void set(CodecParameter param) { + if (param.valueType == CodecParameter.VALUETYPE_NULL) { + codecParameters.remove(param.key); + } else { + codecParameters.put(param.key, param); + } + } + + /** + * Get the hashmap of the cached codec parameters. + * + * @returns The hashmap of cached codec parameters. + */ + public HashMap get() { + return codecParameters; + } + + /** + * Get a cached codec parameter according to its key. + * + * @param key A string representing the key of the codec parameter. + * @returns The requested {@link CodecParameter}. + */ + public @Nullable CodecParameter get(String key) { + return codecParameters.get(key); + } + + /** + * Remove all cached codec parameters from the cache. + */ + public void clear() { + codecParameters.clear(); + } + + /** + * Convert the cached codec parameters to a bundle. + * + * @return A {@link Bundle} containing the cached 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.VALUETYPE_INT: + bundle.putInt(key, (int) param.value); + break; + case CodecParameter.VALUETYPE_LONG: + bundle.putLong(key, (long) param.value); + break; + case CodecParameter.VALUETYPE_FLOAT: + bundle.putFloat(key, (float) param.value); + break; + case CodecParameter.VALUETYPE_STRING: + bundle.putString(key, (String) param.value); + break; + case CodecParameter.VALUETYPE_BYTE_BUFFER: + bundle.putByteArray(key, ((ByteBuffer) param.value).array()); + break; + case CodecParameter.VALUETYPE_NULL: + default: + break; + } + } + return bundle; + } + + /** + * Convert the entries of a MediaFormat to codec parameters and cache them. + * + * @param mediaFormat The media format to be converted and cached. + * @param filterKeys A list of media format entry keys which should be converted and cached. + * If null, all entries in the media format will be cached. + */ + public void setFromMediaFormat(MediaFormat mediaFormat, @Nullable ArrayList filterKeys) { + CodecParameter param; + if (filterKeys == null) { + if (Build.VERSION.SDK_INT >= 29) { + Set keys = mediaFormat.getKeys(); + for (String key : keys) { + int type = mediaFormat.getValueTypeForKey(key); + switch (type) { + case MediaFormat.TYPE_INTEGER: + param = new CodecParameter(key, mediaFormat.getInteger(key), + CodecParameter.VALUETYPE_INT); + codecParameters.put(key, param); + break; + case MediaFormat.TYPE_LONG: + param = new CodecParameter(key, mediaFormat.getLong(key), + CodecParameter.VALUETYPE_LONG); + codecParameters.put(key, param); + break; + case MediaFormat.TYPE_FLOAT: + param = new CodecParameter(key, mediaFormat.getFloat(key), + CodecParameter.VALUETYPE_FLOAT); + codecParameters.put(key, param); + break; + case MediaFormat.TYPE_STRING: + param = new CodecParameter(key, mediaFormat.getString(key), + CodecParameter.VALUETYPE_STRING); + codecParameters.put(key, param); + break; + case MediaFormat.TYPE_BYTE_BUFFER: + param = new CodecParameter(key, mediaFormat.getByteBuffer(key), + CodecParameter.VALUETYPE_BYTE_BUFFER); + codecParameters.put(key, param); + break; + case MediaFormat.TYPE_NULL: + default: + break; + } + } + } else { + // not implemented + } + } else { + for (String key : filterKeys) { + if (mediaFormat.containsKey(key)) { + @Nullable Object value = null; + @CodecParameter.ValueType int type = CodecParameter.VALUETYPE_NULL; + boolean success = false; + try { + value = mediaFormat.getInteger(key); + type = CodecParameter.VALUETYPE_INT; + success = true; + } catch (Exception e) { + e.printStackTrace(); + } + + if (!success) { + try { + value = mediaFormat.getLong(key); + type = CodecParameter.VALUETYPE_LONG; + success = true; + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (!success) { + try { + value = mediaFormat.getFloat(key); + type = CodecParameter.VALUETYPE_FLOAT; + success = true; + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (!success) { + try { + value = mediaFormat.getString(key); + type = CodecParameter.VALUETYPE_STRING; + success = true; + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (!success) { + try { + value = mediaFormat.getByteBuffer(key); + type = CodecParameter.VALUETYPE_BYTE_BUFFER; + success = true; + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (success) { + param = new CodecParameter(key, value, type); + codecParameters.put(param.key, param); + } + } + } + } + } + + + /** + * Append/overwrite the entries of a MediaFormat by all cached codec parameters. + * + * @param mediaFormat The media format 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.VALUETYPE_INT: + mediaFormat.setInteger(key, (int) param.value); + break; + case CodecParameter.VALUETYPE_LONG: + mediaFormat.setLong(key, (long) param.value); + break; + case CodecParameter.VALUETYPE_FLOAT: + mediaFormat.setFloat(key, (float) param.value); + break; + case CodecParameter.VALUETYPE_STRING: + mediaFormat.setString(key, (String) param.value); + break; + case CodecParameter.VALUETYPE_BYTE_BUFFER: + mediaFormat.setByteBuffer(key, (ByteBuffer) param.value); + break; + case CodecParameter.VALUETYPE_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..5a226bfd3bc --- /dev/null +++ b/libraries/common/src/main/java/androidx/media3/common/CodecParametersChangeListener.java @@ -0,0 +1,24 @@ +package androidx.media3.common; + +import java.util.ArrayList; + +/** + * A codec parameter change listener provides the ability to be notified about state/parameter + * changes in a codec instance. + */ +public interface CodecParametersChangeListener { + + /** + * Inform about changes to the output buffer format. + * + * @param codecParameters A set of codec parameters. + */ + void onCodecParametersChanged(CodecParameters codecParameters); + + /** + * Get a list of key values which should be returned by {@link #onCodecParametersChanged(CodecParameters)} + * + * @returns An ArrayList of the requested keys. + */ + ArrayList getFilterKeys(); +} 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..62c5d12c787 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,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.CodecParametersChangeListener; +import androidx.media3.common.DeviceInfo; import androidx.media3.common.Effect; import androidx.media3.common.Format; import androidx.media3.common.MediaItem; @@ -1961,4 +1964,12 @@ void setVideoChangeFrameRateStrategy( */ @UnstableApi void setImageOutput(@Nullable ImageOutput imageOutput); + + /** Set the CodecParameters */ + @UnstableApi + void setCodecParameter(CodecParameter codecParameter); + + /** Set the CodecParametersChangeListener */ + @UnstableApi + void setCodecParametersChangeListener(@Nullable CodecParametersChangeListener codecParametersChangeListener); } 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..08960a4cd4c 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,24 @@ public void setImageOutput(@Nullable ImageOutput imageOutput) { sendRendererMessage(TRACK_TYPE_IMAGE, MSG_SET_IMAGE_OUTPUT, imageOutput); } + @Override + public void setCodecParameter(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..53e9d12c349 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,8 +197,9 @@ 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 - * app-defined value (see {@link #MSG_CUSTOM_BASE}). + * {@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 @Retention(RetentionPolicy.SOURCE) @@ -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,20 @@ 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..d78a5dd5f9f 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,21 @@ public void setImageOutput(@Nullable ImageOutput imageOutput) { player.setImageOutput(imageOutput); } + @Override + public void setCodecParameter(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..b92e2d045c3 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 @@ -32,6 +32,7 @@ import android.media.MediaFormat; import android.os.Build; import android.os.Bundle; +import android.os.Build; import android.os.Handler; import android.util.Pair; import androidx.annotation.CallSuper; @@ -39,6 +40,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; @@ -129,6 +133,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean isStarted; private long nextBufferToWritePresentationTimeUs; + /** {@link CodecParameters} instance used for caching several {@link CodecParameter} **/ + private final CodecParameters codecParameters = new CodecParameters(); + @Nullable protected CodecParametersChangeListener codecParametersChangeListener = null; + + /** * @param context A context. * @param mediaCodecSelector A decoder selector. @@ -598,6 +607,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 +932,24 @@ 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) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + 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 +1069,9 @@ protected MediaFormat getMediaFormat( if (SDK_INT >= 35) { mediaFormat.setInteger(MediaFormat.KEY_IMPORTANCE, max(0, -rendererPriority)); } + + codecParameters.addToMediaFormat(mediaFormat); + return mediaFormat; } 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(); + } } From 213ed2f0479c19c0edf806f833d7efd5da4c5935 Mon Sep 17 00:00:00 2001 From: rohitjoins Date: Mon, 6 May 2024 20:32:05 +0200 Subject: [PATCH 2/3] fixed enumaration values after rebase from main --- .../src/main/java/androidx/media3/demo/main/PlayerActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java index 1ec2c990506..a2f9932d6a1 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java @@ -290,7 +290,7 @@ protected boolean initializePlayer() { public void onCodecParametersChanged(CodecParameters codecParameters) { HashMap parameters = codecParameters.get(); for (String key : parameters.keySet()) { - Log.e("PlayerActivity", "key = " + key + " value = " + parameters.get(key)); + Log.e("PlayerActivity", "key = " + key + " value = " + parameters.get(key).value); } } From e904f87b87c9e5db535e13250d6146c6dd77ef82 Mon Sep 17 00:00:00 2001 From: rohitjoins Date: Fri, 19 Sep 2025 16:17:33 +0100 Subject: [PATCH 3/3] Refactor and add tests --- .../media3/demo/main/PlayerActivity.java | 94 --------- .../media3/common/CodecParameter.java | 188 ++++++++++-------- .../media3/common/CodecParameters.java | 173 ++++++++-------- .../common/CodecParametersChangeListener.java | 39 +++- .../media3/common/CodecParametersTest.java | 131 ++++++++++++ .../androidx/media3/exoplayer/ExoPlayer.java | 26 ++- .../media3/exoplayer/ExoPlayerImpl.java | 8 +- .../androidx/media3/exoplayer/Renderer.java | 12 +- .../media3/exoplayer/SimpleExoPlayer.java | 4 +- .../audio/MediaCodecAudioRenderer.java | 27 ++- .../media3/exoplayer/ExoPlayerTest.java | 66 ++++++ .../audio/MediaCodecAudioRendererTest.java | 163 +++++++++++++++ 12 files changed, 624 insertions(+), 307 deletions(-) create mode 100644 libraries/common/src/test/java/androidx/media3/common/CodecParametersTest.java diff --git a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java index a2f9932d6a1..449e4312a4d 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java @@ -34,16 +34,12 @@ import androidx.appcompat.app.AppCompatActivity; 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.ErrorMessageProvider; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.Tracks; -import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSchemeDataSource; @@ -65,7 +61,6 @@ import androidx.media3.ui.PlayerView; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -266,7 +261,6 @@ protected void setContentView() { /** * @return Whether initialization was successful. */ - @OptIn(markerClass = UnstableApi.class) protected boolean initializePlayer() { Intent intent = getIntent(); if (player == null) { @@ -283,56 +277,6 @@ protected boolean initializePlayer() { setRenderersFactory( playerBuilder, intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false)); player = playerBuilder.build(); - - // --------------------------- TESTING CODE ONLY -- REMOVE AGAIN ---------------------------- - player.setCodecParametersChangeListener(new CodecParametersChangeListener() { - @Override - public void onCodecParametersChanged(CodecParameters codecParameters) { - HashMap parameters = codecParameters.get(); - for (String key : parameters.keySet()) { - Log.e("PlayerActivity", "key = " + key + " value = " + parameters.get(key).value); - } - } - - @Override - public ArrayList getFilterKeys() { - ArrayList filterKeys = new ArrayList<>(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - filterKeys.add(CodecParameter.KEY_AAC_DRC_OUTPUT_LOUDNESS); - } - return filterKeys; - } - }); - - CodecParameter codecParameter; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // For testing set initial targetLoudness to -16 LUFS - codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL, 64, - CodecParameter.VALUETYPE_INT); - player.setCodecParameter(codecParameter); - // For testing set initial BoostFactor to 32 - codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_BOOST_FACTOR, 32, - CodecParameter.VALUETYPE_INT); - player.setCodecParameter(codecParameter); - // For testing set initial AttenuationFactor to 16 - codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_ATTENUATION_FACTOR, 16, - CodecParameter.VALUETYPE_INT); - player.setCodecParameter(codecParameter); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - // For testing set initial EffectType to NOISY_ENVIRONMENT - codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_EFFECT_TYPE, 2, - CodecParameter.VALUETYPE_INT); - player.setCodecParameter(codecParameter); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // For testing set initial AlbumMode to ENABLED - codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_ALBUM_MODE, 1, - CodecParameter.VALUETYPE_INT); - player.setCodecParameter(codecParameter); - } - // --------------------------- TESTING CODE ONLY -- REMOVE AGAIN ---------------------------- - player.setTrackSelectionParameters(trackSelectionParameters); player.addListener(new PlayerEventListener()); player.addAnalyticsListener(new EventLogger()); @@ -354,44 +298,6 @@ public ArrayList getFilterKeys() { player.setRepeatMode(IntentUtil.parseRepeatModeExtra(repeatModeExtra)); } updateButtonVisibility(); - - // --------------------------- TESTING CODE ONLY -- REMOVE AGAIN ---------------------------- - // Simulate sleep so the MPEG-D DRC can change during runtime - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - CodecParameter codecParameter; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // Testing a change to the targetLoudness during codec runtime (set to -24 LUFS) - codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL, 96, - CodecParameter.VALUETYPE_INT); - player.setCodecParameter(codecParameter); - // Testing a change to the boost factor during codec runtime (set to 96) - codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_BOOST_FACTOR, 96, - CodecParameter.VALUETYPE_INT); - player.setCodecParameter(codecParameter); - // Testing a change to the attenuation factor during codec runtime (set to 64) - codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_ATTENUATION_FACTOR, 64, - CodecParameter.VALUETYPE_INT); - player.setCodecParameter(codecParameter); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - // Testing a change to the EffectType during codec runtime (set to OFF) - codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_EFFECT_TYPE, -1, - CodecParameter.VALUETYPE_INT); - player.setCodecParameter(codecParameter); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // Testing a change to the album mode during codec runtime (set to DISABLED) - codecParameter = new CodecParameter(CodecParameter.KEY_AAC_DRC_ALBUM_MODE, 0, - CodecParameter.VALUETYPE_INT); - player.setCodecParameter(codecParameter); - } - // --------------------------- TESTING CODE ONLY -- REMOVE AGAIN ---------------------------- - return true; } diff --git a/libraries/common/src/main/java/androidx/media3/common/CodecParameter.java b/libraries/common/src/main/java/androidx/media3/common/CodecParameter.java index 5379f59d9bf..2adabcfdd0f 100644 --- a/libraries/common/src/main/java/androidx/media3/common/CodecParameter.java +++ b/libraries/common/src/main/java/androidx/media3/common/CodecParameter.java @@ -1,161 +1,175 @@ +/* + * 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 {@link CodecParameter} holds the key, value and the value type for signalling a parameter change to a - * decoder. The key can be an arbitrary string which must be known by the decoder instance. + * A parameter for configuring an underlying {@link android.media.MediaCodec}. + * + *

The key must be a key that is understood by the underlying decoder instance. */ -public class CodecParameter { - - private static final String TAG = "CodecParameter"; +@UnstableApi +public final class CodecParameter { /** - * @see MediaFormat#KEY_AAC_DRC_ALBUM_MODE + * 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}. */ - @RequiresApi(api = Build.VERSION_CODES.R) - public final static String KEY_AAC_DRC_ALBUM_MODE = MediaFormat.KEY_AAC_DRC_ALBUM_MODE; + @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#KEY_AAC_DRC_ATTENUATION_FACTOR + * @see MediaFormat#TYPE_NULL */ - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public final static String KEY_AAC_DRC_ATTENUATION_FACTOR = MediaFormat.KEY_AAC_DRC_ATTENUATION_FACTOR; + public static final int TYPE_NULL = 0; /** - * @see MediaFormat#KEY_AAC_DRC_BOOST_FACTOR + * @see MediaFormat#TYPE_INTEGER */ - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public final static String KEY_AAC_DRC_BOOST_FACTOR = MediaFormat.KEY_AAC_DRC_BOOST_FACTOR; + public static final int TYPE_INT = 1; /** - * @see MediaFormat#KEY_AAC_DRC_EFFECT_TYPE + * @see MediaFormat#TYPE_LONG */ - @RequiresApi(api = Build.VERSION_CODES.P) - public final static String KEY_AAC_DRC_EFFECT_TYPE = MediaFormat.KEY_AAC_DRC_EFFECT_TYPE; + public static final int TYPE_LONG = 2; /** - * @see MediaFormat#KEY_AAC_DRC_TARGET_REFERENCE_LEVEL + * @see MediaFormat#TYPE_FLOAT */ - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public final static String KEY_AAC_DRC_TARGET_REFERENCE_LEVEL = MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL; + public static final int TYPE_FLOAT = 3; /** - * @see MediaFormat#KEY_AAC_DRC_OUTPUT_LOUDNESS + * @see MediaFormat#TYPE_STRING */ - @RequiresApi(api = Build.VERSION_CODES.R) - public final static String KEY_AAC_DRC_OUTPUT_LOUDNESS = MediaFormat.KEY_AAC_DRC_OUTPUT_LOUDNESS; - + public static final int TYPE_STRING = 4; /** - * Key to set the MPEG-H output mode. - * The corresponding value must be of value type {@link ValueType#VALUETYPE_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) + * @see MediaFormat#TYPE_BYTE_BUFFER */ - public final static String KEY_MPEGH_OUTPUT_MODE = "mpegh-output-mode"; + public static final int TYPE_BYTE_BUFFER = 5; /** - * Key to set the MPEG-H target layout CICP index. - * The corresponding value must be of value type {@link ValueType#VALUETYPE_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. + * @see MediaFormat#KEY_AAC_DRC_ALBUM_MODE */ - public final static String KEY_MPEGH_TARGET_LAYOUT = "mpegh-target-layout"; + @RequiresApi(api = Build.VERSION_CODES.R) + public static final String KEY_AAC_DRC_ALBUM_MODE = MediaFormat.KEY_AAC_DRC_ALBUM_MODE; /** - * Key to set the MPEG-H UI configuration. - * The corresponding value must be of value type {@link ValueType#VALUETYPE_STRING}. - * This key is returned from the MPEG-H UI manager. + * @see MediaFormat#KEY_AAC_DRC_ATTENUATION_FACTOR */ - public final static String KEY_MPEGH_UI_CONFIG = "mpegh-ui-config"; + public static final String KEY_AAC_DRC_ATTENUATION_FACTOR = + MediaFormat.KEY_AAC_DRC_ATTENUATION_FACTOR; /** - * Key to set the MPEG-H UI command. - * The corresponding value must be of value type {@link ValueType#VALUETYPE_STRING}. - * This key is passed to the MPEG-H UI manager. + * @see MediaFormat#KEY_AAC_DRC_BOOST_FACTOR */ - public final static String KEY_MPEGH_UI_COMMAND = "mpegh-ui-command"; + public static final String KEY_AAC_DRC_BOOST_FACTOR = MediaFormat.KEY_AAC_DRC_BOOST_FACTOR; /** - * Key to set the MPEG-H UI persistence storage path. - * The corresponding value must be of value type {@link ValueType#VALUETYPE_STRING}. - * This key is passed to the MPEG-H UI manager. + * @see MediaFormat#KEY_AAC_DRC_EFFECT_TYPE */ - public final static String KEY_MPEGH_UI_PERSISTENCESTORAGE_PATH = "mpegh-ui-persistencestorage-path"; + @RequiresApi(api = Build.VERSION_CODES.P) + public static final String KEY_AAC_DRC_EFFECT_TYPE = MediaFormat.KEY_AAC_DRC_EFFECT_TYPE; /** - * @see MediaFormat#TYPE_NULL + * @see MediaFormat#KEY_AAC_DRC_TARGET_REFERENCE_LEVEL */ - public static final int VALUETYPE_NULL = 0; // MediaFormat.TYPE_NULL; + public static final String KEY_AAC_DRC_TARGET_REFERENCE_LEVEL = + MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL; + /** - * @see MediaFormat#TYPE_INTEGER + * @see MediaFormat#KEY_AAC_DRC_OUTPUT_LOUDNESS */ - public static final int VALUETYPE_INT = 1; // MediaFormat.TYPE_INTEGER; + @RequiresApi(api = Build.VERSION_CODES.R) + public static final String KEY_AAC_DRC_OUTPUT_LOUDNESS = MediaFormat.KEY_AAC_DRC_OUTPUT_LOUDNESS; + /** - * @see MediaFormat#TYPE_LONG + * 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 int VALUETYPE_LONG = 2; // MediaFormat.TYPE_LONG; + public static final String KEY_MPEGH_OUTPUT_MODE = "mpegh-output-mode"; + /** - * @see MediaFormat#TYPE_FLOAT + * 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 int VALUETYPE_FLOAT = 3; // MediaFormat.TYPE_FLOAT; + public static final String KEY_MPEGH_TARGET_LAYOUT = "mpegh-target-layout"; + /** - * @see MediaFormat#TYPE_STRING + * 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 int VALUETYPE_STRING = 4; // MediaFormat.TYPE_STRING; + public static final String KEY_MPEGH_UI_CONFIG = "mpegh-ui-config"; + /** - * @see MediaFormat#TYPE_BYTE_BUFFER + * 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 int VALUETYPE_BYTE_BUFFER = 5; // MediaFormat.TYPE_BYTE_BUFFER; + public static final String KEY_MPEGH_UI_COMMAND = "mpegh-ui-command"; /** - * Value types for a {@link CodecParameter}. - * One of {@link #VALUETYPE_NULL}, {@link #VALUETYPE_INT}, {@link #VALUETYPE_LONG}, - * {@link #VALUETYPE_FLOAT}, {@link #VALUETYPE_STRING} or {@link #VALUETYPE_BYTE_BUFFER}. + * 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. */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - VALUETYPE_NULL, - VALUETYPE_INT, - VALUETYPE_LONG, - VALUETYPE_FLOAT, - VALUETYPE_STRING, - VALUETYPE_BYTE_BUFFER - }) - public @interface ValueType { - - } + public static final String KEY_MPEGH_UI_PERSISTENCESTORAGE_PATH = + "mpegh-ui-persistencestorage-path"; + /** The key of the codec parameter. */ + public final String key; - public String key; - public @Nullable Object value; - public @ValueType int valueType; + /** The value of the codec parameter. */ + public final @Nullable Object value; + /** The {@link ValueType} of the value object. */ + public final @ValueType int valueType; /** - * Creates a new codec parameter. + * Creates an instance. * - * @param key A string holding the key of the codec parameter. - * @param value An object representing the value of the codec parameter. - * @param valueType The value type of the value object. + * @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; diff --git a/libraries/common/src/main/java/androidx/media3/common/CodecParameters.java b/libraries/common/src/main/java/androidx/media3/common/CodecParameters.java index cf8ca087d47..84bc70ef5fb 100644 --- a/libraries/common/src/main/java/androidx/media3/common/CodecParameters.java +++ b/libraries/common/src/main/java/androidx/media3/common/CodecParameters.java @@ -1,83 +1,84 @@ +/* + * 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; -import javax.annotation.Nullable; - -/** - * A {@link CodecParameters} instance provides the possibility to store/cache a set of {@link CodecParameter}. - * Furthermore, some conversion functions ({@link CodecParameters#setFromMediaFormat}, {@link CodecParameters#addToMediaFormat} - * and {@link CodecParameters#toBundle}) are given. - */ -public class CodecParameters { - private static final String TAG = "CodecParameters"; +/** A collection of {@link CodecParameter} objects. */ +@UnstableApi +public final class CodecParameters { - /** - * A hashmap for storing several codec parameters. - */ - private final HashMap codecParameters; + private final Map codecParameters; - /** - * Creates a new codec parameters instance. - */ + /** Creates an instance. */ public CodecParameters() { codecParameters = new HashMap<>(); } - /** - * Set, replace or remove a codec parameter in/from the cache. - * If the value type of the codec parameter is of value type VALUETYPE_NULL, the - * cached codec parameter will be removed. + * Sets, replaces, or removes a codec parameter in the collection. * - * @param param A {@link CodecParameter} to be set, replaced or removed in/from the cache. + *

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.VALUETYPE_NULL) { + if (param.valueType == CodecParameter.TYPE_NULL) { codecParameters.remove(param.key); } else { codecParameters.put(param.key, param); } } - /** - * Get the hashmap of the cached codec parameters. - * - * @returns The hashmap of cached codec parameters. - */ - public HashMap get() { + /** Returns the map of codec parameters in this collection. */ + public Map get() { return codecParameters; } /** - * Get a cached codec parameter according to its key. + * Returns a codec parameter from the collection by its key. * * @param key A string representing the key of the codec parameter. - * @returns The requested {@link CodecParameter}. + * @return The requested {@link CodecParameter}, or {@code null} if not found. */ - public @Nullable CodecParameter get(String key) { + @Nullable + public CodecParameter get(String key) { return codecParameters.get(key); } - /** - * Remove all cached codec parameters from the cache. - */ + /** Removes all codec parameters from the collection. */ public void clear() { codecParameters.clear(); } /** - * Convert the cached codec parameters to a bundle. + * Converts the collection of codec parameters to a {@link Bundle}. * - * @return A {@link Bundle} containing the cached codec parameters. + * @return A {@link Bundle} containing the codec parameters. */ public Bundle toBundle() { Bundle bundle = new Bundle(); @@ -86,22 +87,22 @@ public Bundle toBundle() { CodecParameter param = entry.getValue(); switch (param.valueType) { - case CodecParameter.VALUETYPE_INT: + case CodecParameter.TYPE_INT: bundle.putInt(key, (int) param.value); break; - case CodecParameter.VALUETYPE_LONG: + case CodecParameter.TYPE_LONG: bundle.putLong(key, (long) param.value); break; - case CodecParameter.VALUETYPE_FLOAT: + case CodecParameter.TYPE_FLOAT: bundle.putFloat(key, (float) param.value); break; - case CodecParameter.VALUETYPE_STRING: + case CodecParameter.TYPE_STRING: bundle.putString(key, (String) param.value); break; - case CodecParameter.VALUETYPE_BYTE_BUFFER: + case CodecParameter.TYPE_BYTE_BUFFER: bundle.putByteArray(key, ((ByteBuffer) param.value).array()); break; - case CodecParameter.VALUETYPE_NULL: + case CodecParameter.TYPE_NULL: default: break; } @@ -110,121 +111,115 @@ public Bundle toBundle() { } /** - * Convert the entries of a MediaFormat to codec parameters and cache them. + * Populates this collection from the entries of a {@link MediaFormat}. * - * @param mediaFormat The media format to be converted and cached. - * @param filterKeys A list of media format entry keys which should be converted and cached. - * If null, all entries in the media format will be cached. + * @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 ArrayList filterKeys) { - CodecParameter param; + 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: - param = new CodecParameter(key, mediaFormat.getInteger(key), - CodecParameter.VALUETYPE_INT); - codecParameters.put(key, param); + value = mediaFormat.getInteger(key); + valueType = CodecParameter.TYPE_INT; break; case MediaFormat.TYPE_LONG: - param = new CodecParameter(key, mediaFormat.getLong(key), - CodecParameter.VALUETYPE_LONG); - codecParameters.put(key, param); + value = mediaFormat.getLong(key); + valueType = CodecParameter.TYPE_LONG; break; case MediaFormat.TYPE_FLOAT: - param = new CodecParameter(key, mediaFormat.getFloat(key), - CodecParameter.VALUETYPE_FLOAT); - codecParameters.put(key, param); + value = mediaFormat.getFloat(key); + valueType = CodecParameter.TYPE_FLOAT; break; case MediaFormat.TYPE_STRING: - param = new CodecParameter(key, mediaFormat.getString(key), - CodecParameter.VALUETYPE_STRING); - codecParameters.put(key, param); + value = mediaFormat.getString(key); + valueType = CodecParameter.TYPE_STRING; break; case MediaFormat.TYPE_BYTE_BUFFER: - param = new CodecParameter(key, mediaFormat.getByteBuffer(key), - CodecParameter.VALUETYPE_BYTE_BUFFER); - codecParameters.put(key, param); + value = mediaFormat.getByteBuffer(key); + valueType = CodecParameter.TYPE_BYTE_BUFFER; break; case MediaFormat.TYPE_NULL: default: - break; + continue; } + codecParameters.put(key, new CodecParameter(key, value, valueType)); } - } else { - // not implemented } } else { for (String key : filterKeys) { if (mediaFormat.containsKey(key)) { @Nullable Object value = null; - @CodecParameter.ValueType int type = CodecParameter.VALUETYPE_NULL; + @CodecParameter.ValueType int type = CodecParameter.TYPE_NULL; boolean success = false; try { value = mediaFormat.getInteger(key); - type = CodecParameter.VALUETYPE_INT; + type = CodecParameter.TYPE_INT; success = true; } catch (Exception e) { - e.printStackTrace(); + // Do nothing. } if (!success) { try { value = mediaFormat.getLong(key); - type = CodecParameter.VALUETYPE_LONG; + type = CodecParameter.TYPE_LONG; success = true; } catch (Exception e) { - e.printStackTrace(); + // Do nothing. } } if (!success) { try { value = mediaFormat.getFloat(key); - type = CodecParameter.VALUETYPE_FLOAT; + type = CodecParameter.TYPE_FLOAT; success = true; } catch (Exception e) { - e.printStackTrace(); + // Do nothing. } } if (!success) { try { value = mediaFormat.getString(key); - type = CodecParameter.VALUETYPE_STRING; + type = CodecParameter.TYPE_STRING; success = true; } catch (Exception e) { - e.printStackTrace(); + // Do nothing. } } if (!success) { try { value = mediaFormat.getByteBuffer(key); - type = CodecParameter.VALUETYPE_BYTE_BUFFER; + type = CodecParameter.TYPE_BYTE_BUFFER; success = true; } catch (Exception e) { - e.printStackTrace(); + // Do nothing. } } if (success) { - param = new CodecParameter(key, value, type); - codecParameters.put(param.key, param); + codecParameters.put(key, new CodecParameter(key, value, type)); } } } } } - /** - * Append/overwrite the entries of a MediaFormat by all cached codec parameters. + * Adds all parameters from this collection to a {@link MediaFormat}. Existing keys in the {@link + * MediaFormat} will be overwritten. * - * @param mediaFormat The media format to which codec parameters should be added. + * @param mediaFormat The {@link MediaFormat} to which codec parameters should be added. */ public void addToMediaFormat(MediaFormat mediaFormat) { for (Map.Entry entry : codecParameters.entrySet()) { @@ -232,22 +227,22 @@ public void addToMediaFormat(MediaFormat mediaFormat) { CodecParameter param = entry.getValue(); switch (param.valueType) { - case CodecParameter.VALUETYPE_INT: + case CodecParameter.TYPE_INT: mediaFormat.setInteger(key, (int) param.value); break; - case CodecParameter.VALUETYPE_LONG: + case CodecParameter.TYPE_LONG: mediaFormat.setLong(key, (long) param.value); break; - case CodecParameter.VALUETYPE_FLOAT: + case CodecParameter.TYPE_FLOAT: mediaFormat.setFloat(key, (float) param.value); break; - case CodecParameter.VALUETYPE_STRING: + case CodecParameter.TYPE_STRING: mediaFormat.setString(key, (String) param.value); break; - case CodecParameter.VALUETYPE_BYTE_BUFFER: + case CodecParameter.TYPE_BYTE_BUFFER: mediaFormat.setByteBuffer(key, (ByteBuffer) param.value); break; - case CodecParameter.VALUETYPE_NULL: + 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 index 5a226bfd3bc..449ea361efc 100644 --- a/libraries/common/src/main/java/androidx/media3/common/CodecParametersChangeListener.java +++ b/libraries/common/src/main/java/androidx/media3/common/CodecParametersChangeListener.java @@ -1,24 +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 java.util.ArrayList; +import androidx.media3.common.util.UnstableApi; +import java.util.List; -/** - * A codec parameter change listener provides the ability to be notified about state/parameter - * changes in a codec instance. - */ +/** A listener for changes to codec parameters. */ +@UnstableApi public interface CodecParametersChangeListener { /** - * Inform about changes to the output buffer format. + * Called when one or more codec parameters have changed. * - * @param codecParameters A set of codec parameters. + * @param codecParameters A {@link CodecParameters} instance containing the set of changed + * parameters. */ void onCodecParametersChanged(CodecParameters codecParameters); /** - * Get a list of key values which should be returned by {@link #onCodecParametersChanged(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. * - * @returns An ArrayList of the requested keys. + * @return An {@link List} of the requested keys. */ - ArrayList getFilterKeys(); + 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 62c5d12c787..de4a925d4f1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -40,7 +40,6 @@ 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; import androidx.media3.common.MediaItem; @@ -1965,11 +1964,28 @@ void setVideoChangeFrameRateStrategy( @UnstableApi void setImageOutput(@Nullable ImageOutput imageOutput); - /** Set the CodecParameters */ + /** + * 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(CodecParameter codecParameter); + void setCodecParameter(@Nullable CodecParameter codecParameter); - /** Set the CodecParametersChangeListener */ + /** + * 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 codecParametersChangeListener); + 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 08960a4cd4c..dabcecf7ba5 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -1985,7 +1985,7 @@ public void setImageOutput(@Nullable ImageOutput imageOutput) { } @Override - public void setCodecParameter(CodecParameter codecParameter) { + public void setCodecParameter(@Nullable CodecParameter codecParameter) { verifyApplicationThread(); for (Renderer renderer : renderers) { createMessage(renderer).setType(MSG_SET_CODEC_PARAMETER).setPayload(codecParameter).send(); @@ -1997,8 +1997,10 @@ public void setCodecParametersChangeListener( @Nullable CodecParametersChangeListener codecParametersChangeListener) { verifyApplicationThread(); for (Renderer renderer : renderers) { - createMessage(renderer).setType(MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER) - .setPayload(codecParametersChangeListener).send(); + createMessage(renderer) + .setType(MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER) + .setPayload(codecParametersChangeListener) + .send(); } } 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 53e9d12c349..aef96280b8f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java @@ -197,9 +197,9 @@ 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}, {@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}). + * {@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 @Retention(RetentionPolicy.SOURCE) @@ -377,8 +377,10 @@ 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}. + /** + * 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 */ 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 d78a5dd5f9f..c267c196236 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java @@ -1352,7 +1352,7 @@ public void setImageOutput(@Nullable ImageOutput imageOutput) { } @Override - public void setCodecParameter(CodecParameter codecParameter) { + public void setCodecParameter(@Nullable CodecParameter codecParameter) { blockUntilConstructorFinished(); player.setCodecParameter(codecParameter); } @@ -1364,8 +1364,6 @@ public void setCodecParametersChangeListener( 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 b92e2d045c3..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 @@ -32,7 +32,6 @@ import android.media.MediaFormat; import android.os.Build; import android.os.Bundle; -import android.os.Build; import android.os.Handler; import android.util.Pair; import androidx.annotation.CallSuper; @@ -117,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; @@ -133,10 +140,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean isStarted; private long nextBufferToWritePresentationTimeUs; - /** {@link CodecParameters} instance used for caching several {@link CodecParameter} **/ - private final CodecParameters codecParameters = new CodecParameters(); - @Nullable protected CodecParametersChangeListener codecParametersChangeListener = null; - + /** + * A listener for codec parameter changes. + * + * @see ExoPlayer#setCodecParametersChangeListener(CodecParametersChangeListener) + */ + @Nullable private CodecParametersChangeListener codecParametersChangeListener; /** * @param context A context. @@ -317,6 +326,7 @@ public MediaCodecAudioRenderer( eventDispatcher = new EventDispatcher(eventHandler, eventListener); nextBufferToWritePresentationTimeUs = C.TIME_UNSET; audioSink.setListener(new AudioSinkListener()); + codecParameters = new CodecParameters(); } @Override @@ -938,12 +948,8 @@ public void handleMessage(@MessageType int messageType, @Nullable Object message } else { this.codecParameters.set((CodecParameter) message); } - @Nullable MediaCodecAdapter codec = getCodec(); - if (codec == null) { - return; - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (codec != null) { codec.setParameters(codecParameters.toBundle()); } break; @@ -1071,7 +1077,6 @@ protected MediaFormat getMediaFormat( } 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();