diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioSamplePipeline.java index 22a6bd7d75..82633e7da4 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AudioSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AudioSamplePipeline.java @@ -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 diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/MediaCodecAdapterWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/MediaCodecAdapterWrapper.java index eab805c806..9a5ac58e39 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/MediaCodecAdapterWrapper.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/MediaCodecAdapterWrapper.java @@ -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 additionalEncoderConfig) throws IOException { + Format format, Map additionalEncoderConfig) throws TransformationException { checkArgument(format.width != Format.NO_VALUE); checkArgument(format.height != Format.NO_VALUE); checkArgument(format.height < format.width); checkArgument(format.rotationDegrees == 0); - @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 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 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); + } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SamplePipeline.java index f647320884..d8c016e50d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SamplePipeline.java @@ -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 diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java index 7b0122f792..4a0eee76cc 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java @@ -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"; } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index 6608d19c3e..0e7b3c1852 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -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); } } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerAudioRenderer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerAudioRenderer.java index c1d0f2f1ed..3819659d3b 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerAudioRenderer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerAudioRenderer.java @@ -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); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerBaseRenderer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerBaseRenderer.java index 29bdce9048..85a8eab957 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerBaseRenderer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerBaseRenderer.java @@ -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) { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java index e2e5d2fcf7..dee7d7f048 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java @@ -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); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoSamplePipeline.java index 52705612ce..f8bf864e57 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoSamplePipeline.java @@ -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); - } } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerTest.java index 6d95dd7a9e..4887a3a2d2 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerTest.java @@ -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() {