Use CRYPTO_ASYNC mode for video from API 34

This allows us to remove the additional thread we create
for asynchronous buffer queuing and use a synchronized
queueing approach again.

This API looks strictly beneficial on all tested devices,
but since this code path in MediaCodec has not been used
widely, we leave an opt-out flag for now.

PiperOrigin-RevId: 591867472
This commit is contained in:
tonihei 2023-12-18 05:30:02 -08:00 committed by Copybara-Service
parent bb7aa2fb19
commit e5aa69237e
7 changed files with 257 additions and 38 deletions

View File

@ -163,6 +163,20 @@ public class DefaultRenderersFactory implements RenderersFactory {
return this; return this;
} }
/**
* Sets whether to enable {@link MediaCodec#CONFIGURE_FLAG_USE_CRYPTO_ASYNC} on API 34 and above
* when operating the codec in asynchronous mode.
*
* <p>This method is experimental. Its default value may change, or it may be renamed or removed
* in a future release.
*/
@CanIgnoreReturnValue
public DefaultRenderersFactory experimentalSetMediaCodecAsyncCryptoFlagEnabled(
boolean enableAsyncCryptoFlag) {
codecAdapterFactory.experimentalSetAsyncCryptoFlagEnabled(enableAsyncCryptoFlag);
return this;
}
/** /**
* Sets whether to enable fallback to lower-priority decoders if decoder initialization fails. * Sets whether to enable fallback to lower-priority decoders if decoder initialization fails.
* This may result in using a decoder that is less efficient or slower than the primary decoder. * This may result in using a decoder that is less efficient or slower than the primary decoder.

View File

@ -26,12 +26,16 @@ import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.PersistableBundle; import android.os.PersistableBundle;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.TraceUtil; import androidx.media3.common.util.TraceUtil;
import androidx.media3.common.util.Util;
import androidx.media3.decoder.CryptoInfo; import androidx.media3.decoder.CryptoInfo;
import com.google.common.base.Supplier; import com.google.common.base.Supplier;
import java.io.IOException; import java.io.IOException;
@ -54,6 +58,8 @@ import java.nio.ByteBuffer;
private final Supplier<HandlerThread> callbackThreadSupplier; private final Supplier<HandlerThread> callbackThreadSupplier;
private final Supplier<HandlerThread> queueingThreadSupplier; private final Supplier<HandlerThread> queueingThreadSupplier;
private boolean enableSynchronousBufferQueueingWithAsyncCryptoFlag;
/** /**
* Creates an factory for {@link AsynchronousMediaCodecAdapter} instances. * Creates an factory for {@link AsynchronousMediaCodecAdapter} instances.
* *
@ -74,6 +80,18 @@ import java.nio.ByteBuffer;
Supplier<HandlerThread> queueingThreadSupplier) { Supplier<HandlerThread> queueingThreadSupplier) {
this.callbackThreadSupplier = callbackThreadSupplier; this.callbackThreadSupplier = callbackThreadSupplier;
this.queueingThreadSupplier = queueingThreadSupplier; this.queueingThreadSupplier = queueingThreadSupplier;
enableSynchronousBufferQueueingWithAsyncCryptoFlag = true;
}
/**
* Sets whether to enable {@link MediaCodec#CONFIGURE_FLAG_USE_CRYPTO_ASYNC} on API 34 and
* above.
*
* <p>This method is experimental. Its default value may change, or it may be renamed or removed
* in a future release.
*/
public void experimentalSetAsyncCryptoFlagEnabled(boolean enableAsyncCryptoFlag) {
enableSynchronousBufferQueueingWithAsyncCryptoFlag = enableAsyncCryptoFlag;
} }
@Override @Override
@ -85,15 +103,21 @@ import java.nio.ByteBuffer;
try { try {
TraceUtil.beginSection("createCodec:" + codecName); TraceUtil.beginSection("createCodec:" + codecName);
codec = MediaCodec.createByCodecName(codecName); codec = MediaCodec.createByCodecName(codecName);
int flags = configuration.flags;
MediaCodecBufferEnqueuer bufferEnqueuer;
if (enableSynchronousBufferQueueingWithAsyncCryptoFlag
&& useSynchronousBufferQueueingWithAsyncCryptoFlag(configuration.format)) {
bufferEnqueuer = new SynchronousMediaCodecBufferEnqueuer(codec);
flags |= MediaCodec.CONFIGURE_FLAG_USE_CRYPTO_ASYNC;
} else {
bufferEnqueuer =
new AsynchronousMediaCodecBufferEnqueuer(codec, queueingThreadSupplier.get());
}
codecAdapter = codecAdapter =
new AsynchronousMediaCodecAdapter( new AsynchronousMediaCodecAdapter(codec, callbackThreadSupplier.get(), bufferEnqueuer);
codec, callbackThreadSupplier.get(), queueingThreadSupplier.get());
TraceUtil.endSection(); TraceUtil.endSection();
codecAdapter.initialize( codecAdapter.initialize(
configuration.mediaFormat, configuration.mediaFormat, configuration.surface, configuration.crypto, flags);
configuration.surface,
configuration.crypto,
configuration.flags);
return codecAdapter; return codecAdapter;
} catch (Exception e) { } catch (Exception e) {
if (codecAdapter != null) { if (codecAdapter != null) {
@ -104,6 +128,16 @@ import java.nio.ByteBuffer;
throw e; throw e;
} }
} }
@ChecksSdkIntAtLeast(api = 34)
private static boolean useSynchronousBufferQueueingWithAsyncCryptoFlag(Format format) {
if (Util.SDK_INT < 34) {
return false;
}
// TODO: b/316565675 - Remove restriction to video once MediaCodec supports
// CONFIGURE_FLAG_USE_CRYPTO_ASYNC for audio too
return MimeTypes.isVideo(format.sampleMimeType);
}
} }
@Documented @Documented
@ -118,15 +152,15 @@ import java.nio.ByteBuffer;
private final MediaCodec codec; private final MediaCodec codec;
private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback; private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback;
private final AsynchronousMediaCodecBufferEnqueuer bufferEnqueuer; private final MediaCodecBufferEnqueuer bufferEnqueuer;
private boolean codecReleased; private boolean codecReleased;
private @State int state; private @State int state;
private AsynchronousMediaCodecAdapter( private AsynchronousMediaCodecAdapter(
MediaCodec codec, HandlerThread callbackThread, HandlerThread enqueueingThread) { MediaCodec codec, HandlerThread callbackThread, MediaCodecBufferEnqueuer bufferEnqueuer) {
this.codec = codec; this.codec = codec;
this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread); this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread);
this.bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, enqueueingThread); this.bufferEnqueuer = bufferEnqueuer;
this.state = STATE_CREATED; this.state = STATE_CREATED;
} }

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package androidx.media3.exoplayer.mediacodec; package androidx.media3.exoplayer.mediacodec;
import static androidx.annotation.VisibleForTesting.NONE; import static androidx.annotation.VisibleForTesting.NONE;
@ -39,13 +38,11 @@ import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* Performs {@link MediaCodec} input buffer queueing on a background thread. * Performs {@link MediaCodec} input buffer queueing on a background thread. This is required on API
* * 33 and below because queuing secure buffers blocks until decryption is complete.
* <p>The implementation of this class assumes that its public methods will be called from the same
* thread.
*/ */
@RequiresApi(23) @RequiresApi(23)
class AsynchronousMediaCodecBufferEnqueuer { /* package */ class AsynchronousMediaCodecBufferEnqueuer implements MediaCodecBufferEnqueuer {
private static final int MSG_QUEUE_INPUT_BUFFER = 0; private static final int MSG_QUEUE_INPUT_BUFFER = 0;
private static final int MSG_QUEUE_SECURE_INPUT_BUFFER = 1; private static final int MSG_QUEUE_SECURE_INPUT_BUFFER = 1;
@ -83,11 +80,7 @@ class AsynchronousMediaCodecBufferEnqueuer {
pendingRuntimeException = new AtomicReference<>(); pendingRuntimeException = new AtomicReference<>();
} }
/** @Override
* Starts this instance.
*
* <p>Call this method after creating an instance and before queueing input buffers.
*/
public void start() { public void start() {
if (!started) { if (!started) {
handlerThread.start(); handlerThread.start();
@ -102,11 +95,7 @@ class AsynchronousMediaCodecBufferEnqueuer {
} }
} }
/** @Override
* Submits an input buffer for decoding.
*
* @see android.media.MediaCodec#queueInputBuffer
*/
public void queueInputBuffer( public void queueInputBuffer(
int index, int offset, int size, long presentationTimeUs, int flags) { int index, int offset, int size, long presentationTimeUs, int flags) {
maybeThrowException(); maybeThrowException();
@ -116,15 +105,7 @@ class AsynchronousMediaCodecBufferEnqueuer {
message.sendToTarget(); message.sendToTarget();
} }
/** @Override
* Submits an input buffer that potentially contains encrypted data for decoding.
*
* <p>Note: This method behaves as {@link MediaCodec#queueSecureInputBuffer} with the difference
* that {@code info} is of type {@link CryptoInfo} and not {@link
* android.media.MediaCodec.CryptoInfo}.
*
* @see android.media.MediaCodec#queueSecureInputBuffer
*/
public void queueSecureInputBuffer( public void queueSecureInputBuffer(
int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) {
maybeThrowException(); maybeThrowException();
@ -136,12 +117,13 @@ class AsynchronousMediaCodecBufferEnqueuer {
message.sendToTarget(); message.sendToTarget();
} }
@Override
public void setParameters(Bundle params) { public void setParameters(Bundle params) {
maybeThrowException(); maybeThrowException();
castNonNull(handler).obtainMessage(MSG_SET_PARAMETERS, params).sendToTarget(); castNonNull(handler).obtainMessage(MSG_SET_PARAMETERS, params).sendToTarget();
} }
/** Flushes the instance. */ @Override
public void flush() { public void flush() {
if (started) { if (started) {
try { try {
@ -155,7 +137,7 @@ class AsynchronousMediaCodecBufferEnqueuer {
} }
} }
/** Shuts down the instance. Make sure to call this method to release its internal resources. */ @Override
public void shutdown() { public void shutdown() {
if (started) { if (started) {
flush(); flush();
@ -164,12 +146,12 @@ class AsynchronousMediaCodecBufferEnqueuer {
started = false; started = false;
} }
/** Blocks the current thread until all input buffers pending queueing are submitted. */ @Override
public void waitUntilQueueingComplete() throws InterruptedException { public void waitUntilQueueingComplete() throws InterruptedException {
blockUntilHandlerThreadIsIdle(); blockUntilHandlerThreadIsIdle();
} }
/** Throw any exception that occurred on the enqueuer's background queueing thread. */ @Override
public void maybeThrowException() { public void maybeThrowException() {
@Nullable RuntimeException exception = pendingRuntimeException.getAndSet(null); @Nullable RuntimeException exception = pendingRuntimeException.getAndSet(null);
if (exception != null) { if (exception != null) {

View File

@ -63,6 +63,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable @Nullable
private MediaCodec.CodecException mediaCodecException; private MediaCodec.CodecException mediaCodecException;
@GuardedBy("lock")
@Nullable
private MediaCodec.CryptoException mediaCodecCryptoException;
@GuardedBy("lock") @GuardedBy("lock")
private long pendingFlushCount; private long pendingFlushCount;
@ -228,6 +232,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
@Override
public void onCryptoError(MediaCodec codec, MediaCodec.CryptoException e) {
synchronized (lock) {
mediaCodecCryptoException = e;
}
}
@Override @Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
synchronized (lock) { synchronized (lock) {
@ -287,6 +298,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private void maybeThrowException() { private void maybeThrowException() {
maybeThrowInternalException(); maybeThrowInternalException();
maybeThrowMediaCodecException(); maybeThrowMediaCodecException();
maybeThrowMediaCodecCryptoException();
} }
@GuardedBy("lock") @GuardedBy("lock")
@ -307,6 +319,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
@GuardedBy("lock")
private void maybeThrowMediaCodecCryptoException() {
if (mediaCodecCryptoException != null) {
MediaCodec.CryptoException cryptoException = mediaCodecCryptoException;
mediaCodecCryptoException = null;
throw cryptoException;
}
}
private void setInternalException(IllegalStateException e) { private void setInternalException(IllegalStateException e) {
synchronized (lock) { synchronized (lock) {
internalException = e; internalException = e;

View File

@ -17,6 +17,7 @@ package androidx.media3.exoplayer.mediacodec;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
import android.media.MediaCodec;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
@ -54,9 +55,11 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter.
private static final String TAG = "DMCodecAdapterFactory"; private static final String TAG = "DMCodecAdapterFactory";
private @Mode int asynchronousMode; private @Mode int asynchronousMode;
private boolean asyncCryptoFlagEnabled;
public DefaultMediaCodecAdapterFactory() { public DefaultMediaCodecAdapterFactory() {
asynchronousMode = MODE_DEFAULT; asynchronousMode = MODE_DEFAULT;
asyncCryptoFlagEnabled = true;
} }
/** /**
@ -83,6 +86,20 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter.
return this; return this;
} }
/**
* Sets whether to enable {@link MediaCodec#CONFIGURE_FLAG_USE_CRYPTO_ASYNC} on API 34 and above
* for {@link AsynchronousMediaCodecAdapter} instances.
*
* <p>This method is experimental. Its default value may change, or it may be renamed or removed
* in a future release.
*/
@CanIgnoreReturnValue
public DefaultMediaCodecAdapterFactory experimentalSetAsyncCryptoFlagEnabled(
boolean enableAsyncCryptoFlag) {
asyncCryptoFlagEnabled = enableAsyncCryptoFlag;
return this;
}
@Override @Override
public MediaCodecAdapter createAdapter(MediaCodecAdapter.Configuration configuration) public MediaCodecAdapter createAdapter(MediaCodecAdapter.Configuration configuration)
throws IOException { throws IOException {
@ -96,6 +113,7 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter.
+ Util.getTrackTypeString(trackType)); + Util.getTrackTypeString(trackType));
AsynchronousMediaCodecAdapter.Factory factory = AsynchronousMediaCodecAdapter.Factory factory =
new AsynchronousMediaCodecAdapter.Factory(trackType); new AsynchronousMediaCodecAdapter.Factory(trackType);
factory.experimentalSetAsyncCryptoFlagEnabled(asyncCryptoFlagEnabled);
return factory.createAdapter(configuration); return factory.createAdapter(configuration);
} }
return new SynchronousMediaCodecAdapter.Factory().createAdapter(configuration); return new SynchronousMediaCodecAdapter.Factory().createAdapter(configuration);

View File

@ -0,0 +1,74 @@
/*
* Copyright 2023 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 android.media.MediaCodec;
import android.os.Bundle;
import androidx.annotation.RequiresApi;
import androidx.media3.decoder.CryptoInfo;
/**
* Interface to queue buffers to a {@link MediaCodec}.
*
* <p>All methods must be called from the same thread.
*/
/* package */ interface MediaCodecBufferEnqueuer {
/**
* Starts this instance.
*
* <p>Call this method after creating an instance and before queueing input buffers.
*/
void start();
/**
* Submits an input buffer for decoding.
*
* @see android.media.MediaCodec#queueInputBuffer
*/
void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags);
/**
* Submits an input buffer that potentially contains encrypted data for decoding.
*
* <p>Note: This method behaves as {@link MediaCodec#queueSecureInputBuffer} with the difference
* that {@code info} is of type {@link CryptoInfo} and not {@link MediaCodec.CryptoInfo}.
*
* @see MediaCodec#queueSecureInputBuffer
*/
void queueSecureInputBuffer(
int index, int offset, CryptoInfo info, long presentationTimeUs, int flags);
/**
* Submits new codec parameters that should be applied from the next queued input buffer.
*
* @see MediaCodec#setParameters(Bundle)
*/
@RequiresApi(19)
void setParameters(Bundle parameters);
/** Flushes the instance. */
void flush();
/** Shuts down the instance. Make sure to call this method to release its internal resources. */
void shutdown();
/** Blocks the current thread until all input buffers pending queueing are submitted. */
void waitUntilQueueingComplete() throws InterruptedException;
/** Throw any exception that occurred during the enqueueing process. */
void maybeThrowException();
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2023 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 android.media.MediaCodec;
import android.os.Bundle;
import androidx.annotation.RequiresApi;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.decoder.CryptoInfo;
@RequiresApi(23)
@UnstableApi
/* package */ class SynchronousMediaCodecBufferEnqueuer implements MediaCodecBufferEnqueuer {
private final MediaCodec codec;
public SynchronousMediaCodecBufferEnqueuer(MediaCodec codec) {
this.codec = codec;
}
@Override
public void start() {
// Do nothing.
}
@Override
public void queueInputBuffer(
int index, int offset, int size, long presentationTimeUs, int flags) {
codec.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
}
@Override
public void queueSecureInputBuffer(
int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) {
codec.queueSecureInputBuffer(
index, offset, info.getFrameworkCryptoInfo(), presentationTimeUs, flags);
}
@Override
public void setParameters(Bundle parameters) {
codec.setParameters(parameters);
}
@Override
public void flush() {
// Do nothing.
}
@Override
public void shutdown() {
// Do nothing.
}
@Override
public void waitUntilQueueingComplete() {
// Do nothing.
}
@Override
public void maybeThrowException() {
// Do nothing.
}
}