Move audio decoding to AssetLoader

PiperOrigin-RevId: 491933937
This commit is contained in:
kimvde 2022-11-30 17:05:41 +00:00 committed by Ian Baker
parent ff7fe222b8
commit eecf7caed0
7 changed files with 188 additions and 55 deletions

View File

@ -41,14 +41,14 @@ import org.checkerframework.dataflow.qual.Pure;
private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024; private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024;
private final Codec decoder; private final DecoderInputBuffer inputBuffer;
private final DecoderInputBuffer decoderInputBuffer;
private final AudioProcessingPipeline audioProcessingPipeline; private final AudioProcessingPipeline audioProcessingPipeline;
private final Codec encoder; private final Codec encoder;
private final AudioFormat encoderInputAudioFormat; private final AudioFormat encoderInputAudioFormat;
private final DecoderInputBuffer encoderInputBuffer; private final DecoderInputBuffer encoderInputBuffer;
private final DecoderInputBuffer encoderOutputBuffer; private final DecoderInputBuffer encoderOutputBuffer;
private boolean hasPendingInputBuffer;
private long nextEncoderInputBufferTimeUs; private long nextEncoderInputBufferTimeUs;
private long encoderBufferDurationRemainder; private long encoderBufferDurationRemainder;
@ -58,7 +58,6 @@ import org.checkerframework.dataflow.qual.Pure;
long streamOffsetUs, long streamOffsetUs,
TransformationRequest transformationRequest, TransformationRequest transformationRequest,
ImmutableList<AudioProcessor> audioProcessors, ImmutableList<AudioProcessor> audioProcessors,
Codec.DecoderFactory decoderFactory,
Codec.EncoderFactory encoderFactory, Codec.EncoderFactory encoderFactory,
MuxerWrapper muxerWrapper, MuxerWrapper muxerWrapper,
Listener listener, Listener listener,
@ -72,12 +71,10 @@ import org.checkerframework.dataflow.qual.Pure;
muxerWrapper, muxerWrapper,
listener); listener);
decoderInputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED); inputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED);
encoderInputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED); encoderInputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED);
encoderOutputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED); encoderOutputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED);
decoder = decoderFactory.createForAudioDecoding(inputFormat);
if (transformationRequest.flattenForSlowMotion) { if (transformationRequest.flattenForSlowMotion) {
audioProcessors = audioProcessors =
new ImmutableList.Builder<AudioProcessor>() new ImmutableList.Builder<AudioProcessor>()
@ -139,30 +136,34 @@ import org.checkerframework.dataflow.qual.Pure;
nextEncoderInputBufferTimeUs = streamOffsetUs; nextEncoderInputBufferTimeUs = streamOffsetUs;
} }
@Override
public boolean expectsDecodedData() {
return true;
}
@Override @Override
public void release() { public void release() {
audioProcessingPipeline.reset(); audioProcessingPipeline.reset();
decoder.release();
encoder.release(); encoder.release();
} }
@Override @Override
@Nullable @Nullable
protected DecoderInputBuffer dequeueInputBufferInternal() throws TransformationException { protected DecoderInputBuffer dequeueInputBufferInternal() {
return decoder.maybeDequeueInputBuffer(decoderInputBuffer) ? decoderInputBuffer : null; return hasPendingInputBuffer ? null : inputBuffer;
} }
@Override @Override
protected void queueInputBufferInternal() throws TransformationException { protected void queueInputBufferInternal() {
decoder.queueInputBuffer(decoderInputBuffer); hasPendingInputBuffer = true;
} }
@Override @Override
protected boolean processDataUpToMuxer() throws TransformationException { protected boolean processDataUpToMuxer() throws TransformationException {
if (audioProcessingPipeline.isOperational()) { if (audioProcessingPipeline.isOperational()) {
return feedEncoderFromProcessingPipeline() || feedProcessingPipelineFromDecoder(); return feedEncoderFromProcessingPipeline() || feedProcessingPipelineFromInput();
} else { } else {
return feedEncoderFromDecoder(); return feedEncoderFromInput();
} }
} }
@ -195,27 +196,25 @@ import org.checkerframework.dataflow.qual.Pure;
} }
/** /**
* Attempts to pass decoder output data to the encoder, and returns whether it may be possible to * Attempts to pass input data to the encoder.
* pass more data immediately by calling this method again. *
* @return Whether it may be possible to feed more data immediately by calling this method again.
*/ */
private boolean feedEncoderFromDecoder() throws TransformationException { private boolean feedEncoderFromInput() throws TransformationException {
if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { if (!hasPendingInputBuffer || !encoder.maybeDequeueInputBuffer(encoderInputBuffer)) {
return false; return false;
} }
if (decoder.isEnded()) { if (inputBuffer.isEndOfStream()) {
queueEndOfStreamToEncoder(); queueEndOfStreamToEncoder();
hasPendingInputBuffer = false;
return false; return false;
} }
@Nullable ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer(); ByteBuffer inputData = checkNotNull(inputBuffer.data);
if (decoderOutputBuffer == null) { feedEncoder(inputData);
return false; if (!inputData.hasRemaining()) {
} hasPendingInputBuffer = false;
feedEncoder(decoderOutputBuffer);
if (!decoderOutputBuffer.hasRemaining()) {
decoder.releaseOutputBuffer(/* render= */ false);
} }
return true; return true;
} }
@ -223,7 +222,7 @@ import org.checkerframework.dataflow.qual.Pure;
/** /**
* Attempts to feed audio processor output data to the encoder. * Attempts to feed audio processor output data to the encoder.
* *
* @return Whether more data can be fed immediately, by calling this method again. * @return Whether it may be possible to feed more data immediately by calling this method again.
*/ */
private boolean feedEncoderFromProcessingPipeline() throws TransformationException { private boolean feedEncoderFromProcessingPipeline() throws TransformationException {
if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) { if (!encoder.maybeDequeueInputBuffer(encoderInputBuffer)) {
@ -244,28 +243,28 @@ import org.checkerframework.dataflow.qual.Pure;
} }
/** /**
* Attempts to feed decoder output data to the {@link AudioProcessingPipeline}. * Attempts to feed input data to the {@link AudioProcessingPipeline}.
* *
* @return Whether it may be possible to feed more data immediately by calling this method again. * @return Whether it may be possible to feed more data immediately by calling this method again.
*/ */
private boolean feedProcessingPipelineFromDecoder() throws TransformationException { private boolean feedProcessingPipelineFromInput() {
if (decoder.isEnded()) { if (!hasPendingInputBuffer) {
return false;
}
if (inputBuffer.isEndOfStream()) {
audioProcessingPipeline.queueEndOfStream(); audioProcessingPipeline.queueEndOfStream();
hasPendingInputBuffer = false;
return false; return false;
} }
checkState(!audioProcessingPipeline.isEnded()); checkState(!audioProcessingPipeline.isEnded());
@Nullable ByteBuffer decoderOutputBuffer = decoder.getOutputBuffer(); ByteBuffer inputData = checkNotNull(inputBuffer.data);
if (decoderOutputBuffer == null) { audioProcessingPipeline.queueInput(inputData);
if (inputData.hasRemaining()) {
return false; return false;
} }
hasPendingInputBuffer = false;
audioProcessingPipeline.queueInput(decoderOutputBuffer);
if (decoderOutputBuffer.hasRemaining()) {
return false;
}
// Decoder output buffer was fully consumed by the processing pipeline.
decoder.releaseOutputBuffer(/* render= */ false);
return true; return true;
} }

View File

@ -69,6 +69,7 @@ import androidx.media3.exoplayer.video.VideoRendererEventListener;
boolean removeAudio, boolean removeAudio,
boolean removeVideo, boolean removeVideo,
MediaSource.Factory mediaSourceFactory, MediaSource.Factory mediaSourceFactory,
Codec.DecoderFactory decoderFactory,
Looper looper, Looper looper,
Listener listener, Listener listener,
Clock clock) { Clock clock) {
@ -89,7 +90,9 @@ import androidx.media3.exoplayer.video.VideoRendererEventListener;
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10) DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10)
.build(); .build();
ExoPlayer.Builder playerBuilder = ExoPlayer.Builder playerBuilder =
new ExoPlayer.Builder(context, new RenderersFactoryImpl(removeAudio, removeVideo, listener)) new ExoPlayer.Builder(
context,
new RenderersFactoryImpl(removeAudio, removeVideo, decoderFactory, listener))
.setMediaSourceFactory(mediaSourceFactory) .setMediaSourceFactory(mediaSourceFactory)
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.setLoadControl(loadControl) .setLoadControl(loadControl)
@ -120,14 +123,17 @@ import androidx.media3.exoplayer.video.VideoRendererEventListener;
private final TransformerMediaClock mediaClock; private final TransformerMediaClock mediaClock;
private final boolean removeAudio; private final boolean removeAudio;
private final boolean removeVideo; private final boolean removeVideo;
private final Codec.DecoderFactory decoderFactory;
private final ExoPlayerAssetLoader.Listener assetLoaderListener; private final ExoPlayerAssetLoader.Listener assetLoaderListener;
public RenderersFactoryImpl( public RenderersFactoryImpl(
boolean removeAudio, boolean removeAudio,
boolean removeVideo, boolean removeVideo,
Codec.DecoderFactory decoderFactory,
ExoPlayerAssetLoader.Listener assetLoaderListener) { ExoPlayerAssetLoader.Listener assetLoaderListener) {
this.removeAudio = removeAudio; this.removeAudio = removeAudio;
this.removeVideo = removeVideo; this.removeVideo = removeVideo;
this.decoderFactory = decoderFactory;
this.assetLoaderListener = assetLoaderListener; this.assetLoaderListener = assetLoaderListener;
mediaClock = new TransformerMediaClock(); mediaClock = new TransformerMediaClock();
} }
@ -144,12 +150,14 @@ import androidx.media3.exoplayer.video.VideoRendererEventListener;
int index = 0; int index = 0;
if (!removeAudio) { if (!removeAudio) {
renderers[index] = renderers[index] =
new ExoPlayerAssetLoaderRenderer(C.TRACK_TYPE_AUDIO, mediaClock, assetLoaderListener); new ExoPlayerAssetLoaderRenderer(
C.TRACK_TYPE_AUDIO, decoderFactory, mediaClock, assetLoaderListener);
index++; index++;
} }
if (!removeVideo) { if (!removeVideo) {
renderers[index] = renderers[index] =
new ExoPlayerAssetLoaderRenderer(C.TRACK_TYPE_VIDEO, mediaClock, assetLoaderListener); new ExoPlayerAssetLoaderRenderer(
C.TRACK_TYPE_VIDEO, decoderFactory, mediaClock, assetLoaderListener);
index++; index++;
} }
return renderers; return renderers;

View File

@ -20,6 +20,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.decoder.DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED; import static androidx.media3.decoder.DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED;
import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT; import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT;
import android.media.MediaCodec;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
@ -30,6 +31,7 @@ import androidx.media3.exoplayer.FormatHolder;
import androidx.media3.exoplayer.MediaClock; import androidx.media3.exoplayer.MediaClock;
import androidx.media3.exoplayer.RendererCapabilities; import androidx.media3.exoplayer.RendererCapabilities;
import androidx.media3.exoplayer.source.SampleStream.ReadDataResult; import androidx.media3.exoplayer.source.SampleStream.ReadDataResult;
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;
import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@ -38,6 +40,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private static final String TAG = "ExoPlayerAssetLoaderRenderer"; private static final String TAG = "ExoPlayerAssetLoaderRenderer";
private final Codec.DecoderFactory decoderFactory;
private final TransformerMediaClock mediaClock; private final TransformerMediaClock mediaClock;
private final ExoPlayerAssetLoader.Listener assetLoaderListener; private final ExoPlayerAssetLoader.Listener assetLoaderListener;
private final DecoderInputBuffer decoderInputBuffer; private final DecoderInputBuffer decoderInputBuffer;
@ -45,14 +48,18 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private boolean isTransformationRunning; private boolean isTransformationRunning;
private long streamStartPositionUs; private long streamStartPositionUs;
private long streamOffsetUs; private long streamOffsetUs;
private @MonotonicNonNull Codec decoder;
@Nullable private ByteBuffer pendingDecoderOutputBuffer;
private SamplePipeline.@MonotonicNonNull Input samplePipelineInput; private SamplePipeline.@MonotonicNonNull Input samplePipelineInput;
private boolean isEnded; private boolean isEnded;
public ExoPlayerAssetLoaderRenderer( public ExoPlayerAssetLoaderRenderer(
int trackType, int trackType,
Codec.DecoderFactory decoderFactory,
TransformerMediaClock mediaClock, TransformerMediaClock mediaClock,
ExoPlayerAssetLoader.Listener assetLoaderListener) { ExoPlayerAssetLoader.Listener assetLoaderListener) {
super(trackType); super(trackType);
this.decoderFactory = decoderFactory;
this.mediaClock = mediaClock; this.mediaClock = mediaClock;
this.assetLoaderListener = assetLoaderListener; this.assetLoaderListener = assetLoaderListener;
decoderInputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED); decoderInputBuffer = new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED);
@ -99,7 +106,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return; return;
} }
while (feedPipelineFromInput()) {} if (samplePipelineInput.expectsDecodedData()) {
while (feedPipelineFromDecoder() || feedDecoderFromInput()) {}
} else {
while (feedPipelineFromInput()) {}
}
} catch (TransformationException e) { } catch (TransformationException e) {
isTransformationRunning = false; isTransformationRunning = false;
assetLoaderListener.onError(e); assetLoaderListener.onError(e);
@ -128,6 +139,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
isTransformationRunning = false; isTransformationRunning = false;
} }
@Override
protected void onReset() {
if (decoder != null) {
decoder.release();
}
}
@EnsuresNonNullIf(expression = "samplePipelineInput", result = true) @EnsuresNonNullIf(expression = "samplePipelineInput", result = true)
private boolean ensureConfigured() throws TransformationException { private boolean ensureConfigured() throws TransformationException {
if (samplePipelineInput != null) { if (samplePipelineInput != null) {
@ -143,6 +161,74 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
Format inputFormat = checkNotNull(formatHolder.format); Format inputFormat = checkNotNull(formatHolder.format);
samplePipelineInput = samplePipelineInput =
assetLoaderListener.onTrackAdded(inputFormat, streamStartPositionUs, streamOffsetUs); assetLoaderListener.onTrackAdded(inputFormat, streamStartPositionUs, streamOffsetUs);
if (samplePipelineInput.expectsDecodedData()) {
decoder = decoderFactory.createForAudioDecoding(inputFormat);
}
return true;
}
/**
* Attempts to read decoded data and pass it to the sample pipeline.
*
* @return Whether it may be possible to read more data immediately by calling this method again.
* @throws TransformationException If an error occurs in the decoder or in the {@link
* SamplePipeline}.
*/
@RequiresNonNull("samplePipelineInput")
private boolean feedPipelineFromDecoder() throws TransformationException {
@Nullable
DecoderInputBuffer samplePipelineInputBuffer = samplePipelineInput.dequeueInputBuffer();
if (samplePipelineInputBuffer == null) {
return false;
}
Codec decoder = checkNotNull(this.decoder);
if (pendingDecoderOutputBuffer != null) {
if (pendingDecoderOutputBuffer.hasRemaining()) {
return false;
} else {
decoder.releaseOutputBuffer(/* render= */ false);
pendingDecoderOutputBuffer = null;
}
}
if (decoder.isEnded()) {
samplePipelineInputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
samplePipelineInput.queueInputBuffer();
isEnded = true;
return false;
}
pendingDecoderOutputBuffer = decoder.getOutputBuffer();
if (pendingDecoderOutputBuffer == null) {
return false;
}
samplePipelineInputBuffer.data = pendingDecoderOutputBuffer;
MediaCodec.BufferInfo bufferInfo = checkNotNull(decoder.getOutputBufferInfo());
samplePipelineInputBuffer.timeUs = bufferInfo.presentationTimeUs;
samplePipelineInputBuffer.setFlags(bufferInfo.flags);
samplePipelineInput.queueInputBuffer();
return true;
}
/**
* Attempts to read input data and pass it to the decoder.
*
* @return Whether it may be possible to read more data immediately by calling this method again.
* @throws TransformationException If an error occurs in the decoder.
*/
private boolean feedDecoderFromInput() throws TransformationException {
Codec decoder = checkNotNull(this.decoder);
if (!decoder.maybeDequeueInputBuffer(decoderInputBuffer)) {
return false;
}
if (!readInput(decoderInputBuffer)) {
return false;
}
decoder.queueInputBuffer(decoderInputBuffer);
return true; return true;
} }
@ -159,18 +245,32 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return false; return false;
} }
@ReadDataResult if (!readInput(samplePipelineInputBuffer)) {
int result = readSource(getFormatHolder(), samplePipelineInputBuffer, /* readFlags= */ 0); return false;
}
samplePipelineInput.queueInputBuffer();
if (samplePipelineInputBuffer.isEndOfStream()) {
isEnded = true;
return false;
}
return true;
}
/**
* Attempts to populate {@code buffer} with input data.
*
* @param buffer The buffer to populate.
* @return Whether the {@code buffer} has been populated.
*/
private boolean readInput(DecoderInputBuffer buffer) {
@ReadDataResult int result = readSource(getFormatHolder(), buffer, /* readFlags= */ 0);
switch (result) { switch (result) {
case C.RESULT_BUFFER_READ: case C.RESULT_BUFFER_READ:
samplePipelineInputBuffer.flip(); buffer.flip();
if (samplePipelineInputBuffer.isEndOfStream()) { if (!buffer.isEndOfStream()) {
samplePipelineInput.queueInputBuffer(); mediaClock.updateTimeForTrackType(getTrackType(), buffer.timeUs);
isEnded = true;
return false;
} }
mediaClock.updateTimeForTrackType(getTrackType(), samplePipelineInputBuffer.timeUs);
samplePipelineInput.queueInputBuffer();
return true; return true;
case C.RESULT_FORMAT_READ: case C.RESULT_FORMAT_READ:
throw new IllegalStateException("Format changes are not supported."); throw new IllegalStateException("Format changes are not supported.");

View File

@ -48,6 +48,11 @@ import androidx.media3.decoder.DecoderInputBuffer;
fallbackListener.onTransformationRequestFinalized(transformationRequest); fallbackListener.onTransformationRequestFinalized(transformationRequest);
} }
@Override
public boolean expectsDecodedData() {
return false;
}
@Override @Override
public void release() {} public void release() {}

View File

@ -29,6 +29,9 @@ import androidx.media3.decoder.DecoderInputBuffer;
/** Input of a {@link SamplePipeline}. */ /** Input of a {@link SamplePipeline}. */
interface Input { interface Input {
/** See {@link SamplePipeline#expectsDecodedData()}. */
boolean expectsDecodedData();
/** See {@link SamplePipeline#dequeueInputBuffer()}. */ /** See {@link SamplePipeline#dequeueInputBuffer()}. */
@Nullable @Nullable
DecoderInputBuffer dequeueInputBuffer(); DecoderInputBuffer dequeueInputBuffer();
@ -56,6 +59,12 @@ import androidx.media3.decoder.DecoderInputBuffer;
void onTransformationError(TransformationException exception); void onTransformationError(TransformationException exception);
} }
/**
* Returns whether the pipeline should be fed with decoded sample data. If false, encoded sample
* data should be queued.
*/
boolean expectsDecodedData();
/** Returns a buffer if the pipeline is ready to accept input, and {@code null} otherwise. */ /** Returns a buffer if the pipeline is ready to accept input, and {@code null} otherwise. */
@Nullable @Nullable
DecoderInputBuffer dequeueInputBuffer() throws TransformationException; DecoderInputBuffer dequeueInputBuffer() throws TransformationException;

View File

@ -163,6 +163,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
removeAudio, removeAudio,
removeVideo, removeVideo,
mediaSourceFactory, mediaSourceFactory,
decoderFactory,
internalLooper, internalLooper,
componentListener, componentListener,
clock); clock);
@ -409,7 +410,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
int samplePipelineIndex = tracksAddedCount; int samplePipelineIndex = tracksAddedCount;
tracksAddedCount++; tracksAddedCount++;
return new SamplePipelineInput(samplePipelineIndex); return new SamplePipelineInput(samplePipelineIndex, samplePipeline.expectsDecodedData());
} }
@Override @Override
@ -458,7 +459,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
streamOffsetUs, streamOffsetUs,
transformationRequest, transformationRequest,
audioProcessors, audioProcessors,
decoderFactory,
encoderFactory, encoderFactory,
muxerWrapper, muxerWrapper,
/* listener= */ this, /* listener= */ this,
@ -573,9 +573,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private class SamplePipelineInput implements SamplePipeline.Input { private class SamplePipelineInput implements SamplePipeline.Input {
private final int samplePipelineIndex; private final int samplePipelineIndex;
private final boolean expectsDecodedData;
public SamplePipelineInput(int samplePipelineIndex) { public SamplePipelineInput(int samplePipelineIndex, boolean expectsDecodedData) {
this.samplePipelineIndex = samplePipelineIndex; this.samplePipelineIndex = samplePipelineIndex;
this.expectsDecodedData = expectsDecodedData;
}
@Override
public boolean expectsDecodedData() {
return expectsDecodedData;
} }
@Nullable @Nullable

View File

@ -207,6 +207,11 @@ import org.checkerframework.dataflow.qual.Pure;
maxPendingFrameCount = decoder.getMaxPendingFrameCount(); maxPendingFrameCount = decoder.getMaxPendingFrameCount();
} }
@Override
public boolean expectsDecodedData() {
return false;
}
@Override @Override
public void release() { public void release() {
frameProcessor.release(); frameProcessor.release();