Add support for looping sequences in SequenceAssetLoader

PiperOrigin-RevId: 517418886
This commit is contained in:
kimvde 2023-03-17 15:26:53 +00:00 committed by microkatz
parent e54c7a7c79
commit d41d025803
3 changed files with 116 additions and 31 deletions

View File

@ -153,6 +153,9 @@ import org.checkerframework.dataflow.qual.Pure;
@Nullable Format trackFormat, @Nullable Format trackFormat,
boolean isLast) { boolean isLast) {
if (trackFormat == null) { if (trackFormat == null) {
checkState(
durationUs != C.TIME_UNSET,
"Could not generate silent audio because duration is unknown.");
silentAudioGenerator.addSilence(durationUs); silentAudioGenerator.addSilence(durationUs);
if (isLast) { if (isLast) {
queueEndOfStreamAfterSilence = true; queueEndOfStreamAfterSilence = true;

View File

@ -36,6 +36,8 @@ public final class EditedMediaItemSequence {
*/ */
public final ImmutableList<EditedMediaItem> editedMediaItems; public final ImmutableList<EditedMediaItem> editedMediaItems;
/* package */ final boolean isLooping;
/** /**
* Creates an instance. * Creates an instance.
* *
@ -44,5 +46,6 @@ public final class EditedMediaItemSequence {
public EditedMediaItemSequence(List<EditedMediaItem> editedMediaItems) { public EditedMediaItemSequence(List<EditedMediaItem> editedMediaItems) {
checkArgument(!editedMediaItems.isEmpty()); checkArgument(!editedMediaItems.isEmpty());
this.editedMediaItems = ImmutableList.copyOf(editedMediaItems); this.editedMediaItems = ImmutableList.copyOf(editedMediaItems);
isLooping = false;
} }
} }

View File

@ -57,7 +57,7 @@ import java.util.concurrent.atomic.AtomicInteger;
.build(); .build();
private final List<EditedMediaItem> editedMediaItems; private final List<EditedMediaItem> editedMediaItems;
private final AtomicInteger currentMediaItemIndex; private final boolean isLooping;
private final boolean forceAudioTrack; private final boolean forceAudioTrack;
private final AssetLoader.Factory assetLoaderFactory; private final AssetLoader.Factory assetLoaderFactory;
private final HandlerWrapper handler; private final HandlerWrapper handler;
@ -80,13 +80,22 @@ import java.util.concurrent.atomic.AtomicInteger;
private final ImmutableList.Builder<ExportResult.ProcessedInput> processedInputsBuilder; private final ImmutableList.Builder<ExportResult.ProcessedInput> processedInputsBuilder;
private final AtomicInteger nonEndedTracks; private final AtomicInteger nonEndedTracks;
private boolean isCurrentAssetFirstAsset;
private int currentMediaItemIndex;
private AssetLoader currentAssetLoader; private AssetLoader currentAssetLoader;
private boolean trackCountReported; private boolean trackCountReported;
private int processedInputsSize; private long currentAssetStartTimeUs;
private boolean decodeAudio; private boolean decodeAudio;
private boolean decodeVideo; private boolean decodeVideo;
private long totalDurationUs;
private long maxSequenceDurationUs;
private boolean isMaxSequenceDurationUsFinal;
private int sequenceLoopCount;
private boolean audioLoopingEnded;
private boolean videoLoopingEnded;
private int processedInputsSize;
private volatile long currentDurationUs; private volatile long currentAssetDurationUs;
public SequenceAssetLoader( public SequenceAssetLoader(
EditedMediaItemSequence sequence, EditedMediaItemSequence sequence,
@ -96,15 +105,16 @@ import java.util.concurrent.atomic.AtomicInteger;
Listener listener, Listener listener,
Clock clock) { Clock clock) {
editedMediaItems = sequence.editedMediaItems; editedMediaItems = sequence.editedMediaItems;
isLooping = sequence.isLooping;
this.forceAudioTrack = forceAudioTrack; this.forceAudioTrack = forceAudioTrack;
this.assetLoaderFactory = assetLoaderFactory; this.assetLoaderFactory = assetLoaderFactory;
sequenceAssetLoaderListener = listener; sequenceAssetLoaderListener = listener;
currentMediaItemIndex = new AtomicInteger();
handler = clock.createHandler(looper, /* callback= */ null); handler = clock.createHandler(looper, /* callback= */ null);
sampleConsumersByTrackType = new HashMap<>(); sampleConsumersByTrackType = new HashMap<>();
mediaItemChangedListenersByTrackType = new ConcurrentHashMap<>(); mediaItemChangedListenersByTrackType = new ConcurrentHashMap<>();
processedInputsBuilder = new ImmutableList.Builder<>(); processedInputsBuilder = new ImmutableList.Builder<>();
nonEndedTracks = new AtomicInteger(); nonEndedTracks = new AtomicInteger();
isCurrentAssetFirstAsset = true;
// 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.
@SuppressWarnings("nullness:argument.type.incompatible") @SuppressWarnings("nullness:argument.type.incompatible")
@ -116,17 +126,23 @@ import java.util.concurrent.atomic.AtomicInteger;
@Override @Override
public void start() { public void start() {
currentAssetLoader.start(); currentAssetLoader.start();
if (editedMediaItems.size() > 1 || isLooping) {
sequenceAssetLoaderListener.onDurationUs(C.TIME_UNSET);
}
} }
@Override @Override
public @Transformer.ProgressState int getProgress(ProgressHolder progressHolder) { public @Transformer.ProgressState int getProgress(ProgressHolder progressHolder) {
if (isLooping) {
return Transformer.PROGRESS_STATE_UNAVAILABLE;
}
int progressState = currentAssetLoader.getProgress(progressHolder); int progressState = currentAssetLoader.getProgress(progressHolder);
int mediaItemCount = editedMediaItems.size(); int mediaItemCount = editedMediaItems.size();
if (mediaItemCount == 1 || progressState == PROGRESS_STATE_NOT_STARTED) { if (mediaItemCount == 1 || progressState == PROGRESS_STATE_NOT_STARTED) {
return progressState; return progressState;
} }
int progress = currentMediaItemIndex.get() * 100 / mediaItemCount; int progress = currentMediaItemIndex * 100 / mediaItemCount;
if (progressState == PROGRESS_STATE_AVAILABLE) { if (progressState == PROGRESS_STATE_AVAILABLE) {
progress += progressHolder.progress / mediaItemCount; progress += progressHolder.progress / mediaItemCount;
} }
@ -170,21 +186,35 @@ import java.util.concurrent.atomic.AtomicInteger;
mediaItemChangedListenersByTrackType.put(trackType, onMediaItemChangedListener); mediaItemChangedListenersByTrackType.put(trackType, onMediaItemChangedListener);
} }
/**
* Sets the maximum {@link EditedMediaItemSequence} duration in the {@link Composition}.
*
* <p>The duration passed is the current maximum duration. This method can be called multiple
* times as this duration increases. Indeed, a sequence duration will increase during an export
* when a new {@link MediaItem} is loaded, which can increase the maximum sequence duration.
*
* <p>Must be called from the thread used by the current {@link AssetLoader} to pass data to the
* {@link SampleConsumer}.
*
* @param maxSequenceDurationUs The current maximum sequence duration, in microseconds.
* @param isFinal Whether the duration passed is final. Setting this value to {@code true} means
* that the duration passed will not change anymore during the entire export.
*/
public void setMaxSequenceDurationUs(long maxSequenceDurationUs, boolean isFinal) {
this.maxSequenceDurationUs = maxSequenceDurationUs;
isMaxSequenceDurationUsFinal = isFinal;
}
// AssetLoader.Listener implementation. // AssetLoader.Listener implementation.
@Override @Override
public void onDurationUs(long durationUs) { public void onDurationUs(long durationUs) {
int currentMediaItemIndex = this.currentMediaItemIndex.get();
checkArgument( checkArgument(
durationUs != C.TIME_UNSET || currentMediaItemIndex == editedMediaItems.size() - 1, durationUs != C.TIME_UNSET || currentMediaItemIndex == editedMediaItems.size() - 1,
"Could not retrieve the duration for EditedMediaItem " "Could not retrieve required duration for EditedMediaItem " + currentMediaItemIndex);
+ currentMediaItemIndex currentAssetDurationUs = durationUs;
+ ". An unset duration is only allowed for the last EditedMediaItem in the sequence."); if (editedMediaItems.size() == 1 && !isLooping) {
currentDurationUs = durationUs;
if (editedMediaItems.size() == 1) {
sequenceAssetLoaderListener.onDurationUs(durationUs); sequenceAssetLoaderListener.onDurationUs(durationUs);
} else if (currentMediaItemIndex == 0) {
sequenceAssetLoaderListener.onDurationUs(C.TIME_UNSET);
} }
} }
@ -200,9 +230,10 @@ import java.util.concurrent.atomic.AtomicInteger;
@SupportedOutputTypes int supportedOutputTypes, @SupportedOutputTypes int supportedOutputTypes,
long streamStartPositionUs, long streamStartPositionUs,
long streamOffsetUs) { long streamOffsetUs) {
boolean isAudio = getProcessedTrackType(inputFormat.sampleMimeType) == C.TRACK_TYPE_AUDIO; currentAssetStartTimeUs = streamStartPositionUs;
if (currentMediaItemIndex.get() != 0) { boolean isAudio = getProcessedTrackType(inputFormat.sampleMimeType) == C.TRACK_TYPE_AUDIO;
if (!isCurrentAssetFirstAsset) {
return isAudio ? decodeAudio : decodeVideo; return isAudio ? decodeAudio : decodeVideo;
} }
@ -240,7 +271,7 @@ import java.util.concurrent.atomic.AtomicInteger;
public SampleConsumer onOutputFormat(Format format) throws ExportException { public SampleConsumer onOutputFormat(Format format) throws ExportException {
@C.TrackType int trackType = getProcessedTrackType(format.sampleMimeType); @C.TrackType int trackType = getProcessedTrackType(format.sampleMimeType);
SampleConsumer sampleConsumer; SampleConsumer sampleConsumer;
if (currentMediaItemIndex.get() == 0) { if (isCurrentAssetFirstAsset) {
@Nullable @Nullable
SampleConsumer wrappedSampleConsumer = sequenceAssetLoaderListener.onOutputFormat(format); SampleConsumer wrappedSampleConsumer = sequenceAssetLoaderListener.onOutputFormat(format);
if (wrappedSampleConsumer == null) { if (wrappedSampleConsumer == null) {
@ -300,16 +331,18 @@ import java.util.concurrent.atomic.AtomicInteger;
return; return;
} }
onMediaItemChangedListener.onMediaItemChanged( onMediaItemChangedListener.onMediaItemChanged(
editedMediaItems.get(currentMediaItemIndex.get()), editedMediaItems.get(currentMediaItemIndex),
currentDurationUs, currentAssetDurationUs,
format, format,
/* isLast= */ currentMediaItemIndex.get() == editedMediaItems.size() - 1); /* isLast= */ currentMediaItemIndex == editedMediaItems.size() - 1);
} }
private void addCurrentProcessedInput() { private void addCurrentProcessedInput() {
int currentMediaItemIndex = this.currentMediaItemIndex.get(); if ((sequenceLoopCount * editedMediaItems.size() + currentMediaItemIndex)
if (currentMediaItemIndex >= processedInputsSize) { >= processedInputsSize) {
MediaItem mediaItem = editedMediaItems.get(currentMediaItemIndex).mediaItem; MediaItem mediaItem = editedMediaItems.get(currentMediaItemIndex).mediaItem;
// TODO(b/252537210): consider getting the decoder names via a callback to simplify the logic
// of adding processed inputs.
ImmutableMap<Integer, String> decoders = currentAssetLoader.getDecoderNames(); ImmutableMap<Integer, String> decoders = currentAssetLoader.getDecoderNames();
processedInputsBuilder.add( processedInputsBuilder.add(
new ExportResult.ProcessedInput( new ExportResult.ProcessedInput(
@ -335,9 +368,23 @@ import java.util.concurrent.atomic.AtomicInteger;
@Override @Override
public boolean queueInputBuffer() { public boolean queueInputBuffer() {
DecoderInputBuffer inputBuffer = checkStateNotNull(sampleConsumer.getInputBuffer()); DecoderInputBuffer inputBuffer = checkStateNotNull(sampleConsumer.getInputBuffer());
long globalTimestampUs = totalDurationUs + inputBuffer.timeUs - currentAssetStartTimeUs;
if (isLooping && globalTimestampUs >= maxSequenceDurationUs) {
if (isMaxSequenceDurationUsFinal && !audioLoopingEnded) {
nonEndedTracks.decrementAndGet();
checkNotNull(inputBuffer.data).limit(0);
inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
// We know that queueInputBuffer() will always return true for the underlying
// SampleConsumer so there is no need to handle the case where the sample wasn't queued.
checkState(sampleConsumer.queueInputBuffer());
audioLoopingEnded = true;
}
return false;
}
if (inputBuffer.isEndOfStream()) { if (inputBuffer.isEndOfStream()) {
nonEndedTracks.decrementAndGet(); nonEndedTracks.decrementAndGet();
if (currentMediaItemIndex.get() < editedMediaItems.size() - 1) { if (currentMediaItemIndex < editedMediaItems.size() - 1 || isLooping) {
inputBuffer.clear(); inputBuffer.clear();
inputBuffer.timeUs = 0; inputBuffer.timeUs = 0;
if (nonEndedTracks.get() == 0) { if (nonEndedTracks.get() == 0) {
@ -346,13 +393,30 @@ import java.util.concurrent.atomic.AtomicInteger;
return true; return true;
} }
} }
return sampleConsumer.queueInputBuffer();
checkState(sampleConsumer.queueInputBuffer());
return true;
} }
// TODO(b/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 ImageAssetLoader implementation is complete. // once ImageAssetLoader implementation is complete.
@Override @Override
public boolean queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) { public boolean queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) {
if (isLooping && totalDurationUs + durationUs > maxSequenceDurationUs) {
if (!isMaxSequenceDurationUsFinal) {
return false;
}
durationUs = maxSequenceDurationUs - totalDurationUs;
if (durationUs == 0) {
if (!videoLoopingEnded) {
videoLoopingEnded = true;
signalEndOfVideoInput();
}
return false;
}
videoLoopingEnded = true;
}
return sampleConsumer.queueInputBitmap(inputBitmap, durationUs, frameRate); return sampleConsumer.queueInputBitmap(inputBitmap, durationUs, frameRate);
} }
@ -373,28 +437,43 @@ import java.util.concurrent.atomic.AtomicInteger;
@Override @Override
public boolean registerVideoFrame(long presentationTimeUs) { public boolean registerVideoFrame(long presentationTimeUs) {
long globalTimestampUs = totalDurationUs + presentationTimeUs - currentAssetStartTimeUs;
if (isLooping && globalTimestampUs >= maxSequenceDurationUs) {
if (isMaxSequenceDurationUsFinal && !videoLoopingEnded) {
videoLoopingEnded = true;
signalEndOfVideoInput();
}
return false;
}
return sampleConsumer.registerVideoFrame(presentationTimeUs); return sampleConsumer.registerVideoFrame(presentationTimeUs);
} }
@Override @Override
public void signalEndOfVideoInput() { public void signalEndOfVideoInput() {
nonEndedTracks.decrementAndGet(); nonEndedTracks.decrementAndGet();
if (currentMediaItemIndex.get() < editedMediaItems.size() - 1) { boolean videoEnded =
if (nonEndedTracks.get() == 0) { isLooping ? videoLoopingEnded : currentMediaItemIndex == editedMediaItems.size() - 1;
switchAssetLoader(); if (videoEnded) {
} sampleConsumer.signalEndOfVideoInput();
return; } else if (nonEndedTracks.get() == 0) {
switchAssetLoader();
} }
sampleConsumer.signalEndOfVideoInput();
} }
private void switchAssetLoader() { private void switchAssetLoader() {
handler.post( handler.post(
() -> { () -> {
addCurrentProcessedInput(); addCurrentProcessedInput();
totalDurationUs += currentAssetDurationUs;
currentAssetLoader.release(); currentAssetLoader.release();
EditedMediaItem editedMediaItem = isCurrentAssetFirstAsset = false;
editedMediaItems.get(currentMediaItemIndex.incrementAndGet()); currentMediaItemIndex++;
if (currentMediaItemIndex == editedMediaItems.size()) {
currentMediaItemIndex = 0;
sequenceLoopCount++;
}
EditedMediaItem editedMediaItem = editedMediaItems.get(currentMediaItemIndex);
currentAssetLoader = currentAssetLoader =
assetLoaderFactory.createAssetLoader( assetLoaderFactory.createAssetLoader(
editedMediaItem, editedMediaItem,