diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioSamplePipeline.java index 82633e7da4..9d4b1eb873 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioSamplePipeline.java @@ -44,8 +44,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final Format inputFormat; private final Transformation transformation; + private final Codec.EncoderFactory encoderFactory; - private final MediaCodecAdapterWrapper decoder; + private final Codec decoder; private final DecoderInputBuffer decoderInputBuffer; private final SonicAudioProcessor sonicAudioProcessor; @@ -55,7 +56,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final DecoderInputBuffer encoderOutputBuffer; private @MonotonicNonNull AudioFormat encoderInputAudioFormat; - private @MonotonicNonNull MediaCodecAdapterWrapper encoder; + private @MonotonicNonNull Codec encoder; private long nextEncoderInputBufferTimeUs; private long encoderBufferDurationRemainder; @@ -63,10 +64,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private boolean drainingSonicForSpeedChange; private float currentSpeed; - public AudioSamplePipeline(Format inputFormat, Transformation transformation) + public AudioSamplePipeline( + Format inputFormat, + Transformation transformation, + Codec.EncoderFactory encoderFactory, + Codec.DecoderFactory decoderFactory) throws TransformationException { this.inputFormat = inputFormat; this.transformation = transformation; + this.encoderFactory = encoderFactory; decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); encoderInputBuffer = @@ -77,7 +83,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; speedProvider = new SegmentSpeedProvider(inputFormat); currentSpeed = speedProvider.getSpeed(0); - this.decoder = MediaCodecAdapterWrapper.createForAudioDecoding(inputFormat); + this.decoder = decoderFactory.createForAudioDecoding(inputFormat); } @Override @@ -301,7 +307,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } } encoder = - MediaCodecAdapterWrapper.createForAudioEncoding( + encoderFactory.createForAudioEncoding( new Format.Builder() .setSampleMimeType( transformation.audioMimeType == null diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java new file mode 100644 index 0000000000..f19cb91b64 --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java @@ -0,0 +1,324 @@ +/* + * 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 static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; + +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.UnstableApi; +import androidx.media3.decoder.DecoderInputBuffer; +import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter; +import com.google.common.collect.ImmutableList; +import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * A wrapper around {@link MediaCodecAdapter}. + * + *

Provides a layer of abstraction for callers that need to interact with {@link MediaCodec} + * through {@link MediaCodecAdapter}. This is done by simplifying the calls needed to queue and + * dequeue buffers, removing the need to track buffer indices and codec events. + */ +@UnstableApi +public final class Codec { + + /** A factory for {@link Codec decoder} instances. */ + public interface DecoderFactory { + + /** A default {@code DecoderFactory} implementation. */ + DecoderFactory DEFAULT = new DefaultCodecFactory(); + + /** + * Returns a {@link Codec} for audio decoding. + * + * @param format The {@link Format} (of the input data) used to determine the underlying {@link + * MediaCodec} and its configuration values. + * @return A configured and started decoder wrapper. + * @throws TransformationException If the underlying codec cannot be created. + */ + Codec createForAudioDecoding(Format format) throws TransformationException; + + /** + * Returns a {@link Codec} for video decoding. + * + * @param format The {@link Format} (of the input data) used to determine the underlying {@link + * MediaCodec} and its configuration values. + * @param surface The {@link Surface} to which the decoder output is rendered. + * @return A configured and started decoder wrapper. + * @throws TransformationException If the underlying codec cannot be created. + */ + Codec createForVideoDecoding(Format format, Surface surface) throws TransformationException; + } + + /** A factory for {@link Codec encoder} instances. */ + public interface EncoderFactory { + + /** A default {@code EncoderFactory} implementation. */ + EncoderFactory DEFAULT = new DefaultCodecFactory(); + + /** + * Returns a {@link Codec} for audio encoding. + * + * @param format The {@link Format} (of the output data) used to determine the underlying {@link + * MediaCodec} and its configuration values. + * @return A configured and started encoder wrapper. + * @throws TransformationException If the underlying codec cannot be created. + */ + Codec createForAudioEncoding(Format format) throws TransformationException; + + /** + * Returns a {@link Codec} for video encoding. + * + * @param format The {@link Format} (of the output data) used to determine the underlying {@link + * MediaCodec} and its configuration values. {@link Format#sampleMimeType}, {@link + * Format#width} and {@link Format#height} must be set to those of the desired output video + * format. {@link Format#rotationDegrees} should be 0. The video should always be in + * landscape orientation. + * @return A configured and started encoder wrapper. + * @throws TransformationException If the underlying codec cannot be created. + */ + Codec createForVideoEncoding(Format format) 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 MediaCodecAdapter mediaCodecAdapter; + + 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 MediaCodecAdapter}. */ + public Codec(MediaCodecAdapter mediaCodecAdapter) { + this.mediaCodecAdapter = mediaCodecAdapter; + outputBufferInfo = new BufferInfo(); + inputBufferIndex = C.INDEX_UNSET; + outputBufferIndex = C.INDEX_UNSET; + } + + /** Returns the input {@link Surface}, or null if the input is not a surface. */ + @Nullable + public Surface getInputSurface() { + return mediaCodecAdapter.getInputSurface(); + } + + /** + * Dequeues a writable input buffer, if available. + * + * @param inputBuffer The buffer where the dequeued buffer data is stored. + * @return Whether an input buffer is ready to be used. + */ + @EnsuresNonNullIf(expression = "#1.data", result = true) + public boolean maybeDequeueInputBuffer(DecoderInputBuffer inputBuffer) { + if (inputStreamEnded) { + return false; + } + if (inputBufferIndex < 0) { + inputBufferIndex = mediaCodecAdapter.dequeueInputBufferIndex(); + if (inputBufferIndex < 0) { + return false; + } + inputBuffer.data = mediaCodecAdapter.getInputBuffer(inputBufferIndex); + inputBuffer.clear(); + } + checkNotNull(inputBuffer.data); + return true; + } + + /** + * Queues an input buffer to the decoder. No buffers may be queued after an {@link + * DecoderInputBuffer#isEndOfStream() end of stream} buffer has been queued. + */ + public void queueInputBuffer(DecoderInputBuffer inputBuffer) { + 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; + } + mediaCodecAdapter.queueInputBuffer(inputBufferIndex, offset, size, inputBuffer.timeUs, flags); + inputBufferIndex = C.INDEX_UNSET; + inputBuffer.data = null; + } + + public void signalEndOfInputStream() { + mediaCodecAdapter.signalEndOfInputStream(); + } + + /** Returns the current output format, if available. */ + @Nullable + public Format getOutputFormat() { + // The format is updated when dequeueing a 'special' buffer index, so attempt to dequeue now. + maybeDequeueOutputBuffer(); + return outputFormat; + } + + /** Returns the current output {@link ByteBuffer}, if available. */ + @Nullable + public ByteBuffer getOutputBuffer() { + return maybeDequeueAndSetOutputBuffer() ? outputBuffer : null; + } + + /** Returns the {@link BufferInfo} associated with the current output buffer, if available. */ + @Nullable + public BufferInfo getOutputBufferInfo() { + return maybeDequeueOutputBuffer() ? outputBufferInfo : null; + } + + /** + * 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. + */ + public void releaseOutputBuffer() { + 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 + * 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. + */ + public void releaseOutputBuffer(boolean render) { + outputBuffer = null; + mediaCodecAdapter.releaseOutputBuffer(outputBufferIndex, render); + 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; + mediaCodecAdapter.release(); + } + + /** + * Tries obtaining an output buffer and sets {@link #outputBuffer} to the obtained output buffer. + * + * @return {@code true} if a buffer is successfully obtained, {@code false} otherwise. + */ + private boolean maybeDequeueAndSetOutputBuffer() { + if (!maybeDequeueOutputBuffer()) { + return false; + } + + outputBuffer = checkNotNull(mediaCodecAdapter.getOutputBuffer(outputBufferIndex)); + outputBuffer.position(outputBufferInfo.offset); + outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); + return true; + } + + /** + * Returns true if there is already an output buffer pending. Otherwise attempts to dequeue an + * output buffer and returns whether there is a new output buffer. + */ + private boolean maybeDequeueOutputBuffer() { + if (outputBufferIndex >= 0) { + return true; + } + if (outputStreamEnded) { + return false; + } + + outputBufferIndex = mediaCodecAdapter.dequeueOutputBufferIndex(outputBufferInfo); + if (outputBufferIndex < 0) { + if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + outputFormat = getFormat(mediaCodecAdapter.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; + } + return true; + } + + 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(); + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodecFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodecFactory.java new file mode 100644 index 0000000000..4ed894db75 --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodecFactory.java @@ -0,0 +1,194 @@ +/* + * 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 static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Util.SDK_INT; + +import android.annotation.SuppressLint; +import android.media.MediaCodec; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaFormat; +import android.view.Surface; +import androidx.media3.common.Format; +import androidx.media3.common.util.MediaFormatUtil; +import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter; +import androidx.media3.exoplayer.mediacodec.MediaCodecInfo; +import androidx.media3.exoplayer.mediacodec.SynchronousMediaCodecAdapter; +import java.io.IOException; + +/** A default {@link Codec.DecoderFactory} and {@link Codec.EncoderFactory}. */ +/* package */ final class DefaultCodecFactory + implements Codec.DecoderFactory, Codec.EncoderFactory { + + @Override + public Codec createForAudioDecoding(Format format) throws TransformationException { + MediaFormat mediaFormat = + MediaFormat.createAudioFormat( + checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); + MediaFormatUtil.maybeSetInteger( + mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); + MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + + MediaCodecAdapter adapter; + try { + adapter = + new MediaCodecFactory() + .createAdapter( + MediaCodecAdapter.Configuration.createForAudioDecoding( + createPlaceholderMediaCodecInfo(), mediaFormat, format, /* crypto= */ null)); + } catch (Exception e) { + throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ true); + } + return new Codec(adapter); + } + + @Override + @SuppressLint("InlinedApi") + public Codec createForVideoDecoding(Format format, Surface surface) + throws TransformationException { + MediaFormat mediaFormat = + MediaFormat.createVideoFormat( + checkNotNull(format.sampleMimeType), format.width, format.height); + MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees); + MediaFormatUtil.maybeSetInteger( + mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); + MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + if (SDK_INT >= 29) { + // On API levels over 29, Transformer decodes as many frames as possible in one render + // cycle. This key ensures no frame dropping when the decoder's output surface is full. + mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0); + } + + MediaCodecAdapter adapter; + try { + adapter = + new MediaCodecFactory() + .createAdapter( + MediaCodecAdapter.Configuration.createForVideoDecoding( + createPlaceholderMediaCodecInfo(), + mediaFormat, + format, + surface, + /* crypto= */ null)); + } catch (Exception e) { + throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ true); + } + return new Codec(adapter); + } + + @Override + public Codec createForAudioEncoding(Format format) throws TransformationException { + MediaFormat mediaFormat = + MediaFormat.createAudioFormat( + checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate); + + MediaCodecAdapter adapter; + try { + adapter = + new MediaCodecFactory() + .createAdapter( + MediaCodecAdapter.Configuration.createForAudioEncoding( + createPlaceholderMediaCodecInfo(), mediaFormat, format)); + } catch (Exception e) { + throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ false); + } + return new Codec(adapter); + } + + @Override + public Codec createForVideoEncoding(Format format) throws TransformationException { + checkArgument(format.width != Format.NO_VALUE); + checkArgument(format.height != Format.NO_VALUE); + // According to interface Javadoc, format.rotationDegrees should be 0. The video should always + // be in landscape orientation. + checkArgument(format.height < format.width); + checkArgument(format.rotationDegrees == 0); + + MediaFormat mediaFormat = + MediaFormat.createVideoFormat( + checkNotNull(format.sampleMimeType), format.width, format.height); + mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface); + mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30); + mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 413_000); + + MediaCodecAdapter adapter; + try { + adapter = + new MediaCodecFactory() + .createAdapter( + MediaCodecAdapter.Configuration.createForVideoEncoding( + createPlaceholderMediaCodecInfo(), mediaFormat, format)); + } catch (Exception e) { + throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ false); + } + return new Codec(adapter); + } + + private static final class MediaCodecFactory extends SynchronousMediaCodecAdapter.Factory { + @Override + protected MediaCodec createCodec(MediaCodecAdapter.Configuration configuration) + throws IOException { + String sampleMimeType = + checkNotNull(configuration.mediaFormat.getString(MediaFormat.KEY_MIME)); + boolean isDecoder = (configuration.flags & MediaCodec.CONFIGURE_FLAG_ENCODE) == 0; + return isDecoder + ? MediaCodec.createDecoderByType(checkNotNull(sampleMimeType)) + : MediaCodec.createEncoderByType(checkNotNull(sampleMimeType)); + } + } + + private static MediaCodecInfo createPlaceholderMediaCodecInfo() { + return MediaCodecInfo.newInstance( + /* name= */ "name-placeholder", + /* mimeType= */ "mime-type-placeholder", + /* codecMimeType= */ "mime-type-placeholder", + /* capabilities= */ null, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ false, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false); + } + + private static TransformationException createTransformationException( + Exception cause, Format format, boolean isVideo, boolean isDecoder) { + String componentName = (isVideo ? "Video" : "Audio") + (isDecoder ? "Decoder" : "Encoder"); + if (cause instanceof IOException) { + return TransformationException.createForCodec( + cause, + componentName, + format, + isDecoder + ? TransformationException.ERROR_CODE_DECODER_INIT_FAILED + : TransformationException.ERROR_CODE_ENCODER_INIT_FAILED); + } + if (cause instanceof IllegalArgumentException) { + return TransformationException.createForCodec( + cause, + componentName, + format, + isDecoder + ? TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED + : TransformationException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED); + } + return TransformationException.createForUnexpected(cause); + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/MediaCodecAdapterWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/MediaCodecAdapterWrapper.java deleted file mode 100644 index 9a5ac58e39..0000000000 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/MediaCodecAdapterWrapper.java +++ /dev/null @@ -1,471 +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 static androidx.media3.common.util.Assertions.checkArgument; -import static androidx.media3.common.util.Assertions.checkNotNull; -import static androidx.media3.common.util.Assertions.checkState; -import static androidx.media3.common.util.Util.SDK_INT; - -import android.annotation.SuppressLint; -import android.media.MediaCodec; -import android.media.MediaCodec.BufferInfo; -import android.media.MediaCodecInfo.CodecCapabilities; -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.MediaFormatUtil; -import androidx.media3.decoder.DecoderInputBuffer; -import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter; -import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter.Configuration; -import androidx.media3.exoplayer.mediacodec.MediaCodecInfo; -import androidx.media3.exoplayer.mediacodec.SynchronousMediaCodecAdapter; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Map; -import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; - -/** - * A wrapper around {@link MediaCodecAdapter}. - * - *

Provides a layer of abstraction for callers that need to interact with {@link MediaCodec} - * through {@link MediaCodecAdapter}. This is done by simplifying the calls needed to queue and - * dequeue buffers, removing the need to track buffer indices and codec events. - */ -/* package */ final class MediaCodecAdapterWrapper { - - // 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 MediaCodecAdapter codec; - - private @MonotonicNonNull Format outputFormat; - @Nullable private ByteBuffer outputBuffer; - - private int inputBufferIndex; - private int outputBufferIndex; - private boolean inputStreamEnded; - private boolean outputStreamEnded; - - private static class Factory extends SynchronousMediaCodecAdapter.Factory { - @Override - protected MediaCodec createCodec(Configuration configuration) throws IOException { - String sampleMimeType = - checkNotNull(configuration.mediaFormat.getString(MediaFormat.KEY_MIME)); - boolean isDecoder = (configuration.flags & MediaCodec.CONFIGURE_FLAG_ENCODE) == 0; - return isDecoder - ? MediaCodec.createDecoderByType(checkNotNull(sampleMimeType)) - : MediaCodec.createEncoderByType(checkNotNull(sampleMimeType)); - } - } - - private static MediaCodecInfo createPlaceholderMediaCodecInfo() { - return MediaCodecInfo.newInstance( - /* name= */ "name-placeholder", - /* mimeType= */ "mime-type-placeholder", - /* codecMimeType= */ "mime-type-placeholder", - /* capabilities= */ null, - /* hardwareAccelerated= */ false, - /* softwareOnly= */ false, - /* vendor= */ false, - /* forceDisableAdaptive= */ false, - /* forceSecure= */ false); - } - - /** - * Returns a {@link MediaCodecAdapterWrapper} for a configured and started {@link - * MediaCodecAdapter} audio decoder. - * - * @param format The {@link Format} (of the input data) used to determine the underlying {@link - * MediaCodec} and its configuration values. - * @return A configured and started decoder wrapper. - * @throws TransformationException If the underlying codec cannot be created. - */ - public static MediaCodecAdapterWrapper createForAudioDecoding(Format format) - throws TransformationException { - MediaFormat mediaFormat = - MediaFormat.createAudioFormat( - checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); - MediaFormatUtil.maybeSetInteger( - mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); - MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); - - MediaCodecAdapter adapter; - try { - adapter = - new Factory() - .createAdapter( - MediaCodecAdapter.Configuration.createForAudioDecoding( - createPlaceholderMediaCodecInfo(), mediaFormat, format, /* crypto= */ null)); - } catch (Exception e) { - throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ true); - } - return new MediaCodecAdapterWrapper(adapter); - } - - /** - * Returns a {@link MediaCodecAdapterWrapper} for a configured and started {@link - * MediaCodecAdapter} video decoder. - * - * @param format The {@link Format} (of the input data) used to determine the underlying {@link - * MediaCodec} and its configuration values. - * @param surface The {@link Surface} to which the decoder output is rendered. - * @return A configured and started decoder wrapper. - * @throws TransformationException If the underlying codec cannot be created. - */ - @SuppressLint("InlinedApi") - public static MediaCodecAdapterWrapper createForVideoDecoding(Format format, Surface surface) - throws TransformationException { - MediaFormat mediaFormat = - MediaFormat.createVideoFormat( - checkNotNull(format.sampleMimeType), format.width, format.height); - MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees); - MediaFormatUtil.maybeSetInteger( - mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); - MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); - if (SDK_INT >= 29) { - // On API levels over 29, Transformer decodes as many frames as possible in one render - // cycle. This key ensures no frame dropping when the decoder's output surface is full. - mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0); - } - - MediaCodecAdapter adapter; - try { - adapter = - new Factory() - .createAdapter( - MediaCodecAdapter.Configuration.createForVideoDecoding( - createPlaceholderMediaCodecInfo(), - mediaFormat, - format, - surface, - /* crypto= */ null)); - } catch (Exception e) { - throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ true); - } - return new MediaCodecAdapterWrapper(adapter); - } - - /** - * Returns a {@link MediaCodecAdapterWrapper} for a configured and started {@link - * MediaCodecAdapter} audio encoder. - * - * @param format The {@link Format} (of the output data) used to determine the underlying {@link - * MediaCodec} and its configuration values. - * @return A configured and started encoder wrapper. - * @throws TransformationException If the underlying codec cannot be created. - */ - public static MediaCodecAdapterWrapper createForAudioEncoding(Format format) - throws TransformationException { - MediaFormat mediaFormat = - MediaFormat.createAudioFormat( - checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); - mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate); - - MediaCodecAdapter adapter; - try { - adapter = - new Factory() - .createAdapter( - MediaCodecAdapter.Configuration.createForAudioEncoding( - createPlaceholderMediaCodecInfo(), mediaFormat, format)); - } catch (Exception e) { - throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ false); - } - return new MediaCodecAdapterWrapper(adapter); - } - - /** - * Returns a {@link MediaCodecAdapterWrapper} for a configured and started {@link - * MediaCodecAdapter} video encoder. - * - * @param format The {@link Format} (of the output data) used to determine the underlying {@link - * MediaCodec} and its configuration values. {@link Format#sampleMimeType}, {@link - * Format#width} and {@link Format#height} must be set to those of the desired output video - * format. {@link Format#rotationDegrees} should be 0. The video should always be in landscape - * orientation. - * @param additionalEncoderConfig A map of {@link MediaFormat}'s integer settings, where the keys - * are from {@code MediaFormat.KEY_*} constants. Its values will override those in {@code - * format}. - * @return A configured and started encoder wrapper. - * @throws TransformationException If the underlying codec cannot be created. - */ - public static MediaCodecAdapterWrapper createForVideoEncoding( - Format format, Map additionalEncoderConfig) throws TransformationException { - checkArgument(format.width != Format.NO_VALUE); - checkArgument(format.height != Format.NO_VALUE); - checkArgument(format.height < format.width); - checkArgument(format.rotationDegrees == 0); - - MediaFormat mediaFormat = - MediaFormat.createVideoFormat( - checkNotNull(format.sampleMimeType), format.width, format.height); - mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface); - mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30); - mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); - mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 413_000); - for (Map.Entry encoderSetting : additionalEncoderConfig.entrySet()) { - mediaFormat.setInteger(encoderSetting.getKey(), encoderSetting.getValue()); - } - - MediaCodecAdapter adapter; - try { - adapter = - new Factory() - .createAdapter( - MediaCodecAdapter.Configuration.createForVideoEncoding( - createPlaceholderMediaCodecInfo(), mediaFormat, format)); - } catch (Exception e) { - throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ false); - } - return new MediaCodecAdapterWrapper(adapter); - } - - private MediaCodecAdapterWrapper(MediaCodecAdapter codec) { - this.codec = codec; - outputBufferInfo = new BufferInfo(); - inputBufferIndex = C.INDEX_UNSET; - outputBufferIndex = C.INDEX_UNSET; - } - - /** Returns the input {@link Surface}, or null if the input is not a surface. */ - @Nullable - public Surface getInputSurface() { - return codec.getInputSurface(); - } - - /** - * Dequeues a writable input buffer, if available. - * - * @param inputBuffer The buffer where the dequeued buffer data is stored. - * @return Whether an input buffer is ready to be used. - */ - @EnsuresNonNullIf(expression = "#1.data", result = true) - public boolean maybeDequeueInputBuffer(DecoderInputBuffer inputBuffer) { - if (inputStreamEnded) { - return false; - } - if (inputBufferIndex < 0) { - inputBufferIndex = codec.dequeueInputBufferIndex(); - if (inputBufferIndex < 0) { - return false; - } - inputBuffer.data = codec.getInputBuffer(inputBufferIndex); - inputBuffer.clear(); - } - checkNotNull(inputBuffer.data); - return true; - } - - /** - * Queues an input buffer to the decoder. No buffers may be queued after an {@link - * DecoderInputBuffer#isEndOfStream() end of stream} buffer has been queued. - */ - public void queueInputBuffer(DecoderInputBuffer inputBuffer) { - 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; - } - codec.queueInputBuffer(inputBufferIndex, offset, size, inputBuffer.timeUs, flags); - inputBufferIndex = C.INDEX_UNSET; - inputBuffer.data = null; - } - - public void signalEndOfInputStream() { - codec.signalEndOfInputStream(); - } - - /** Returns the current output format, if available. */ - @Nullable - public Format getOutputFormat() { - // The format is updated when dequeueing a 'special' buffer index, so attempt to dequeue now. - maybeDequeueOutputBuffer(); - return outputFormat; - } - - /** Returns the current output {@link ByteBuffer}, if available. */ - @Nullable - public ByteBuffer getOutputBuffer() { - return maybeDequeueAndSetOutputBuffer() ? outputBuffer : null; - } - - /** Returns the {@link BufferInfo} associated with the current output buffer, if available. */ - @Nullable - public BufferInfo getOutputBufferInfo() { - return maybeDequeueOutputBuffer() ? outputBufferInfo : null; - } - - /** - * 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. - */ - public void releaseOutputBuffer() { - 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 - * 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. - */ - public void releaseOutputBuffer(boolean render) { - outputBuffer = null; - codec.releaseOutputBuffer(outputBufferIndex, render); - 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; - codec.release(); - } - - /** - * Tries obtaining an output buffer and sets {@link #outputBuffer} to the obtained output buffer. - * - * @return {@code true} if a buffer is successfully obtained, {@code false} otherwise. - */ - private boolean maybeDequeueAndSetOutputBuffer() { - if (!maybeDequeueOutputBuffer()) { - return false; - } - - outputBuffer = checkNotNull(codec.getOutputBuffer(outputBufferIndex)); - outputBuffer.position(outputBufferInfo.offset); - outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); - return true; - } - - /** - * Returns true if there is already an output buffer pending. Otherwise attempts to dequeue an - * output buffer and returns whether there is a new output buffer. - */ - private boolean maybeDequeueOutputBuffer() { - if (outputBufferIndex >= 0) { - return true; - } - if (outputStreamEnded) { - return false; - } - - outputBufferIndex = codec.dequeueOutputBufferIndex(outputBufferInfo); - if (outputBufferIndex < 0) { - if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - outputFormat = getFormat(codec.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; - } - return true; - } - - 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 TransformationException createTransformationException( - Exception cause, Format format, boolean isVideo, boolean isDecoder) { - String componentName = (isVideo ? "Video" : "Audio") + (isDecoder ? "Decoder" : "Encoder"); - if (cause instanceof IOException) { - return TransformationException.createForCodec( - cause, - componentName, - format, - isDecoder - ? TransformationException.ERROR_CODE_DECODER_INIT_FAILED - : TransformationException.ERROR_CODE_ENCODER_INIT_FAILED); - } - if (cause instanceof IllegalArgumentException) { - return TransformationException.createForCodec( - cause, - componentName, - format, - isDecoder - ? TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED - : TransformationException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED); - } - return TransformationException.createForUnexpected(cause); - } -} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index 1b6c447f0e..29c2f2eb3a 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -109,6 +109,7 @@ public final class Transformer { private DebugViewProvider debugViewProvider; private Looper looper; private Clock clock; + private Codec.EncoderFactory encoderFactory; /** @deprecated Use {@link #Builder(Context)} instead. */ @Deprecated @@ -120,6 +121,7 @@ public final class Transformer { listener = new Listener() {}; looper = Util.getCurrentOrMainLooper(); clock = Clock.DEFAULT; + encoderFactory = Codec.EncoderFactory.DEFAULT; debugViewProvider = DebugViewProvider.NONE; } @@ -137,6 +139,7 @@ public final class Transformer { listener = new Listener() {}; looper = Util.getCurrentOrMainLooper(); clock = Clock.DEFAULT; + encoderFactory = Codec.EncoderFactory.DEFAULT; debugViewProvider = DebugViewProvider.NONE; } @@ -155,6 +158,7 @@ public final class Transformer { this.videoMimeType = transformer.transformation.videoMimeType; this.listener = transformer.listener; this.looper = transformer.looper; + this.encoderFactory = transformer.encoderFactory; this.debugViewProvider = transformer.debugViewProvider; this.clock = transformer.clock; } @@ -362,6 +366,18 @@ public final class Transformer { return this; } + /** + * Sets the {@link Codec.EncoderFactory} that will be used by the transformer. The default value + * is {@link Codec.EncoderFactory#DEFAULT}. + * + * @param encoderFactory The {@link Codec.EncoderFactory} instance. + * @return This builder. + */ + public Builder setEncoderFactory(Codec.EncoderFactory encoderFactory) { + this.encoderFactory = encoderFactory; + return this; + } + /** * Sets a provider for views to show diagnostic information (if available) during * transformation. This is intended for debugging. The default value is {@link @@ -450,6 +466,8 @@ public final class Transformer { listener, looper, clock, + encoderFactory, + Codec.DecoderFactory.DEFAULT, debugViewProvider); } @@ -538,6 +556,8 @@ public final class Transformer { private final Transformation transformation; private final Looper looper; private final Clock clock; + private final Codec.EncoderFactory encoderFactory; + private final Codec.DecoderFactory decoderFactory; private final Transformer.DebugViewProvider debugViewProvider; private Transformer.Listener listener; @@ -553,6 +573,8 @@ public final class Transformer { Transformer.Listener listener, Looper looper, Clock clock, + Codec.EncoderFactory encoderFactory, + Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider) { checkState( !transformation.removeAudio || !transformation.removeVideo, @@ -564,6 +586,8 @@ public final class Transformer { this.listener = listener; this.looper = looper; this.clock = clock; + this.encoderFactory = encoderFactory; + this.decoderFactory = decoderFactory; this.debugViewProvider = debugViewProvider; progressState = PROGRESS_STATE_NO_TRANSFORMATION; } @@ -664,7 +688,12 @@ public final class Transformer { new ExoPlayer.Builder( context, new TransformerRenderersFactory( - context, muxerWrapper, transformation, debugViewProvider)) + context, + muxerWrapper, + transformation, + encoderFactory, + decoderFactory, + debugViewProvider)) .setMediaSourceFactory(mediaSourceFactory) .setTrackSelector(trackSelector) .setLoadControl(loadControl) @@ -753,16 +782,22 @@ public final class Transformer { private final MuxerWrapper muxerWrapper; private final TransformerMediaClock mediaClock; private final Transformation transformation; + private final Codec.EncoderFactory encoderFactory; + private final Codec.DecoderFactory decoderFactory; private final Transformer.DebugViewProvider debugViewProvider; public TransformerRenderersFactory( Context context, MuxerWrapper muxerWrapper, Transformation transformation, + Codec.EncoderFactory encoderFactory, + Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider) { this.context = context; this.muxerWrapper = muxerWrapper; this.transformation = transformation; + this.encoderFactory = encoderFactory; + this.decoderFactory = decoderFactory; this.debugViewProvider = debugViewProvider; mediaClock = new TransformerMediaClock(); } @@ -778,13 +813,21 @@ public final class Transformer { Renderer[] renderers = new Renderer[rendererCount]; int index = 0; if (!transformation.removeAudio) { - renderers[index] = new TransformerAudioRenderer(muxerWrapper, mediaClock, transformation); + renderers[index] = + new TransformerAudioRenderer( + muxerWrapper, mediaClock, transformation, encoderFactory, decoderFactory); index++; } if (!transformation.removeVideo) { renderers[index] = new TransformerVideoRenderer( - context, muxerWrapper, mediaClock, transformation, debugViewProvider); + context, + muxerWrapper, + mediaClock, + transformation, + encoderFactory, + decoderFactory, + debugViewProvider); index++; } return renderers; diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerAudioRenderer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerAudioRenderer.java index 3819659d3b..9bc0ed82d5 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerAudioRenderer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerAudioRenderer.java @@ -32,11 +32,19 @@ import androidx.media3.extractor.metadata.mp4.SlowMotionData; private static final String TAG = "TAudioRenderer"; + private final Codec.EncoderFactory encoderFactory; + private final Codec.DecoderFactory decoderFactory; private final DecoderInputBuffer decoderInputBuffer; public TransformerAudioRenderer( - MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, Transformation transformation) { + MuxerWrapper muxerWrapper, + TransformerMediaClock mediaClock, + Transformation transformation, + Codec.EncoderFactory encoderFactory, + Codec.DecoderFactory decoderFactory) { super(C.TRACK_TYPE_AUDIO, muxerWrapper, mediaClock, transformation); + this.encoderFactory = encoderFactory; + this.decoderFactory = decoderFactory; decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); } @@ -60,7 +68,8 @@ import androidx.media3.extractor.metadata.mp4.SlowMotionData; } Format inputFormat = checkNotNull(formatHolder.format); if (shouldTranscode(inputFormat)) { - samplePipeline = new AudioSamplePipeline(inputFormat, transformation); + samplePipeline = + new AudioSamplePipeline(inputFormat, transformation, encoderFactory, decoderFactory); } else { samplePipeline = new PassthroughSamplePipeline(inputFormat); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java index dee7d7f048..ea70704118 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java @@ -34,6 +34,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private static final String TAG = "TVideoRenderer"; private final Context context; + private final Codec.EncoderFactory encoderFactory; + private final Codec.DecoderFactory decoderFactory; private final Transformer.DebugViewProvider debugViewProvider; private final DecoderInputBuffer decoderInputBuffer; @@ -44,9 +46,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; MuxerWrapper muxerWrapper, TransformerMediaClock mediaClock, Transformation transformation, + Codec.EncoderFactory encoderFactory, + Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider) { super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformation); this.context = context; + this.encoderFactory = encoderFactory; + this.decoderFactory = decoderFactory; this.debugViewProvider = debugViewProvider; decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); @@ -72,7 +78,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; Format inputFormat = checkNotNull(formatHolder.format); if (shouldTranscode(inputFormat)) { samplePipeline = - new VideoSamplePipeline(context, inputFormat, transformation, debugViewProvider); + new VideoSamplePipeline( + context, + inputFormat, + transformation, + encoderFactory, + decoderFactory, + debugViewProvider); } else { samplePipeline = new PassthroughSamplePipeline(inputFormat); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoSamplePipeline.java index f8bf864e57..e2416fb1dc 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoSamplePipeline.java @@ -27,7 +27,6 @@ import androidx.annotation.RequiresApi; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.decoder.DecoderInputBuffer; -import com.google.common.collect.ImmutableMap; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -39,9 +38,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final int outputRotationDegrees; private final DecoderInputBuffer decoderInputBuffer; - private final MediaCodecAdapterWrapper decoder; + private final Codec decoder; - private final MediaCodecAdapterWrapper encoder; + private final Codec encoder; private final DecoderInputBuffer encoderOutputBuffer; private @MonotonicNonNull FrameEditor frameEditor; @@ -52,6 +51,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Context context, Format inputFormat, Transformation transformation, + Codec.EncoderFactory encoderFactory, + Codec.DecoderFactory decoderFactory, Transformer.DebugViewProvider debugViewProvider) throws TransformationException { decoderInputBuffer = @@ -85,7 +86,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; transformation.transformationMatrix.postRotate(outputRotationDegrees); encoder = - MediaCodecAdapterWrapper.createForVideoEncoding( + encoderFactory.createForVideoEncoding( new Format.Builder() .setWidth(outputWidth) .setHeight(outputHeight) @@ -94,8 +95,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; transformation.videoMimeType != null ? transformation.videoMimeType : inputFormat.sampleMimeType) - .build(), - ImmutableMap.of()); + .build()); if (inputFormat.height != outputHeight || inputFormat.width != outputWidth || !transformation.transformationMatrix.isIdentity()) { @@ -109,7 +109,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; debugViewProvider); } decoder = - MediaCodecAdapterWrapper.createForVideoDecoding( + decoderFactory.createForVideoDecoding( inputFormat, frameEditor == null ? checkNotNull(encoder.getInputSurface())