mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Remove ExoPlaybackException dependency from sample pipelines.
Use TransformationException for codec and audio processor initialization problems instead. PiperOrigin-RevId: 416765510
This commit is contained in:
parent
1633ad12a3
commit
a8dbc744db
@ -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
|
||||
|
@ -100,29 +100,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
* @param format The {@link Format} (of the input data) used to determine the underlying {@link
|
||||
* MediaCodec} and its configuration values.
|
||||
* @return A configured and started decoder wrapper.
|
||||
* @throws IOException If the underlying codec cannot be created.
|
||||
* @throws TransformationException If the underlying codec cannot be created.
|
||||
*/
|
||||
public static MediaCodecAdapterWrapper createForAudioDecoding(Format format) throws IOException {
|
||||
@Nullable MediaCodecAdapter adapter = null;
|
||||
public static MediaCodecAdapterWrapper createForAudioDecoding(Format format)
|
||||
throws TransformationException {
|
||||
MediaFormat mediaFormat =
|
||||
MediaFormat.createAudioFormat(
|
||||
checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount);
|
||||
MediaFormatUtil.maybeSetInteger(
|
||||
mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize);
|
||||
MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
|
||||
|
||||
MediaCodecAdapter adapter;
|
||||
try {
|
||||
MediaFormat mediaFormat =
|
||||
MediaFormat.createAudioFormat(
|
||||
checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount);
|
||||
MediaFormatUtil.maybeSetInteger(
|
||||
mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize);
|
||||
MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
|
||||
adapter =
|
||||
new Factory()
|
||||
.createAdapter(
|
||||
MediaCodecAdapter.Configuration.createForAudioDecoding(
|
||||
createPlaceholderMediaCodecInfo(), mediaFormat, format, /* crypto= */ null));
|
||||
return new MediaCodecAdapterWrapper(adapter);
|
||||
} catch (Exception e) {
|
||||
if (adapter != null) {
|
||||
adapter.release();
|
||||
}
|
||||
throw e;
|
||||
throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ true);
|
||||
}
|
||||
return new MediaCodecAdapterWrapper(adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,28 +132,26 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
* MediaCodec} and its configuration values.
|
||||
* @param surface The {@link Surface} to which the decoder output is rendered.
|
||||
* @return A configured and started decoder wrapper.
|
||||
* @throws IOException If the underlying codec cannot be created.
|
||||
* @throws TransformationException If the underlying codec cannot be created.
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
public static MediaCodecAdapterWrapper createForVideoDecoding(Format format, Surface surface)
|
||||
throws IOException {
|
||||
@Nullable MediaCodecAdapter adapter = null;
|
||||
throws TransformationException {
|
||||
MediaFormat mediaFormat =
|
||||
MediaFormat.createVideoFormat(
|
||||
checkNotNull(format.sampleMimeType), format.width, format.height);
|
||||
MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees);
|
||||
MediaFormatUtil.maybeSetInteger(
|
||||
mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize);
|
||||
MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
|
||||
if (SDK_INT >= 29) {
|
||||
// On API levels over 29, Transformer decodes as many frames as possible in one render
|
||||
// cycle. This key ensures no frame dropping when the decoder's output surface is full.
|
||||
mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0);
|
||||
}
|
||||
|
||||
MediaCodecAdapter adapter;
|
||||
try {
|
||||
MediaFormat mediaFormat =
|
||||
MediaFormat.createVideoFormat(
|
||||
checkNotNull(format.sampleMimeType), format.width, format.height);
|
||||
MediaFormatUtil.maybeSetInteger(
|
||||
mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees);
|
||||
MediaFormatUtil.maybeSetInteger(
|
||||
mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize);
|
||||
MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData);
|
||||
|
||||
if (SDK_INT >= 29) {
|
||||
// On API levels over 29, Transformer decodes as many frames as possible in one render
|
||||
// cycle. This key ensures no frame dropping when the decoder's output surface is full.
|
||||
mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0);
|
||||
}
|
||||
|
||||
adapter =
|
||||
new Factory()
|
||||
.createAdapter(
|
||||
@ -164,13 +161,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
format,
|
||||
surface,
|
||||
/* crypto= */ null));
|
||||
return new MediaCodecAdapterWrapper(adapter);
|
||||
} catch (Exception e) {
|
||||
if (adapter != null) {
|
||||
adapter.release();
|
||||
}
|
||||
throw e;
|
||||
throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ true);
|
||||
}
|
||||
return new MediaCodecAdapterWrapper(adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -180,30 +174,26 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
* @param format The {@link Format} (of the output data) used to determine the underlying {@link
|
||||
* MediaCodec} and its configuration values.
|
||||
* @return A configured and started encoder wrapper.
|
||||
* @throws IOException If the underlying codec cannot be created.
|
||||
* @throws TransformationException If the underlying codec cannot be created.
|
||||
*/
|
||||
public static MediaCodecAdapterWrapper createForAudioEncoding(Format format) throws IOException {
|
||||
@Nullable MediaCodec encoder = null;
|
||||
@Nullable MediaCodecAdapter adapter = null;
|
||||
public static MediaCodecAdapterWrapper createForAudioEncoding(Format format)
|
||||
throws TransformationException {
|
||||
MediaFormat mediaFormat =
|
||||
MediaFormat.createAudioFormat(
|
||||
checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate);
|
||||
|
||||
MediaCodecAdapter adapter;
|
||||
try {
|
||||
MediaFormat mediaFormat =
|
||||
MediaFormat.createAudioFormat(
|
||||
checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate);
|
||||
adapter =
|
||||
new Factory()
|
||||
.createAdapter(
|
||||
MediaCodecAdapter.Configuration.createForAudioEncoding(
|
||||
createPlaceholderMediaCodecInfo(), mediaFormat, format));
|
||||
return new MediaCodecAdapterWrapper(adapter);
|
||||
} catch (Exception e) {
|
||||
if (adapter != null) {
|
||||
adapter.release();
|
||||
} else if (encoder != null) {
|
||||
encoder.release();
|
||||
}
|
||||
throw e;
|
||||
throw createTransformationException(e, format, /* isVideo= */ false, /* isDecoder= */ false);
|
||||
}
|
||||
return new MediaCodecAdapterWrapper(adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -219,41 +209,37 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
* are from {@code MediaFormat.KEY_*} constants. Its values will override those in {@code
|
||||
* format}.
|
||||
* @return A configured and started encoder wrapper.
|
||||
* @throws IOException If the underlying codec cannot be created.
|
||||
* @throws TransformationException If the underlying codec cannot be created.
|
||||
*/
|
||||
public static MediaCodecAdapterWrapper createForVideoEncoding(
|
||||
Format format, Map<String, Integer> additionalEncoderConfig) throws IOException {
|
||||
Format format, Map<String, Integer> additionalEncoderConfig) throws TransformationException {
|
||||
checkArgument(format.width != Format.NO_VALUE);
|
||||
checkArgument(format.height != Format.NO_VALUE);
|
||||
checkArgument(format.height < format.width);
|
||||
checkArgument(format.rotationDegrees == 0);
|
||||
|
||||
@Nullable MediaCodecAdapter adapter = null;
|
||||
MediaFormat mediaFormat =
|
||||
MediaFormat.createVideoFormat(
|
||||
checkNotNull(format.sampleMimeType), format.width, format.height);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 413_000);
|
||||
for (Map.Entry<String, Integer> encoderSetting : additionalEncoderConfig.entrySet()) {
|
||||
mediaFormat.setInteger(encoderSetting.getKey(), encoderSetting.getValue());
|
||||
}
|
||||
|
||||
MediaCodecAdapter adapter;
|
||||
try {
|
||||
MediaFormat mediaFormat =
|
||||
MediaFormat.createVideoFormat(
|
||||
checkNotNull(format.sampleMimeType), format.width, format.height);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 413_000);
|
||||
|
||||
for (Map.Entry<String, Integer> encoderSetting : additionalEncoderConfig.entrySet()) {
|
||||
mediaFormat.setInteger(encoderSetting.getKey(), encoderSetting.getValue());
|
||||
}
|
||||
|
||||
adapter =
|
||||
new Factory()
|
||||
.createAdapter(
|
||||
MediaCodecAdapter.Configuration.createForVideoEncoding(
|
||||
createPlaceholderMediaCodecInfo(), mediaFormat, format));
|
||||
return new MediaCodecAdapterWrapper(adapter);
|
||||
} catch (Exception e) {
|
||||
if (adapter != null) {
|
||||
adapter.release();
|
||||
}
|
||||
throw e;
|
||||
throw createTransformationException(e, format, /* isVideo= */ true, /* isDecoder= */ false);
|
||||
}
|
||||
return new MediaCodecAdapterWrapper(adapter);
|
||||
}
|
||||
|
||||
private MediaCodecAdapterWrapper(MediaCodecAdapter codec) {
|
||||
@ -458,4 +444,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
}
|
||||
return formatBuilder.build();
|
||||
}
|
||||
|
||||
private static TransformationException createTransformationException(
|
||||
Exception cause, Format format, boolean isVideo, boolean isDecoder) {
|
||||
String componentName = (isVideo ? "Video" : "Audio") + (isDecoder ? "Decoder" : "Encoder");
|
||||
if (cause instanceof IOException) {
|
||||
return TransformationException.createForCodec(
|
||||
cause,
|
||||
componentName,
|
||||
format,
|
||||
isDecoder
|
||||
? TransformationException.ERROR_CODE_DECODER_INIT_FAILED
|
||||
: TransformationException.ERROR_CODE_ENCODER_INIT_FAILED);
|
||||
}
|
||||
if (cause instanceof IllegalArgumentException) {
|
||||
return TransformationException.createForCodec(
|
||||
cause,
|
||||
componentName,
|
||||
format,
|
||||
isDecoder
|
||||
? TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED
|
||||
: TransformationException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED);
|
||||
}
|
||||
return TransformationException.createForUnexpected(cause);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user