mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
a52df6d29e
commit
f7a726bb11
@ -38,6 +38,8 @@
|
||||
extension 7 instead of API level 34
|
||||
([#1262](https://github.com/androidx/media/issues/1262)).
|
||||
* Audio:
|
||||
* Automatically configure CTA-2075 loudness metadata on the codec if
|
||||
present in the media.
|
||||
* Video:
|
||||
* `MediaCodecVideoRenderer` avoids decoding samples that are neither
|
||||
rendered nor used as reference by other samples.
|
||||
|
@ -58,6 +58,7 @@ import androidx.media3.exoplayer.RendererCapabilities;
|
||||
import androidx.media3.exoplayer.audio.AudioRendererEventListener.EventDispatcher;
|
||||
import androidx.media3.exoplayer.audio.AudioSink.InitializationException;
|
||||
import androidx.media3.exoplayer.audio.AudioSink.WriteException;
|
||||
import androidx.media3.exoplayer.mediacodec.LoudnessCodecController;
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter;
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecInfo;
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecRenderer;
|
||||
@ -107,6 +108,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
private final Context context;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final AudioSink audioSink;
|
||||
@Nullable private final LoudnessCodecController loudnessCodecController;
|
||||
|
||||
private int codecMaxInputSize;
|
||||
private boolean codecNeedsDiscardChannelsWorkaround;
|
||||
@ -252,6 +254,43 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable AudioRendererEventListener eventListener,
|
||||
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(
|
||||
C.TRACK_TYPE_AUDIO,
|
||||
codecAdapterFactory,
|
||||
@ -261,6 +300,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
context = context.getApplicationContext();
|
||||
this.context = context;
|
||||
this.audioSink = audioSink;
|
||||
this.loudnessCodecController = loudnessCodecController;
|
||||
rendererPriority = C.PRIORITY_PLAYBACK;
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
nextBufferToWritePresentationTimeUs = C.TIME_UNSET;
|
||||
@ -443,7 +483,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
&& !MimeTypes.AUDIO_RAW.equals(format.sampleMimeType);
|
||||
decryptOnlyCodecFormat = decryptOnlyCodecEnabled ? format : null;
|
||||
return MediaCodecAdapter.Configuration.createForAudioDecoding(
|
||||
codecInfo, mediaFormat, format, crypto);
|
||||
codecInfo, mediaFormat, format, crypto, loudnessCodecController);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -688,6 +728,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
@Override
|
||||
protected void onRelease() {
|
||||
audioSink.release();
|
||||
if (Util.SDK_INT >= 35 && loudnessCodecController != null) {
|
||||
loudnessCodecController.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -851,7 +894,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
audioSink.setSkipSilenceEnabled((Boolean) checkNotNull(message));
|
||||
break;
|
||||
case MSG_SET_AUDIO_SESSION_ID:
|
||||
audioSink.setAudioSessionId((Integer) checkNotNull(message));
|
||||
setAudioSessionId((int) checkNotNull(message));
|
||||
break;
|
||||
case MSG_SET_PRIORITY:
|
||||
rendererPriority = (int) checkNotNull(message);
|
||||
@ -974,6 +1017,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
return mediaFormat;
|
||||
}
|
||||
|
||||
private void setAudioSessionId(int audioSessionId) {
|
||||
audioSink.setAudioSessionId(audioSessionId);
|
||||
if (Util.SDK_INT >= 35 && loudnessCodecController != null) {
|
||||
loudnessCodecController.setAudioSessionId(audioSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCodecImportance() {
|
||||
@Nullable MediaCodecAdapter codec = getCodec();
|
||||
if (codec == null) {
|
||||
|
@ -114,7 +114,11 @@ import java.nio.ByteBuffer;
|
||||
new AsynchronousMediaCodecBufferEnqueuer(codec, queueingThreadSupplier.get());
|
||||
}
|
||||
codecAdapter =
|
||||
new AsynchronousMediaCodecAdapter(codec, callbackThreadSupplier.get(), bufferEnqueuer);
|
||||
new AsynchronousMediaCodecAdapter(
|
||||
codec,
|
||||
callbackThreadSupplier.get(),
|
||||
bufferEnqueuer,
|
||||
configuration.loudnessCodecController);
|
||||
TraceUtil.endSection();
|
||||
if (configuration.surface == null
|
||||
&& configuration.codecInfo.detachedSurfaceSupported
|
||||
@ -157,14 +161,20 @@ import java.nio.ByteBuffer;
|
||||
private final MediaCodec codec;
|
||||
private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback;
|
||||
private final MediaCodecBufferEnqueuer bufferEnqueuer;
|
||||
@Nullable private final LoudnessCodecController loudnessCodecController;
|
||||
|
||||
private boolean codecReleased;
|
||||
private @State int state;
|
||||
|
||||
private AsynchronousMediaCodecAdapter(
|
||||
MediaCodec codec, HandlerThread callbackThread, MediaCodecBufferEnqueuer bufferEnqueuer) {
|
||||
MediaCodec codec,
|
||||
HandlerThread callbackThread,
|
||||
MediaCodecBufferEnqueuer bufferEnqueuer,
|
||||
@Nullable LoudnessCodecController loudnessCodecController) {
|
||||
this.codec = codec;
|
||||
this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread);
|
||||
this.bufferEnqueuer = bufferEnqueuer;
|
||||
this.loudnessCodecController = loudnessCodecController;
|
||||
this.state = STATE_CREATED;
|
||||
}
|
||||
|
||||
@ -181,6 +191,9 @@ import java.nio.ByteBuffer;
|
||||
TraceUtil.beginSection("startCodec");
|
||||
codec.start();
|
||||
TraceUtil.endSection();
|
||||
if (Util.SDK_INT >= 35 && loudnessCodecController != null) {
|
||||
loudnessCodecController.addMediaCodec(codec);
|
||||
}
|
||||
state = STATE_INITIALIZED;
|
||||
}
|
||||
|
||||
@ -273,6 +286,9 @@ import java.nio.ByteBuffer;
|
||||
codec.stop();
|
||||
}
|
||||
} finally {
|
||||
if (Util.SDK_INT >= 35 && loudnessCodecController != null) {
|
||||
loudnessCodecController.removeMediaCodec(codec);
|
||||
}
|
||||
codec.release();
|
||||
codecReleased = true;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -50,14 +50,17 @@ public interface MediaCodecAdapter {
|
||||
* @param mediaFormat See {@link #mediaFormat}.
|
||||
* @param format See {@link #format}.
|
||||
* @param crypto See {@link #crypto}.
|
||||
* @param loudnessCodecController See {@link #loudnessCodecController}.
|
||||
* @return The created instance.
|
||||
*/
|
||||
public static Configuration createForAudioDecoding(
|
||||
MediaCodecInfo codecInfo,
|
||||
MediaFormat mediaFormat,
|
||||
Format format,
|
||||
@Nullable MediaCrypto crypto) {
|
||||
return new Configuration(codecInfo, mediaFormat, format, /* surface= */ null, crypto);
|
||||
@Nullable MediaCrypto crypto,
|
||||
@Nullable LoudnessCodecController loudnessCodecController) {
|
||||
return new Configuration(
|
||||
codecInfo, mediaFormat, format, /* surface= */ null, crypto, loudnessCodecController);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,7 +79,8 @@ public interface MediaCodecAdapter {
|
||||
Format format,
|
||||
@Nullable Surface surface,
|
||||
@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. */
|
||||
@ -98,17 +102,22 @@ public interface MediaCodecAdapter {
|
||||
/** For DRM protected playbacks, a {@link MediaCrypto} to use for decryption. */
|
||||
@Nullable public final MediaCrypto crypto;
|
||||
|
||||
/** The {@link LoudnessCodecController} for audio codecs. */
|
||||
@Nullable public final LoudnessCodecController loudnessCodecController;
|
||||
|
||||
private Configuration(
|
||||
MediaCodecInfo codecInfo,
|
||||
MediaFormat mediaFormat,
|
||||
Format format,
|
||||
@Nullable Surface surface,
|
||||
@Nullable MediaCrypto crypto) {
|
||||
@Nullable MediaCrypto crypto,
|
||||
@Nullable LoudnessCodecController loudnessCodecController) {
|
||||
this.codecInfo = codecInfo;
|
||||
this.mediaFormat = mediaFormat;
|
||||
this.format = format;
|
||||
this.surface = surface;
|
||||
this.crypto = crypto;
|
||||
this.loudnessCodecController = loudnessCodecController;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
|
||||
TraceUtil.beginSection("startCodec");
|
||||
codec.start();
|
||||
TraceUtil.endSection();
|
||||
return new SynchronousMediaCodecAdapter(codec);
|
||||
return new SynchronousMediaCodecAdapter(codec, configuration.loudnessCodecController);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
if (codec != null) {
|
||||
codec.release();
|
||||
@ -84,9 +84,15 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
|
||||
}
|
||||
|
||||
private final MediaCodec codec;
|
||||
@Nullable private final LoudnessCodecController loudnessCodecController;
|
||||
|
||||
private SynchronousMediaCodecAdapter(MediaCodec mediaCodec) {
|
||||
private SynchronousMediaCodecAdapter(
|
||||
MediaCodec mediaCodec, @Nullable LoudnessCodecController loudnessCodecController) {
|
||||
this.codec = mediaCodec;
|
||||
this.loudnessCodecController = loudnessCodecController;
|
||||
if (Util.SDK_INT >= 35 && loudnessCodecController != null) {
|
||||
loudnessCodecController.addMediaCodec(codec);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -165,6 +171,9 @@ public final class SynchronousMediaCodecAdapter implements MediaCodecAdapter {
|
||||
codec.stop();
|
||||
}
|
||||
} finally {
|
||||
if (Util.SDK_INT >= 35 && loudnessCodecController != null) {
|
||||
loudnessCodecController.removeMediaCodec(codec);
|
||||
}
|
||||
codec.release();
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,8 @@ public class AsynchronousMediaCodecAdapterTest {
|
||||
codecInfo,
|
||||
createMediaFormat("format"),
|
||||
new Format.Builder().build(),
|
||||
/* crypto= */ null);
|
||||
/* crypto= */ null,
|
||||
/* loudnessCodecController= */ null);
|
||||
callbackThread = new HandlerThread("TestCallbackThread");
|
||||
queueingThread = new HandlerThread("TestQueueingThread");
|
||||
adapter =
|
||||
|
@ -599,7 +599,7 @@ public class MediaCodecRendererTest {
|
||||
@Nullable MediaCrypto crypto,
|
||||
float codecOperatingRate) {
|
||||
return MediaCodecAdapter.Configuration.createForAudioDecoding(
|
||||
codecInfo, new MediaFormat(), format, crypto);
|
||||
codecInfo, new MediaFormat(), format, crypto, /* loudnessCodecController= */ null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
Loading…
x
Reference in New Issue
Block a user