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 = encoder =
transformation.audioMimeType == null MediaCodecAdapterWrapper.createForAudioEncoding(
? inputFormat.sampleMimeType new Format.Builder()
: transformation.audioMimeType; .setSampleMimeType(
try { transformation.audioMimeType == null
encoder = ? inputFormat.sampleMimeType
MediaCodecAdapterWrapper.createForAudioEncoding( : transformation.audioMimeType)
new Format.Builder() .setSampleRate(outputAudioFormat.sampleRate)
.setSampleMimeType(audioMimeType) .setChannelCount(outputAudioFormat.channelCount)
.setSampleRate(outputAudioFormat.sampleRate) .setAverageBitrate(DEFAULT_ENCODER_BITRATE)
.setChannelCount(outputAudioFormat.channelCount) .build());
.setAverageBitrate(DEFAULT_ENCODER_BITRATE)
.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 {
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 { 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 = 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; 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 { 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 = 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; MediaFormat mediaFormat =
MediaFormat.createAudioFormat(
checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate);
MediaCodecAdapter adapter;
try { try {
MediaFormat mediaFormat =
MediaFormat.createAudioFormat(
checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate);
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,41 +209,37 @@ 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; 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 { 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 = 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(
new IllegalStateException( TransformationException.createForUnexpected(
"The output does not contain any tracks. Check that at least one of the input" new IllegalStateException(
+ " sample formats is supported.")); "The output does not contain any tracks. Check that at least one of the input"
+ " 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 {
if (!isRendererStarted || isEnded() || !ensureConfigured()) { try {
return; 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 @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,24 +84,18 @@ 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() .setWidth(outputWidth)
.setWidth(outputWidth) .setHeight(outputHeight)
.setHeight(outputHeight) .setRotationDegrees(0)
.setRotationDegrees(0) .setSampleMimeType(
.setSampleMimeType( transformation.videoMimeType != null
transformation.videoMimeType != null ? transformation.videoMimeType
? transformation.videoMimeType : 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() {