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.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.PlaybackException;
import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.audio.AudioProcessor; import androidx.media3.exoplayer.audio.AudioProcessor;
import androidx.media3.exoplayer.audio.AudioProcessor.AudioFormat; import androidx.media3.exoplayer.audio.AudioProcessor.AudioFormat;
import androidx.media3.exoplayer.audio.SonicAudioProcessor; import androidx.media3.exoplayer.audio.SonicAudioProcessor;
import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -47,7 +44,6 @@ 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 int rendererIndex;
private final MediaCodecAdapterWrapper decoder; private final MediaCodecAdapterWrapper decoder;
private final DecoderInputBuffer decoderInputBuffer; private final DecoderInputBuffer decoderInputBuffer;
@ -67,11 +63,10 @@ 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, int rendererIndex) public AudioSamplePipeline(Format inputFormat, Transformation transformation)
throws ExoPlaybackException { throws TransformationException {
this.inputFormat = inputFormat; this.inputFormat = inputFormat;
this.transformation = transformation; this.transformation = transformation;
this.rendererIndex = rendererIndex;
decoderInputBuffer = decoderInputBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
encoderInputBuffer = encoderInputBuffer =
@ -82,19 +77,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);
try {
this.decoder = MediaCodecAdapterWrapper.createForAudioDecoding(inputFormat); 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);
}
} }
@Override @Override
@ -109,7 +92,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
@Override @Override
public boolean processData() throws ExoPlaybackException { public boolean processData() throws TransformationException {
if (!ensureEncoderAndAudioProcessingConfigured()) { if (!ensureEncoderAndAudioProcessingConfigured()) {
return false; return false;
} }
@ -292,7 +275,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@EnsuresNonNullIf( @EnsuresNonNullIf(
expression = {"encoder", "encoderInputAudioFormat"}, expression = {"encoder", "encoderInputAudioFormat"},
result = true) result = true)
private boolean ensureEncoderAndAudioProcessingConfigured() throws ExoPlaybackException { private boolean ensureEncoderAndAudioProcessingConfigured() throws TransformationException {
if (encoder != null && encoderInputAudioFormat != null) { if (encoder != null && encoderInputAudioFormat != null) {
return true; return true;
} }
@ -310,27 +293,24 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
outputAudioFormat = sonicAudioProcessor.configure(outputAudioFormat); outputAudioFormat = sonicAudioProcessor.configure(outputAudioFormat);
flushSonicAndSetSpeed(currentSpeed); flushSonicAndSetSpeed(currentSpeed);
} catch (AudioProcessor.UnhandledAudioFormatException e) { } catch (AudioProcessor.UnhandledAudioFormatException e) {
// TODO(internal b/192864511): Assign an adequate error code. throw TransformationException.createForAudioProcessor(
throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED); e,
"Sonic",
outputAudioFormat,
TransformationException.ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED);
} }
} }
String audioMimeType =
transformation.audioMimeType == null
? inputFormat.sampleMimeType
: transformation.audioMimeType;
try {
encoder = encoder =
MediaCodecAdapterWrapper.createForAudioEncoding( MediaCodecAdapterWrapper.createForAudioEncoding(
new Format.Builder() new Format.Builder()
.setSampleMimeType(audioMimeType) .setSampleMimeType(
transformation.audioMimeType == null
? inputFormat.sampleMimeType
: transformation.audioMimeType)
.setSampleRate(outputAudioFormat.sampleRate) .setSampleRate(outputAudioFormat.sampleRate)
.setChannelCount(outputAudioFormat.channelCount) .setChannelCount(outputAudioFormat.channelCount)
.setAverageBitrate(DEFAULT_ENCODER_BITRATE) .setAverageBitrate(DEFAULT_ENCODER_BITRATE)
.build()); .build());
} catch (IOException e) {
// TODO(internal b/192864511): Assign an adequate error code.
throw createRendererException(e, PlaybackException.ERROR_CODE_UNSPECIFIED);
}
encoderInputAudioFormat = outputAudioFormat; encoderInputAudioFormat = outputAudioFormat;
return true; return true;
} }
@ -351,17 +331,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sonicAudioProcessor.flush(); 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( private void computeNextEncoderInputBufferTimeUs(
long bytesWritten, int bytesPerFrame, int sampleRate) { long bytesWritten, int bytesPerFrame, int sampleRate) {
// The calculation below accounts for remainders and rounding. Without that it corresponds to // 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 * @param format The {@link Format} (of the input data) used to determine the underlying {@link
* MediaCodec} and its configuration values. * MediaCodec} and its configuration values.
* @return A configured and started decoder wrapper. * @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 { public static MediaCodecAdapterWrapper createForAudioDecoding(Format format)
@Nullable MediaCodecAdapter adapter = null; throws TransformationException {
try {
MediaFormat mediaFormat = MediaFormat mediaFormat =
MediaFormat.createAudioFormat( MediaFormat.createAudioFormat(
checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount);
MediaFormatUtil.maybeSetInteger( MediaFormatUtil.maybeSetInteger(
mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize);
MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
MediaCodecAdapter adapter;
try {
adapter = adapter =
new Factory() new Factory()
.createAdapter( .createAdapter(
MediaCodecAdapter.Configuration.createForAudioDecoding( MediaCodecAdapter.Configuration.createForAudioDecoding(
createPlaceholderMediaCodecInfo(), mediaFormat, format, /* crypto= */ null)); createPlaceholderMediaCodecInfo(), mediaFormat, format, /* crypto= */ null));
return new MediaCodecAdapterWrapper(adapter);
} catch (Exception e) { } catch (Exception e) {
if (adapter != null) { throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ true);
adapter.release();
}
throw e;
} }
return new MediaCodecAdapterWrapper(adapter);
} }
/** /**
@ -133,28 +132,26 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* MediaCodec} and its configuration values. * MediaCodec} and its configuration values.
* @param surface The {@link Surface} to which the decoder output is rendered. * @param surface The {@link Surface} to which the decoder output is rendered.
* @return A configured and started decoder wrapper. * @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") @SuppressLint("InlinedApi")
public static MediaCodecAdapterWrapper createForVideoDecoding(Format format, Surface surface) public static MediaCodecAdapterWrapper createForVideoDecoding(Format format, Surface surface)
throws IOException { throws TransformationException {
@Nullable MediaCodecAdapter adapter = null;
try {
MediaFormat mediaFormat = MediaFormat mediaFormat =
MediaFormat.createVideoFormat( MediaFormat.createVideoFormat(
checkNotNull(format.sampleMimeType), format.width, format.height); checkNotNull(format.sampleMimeType), format.width, format.height);
MediaFormatUtil.maybeSetInteger( MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees);
mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees);
MediaFormatUtil.maybeSetInteger( MediaFormatUtil.maybeSetInteger(
mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize);
MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
if (SDK_INT >= 29) { if (SDK_INT >= 29) {
// On API levels over 29, Transformer decodes as many frames as possible in one render // 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. // cycle. This key ensures no frame dropping when the decoder's output surface is full.
mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0); mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0);
} }
MediaCodecAdapter adapter;
try {
adapter = adapter =
new Factory() new Factory()
.createAdapter( .createAdapter(
@ -164,13 +161,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
format, format,
surface, surface,
/* crypto= */ null)); /* crypto= */ null));
return new MediaCodecAdapterWrapper(adapter);
} catch (Exception e) { } catch (Exception e) {
if (adapter != null) { throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ true);
adapter.release();
}
throw e;
} }
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 * @param format The {@link Format} (of the output data) used to determine the underlying {@link
* MediaCodec} and its configuration values. * MediaCodec} and its configuration values.
* @return A configured and started encoder wrapper. * @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 { public static MediaCodecAdapterWrapper createForAudioEncoding(Format format)
@Nullable MediaCodec encoder = null; throws TransformationException {
@Nullable MediaCodecAdapter adapter = null;
try {
MediaFormat mediaFormat = MediaFormat mediaFormat =
MediaFormat.createAudioFormat( MediaFormat.createAudioFormat(
checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate); mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate);
MediaCodecAdapter adapter;
try {
adapter = adapter =
new Factory() new Factory()
.createAdapter( .createAdapter(
MediaCodecAdapter.Configuration.createForAudioEncoding( MediaCodecAdapter.Configuration.createForAudioEncoding(
createPlaceholderMediaCodecInfo(), mediaFormat, format)); createPlaceholderMediaCodecInfo(), mediaFormat, format));
return new MediaCodecAdapterWrapper(adapter);
} catch (Exception e) { } catch (Exception e) {
if (adapter != null) { throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ false);
adapter.release();
} else if (encoder != null) {
encoder.release();
}
throw e;
} }
return new MediaCodecAdapterWrapper(adapter);
} }
/** /**
@ -219,17 +209,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* are from {@code MediaFormat.KEY_*} constants. Its values will override those in {@code * are from {@code MediaFormat.KEY_*} constants. Its values will override those in {@code
* format}. * format}.
* @return A configured and started encoder wrapper. * @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( 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.width != Format.NO_VALUE);
checkArgument(format.height != Format.NO_VALUE); checkArgument(format.height != Format.NO_VALUE);
checkArgument(format.height < format.width); checkArgument(format.height < format.width);
checkArgument(format.rotationDegrees == 0); checkArgument(format.rotationDegrees == 0);
@Nullable MediaCodecAdapter adapter = null;
try {
MediaFormat mediaFormat = MediaFormat mediaFormat =
MediaFormat.createVideoFormat( MediaFormat.createVideoFormat(
checkNotNull(format.sampleMimeType), format.width, format.height); checkNotNull(format.sampleMimeType), format.width, format.height);
@ -237,23 +225,21 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30); mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 413_000); mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 413_000);
for (Map.Entry<String, Integer> encoderSetting : additionalEncoderConfig.entrySet()) { for (Map.Entry<String, Integer> encoderSetting : additionalEncoderConfig.entrySet()) {
mediaFormat.setInteger(encoderSetting.getKey(), encoderSetting.getValue()); mediaFormat.setInteger(encoderSetting.getKey(), encoderSetting.getValue());
} }
MediaCodecAdapter adapter;
try {
adapter = adapter =
new Factory() new Factory()
.createAdapter( .createAdapter(
MediaCodecAdapter.Configuration.createForVideoEncoding( MediaCodecAdapter.Configuration.createForVideoEncoding(
createPlaceholderMediaCodecInfo(), mediaFormat, format)); createPlaceholderMediaCodecInfo(), mediaFormat, format));
return new MediaCodecAdapterWrapper(adapter);
} catch (Exception e) { } catch (Exception e) {
if (adapter != null) { throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ false);
adapter.release();
}
throw e;
} }
return new MediaCodecAdapterWrapper(adapter);
} }
private MediaCodecAdapterWrapper(MediaCodecAdapter codec) { private MediaCodecAdapterWrapper(MediaCodecAdapter codec) {
@ -458,4 +444,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
return formatBuilder.build(); 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.annotation.Nullable;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.ExoPlaybackException;
/** /**
* Pipeline for processing {@link DecoderInputBuffer DecoderInputBuffers}. * 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 * Processes the input data and returns whether more data can be processed by calling this method
* again. * again.
*/ */
boolean processData() throws ExoPlaybackException; boolean processData() throws TransformationException;
/** Returns the output format of the pipeline if available, and {@code null} otherwise. */ /** Returns the output format of the pipeline if available, and {@code null} otherwise. */
@Nullable @Nullable

View File

@ -24,9 +24,12 @@ import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.SystemClock; import android.os.SystemClock;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.Format;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; 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.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@ -36,6 +39,36 @@ import java.lang.annotation.Target;
@UnstableApi @UnstableApi
public final class TransformationException extends Exception { 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. * Creates an instance for an unexpected exception.
* *
@ -74,6 +107,7 @@ public final class TransformationException extends Exception {
ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED, ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED,
ERROR_CODE_GL_INIT_FAILED, ERROR_CODE_GL_INIT_FAILED,
ERROR_CODE_GL_PROCESSING_FAILED, ERROR_CODE_GL_PROCESSING_FAILED,
ERROR_CODE_AUDIO_PROCESSOR_INIT_FAILED,
}) })
public @interface ErrorCode {} 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. */ /** 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; public static final int ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED = 3003;
// GL errors (4xxx). // Video editing errors (4xxx).
/** Caused by a GL initialization failure. */ /** Caused by a GL initialization failure. */
public static final int ERROR_CODE_GL_INIT_FAILED = 4001; public static final int ERROR_CODE_GL_INIT_FAILED = 4001;
/** Caused by a failure while using or releasing a GL program. */ /** Caused by a failure while using or releasing a GL program. */
public static final int ERROR_CODE_GL_PROCESSING_FAILED = 4002; 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}. */ /** Returns the name of a given {@code errorCode}. */
public static String getErrorCodeName(@ErrorCode int errorCode) { public static String getErrorCodeName(@ErrorCode int errorCode) {
switch (errorCode) { switch (errorCode) {
@ -136,6 +175,8 @@ public final class TransformationException extends Exception {
return "ERROR_CODE_GL_INIT_FAILED"; return "ERROR_CODE_GL_INIT_FAILED";
case ERROR_CODE_GL_PROCESSING_FAILED: case ERROR_CODE_GL_PROCESSING_FAILED:
return "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: default:
return "invalid error code"; return "invalid error code";
} }

View File

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

View File

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

View File

@ -22,6 +22,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.BaseRenderer; import androidx.media3.exoplayer.BaseRenderer;
import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.ExoPlaybackException;
@ -95,11 +96,24 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Override @Override
public final void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { public final void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
try {
if (!isRendererStarted || isEnded() || !ensureConfigured()) { if (!isRendererStarted || isEnded() || !ensureConfigured()) {
return; 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 @Override
@ -134,7 +148,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@ForOverride @ForOverride
@EnsuresNonNullIf(expression = "samplePipeline", result = true) @EnsuresNonNullIf(expression = "samplePipeline", result = true)
protected abstract boolean ensureConfigured() throws ExoPlaybackException; protected abstract boolean ensureConfigured() throws TransformationException;
@RequiresNonNull({"samplePipeline", "#1.data"}) @RequiresNonNull({"samplePipeline", "#1.data"})
protected void maybeQueueSampleToPipeline(DecoderInputBuffer inputBuffer) { protected void maybeQueueSampleToPipeline(DecoderInputBuffer inputBuffer) {

View File

@ -23,7 +23,6 @@ import android.content.Context;
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 androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.FormatHolder; import androidx.media3.exoplayer.FormatHolder;
import androidx.media3.exoplayer.source.SampleStream.ReadDataResult; import androidx.media3.exoplayer.source.SampleStream.ReadDataResult;
import java.nio.ByteBuffer; 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}. */ /** Attempts to read the input format and to initialize the {@link SamplePipeline}. */
@Override @Override
protected boolean ensureConfigured() throws ExoPlaybackException { protected boolean ensureConfigured() throws TransformationException {
if (samplePipeline != null) { if (samplePipeline != null) {
return true; return true;
} }
@ -73,8 +72,7 @@ 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( new VideoSamplePipeline(context, inputFormat, transformation, debugViewProvider);
context, inputFormat, transformation, getIndex(), debugViewProvider);
} else { } else {
samplePipeline = new PassthroughSamplePipeline(inputFormat); samplePipeline = new PassthroughSamplePipeline(inputFormat);
} }

View File

@ -26,11 +26,8 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; 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.common.PlaybackException;
import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.ExoPlaybackException;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
@ -55,9 +52,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Context context, Context context,
Format inputFormat, Format inputFormat,
Transformation transformation, Transformation transformation,
int rendererIndex,
Transformer.DebugViewProvider debugViewProvider) Transformer.DebugViewProvider debugViewProvider)
throws ExoPlaybackException { throws TransformationException {
decoderInputBuffer = decoderInputBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
encoderOutputBuffer = encoderOutputBuffer =
@ -88,7 +84,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// postrotation in a later vertex shader. // postrotation in a later vertex shader.
transformation.transformationMatrix.postRotate(outputRotationDegrees); transformation.transformationMatrix.postRotate(outputRotationDegrees);
try {
encoder = encoder =
MediaCodecAdapterWrapper.createForVideoEncoding( MediaCodecAdapterWrapper.createForVideoEncoding(
new Format.Builder() new Format.Builder()
@ -101,11 +96,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
: inputFormat.sampleMimeType) : inputFormat.sampleMimeType)
.build(), .build(),
ImmutableMap.of()); ImmutableMap.of());
} catch (IOException e) {
// TODO(internal b/192864511): Assign a specific error code.
throw createRendererException(
e, rendererIndex, inputFormat, PlaybackException.ERROR_CODE_UNSPECIFIED);
}
if (inputFormat.height != outputHeight if (inputFormat.height != outputHeight
|| inputFormat.width != outputWidth || inputFormat.width != outputWidth
|| !transformation.transformationMatrix.isIdentity()) { || !transformation.transformationMatrix.isIdentity()) {
@ -118,17 +108,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* outputSurface= */ checkNotNull(encoder.getInputSurface()), /* outputSurface= */ checkNotNull(encoder.getInputSurface()),
debugViewProvider); debugViewProvider);
} }
try {
decoder = decoder =
MediaCodecAdapterWrapper.createForVideoDecoding( MediaCodecAdapterWrapper.createForVideoDecoding(
inputFormat, inputFormat,
frameEditor == null frameEditor == null
? checkNotNull(encoder.getInputSurface()) ? checkNotNull(encoder.getInputSurface())
: frameEditor.getInputSurface()); : frameEditor.getInputSurface());
} catch (IOException e) {
throw createRendererException(
e, rendererIndex, inputFormat, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED);
}
} }
@Override @Override
@ -257,16 +242,4 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
decoder.release(); decoder.release();
encoder.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 static org.junit.Assert.assertThrows;
import android.content.Context; import android.content.Context;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
@ -39,6 +43,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
@ -232,6 +237,63 @@ public final class TransformerTest {
DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_WITH_SEF_SLOW_MOTION)); 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 @Test
public void startTransformation_withPlayerError_completesWithError() throws Exception { public void startTransformation_withPlayerError_completesWithError() throws Exception {
Transformer transformer = new Transformer.Builder(context).setClock(clock).build(); 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_AAC, codecConfig);
ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AMR_NB, codecConfig); ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AMR_NB, codecConfig);
ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AAC, 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() { private static void removeEncodersAndDecoders() {