diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java index 092e8d9110..49fb1beadf 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java @@ -163,6 +163,20 @@ public class DefaultRenderersFactory implements RenderersFactory { 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. + * + *

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. * This may result in using a decoder that is less efficient or slower than the primary decoder. 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 745a5b0d5a..cdd391657b 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 @@ -26,12 +26,16 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.PersistableBundle; import android.view.Surface; +import androidx.annotation.ChecksSdkIntAtLeast; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; 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.Util; import androidx.media3.decoder.CryptoInfo; import com.google.common.base.Supplier; import java.io.IOException; @@ -54,6 +58,8 @@ import java.nio.ByteBuffer; private final Supplier callbackThreadSupplier; private final Supplier queueingThreadSupplier; + private boolean enableSynchronousBufferQueueingWithAsyncCryptoFlag; + /** * Creates an factory for {@link AsynchronousMediaCodecAdapter} instances. * @@ -74,6 +80,18 @@ import java.nio.ByteBuffer; Supplier queueingThreadSupplier) { this.callbackThreadSupplier = callbackThreadSupplier; this.queueingThreadSupplier = queueingThreadSupplier; + enableSynchronousBufferQueueingWithAsyncCryptoFlag = true; + } + + /** + * Sets whether to enable {@link MediaCodec#CONFIGURE_FLAG_USE_CRYPTO_ASYNC} on API 34 and + * above. + * + *

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 @@ -85,15 +103,21 @@ import java.nio.ByteBuffer; try { TraceUtil.beginSection("createCodec:" + 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 = - new AsynchronousMediaCodecAdapter( - codec, callbackThreadSupplier.get(), queueingThreadSupplier.get()); + new AsynchronousMediaCodecAdapter(codec, callbackThreadSupplier.get(), bufferEnqueuer); TraceUtil.endSection(); codecAdapter.initialize( - configuration.mediaFormat, - configuration.surface, - configuration.crypto, - configuration.flags); + configuration.mediaFormat, configuration.surface, configuration.crypto, flags); return codecAdapter; } catch (Exception e) { if (codecAdapter != null) { @@ -104,6 +128,16 @@ import java.nio.ByteBuffer; 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 @@ -118,15 +152,15 @@ import java.nio.ByteBuffer; private final MediaCodec codec; private final AsynchronousMediaCodecCallback asynchronousMediaCodecCallback; - private final AsynchronousMediaCodecBufferEnqueuer bufferEnqueuer; + private final MediaCodecBufferEnqueuer bufferEnqueuer; private boolean codecReleased; private @State int state; private AsynchronousMediaCodecAdapter( - MediaCodec codec, HandlerThread callbackThread, HandlerThread enqueueingThread) { + MediaCodec codec, HandlerThread callbackThread, MediaCodecBufferEnqueuer bufferEnqueuer) { this.codec = codec; this.asynchronousMediaCodecCallback = new AsynchronousMediaCodecCallback(callbackThread); - this.bufferEnqueuer = new AsynchronousMediaCodecBufferEnqueuer(codec, enqueueingThread); + this.bufferEnqueuer = bufferEnqueuer; this.state = STATE_CREATED; } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java index f6d33f78bb..2ae384501f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecBufferEnqueuer.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package androidx.media3.exoplayer.mediacodec; import static androidx.annotation.VisibleForTesting.NONE; @@ -39,13 +38,11 @@ import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * Performs {@link MediaCodec} input buffer queueing on a background thread. - * - *

The implementation of this class assumes that its public methods will be called from the same - * 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. */ @RequiresApi(23) -class AsynchronousMediaCodecBufferEnqueuer { +/* package */ class AsynchronousMediaCodecBufferEnqueuer implements MediaCodecBufferEnqueuer { private static final int MSG_QUEUE_INPUT_BUFFER = 0; private static final int MSG_QUEUE_SECURE_INPUT_BUFFER = 1; @@ -83,11 +80,7 @@ class AsynchronousMediaCodecBufferEnqueuer { pendingRuntimeException = new AtomicReference<>(); } - /** - * Starts this instance. - * - *

Call this method after creating an instance and before queueing input buffers. - */ + @Override public void start() { if (!started) { handlerThread.start(); @@ -102,11 +95,7 @@ class AsynchronousMediaCodecBufferEnqueuer { } } - /** - * Submits an input buffer for decoding. - * - * @see android.media.MediaCodec#queueInputBuffer - */ + @Override public void queueInputBuffer( int index, int offset, int size, long presentationTimeUs, int flags) { maybeThrowException(); @@ -116,15 +105,7 @@ class AsynchronousMediaCodecBufferEnqueuer { message.sendToTarget(); } - /** - * Submits an input buffer that potentially contains encrypted data for decoding. - * - *

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 - */ + @Override public void queueSecureInputBuffer( int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { maybeThrowException(); @@ -136,12 +117,13 @@ class AsynchronousMediaCodecBufferEnqueuer { message.sendToTarget(); } + @Override public void setParameters(Bundle params) { maybeThrowException(); castNonNull(handler).obtainMessage(MSG_SET_PARAMETERS, params).sendToTarget(); } - /** Flushes the instance. */ + @Override public void flush() { if (started) { 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() { if (started) { flush(); @@ -164,12 +146,12 @@ class AsynchronousMediaCodecBufferEnqueuer { started = false; } - /** Blocks the current thread until all input buffers pending queueing are submitted. */ + @Override public void waitUntilQueueingComplete() throws InterruptedException { blockUntilHandlerThreadIsIdle(); } - /** Throw any exception that occurred on the enqueuer's background queueing thread. */ + @Override public void maybeThrowException() { @Nullable RuntimeException exception = pendingRuntimeException.getAndSet(null); if (exception != null) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallback.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallback.java index 5b595ffa89..dd40266f4f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallback.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/AsynchronousMediaCodecCallback.java @@ -63,6 +63,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Nullable private MediaCodec.CodecException mediaCodecException; + @GuardedBy("lock") + @Nullable + private MediaCodec.CryptoException mediaCodecCryptoException; + @GuardedBy("lock") 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 public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { synchronized (lock) { @@ -287,6 +298,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private void maybeThrowException() { maybeThrowInternalException(); maybeThrowMediaCodecException(); + maybeThrowMediaCodecCryptoException(); } @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) { synchronized (lock) { internalException = e; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/DefaultMediaCodecAdapterFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/DefaultMediaCodecAdapterFactory.java index ec46fadd8e..48ffcc87a0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/DefaultMediaCodecAdapterFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/DefaultMediaCodecAdapterFactory.java @@ -17,6 +17,7 @@ package androidx.media3.exoplayer.mediacodec; import static java.lang.annotation.ElementType.TYPE_USE; +import android.media.MediaCodec; import androidx.annotation.IntDef; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Log; @@ -54,9 +55,11 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. private static final String TAG = "DMCodecAdapterFactory"; private @Mode int asynchronousMode; + private boolean asyncCryptoFlagEnabled; public DefaultMediaCodecAdapterFactory() { asynchronousMode = MODE_DEFAULT; + asyncCryptoFlagEnabled = true; } /** @@ -83,6 +86,20 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. return this; } + /** + * Sets whether to enable {@link MediaCodec#CONFIGURE_FLAG_USE_CRYPTO_ASYNC} on API 34 and above + * for {@link AsynchronousMediaCodecAdapter} instances. + * + *

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 public MediaCodecAdapter createAdapter(MediaCodecAdapter.Configuration configuration) throws IOException { @@ -96,6 +113,7 @@ public final class DefaultMediaCodecAdapterFactory implements MediaCodecAdapter. + Util.getTrackTypeString(trackType)); AsynchronousMediaCodecAdapter.Factory factory = new AsynchronousMediaCodecAdapter.Factory(trackType); + factory.experimentalSetAsyncCryptoFlagEnabled(asyncCryptoFlagEnabled); return factory.createAdapter(configuration); } return new SynchronousMediaCodecAdapter.Factory().createAdapter(configuration); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecBufferEnqueuer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecBufferEnqueuer.java new file mode 100644 index 0000000000..4267b741fd --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecBufferEnqueuer.java @@ -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}. + * + *

All methods must be called from the same thread. + */ +/* package */ interface MediaCodecBufferEnqueuer { + + /** + * Starts this instance. + * + *

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. + * + *

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(); +} diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/SynchronousMediaCodecBufferEnqueuer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/SynchronousMediaCodecBufferEnqueuer.java new file mode 100644 index 0000000000..7584403080 --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/SynchronousMediaCodecBufferEnqueuer.java @@ -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. + } +}