Add support for looping sequences in SequenceAssetLoader
PiperOrigin-RevId: 517418886
This commit is contained in:
parent
e54c7a7c79
commit
d41d025803
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user