Remove ExoPlaybackException dependency from sample pipelines.

Use TransformationException for codec and audio processor
initialization problems instead.

PiperOrigin-RevId: 416765510
This commit is contained in:
hschlueter 2021-12-16 11:04:35 +00:00 committed by tonihei
parent 1633ad12a3
commit a8dbc744db
10 changed files with 292 additions and 204 deletions

View File

@ -25,13 +25,10 @@ import android.media.MediaCodec.BufferInfo;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.PlaybackException;
import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.audio.AudioProcessor;
import androidx.media3.exoplayer.audio.AudioProcessor.AudioFormat;
import androidx.media3.exoplayer.audio.SonicAudioProcessor;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -47,7 +44,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final Format inputFormat;
private final Transformation transformation;
private final int rendererIndex;
private final MediaCodecAdapterWrapper decoder;
private final DecoderInputBuffer decoderInputBuffer;
@ -67,11 +63,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private boolean drainingSonicForSpeedChange;
private float currentSpeed;
public AudioSamplePipeline(Format inputFormat, Transformation transformation, int rendererIndex)
throws ExoPlaybackException {
public AudioSamplePipeline(Format inputFormat, Transformation transformation)
throws TransformationException {
this.inputFormat = inputFormat;
this.transformation = transformation;
this.rendererIndex = rendererIndex;
decoderInputBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
encoderInputBuffer =
@ -82,19 +77,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER;
speedProvider = new SegmentSpeedProvider(inputFormat);
currentSpeed = speedProvider.getSpeed(0);
try {
this.decoder = MediaCodecAdapterWrapper.createForAudioDecoding(inputFormat);
} catch (IOException e) {
// TODO(internal b/192864511): Assign a specific error code.
throw ExoPlaybackException.createForRenderer(
e,
TAG,
rendererIndex,
inputFormat,
/* rendererFormatSupport= */ C.FORMAT_HANDLED,
/* isRecoverable= */ false,
PlaybackException.ERROR_CODE_UNSPECIFIED);
}
this.decoder = MediaCodecAdapterWrapper.createForAudioDecoding(inputFormat);
}
@Override
@ -109,7 +92,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
@Override
public boolean processData() throws ExoPlaybackException {
public boolean processData() throws TransformationException {
if (!ensureEncoderAndAudioProcessingConfigured()) {
return false;
}
@ -292,7 +275,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@EnsuresNonNullIf(
expression = {"encoder", "encoderInputAudioFormat"},
result = true)
private boolean ensureEncoderAndAudioProcessingConfigured() throws ExoPlaybackException {
private boolean ensureEncoderAndAudioProcessingConfigured() throws TransformationException {
if (encoder != null && encoderInputAudioFormat != null) {
return true;
}
@ -310,27 +293,24 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
outputAudioFormat = sonicAudioProcessor.configure(outputAudioFormat);
flushSonicAndSetSpeed(currentSpeed);
} catch (AudioProcessor.UnhandledAudioFormatException e) {
// TODO(internal b/192864511): Assign an adequate error code.
throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED);
throw TransformationException.createForAudioProcessor(
e,
"Sonic",
outputAudioFormat,
TransformationException.ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED);
}
}
String audioMimeType =
transformation.audioMimeType == null
? inputFormat.sampleMimeType
: transformation.audioMimeType;
try {
encoder =
MediaCodecAdapterWrapper.createForAudioEncoding(
new Format.Builder()
.setSampleMimeType(audioMimeType)
.setSampleRate(outputAudioFormat.sampleRate)
.setChannelCount(outputAudioFormat.channelCount)
.setAverageBitrate(DEFAULT_ENCODER_BITRATE)
.build());
} catch (IOException e) {
// TODO(internal b/192864511): Assign an adequate error code.
throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED);
}
encoder =
MediaCodecAdapterWrapper.createForAudioEncoding(
new Format.Builder()
.setSampleMimeType(
transformation.audioMimeType == null
? inputFormat.sampleMimeType
: transformation.audioMimeType)
.setSampleRate(outputAudioFormat.sampleRate)
.setChannelCount(outputAudioFormat.channelCount)
.setAverageBitrate(DEFAULT_ENCODER_BITRATE)
.build());
encoderInputAudioFormat = outputAudioFormat;
return true;
}
@ -351,17 +331,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sonicAudioProcessor.flush();
}
private ExoPlaybackException createRendererException(Throwable cause, int errorCode) {
return ExoPlaybackException.createForRenderer(
cause,
TAG,
rendererIndex,
inputFormat,
/* rendererFormatSupport= */ C.FORMAT_HANDLED,
/* isRecoverable= */ false,
errorCode);
}
private void computeNextEncoderInputBufferTimeUs(
long bytesWritten, int bytesPerFrame, int sampleRate) {
// The calculation below accounts for remainders and rounding. Without that it corresponds to

View File

@ -100,29 +100,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* @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 IOException If the underlying codec cannot be created.
* @throws TransformationException If the underlying codec cannot be created.
*/
public static MediaCodecAdapterWrapper createForAudioDecoding(Format format) throws IOException {
@Nullable MediaCodecAdapter adapter = null;
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 {
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);
adapter =
new Factory()
.createAdapter(
MediaCodecAdapter.Configuration.createForAudioDecoding(
createPlaceholderMediaCodecInfo(), mediaFormat, format, /* crypto= */ null));
return new MediaCodecAdapterWrapper(adapter);
} catch (Exception e) {
if (adapter != null) {
adapter.release();
}
throw e;
throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ true);
}
return new MediaCodecAdapterWrapper(adapter);
}
/**
@ -133,28 +132,26 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* 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 IOException If the underlying codec cannot be created.
* @throws TransformationException If the underlying codec cannot be created.
*/
@SuppressLint("InlinedApi")
public static MediaCodecAdapterWrapper createForVideoDecoding(Format format, Surface surface)
throws IOException {
@Nullable MediaCodecAdapter adapter = null;
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 {
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);
}
adapter =
new Factory()
.createAdapter(
@ -164,13 +161,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
format,
surface,
/* crypto= */ null));
return new MediaCodecAdapterWrapper(adapter);
} catch (Exception e) {
if (adapter != null) {
adapter.release();
}
throw e;
throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ true);
}
return new MediaCodecAdapterWrapper(adapter);
}
/**
@ -180,30 +174,26 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* @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 IOException If the underlying codec cannot be created.
* @throws TransformationException If the underlying codec cannot be created.
*/
public static MediaCodecAdapterWrapper createForAudioEncoding(Format format) throws IOException {
@Nullable MediaCodec encoder = null;
@Nullable MediaCodecAdapter adapter = null;
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 {
MediaFormat mediaFormat =
MediaFormat.createAudioFormat(
checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate);
adapter =
new Factory()
.createAdapter(
MediaCodecAdapter.Configuration.createForAudioEncoding(
createPlaceholderMediaCodecInfo(), mediaFormat, format));
return new MediaCodecAdapterWrapper(adapter);
} catch (Exception e) {
if (adapter != null) {
adapter.release();
} else if (encoder != null) {
encoder.release();
}
throw e;
throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ false);
}
return new MediaCodecAdapterWrapper(adapter);
}
/**
@ -219,41 +209,37 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* are from {@code MediaFormat.KEY_*} constants. Its values will override those in {@code
* format}.
* @return A configured and started encoder wrapper.
* @throws IOException If the underlying codec cannot be created.
* @throws TransformationException If the underlying codec cannot be created.
*/
public static MediaCodecAdapterWrapper createForVideoEncoding(
Format format, Map<String, Integer> additionalEncoderConfig) throws IOException {
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);
@Nullable MediaCodecAdapter adapter = null;
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 {
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());
}
adapter =
new Factory()
.createAdapter(
MediaCodecAdapter.Configuration.createForVideoEncoding(
createPlaceholderMediaCodecInfo(), mediaFormat, format));
return new MediaCodecAdapterWrapper(adapter);
} catch (Exception e) {
if (adapter != null) {
adapter.release();
}
throw e;
throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ false);
}
return new MediaCodecAdapterWrapper(adapter);
}
private MediaCodecAdapterWrapper(MediaCodecAdapter codec) {
@ -458,4 +444,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
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

@ -19,7 +19,6 @@ package androidx.media3.transformer;
import androidx.annotation.Nullable;
import androidx.media3.common.Format;
import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.ExoPlaybackException;
/**
* Pipeline for processing {@link DecoderInputBuffer DecoderInputBuffers}.
@ -44,7 +43,7 @@ import androidx.media3.exoplayer.ExoPlaybackException;
* Processes the input data and returns whether more data can be processed by calling this method
* again.
*/
boolean processData() throws ExoPlaybackException;
boolean processData() throws TransformationException;
/** Returns the output format of the pipeline if available, and {@code null} otherwise. */
@Nullable

View File

@ -24,9 +24,12 @@ import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.SystemClock;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.Format;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.audio.AudioProcessor;
import androidx.media3.exoplayer.audio.AudioProcessor.AudioFormat;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -36,6 +39,36 @@ import java.lang.annotation.Target;
@UnstableApi
public final class TransformationException extends Exception {
/**
* Creates an instance for a decoder or encoder related exception.
*
* @param cause The cause of the failure.
* @param componentName The name of the component used, e.g. 'VideoEncoder'.
* @param format The {@link Format} used for the decoder/encoder.
* @param errorCode See {@link #errorCode}.
* @return The created instance.
*/
public static TransformationException createForCodec(
Throwable cause, String componentName, Format format, int errorCode) {
return new TransformationException(
componentName + " error, format = " + format, cause, errorCode);
}
/**
* Creates an instance for an audio processing related exception.
*
* @param cause The cause of the failure.
* @param componentName The name of the {@link AudioProcessor} used.
* @param audioFormat The {@link AudioFormat} used.
* @param errorCode See {@link #errorCode}.
* @return The created instance.
*/
public static TransformationException createForAudioProcessor(
Throwable cause, String componentName, AudioFormat audioFormat, int errorCode) {
return new TransformationException(
componentName + " error, audio_format = " + audioFormat, cause, errorCode);
}
/**
* Creates an instance for an unexpected exception.
*
@ -74,6 +107,7 @@ public final class TransformationException extends Exception {
ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED,
ERROR_CODE_GL_INIT_FAILED,
ERROR_CODE_GL_PROCESSING_FAILED,
ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED,
})
public @interface ErrorCode {}
@ -106,13 +140,18 @@ public final class TransformationException extends Exception {
/** Caused by requesting to encode content in a format that is not supported by the device. */
public static final int ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED = 3003;
// GL errors (4xxx).
// Video editing errors (4xxx).
/** Caused by a GL initialization failure. */
public static final int ERROR_CODE_GL_INIT_FAILED = 4001;
/** Caused by a failure while using or releasing a GL program. */
public static final int ERROR_CODE_GL_PROCESSING_FAILED = 4002;
// Audio editing errors (5xxx).
/** Caused by an audio processor initialization failure. */
public static final int ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED = 5001;
/** Returns the name of a given {@code errorCode}. */
public static String getErrorCodeName(@ErrorCode int errorCode) {
switch (errorCode) {
@ -136,6 +175,8 @@ public final class TransformationException extends Exception {
return "ERROR_CODE_GL_INIT_FAILED";
case ERROR_CODE_GL_PROCESSING_FAILED:
return "ERROR_CODE_GL_PROCESSING_FAILED";
case ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED:
return "ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED";
default:
return "invalid error code";
}

View File

@ -831,29 +831,33 @@ public final class Transformer {
@Override
public void onTracksInfoChanged(TracksInfo tracksInfo) {
if (muxerWrapper.getTrackCount() == 0) {
// TODO(b/209469847): Do not silently drop unsupported tracks and throw a more specific
// exception earlier.
handleTransformationEnded(
new IllegalStateException(
"The output does not contain any tracks. Check that at least one of the input"
+ " sample formats is supported."));
TransformationException.createForUnexpected(
new IllegalStateException(
"The output does not contain any tracks. Check that at least one of the input"
+ " sample formats is supported.")));
}
}
@Override
public void onPlayerError(PlaybackException error) {
// TODO(internal b/209469847): Once TransformationException is used in transformer components,
// extract TransformationExceptions wrapped in the PlaybackExceptions here before passing them
// on.
handleTransformationEnded(error);
Throwable cause = error.getCause();
handleTransformationEnded(
cause instanceof TransformationException
? (TransformationException) cause
: TransformationException.createForUnexpected(error));
}
private void handleTransformationEnded(@Nullable Exception exception) {
@Nullable Exception resourceReleaseException = null;
private void handleTransformationEnded(@Nullable TransformationException exception) {
@Nullable TransformationException resourceReleaseException = null;
try {
releaseResources(/* forCancellation= */ false);
} catch (IllegalStateException e) {
// TODO(internal b/209469847): Use a TransformationException with a specific error code when
// the IllegalStateException is caused by the muxer.
resourceReleaseException = e;
// TODO(internal b/209469847): Use a more specific error code when the IllegalStateException
// is caused by the muxer.
resourceReleaseException = TransformationException.createForUnexpected(e);
}
if (exception == null && resourceReleaseException == null) {
@ -862,15 +866,10 @@ public final class Transformer {
}
if (exception != null) {
listener.onTransformationError(
mediaItem,
exception instanceof TransformationException
? exception
: TransformationException.createForUnexpected(exception));
listener.onTransformationError(mediaItem, exception);
}
if (resourceReleaseException != null) {
listener.onTransformationError(
mediaItem, TransformationException.createForUnexpected(resourceReleaseException));
listener.onTransformationError(mediaItem, resourceReleaseException);
}
}
}

View File

@ -24,7 +24,6 @@ import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.Metadata;
import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.FormatHolder;
import androidx.media3.exoplayer.source.SampleStream.ReadDataResult;
import androidx.media3.extractor.metadata.mp4.SlowMotionData;
@ -49,7 +48,7 @@ import androidx.media3.extractor.metadata.mp4.SlowMotionData;
/** Attempts to read the input format and to initialize the {@link SamplePipeline}. */
@Override
protected boolean ensureConfigured() throws ExoPlaybackException {
protected boolean ensureConfigured() throws TransformationException {
if (samplePipeline != null) {
return true;
}
@ -61,7 +60,7 @@ import androidx.media3.extractor.metadata.mp4.SlowMotionData;
}
Format inputFormat = checkNotNull(formatHolder.format);
if (shouldTranscode(inputFormat)) {
samplePipeline = new AudioSamplePipeline(inputFormat, transformation, getIndex());
samplePipeline = new AudioSamplePipeline(inputFormat, transformation);
} else {
samplePipeline = new PassthroughSamplePipeline(inputFormat);
}

View File

@ -22,6 +22,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.BaseRenderer;
import androidx.media3.exoplayer.ExoPlaybackException;
@ -95,11 +96,24 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Override
public final void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (!isRendererStarted || isEnded() || !ensureConfigured()) {
return;
}
try {
if (!isRendererStarted || isEnded() || !ensureConfigured()) {
return;
}
while (feedMuxerFromPipeline() || samplePipeline.processData() || feedPipelineFromInput()) {}
while (feedMuxerFromPipeline() || samplePipeline.processData() || feedPipelineFromInput()) {}
} catch (TransformationException e) {
// Transformer extracts the TransformationException from this ExoPlaybackException again. This
// temporary wrapping is needed due to the dependence on ExoPlayer's BaseRenderer.
throw ExoPlaybackException.createForRenderer(
e,
"Transformer",
getIndex(),
/* rendererFormat= */ null,
C.FORMAT_HANDLED,
/* isRecoverable= */ false,
PlaybackException.ERROR_CODE_UNSPECIFIED);
}
}
@Override
@ -134,7 +148,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@ForOverride
@EnsuresNonNullIf(expression = "samplePipeline", result = true)
protected abstract boolean ensureConfigured() throws ExoPlaybackException;
protected abstract boolean ensureConfigured() throws TransformationException;
@RequiresNonNull({"samplePipeline", "#1.data"})
protected void maybeQueueSampleToPipeline(DecoderInputBuffer inputBuffer) {

View File

@ -23,7 +23,6 @@ import android.content.Context;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.FormatHolder;
import androidx.media3.exoplayer.source.SampleStream.ReadDataResult;
import java.nio.ByteBuffer;
@ -60,7 +59,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** Attempts to read the input format and to initialize the {@link SamplePipeline}. */
@Override
protected boolean ensureConfigured() throws ExoPlaybackException {
protected boolean ensureConfigured() throws TransformationException {
if (samplePipeline != null) {
return true;
}
@ -73,8 +72,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
Format inputFormat = checkNotNull(formatHolder.format);
if (shouldTranscode(inputFormat)) {
samplePipeline =
new VideoSamplePipeline(
context, inputFormat, transformation, getIndex(), debugViewProvider);
new VideoSamplePipeline(context, inputFormat, transformation, debugViewProvider);
} else {
samplePipeline = new PassthroughSamplePipeline(inputFormat);
}

View File

@ -26,11 +26,8 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.PlaybackException;
import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.ExoPlaybackException;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
@ -55,9 +52,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Context context,
Format inputFormat,
Transformation transformation,
int rendererIndex,
Transformer.DebugViewProvider debugViewProvider)
throws ExoPlaybackException {
throws TransformationException {
decoderInputBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
encoderOutputBuffer =
@ -88,24 +84,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// postrotation in a later vertex shader.
transformation.transformationMatrix.postRotate(outputRotationDegrees);
try {
encoder =
MediaCodecAdapterWrapper.createForVideoEncoding(
new Format.Builder()
.setWidth(outputWidth)
.setHeight(outputHeight)
.setRotationDegrees(0)
.setSampleMimeType(
transformation.videoMimeType != null
? transformation.videoMimeType
: inputFormat.sampleMimeType)
.build(),
ImmutableMap.of());
} catch (IOException e) {
// TODO(internal b/192864511): Assign a specific error code.
throw createRendererException(
e, rendererIndex, inputFormat, PlaybackException.ERROR_CODE_UNSPECIFIED);
}
encoder =
MediaCodecAdapterWrapper.createForVideoEncoding(
new Format.Builder()
.setWidth(outputWidth)
.setHeight(outputHeight)
.setRotationDegrees(0)
.setSampleMimeType(
transformation.videoMimeType != null
? transformation.videoMimeType
: inputFormat.sampleMimeType)
.build(),
ImmutableMap.of());
if (inputFormat.height != outputHeight
|| inputFormat.width != outputWidth
|| !transformation.transformationMatrix.isIdentity()) {
@ -118,17 +108,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* outputSurface= */ checkNotNull(encoder.getInputSurface()),
debugViewProvider);
}
try {
decoder =
MediaCodecAdapterWrapper.createForVideoDecoding(
inputFormat,
frameEditor == null
? checkNotNull(encoder.getInputSurface())
: frameEditor.getInputSurface());
} catch (IOException e) {
throw createRendererException(
e, rendererIndex, inputFormat, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED);
}
decoder =
MediaCodecAdapterWrapper.createForVideoDecoding(
inputFormat,
frameEditor == null
? checkNotNull(encoder.getInputSurface())
: frameEditor.getInputSurface());
}
@Override
@ -257,16 +242,4 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
decoder.release();
encoder.release();
}
private static ExoPlaybackException createRendererException(
Throwable cause, int rendererIndex, Format inputFormat, int errorCode) {
return ExoPlaybackException.createForRenderer(
cause,
TAG,
rendererIndex,
inputFormat,
/* rendererFormatSupport= */ C.FORMAT_HANDLED,
/* isRecoverable= */ false,
errorCode);
}
}

View File

@ -24,11 +24,15 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.content.Context;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util;
@ -39,6 +43,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.Iterables;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
@ -232,6 +237,63 @@ public final class TransformerTest {
DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_WITH_SEF_SLOW_MOTION));
}
@Test
public void startTransformation_withAudioEncoderFormatUnsupported_completesWithError()
throws Exception {
Transformer transformer =
new Transformer.Builder(context)
.setClock(clock)
.setMuxerFactory(new TestMuxerFactory())
.setAudioMimeType(MimeTypes.AUDIO_AMR_WB) // unsupported encoder MIME type
.build();
MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_ONLY);
transformer.startTransformation(mediaItem, outputPath);
TransformationException exception = TransformerTestRunner.runUntilError(transformer);
assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
assertThat(exception.errorCode)
.isEqualTo(TransformationException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED);
}
@Test
public void startTransformation_withAudioDecoderFormatUnsupported_completesWithError()
throws Exception {
Transformer transformer =
new Transformer.Builder(context)
.setClock(clock)
.setMuxerFactory(new TestMuxerFactory())
.setAudioMimeType(MimeTypes.AUDIO_AAC) // supported encoder MIME type
.build();
MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_ALL_SAMPLE_FORMATS_UNSUPPORTED);
transformer.startTransformation(mediaItem, outputPath);
TransformationException exception = TransformerTestRunner.runUntilError(transformer);
assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
assertThat(exception.errorCode)
.isEqualTo(TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED);
}
@Test
public void startTransformation_withVideoEncoderFormatUnsupported_completesWithError()
throws Exception {
Transformer transformer =
new Transformer.Builder(context)
.setClock(clock)
.setMuxerFactory(new TestMuxerFactory())
.setVideoMimeType(MimeTypes.VIDEO_H263) // unsupported encoder MIME type
.build();
MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY);
transformer.startTransformation(mediaItem, outputPath);
TransformationException exception = TransformerTestRunner.runUntilError(transformer);
assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
assertThat(exception.errorCode)
.isEqualTo(TransformationException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED);
}
@Test
public void startTransformation_withPlayerError_completesWithError() throws Exception {
Transformer transformer = new Transformer.Builder(context).setClock(clock).build();
@ -541,6 +603,30 @@ public final class TransformerTest {
ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AAC, codecConfig);
ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AMR_NB, codecConfig);
ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AAC, codecConfig);
ShadowMediaCodec.CodecConfig throwingCodecConfig =
new ShadowMediaCodec.CodecConfig(
/* inputBufferSize= */ 10_000,
/* outputBufferSize= */ 10_000,
new ShadowMediaCodec.CodecConfig.Codec() {
@Override
public void process(ByteBuffer in, ByteBuffer out) {
out.put(in);
}
@Override
public void onConfigured(
MediaFormat format,
@Nullable Surface surface,
@Nullable MediaCrypto crypto,
int flags) {
throw new IllegalArgumentException("Format unsupported");
}
});
ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AC3, throwingCodecConfig);
ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AMR_WB, throwingCodecConfig);
ShadowMediaCodec.addEncoder(MimeTypes.VIDEO_H263, throwingCodecConfig);
}
private static void removeEncodersAndDecoders() {