Android 12L: Always set codec max output channels

With this change, MediaCodecAudioRenderer always configures MediaCodec
with max output channels set to 99 on API 32+.

#minor-release

PiperOrigin-RevId: 427192801
This commit is contained in:
christosts 2022-02-08 16:02:04 +00:00 committed by Ian Baker
parent 11e5c67356
commit 1223fa23b0
2 changed files with 5 additions and 388 deletions

View File

@ -16,7 +16,6 @@
package androidx.media3.exoplayer.audio;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.exoplayer.DecoderReuseEvaluation.DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED;
import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_NO;
import static com.google.common.base.MoreObjects.firstNonNull;
@ -30,9 +29,7 @@ import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.os.Handler;
import androidx.annotation.CallSuper;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.AuxEffectInfo;
import androidx.media3.common.C;
@ -63,10 +60,8 @@ import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.common.collect.ImmutableList;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Decodes and renders audio using {@link MediaCodec} and an {@link AudioSink}.
@ -103,7 +98,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private final Context context;
private final EventDispatcher eventDispatcher;
private final AudioSink audioSink;
private final SpatializationHelper spatializationHelper;
private int codecMaxInputSize;
private boolean codecNeedsDiscardChannelsWorkaround;
@ -263,7 +257,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
this.context = context;
this.audioSink = audioSink;
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
spatializationHelper = new SpatializationHelper(context, audioSink.getAudioAttributes());
audioSink.setListener(new AudioSinkListener());
}
@ -421,11 +414,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return audioSink.supportsFormat(format);
}
@Override
protected boolean shouldReinitCodec() {
return spatializationHelper.shouldReinitCodec();
}
@Override
protected MediaCodecAdapter.Configuration getMediaCodecConfiguration(
MediaCodecInfo codecInfo,
@ -490,7 +478,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
MediaCodecAdapter.Configuration configuration,
long initializedTimestampMs,
long initializationDurationMs) {
spatializationHelper.onCodecInitialized(configuration);
eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs);
}
@ -581,7 +568,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
audioSink.disableTunneling();
}
audioSink.setPlayerId(getPlayerId());
spatializationHelper.enable();
}
@Override
@ -634,7 +620,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
audioSinkNeedsReset = false;
audioSink.reset();
}
spatializationHelper.reset();
}
}
@ -759,7 +744,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
case MSG_SET_AUDIO_ATTRIBUTES:
AudioAttributes audioAttributes = (AudioAttributes) message;
audioSink.setAudioAttributes(audioAttributes);
spatializationHelper.setAudioAttributes(audioSink.getAudioAttributes());
break;
case MSG_SET_AUX_EFFECT_INFO:
AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message;
@ -871,7 +855,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
== AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY) {
mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_FLOAT);
}
spatializationHelper.configureForSpatialization(mediaFormat, format);
if (Util.SDK_INT >= 32) {
// TODO[b/190759307] Use MediaFormat.KEY_MAX_OUTPUT_CHANNEL_COUNT once the
// compile SDK target is set to 32.
mediaFormat.setInteger("max-output-channel-count", 99);
}
return mediaFormat;
}
@ -956,163 +944,4 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
eventDispatcher.audioSinkError(audioSinkError);
}
}
/**
* A helper class that signals whether the codec needs to be re-initialized because spatialization
* properties changed.
*/
private static final class SpatializationHelper implements SpatializerDelegate.Listener {
// TODO[b/190759307] Remove and use MediaFormat.KEY_MAX_OUTPUT_CHANNEL_COUNT once the
// compile SDK target is set to 32.
private static final String KEY_MAX_OUTPUT_CHANNEL_COUNT = "max-output-channel-count";
private static final int SPATIALIZATION_CHANNEL_COUNT = 99;
@Nullable private final SpatializerDelegate spatializerDelegate;
private @MonotonicNonNull Handler handler;
@Nullable private AudioAttributes audioAttributes;
@Nullable private Format inputFormat;
private boolean codecConfiguredForSpatialization;
private boolean codecNeedsReinit;
private boolean listenerAdded;
/** Creates a new instance. */
public SpatializationHelper(Context context, @Nullable AudioAttributes audioAttributes) {
this.spatializerDelegate = maybeCreateSpatializer(context);
this.audioAttributes = audioAttributes;
}
/** Enables this helper. Call this method when the renderer is enabled. */
public void enable() {
maybeAddSpatalizationListener();
}
/** Resets the helper and releases any resources. Call this method when renderer is reset. */
public void reset() {
maybeRemoveSpatalizationListener();
}
/** Sets the audio attributes set by the player. */
public void setAudioAttributes(@Nullable AudioAttributes audioAttributes) {
if (Util.areEqual(this.audioAttributes, audioAttributes)) {
return;
}
this.audioAttributes = audioAttributes;
updateCodecNeedsReinit();
}
/**
* Sets keys for audio spatialization on the {@code mediaFormat} if the platform can apply
* spatialization to this {@code format}.
*/
public void configureForSpatialization(MediaFormat mediaFormat, Format format) {
if (canBeSpatialized(format)) {
mediaFormat.setInteger(KEY_MAX_OUTPUT_CHANNEL_COUNT, SPATIALIZATION_CHANNEL_COUNT);
}
}
/** Informs the helper that a codec was initialized. */
public void onCodecInitialized(MediaCodecAdapter.Configuration configuration) {
codecNeedsReinit = false;
inputFormat = configuration.format;
codecConfiguredForSpatialization =
configuration.mediaFormat.containsKey(KEY_MAX_OUTPUT_CHANNEL_COUNT)
&& configuration.mediaFormat.getInteger(KEY_MAX_OUTPUT_CHANNEL_COUNT)
== SPATIALIZATION_CHANNEL_COUNT;
}
/**
* Returns whether the codec should be re-initialized, caused by a change in the spatialization
* properties.
*/
public boolean shouldReinitCodec() {
return codecNeedsReinit;
}
// SpatializerDelegate.Listener
@Override
public void onSpatializerEnabledChanged(SpatializerDelegate spatializer, boolean enabled) {
updateCodecNeedsReinit();
}
@Override
public void onSpatializerAvailableChanged(SpatializerDelegate spatializer, boolean available) {
updateCodecNeedsReinit();
}
// Other internal methods
/** Returns whether this format can be spatialized by the platform. */
private boolean canBeSpatialized(@Nullable Format format) {
if (Util.SDK_INT < 32
|| format == null
|| audioAttributes == null
|| spatializerDelegate == null
|| spatializerDelegate.getImmersiveAudioLevel()
!= SpatializerDelegate.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL
|| !spatializerDelegate.isAvailable()
|| !spatializerDelegate.isEnabled()) {
return false;
}
AudioFormat.Builder audioFormatBuilder =
new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setChannelMask(Util.getAudioTrackChannelConfig(format.channelCount));
if (format.sampleRate != Format.NO_VALUE) {
audioFormatBuilder.setSampleRate(format.sampleRate);
}
return spatializerDelegate.canBeSpatialized(
audioAttributes.getAudioAttributesV21(), audioFormatBuilder.build());
}
private void maybeAddSpatalizationListener() {
if (!listenerAdded && spatializerDelegate != null && Util.SDK_INT >= 32) {
if (handler == null) {
// Route callbacks to the playback thread.
handler = Util.createHandlerForCurrentLooper();
}
spatializerDelegate.addOnSpatializerStateChangedListener(handler::post, this);
listenerAdded = true;
}
}
private void maybeRemoveSpatalizationListener() {
if (listenerAdded && spatializerDelegate != null && Util.SDK_INT >= 32) {
spatializerDelegate.removeOnSpatializerStateChangedListener(this);
checkStateNotNull(handler).removeCallbacksAndMessages(null);
}
}
private void updateCodecNeedsReinit() {
codecNeedsReinit = codecConfiguredForSpatialization != canBeSpatialized(inputFormat);
}
@Nullable
private static SpatializerDelegate maybeCreateSpatializer(Context context) {
if (Util.SDK_INT >= 32) {
return Api32.createSpatializer(context);
}
return null;
}
}
@RequiresApi(32)
private static final class Api32 {
private Api32() {}
@DoNotInline
@Nullable
public static SpatializerDelegate createSpatializer(Context context) {
try {
return new SpatializerDelegate(context);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
// Do nothing for these cases.
} catch (InvocationTargetException e) {
Log.w(TAG, "Failed to load Spatializer with reflection", e);
}
return null;
}
}
}

View File

@ -1,212 +0,0 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.audio;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* Exposes the android.media.Spatializer API via reflection. This is so that we can use the
* Spatializer while the compile SDK target is set to 31.
*/
@RequiresApi(31)
/* package */ final class SpatializerDelegate {
/** Level of support for audio spatialization. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL,
SPATIALIZER_IMMERSIVE_LEVEL_NONE,
SPATIALIZER_IMMERSIVE_LEVEL_OTHER
})
@interface ImmersiveAudioLevel {}
/** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL */
public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1;
/** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_NONE */
public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0;
/** See Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_OTHER */
public static final int SPATIALIZER_IMMERSIVE_LEVEL_OTHER = -1;
/** Wrapper for Spatializer.OnSpatializerStateChangedListener */
public interface Listener {
/** See Spatializer.OnSpatializerStateChangedListener.onSpatializerEnabledChanged */
void onSpatializerEnabledChanged(SpatializerDelegate spatializer, boolean enabled);
/** See Spatializer.OnSpatializerStateChangedListener.onSpatializerAvailableChanged */
void onSpatializerAvailableChanged(SpatializerDelegate spatializer, boolean available);
}
private final Object spatializer;
private final Class<?> spatializerClass;
private final Class<?> spatializerListenerClass;
private final Method isEnabled;
private final Method isAvailable;
private final Method getImmersiveAudioLevel;
private final Method canBeSpatialized;
private final Method addListener;
private final Method removeListener;
private final Map<Listener, Object> listeners;
/** Creates an instance. */
public SpatializerDelegate(Context context)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
IllegalAccessException {
Method getSpatializerMethod = AudioManager.class.getMethod("getSpatializer");
AudioManager manager =
Assertions.checkNotNull(
(AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE));
spatializer = checkStateNotNull(getSpatializerMethod.invoke(manager));
spatializerClass = Class.forName("android.media.Spatializer");
spatializerListenerClass =
Class.forName("android.media.Spatializer$OnSpatializerStateChangedListener");
isEnabled = spatializerClass.getMethod("isEnabled");
isAvailable = spatializerClass.getMethod("isAvailable");
getImmersiveAudioLevel = spatializerClass.getMethod("getImmersiveAudioLevel");
canBeSpatialized =
spatializerClass.getMethod(
"canBeSpatialized", android.media.AudioAttributes.class, AudioFormat.class);
addListener =
spatializerClass.getMethod(
"addOnSpatializerStateChangedListener", Executor.class, spatializerListenerClass);
removeListener =
spatializerClass.getMethod(
"removeOnSpatializerStateChangedListener", spatializerListenerClass);
listeners = new HashMap<>();
}
/** Delegates to Spatializer.isEnabled() */
public boolean isEnabled() {
try {
return (boolean) Util.castNonNull(isEnabled.invoke(spatializer));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.isAvailable() */
public boolean isAvailable() {
try {
return (boolean) Util.castNonNull(isAvailable.invoke(spatializer));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.getImmersiveAudioLevel() */
public @ImmersiveAudioLevel int getImmersiveAudioLevel() {
try {
return (int) Util.castNonNull(getImmersiveAudioLevel.invoke(spatializer));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.canBeSpatialized() */
public boolean canBeSpatialized(AudioAttributes attributes, AudioFormat format) {
try {
return (boolean) Util.castNonNull(canBeSpatialized.invoke(spatializer, attributes, format));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.addOnSpatializerStateChangedListener() */
public void addOnSpatializerStateChangedListener(Executor executor, Listener listener) {
if (listeners.containsKey(listener)) {
return;
}
Object listenerProxy = createSpatializerListenerProxy(listener);
try {
addListener.invoke(spatializer, executor, listenerProxy);
listeners.put(listener, listenerProxy);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/** Delegates to Spatializer.removeOnSpatializerStateChangedListener() */
public void removeOnSpatializerStateChangedListener(Listener listener) {
@Nullable Object proxy = listeners.get(listener);
if (proxy == null) {
return;
}
try {
removeListener.invoke(spatializer, proxy);
listeners.remove(listener);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
private Object createSpatializerListenerProxy(Listener listener) {
return Proxy.newProxyInstance(
spatializerListenerClass.getClassLoader(),
new Class<?>[] {spatializerListenerClass},
new ProxySpatializerListener(this, listener));
}
/** Proxy-based implementation of Spatializer.OnSpatializerStateChangedListener. */
private static final class ProxySpatializerListener implements InvocationHandler {
private final SpatializerDelegate spatializerDelegate;
private final Listener listener;
private ProxySpatializerListener(SpatializerDelegate spatializerDelegate, Listener listener) {
this.spatializerDelegate = spatializerDelegate;
this.listener = listener;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (methodName.equals("onSpatializerAvailableChanged")
&& parameterTypes.length == 2
&& spatializerDelegate.spatializerClass.isAssignableFrom(parameterTypes[0])
&& parameterTypes[1].equals(Boolean.TYPE)) {
listener.onSpatializerAvailableChanged(spatializerDelegate, (boolean) objects[1]);
} else if (methodName.equals("onSpatializerEnabledChanged")
&& parameterTypes.length == 2
&& spatializerDelegate.spatializerClass.isAssignableFrom(parameterTypes[0])
&& parameterTypes[1].equals(Boolean.TYPE)) {
listener.onSpatializerEnabledChanged(spatializerDelegate, (boolean) objects[1]);
}
return this;
}
}
}