Commit 8bb53b40 by hschlueter Committed by Ian Baker

Remove ExoPlaybackException dependency from sample pipelines.

Use TransformationException for codec and audio processor
initialization problems instead.

PiperOrigin-RevId: 416765510
parent 66adeabb
...@@ -24,14 +24,11 @@ import static java.lang.Math.min; ...@@ -24,14 +24,11 @@ import static java.lang.Math.min;
import android.media.MediaCodec.BufferInfo; import android.media.MediaCodec.BufferInfo;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat; import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat;
import com.google.android.exoplayer2.audio.SonicAudioProcessor; import com.google.android.exoplayer2.audio.SonicAudioProcessor;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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
......
...@@ -100,29 +100,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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);
}
} }
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
package com.google.android.exoplayer2.transformer; package com.google.android.exoplayer2.transformer;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
...@@ -44,7 +43,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; ...@@ -44,7 +43,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
* 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
......
...@@ -24,6 +24,9 @@ import static java.lang.annotation.ElementType.TYPE_USE; ...@@ -24,6 +24,9 @@ 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 com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat;
import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
...@@ -35,6 +38,36 @@ import java.lang.annotation.Target; ...@@ -35,6 +38,36 @@ import java.lang.annotation.Target;
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.
* *
* <p>If the exception is a runtime exception, error code {@link ERROR_CODE_FAILED_RUNTIME_CHECK} * <p>If the exception is a runtime exception, error code {@link ERROR_CODE_FAILED_RUNTIME_CHECK}
...@@ -72,6 +105,7 @@ public final class TransformationException extends Exception { ...@@ -72,6 +105,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 {}
...@@ -104,13 +138,18 @@ public final class TransformationException extends Exception { ...@@ -104,13 +138,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) {
...@@ -134,6 +173,8 @@ public final class TransformationException extends Exception { ...@@ -134,6 +173,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";
} }
......
...@@ -829,29 +829,33 @@ public final class Transformer { ...@@ -829,29 +829,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) {
...@@ -860,15 +864,10 @@ public final class Transformer { ...@@ -860,15 +864,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));
} }
} }
} }
......
...@@ -21,7 +21,6 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull; ...@@ -21,7 +21,6 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
...@@ -49,7 +48,7 @@ import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; ...@@ -49,7 +48,7 @@ import com.google.android.exoplayer2.source.SampleStream.ReadDataResult;
/** 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 com.google.android.exoplayer2.source.SampleStream.ReadDataResult; ...@@ -61,7 +60,7 @@ import com.google.android.exoplayer2.source.SampleStream.ReadDataResult;
} }
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);
} }
......
...@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.BaseRenderer; ...@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; import com.google.android.exoplayer2.source.SampleStream.ReadDataResult;
...@@ -95,11 +96,24 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -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; ...@@ -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) {
......
...@@ -21,7 +21,6 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull; ...@@ -21,7 +21,6 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.content.Context; import android.content.Context;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
...@@ -60,7 +59,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -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; ...@@ -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);
} }
......
...@@ -25,12 +25,9 @@ import android.media.MediaFormat; ...@@ -25,12 +25,9 @@ import android.media.MediaFormat;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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);
}
} }
...@@ -24,11 +24,15 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -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.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
...@@ -39,6 +43,7 @@ import com.google.android.exoplayer2.util.MimeTypes; ...@@ -39,6 +43,7 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
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;
...@@ -233,6 +238,63 @@ public final class TransformerTest { ...@@ -233,6 +238,63 @@ public final class TransformerTest {
} }
@Test @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 { 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();
MediaItem mediaItem = MediaItem.fromUri("asset:///non-existing-path.mp4"); MediaItem mediaItem = MediaItem.fromUri("asset:///non-existing-path.mp4");
...@@ -541,6 +603,30 @@ public final class TransformerTest { ...@@ -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() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment