allowedMimeTypes)
throws TransformationException;
}
- // MediaCodec decoders always output 16 bit PCM, unless configured to output PCM float.
- // https://developer.android.com/reference/android/media/MediaCodec#raw-audio-buffers.
- private static final int MEDIA_CODEC_PCM_ENCODING = C.ENCODING_PCM_16BIT;
-
- private final BufferInfo outputBufferInfo;
- private final MediaCodec mediaCodec;
- private final Format configurationFormat;
- @Nullable private final Surface inputSurface;
-
- private @MonotonicNonNull Format outputFormat;
- @Nullable private ByteBuffer outputBuffer;
-
- private int inputBufferIndex;
- private int outputBufferIndex;
- private boolean inputStreamEnded;
- private boolean outputStreamEnded;
-
/**
- * Creates a {@code Codec} from a configured and started {@link MediaCodec}.
- *
- * @param mediaCodec The configured and started {@link MediaCodec}.
- * @param configurationFormat See {@link #getConfigurationFormat()}.
- * @param inputSurface The input {@link Surface} if the {@link MediaCodec} receives input from a
- * surface.
- */
- public Codec(MediaCodec mediaCodec, Format configurationFormat, @Nullable Surface inputSurface) {
- this.mediaCodec = mediaCodec;
- this.configurationFormat = configurationFormat;
- this.inputSurface = inputSurface;
- outputBufferInfo = new BufferInfo();
- inputBufferIndex = C.INDEX_UNSET;
- outputBufferIndex = C.INDEX_UNSET;
- }
-
- /**
- * Returns the {@link Format} used for configuring the codec.
+ * Returns the {@link Format} used for configuring the {@code Codec}.
*
* The configuration {@link Format} is the input {@link Format} used by the {@link
* DecoderFactory} or output {@link Format} used by the {@link EncoderFactory} for selecting and
- * configuring the underlying {@link MediaCodec}.
+ * configuring the underlying decoder or encoder.
*/
- public Format getConfigurationFormat() {
- return configurationFormat;
- }
+ Format getConfigurationFormat();
- /** Returns the input {@link Surface}, or null if the input is not a surface. */
- @Nullable
- public Surface getInputSurface() {
- return inputSurface;
- }
+ /**
+ * Returns the input {@link Surface} of an underlying video encoder.
+ *
+ *
This method must only be called on video encoders because audio/video decoders and audio
+ * encoders don't use a {@link Surface} as input.
+ */
+ Surface getInputSurface();
/**
* Dequeues a writable input buffer, if available.
*
- * @param inputBuffer The buffer where the dequeued buffer data is stored.
+ *
This method must not be called from video encoders because they must use {@link Surface
+ * surfaces} as inputs.
+ *
+ * @param inputBuffer The buffer where the dequeued buffer data is stored, at {@link
+ * DecoderInputBuffer#data inputBuffer.data}.
* @return Whether an input buffer is ready to be used.
- * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem.
+ * @throws TransformationException If the underlying decoder or encoder encounters a problem.
*/
- @EnsuresNonNullIf(expression = "#1.data", result = true)
- public boolean maybeDequeueInputBuffer(DecoderInputBuffer inputBuffer)
- throws TransformationException {
- if (inputStreamEnded) {
- return false;
- }
- if (inputBufferIndex < 0) {
- try {
- inputBufferIndex = mediaCodec.dequeueInputBuffer(/* timeoutUs= */ 0);
- } catch (RuntimeException e) {
- throw createTransformationException(e);
- }
- if (inputBufferIndex < 0) {
- return false;
- }
- try {
- inputBuffer.data = mediaCodec.getInputBuffer(inputBufferIndex);
- } catch (RuntimeException e) {
- throw createTransformationException(e);
- }
- inputBuffer.clear();
- }
- checkNotNull(inputBuffer.data);
- return true;
- }
+ boolean maybeDequeueInputBuffer(DecoderInputBuffer inputBuffer) throws TransformationException;
/**
- * Queues an input buffer to the decoder. No buffers may be queued after an {@link
+ * Queues an input buffer to the {@code Codec}. No buffers may be queued after {@link
* DecoderInputBuffer#isEndOfStream() end of stream} buffer has been queued.
*
+ *
This method must not be called from video encoders because they must use {@link Surface
+ * surfaces} as inputs.
+ *
* @param inputBuffer The {@link DecoderInputBuffer input buffer}.
- * @throws IllegalStateException If called again after an {@link
- * DecoderInputBuffer#isEndOfStream() end of stream} buffer has been queued.
- * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem.
+ * @throws TransformationException If the underlying decoder or encoder encounters a problem.
*/
- public void queueInputBuffer(DecoderInputBuffer inputBuffer) throws TransformationException {
- checkState(
- !inputStreamEnded, "Input buffer can not be queued after the input stream has ended.");
-
- int offset = 0;
- int size = 0;
- if (inputBuffer.data != null && inputBuffer.data.hasRemaining()) {
- offset = inputBuffer.data.position();
- size = inputBuffer.data.remaining();
- }
- int flags = 0;
- if (inputBuffer.isEndOfStream()) {
- inputStreamEnded = true;
- flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
- }
- try {
- mediaCodec.queueInputBuffer(inputBufferIndex, offset, size, inputBuffer.timeUs, flags);
- } catch (RuntimeException e) {
- throw createTransformationException(e);
- }
- inputBufferIndex = C.INDEX_UNSET;
- inputBuffer.data = null;
- }
+ void queueInputBuffer(DecoderInputBuffer inputBuffer) throws TransformationException;
/**
* Signals end-of-stream on input to a video encoder.
*
- *
This method does not need to be called for audio/video decoders or audio encoders. For these
- * the {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag should be set on the last input buffer
- * {@link #queueInputBuffer(DecoderInputBuffer) queued}.
+ *
This method must only be called on video encoders because they must use a {@link Surface} as
+ * input. For audio/video decoders or audio encoders, the {@link C#BUFFER_FLAG_END_OF_STREAM} flag
+ * should be set on the last input buffer {@link #queueInputBuffer(DecoderInputBuffer) queued}.
*
- * @throws IllegalStateException If the codec is not an encoder receiving input from a {@link
- * Surface}.
- * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem.
+ * @throws TransformationException If the underlying video encoder encounters a problem.
*/
- public void signalEndOfInputStream() throws TransformationException {
- checkState(mediaCodec.getCodecInfo().isEncoder() && inputSurface != null);
- try {
- mediaCodec.signalEndOfInputStream();
- } catch (RuntimeException e) {
- throw createTransformationException(e);
- }
- }
+ void signalEndOfInputStream() throws TransformationException;
/**
- * Returns the current output format, if available.
+ * Returns the current output format, or {@code null} if unavailable.
*
- * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem.
+ * @throws TransformationException If the underlying decoder or encoder encounters a problem.
*/
@Nullable
- public Format getOutputFormat() throws TransformationException {
- // The format is updated when dequeueing a 'special' buffer index, so attempt to dequeue now.
- maybeDequeueOutputBuffer(/* setOutputBuffer= */ false);
- return outputFormat;
- }
+ Format getOutputFormat() throws TransformationException;
/**
- * Returns the current output {@link ByteBuffer}, if available.
+ * Returns the current output {@link ByteBuffer}, or {@code null} if unavailable.
*
- * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem.
+ *
This method must not be called on video decoders because they must output to a {@link
+ * Surface}.
+ *
+ * @throws TransformationException If the underlying decoder or encoder encounters a problem.
*/
@Nullable
- public ByteBuffer getOutputBuffer() throws TransformationException {
- return maybeDequeueOutputBuffer(/* setOutputBuffer= */ true) ? outputBuffer : null;
- }
+ ByteBuffer getOutputBuffer() throws TransformationException;
/**
- * Returns the {@link BufferInfo} associated with the current output buffer, if available.
+ * Returns the {@link BufferInfo} associated with the current output buffer, or {@code null} if
+ * there is no output buffer available.
*
- * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem.
+ *
This method returns {@code null} if and only if {@link #getOutputBuffer()} returns null.
+ *
+ * @throws TransformationException If the underlying decoder or encoder encounters a problem.
*/
@Nullable
- public BufferInfo getOutputBufferInfo() throws TransformationException {
- return maybeDequeueOutputBuffer(/* setOutputBuffer= */ false) ? outputBufferInfo : null;
- }
+ BufferInfo getOutputBufferInfo() throws TransformationException;
/**
* Releases the current output buffer.
*
- *
This should be called after the buffer has been processed. The next output buffer will not
- * be available until the previous has been released.
- *
- * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem.
- */
- public void releaseOutputBuffer() throws TransformationException {
- releaseOutputBuffer(/* render= */ false);
- }
-
- /**
- * Releases the current output buffer. If the {@link MediaCodec} was configured with an output
- * surface, setting {@code render} to {@code true} will first send the buffer to the output
- * surface. The surface will release the buffer back to the codec once it is no longer
+ *
Only set {@code render} to {@code true} when the {@code Codec} is a video decoder. Setting
+ * {@code render} to {@code true} will first render the buffer to the output surface. In this
+ * case, the surface will release the buffer back to the {@code Codec} once it is no longer
* used/displayed.
*
*
This should be called after the buffer has been processed. The next output buffer will not
- * be available until the previous has been released.
+ * be available until the current output buffer has been released.
*
- * @param render Whether the buffer needs to be sent to the output {@link Surface}.
- * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem.
+ * @param render Whether the buffer needs to be rendered to the output {@link Surface}.
+ * @throws TransformationException If the underlying decoder or encoder encounters a problem.
*/
- public void releaseOutputBuffer(boolean render) throws TransformationException {
- outputBuffer = null;
- try {
- mediaCodec.releaseOutputBuffer(outputBufferIndex, render);
- } catch (RuntimeException e) {
- throw createTransformationException(e);
- }
- outputBufferIndex = C.INDEX_UNSET;
- }
-
- /** Returns whether the codec output stream has ended, and no more data can be dequeued. */
- public boolean isEnded() {
- return outputStreamEnded && outputBufferIndex == C.INDEX_UNSET;
- }
-
- /** Releases the underlying codec. */
- public void release() {
- outputBuffer = null;
- if (inputSurface != null) {
- inputSurface.release();
- }
- mediaCodec.release();
- }
+ void releaseOutputBuffer(boolean render) throws TransformationException;
/**
- * Attempts to dequeue an output buffer if there is no output buffer pending. Does nothing
- * otherwise.
- *
- * @param setOutputBuffer Whether to read the bytes of the dequeued output buffer and copy them
- * into {@link #outputBuffer}.
- * @return Whether there is an output buffer available.
- * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem.
+ * Returns whether the {@code Codec}'s output stream has ended, and no more data can be dequeued.
*/
- private boolean maybeDequeueOutputBuffer(boolean setOutputBuffer) throws TransformationException {
- if (outputBufferIndex >= 0) {
- return true;
- }
- if (outputStreamEnded) {
- return false;
- }
+ boolean isEnded();
- try {
- outputBufferIndex = mediaCodec.dequeueOutputBuffer(outputBufferInfo, /* timeoutUs= */ 0);
- } catch (RuntimeException e) {
- throw createTransformationException(e);
- }
- if (outputBufferIndex < 0) {
- if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
- outputFormat = getFormat(mediaCodec.getOutputFormat());
- }
- return false;
- }
- if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
- outputStreamEnded = true;
- if (outputBufferInfo.size == 0) {
- releaseOutputBuffer();
- return false;
- }
- }
- if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
- // Encountered a CSD buffer, skip it.
- releaseOutputBuffer();
- return false;
- }
-
- if (setOutputBuffer) {
- try {
- outputBuffer = checkNotNull(mediaCodec.getOutputBuffer(outputBufferIndex));
- } catch (RuntimeException e) {
- throw createTransformationException(e);
- }
- outputBuffer.position(outputBufferInfo.offset);
- outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size);
- }
- return true;
- }
-
- private TransformationException createTransformationException(Exception cause) {
- boolean isEncoder = mediaCodec.getCodecInfo().isEncoder();
- boolean isVideo = MimeTypes.isVideo(configurationFormat.sampleMimeType);
- String componentName = (isVideo ? "Video" : "Audio") + (isEncoder ? "Encoder" : "Decoder");
- return TransformationException.createForCodec(
- cause,
- componentName,
- configurationFormat,
- mediaCodec.getName(),
- isEncoder
- ? TransformationException.ERROR_CODE_ENCODING_FAILED
- : TransformationException.ERROR_CODE_DECODING_FAILED);
- }
-
- private static Format getFormat(MediaFormat mediaFormat) {
- ImmutableList.Builder csdBuffers = new ImmutableList.Builder<>();
- int csdIndex = 0;
- while (true) {
- @Nullable ByteBuffer csdByteBuffer = mediaFormat.getByteBuffer("csd-" + csdIndex);
- if (csdByteBuffer == null) {
- break;
- }
- byte[] csdBufferData = new byte[csdByteBuffer.remaining()];
- csdByteBuffer.get(csdBufferData);
- csdBuffers.add(csdBufferData);
- csdIndex++;
- }
- String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
- Format.Builder formatBuilder =
- new Format.Builder()
- .setSampleMimeType(mediaFormat.getString(MediaFormat.KEY_MIME))
- .setInitializationData(csdBuffers.build());
- if (MimeTypes.isVideo(mimeType)) {
- formatBuilder
- .setWidth(mediaFormat.getInteger(MediaFormat.KEY_WIDTH))
- .setHeight(mediaFormat.getInteger(MediaFormat.KEY_HEIGHT));
- } else if (MimeTypes.isAudio(mimeType)) {
- // TODO(internal b/178685617): Only set the PCM encoding for audio/raw, once we have a way to
- // simulate more realistic codec input/output formats in tests.
- formatBuilder
- .setChannelCount(mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT))
- .setSampleRate(mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE))
- .setPcmEncoding(MEDIA_CODEC_PCM_ENCODING);
- }
- return formatBuilder.build();
- }
+ /** Releases the {@code Codec}. */
+ void release();
}
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/CodecFactoryUtil.java b/libraries/transformer/src/main/java/androidx/media3/transformer/CodecFactoryUtil.java
deleted file mode 100644
index 87fffb66d6..0000000000
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/CodecFactoryUtil.java
+++ /dev/null
@@ -1,119 +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.transformer;
-
-import android.media.MediaCodec;
-import android.media.MediaFormat;
-import android.view.Surface;
-import androidx.annotation.Nullable;
-import androidx.media3.common.Format;
-import androidx.media3.common.util.TraceUtil;
-import java.io.IOException;
-import org.checkerframework.checker.nullness.qual.RequiresNonNull;
-
-/** Utility methods for {@link Codec}'s factory methods. */
-/* package */ final class CodecFactoryUtil {
- /** Creates a {@link Codec}. */
- @RequiresNonNull("#1.sampleMimeType")
- public static Codec createCodec(
- Format format,
- MediaFormat mediaFormat,
- @Nullable String mediaCodecName,
- boolean isVideo,
- boolean isDecoder,
- @Nullable Surface outputSurface)
- throws TransformationException {
- @Nullable MediaCodec mediaCodec = null;
- @Nullable Surface inputSurface = null;
- try {
- mediaCodec =
- mediaCodecName != null
- ? MediaCodec.createByCodecName(mediaCodecName)
- : isDecoder
- ? MediaCodec.createDecoderByType(format.sampleMimeType)
- : MediaCodec.createEncoderByType(format.sampleMimeType);
- configureCodec(mediaCodec, mediaFormat, isDecoder, outputSurface);
- if (isVideo && !isDecoder) {
- inputSurface = mediaCodec.createInputSurface();
- }
- startCodec(mediaCodec);
- } catch (Exception e) {
- if (inputSurface != null) {
- inputSurface.release();
- }
- if (mediaCodec != null) {
- mediaCodecName = mediaCodec.getName();
- mediaCodec.release();
- }
- throw createTransformationException(e, format, isVideo, isDecoder, mediaCodecName);
- }
- return new Codec(mediaCodec, format, inputSurface);
- }
-
- /** Creates a {@link TransformationException}. */
- public static TransformationException createTransformationException(
- Exception cause,
- Format format,
- boolean isVideo,
- boolean isDecoder,
- @Nullable String mediaCodecName) {
- String componentName = (isVideo ? "Video" : "Audio") + (isDecoder ? "Decoder" : "Encoder");
- if (cause instanceof IOException || cause instanceof MediaCodec.CodecException) {
- return TransformationException.createForCodec(
- cause,
- componentName,
- format,
- mediaCodecName,
- isDecoder
- ? TransformationException.ERROR_CODE_DECODER_INIT_FAILED
- : TransformationException.ERROR_CODE_ENCODER_INIT_FAILED);
- }
- if (cause instanceof IllegalArgumentException) {
- return TransformationException.createForCodec(
- cause,
- componentName,
- format,
- mediaCodecName,
- isDecoder
- ? TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED
- : TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED);
- }
- return TransformationException.createForUnexpected(cause);
- }
-
- private static void configureCodec(
- MediaCodec codec,
- MediaFormat mediaFormat,
- boolean isDecoder,
- @Nullable Surface outputSurface) {
- TraceUtil.beginSection("configureCodec");
- codec.configure(
- mediaFormat,
- outputSurface,
- /* crypto= */ null,
- isDecoder ? 0 : MediaCodec.CONFIGURE_FLAG_ENCODE);
- TraceUtil.endSection();
- }
-
- private static void startCodec(MediaCodec codec) {
- TraceUtil.beginSection("startCodec");
- codec.start();
- TraceUtil.endSection();
- }
-
- private CodecFactoryUtil() {}
-}
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java
new file mode 100644
index 0000000000..3c1c8aea61
--- /dev/null
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright 2022 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.transformer;
+
+import static androidx.media3.common.util.Assertions.checkNotNull;
+import static androidx.media3.common.util.Assertions.checkState;
+import static androidx.media3.common.util.Assertions.checkStateNotNull;
+
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaFormat;
+import android.view.Surface;
+import androidx.annotation.Nullable;
+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.UnstableApi;
+import androidx.media3.decoder.DecoderInputBuffer;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/** A default {@link Codec} implementation that uses {@link MediaCodec}. */
+@UnstableApi
+public final class DefaultCodec implements Codec {
+ // MediaCodec decoders always output 16 bit PCM, unless configured to output PCM float.
+ // https://developer.android.com/reference/android/media/MediaCodec#raw-audio-buffers.
+ private static final int MEDIA_CODEC_PCM_ENCODING = C.ENCODING_PCM_16BIT;
+
+ private final BufferInfo outputBufferInfo;
+ private final Format configurationFormat;
+ private final MediaCodec mediaCodec;
+ @Nullable private final Surface inputSurface;
+
+ private @MonotonicNonNull Format outputFormat;
+ @Nullable private ByteBuffer outputBuffer;
+
+ private int inputBufferIndex;
+ private int outputBufferIndex;
+ private boolean inputStreamEnded;
+ private boolean outputStreamEnded;
+
+ /**
+ * Creates a {@code DefaultCodec}.
+ *
+ * @param configurationFormat The {@link Format} to configure the {@code DefaultCodec}. See {@link
+ * #getConfigurationFormat()}. The {@link Format#sampleMimeType sampleMimeType} must not be
+ * {@code null}.
+ * @param mediaFormat The {@link MediaFormat} to configure the underlying {@link MediaCodec}.
+ * @param mediaCodecName The name of a specific {@link MediaCodec} to instantiate. If {@code
+ * null}, {@code DefaultCodec} uses {@link Format#sampleMimeType
+ * configurationFormat.sampleMimeType} to create the underlying {@link MediaCodec codec}.
+ * @param isDecoder Whether the {@code DefaultCodec} is intended as a decoder.
+ * @param outputSurface The output {@link Surface} if the {@link MediaCodec} outputs to a surface.
+ */
+ public DefaultCodec(
+ Format configurationFormat,
+ MediaFormat mediaFormat,
+ @Nullable String mediaCodecName,
+ boolean isDecoder,
+ @Nullable Surface outputSurface)
+ throws TransformationException {
+ this.configurationFormat = configurationFormat;
+ outputBufferInfo = new BufferInfo();
+ inputBufferIndex = C.INDEX_UNSET;
+ outputBufferIndex = C.INDEX_UNSET;
+
+ String sampleMimeType = checkNotNull(configurationFormat.sampleMimeType);
+ boolean isVideo = MimeTypes.isVideo(sampleMimeType);
+ @Nullable MediaCodec mediaCodec = null;
+ @Nullable Surface inputSurface = null;
+ try {
+ mediaCodec =
+ mediaCodecName != null
+ ? MediaCodec.createByCodecName(mediaCodecName)
+ : isDecoder
+ ? MediaCodec.createDecoderByType(sampleMimeType)
+ : MediaCodec.createEncoderByType(sampleMimeType);
+ configureCodec(mediaCodec, mediaFormat, isDecoder, outputSurface);
+ if (isVideo && !isDecoder) {
+ inputSurface = mediaCodec.createInputSurface();
+ }
+ startCodec(mediaCodec);
+ } catch (Exception e) {
+ if (inputSurface != null) {
+ inputSurface.release();
+ }
+ if (mediaCodec != null) {
+ mediaCodecName = mediaCodec.getName();
+ mediaCodec.release();
+ }
+
+ throw createInitializationTransformationException(
+ e, configurationFormat, isVideo, isDecoder, mediaCodecName);
+ }
+ this.mediaCodec = mediaCodec;
+ this.inputSurface = inputSurface;
+ }
+
+ @Override
+ public Format getConfigurationFormat() {
+ return configurationFormat;
+ }
+
+ @Override
+ public Surface getInputSurface() {
+ return checkStateNotNull(inputSurface);
+ }
+
+ @Override
+ @EnsuresNonNullIf(expression = "#1.data", result = true)
+ public boolean maybeDequeueInputBuffer(DecoderInputBuffer inputBuffer)
+ throws TransformationException {
+ if (inputStreamEnded) {
+ return false;
+ }
+ if (inputBufferIndex < 0) {
+ try {
+ inputBufferIndex = mediaCodec.dequeueInputBuffer(/* timeoutUs= */ 0);
+ } catch (RuntimeException e) {
+ throw createTransformationException(e);
+ }
+ if (inputBufferIndex < 0) {
+ return false;
+ }
+ try {
+ inputBuffer.data = mediaCodec.getInputBuffer(inputBufferIndex);
+ } catch (RuntimeException e) {
+ throw createTransformationException(e);
+ }
+ inputBuffer.clear();
+ }
+ checkNotNull(inputBuffer.data);
+ return true;
+ }
+
+ @Override
+ public void queueInputBuffer(DecoderInputBuffer inputBuffer) throws TransformationException {
+ checkState(
+ !inputStreamEnded, "Input buffer can not be queued after the input stream has ended.");
+
+ int offset = 0;
+ int size = 0;
+ if (inputBuffer.data != null && inputBuffer.data.hasRemaining()) {
+ offset = inputBuffer.data.position();
+ size = inputBuffer.data.remaining();
+ }
+ int flags = 0;
+ if (inputBuffer.isEndOfStream()) {
+ inputStreamEnded = true;
+ flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+ }
+ try {
+ mediaCodec.queueInputBuffer(inputBufferIndex, offset, size, inputBuffer.timeUs, flags);
+ } catch (RuntimeException e) {
+ throw createTransformationException(e);
+ }
+ inputBufferIndex = C.INDEX_UNSET;
+ inputBuffer.data = null;
+ }
+
+ @Override
+ public void signalEndOfInputStream() throws TransformationException {
+ try {
+ mediaCodec.signalEndOfInputStream();
+ } catch (RuntimeException e) {
+ throw createTransformationException(e);
+ }
+ }
+
+ @Override
+ @Nullable
+ public Format getOutputFormat() throws TransformationException {
+ // The format is updated when dequeueing a 'special' buffer index, so attempt to dequeue now.
+ maybeDequeueOutputBuffer(/* setOutputBuffer= */ false);
+ return outputFormat;
+ }
+
+ @Override
+ @Nullable
+ public ByteBuffer getOutputBuffer() throws TransformationException {
+ return maybeDequeueOutputBuffer(/* setOutputBuffer= */ true) ? outputBuffer : null;
+ }
+
+ @Override
+ @Nullable
+ public BufferInfo getOutputBufferInfo() throws TransformationException {
+ return maybeDequeueOutputBuffer(/* setOutputBuffer= */ false) ? outputBufferInfo : null;
+ }
+
+ @Override
+ public void releaseOutputBuffer(boolean render) throws TransformationException {
+ outputBuffer = null;
+ try {
+ mediaCodec.releaseOutputBuffer(outputBufferIndex, render);
+ } catch (RuntimeException e) {
+ throw createTransformationException(e);
+ }
+ outputBufferIndex = C.INDEX_UNSET;
+ }
+
+ @Override
+ public boolean isEnded() {
+ return outputStreamEnded && outputBufferIndex == C.INDEX_UNSET;
+ }
+
+ @Override
+ public void release() {
+ outputBuffer = null;
+ if (inputSurface != null) {
+ inputSurface.release();
+ }
+ mediaCodec.release();
+ }
+
+ /**
+ * Attempts to dequeue an output buffer if there is no output buffer pending. Does nothing
+ * otherwise.
+ *
+ * @param setOutputBuffer Whether to read the bytes of the dequeued output buffer and copy them
+ * into {@link #outputBuffer}.
+ * @return Whether there is an output buffer available.
+ * @throws TransformationException If the underlying {@link MediaCodec} encounters a problem.
+ */
+ private boolean maybeDequeueOutputBuffer(boolean setOutputBuffer) throws TransformationException {
+ if (outputBufferIndex >= 0) {
+ return true;
+ }
+ if (outputStreamEnded) {
+ return false;
+ }
+
+ try {
+ outputBufferIndex = mediaCodec.dequeueOutputBuffer(outputBufferInfo, /* timeoutUs= */ 0);
+ } catch (RuntimeException e) {
+ throw createTransformationException(e);
+ }
+ if (outputBufferIndex < 0) {
+ if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ outputFormat = getFormat(mediaCodec.getOutputFormat());
+ }
+ return false;
+ }
+ if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ outputStreamEnded = true;
+ if (outputBufferInfo.size == 0) {
+ releaseOutputBuffer(/* render= */ false);
+ return false;
+ }
+ }
+ if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
+ // Encountered a CSD buffer, skip it.
+ releaseOutputBuffer(/* render= */ false);
+ return false;
+ }
+
+ if (setOutputBuffer) {
+ try {
+ outputBuffer = checkNotNull(mediaCodec.getOutputBuffer(outputBufferIndex));
+ } catch (RuntimeException e) {
+ throw createTransformationException(e);
+ }
+ outputBuffer.position(outputBufferInfo.offset);
+ outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size);
+ }
+ return true;
+ }
+
+ private TransformationException createTransformationException(Exception cause) {
+ boolean isDecoder = !mediaCodec.getCodecInfo().isEncoder();
+ boolean isVideo = MimeTypes.isVideo(configurationFormat.sampleMimeType);
+ return TransformationException.createForCodec(
+ cause,
+ configurationFormat,
+ isVideo,
+ isDecoder,
+ mediaCodec.getName(),
+ isDecoder
+ ? TransformationException.ERROR_CODE_DECODING_FAILED
+ : TransformationException.ERROR_CODE_ENCODING_FAILED);
+ }
+
+ private static TransformationException createInitializationTransformationException(
+ Exception cause,
+ Format format,
+ boolean isVideo,
+ boolean isDecoder,
+ @Nullable String mediaCodecName) {
+ if (cause instanceof IOException || cause instanceof MediaCodec.CodecException) {
+ return TransformationException.createForCodec(
+ cause,
+ format,
+ isVideo,
+ isDecoder,
+ mediaCodecName,
+ isDecoder
+ ? TransformationException.ERROR_CODE_DECODER_INIT_FAILED
+ : TransformationException.ERROR_CODE_ENCODER_INIT_FAILED);
+ }
+ if (cause instanceof IllegalArgumentException) {
+ return TransformationException.createForCodec(
+ cause,
+ format,
+ isVideo,
+ isDecoder,
+ mediaCodecName,
+ isDecoder
+ ? TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED
+ : TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED);
+ }
+ return TransformationException.createForUnexpected(cause);
+ }
+
+ private static Format getFormat(MediaFormat mediaFormat) {
+ ImmutableList.Builder csdBuffers = new ImmutableList.Builder<>();
+ int csdIndex = 0;
+ while (true) {
+ @Nullable ByteBuffer csdByteBuffer = mediaFormat.getByteBuffer("csd-" + csdIndex);
+ if (csdByteBuffer == null) {
+ break;
+ }
+ byte[] csdBufferData = new byte[csdByteBuffer.remaining()];
+ csdByteBuffer.get(csdBufferData);
+ csdBuffers.add(csdBufferData);
+ csdIndex++;
+ }
+ String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
+ Format.Builder formatBuilder =
+ new Format.Builder()
+ .setSampleMimeType(mediaFormat.getString(MediaFormat.KEY_MIME))
+ .setInitializationData(csdBuffers.build());
+ if (MimeTypes.isVideo(mimeType)) {
+ formatBuilder
+ .setWidth(mediaFormat.getInteger(MediaFormat.KEY_WIDTH))
+ .setHeight(mediaFormat.getInteger(MediaFormat.KEY_HEIGHT));
+ } else if (MimeTypes.isAudio(mimeType)) {
+ // TODO(internal b/178685617): Only set the PCM encoding for audio/raw, once we have a way to
+ // simulate more realistic codec input/output formats in tests.
+ formatBuilder
+ .setChannelCount(mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT))
+ .setSampleRate(mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE))
+ .setPcmEncoding(MEDIA_CODEC_PCM_ENCODING);
+ }
+ return formatBuilder.build();
+ }
+
+ private static void configureCodec(
+ MediaCodec codec,
+ MediaFormat mediaFormat,
+ boolean isDecoder,
+ @Nullable Surface outputSurface) {
+ TraceUtil.beginSection("configureCodec");
+ codec.configure(
+ mediaFormat,
+ outputSurface,
+ /* crypto= */ null,
+ isDecoder ? 0 : MediaCodec.CONFIGURE_FLAG_ENCODE);
+ TraceUtil.endSection();
+ }
+
+ private static void startCodec(MediaCodec codec) {
+ TraceUtil.beginSection("startCodec");
+ codec.start();
+ TraceUtil.endSection();
+ }
+}
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java
index b47a18d97c..5e951a6847 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java
@@ -18,7 +18,6 @@ package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.SDK_INT;
-import static androidx.media3.transformer.CodecFactoryUtil.createCodec;
import android.media.MediaFormat;
import android.view.Surface;
@@ -36,11 +35,10 @@ import androidx.media3.common.util.MediaFormatUtil;
mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize);
MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
- return createCodec(
+ return new DefaultCodec(
format,
mediaFormat,
/* mediaCodecName= */ null,
- /* isVideo= */ false,
/* isDecoder= */ true,
/* outputSurface= */ null);
}
@@ -61,12 +59,7 @@ import androidx.media3.common.util.MediaFormatUtil;
mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0);
}
- return createCodec(
- format,
- mediaFormat,
- /* mediaCodecName= */ null,
- /* isVideo= */ true,
- /* isDecoder= */ true,
- outputSurface);
+ return new DefaultCodec(
+ format, mediaFormat, /* mediaCodecName= */ null, /* isDecoder= */ true, outputSurface);
}
}
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java
index 9b7d2ba4e1..639e9728bc 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java
@@ -21,8 +21,6 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.SDK_INT;
-import static androidx.media3.transformer.CodecFactoryUtil.createCodec;
-import static androidx.media3.transformer.CodecFactoryUtil.createTransformationException;
import static java.lang.Math.abs;
import android.media.MediaCodecInfo;
@@ -81,12 +79,13 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory {
// capabilities limitations.
format = format.buildUpon().setSampleMimeType(allowedMimeTypes.get(0)).build();
} else {
- throw createTransformationException(
+ throw TransformationException.createForCodec(
new IllegalArgumentException("The requested output format is not supported."),
format,
/* isVideo= */ false,
/* isDecoder= */ false,
- /* mediaCodecName= */ null);
+ /* mediaCodecName= */ null,
+ TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED);
}
}
MediaFormat mediaFormat =
@@ -94,11 +93,10 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory {
checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate);
- return createCodec(
+ return new DefaultCodec(
format,
mediaFormat,
/* mediaCodecName= */ null,
- /* isVideo= */ false,
/* isDecoder= */ false,
/* outputSurface= */ null);
}
@@ -121,12 +119,13 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory {
findEncoderWithClosestFormatSupport(
format, videoEncoderSelector, allowedMimeTypes, disableFallback);
if (encoderAndClosestFormatSupport == null) {
- throw createTransformationException(
+ throw TransformationException.createForCodec(
new IllegalArgumentException("The requested output format is not supported."),
format,
/* isVideo= */ true,
/* isDecoder= */ false,
- /* mediaCodecName= */ null);
+ /* mediaCodecName= */ null,
+ TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED);
}
MediaCodecInfo encoderInfo = encoderAndClosestFormatSupport.first;
@@ -198,11 +197,10 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory {
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, DEFAULT_COLOR_FORMAT);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL_SECS);
- return createCodec(
+ return new DefaultCodec(
format,
mediaFormat,
encoderInfo.getName(),
- /* isVideo= */ true,
/* isDecoder= */ false,
/* outputSurface= */ null);
}
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java
index 2e1721d53d..5dce35e195 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java
@@ -207,24 +207,23 @@ public final class TransformationException extends Exception {
* Creates an instance for a decoder or encoder related exception.
*
* @param cause The cause of the failure.
- * @param componentName The name of the component used, e.g. 'VideoEncoder'.
- * @param configurationFormat The {@link Format} used for configuring the decoder/encoder.
+ * @param format The {@link Format} used for configuring the decoder/encoder.
+ * @param isVideo Whether the decoder or encoder is configured for video.
+ * @param isDecoder Whether the exception is created for a decoder.
* @param mediaCodecName The name of the {@link MediaCodec} used, if known.
* @param errorCode See {@link #errorCode}.
* @return The created instance.
*/
public static TransformationException createForCodec(
Throwable cause,
- String componentName,
- Format configurationFormat,
+ Format format,
+ boolean isVideo,
+ boolean isDecoder,
@Nullable String mediaCodecName,
int errorCode) {
+ String componentName = (isVideo ? "Video" : "Audio") + (isDecoder ? "Decoder" : "Encoder");
return new TransformationException(
- componentName
- + " error, format = "
- + configurationFormat
- + ", mediaCodecName="
- + mediaCodecName,
+ componentName + " error, format = " + format + ", mediaCodecName=" + mediaCodecName,
cause,
errorCode);
}
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java
index dee399daa2..843a389882 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java
@@ -135,7 +135,7 @@ import org.checkerframework.dataflow.qual.Pure;
actualOutputFormat.height,
inputFormat.pixelWidthHeightRatio,
transformationMatrix,
- /* outputSurface= */ checkNotNull(encoder.getInputSurface()),
+ /* outputSurface= */ encoder.getInputSurface(),
transformationRequest.enableHdrEditing,
debugViewProvider);
} else {
@@ -145,9 +145,7 @@ import org.checkerframework.dataflow.qual.Pure;
decoder =
decoderFactory.createForVideoDecoding(
inputFormat,
- frameEditor == null
- ? checkNotNull(encoder.getInputSurface())
- : frameEditor.getInputSurface());
+ frameEditor == null ? encoder.getInputSurface() : frameEditor.getInputSurface());
}
@Override
@@ -262,7 +260,7 @@ import org.checkerframework.dataflow.qual.Pure;
@Override
public void releaseOutputBuffer() throws TransformationException {
- encoder.releaseOutputBuffer();
+ encoder.releaseOutputBuffer(/* render= */ false);
}
@Override