Improve format propagation in transformer

- Store output format in `MediaCodecAdapterWrapper` when we get a format from
  the codec, instead of creating it on demand.
- Make format building code not audio-specific.
- Remove `MediaCodecAdapterWrapper.getConfigFormat` and instead keep track of
  the input/output formats in the renderer. This will mean that the code still
  works if an audio processor changes the audio format in future.
- Make exceptions thrown during audio rendering use the same (input) renderer
  format.
- Misc other minor cleanup.

#minor-release

PiperOrigin-RevId: 354556619
This commit is contained in:
andrewlewis 2021-01-29 18:11:23 +00:00 committed by Oliver Woodman
parent d5499ee36f
commit 84b96fdff7
6 changed files with 87 additions and 78 deletions

View File

@ -29,11 +29,12 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
import com.google.android.exoplayer2.mediacodec.MediaFormatUtil; import com.google.android.exoplayer2.mediacodec.MediaFormatUtil;
import com.google.android.exoplayer2.mediacodec.SynchronousMediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.SynchronousMediaCodecAdapter;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.io.IOException; 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.RequiresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* A wrapper around {@link MediaCodecAdapter}. * A wrapper around {@link MediaCodecAdapter}.
@ -44,17 +45,20 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
*/ */
/* package */ final class MediaCodecAdapterWrapper { /* package */ final class MediaCodecAdapterWrapper {
// MediaCodec decoders always output 16 bit PCM, unless configured to output PCM float.
// https://developer.android.com/reference/android/media/MediaCodec#raw-audio-buffers.
private static final int MEDIA_CODEC_PCM_ENCODING = C.ENCODING_PCM_16BIT;
private final BufferInfo outputBufferInfo; private final BufferInfo outputBufferInfo;
private final MediaCodecAdapter codec; private final MediaCodecAdapter codec;
private final Format format;
private @MonotonicNonNull Format outputFormat;
@Nullable private ByteBuffer outputBuffer; @Nullable private ByteBuffer outputBuffer;
private int inputBufferIndex; private int inputBufferIndex;
private int outputBufferIndex; private int outputBufferIndex;
private boolean inputStreamEnded; private boolean inputStreamEnded;
private boolean outputStreamEnded; private boolean outputStreamEnded;
private boolean hasOutputFormat;
/** /**
* Returns a {@link MediaCodecAdapterWrapper} for a configured and started {@link * Returns a {@link MediaCodecAdapterWrapper} for a configured and started {@link
@ -65,12 +69,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @return A configured and started decoder wrapper. * @return A configured and started decoder wrapper.
* @throws IOException If the underlying codec cannot be created. * @throws IOException If the underlying codec cannot be created.
*/ */
@RequiresNonNull("#1.sampleMimeType")
public static MediaCodecAdapterWrapper createForAudioDecoding(Format format) throws IOException { public static MediaCodecAdapterWrapper createForAudioDecoding(Format format) throws IOException {
@Nullable MediaCodec decoder = null; @Nullable MediaCodec decoder = null;
@Nullable MediaCodecAdapter adapter = null; @Nullable MediaCodecAdapter adapter = null;
try { try {
decoder = MediaCodec.createDecoderByType(format.sampleMimeType); decoder = MediaCodec.createDecoderByType(checkNotNull(format.sampleMimeType));
MediaFormat mediaFormat = MediaFormat mediaFormat =
MediaFormat.createAudioFormat( MediaFormat.createAudioFormat(
format.sampleMimeType, format.sampleRate, format.channelCount); format.sampleMimeType, format.sampleRate, format.channelCount);
@ -78,7 +81,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
adapter = new SynchronousMediaCodecAdapter.Factory().createAdapter(decoder); adapter = new SynchronousMediaCodecAdapter.Factory().createAdapter(decoder);
adapter.configure(mediaFormat, /* surface= */ null, /* crypto= */ null, /* flags= */ 0); adapter.configure(mediaFormat, /* surface= */ null, /* crypto= */ null, /* flags= */ 0);
adapter.start(); adapter.start();
return new MediaCodecAdapterWrapper(adapter, format); return new MediaCodecAdapterWrapper(adapter);
} catch (Exception e) { } catch (Exception e) {
if (adapter != null) { if (adapter != null) {
adapter.release(); adapter.release();
@ -98,12 +101,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @return A configured and started encoder wrapper. * @return A configured and started encoder wrapper.
* @throws IOException If the underlying codec cannot be created. * @throws IOException If the underlying codec cannot be created.
*/ */
@RequiresNonNull("#1.sampleMimeType")
public static MediaCodecAdapterWrapper createForAudioEncoding(Format format) throws IOException { public static MediaCodecAdapterWrapper createForAudioEncoding(Format format) throws IOException {
@Nullable MediaCodec encoder = null; @Nullable MediaCodec encoder = null;
@Nullable MediaCodecAdapter adapter = null; @Nullable MediaCodecAdapter adapter = null;
try { try {
encoder = MediaCodec.createEncoderByType(format.sampleMimeType); encoder = MediaCodec.createEncoderByType(checkNotNull(format.sampleMimeType));
MediaFormat mediaFormat = MediaFormat mediaFormat =
MediaFormat.createAudioFormat( MediaFormat.createAudioFormat(
format.sampleMimeType, format.sampleRate, format.channelCount); format.sampleMimeType, format.sampleRate, format.channelCount);
@ -115,7 +117,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/* crypto= */ null, /* crypto= */ null,
/* flags= */ MediaCodec.CONFIGURE_FLAG_ENCODE); /* flags= */ MediaCodec.CONFIGURE_FLAG_ENCODE);
adapter.start(); adapter.start();
return new MediaCodecAdapterWrapper(adapter, format); return new MediaCodecAdapterWrapper(adapter);
} catch (Exception e) { } catch (Exception e) {
if (adapter != null) { if (adapter != null) {
adapter.release(); adapter.release();
@ -126,9 +128,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
} }
private MediaCodecAdapterWrapper(MediaCodecAdapter codec, Format format) { private MediaCodecAdapterWrapper(MediaCodecAdapter codec) {
this.codec = codec; this.codec = codec;
this.format = format;
outputBufferInfo = new BufferInfo(); outputBufferInfo = new BufferInfo();
inputBufferIndex = C.INDEX_UNSET; inputBufferIndex = C.INDEX_UNSET;
outputBufferIndex = C.INDEX_UNSET; outputBufferIndex = C.INDEX_UNSET;
@ -202,8 +203,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
outputBufferIndex = codec.dequeueOutputBufferIndex(outputBufferInfo); outputBufferIndex = codec.dequeueOutputBufferIndex(outputBufferInfo);
if (outputBufferIndex < 0) { if (outputBufferIndex < 0) {
if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED && !hasOutputFormat) { if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
hasOutputFormat = true; outputFormat = getFormat(codec.getOutputFormat());
} }
return false; return false;
} }
@ -228,41 +229,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return true; return true;
} }
/** /** Returns the current output format, if available. */
* Returns a {@link Format} based on the {@link MediaCodecAdapter#getOutputFormat() mediaFormat},
* if available.
*/
@Nullable @Nullable
public Format getOutputFormat() { public Format getOutputFormat() {
@Nullable MediaFormat mediaFormat = hasOutputFormat ? codec.getOutputFormat() : null; return outputFormat;
if (mediaFormat == null) {
return null;
}
ImmutableList.Builder<byte[]> csdBuffers = new ImmutableList.Builder<>();
int csdIndex = 0;
while (true) {
@Nullable ByteBuffer csdByteBuffer = mediaFormat.getByteBuffer("csd-" + csdIndex);
if (csdByteBuffer == null) {
break;
}
byte[] csdBufferData = new byte[csdByteBuffer.remaining()];
csdByteBuffer.get(csdBufferData);
csdBuffers.add(csdBufferData);
csdIndex++;
}
return new Format.Builder()
.setSampleMimeType(mediaFormat.getString(MediaFormat.KEY_MIME))
.setChannelCount(mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT))
.setSampleRate(mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE))
.setInitializationData(csdBuffers.build())
.build();
}
/** Returns the {@link Format} used to create and configure the underlying {@link MediaCodec}. */
public Format getConfigFormat() {
return format;
} }
/** Returns the current output {@link ByteBuffer}, if available. */ /** Returns the current output {@link ByteBuffer}, if available. */
@ -299,4 +269,37 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
outputBuffer = null; outputBuffer = null;
codec.release(); codec.release();
} }
private static Format getFormat(MediaFormat mediaFormat) {
ImmutableList.Builder<byte[]> csdBuffers = new ImmutableList.Builder<>();
int csdIndex = 0;
while (true) {
@Nullable ByteBuffer csdByteBuffer = mediaFormat.getByteBuffer("csd-" + csdIndex);
if (csdByteBuffer == null) {
break;
}
byte[] csdBufferData = new byte[csdByteBuffer.remaining()];
csdByteBuffer.get(csdBufferData);
csdBuffers.add(csdBufferData);
csdIndex++;
}
String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
Format.Builder formatBuilder =
new Format.Builder()
.setSampleMimeType(mediaFormat.getString(MediaFormat.KEY_MIME))
.setInitializationData(csdBuffers.build());
if (MimeTypes.isVideo(mimeType)) {
formatBuilder
.setWidth(mediaFormat.getInteger(MediaFormat.KEY_WIDTH))
.setHeight(mediaFormat.getInteger(MediaFormat.KEY_HEIGHT));
} else if (MimeTypes.isAudio(mimeType)) {
// TODO(internal b/178685617): Only set the PCM encoding for audio/raw, once we have a way to
// simulate more realistic codec input/output formats in tests.
formatBuilder
.setChannelCount(mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT))
.setSampleRate(mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE))
.setPcmEncoding(MEDIA_CODEC_PCM_ENCODING);
}
return formatBuilder.build();
}
} }

View File

@ -32,7 +32,6 @@ 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 com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -40,9 +39,6 @@ import java.nio.ByteBuffer;
/* package */ final class TransformerAudioRenderer extends TransformerBaseRenderer { /* package */ final class TransformerAudioRenderer extends TransformerBaseRenderer {
private static final String TAG = "TransformerAudioRenderer"; private static final String TAG = "TransformerAudioRenderer";
// MediaCodec decoders always output 16 bit PCM, unless configured to output PCM float.
// https://developer.android.com/reference/android/media/MediaCodec#raw-audio-buffers.
private static final int MEDIA_CODEC_PCM_ENCODING = C.ENCODING_PCM_16BIT;
private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024; private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024;
private static final float SPEED_UNSET = -1f; private static final float SPEED_UNSET = -1f;
@ -53,6 +49,8 @@ import java.nio.ByteBuffer;
@Nullable private MediaCodecAdapterWrapper decoder; @Nullable private MediaCodecAdapterWrapper decoder;
@Nullable private MediaCodecAdapterWrapper encoder; @Nullable private MediaCodecAdapterWrapper encoder;
@Nullable private SpeedProvider speedProvider; @Nullable private SpeedProvider speedProvider;
@Nullable private Format inputFormat;
@Nullable private AudioFormat encoderInputAudioFormat;
private ByteBuffer sonicOutputBuffer; private ByteBuffer sonicOutputBuffer;
private long nextEncoderInputBufferTimeUs; private long nextEncoderInputBufferTimeUs;
@ -100,6 +98,8 @@ import java.nio.ByteBuffer;
encoder = null; encoder = null;
} }
speedProvider = null; speedProvider = null;
inputFormat = null;
encoderInputAudioFormat = null;
sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER; sonicOutputBuffer = AudioProcessor.EMPTY_BUFFER;
nextEncoderInputBufferTimeUs = 0; nextEncoderInputBufferTimeUs = 0;
currentSpeed = SPEED_UNSET; currentSpeed = SPEED_UNSET;
@ -307,6 +307,7 @@ import java.nio.ByteBuffer;
* returns whether it may be possible to write more data. * returns whether it may be possible to write more data.
*/ */
private boolean feedEncoder(ByteBuffer inputBuffer) { private boolean feedEncoder(ByteBuffer inputBuffer) {
AudioFormat encoderInputAudioFormat = checkNotNull(this.encoderInputAudioFormat);
MediaCodecAdapterWrapper encoder = checkNotNull(this.encoder); MediaCodecAdapterWrapper encoder = checkNotNull(this.encoder);
ByteBuffer encoderInputBufferData = checkNotNull(encoderInputBuffer.data); ByteBuffer encoderInputBufferData = checkNotNull(encoderInputBuffer.data);
int bufferLimit = inputBuffer.limit(); int bufferLimit = inputBuffer.limit();
@ -317,9 +318,8 @@ import java.nio.ByteBuffer;
nextEncoderInputBufferTimeUs += nextEncoderInputBufferTimeUs +=
getBufferDurationUs( getBufferDurationUs(
/* bytesWritten= */ encoderInputBufferData.position(), /* bytesWritten= */ encoderInputBufferData.position(),
/* bytesPerFrame= */ Util.getPcmFrameSize( encoderInputAudioFormat.bytesPerFrame,
MEDIA_CODEC_PCM_ENCODING, encoder.getConfigFormat().channelCount), encoderInputAudioFormat.sampleRate);
encoder.getConfigFormat().sampleRate);
encoderInputBuffer.setFlags(0); encoderInputBuffer.setFlags(0);
encoderInputBuffer.flip(); encoderInputBuffer.flip();
@ -342,30 +342,35 @@ import java.nio.ByteBuffer;
* yet. * yet.
*/ */
private void setupEncoderAndMaybeSonic() throws ExoPlaybackException { private void setupEncoderAndMaybeSonic() throws ExoPlaybackException {
MediaCodecAdapterWrapper decoder = checkNotNull(this.decoder);
if (encoder != null) { if (encoder != null) {
return; return;
} }
// TODO(b/161127201): Use the decoder output format once the decoder is fed before setting up
Format decoderFormat = decoder.getConfigFormat(); // the encoder.
AudioFormat outputAudioFormat =
new AudioFormat(
checkNotNull(inputFormat).sampleRate, inputFormat.channelCount, C.ENCODING_PCM_16BIT);
if (transformation.flattenForSlowMotion) { if (transformation.flattenForSlowMotion) {
try { try {
configureSonic(decoderFormat); outputAudioFormat = sonicAudioProcessor.configure(outputAudioFormat);
flushSonicAndSetSpeed(currentSpeed);
} catch (AudioProcessor.UnhandledAudioFormatException e) { } catch (AudioProcessor.UnhandledAudioFormatException e) {
throw ExoPlaybackException.createForRenderer( throw createRendererException(e);
e, TAG, getIndex(), /* rendererFormat= */ null, C.FORMAT_HANDLED);
} }
} }
Format encoderFormat =
decoderFormat.buildUpon().setAverageBitrate(DEFAULT_ENCODER_BITRATE).build();
checkNotNull(encoderFormat.sampleMimeType);
try { try {
encoder = MediaCodecAdapterWrapper.createForAudioEncoding(encoderFormat); encoder =
MediaCodecAdapterWrapper.createForAudioEncoding(
new Format.Builder()
.setSampleMimeType(checkNotNull(inputFormat).sampleMimeType)
.setSampleRate(outputAudioFormat.sampleRate)
.setChannelCount(outputAudioFormat.channelCount)
.setAverageBitrate(DEFAULT_ENCODER_BITRATE)
.build());
} catch (IOException e) { } catch (IOException e) {
throw ExoPlaybackException.createForRenderer( throw createRendererException(e);
e, TAG, getIndex(), encoderFormat, /* rendererFormatSupport= */ C.FORMAT_HANDLED);
} }
encoderInputAudioFormat = outputAudioFormat;
} }
/** /**
@ -383,15 +388,13 @@ import java.nio.ByteBuffer;
if (result != C.RESULT_FORMAT_READ) { if (result != C.RESULT_FORMAT_READ) {
return false; return false;
} }
Format decoderFormat = checkNotNull(formatHolder.format); inputFormat = checkNotNull(formatHolder.format);
checkNotNull(decoderFormat.sampleMimeType);
try { try {
decoder = MediaCodecAdapterWrapper.createForAudioDecoding(decoderFormat); decoder = MediaCodecAdapterWrapper.createForAudioDecoding(inputFormat);
} catch (IOException e) { } catch (IOException e) {
throw ExoPlaybackException.createForRenderer( throw createRendererException(e);
e, TAG, getIndex(), decoderFormat, /* rendererFormatSupport= */ C.FORMAT_HANDLED);
} }
speedProvider = new SegmentSpeedProvider(decoderFormat); speedProvider = new SegmentSpeedProvider(inputFormat);
currentSpeed = speedProvider.getSpeed(0); currentSpeed = speedProvider.getSpeed(0);
return true; return true;
} }
@ -406,18 +409,17 @@ import java.nio.ByteBuffer;
return speedChanging; return speedChanging;
} }
private void configureSonic(Format format) throws AudioProcessor.UnhandledAudioFormatException {
sonicAudioProcessor.configure(
new AudioFormat(format.sampleRate, format.channelCount, MEDIA_CODEC_PCM_ENCODING));
flushSonicAndSetSpeed(currentSpeed);
}
private void flushSonicAndSetSpeed(float speed) { private void flushSonicAndSetSpeed(float speed) {
sonicAudioProcessor.setSpeed(speed); sonicAudioProcessor.setSpeed(speed);
sonicAudioProcessor.setPitch(speed); sonicAudioProcessor.setPitch(speed);
sonicAudioProcessor.flush(); sonicAudioProcessor.flush();
} }
private ExoPlaybackException createRendererException(Throwable cause) {
return ExoPlaybackException.createForRenderer(
cause, TAG, getIndex(), inputFormat, /* rendererFormatSupport= */ C.FORMAT_HANDLED);
}
private static long getBufferDurationUs(long bytesWritten, int bytesPerFrame, int sampleRate) { private static long getBufferDurationUs(long bytesWritten, int bytesPerFrame, int sampleRate) {
long framesWritten = bytesWritten / bytesPerFrame; long framesWritten = bytesWritten / bytesPerFrame;
return framesWritten * C.MICROS_PER_SECOND / sampleRate; return framesWritten * C.MICROS_PER_SECOND / sampleRate;

View File

@ -3,6 +3,7 @@ format 0:
sampleMimeType = audio/3gpp sampleMimeType = audio/3gpp
channelCount = 1 channelCount = 1
sampleRate = 8000 sampleRate = 8000
pcmEncoding = 2
sample: sample:
trackIndex = 0 trackIndex = 0
dataHashCode = 924517484 dataHashCode = 924517484

View File

@ -3,6 +3,7 @@ format 0:
sampleMimeType = audio/mp4a-latm sampleMimeType = audio/mp4a-latm
channelCount = 1 channelCount = 1
sampleRate = 44100 sampleRate = 44100
pcmEncoding = 2
format 1: format 1:
id = 1 id = 1
sampleMimeType = video/avc sampleMimeType = video/avc

View File

@ -3,6 +3,7 @@ format 0:
sampleMimeType = audio/mp4a-latm sampleMimeType = audio/mp4a-latm
channelCount = 1 channelCount = 1
sampleRate = 44100 sampleRate = 44100
pcmEncoding = 2
sample: sample:
trackIndex = 0 trackIndex = 0
dataHashCode = 1205768497 dataHashCode = 1205768497

View File

@ -3,6 +3,7 @@ format 0:
sampleMimeType = audio/mp4a-latm sampleMimeType = audio/mp4a-latm
channelCount = 2 channelCount = 2
sampleRate = 12000 sampleRate = 12000
pcmEncoding = 2
format 1: format 1:
id = 2 id = 2
sampleMimeType = video/avc sampleMimeType = video/avc