Add MediaCodec loudness controller for API35+

This controller connects the audio output to the MediaCodec so
that it can automatically propagate CTA-2075 loudness metadata.

PiperOrigin-RevId: 653628503
This commit is contained in:
tonihei 2024-07-18 08:27:39 -07:00 committed by Copybara-Service
parent a52df6d29e
commit f7a726bb11
8 changed files with 236 additions and 12 deletions

View File

@ -38,6 +38,8 @@
extension 7 instead of API level 34 extension 7 instead of API level 34
([#1262](https://github.com/androidx/media/issues/1262)). ([#1262](https://github.com/androidx/media/issues/1262)).
* Audio: * Audio:
* Automatically configure CTA-2075 loudness metadata on the codec if
present in the media.
* Video: * Video:
* `MediaCodecVideoRenderer` avoids decoding samples that are neither * `MediaCodecVideoRenderer` avoids decoding samples that are neither
rendered nor used as reference by other samples. rendered nor used as reference by other samples.

View File

@ -58,6 +58,7 @@ import androidx.media3.exoplayer.RendererCapabilities;
import androidx.media3.exoplayer.audio.AudioRendererEventListener.EventDispatcher; import androidx.media3.exoplayer.audio.AudioRendererEventListener.EventDispatcher;
import androidx.media3.exoplayer.audio.AudioSink.InitializationException; import androidx.media3.exoplayer.audio.AudioSink.InitializationException;
import androidx.media3.exoplayer.audio.AudioSink.WriteException; import androidx.media3.exoplayer.audio.AudioSink.WriteException;
import androidx.media3.exoplayer.mediacodec.LoudnessCodecController;
import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter; import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter;
import androidx.media3.exoplayer.mediacodec.MediaCodecInfo; import androidx.media3.exoplayer.mediacodec.MediaCodecInfo;
import androidx.media3.exoplayer.mediacodec.MediaCodecRenderer; import androidx.media3.exoplayer.mediacodec.MediaCodecRenderer;
@ -107,6 +108,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private final Context context; private final Context context;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final AudioSink audioSink; private final AudioSink audioSink;
@Nullable private final LoudnessCodecController loudnessCodecController;
private int codecMaxInputSize; private int codecMaxInputSize;
private boolean codecNeedsDiscardChannelsWorkaround; private boolean codecNeedsDiscardChannelsWorkaround;
@ -252,6 +254,43 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Nullable Handler eventHandler, @Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener, @Nullable AudioRendererEventListener eventListener,
AudioSink audioSink) { AudioSink audioSink) {
this(
context,
codecAdapterFactory,
mediaCodecSelector,
enableDecoderFallback,
eventHandler,
eventListener,
audioSink,
Util.SDK_INT >= 35 ? new LoudnessCodecController() : null);
}
/**
* Creates a new instance.
*
* @param context A context.
* @param codecAdapterFactory The {@link MediaCodecAdapter.Factory} used to create {@link
* MediaCodecAdapter} instances.
* @param mediaCodecSelector A decoder selector.
* @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder
* initialization fails. This may result in using a decoder that is slower/less efficient than
* the primary decoder.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioSink The sink to which audio will be output.
* @param loudnessCodecController The {@link LoudnessCodecController}, or null to not control
* loudness.
*/
public MediaCodecAudioRenderer(
Context context,
MediaCodecAdapter.Factory codecAdapterFactory,
MediaCodecSelector mediaCodecSelector,
boolean enableDecoderFallback,
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
AudioSink audioSink,
@Nullable LoudnessCodecController loudnessCodecController) {
super( super(
C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_AUDIO,
codecAdapterFactory, codecAdapterFactory,
@ -261,6 +300,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
context = context.getApplicationContext(); context = context.getApplicationContext();
this.context = context; this.context = context;
this.audioSink = audioSink; this.audioSink = audioSink;
this.loudnessCodecController = loudnessCodecController;
rendererPriority = C.PRIORITY_PLAYBACK; rendererPriority = C.PRIORITY_PLAYBACK;
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
nextBufferToWritePresentationTimeUs = C.TIME_UNSET; nextBufferToWritePresentationTimeUs = C.TIME_UNSET;
@ -443,7 +483,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
&& !MimeTypes.AUDIO_RAW.equals(format.sampleMimeType); && !MimeTypes.AUDIO_RAW.equals(format.sampleMimeType);
decryptOnlyCodecFormat = decryptOnlyCodecEnabled ? format : null; decryptOnlyCodecFormat = decryptOnlyCodecEnabled ? format : null;
return MediaCodecAdapter.Configuration.createForAudioDecoding( return MediaCodecAdapter.Configuration.createForAudioDecoding(
codecInfo, mediaFormat, format, crypto); codecInfo, mediaFormat, format, crypto, loudnessCodecController);
} }
@Override @Override
@ -688,6 +728,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override @Override
protected void onRelease() { protected void onRelease() {
audioSink.release(); audioSink.release();
if (Util.SDK_INT >= 35 && loudnessCodecController != null) {
loudnessCodecController.release();
}
} }
@Override @Override
@ -851,7 +894,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
audioSink.setSkipSilenceEnabled((Boolean) checkNotNull(message)); audioSink.setSkipSilenceEnabled((Boolean) checkNotNull(message));
break; break;
case MSG_SET_AUDIO_SESSION_ID: case MSG_SET_AUDIO_SESSION_ID:
audioSink.setAudioSessionId((Integer) checkNotNull(message)); setAudioSessionId((int) checkNotNull(message));
break; break;
case MSG_SET_PRIORITY: case MSG_SET_PRIORITY:
rendererPriority = (int) checkNotNull(message); rendererPriority = (int) checkNotNull(message);
@ -974,6 +1017,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return mediaFormat; return mediaFormat;
} }
private void setAudioSessionId(int audioSessionId) {
audioSink.setAudioSessionId(audioSessionId);
if (Util.SDK_INT >= 35 && loudnessCodecController != null) {
loudnessCodecController.setAudioSessionId(audioSessionId);
}
}
private void updateCodecImportance() { private void updateCodecImportance() {
@Nullable MediaCodecAdapter codec = getCodec(); @Nullable MediaCodecAdapter codec = getCodec();
if (codec == null) { if (codec == null) {

View File

@ -114,7 +114,11 @@ import java.nio.ByteBuffer;
new AsynchronousMediaCodecBufferEnqueuer(codec, queueingThreadSupplier.get()); new AsynchronousMediaCodecBufferEnqueuer(codec, queueingThreadSupplier.get());
} }
codecAdapter = codecAdapter =
new AsynchronousMediaCodecAdapter(codec, callbackThreadSupplier.get(), bufferEnqueuer); new AsynchronousMediaCodecAdapter(
codec,
callbackThreadSupplier.get(),
bufferEnqueuer,
configuration.loudnessCodecController);
TraceUtil.endSection(); TraceUtil.endSection();
if (configuration.surface == null if (configuration.surface == null
&& configuration.codecInfo.detachedSurfaceSupported && configuration.codecInfo.detachedSurfaceSupported
@ -157,14 +161,20 @@ import java.nio.ByteBuffer;
private final MediaCodec codec; private final MediaCodec codec;
private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback; private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback;
private final MediaCodecBufferEnqueuer bufferEnqueuer; private final MediaCodecBufferEnqueuer bufferEnqueuer;
@Nullable private final LoudnessCodecController loudnessCodecController;
private boolean codecReleased; private boolean codecReleased;
private @State int state; private @State int state;
private AsynchronousMediaCodecAdapter( private AsynchronousMediaCodecAdapter(
MediaCodec codec, HandlerThread callbackThread, MediaCodecBufferEnqueuer bufferEnqueuer) { MediaCodec codec,
HandlerThread callbackThread,
MediaCodecBufferEnqueuer bufferEnqueuer,
@Nullable LoudnessCodecController loudnessCodecController) {
this.codec = codec; this.codec = codec;
this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread); this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread);
this.bufferEnqueuer = bufferEnqueuer; this.bufferEnqueuer = bufferEnqueuer;
this.loudnessCodecController = loudnessCodecController;
this.state = STATE_CREATED; this.state = STATE_CREATED;
} }
@ -181,6 +191,9 @@ import java.nio.ByteBuffer;
TraceUtil.beginSection("startCodec"); TraceUtil.beginSection("startCodec");
codec.start(); codec.start();
TraceUtil.endSection(); TraceUtil.endSection();
if (Util.SDK_INT >= 35 && loudnessCodecController != null) {
loudnessCodecController.addMediaCodec(codec);
}
state = STATE_INITIALIZED; state = STATE_INITIALIZED;
} }
@ -273,6 +286,9 @@ import java.nio.ByteBuffer;
codec.stop(); codec.stop();
} }
} finally { } finally {
if (Util.SDK_INT >= 35 && loudnessCodecController != null) {
loudnessCodecController.removeMediaCodec(codec);
}
codec.release(); codec.release();
codecReleased = true; codecReleased = true;
} }

View File

@ -0,0 +1,137 @@
/*
* Copyright 2024 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.mediacodec;
import static androidx.media3.common.util.Assertions.checkState;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import android.media.LoudnessCodecController.OnLoudnessCodecUpdateListener;
import android.media.MediaCodec;
import android.os.Bundle;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.util.UnstableApi;
import java.util.HashSet;
import java.util.Iterator;
/** Wrapper class for the platform {@link android.media.LoudnessCodecController}. */
@RequiresApi(35)
@UnstableApi
public final class LoudnessCodecController {
/** Interface to intercept and modify loudness parameters before applying them to the codec. */
public interface LoudnessParameterUpdateListener {
/** The default update listener returning an unmodified set of parameters. */
LoudnessParameterUpdateListener DEFAULT = bundle -> bundle;
/**
* Returns the updated loudness parameters to be applied to the codec.
*
* @param parameters The suggested loudness parameters.
* @return The updated loudness parameters.
*/
Bundle onLoudnessParameterUpdate(Bundle parameters);
}
private final HashSet<MediaCodec> mediaCodecs;
private final LoudnessParameterUpdateListener updateListener;
@Nullable private android.media.LoudnessCodecController loudnessCodecController;
/** Creates the loudness controller. */
public LoudnessCodecController() {
this(LoudnessParameterUpdateListener.DEFAULT);
}
/**
* Creates the loudness controller.
*
* @param updateListener The {@link LoudnessParameterUpdateListener} to intercept and modify
* parameters.
*/
public LoudnessCodecController(LoudnessParameterUpdateListener updateListener) {
this.mediaCodecs = new HashSet<>();
this.updateListener = updateListener;
}
/**
* Configures the loudness controller with an audio session id.
*
* @param audioSessionId The audio session ID.
*/
@DoNotInline
public void setAudioSessionId(int audioSessionId) {
if (loudnessCodecController != null) {
loudnessCodecController.close();
loudnessCodecController = null;
}
android.media.LoudnessCodecController loudnessCodecController =
android.media.LoudnessCodecController.create(
audioSessionId,
directExecutor(),
new OnLoudnessCodecUpdateListener() {
@Override
public Bundle onLoudnessCodecUpdate(MediaCodec codec, Bundle parameters) {
return updateListener.onLoudnessParameterUpdate(parameters);
}
});
this.loudnessCodecController = loudnessCodecController;
for (Iterator<MediaCodec> it = mediaCodecs.iterator(); it.hasNext(); ) {
boolean registered = loudnessCodecController.addMediaCodec(it.next());
if (!registered) {
it.remove();
}
}
}
/**
* Adds a codec to be configured by the loudness controller.
*
* @param mediaCodec A {@link MediaCodec}.
*/
@DoNotInline
public void addMediaCodec(MediaCodec mediaCodec) {
if (loudnessCodecController != null && !loudnessCodecController.addMediaCodec(mediaCodec)) {
// Don't add codec if the existing loudness controller can't handle it.
return;
}
checkState(mediaCodecs.add(mediaCodec));
}
/**
* Removes a codec from being configured by the loudness controller.
*
* @param mediaCodec A {@link MediaCodec}.
*/
@DoNotInline
public void removeMediaCodec(MediaCodec mediaCodec) {
boolean removedCodec = mediaCodecs.remove(mediaCodec);
if (removedCodec && loudnessCodecController != null) {
loudnessCodecController.removeMediaCodec(mediaCodec);
}
}
/** Releases the loudness controller. */
@DoNotInline
public void release() {
mediaCodecs.clear();
if (loudnessCodecController != null) {
loudnessCodecController.close();
}
}
}

View File

@ -50,14 +50,17 @@ public interface MediaCodecAdapter {
* @param mediaFormat See {@link #mediaFormat}. * @param mediaFormat See {@link #mediaFormat}.
* @param format See {@link #format}. * @param format See {@link #format}.
* @param crypto See {@link #crypto}. * @param crypto See {@link #crypto}.
* @param loudnessCodecController See {@link #loudnessCodecController}.
* @return The created instance. * @return The created instance.
*/ */
public static Configuration createForAudioDecoding( public static Configuration createForAudioDecoding(
MediaCodecInfo codecInfo, MediaCodecInfo codecInfo,
MediaFormat mediaFormat, MediaFormat mediaFormat,
Format format, Format format,
@Nullable MediaCrypto crypto) { @Nullable MediaCrypto crypto,
return new Configuration(codecInfo, mediaFormat, format, /* surface= */ null, crypto); @Nullable LoudnessCodecController loudnessCodecController) {
return new Configuration(
codecInfo, mediaFormat, format, /* surface= */ null, crypto, loudnessCodecController);
} }
/** /**
@ -76,7 +79,8 @@ public interface MediaCodecAdapter {
Format format, Format format,
@Nullable Surface surface, @Nullable Surface surface,
@Nullable MediaCrypto crypto) { @Nullable MediaCrypto crypto) {
return new Configuration(codecInfo, mediaFormat, format, surface, crypto); return new Configuration(
codecInfo, mediaFormat, format, surface, crypto, /* loudnessCodecController= */ null);
} }
/** Information about the {@link MediaCodec} being configured. */ /** Information about the {@link MediaCodec} being configured. */
@ -98,17 +102,22 @@ public interface MediaCodecAdapter {
/** For DRM protected playbacks, a {@link MediaCrypto} to use for decryption. */ /** For DRM protected playbacks, a {@link MediaCrypto} to use for decryption. */
@Nullable public final MediaCrypto crypto; @Nullable public final MediaCrypto crypto;
/** The {@link LoudnessCodecController} for audio codecs. */
@Nullable public final LoudnessCodecController loudnessCodecController;
private Configuration( private Configuration(
MediaCodecInfo codecInfo, MediaCodecInfo codecInfo,
MediaFormat mediaFormat, MediaFormat mediaFormat,
Format format, Format format,
@Nullable Surface surface, @Nullable Surface surface,
@Nullable MediaCrypto crypto) { @Nullable MediaCrypto crypto,
@Nullable LoudnessCodecController loudnessCodecController) {
this.codecInfo = codecInfo; this.codecInfo = codecInfo;
this.mediaFormat = mediaFormat; this.mediaFormat = mediaFormat;
this.format = format; this.format = format;
this.surface = surface; this.surface = surface;
this.crypto = crypto; this.crypto = crypto;
this.loudnessCodecController = loudnessCodecController;
} }
} }

View File

@ -63,7 +63,7 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
TraceUtil.beginSection("startCodec"); TraceUtil.beginSection("startCodec");
codec.start(); codec.start();
TraceUtil.endSection(); TraceUtil.endSection();
return new SynchronousMediaCodecAdapter(codec); return new SynchronousMediaCodecAdapter(codec, configuration.loudnessCodecController);
} catch (IOException | RuntimeException e) { } catch (IOException | RuntimeException e) {
if (codec != null) { if (codec != null) {
codec.release(); codec.release();
@ -84,9 +84,15 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
} }
private final MediaCodec codec; private final MediaCodec codec;
@Nullable private final LoudnessCodecController loudnessCodecController;
private SynchronousMediaCodecAdapter(MediaCodec mediaCodec) { private SynchronousMediaCodecAdapter(
MediaCodec mediaCodec, @Nullable LoudnessCodecController loudnessCodecController) {
this.codec = mediaCodec; this.codec = mediaCodec;
this.loudnessCodecController = loudnessCodecController;
if (Util.SDK_INT >= 35 && loudnessCodecController != null) {
loudnessCodecController.addMediaCodec(codec);
}
} }
@Override @Override
@ -165,6 +171,9 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
codec.stop(); codec.stop();
} }
} finally { } finally {
if (Util.SDK_INT >= 35 && loudnessCodecController != null) {
loudnessCodecController.removeMediaCodec(codec);
}
codec.release(); codec.release();
} }
} }

View File

@ -47,7 +47,8 @@ public class AsynchronousMediaCodecAdapterTest {
codecInfo, codecInfo,
createMediaFormat("format"), createMediaFormat("format"),
new Format.Builder().build(), new Format.Builder().build(),
/* crypto= */ null); /* crypto= */ null,
/* loudnessCodecController= */ null);
callbackThread = new HandlerThread("TestCallbackThread"); callbackThread = new HandlerThread("TestCallbackThread");
queueingThread = new HandlerThread("TestQueueingThread"); queueingThread = new HandlerThread("TestQueueingThread");
adapter = adapter =

View File

@ -599,7 +599,7 @@ public class MediaCodecRendererTest {
@Nullable MediaCrypto crypto, @Nullable MediaCrypto crypto,
float codecOperatingRate) { float codecOperatingRate) {
return MediaCodecAdapter.Configuration.createForAudioDecoding( return MediaCodecAdapter.Configuration.createForAudioDecoding(
codecInfo, new MediaFormat(), format, crypto); codecInfo, new MediaFormat(), format, crypto, /* loudnessCodecController= */ null);
} }
@Override @Override