From 87bbda5a537fd82268a0207ab6b0e33e80984540 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 # Conflicts: # libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java # libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java --- 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 | 23 ++ .../media3/exoplayer/SimpleExoPlayer.java | 17 ++ .../audio/MediaCodecAudioRenderer.java | 35 +++ .../media3/test/utils/StubExoPlayer.java | 13 + 11 files changed, 664 insertions(+) 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 04ace1b5570..f8bc1d76990 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -39,6 +39,9 @@ import androidx.media3.common.AudioAttributes; import androidx.media3.common.AuxEffectInfo; import androidx.media3.common.C; +import androidx.media3.common.CodecParameter; +import androidx.media3.common.CodecParametersChangeListener; +import androidx.media3.common.DeviceInfo; import androidx.media3.common.Effect; import androidx.media3.common.Format; import androidx.media3.common.MediaItem; @@ -2025,4 +2028,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 320e046d51c..c4487efb4bd 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -28,6 +28,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; @@ -62,6 +64,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; @@ -2054,6 +2058,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 a447d2835fa..74c0f888a85 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; @@ -200,6 +202,10 @@ interface WakeupListener { * #MSG_SET_VIDEO_OUTPUT_RESOLUTION}, {@link #MSG_SET_IMAGE_OUTPUT}, {@link #MSG_SET_PRIORITY}, * {@link #MSG_TRANSFER_RESOURCES}, {@link #MSG_SET_SCRUBBING_MODE} or {@link * #MSG_SET_VIRTUAL_DEVICE_ID}. May also be an app-defined value (see {@link #MSG_CUSTOM_BASE}). + * #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}). */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -226,6 +232,9 @@ interface WakeupListener { MSG_TRANSFER_RESOURCES, MSG_SET_SCRUBBING_MODE, MSG_SET_VIRTUAL_DEVICE_ID + MSG_SET_SCRUBBING_MODE, + MSG_SET_CODEC_PARAMETER, + MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER }) public @interface MessageType {} @@ -377,6 +386,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; + /** * The type of a message that can be passed to audio renderers via {@link * ExoPlayer#createMessage(PlayerMessage.Target)}. The message payload should be an {@link 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 65d9a07dbd8..dd479b40319 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; @@ -1379,6 +1381,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 496785832c8..85eae17b330 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; @@ -138,6 +142,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. @@ -607,6 +616,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. @@ -951,6 +965,24 @@ public void handleMessage(@MessageType int messageType, @Nullable Object message case MSG_SET_AUDIO_OUTPUT_PROVIDER: audioSink.setAudioOutputProvider((AudioOutputProvider) checkNotNull(message)); 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; @@ -1070,6 +1102,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 3c128108b08..7320f907ad1 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; @@ -443,4 +445,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 336d94ebae69e755e624a79b26784c429ba25482 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 c0f28570d9686ae8aa58b534c819e4a39e5423f8 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 pick e706cef90e # Refactor and add tests --- RELEASENOTES.md | 2 + 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 -- .../media3/exoplayer/CodecParameters.java | 139 ++++++++ .../CodecParametersChangeListener.java | 44 +++ .../androidx/media3/exoplayer/ExoPlayer.java | 42 ++- .../media3/exoplayer/ExoPlayerImpl.java | 33 +- .../androidx/media3/exoplayer/Renderer.java | 54 +-- .../media3/exoplayer/SimpleExoPlayer.java | 21 +- .../audio/MediaCodecAudioRenderer.java | 213 ++++++++++-- .../AsynchronousMediaCodecAdapter.java | 13 + .../ForwardingMediaCodecAdapter.java | 13 + .../mediacodec/MediaCodecAdapter.java | 20 ++ .../SynchronousMediaCodecAdapter.java | 13 + .../media3/exoplayer/ExoPlayerTest.java | 70 ++++ .../audio/MediaCodecAudioRendererTest.java | 311 ++++++++++++++++++ .../mediacodec/MediaCodecRendererTest.java | 10 + .../media3/test/utils/StubExoPlayer.java | 18 +- 21 files changed, 927 insertions(+), 632 deletions(-) delete mode 100644 libraries/common/src/main/java/androidx/media3/common/CodecParameter.java delete mode 100644 libraries/common/src/main/java/androidx/media3/common/CodecParameters.java delete mode 100644 libraries/common/src/main/java/androidx/media3/common/CodecParametersChangeListener.java create mode 100644 libraries/common/src/main/java/androidx/media3/exoplayer/CodecParameters.java create mode 100644 libraries/common/src/main/java/androidx/media3/exoplayer/CodecParametersChangeListener.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2574976c9d7..a03eb3f4664 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,8 @@ * Append content resume offset when skipping ad playback after seek adjustment or auto transition ([2484](https://github.com/androidx/media/issues/2484)). + * Add API for setting and observing `MediaCodec` parameters dynamically + ([#2794](https://github.com/androidx/media/pull/2794)). * CompositionPlayer: * Transformer: * Track Selection: diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 7afc3ab8eb4..b063b68e140 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -787,10 +787,6 @@ { "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 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 deleted file mode 100644 index 5379f59d9bf..00000000000 --- a/libraries/common/src/main/java/androidx/media3/common/CodecParameter.java +++ /dev/null @@ -1,165 +0,0 @@ -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 deleted file mode 100644 index cf8ca087d47..00000000000 --- a/libraries/common/src/main/java/androidx/media3/common/CodecParameters.java +++ /dev/null @@ -1,256 +0,0 @@ -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 deleted file mode 100644 index 5a226bfd3bc..00000000000 --- a/libraries/common/src/main/java/androidx/media3/common/CodecParametersChangeListener.java +++ /dev/null @@ -1,24 +0,0 @@ -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/common/src/main/java/androidx/media3/exoplayer/CodecParameters.java b/libraries/common/src/main/java/androidx/media3/exoplayer/CodecParameters.java new file mode 100644 index 00000000000..1403641dc75 --- /dev/null +++ b/libraries/common/src/main/java/androidx/media3/exoplayer/CodecParameters.java @@ -0,0 +1,139 @@ +/* + * 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.exoplayer; + +import androidx.annotation.Nullable; +import androidx.media3.common.util.NullableType; +import androidx.media3.common.util.UnstableApi; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * A collection of parameters to be applied to a codec. + * + *

This class provides type-safe methods to set and get codec parameters. + */ +@UnstableApi +public final class CodecParameters { + + private final Map params; + + /** Creates an empty instance. */ + public CodecParameters() { + params = new HashMap<>(); + } + + /** + * Creates a shallow copy of another {@link CodecParameters} instance. + * + * @param other The instance to copy. + */ + public CodecParameters(CodecParameters other) { + this(); + params.putAll(other.params); + } + + /** + * Sets an integer parameter value. + * + * @param key The parameter key. + * @param value The integer value. + */ + public void setInteger(String key, int value) { + params.put(key, value); + } + + /** + * Sets a long parameter value. + * + * @param key The parameter key. + * @param value The long value. + */ + public void setLong(String key, long value) { + params.put(key, value); + } + + /** + * Sets a float parameter value. + * + * @param key The parameter key. + * @param value The float value. + */ + public void setFloat(String key, float value) { + params.put(key, value); + } + + /** + * Sets a string parameter value. + * + * @param key The parameter key. + * @param value The string value, or {@code null} to unset the key. + */ + public void setString(String key, @Nullable String value) { + params.put(key, value); + } + + /** + * Sets a byte buffer parameter value. + * + * @param key The parameter key. + * @param value The ByteBuffer value, or {@code null} to unset the key. + */ + public void setByteBuffer(String key, @Nullable ByteBuffer value) { + params.put(key, value); + } + + /** + * Retrieves a parameter value by its key. + * + * @param key A string representing the key of the codec parameter. + * @return The value of the requested parameter, or {@code null} if the key is not present. + */ + @Nullable + public Object get(String key) { + return params.get(key); + } + + /** + * Returns a Set containing the Keys from this CodecParameters's mapping. + * + * @return a Set of String keys + */ + public Set keySet() { + return Collections.unmodifiableSet(params.keySet()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CodecParameters)) { + return false; + } + CodecParameters other = (CodecParameters) obj; + return Objects.equals(this.params, other.params); + } + + @Override + public int hashCode() { + return Objects.hashCode(params); + } +} diff --git a/libraries/common/src/main/java/androidx/media3/exoplayer/CodecParametersChangeListener.java b/libraries/common/src/main/java/androidx/media3/exoplayer/CodecParametersChangeListener.java new file mode 100644 index 00000000000..f4f6b838e42 --- /dev/null +++ b/libraries/common/src/main/java/androidx/media3/exoplayer/CodecParametersChangeListener.java @@ -0,0 +1,44 @@ +/* + * 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.exoplayer; + +import androidx.media3.common.util.UnstableApi; +import java.util.List; + +/** A listener for changes to codec parameters. */ +@UnstableApi +public interface CodecParametersChangeListener { + + /** + * Called when any of the codec parameters that any listener is subscribed to have changed. + * + *

The provided {@link CodecParameters} object contains the most recent values for all keys + * that are currently set in the underlying {@link android.media.MediaFormat} AND were included in + * the union of keys from {@link #getSubscribedKeys()} across all registered listeners at the time + * the change was detected. + * + * @param codecParameters A {@link CodecParameters} instance representing the current state of all + * keys subscribed to by any listener. + */ + void onCodecParametersChanged(CodecParameters codecParameters); + + /** + * Returns a list of parameter keys that the listener is interested in observing. + * + * @return A {@link List} of the subscribed keys. + */ + List getSubscribedKeys(); +} 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 f8bc1d76990..78890dee973 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -34,14 +34,12 @@ import android.view.TextureView; import androidx.annotation.IntRange; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.VisibleForTesting; 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; @@ -2029,11 +2027,41 @@ void setVideoChangeFrameRateStrategy( @UnstableApi void setImageOutput(@Nullable ImageOutput imageOutput); - /** Set the CodecParameters */ + /** + * Sets a collection of parameters on the underlying codecs. + * + *

This method is asynchronous. The parameters will be applied to the renderers on the playback + * thread. If an underlying decoder does not support a parameter, it will be ignored. + * + * @param codecParameters The {@link CodecParameters} to set. + */ @UnstableApi - void setCodecParameter(CodecParameter codecParameter); + @RequiresApi(29) + void setAudioCodecParameters(CodecParameters codecParameters); - /** Set the CodecParametersChangeListener */ + /** + * Adds a listener for codec parameter changes. + * + *

This method is asynchronous. The listener will be added on the playback thread. + * + *

Note: When used with {@link MediaCodec}, observing vendor-specific parameter changes + * requires API level 31 or higher. On API levels 29 and 30, any requested vendor-specific keys + * will be ignored. + * + * @param listener The {@link CodecParametersChangeListener} to add. + */ + @UnstableApi + @RequiresApi(29) + void addAudioCodecParametersChangeListener(CodecParametersChangeListener listener); + + /** + * Removes a listener for codec parameter changes. + * + *

This method is asynchronous. The listener will be removed on the playback thread. + * + * @param listener The {@link CodecParametersChangeListener} to remove. + */ @UnstableApi - void setCodecParametersChangeListener(@Nullable CodecParametersChangeListener codecParametersChangeListener); + @RequiresApi(29) + void removeAudioCodecParametersChangeListener(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 c4487efb4bd..9a20a1ff8eb 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -22,14 +22,15 @@ import static androidx.media3.common.C.TRACK_TYPE_IMAGE; import static androidx.media3.common.C.TRACK_TYPE_VIDEO; import static androidx.media3.common.util.Util.castNonNull; +import static androidx.media3.exoplayer.Renderer.MSG_ADD_CODEC_PARAMETERS_LISTENER; +import static androidx.media3.exoplayer.Renderer.MSG_REMOVE_CODEC_PARAMETERS_LISTENER; import static androidx.media3.exoplayer.Renderer.MSG_SET_AUDIO_ATTRIBUTES; import static androidx.media3.exoplayer.Renderer.MSG_SET_AUDIO_OUTPUT_PROVIDER; import static androidx.media3.exoplayer.Renderer.MSG_SET_AUDIO_SESSION_ID; 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_CODEC_PARAMETERS; 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; @@ -64,8 +65,6 @@ 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; @@ -2059,21 +2058,27 @@ public void setImageOutput(@Nullable ImageOutput imageOutput) { } @Override - public void setCodecParameter(CodecParameter codecParameter) { + @RequiresApi(29) + public void setAudioCodecParameters(CodecParameters codecParameters) { verifyApplicationThread(); - for (Renderer renderer : renderers) { - createMessage(renderer).setType(MSG_SET_CODEC_PARAMETER).setPayload(codecParameter).send(); - } + checkNotNull(codecParameters); + sendRendererMessage(C.TRACK_TYPE_AUDIO, MSG_SET_CODEC_PARAMETERS, codecParameters); } @Override - public void setCodecParametersChangeListener( - @Nullable CodecParametersChangeListener codecParametersChangeListener) { + @RequiresApi(29) + public void addAudioCodecParametersChangeListener(CodecParametersChangeListener listener) { verifyApplicationThread(); - for (Renderer renderer : renderers) { - createMessage(renderer).setType(MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER) - .setPayload(codecParametersChangeListener).send(); - } + checkNotNull(listener); + sendRendererMessage(C.TRACK_TYPE_AUDIO, MSG_ADD_CODEC_PARAMETERS_LISTENER, listener); + } + + @Override + @RequiresApi(29) + public void removeAudioCodecParametersChangeListener(CodecParametersChangeListener listener) { + verifyApplicationThread(); + checkNotNull(listener); + sendRendererMessage(C.TRACK_TYPE_AUDIO, MSG_REMOVE_CODEC_PARAMETERS_LISTENER, listener); } @SuppressWarnings("deprecation") // Calling deprecated methods. 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 74c0f888a85..807953894b3 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java @@ -24,8 +24,6 @@ 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; @@ -200,12 +198,10 @@ interface WakeupListener { * #MSG_SET_AUDIO_SESSION_ID}, {@link #MSG_SET_WAKEUP_LISTENER}, {@link * #MSG_SET_PREFERRED_AUDIO_DEVICE}, {@link #MSG_SET_VIDEO_EFFECTS}, {@link * #MSG_SET_VIDEO_OUTPUT_RESOLUTION}, {@link #MSG_SET_IMAGE_OUTPUT}, {@link #MSG_SET_PRIORITY}, - * {@link #MSG_TRANSFER_RESOURCES}, {@link #MSG_SET_SCRUBBING_MODE} or {@link - * #MSG_SET_VIRTUAL_DEVICE_ID}. May also be an app-defined value (see {@link #MSG_CUSTOM_BASE}). - * #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_TRANSFER_RESOURCES}, {@link #MSG_SET_SCRUBBING_MODE}, {@link + * #MSG_SET_VIRTUAL_DEVICE_ID}, {@link #MSG_SET_CODEC_PARAMETERS}, {@link + * #MSG_ADD_CODEC_PARAMETERS_LISTENER} or {@link #MSG_REMOVE_CODEC_PARAMETERS_LISTENER}. May also + * be an app-defined value (see {@link #MSG_CUSTOM_BASE}). */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -231,10 +227,10 @@ interface WakeupListener { MSG_SET_PRIORITY, MSG_TRANSFER_RESOURCES, MSG_SET_SCRUBBING_MODE, - MSG_SET_VIRTUAL_DEVICE_ID - MSG_SET_SCRUBBING_MODE, - MSG_SET_CODEC_PARAMETER, - MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER + MSG_SET_VIRTUAL_DEVICE_ID, + MSG_SET_CODEC_PARAMETERS, + MSG_ADD_CODEC_PARAMETERS_LISTENER, + MSG_REMOVE_CODEC_PARAMETERS_LISTENER }) public @interface MessageType {} @@ -386,20 +382,6 @@ 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; - /** * The type of a message that can be passed to audio renderers via {@link * ExoPlayer#createMessage(PlayerMessage.Target)}. The message payload should be an {@link @@ -414,6 +396,26 @@ interface WakeupListener { */ int MSG_SET_AUDIO_OUTPUT_PROVIDER = 20; + /** + * The type of a message that can be passed to renderers via {@link + * ExoPlayer#createMessage(PlayerMessage.Target)}. The message payload should be a {@link + * CodecParameters} instance. + */ + int MSG_SET_CODEC_PARAMETERS = 21; + + /** + * The type of a message that can be passed to renderers via {@link + * ExoPlayer#createMessage(PlayerMessage.Target)}. The message payload must be a {@link + * CodecParametersChangeListener} instance. + */ + int MSG_ADD_CODEC_PARAMETERS_LISTENER = 22; + + /** + * A message to remove a {@link CodecParametersChangeListener}. The message payload will be the + * listener to remove. + */ + int MSG_REMOVE_CODEC_PARAMETERS_LISTENER = 23; + /** * 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 dd479b40319..cec4488e3da 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java @@ -24,13 +24,12 @@ import android.view.TextureView; import androidx.annotation.IntRange; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import androidx.media3.common.AudioAttributes; 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; @@ -1382,19 +1381,25 @@ public void setImageOutput(@Nullable ImageOutput imageOutput) { } @Override - public void setCodecParameter(CodecParameter codecParameter) { + @RequiresApi(29) + public void setAudioCodecParameters(CodecParameters codecParameters) { blockUntilConstructorFinished(); - player.setCodecParameter(codecParameter); + player.setAudioCodecParameters(codecParameters); } @Override - public void setCodecParametersChangeListener( - @Nullable CodecParametersChangeListener codecParametersChangeListener) { + @RequiresApi(29) + public void addAudioCodecParametersChangeListener(CodecParametersChangeListener listener) { blockUntilConstructorFinished(); - player.setCodecParametersChangeListener(codecParametersChangeListener); + player.addAudioCodecParametersChangeListener(listener); } - + @Override + @RequiresApi(29) + public void removeAudioCodecParametersChangeListener(CodecParametersChangeListener listener) { + blockUntilConstructorFinished(); + player.removeAudioCodecParametersChangeListener(listener); + } /* package */ void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) { blockUntilConstructorFinished(); 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 85eae17b330..fc44a4e5ed5 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,17 +32,14 @@ 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; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; 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; @@ -54,6 +51,8 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.decoder.DecoderInputBuffer; +import androidx.media3.exoplayer.CodecParameters; +import androidx.media3.exoplayer.CodecParametersChangeListener; import androidx.media3.exoplayer.DecoderReuseEvaluation; import androidx.media3.exoplayer.DecoderReuseEvaluation.DecoderDiscardReasons; import androidx.media3.exoplayer.ExoPlaybackException; @@ -76,8 +75,11 @@ import com.google.common.collect.ImmutableList; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; /** * Decodes and renders audio using {@link MediaCodec} and an {@link AudioSink}. @@ -108,6 +110,16 @@ * message payload should be an {@link Integer}. *

  • Message with type {@link #MSG_SET_AUDIO_OUTPUT_PROVIDER} to set the audio output provider. * The message payload must be an {@link AudioOutputProvider} instance. + *
  • Message with type {@link #MSG_SET_CODEC_PARAMETERS} to set a collection of codec + * parameters. The message payload should be a {@link CodecParameters} instance. This is only + * supported on API level 29 and above. + *
  • Message with type {@link #MSG_ADD_CODEC_PARAMETERS_LISTENER} to add a listener for codec + * parameter changes. The message payload should be a {@link CodecParametersChangeListener} + * instance. This is only supported on API level 29 and above. + *
  • Message with type {@link #MSG_REMOVE_CODEC_PARAMETERS_LISTENER} to remove a listener for + * codec parameter changes. The message payload should be the {@link + * CodecParametersChangeListener} instance to remove. This is only supported on API level 29 + * and above. * */ @UnstableApi @@ -125,11 +137,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private final EventDispatcher eventDispatcher; private final AudioSink audioSink; @Nullable private final LoudnessCodecController loudnessCodecController; + private final List codecParametersChangeListeners; + private final Set subscribedCodecParameterKeys; private int codecMaxInputSize; private boolean codecNeedsDiscardChannelsWorkaround; private boolean codecNeedsVorbisToAndroidChannelMappingWorkaround; @Nullable private Format inputFormat; + @Nullable private CodecParameters activeCodecParameters; + private CodecParameters lastNotifiedCodecParameters; /** Codec used for DRM decryption only in passthrough and offload. */ @Nullable private Format decryptOnlyCodecFormat; @@ -142,11 +158,6 @@ 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. @@ -326,6 +337,9 @@ public MediaCodecAudioRenderer( eventDispatcher = new EventDispatcher(eventHandler, eventListener); nextBufferToWritePresentationTimeUs = C.TIME_UNSET; audioSink.setListener(new AudioSinkListener()); + this.codecParametersChangeListeners = new ArrayList<>(); + this.subscribedCodecParameterKeys = new HashSet<>(); + this.lastNotifiedCodecParameters = new CodecParameters(); } @Override @@ -616,10 +630,8 @@ 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); + if (SDK_INT >= 29 && mediaFormat != null) { + checkAndNotifyCodecParameterChanges(mediaFormat); } Format audioSinkInputFormat; @Nullable int[] channelMap = null; @@ -965,23 +977,27 @@ public void handleMessage(@MessageType int messageType, @Nullable Object message case MSG_SET_AUDIO_OUTPUT_PROVIDER: audioSink.setAudioOutputProvider((AudioOutputProvider) checkNotNull(message)); 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; + case MSG_SET_CODEC_PARAMETERS: + if (Build.VERSION.SDK_INT >= 29) { + activeCodecParameters = new CodecParameters((CodecParameters) checkNotNull(message)); + + @Nullable MediaCodecAdapter codec = getCodec(); + if (codec != null) { + codec.setParameters(toBundle(activeCodecParameters)); + } } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - codec.setParameters(codecParameters.toBundle()); + break; + case MSG_ADD_CODEC_PARAMETERS_LISTENER: + if (Build.VERSION.SDK_INT >= 29) { + codecParametersChangeListeners.add((CodecParametersChangeListener) checkNotNull(message)); + updateSubscribedKeys(); } break; - case MSG_SET_CODEC_PARAMETERS_CHANGED_LISTENER: - this.codecParametersChangeListener = (CodecParametersChangeListener) message; + case MSG_REMOVE_CODEC_PARAMETERS_LISTENER: + if (Build.VERSION.SDK_INT >= 29) { + codecParametersChangeListeners.remove(message); + updateSubscribedKeys(); + } break; default: super.handleMessage(messageType, message); @@ -1103,11 +1119,150 @@ protected MediaFormat getMediaFormat( mediaFormat.setInteger(MediaFormat.KEY_IMPORTANCE, max(0, -rendererPriority)); } - codecParameters.addToMediaFormat(mediaFormat); - + if (SDK_INT >= 29) { + applyParametersToMediaFormat(activeCodecParameters, mediaFormat); + } return mediaFormat; } + @RequiresApi(29) + private void checkAndNotifyCodecParameterChanges(MediaFormat mediaFormat) { + if (codecParametersChangeListeners.isEmpty() || subscribedCodecParameterKeys.isEmpty()) { + return; + } + + CodecParameters currentValues = + getSubscribedCodecParametersFromMediaFormat(mediaFormat, subscribedCodecParameterKeys); + + if (currentValues.equals(lastNotifiedCodecParameters)) { + return; + } + + lastNotifiedCodecParameters = new CodecParameters(currentValues); + + for (CodecParametersChangeListener listener : codecParametersChangeListeners) { + listener.onCodecParametersChanged(new CodecParameters(currentValues)); + } + } + + @RequiresApi(29) + private static CodecParameters getSubscribedCodecParametersFromMediaFormat( + MediaFormat mediaFormat, Set keys) { + CodecParameters parameters = new CodecParameters(); + for (String key : keys) { + if (mediaFormat.containsKey(key)) { + int type = mediaFormat.getValueTypeForKey(key); + switch (type) { + case MediaFormat.TYPE_INTEGER: + parameters.setInteger(key, mediaFormat.getInteger(key)); + break; + case MediaFormat.TYPE_LONG: + parameters.setLong(key, mediaFormat.getLong(key)); + break; + case MediaFormat.TYPE_FLOAT: + parameters.setFloat(key, mediaFormat.getFloat(key)); + break; + case MediaFormat.TYPE_STRING: + parameters.setString(key, mediaFormat.getString(key)); + break; + case MediaFormat.TYPE_BYTE_BUFFER: + parameters.setByteBuffer(key, mediaFormat.getByteBuffer(key)); + break; + default: + break; + } + } + } + return parameters; + } + + @RequiresApi(29) + private void updateSubscribedKeys() { + Set newKeys = new HashSet<>(); + for (CodecParametersChangeListener listener : codecParametersChangeListeners) { + newKeys.addAll(listener.getSubscribedKeys()); + } + + if (!subscribedCodecParameterKeys.equals(newKeys)) { + Set removedKeys = new HashSet<>(subscribedCodecParameterKeys); + removedKeys.removeAll(newKeys); + Set addedKeys = new HashSet<>(newKeys); + addedKeys.removeAll(subscribedCodecParameterKeys); + + subscribedCodecParameterKeys.clear(); + subscribedCodecParameterKeys.addAll(newKeys); + + if (Build.VERSION.SDK_INT >= 31) { + @Nullable MediaCodecAdapter codec = getCodec(); + if (codec != null) { + if (!removedKeys.isEmpty()) { + codec.unsubscribeFromVendorParameters(new ArrayList<>(removedKeys)); + } + if (!addedKeys.isEmpty()) { + codec.subscribeToVendorParameters(new ArrayList<>(addedKeys)); + } + } + } + } + } + + private static Bundle toBundle(@Nullable CodecParameters parameters) { + Bundle bundle = new Bundle(); + if (parameters == null) { + return bundle; + } + for (String key : parameters.keySet()) { + Object value = parameters.get(key); + if (value == null) { + continue; + } + if (value instanceof Integer) { + bundle.putInt(key, (Integer) value); + } else if (value instanceof Long) { + bundle.putLong(key, (Long) value); + } else if (value instanceof Float) { + bundle.putFloat(key, (Float) value); + } else if (value instanceof String) { + bundle.putString(key, (String) value); + } else if (value instanceof ByteBuffer) { + ByteBuffer byteBuffer = (ByteBuffer) value; + byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.duplicate().get(bytes); + bundle.putByteArray(key, bytes); + } + } + return bundle; + } + + private static void applyParametersToMediaFormat( + @Nullable CodecParameters parameters, MediaFormat mediaFormat) { + if (parameters == null) { + return; + } + for (String key : parameters.keySet()) { + Object value = parameters.get(key); + if (value == null) { + mediaFormat.setString(key, null); + continue; + } + if (value instanceof Integer) { + mediaFormat.setInteger(key, (Integer) value); + } else if (value instanceof Long) { + mediaFormat.setLong(key, (Long) value); + } else if (value instanceof Float) { + mediaFormat.setFloat(key, (Float) value); + } else if (value instanceof String) { + mediaFormat.setString(key, (String) value); + } else if (value instanceof ByteBuffer) { + ByteBuffer original = (ByteBuffer) value; + ByteBuffer clone = ByteBuffer.allocate(original.remaining()); + clone.put(original.duplicate()); + clone.flip(); + mediaFormat.setByteBuffer(key, clone); + } + } + } + private void setAudioSessionId(int audioSessionId) { audioSink.setAudioSessionId(audioSessionId); if (SDK_INT >= 35 && loudnessCodecController != null) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapter.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapter.java index 1adefb4b610..be8254f11aa 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapter.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecAdapter.java @@ -44,6 +44,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.nio.ByteBuffer; +import java.util.List; /** * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in asynchronous mode, @@ -353,6 +354,18 @@ public PersistableBundle getMetrics() { return codec.getMetrics(); } + @Override + @RequiresApi(31) + public void subscribeToVendorParameters(List names) { + codec.subscribeToVendorParameters(names); + } + + @Override + @RequiresApi(31) + public void unsubscribeFromVendorParameters(List names) { + codec.unsubscribeFromVendorParameters(names); + } + @VisibleForTesting /* package */ void onError(MediaCodec.CodecException error) { asynchronousMediaCodecCallback.onError(codec, error); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/ForwardingMediaCodecAdapter.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/ForwardingMediaCodecAdapter.java index 5c1a0d8b14d..483d9a4c1a4 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/ForwardingMediaCodecAdapter.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/ForwardingMediaCodecAdapter.java @@ -26,6 +26,7 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.decoder.CryptoInfo; import java.nio.ByteBuffer; +import java.util.List; /** * A {@link MediaCodecAdapter} instance that forwards all calls to its delegate. @@ -147,4 +148,16 @@ public boolean needsReconfiguration() { public PersistableBundle getMetrics() { return delegate.getMetrics(); } + + @Override + @RequiresApi(31) + public void subscribeToVendorParameters(List names) { + delegate.subscribeToVendorParameters(names); + } + + @Override + @RequiresApi(31) + public void unsubscribeFromVendorParameters(List names) { + delegate.unsubscribeFromVendorParameters(names); + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecAdapter.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecAdapter.java index e1c005bd7bb..cda3b77483f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecAdapter.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecAdapter.java @@ -31,6 +31,7 @@ import androidx.media3.decoder.CryptoInfo; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.List; /** * Abstracts {@link MediaCodec} operations. @@ -335,4 +336,23 @@ default boolean registerOnBufferAvailableListener( */ @RequiresApi(26) PersistableBundle getMetrics(); + + /** + * Subscribe to vendor parameters, so that these parameters will be present in {@link + * #getOutputFormat} and changes to these parameters generate output format change event. + * + * @see MediaCodec#subscribeToVendorParameters(List) + */ + @RequiresApi(31) + void subscribeToVendorParameters(List names); + + /** + * Unsubscribe from vendor parameters, so that these parameters will not be present in {@link + * #getOutputFormat} and changes to these parameters no longer generate output format change + * event. + * + * @see MediaCodec#unsubscribeFromVendorParameters(List) + */ + @RequiresApi(31) + void unsubscribeFromVendorParameters(List names); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/SynchronousMediaCodecAdapter.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/SynchronousMediaCodecAdapter.java index d03e8079d18..c8b6118e42a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/SynchronousMediaCodecAdapter.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/SynchronousMediaCodecAdapter.java @@ -34,6 +34,7 @@ import androidx.media3.decoder.CryptoInfo; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.List; /** * A {@link MediaCodecAdapter} that operates the underlying {@link MediaCodec} in synchronous mode. @@ -213,4 +214,16 @@ public void setVideoScalingMode(@C.VideoScalingMode int scalingMode) { public PersistableBundle getMetrics() { return codec.getMetrics(); } + + @Override + @RequiresApi(31) + public void subscribeToVendorParameters(List names) { + codec.subscribeToVendorParameters(names); + } + + @Override + @RequiresApi(31) + public void unsubscribeFromVendorParameters(List names) { + codec.unsubscribeFromVendorParameters(names); + } } 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 4a92f41212a..3450c468b42 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -16513,6 +16513,76 @@ public void setAudioOutputProvider_forwardsProviderToAudioSink() throws Exceptio player.release(); } + @Test + @Config(sdk = 29) + public void setAudioCodecParameters_whenReady_notifiesListener() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + 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_PARAMETERS) { + if (listener != null) { + CodecParameters params = (CodecParameters) message; + eventHandler.post(() -> listener.onCodecParametersChanged(params)); + } + } else if (messageType == Renderer.MSG_ADD_CODEC_PARAMETERS_LISTENER) { + this.listener = (CodecParametersChangeListener) message; + } else if (messageType == Renderer.MSG_REMOVE_CODEC_PARAMETERS_LISTENER) { + if (this.listener == message) { + this.listener = null; + } + } + 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); + when(mockListener.getSubscribedKeys()).thenReturn(Collections.singletonList("test-key")); + CodecParameters testParameters = new CodecParameters(); + testParameters.setInteger("test-key", 123); + + player.addAudioCodecParametersChangeListener(mockListener); + player.setAudioCodecParameters(testParameters); + advance(player).untilPendingCommandsAreFullyHandled(); + shadowOf(Looper.getMainLooper()).idle(); + + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(CodecParameters.class); + verify(mockListener).onCodecParametersChanged(paramsCaptor.capture()); + assertThat(paramsCaptor.getValue().get("test-key")).isEqualTo(123); + + // Test clearing + player.setAudioCodecParameters(new CodecParameters()); + advance(player).untilPendingCommandsAreFullyHandled(); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mockListener, times(2)).onCodecParametersChanged(paramsCaptor.capture()); + assertThat(paramsCaptor.getValue().get("test-key")).isNull(); + + 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..0c7465bbdfb 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,11 +27,16 @@ 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.never; +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; @@ -43,7 +48,11 @@ import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.util.Clock; +import androidx.media3.common.util.MediaFormatUtil; +import androidx.media3.exoplayer.CodecParameters; +import androidx.media3.exoplayer.CodecParametersChangeListener; 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 +60,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 +71,9 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Before; import org.junit.Rule; @@ -1379,6 +1391,305 @@ public void isReady_returnsTrueOnceAudioSinkHasPendingData() throws Exception { assertThat(isReadyAfterPendingData).isTrue(); } + @Test + @Config(sdk = 29) + public void setAudioCodecParameters_onEnabledRenderer_appliesToCodecInstance() throws Exception { + MediaCodecAdapter mockCodecAdapter = setUpAndEnableRenderer(AUDIO_AAC); + CodecParameters testParameters = new CodecParameters(); + testParameters.setInteger("test-key", 42); + + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_SET_CODEC_PARAMETERS, testParameters); + shadowOf(Looper.getMainLooper()).idle(); + + 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_PARAMETERS, new CodecParameters()); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mockCodecAdapter, times(2)).setParameters(bundleCaptor.capture()); + assertThat(bundleCaptor.getValue().isEmpty()).isTrue(); + } + + @Test + @Config(sdk = 29) + public void setAudioCodecParameters_beforeRendererEnabled_parametersAppliedDuringConfiguration() + throws Exception { + MediaCodecAdapter mockCodecAdapter = mock(MediaCodecAdapter.class); + MediaCodecAdapter.Factory mockCodecAdapterFactory = mock(MediaCodecAdapter.Factory.class); + when(mockCodecAdapterFactory.createAdapter(any())).thenReturn(mockCodecAdapter); + setUpRenderer(mockCodecAdapterFactory); + CodecParameters testParameters = new CodecParameters(); + testParameters.setInteger("test-key", 101); + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_SET_CODEC_PARAMETERS, testParameters); + + enableRenderer(mockCodecAdapter, AUDIO_AAC); + + ArgumentCaptor configurationCaptor = + ArgumentCaptor.forClass(MediaCodecAdapter.Configuration.class); + verify(mockCodecAdapterFactory).createAdapter(configurationCaptor.capture()); + MediaFormat mediaFormat = configurationCaptor.getValue().mediaFormat; + assertThat(mediaFormat.getInteger("test-key")).isEqualTo(101); + } + + @Test + @Config(sdk = 29) + public void onOutputFormatChanged_listenerNotifiedOnValueChange() throws Exception { + MediaCodecAdapter unused = setUpAndEnableRenderer(AUDIO_AAC); + CodecParametersChangeListener mockListener = mock(CodecParametersChangeListener.class); + when(mockListener.getSubscribedKeys()).thenReturn(ImmutableList.of("test-key")); + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_ADD_CODEC_PARAMETERS_LISTENER, mockListener); + MediaFormat mediaFormat = MediaFormatUtil.createMediaFormatFromFormat(AUDIO_AAC); + mediaFormat.setInteger("test-key", 100); + + mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat); + + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(CodecParameters.class); + verify(mockListener).onCodecParametersChanged(paramsCaptor.capture()); + assertThat(paramsCaptor.getValue().get("test-key")).isEqualTo(100); + + mediaFormat.setInteger("test-key", 200); + + mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat); + + verify(mockListener, times(2)).onCodecParametersChanged(paramsCaptor.capture()); + assertThat(paramsCaptor.getValue().get("test-key")).isEqualTo(200); + } + + @Test + @Config(sdk = 29) + public void onOutputFormatChanged_listenerNotifiedOnKeyAdded() throws Exception { + MediaCodecAdapter unused = setUpAndEnableRenderer(AUDIO_AAC); + CodecParametersChangeListener mockListener = mock(CodecParametersChangeListener.class); + when(mockListener.getSubscribedKeys()).thenReturn(ImmutableList.of("test-key")); + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_ADD_CODEC_PARAMETERS_LISTENER, mockListener); + MediaFormat mediaFormat = MediaFormatUtil.createMediaFormatFromFormat(AUDIO_AAC); + + mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat); + + verify(mockListener, never()).onCodecParametersChanged(any()); + + mediaFormat.setInteger("test-key", 100); + + mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat); + + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(CodecParameters.class); + verify(mockListener).onCodecParametersChanged(paramsCaptor.capture()); + assertThat(paramsCaptor.getValue().get("test-key")).isEqualTo(100); + } + + @Test + @Config(sdk = 29) + public void onOutputFormatChanged_listenerNotifiedOnKeyRemoved() throws Exception { + MediaCodecAdapter unused = setUpAndEnableRenderer(AUDIO_AAC); + CodecParametersChangeListener mockListener = mock(CodecParametersChangeListener.class); + when(mockListener.getSubscribedKeys()).thenReturn(ImmutableList.of("test-key")); + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_ADD_CODEC_PARAMETERS_LISTENER, mockListener); + MediaFormat mediaFormat = MediaFormatUtil.createMediaFormatFromFormat(AUDIO_AAC); + mediaFormat.setInteger("test-key", 100); + + mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat); + + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(CodecParameters.class); + verify(mockListener).onCodecParametersChanged(paramsCaptor.capture()); + assertThat(paramsCaptor.getValue().get("test-key")).isEqualTo(100); + + mediaFormat.removeKey("test-key"); + + mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat); + + verify(mockListener, times(2)).onCodecParametersChanged(paramsCaptor.capture()); + assertThat(paramsCaptor.getValue().keySet()).doesNotContain("test-key"); + } + + @Test + @Config(sdk = 29) + public void onOutputFormatChanged_listenerNotifiedOnValueNulled() throws Exception { + MediaCodecAdapter unused = setUpAndEnableRenderer(AUDIO_AAC); + CodecParametersChangeListener mockListener = mock(CodecParametersChangeListener.class); + when(mockListener.getSubscribedKeys()).thenReturn(ImmutableList.of("test-key")); + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_ADD_CODEC_PARAMETERS_LISTENER, mockListener); + MediaFormat mediaFormat = MediaFormatUtil.createMediaFormatFromFormat(AUDIO_AAC); + mediaFormat.setString("test-key", "initial-value"); + + mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat); + + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(CodecParameters.class); + verify(mockListener).onCodecParametersChanged(paramsCaptor.capture()); + assertThat(paramsCaptor.getValue().get("test-key")).isEqualTo("initial-value"); + + mediaFormat.setString("test-key", null); + + mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat); + + verify(mockListener, times(2)).onCodecParametersChanged(paramsCaptor.capture()); + assertThat(paramsCaptor.getValue().keySet()).doesNotContain("test-key"); + } + + @Test + @Config(sdk = 29) + public void onOutputFormatChanged_noSubscribedKeyChange_listenerNotCalled() throws Exception { + MediaCodecAdapter unused = setUpAndEnableRenderer(AUDIO_AAC); + CodecParametersChangeListener mockListener = mock(CodecParametersChangeListener.class); + when(mockListener.getSubscribedKeys()).thenReturn(ImmutableList.of("test-key")); + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_ADD_CODEC_PARAMETERS_LISTENER, mockListener); + MediaFormat mediaFormat = MediaFormatUtil.createMediaFormatFromFormat(AUDIO_AAC); + mediaFormat.setInteger("test-key", 100); + mediaFormat.setString("unsubscribed-key", "value1"); + + mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat); + + verify(mockListener).onCodecParametersChanged(any()); + + mediaFormat.setString("unsubscribed-key", "value2"); + + mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat); + + verify(mockListener).onCodecParametersChanged(any()); + } + + @Test + @Config(sdk = 29) + public void multipleCodecParametersChangeListeners_addAndRemove_notifiedOfUnionKeyChanges() + throws Exception { + MediaCodecAdapter unused = setUpAndEnableRenderer(AUDIO_AAC); + CodecParametersChangeListener listener1 = mock(CodecParametersChangeListener.class); + when(listener1.getSubscribedKeys()).thenReturn(Arrays.asList("keyA", "keyB")); + CodecParametersChangeListener listener2 = mock(CodecParametersChangeListener.class); + when(listener2.getSubscribedKeys()).thenReturn(Arrays.asList("keyB", "keyC")); + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_ADD_CODEC_PARAMETERS_LISTENER, listener1); + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_ADD_CODEC_PARAMETERS_LISTENER, listener2); + MediaFormat mediaFormat = MediaFormatUtil.createMediaFormatFromFormat(AUDIO_AAC); + mediaFormat.setInteger("keyA", 10); + mediaFormat.setInteger("keyB", 20); + mediaFormat.setInteger("keyC", 30); + + mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat); + + // Verify listener1 and listener2 get the first update with all keys {A, B, C}. + ArgumentCaptor captor1 = ArgumentCaptor.forClass(CodecParameters.class); + verify(listener1).onCodecParametersChanged(captor1.capture()); + assertThat(captor1.getValue().keySet()).containsExactly("keyA", "keyB", "keyC"); + + ArgumentCaptor captor2 = ArgumentCaptor.forClass(CodecParameters.class); + verify(listener2).onCodecParametersChanged(captor2.capture()); + assertThat(captor2.getValue().keySet()).containsExactly("keyA", "keyB", "keyC"); + + // Remove listener2. + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_REMOVE_CODEC_PARAMETERS_LISTENER, listener2); + + mediaFormat.setInteger("keyA", 1000); + mediaFormat.setInteger("keyC", 3000); + mediaCodecAudioRenderer.onOutputFormatChanged(AUDIO_AAC, mediaFormat); + + // Listener1 should be called, but only keys "keyA" and "keyB" should be present. + verify(listener1, times(2)).onCodecParametersChanged(captor1.capture()); + CodecParameters params1 = captor1.getValue(); + assertThat(params1.keySet()).containsExactly("keyA", "keyB"); + assertThat(params1.get("keyA")).isEqualTo(1000); + assertThat(params1.get("keyB")).isEqualTo(20); + // Listener2 should not be called again. + verify(listener2).onCodecParametersChanged(any()); + } + + @Test + @Config(sdk = 31) + public void updateSubscribedKeys_api31_managesSubscriptions() throws Exception { + MediaCodecAdapter mockCodecAdapter = setUpAndEnableRenderer(AUDIO_AAC); + CodecParametersChangeListener listener1 = mock(CodecParametersChangeListener.class); + when(listener1.getSubscribedKeys()).thenReturn(Arrays.asList("key1", "key2")); + CodecParametersChangeListener listener2 = mock(CodecParametersChangeListener.class); + when(listener2.getSubscribedKeys()).thenReturn(Arrays.asList("key3", "key1")); + @SuppressWarnings("unchecked") // Method being verified is known to take List. + ArgumentCaptor> stringListCaptor = ArgumentCaptor.forClass(List.class); + + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_ADD_CODEC_PARAMETERS_LISTENER, listener1); + + verify(mockCodecAdapter).subscribeToVendorParameters(stringListCaptor.capture()); + assertThat(stringListCaptor.getValue()).containsExactly("key1", "key2"); + + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_ADD_CODEC_PARAMETERS_LISTENER, listener2); + + verify(mockCodecAdapter, times(2)).subscribeToVendorParameters(stringListCaptor.capture()); + assertThat(stringListCaptor.getValue()).containsExactly("key3"); + + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_REMOVE_CODEC_PARAMETERS_LISTENER, listener1); + + verify(mockCodecAdapter).unsubscribeFromVendorParameters(stringListCaptor.capture()); + assertThat(stringListCaptor.getValue()).containsExactly("key2"); + + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_REMOVE_CODEC_PARAMETERS_LISTENER, listener2); + + verify(mockCodecAdapter, times(2)).unsubscribeFromVendorParameters(stringListCaptor.capture()); + assertThat(stringListCaptor.getValue()).containsExactly("key1", "key3"); + } + + @Test + @Config(sdk = 29) + public void updateSubscribedKeys_api29_doesNotCallSubscribeUnsubscribe() throws Exception { + MediaCodecAdapter mockCodecAdapter = setUpAndEnableRenderer(AUDIO_AAC); + CodecParametersChangeListener listener = mock(CodecParametersChangeListener.class); + when(listener.getSubscribedKeys()).thenReturn(Arrays.asList("key1", "key2")); + + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_ADD_CODEC_PARAMETERS_LISTENER, listener); + mediaCodecAudioRenderer.handleMessage(Renderer.MSG_REMOVE_CODEC_PARAMETERS_LISTENER, listener); + + verify(mockCodecAdapter, never()).subscribeToVendorParameters(any()); + verify(mockCodecAdapter, never()).unsubscribeFromVendorParameters(any()); + } + + private MediaCodecAdapter setUpAndEnableRenderer(Format format) throws Exception { + MediaCodecAdapter mockCodecAdapter = mock(MediaCodecAdapter.class); + MediaCodecAdapter.Factory mockCodecAdapterFactory = configuration -> mockCodecAdapter; + setUpRenderer(mockCodecAdapterFactory); + enableRenderer(mockCodecAdapter, format); + return mockCodecAdapter; + } + + private void setUpRenderer(MediaCodecAdapter.Factory mockCodecAdapterFactory) { + mediaCodecAudioRenderer = + new MediaCodecAudioRenderer( + ApplicationProvider.getApplicationContext(), + mockCodecAdapterFactory, + mediaCodecSelector, + /* enableDecoderFallback= */ false, + new Handler(Looper.getMainLooper()), + audioRendererEventListener, + audioSink); + mediaCodecAudioRenderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT); + } + + private void enableRenderer(MediaCodecAdapter mockCodecAdapter, Format format) throws Exception { + when(mockCodecAdapter.dequeueInputBufferIndex()).thenReturn(MediaCodec.INFO_TRY_AGAIN_LATER); + when(mockCodecAdapter.dequeueOutputBufferIndex(any())) + .thenReturn(MediaCodec.INFO_TRY_AGAIN_LATER); + + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + format, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + mediaCodecAudioRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {format}, + 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); + maybeIdleAsynchronousMediaCodecAdapterThreads(); + } + private void maybeIdleAsynchronousMediaCodecAdapterThreads() { if (queueingThread != null) { shadowOf(queueingThread.getLooper()).idle(); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java index 63be1012d93..c9b7f1d9652 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java @@ -886,6 +886,16 @@ public void setVideoScalingMode(@C.VideoScalingMode int scalingMode) { throw exceptionSupplier.get(); } + @Override + public void subscribeToVendorParameters(List names) { + throw exceptionSupplier.get(); + } + + @Override + public void unsubscribeFromVendorParameters(List names) { + throw exceptionSupplier.get(); + } + @Override public boolean needsReconfiguration() { return false; 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 7320f907ad1..e540ab4f0da 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 @@ -18,16 +18,17 @@ import android.media.AudioDeviceInfo; import android.os.Looper; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; 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; import androidx.media3.common.util.Clock; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.CodecParameters; +import androidx.media3.exoplayer.CodecParametersChangeListener; import androidx.media3.exoplayer.DecoderCounters; import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.ExoPlayer; @@ -447,13 +448,20 @@ public void setImageOutput(@Nullable ImageOutput imageOutput) { } @Override - public void setCodecParameter(@Nullable CodecParameter codecParameter) { + @RequiresApi(29) + public void setAudioCodecParameters(CodecParameters codecParameters) { throw new UnsupportedOperationException(); } @Override - public void setCodecParametersChangeListener( - CodecParametersChangeListener codecParametersChangeListener) { + @RequiresApi(29) + public void addAudioCodecParametersChangeListener(CodecParametersChangeListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + @RequiresApi(29) + public void removeAudioCodecParametersChangeListener(CodecParametersChangeListener listener) { throw new UnsupportedOperationException(); } }