Support added/removed audio track between MediaItems

- Add silent audio when the output contains an audio track but the
  current MediaItem doesn't have any audio.
- Add an audio track when generateSilentAudio is set to true.

PiperOrigin-RevId: 511005887
This commit is contained in:
kimvde 2023-02-20 17:04:31 +00:00 committed by Andrew Lewis
parent 9fa8aba32e
commit 79d32c2488
8 changed files with 188 additions and 125 deletions

View File

@ -35,7 +35,6 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedDeque;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.Pure;
/** Pipeline to process, re-encode and mux raw audio samples. */ /** Pipeline to process, re-encode and mux raw audio samples. */
@ -44,7 +43,7 @@ import org.checkerframework.dataflow.qual.Pure;
private static final int MAX_INPUT_BUFFER_COUNT = 10; private static final int MAX_INPUT_BUFFER_COUNT = 10;
private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024; private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024;
@Nullable private final SilentAudioGenerator silentAudioGenerator; private final SilentAudioGenerator silentAudioGenerator;
private final Queue<DecoderInputBuffer> availableInputBuffers; private final Queue<DecoderInputBuffer> availableInputBuffers;
private final Queue<DecoderInputBuffer> pendingInputBuffers; private final Queue<DecoderInputBuffer> pendingInputBuffers;
private final AudioProcessingPipeline audioProcessingPipeline; private final AudioProcessingPipeline audioProcessingPipeline;
@ -56,7 +55,7 @@ import org.checkerframework.dataflow.qual.Pure;
private long nextEncoderInputBufferTimeUs; private long nextEncoderInputBufferTimeUs;
private long encoderBufferDurationRemainder; private long encoderBufferDurationRemainder;
private volatile long mediaItemOffsetUs; private volatile boolean queueEndOfStreamAfterSilence;
// TODO(b/260618558): Move silent audio generation upstream of this component. // TODO(b/260618558): Move silent audio generation upstream of this component.
public AudioSamplePipeline( public AudioSamplePipeline(
@ -66,19 +65,13 @@ import org.checkerframework.dataflow.qual.Pure;
TransformationRequest transformationRequest, TransformationRequest transformationRequest,
boolean flattenForSlowMotion, boolean flattenForSlowMotion,
ImmutableList<AudioProcessor> audioProcessors, ImmutableList<AudioProcessor> audioProcessors,
long forceAudioTrackDurationUs,
Codec.EncoderFactory encoderFactory, Codec.EncoderFactory encoderFactory,
MuxerWrapper muxerWrapper, MuxerWrapper muxerWrapper,
FallbackListener fallbackListener) FallbackListener fallbackListener)
throws ExportException { throws ExportException {
super(firstInputFormat, streamStartPositionUs, muxerWrapper); super(firstInputFormat, streamStartPositionUs, muxerWrapper);
if (forceAudioTrackDurationUs != C.TIME_UNSET) { silentAudioGenerator = new SilentAudioGenerator(firstInputFormat);
silentAudioGenerator = new SilentAudioGenerator(firstInputFormat, forceAudioTrackDurationUs);
} else {
silentAudioGenerator = null;
}
availableInputBuffers = new ConcurrentLinkedDeque<>(); availableInputBuffers = new ConcurrentLinkedDeque<>();
ByteBuffer emptyBuffer = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()); ByteBuffer emptyBuffer = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder());
for (int i = 0; i < MAX_INPUT_BUFFER_COUNT; i++) { for (int i = 0; i < MAX_INPUT_BUFFER_COUNT; i++) {
@ -150,20 +143,30 @@ import org.checkerframework.dataflow.qual.Pure;
@Override @Override
public void onMediaItemChanged( public void onMediaItemChanged(
EditedMediaItem editedMediaItem, Format trackFormat, long mediaItemOffsetUs) { EditedMediaItem editedMediaItem,
this.mediaItemOffsetUs = mediaItemOffsetUs; long durationUs,
@Nullable Format trackFormat,
boolean isLast) {
if (trackFormat == null) {
silentAudioGenerator.addSilence(durationUs);
if (isLast) {
queueEndOfStreamAfterSilence = true;
}
}
} }
@Override @Override
@Nullable @Nullable
public DecoderInputBuffer getInputBuffer() { public DecoderInputBuffer getInputBuffer() {
if (shouldGenerateSilence()) {
return null;
}
return availableInputBuffers.peek(); return availableInputBuffers.peek();
} }
@Override @Override
public void queueInputBuffer() { public void queueInputBuffer() {
DecoderInputBuffer inputBuffer = availableInputBuffers.remove(); DecoderInputBuffer inputBuffer = availableInputBuffers.remove();
inputBuffer.timeUs += mediaItemOffsetUs;
pendingInputBuffers.add(inputBuffer); pendingInputBuffers.add(inputBuffer);
} }
@ -220,16 +223,17 @@ import org.checkerframework.dataflow.qual.Pure;
return false; return false;
} }
if (isInputSilent()) { if (shouldGenerateSilence()) {
if (silentAudioGenerator.isEnded()) {
queueEndOfStreamToEncoder();
return false;
}
feedEncoder(silentAudioGenerator.getBuffer()); feedEncoder(silentAudioGenerator.getBuffer());
return true; return true;
} }
if (pendingInputBuffers.isEmpty()) { if (pendingInputBuffers.isEmpty()) {
// Only read volatile variable queueEndOfStreamAfterSilence if there is a chance that end of
// stream should be queued.
if (!silentAudioGenerator.hasRemaining() && queueEndOfStreamAfterSilence) {
queueEndOfStreamToEncoder();
}
return false; return false;
} }
@ -277,17 +281,18 @@ import org.checkerframework.dataflow.qual.Pure;
* @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 feedProcessingPipelineFromInput() { private boolean feedProcessingPipelineFromInput() {
if (isInputSilent()) { if (shouldGenerateSilence()) {
if (silentAudioGenerator.isEnded()) {
audioProcessingPipeline.queueEndOfStream();
return false;
}
ByteBuffer inputData = silentAudioGenerator.getBuffer(); ByteBuffer inputData = silentAudioGenerator.getBuffer();
audioProcessingPipeline.queueInput(inputData); audioProcessingPipeline.queueInput(inputData);
return !inputData.hasRemaining(); return !inputData.hasRemaining();
} }
if (pendingInputBuffers.isEmpty()) { if (pendingInputBuffers.isEmpty()) {
// Only read volatile variable queueEndOfStreamAfterSilence if there is a chance that end of
// stream should be queued.
if (!silentAudioGenerator.hasRemaining() && queueEndOfStreamAfterSilence) {
audioProcessingPipeline.queueEndOfStream();
}
return false; return false;
} }
@ -370,8 +375,7 @@ import org.checkerframework.dataflow.qual.Pure;
nextEncoderInputBufferTimeUs += bufferDurationUs; nextEncoderInputBufferTimeUs += bufferDurationUs;
} }
@EnsuresNonNullIf(expression = "silentAudioGenerator", result = true) private boolean shouldGenerateSilence() {
private boolean isInputSilent() { return silentAudioGenerator.hasRemaining() && pendingInputBuffers.isEmpty();
return silentAudioGenerator != null;
} }
} }

View File

@ -39,7 +39,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/** /**
* An {@link AssetLoader} that is composed of a sequence of non-overlapping {@linkplain AssetLoader * An {@link AssetLoader} that is composed of a sequence of non-overlapping {@linkplain AssetLoader
@ -49,13 +48,13 @@ import java.util.concurrent.atomic.AtomicLong;
private final List<EditedMediaItem> editedMediaItems; private final List<EditedMediaItem> editedMediaItems;
private final AtomicInteger currentMediaItemIndex; private final AtomicInteger currentMediaItemIndex;
private final boolean forceAudioTrack;
private final AssetLoader.Factory assetLoaderFactory; private final AssetLoader.Factory assetLoaderFactory;
private final HandlerWrapper handler; private final HandlerWrapper handler;
private final Listener compositeAssetLoaderListener; private final Listener compositeAssetLoaderListener;
private final Map<Integer, SampleConsumer> sampleConsumersByTrackType; private final Map<Integer, SampleConsumer> sampleConsumersByTrackType;
private final Map<Integer, OnMediaItemChangedListener> mediaItemChangedListenersByTrackType; private final Map<Integer, OnMediaItemChangedListener> mediaItemChangedListenersByTrackType;
private final ImmutableList.Builder<ExportResult.ProcessedInput> processedInputsBuilder; private final ImmutableList.Builder<ExportResult.ProcessedInput> processedInputsBuilder;
private final AtomicLong totalDurationUs;
private final AtomicInteger nonEndedTracks; private final AtomicInteger nonEndedTracks;
private AssetLoader currentAssetLoader; private AssetLoader currentAssetLoader;
@ -65,11 +64,13 @@ import java.util.concurrent.atomic.AtomicLong;
public CompositeAssetLoader( public CompositeAssetLoader(
EditedMediaItemSequence sequence, EditedMediaItemSequence sequence,
boolean forceAudioTrack,
AssetLoader.Factory assetLoaderFactory, AssetLoader.Factory assetLoaderFactory,
Looper looper, Looper looper,
Listener listener, Listener listener,
Clock clock) { Clock clock) {
this.editedMediaItems = sequence.editedMediaItems; editedMediaItems = sequence.editedMediaItems;
this.forceAudioTrack = forceAudioTrack;
this.assetLoaderFactory = assetLoaderFactory; this.assetLoaderFactory = assetLoaderFactory;
compositeAssetLoaderListener = listener; compositeAssetLoaderListener = listener;
currentMediaItemIndex = new AtomicInteger(); currentMediaItemIndex = new AtomicInteger();
@ -77,7 +78,6 @@ import java.util.concurrent.atomic.AtomicLong;
sampleConsumersByTrackType = new HashMap<>(); sampleConsumersByTrackType = new HashMap<>();
mediaItemChangedListenersByTrackType = new HashMap<>(); mediaItemChangedListenersByTrackType = new HashMap<>();
processedInputsBuilder = new ImmutableList.Builder<>(); processedInputsBuilder = new ImmutableList.Builder<>();
totalDurationUs = new AtomicLong();
nonEndedTracks = new AtomicInteger(); nonEndedTracks = new AtomicInteger();
// It's safe to use "this" because we don't start the AssetLoader before exiting the // It's safe to use "this" because we don't start the AssetLoader before exiting the
// constructor. // constructor.
@ -145,26 +145,24 @@ import java.util.concurrent.atomic.AtomicLong;
@Override @Override
public void onDurationUs(long durationUs) { public void onDurationUs(long durationUs) {
int currentMediaItemIndex = this.currentMediaItemIndex.get();
checkArgument(
durationUs != C.TIME_UNSET || currentMediaItemIndex == editedMediaItems.size() - 1,
"Could not retrieve the duration for EditedMediaItem "
+ currentMediaItemIndex
+ ". An unset duration is only allowed for the last EditedMediaItem in the sequence.");
currentDurationUs = durationUs; currentDurationUs = durationUs;
if (editedMediaItems.size() == 1) { if (editedMediaItems.size() == 1) {
compositeAssetLoaderListener.onDurationUs(durationUs); compositeAssetLoaderListener.onDurationUs(durationUs);
} else if (currentMediaItemIndex.get() == 0) { } else if (currentMediaItemIndex == 0) {
// TODO(b/252537210): support silent audio track for sequence of AssetLoaders (silent audio
// track is the only usage of the duration).
compositeAssetLoaderListener.onDurationUs(C.TIME_UNSET); compositeAssetLoaderListener.onDurationUs(C.TIME_UNSET);
} }
} }
@Override @Override
public void onTrackCount(int trackCount) { public void onTrackCount(int trackCount) {
nonEndedTracks.set(trackCount);
// TODO(b/252537210): support varying track count and track types between AssetLoaders. // TODO(b/252537210): support varying track count and track types between AssetLoaders.
if (currentMediaItemIndex.get() == 0) { nonEndedTracks.set(trackCount);
compositeAssetLoaderListener.onTrackCount(trackCount);
} else if (trackCount != sampleConsumersByTrackType.size()) {
throw new IllegalStateException(
"The number of tracks is not allowed to change between MediaItems.");
}
} }
@Override @Override
@ -177,25 +175,45 @@ import java.util.concurrent.atomic.AtomicLong;
int trackType = MimeTypes.getTrackType(format.sampleMimeType); int trackType = MimeTypes.getTrackType(format.sampleMimeType);
SampleConsumer sampleConsumer; SampleConsumer sampleConsumer;
if (currentMediaItemIndex.get() == 0) { if (currentMediaItemIndex.get() == 0) {
boolean addAudioTrack =
forceAudioTrack && nonEndedTracks.get() == 1 && trackType == C.TRACK_TYPE_VIDEO;
int trackCount = nonEndedTracks.get() + (addAudioTrack ? 1 : 0);
compositeAssetLoaderListener.onTrackCount(trackCount);
sampleConsumer = sampleConsumer =
new SampleConsumerWrapper( new SampleConsumerWrapper(
compositeAssetLoaderListener.onTrackAdded( compositeAssetLoaderListener.onTrackAdded(
format, supportedOutputTypes, streamStartPositionUs, streamOffsetUs)); format, supportedOutputTypes, streamStartPositionUs, streamOffsetUs));
sampleConsumersByTrackType.put(trackType, sampleConsumer); sampleConsumersByTrackType.put(trackType, sampleConsumer);
if (addAudioTrack) {
Format firstAudioFormat =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_AAC)
.setSampleRate(44100)
.setChannelCount(2)
.build();
SampleConsumer audioSampleConsumer =
new SampleConsumerWrapper(
compositeAssetLoaderListener.onTrackAdded(
firstAudioFormat,
SUPPORTED_OUTPUT_TYPE_DECODED,
/* streamStartPositionUs= */ streamOffsetUs,
streamOffsetUs));
sampleConsumersByTrackType.put(C.TRACK_TYPE_AUDIO, audioSampleConsumer);
}
} else { } else {
sampleConsumer = sampleConsumer =
checkStateNotNull( checkStateNotNull(
sampleConsumersByTrackType.get(trackType), sampleConsumersByTrackType.get(trackType),
"The preceding MediaItem does not contain any track of type " + trackType); "The preceding MediaItem does not contain any track of type " + trackType);
} }
@Nullable onMediaItemChanged(trackType, format);
OnMediaItemChangedListener onMediaItemChangedListener = if (nonEndedTracks.get() == 1 && sampleConsumersByTrackType.size() == 2) {
mediaItemChangedListenersByTrackType.get(trackType); for (Map.Entry<Integer, SampleConsumer> entry : sampleConsumersByTrackType.entrySet()) {
if (onMediaItemChangedListener != null) { int listenerTrackType = entry.getKey();
onMediaItemChangedListener.onMediaItemChanged( if (trackType != listenerTrackType) {
editedMediaItems.get(currentMediaItemIndex.get()), onMediaItemChanged(listenerTrackType, /* format= */ null);
format, }
/* mediaItemOffsetUs= */ totalDurationUs.get()); }
} }
return sampleConsumer; return sampleConsumer;
} }
@ -205,6 +223,20 @@ import java.util.concurrent.atomic.AtomicLong;
compositeAssetLoaderListener.onError(exportException); compositeAssetLoaderListener.onError(exportException);
} }
private void onMediaItemChanged(int trackType, @Nullable Format format) {
@Nullable
OnMediaItemChangedListener onMediaItemChangedListener =
mediaItemChangedListenersByTrackType.get(trackType);
if (onMediaItemChangedListener == null) {
return;
}
onMediaItemChangedListener.onMediaItemChanged(
editedMediaItems.get(currentMediaItemIndex.get()),
currentDurationUs,
format,
/* isLast= */ currentMediaItemIndex.get() == editedMediaItems.size() - 1);
}
private void addCurrentProcessedInput() { private void addCurrentProcessedInput() {
int currentMediaItemIndex = this.currentMediaItemIndex.get(); int currentMediaItemIndex = this.currentMediaItemIndex.get();
if (currentMediaItemIndex >= processedInputsSize) { if (currentMediaItemIndex >= processedInputsSize) {
@ -258,8 +290,8 @@ import java.util.concurrent.atomic.AtomicLong;
sampleConsumer.queueInputBuffer(); sampleConsumer.queueInputBuffer();
} }
// TODO(262693274): Test that concatenate 2 images or an image and a video works as expected // TODO(b/262693274): Test that concatenate 2 images or an image and a video works as expected
// once Image Asset Loader Implementation is complete. // once ImageAssetLoader implementation is complete.
@Override @Override
public void queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) { public void queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) {
sampleConsumer.queueInputBitmap(inputBitmap, durationUs, frameRate); sampleConsumer.queueInputBitmap(inputBitmap, durationUs, frameRate);
@ -298,7 +330,6 @@ import java.util.concurrent.atomic.AtomicLong;
} }
private void switchAssetLoader() { private void switchAssetLoader() {
totalDurationUs.addAndGet(currentDurationUs);
handler.post( handler.post(
() -> { () -> {
addCurrentProcessedInput(); addCurrentProcessedInput();

View File

@ -25,6 +25,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicLong;
/** Pipeline that muxes encoded samples without any transcoding or transformation. */ /** Pipeline that muxes encoded samples without any transcoding or transformation. */
/* package */ final class EncodedSamplePipeline extends SamplePipeline { /* package */ final class EncodedSamplePipeline extends SamplePipeline {
@ -32,6 +33,7 @@ import java.util.concurrent.ConcurrentLinkedDeque;
private static final int MAX_INPUT_BUFFER_COUNT = 10; private static final int MAX_INPUT_BUFFER_COUNT = 10;
private final Format format; private final Format format;
private final AtomicLong nextMediaItemOffsetUs;
private final Queue<DecoderInputBuffer> availableInputBuffers; private final Queue<DecoderInputBuffer> availableInputBuffers;
private final Queue<DecoderInputBuffer> pendingInputBuffers; private final Queue<DecoderInputBuffer> pendingInputBuffers;
@ -46,6 +48,7 @@ import java.util.concurrent.ConcurrentLinkedDeque;
FallbackListener fallbackListener) { FallbackListener fallbackListener) {
super(format, streamStartPositionUs, muxerWrapper); super(format, streamStartPositionUs, muxerWrapper);
this.format = format; this.format = format;
nextMediaItemOffsetUs = new AtomicLong();
availableInputBuffers = new ConcurrentLinkedDeque<>(); availableInputBuffers = new ConcurrentLinkedDeque<>();
ByteBuffer emptyBuffer = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()); ByteBuffer emptyBuffer = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder());
for (int i = 0; i < MAX_INPUT_BUFFER_COUNT; i++) { for (int i = 0; i < MAX_INPUT_BUFFER_COUNT; i++) {
@ -59,8 +62,12 @@ import java.util.concurrent.ConcurrentLinkedDeque;
@Override @Override
public void onMediaItemChanged( public void onMediaItemChanged(
EditedMediaItem editedMediaItem, Format trackFormat, long mediaItemOffsetUs) { EditedMediaItem editedMediaItem,
this.mediaItemOffsetUs = mediaItemOffsetUs; long durationUs,
@Nullable Format trackFormat,
boolean isLast) {
mediaItemOffsetUs = nextMediaItemOffsetUs.get();
nextMediaItemOffsetUs.addAndGet(durationUs);
} }
@Override @Override

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.transformer; package com.google.android.exoplayer2.transformer;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
@ -28,11 +29,15 @@ import com.google.android.exoplayer2.MediaItem;
* <p>Can be called from any thread. * <p>Can be called from any thread.
* *
* @param editedMediaItem The {@link MediaItem} with the transformations to apply to it. * @param editedMediaItem The {@link MediaItem} with the transformations to apply to it.
* @param trackFormat The {@link Format} of the {@link EditedMediaItem} track corresponding to the * @param durationUs The duration of the {@link MediaItem}, in microseconds.
* {@link SamplePipeline}. * @param trackFormat The {@link Format} of the {@link MediaItem} track corresponding to the
* @param mediaItemOffsetUs The offset to add to the presentation timestamps of the {@link * {@link SamplePipeline}, or {@code null} if no such track was extracted.
* EditedMediaItem} samples received by the {@link SamplePipeline}, in microseconds. * @param isLast Whether the {@link MediaItem} is the last one passed to the {@link
* SamplePipeline}.
*/ */
void onMediaItemChanged( void onMediaItemChanged(
EditedMediaItem editedMediaItem, Format trackFormat, long mediaItemOffsetUs); EditedMediaItem editedMediaItem,
long durationUs,
@Nullable Format trackFormat,
boolean isLast);
} }

View File

@ -21,28 +21,43 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.concurrent.atomic.AtomicLong;
/* package */ final class SilentAudioGenerator { /* package */ final class SilentAudioGenerator {
private static final int DEFAULT_BUFFER_SIZE_FRAMES = 1024; private static final int DEFAULT_BUFFER_SIZE_FRAMES = 1024;
private final int sampleRate;
private final int frameSize;
private final ByteBuffer internalBuffer; private final ByteBuffer internalBuffer;
private final AtomicLong remainingBytesToOutput;
private long remainingBytesToOutput; public SilentAudioGenerator(Format format) {
sampleRate = format.sampleRate;
public SilentAudioGenerator(Format format, long totalDurationUs) { frameSize =
int frameSize =
Util.getPcmFrameSize( Util.getPcmFrameSize(
format.pcmEncoding == Format.NO_VALUE ? C.ENCODING_PCM_16BIT : format.pcmEncoding, format.pcmEncoding == Format.NO_VALUE ? C.ENCODING_PCM_16BIT : format.pcmEncoding,
format.channelCount); format.channelCount);
long outputFrameCount = (format.sampleRate * totalDurationUs) / C.MICROS_PER_SECOND;
remainingBytesToOutput = frameSize * outputFrameCount;
internalBuffer = internalBuffer =
ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE_FRAMES * frameSize) ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE_FRAMES * frameSize)
.order(ByteOrder.nativeOrder()); .order(ByteOrder.nativeOrder());
internalBuffer.flip(); internalBuffer.flip();
remainingBytesToOutput = new AtomicLong();
}
/**
* Adds a silence duration to generate.
*
* <p>Can be called from any thread.
*
* @param durationUs The duration of the additional silence to generate, in microseconds.
*/
public void addSilence(long durationUs) {
long outputFrameCount = (sampleRate * durationUs) / C.MICROS_PER_SECOND;
remainingBytesToOutput.addAndGet(frameSize * outputFrameCount);
} }
public ByteBuffer getBuffer() { public ByteBuffer getBuffer() {
long remainingBytesToOutput = this.remainingBytesToOutput.get();
if (!internalBuffer.hasRemaining()) { if (!internalBuffer.hasRemaining()) {
// "next" buffer. // "next" buffer.
internalBuffer.clear(); internalBuffer.clear();
@ -50,12 +65,12 @@ import java.nio.ByteOrder;
internalBuffer.limit((int) remainingBytesToOutput); internalBuffer.limit((int) remainingBytesToOutput);
} }
// Only reduce remaining bytes when we "generate" a new one. // Only reduce remaining bytes when we "generate" a new one.
remainingBytesToOutput -= internalBuffer.remaining(); this.remainingBytesToOutput.addAndGet(-internalBuffer.remaining());
} }
return internalBuffer; return internalBuffer;
} }
public boolean isEnded() { public boolean hasRemaining() {
return !internalBuffer.hasRemaining() && remainingBytesToOutput == 0; return internalBuffer.hasRemaining() || remainingBytesToOutput.get() > 0;
} }
} }

View File

@ -100,7 +100,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final ConditionVariable transformerConditionVariable; private final ConditionVariable transformerConditionVariable;
private final ExportResult.Builder exportResultBuilder; private final ExportResult.Builder exportResultBuilder;
private boolean forceAudioTrack;
private boolean isDrainingPipelines; private boolean isDrainingPipelines;
private @Transformer.ProgressState int progressState; private @Transformer.ProgressState int progressState;
private @MonotonicNonNull RuntimeException cancelException; private @MonotonicNonNull RuntimeException cancelException;
@ -123,7 +122,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Clock clock) { Clock clock) {
this.context = context; this.context = context;
this.transformationRequest = transformationRequest; this.transformationRequest = transformationRequest;
this.forceAudioTrack = composition.experimentalForceAudioTrack;
this.encoderFactory = new CapturingEncoderFactory(encoderFactory); this.encoderFactory = new CapturingEncoderFactory(encoderFactory);
this.listener = listener; this.listener = listener;
this.applicationHandler = applicationHandler; this.applicationHandler = applicationHandler;
@ -137,7 +135,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
new ComponentListener(sequence, transmux, fallbackListener); new ComponentListener(sequence, transmux, fallbackListener);
compositeAssetLoader = compositeAssetLoader =
new CompositeAssetLoader( new CompositeAssetLoader(
sequence, assetLoaderFactory, internalLooper, componentListener, clock); sequence,
composition.experimentalForceAudioTrack,
assetLoaderFactory,
internalLooper,
componentListener,
clock);
samplePipelines = new ArrayList<>(); samplePipelines = new ArrayList<>();
muxerWrapper = new MuxerWrapper(outputPath, muxerFactory, componentListener); muxerWrapper = new MuxerWrapper(outputPath, muxerFactory, componentListener);
transformerConditionVariable = new ConditionVariable(); transformerConditionVariable = new ConditionVariable();
@ -316,8 +319,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private boolean trackAdded; private boolean trackAdded;
private volatile long durationUs;
public ComponentListener( public ComponentListener(
EditedMediaItemSequence sequence, boolean transmux, FallbackListener fallbackListener) { EditedMediaItemSequence sequence, boolean transmux, FallbackListener fallbackListener) {
firstEditedMediaItem = sequence.editedMediaItems.get(0); firstEditedMediaItem = sequence.editedMediaItems.get(0);
@ -325,7 +326,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.transmux = transmux; this.transmux = transmux;
this.fallbackListener = fallbackListener; this.fallbackListener = fallbackListener;
trackCount = new AtomicInteger(); trackCount = new AtomicInteger();
durationUs = C.TIME_UNSET;
} }
// AssetLoader.Listener and MuxerWrapper.Listener implementation. // AssetLoader.Listener and MuxerWrapper.Listener implementation.
@ -340,9 +340,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// AssetLoader.Listener implementation. // AssetLoader.Listener implementation.
@Override @Override
public void onDurationUs(long durationUs) { public void onDurationUs(long durationUs) {}
this.durationUs = durationUs;
}
@Override @Override
public void onTrackCount(int trackCount) { public void onTrackCount(int trackCount) {
@ -365,14 +363,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
throws ExportException { throws ExportException {
int trackType = MimeTypes.getTrackType(firstInputFormat.sampleMimeType); int trackType = MimeTypes.getTrackType(firstInputFormat.sampleMimeType);
if (!trackAdded) { if (!trackAdded) {
if (forceAudioTrack) {
if (trackCount.get() == 1 && trackType == C.TRACK_TYPE_VIDEO) {
trackCount.incrementAndGet();
} else {
forceAudioTrack = false;
}
}
// Call setTrackCount() methods here so that they are called from the same thread as the // Call setTrackCount() methods here so that they are called from the same thread as the
// MuxerWrapper and FallbackListener methods called when building the sample pipelines. // MuxerWrapper and FallbackListener methods called when building the sample pipelines.
muxerWrapper.setTrackCount(trackCount.get()); muxerWrapper.setTrackCount(trackCount.get());
@ -386,25 +376,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
compositeAssetLoader.addOnMediaItemChangedListener(samplePipeline, trackType); compositeAssetLoader.addOnMediaItemChangedListener(samplePipeline, trackType);
internalHandler.obtainMessage(MSG_REGISTER_SAMPLE_PIPELINE, samplePipeline).sendToTarget(); internalHandler.obtainMessage(MSG_REGISTER_SAMPLE_PIPELINE, samplePipeline).sendToTarget();
if (forceAudioTrack) {
Format silentAudioFormat =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_AAC)
.setSampleRate(44100)
.setChannelCount(2)
.build();
SamplePipeline audioSamplePipeline =
getSamplePipeline(
silentAudioFormat,
SUPPORTED_OUTPUT_TYPE_DECODED,
streamStartPositionUs,
streamOffsetUs);
compositeAssetLoader.addOnMediaItemChangedListener(audioSamplePipeline, C.TRACK_TYPE_AUDIO);
internalHandler
.obtainMessage(MSG_REGISTER_SAMPLE_PIPELINE, audioSamplePipeline)
.sendToTarget();
}
return samplePipeline; return samplePipeline;
} }
@ -478,7 +449,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
transformationRequest, transformationRequest,
firstEditedMediaItem.flattenForSlowMotion, firstEditedMediaItem.flattenForSlowMotion,
firstEditedMediaItem.effects.audioProcessors, firstEditedMediaItem.effects.audioProcessors,
forceAudioTrack ? durationUs : C.TIME_UNSET,
encoderFactory, encoderFactory,
muxerWrapper, muxerWrapper,
fallbackListener); fallbackListener);
@ -524,9 +494,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
if (!firstEditedMediaItem.effects.audioProcessors.isEmpty()) { if (!firstEditedMediaItem.effects.audioProcessors.isEmpty()) {
return true; return true;
} }
if (forceAudioTrack) {
return true;
}
return false; return false;
} }

View File

@ -47,6 +47,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.Pure;
@ -56,10 +57,10 @@ import org.checkerframework.dataflow.qual.Pure;
/** MIME type to use for output video if the input type is not a video. */ /** MIME type to use for output video if the input type is not a video. */
private static final String DEFAULT_OUTPUT_MIME_TYPE = MimeTypes.VIDEO_H265; private static final String DEFAULT_OUTPUT_MIME_TYPE = MimeTypes.VIDEO_H265;
private final AtomicLong mediaItemOffsetUs;
private final VideoFrameProcessor videoFrameProcessor; private final VideoFrameProcessor videoFrameProcessor;
private final ColorInfo videoFrameProcessorInputColor; private final ColorInfo videoFrameProcessorInputColor;
private final FrameInfo firstFrameInfo; private final FrameInfo firstFrameInfo;
private final EncoderWrapper encoderWrapper; private final EncoderWrapper encoderWrapper;
private final DecoderInputBuffer encoderOutputBuffer; private final DecoderInputBuffer encoderOutputBuffer;
@ -85,6 +86,7 @@ import org.checkerframework.dataflow.qual.Pure;
throws ExportException { throws ExportException {
super(firstInputFormat, streamStartPositionUs, muxerWrapper); super(firstInputFormat, streamStartPositionUs, muxerWrapper);
mediaItemOffsetUs = new AtomicLong();
finalFramePresentationTimeUs = C.TIME_UNSET; finalFramePresentationTimeUs = C.TIME_UNSET;
encoderOutputBuffer = encoderOutputBuffer =
@ -187,9 +189,13 @@ import org.checkerframework.dataflow.qual.Pure;
@Override @Override
public void onMediaItemChanged( public void onMediaItemChanged(
EditedMediaItem editedMediaItem, Format trackFormat, long mediaItemOffsetUs) { EditedMediaItem editedMediaItem,
long durationUs,
@Nullable Format trackFormat,
boolean isLast) {
videoFrameProcessor.setInputFrameInfo( videoFrameProcessor.setInputFrameInfo(
new FrameInfo.Builder(firstFrameInfo).setOffsetToAddUs(mediaItemOffsetUs).build()); new FrameInfo.Builder(firstFrameInfo).setOffsetToAddUs(mediaItemOffsetUs.get()).build());
mediaItemOffsetUs.addAndGet(durationUs);
} }
@Override @Override

View File

@ -29,27 +29,41 @@ import org.junit.runner.RunWith;
public class SilentAudioGeneratorTest { public class SilentAudioGeneratorTest {
@Test @Test
public void numberOfBytesProduced_isCorrect() { public void addSilenceOnce_numberOfBytesProduced_isCorrect() {
SilentAudioGenerator generator = SilentAudioGenerator generator =
new SilentAudioGenerator( new SilentAudioGenerator(
new Format.Builder() new Format.Builder()
.setSampleRate(88_200) .setSampleRate(88_200)
.setPcmEncoding(C.ENCODING_PCM_16BIT) .setPcmEncoding(C.ENCODING_PCM_16BIT)
.setChannelCount(6) .setChannelCount(6)
.build(), .build());
/* totalDurationUs= */ 3_000_000);
int bytesOutput = 0; generator.addSilence(/* durationUs= */ 3_000_000);
while (!generator.isEnded()) { int bytesOutput = drainGenerator(generator);
ByteBuffer output = generator.getBuffer();
bytesOutput += output.remaining();
// "Consume" buffer.
output.position(output.limit());
}
// 88_200 * 12 * 3s = 3175200 // 88_200 * 12 * 3s = 3175200
assertThat(bytesOutput).isEqualTo(3_175_200); assertThat(bytesOutput).isEqualTo(3_175_200);
} }
@Test
public void addSilenceTwice_numberOfBytesProduced_isCorrect() {
SilentAudioGenerator generator =
new SilentAudioGenerator(
new Format.Builder()
.setSampleRate(88_200)
.setPcmEncoding(C.ENCODING_PCM_16BIT)
.setChannelCount(6)
.build());
generator.addSilence(/* durationUs= */ 3_000_000);
int bytesOutput = drainGenerator(generator);
generator.addSilence(/* durationUs= */ 1_500_000);
bytesOutput += drainGenerator(generator);
// 88_200 * 12 * 4.5s = 4_762_800
assertThat(bytesOutput).isEqualTo(4_762_800);
}
@Test @Test
public void lastBufferProduced_isCorrectSize() { public void lastBufferProduced_isCorrectSize() {
SilentAudioGenerator generator = SilentAudioGenerator generator =
@ -58,11 +72,11 @@ public class SilentAudioGeneratorTest {
.setSampleRate(44_100) .setSampleRate(44_100)
.setPcmEncoding(C.ENCODING_PCM_16BIT) .setPcmEncoding(C.ENCODING_PCM_16BIT)
.setChannelCount(2) .setChannelCount(2)
.build(), .build());
/* totalDurationUs= */ 1_000_000); generator.addSilence(/* durationUs= */ 1_000_000);
int currentBufferSize = 0; int currentBufferSize = 0;
while (!generator.isEnded()) { while (generator.hasRemaining()) {
ByteBuffer output = generator.getBuffer(); ByteBuffer output = generator.getBuffer();
currentBufferSize = output.remaining(); currentBufferSize = output.remaining();
// "Consume" buffer. // "Consume" buffer.
@ -82,9 +96,23 @@ public class SilentAudioGeneratorTest {
.setSampleRate(48_000) .setSampleRate(48_000)
.setPcmEncoding(C.ENCODING_PCM_16BIT) .setPcmEncoding(C.ENCODING_PCM_16BIT)
.setChannelCount(2) .setChannelCount(2)
.build(), .build());
/* totalDurationUs= */ 5_000);
generator.addSilence(/* durationUs= */ 5_000);
// 5_000 * 48_000 * 4 / 1_000_000 = 960 // 5_000 * 48_000 * 4 / 1_000_000 = 960
assertThat(generator.getBuffer().remaining()).isEqualTo(960); assertThat(generator.getBuffer().remaining()).isEqualTo(960);
} }
/** Drains the generator and returns the number of bytes output. */
private static int drainGenerator(SilentAudioGenerator generator) {
int bytesOutput = 0;
while (generator.hasRemaining()) {
ByteBuffer output = generator.getBuffer();
bytesOutput += output.remaining();
// "Consume" buffer.
output.position(output.limit());
}
return bytesOutput;
}
} }