Rename MediaCodecAdapterWrapper to Codec.

Move static factories into a separate class and make it implement an interface
that will let tests customize encoder/decoder creation.

PiperOrigin-RevId: 417610825
This commit is contained in:
claincly 2021-12-21 14:41:32 +00:00 committed by tonihei
parent 89271eb722
commit 4706d3a59f
8 changed files with 606 additions and 489 deletions

View File

@ -44,8 +44,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final Format inputFormat; private final Format inputFormat;
private final Transformation transformation; private final Transformation transformation;
private final Codec.EncoderFactory encoderFactory;
private final MediaCodecAdapterWrapper decoder; private final Codec decoder;
private final DecoderInputBuffer decoderInputBuffer; private final DecoderInputBuffer decoderInputBuffer;
private final SonicAudioProcessor sonicAudioProcessor; private final SonicAudioProcessor sonicAudioProcessor;
@ -55,7 +56,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final DecoderInputBuffer encoderOutputBuffer; private final DecoderInputBuffer encoderOutputBuffer;
private @MonotonicNonNull AudioFormat encoderInputAudioFormat; private @MonotonicNonNull AudioFormat encoderInputAudioFormat;
private @MonotonicNonNull MediaCodecAdapterWrapper encoder; private @MonotonicNonNull Codec encoder;
private long nextEncoderInputBufferTimeUs; private long nextEncoderInputBufferTimeUs;
private long encoderBufferDurationRemainder; private long encoderBufferDurationRemainder;
@ -63,10 +64,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private boolean drainingSonicForSpeedChange; private boolean drainingSonicForSpeedChange;
private float currentSpeed; private float currentSpeed;
public AudioSamplePipeline(Format inputFormat, Transformation transformation) public AudioSamplePipeline(
Format inputFormat,
Transformation transformation,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory)
throws TransformationException { throws TransformationException {
this.inputFormat = inputFormat; this.inputFormat = inputFormat;
this.transformation = transformation; this.transformation = transformation;
this.encoderFactory = encoderFactory;
decoderInputBuffer = decoderInputBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
encoderInputBuffer = encoderInputBuffer =
@ -77,7 +83,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER;
speedProvider = new SegmentSpeedProvider(inputFormat); speedProvider = new SegmentSpeedProvider(inputFormat);
currentSpeed = speedProvider.getSpeed(0); currentSpeed = speedProvider.getSpeed(0);
this.decoder = MediaCodecAdapterWrapper.createForAudioDecoding(inputFormat); this.decoder = decoderFactory.createForAudioDecoding(inputFormat);
} }
@Override @Override
@ -301,7 +307,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
} }
encoder = encoder =
MediaCodecAdapterWrapper.createForAudioEncoding( encoderFactory.createForAudioEncoding(
new Format.Builder() new Format.Builder()
.setSampleMimeType( .setSampleMimeType(
transformation.audioMimeType == null transformation.audioMimeType == null

View File

@ -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}.
*
* <p>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.
*
* <p>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.
*
* <p>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<byte[]> 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();
}
}

View File

@ -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);
}
}

View File

@ -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}.
*
* <p>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<String, Integer> 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<String, Integer> 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.
*
* <p>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.
*
* <p>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<byte[]> 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);
}
}

View File

@ -109,6 +109,7 @@ public final class Transformer {
private DebugViewProvider debugViewProvider; private DebugViewProvider debugViewProvider;
private Looper looper; private Looper looper;
private Clock clock; private Clock clock;
private Codec.EncoderFactory encoderFactory;
/** @deprecated Use {@link #Builder(Context)} instead. */ /** @deprecated Use {@link #Builder(Context)} instead. */
@Deprecated @Deprecated
@ -120,6 +121,7 @@ public final class Transformer {
listener = new Listener() {}; listener = new Listener() {};
looper = Util.getCurrentOrMainLooper(); looper = Util.getCurrentOrMainLooper();
clock = Clock.DEFAULT; clock = Clock.DEFAULT;
encoderFactory = Codec.EncoderFactory.DEFAULT;
debugViewProvider = DebugViewProvider.NONE; debugViewProvider = DebugViewProvider.NONE;
} }
@ -137,6 +139,7 @@ public final class Transformer {
listener = new Listener() {}; listener = new Listener() {};
looper = Util.getCurrentOrMainLooper(); looper = Util.getCurrentOrMainLooper();
clock = Clock.DEFAULT; clock = Clock.DEFAULT;
encoderFactory = Codec.EncoderFactory.DEFAULT;
debugViewProvider = DebugViewProvider.NONE; debugViewProvider = DebugViewProvider.NONE;
} }
@ -155,6 +158,7 @@ public final class Transformer {
this.videoMimeType = transformer.transformation.videoMimeType; this.videoMimeType = transformer.transformation.videoMimeType;
this.listener = transformer.listener; this.listener = transformer.listener;
this.looper = transformer.looper; this.looper = transformer.looper;
this.encoderFactory = transformer.encoderFactory;
this.debugViewProvider = transformer.debugViewProvider; this.debugViewProvider = transformer.debugViewProvider;
this.clock = transformer.clock; this.clock = transformer.clock;
} }
@ -362,6 +366,18 @@ public final class Transformer {
return this; 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 * Sets a provider for views to show diagnostic information (if available) during
* transformation. This is intended for debugging. The default value is {@link * transformation. This is intended for debugging. The default value is {@link
@ -450,6 +466,8 @@ public final class Transformer {
listener, listener,
looper, looper,
clock, clock,
encoderFactory,
Codec.DecoderFactory.DEFAULT,
debugViewProvider); debugViewProvider);
} }
@ -538,6 +556,8 @@ public final class Transformer {
private final Transformation transformation; private final Transformation transformation;
private final Looper looper; private final Looper looper;
private final Clock clock; private final Clock clock;
private final Codec.EncoderFactory encoderFactory;
private final Codec.DecoderFactory decoderFactory;
private final Transformer.DebugViewProvider debugViewProvider; private final Transformer.DebugViewProvider debugViewProvider;
private Transformer.Listener listener; private Transformer.Listener listener;
@ -553,6 +573,8 @@ public final class Transformer {
Transformer.Listener listener, Transformer.Listener listener,
Looper looper, Looper looper,
Clock clock, Clock clock,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory,
Transformer.DebugViewProvider debugViewProvider) { Transformer.DebugViewProvider debugViewProvider) {
checkState( checkState(
!transformation.removeAudio || !transformation.removeVideo, !transformation.removeAudio || !transformation.removeVideo,
@ -564,6 +586,8 @@ public final class Transformer {
this.listener = listener; this.listener = listener;
this.looper = looper; this.looper = looper;
this.clock = clock; this.clock = clock;
this.encoderFactory = encoderFactory;
this.decoderFactory = decoderFactory;
this.debugViewProvider = debugViewProvider; this.debugViewProvider = debugViewProvider;
progressState = PROGRESS_STATE_NO_TRANSFORMATION; progressState = PROGRESS_STATE_NO_TRANSFORMATION;
} }
@ -664,7 +688,12 @@ public final class Transformer {
new ExoPlayer.Builder( new ExoPlayer.Builder(
context, context,
new TransformerRenderersFactory( new TransformerRenderersFactory(
context, muxerWrapper, transformation, debugViewProvider)) context,
muxerWrapper,
transformation,
encoderFactory,
decoderFactory,
debugViewProvider))
.setMediaSourceFactory(mediaSourceFactory) .setMediaSourceFactory(mediaSourceFactory)
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.setLoadControl(loadControl) .setLoadControl(loadControl)
@ -753,16 +782,22 @@ public final class Transformer {
private final MuxerWrapper muxerWrapper; private final MuxerWrapper muxerWrapper;
private final TransformerMediaClock mediaClock; private final TransformerMediaClock mediaClock;
private final Transformation transformation; private final Transformation transformation;
private final Codec.EncoderFactory encoderFactory;
private final Codec.DecoderFactory decoderFactory;
private final Transformer.DebugViewProvider debugViewProvider; private final Transformer.DebugViewProvider debugViewProvider;
public TransformerRenderersFactory( public TransformerRenderersFactory(
Context context, Context context,
MuxerWrapper muxerWrapper, MuxerWrapper muxerWrapper,
Transformation transformation, Transformation transformation,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory,
Transformer.DebugViewProvider debugViewProvider) { Transformer.DebugViewProvider debugViewProvider) {
this.context = context; this.context = context;
this.muxerWrapper = muxerWrapper; this.muxerWrapper = muxerWrapper;
this.transformation = transformation; this.transformation = transformation;
this.encoderFactory = encoderFactory;
this.decoderFactory = decoderFactory;
this.debugViewProvider = debugViewProvider; this.debugViewProvider = debugViewProvider;
mediaClock = new TransformerMediaClock(); mediaClock = new TransformerMediaClock();
} }
@ -778,13 +813,21 @@ public final class Transformer {
Renderer[] renderers = new Renderer[rendererCount]; Renderer[] renderers = new Renderer[rendererCount];
int index = 0; int index = 0;
if (!transformation.removeAudio) { if (!transformation.removeAudio) {
renderers[index] = new TransformerAudioRenderer(muxerWrapper, mediaClock, transformation); renderers[index] =
new TransformerAudioRenderer(
muxerWrapper, mediaClock, transformation, encoderFactory, decoderFactory);
index++; index++;
} }
if (!transformation.removeVideo) { if (!transformation.removeVideo) {
renderers[index] = renderers[index] =
new TransformerVideoRenderer( new TransformerVideoRenderer(
context, muxerWrapper, mediaClock, transformation, debugViewProvider); context,
muxerWrapper,
mediaClock,
transformation,
encoderFactory,
decoderFactory,
debugViewProvider);
index++; index++;
} }
return renderers; return renderers;

View File

@ -32,11 +32,19 @@ import androidx.media3.extractor.metadata.mp4.SlowMotionData;
private static final String TAG = "TAudioRenderer"; private static final String TAG = "TAudioRenderer";
private final Codec.EncoderFactory encoderFactory;
private final Codec.DecoderFactory decoderFactory;
private final DecoderInputBuffer decoderInputBuffer; private final DecoderInputBuffer decoderInputBuffer;
public TransformerAudioRenderer( 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); super(C.TRACK_TYPE_AUDIO, muxerWrapper, mediaClock, transformation);
this.encoderFactory = encoderFactory;
this.decoderFactory = decoderFactory;
decoderInputBuffer = decoderInputBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
} }
@ -60,7 +68,8 @@ import androidx.media3.extractor.metadata.mp4.SlowMotionData;
} }
Format inputFormat = checkNotNull(formatHolder.format); Format inputFormat = checkNotNull(formatHolder.format);
if (shouldTranscode(inputFormat)) { if (shouldTranscode(inputFormat)) {
samplePipeline = new AudioSamplePipeline(inputFormat, transformation); samplePipeline =
new AudioSamplePipeline(inputFormat, transformation, encoderFactory, decoderFactory);
} else { } else {
samplePipeline = new PassthroughSamplePipeline(inputFormat); samplePipeline = new PassthroughSamplePipeline(inputFormat);
} }

View File

@ -34,6 +34,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private static final String TAG = "TVideoRenderer"; private static final String TAG = "TVideoRenderer";
private final Context context; private final Context context;
private final Codec.EncoderFactory encoderFactory;
private final Codec.DecoderFactory decoderFactory;
private final Transformer.DebugViewProvider debugViewProvider; private final Transformer.DebugViewProvider debugViewProvider;
private final DecoderInputBuffer decoderInputBuffer; private final DecoderInputBuffer decoderInputBuffer;
@ -44,9 +46,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
MuxerWrapper muxerWrapper, MuxerWrapper muxerWrapper,
TransformerMediaClock mediaClock, TransformerMediaClock mediaClock,
Transformation transformation, Transformation transformation,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory,
Transformer.DebugViewProvider debugViewProvider) { Transformer.DebugViewProvider debugViewProvider) {
super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformation); super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformation);
this.context = context; this.context = context;
this.encoderFactory = encoderFactory;
this.decoderFactory = decoderFactory;
this.debugViewProvider = debugViewProvider; this.debugViewProvider = debugViewProvider;
decoderInputBuffer = decoderInputBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
@ -72,7 +78,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
Format inputFormat = checkNotNull(formatHolder.format); Format inputFormat = checkNotNull(formatHolder.format);
if (shouldTranscode(inputFormat)) { if (shouldTranscode(inputFormat)) {
samplePipeline = samplePipeline =
new VideoSamplePipeline(context, inputFormat, transformation, debugViewProvider); new VideoSamplePipeline(
context,
inputFormat,
transformation,
encoderFactory,
decoderFactory,
debugViewProvider);
} else { } else {
samplePipeline = new PassthroughSamplePipeline(inputFormat); samplePipeline = new PassthroughSamplePipeline(inputFormat);
} }

View File

@ -27,7 +27,6 @@ import androidx.annotation.RequiresApi;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.decoder.DecoderInputBuffer;
import com.google.common.collect.ImmutableMap;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
@ -39,9 +38,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final int outputRotationDegrees; private final int outputRotationDegrees;
private final DecoderInputBuffer decoderInputBuffer; 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 final DecoderInputBuffer encoderOutputBuffer;
private @MonotonicNonNull FrameEditor frameEditor; private @MonotonicNonNull FrameEditor frameEditor;
@ -52,6 +51,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Context context, Context context,
Format inputFormat, Format inputFormat,
Transformation transformation, Transformation transformation,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory,
Transformer.DebugViewProvider debugViewProvider) Transformer.DebugViewProvider debugViewProvider)
throws TransformationException { throws TransformationException {
decoderInputBuffer = decoderInputBuffer =
@ -85,7 +86,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
transformation.transformationMatrix.postRotate(outputRotationDegrees); transformation.transformationMatrix.postRotate(outputRotationDegrees);
encoder = encoder =
MediaCodecAdapterWrapper.createForVideoEncoding( encoderFactory.createForVideoEncoding(
new Format.Builder() new Format.Builder()
.setWidth(outputWidth) .setWidth(outputWidth)
.setHeight(outputHeight) .setHeight(outputHeight)
@ -94,8 +95,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
transformation.videoMimeType != null transformation.videoMimeType != null
? transformation.videoMimeType ? transformation.videoMimeType
: inputFormat.sampleMimeType) : inputFormat.sampleMimeType)
.build(), .build());
ImmutableMap.of());
if (inputFormat.height != outputHeight if (inputFormat.height != outputHeight
|| inputFormat.width != outputWidth || inputFormat.width != outputWidth
|| !transformation.transformationMatrix.isIdentity()) { || !transformation.transformationMatrix.isIdentity()) {
@ -109,7 +109,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
debugViewProvider); debugViewProvider);
} }
decoder = decoder =
MediaCodecAdapterWrapper.createForVideoDecoding( decoderFactory.createForVideoDecoding(
inputFormat, inputFormat,
frameEditor == null frameEditor == null
? checkNotNull(encoder.getInputSurface()) ? checkNotNull(encoder.getInputSurface())