Add safer gap based checks to Transformer API boundary points.
PiperOrigin-RevId: 678278666
This commit is contained in:
parent
8b7c8ffb86
commit
fc07ce056a
@ -337,6 +337,7 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
public void setComposition(Composition composition) {
|
public void setComposition(Composition composition) {
|
||||||
verifyApplicationThread();
|
verifyApplicationThread();
|
||||||
checkArgument(!composition.sequences.isEmpty());
|
checkArgument(!composition.sequences.isEmpty());
|
||||||
|
checkArgument(!composition.hasGaps());
|
||||||
checkState(this.composition == null);
|
checkState(this.composition == null);
|
||||||
composition = deactivateSpeedAdjustingVideoEffects(composition);
|
composition = deactivateSpeedAdjustingVideoEffects(composition);
|
||||||
|
|
||||||
|
@ -287,6 +287,10 @@ public final class EditedMediaItem {
|
|||||||
int frameRate,
|
int frameRate,
|
||||||
Effects effects) {
|
Effects effects) {
|
||||||
checkState(!removeAudio || !removeVideo, "Audio and video cannot both be removed");
|
checkState(!removeAudio || !removeVideo, "Audio and video cannot both be removed");
|
||||||
|
if (isGap(mediaItem)) {
|
||||||
|
checkArgument(durationUs != C.TIME_UNSET);
|
||||||
|
checkArgument(!removeAudio && !flattenForSlowMotion && effects.audioProcessors.isEmpty());
|
||||||
|
}
|
||||||
this.mediaItem = mediaItem;
|
this.mediaItem = mediaItem;
|
||||||
this.removeAudio = removeAudio;
|
this.removeAudio = removeAudio;
|
||||||
this.removeVideo = removeVideo;
|
this.removeVideo = removeVideo;
|
||||||
@ -349,6 +353,10 @@ public final class EditedMediaItem {
|
|||||||
* EditedMediaItemSequence.Builder#addGap(long) gap}.
|
* EditedMediaItemSequence.Builder#addGap(long) gap}.
|
||||||
*/
|
*/
|
||||||
/* package */ boolean isGap() {
|
/* package */ boolean isGap() {
|
||||||
|
return isGap(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isGap(MediaItem mediaItem) {
|
||||||
return Objects.equals(mediaItem.mediaId, GAP_MEDIA_ID);
|
return Objects.equals(mediaItem.mediaId, GAP_MEDIA_ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -605,6 +605,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
@AssetLoader.SupportedOutputTypes int supportedOutputTypes) {
|
@AssetLoader.SupportedOutputTypes int supportedOutputTypes) {
|
||||||
@C.TrackType
|
@C.TrackType
|
||||||
int trackType = getProcessedTrackType(firstAssetLoaderInputFormat.sampleMimeType);
|
int trackType = getProcessedTrackType(firstAssetLoaderInputFormat.sampleMimeType);
|
||||||
|
|
||||||
|
checkArgument(
|
||||||
|
trackType != TRACK_TYPE_VIDEO || !composition.sequences.get(sequenceIndex).hasGaps(),
|
||||||
|
"Gaps in video sequences are not supported.");
|
||||||
|
|
||||||
synchronized (assetLoaderLock) {
|
synchronized (assetLoaderLock) {
|
||||||
assetLoaderInputTracker.registerTrack(sequenceIndex, firstAssetLoaderInputFormat);
|
assetLoaderInputTracker.registerTrack(sequenceIndex, firstAssetLoaderInputFormat);
|
||||||
if (assetLoaderInputTracker.hasRegisteredAllTracks()) {
|
if (assetLoaderInputTracker.hasRegisteredAllTracks()) {
|
||||||
@ -749,6 +754,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
@GuardedBy("assetLoaderLock")
|
@GuardedBy("assetLoaderLock")
|
||||||
private void createEncodedSampleExporter(@C.TrackType int trackType) {
|
private void createEncodedSampleExporter(@C.TrackType int trackType) {
|
||||||
checkState(assetLoaderInputTracker.getSampleExporter(trackType) == null);
|
checkState(assetLoaderInputTracker.getSampleExporter(trackType) == null);
|
||||||
|
checkArgument(
|
||||||
|
trackType != TRACK_TYPE_AUDIO || !composition.sequences.get(sequenceIndex).hasGaps(),
|
||||||
|
"Gaps can not be transmuxed.");
|
||||||
assetLoaderInputTracker.registerSampleExporter(
|
assetLoaderInputTracker.registerSampleExporter(
|
||||||
trackType,
|
trackType,
|
||||||
new EncodedSampleExporter(
|
new EncodedSampleExporter(
|
||||||
|
@ -18,6 +18,7 @@ package androidx.media3.transformer;
|
|||||||
|
|
||||||
import static androidx.media3.common.ColorInfo.SDR_BT709_LIMITED;
|
import static androidx.media3.common.ColorInfo.SDR_BT709_LIMITED;
|
||||||
import static androidx.media3.common.ColorInfo.isTransferHdr;
|
import static androidx.media3.common.ColorInfo.isTransferHdr;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||||
import static androidx.media3.transformer.Composition.HDR_MODE_KEEP_HDR;
|
import static androidx.media3.transformer.Composition.HDR_MODE_KEEP_HDR;
|
||||||
import static androidx.media3.transformer.Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL;
|
import static androidx.media3.transformer.Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL;
|
||||||
import static androidx.media3.transformer.EncoderUtil.getSupportedEncodersForHdrEditing;
|
import static androidx.media3.transformer.EncoderUtil.getSupportedEncodersForHdrEditing;
|
||||||
@ -81,6 +82,8 @@ public final class TransformerUtil {
|
|||||||
MuxerWrapper muxerWrapper) {
|
MuxerWrapper muxerWrapper) {
|
||||||
if (composition.sequences.size() > 1
|
if (composition.sequences.size() > 1
|
||||||
|| composition.sequences.get(sequenceIndex).editedMediaItems.size() > 1) {
|
|| composition.sequences.get(sequenceIndex).editedMediaItems.size() > 1) {
|
||||||
|
checkArgument(
|
||||||
|
!composition.hasGaps() || !composition.transmuxAudio, "Gaps can not be transmuxed.");
|
||||||
return !composition.transmuxAudio;
|
return !composition.transmuxAudio;
|
||||||
}
|
}
|
||||||
if (composition.hasGaps()) {
|
if (composition.hasGaps()) {
|
||||||
|
@ -19,6 +19,7 @@ package androidx.media3.transformer;
|
|||||||
import static androidx.media3.common.VideoFrameProcessor.INPUT_TYPE_BITMAP;
|
import static androidx.media3.common.VideoFrameProcessor.INPUT_TYPE_BITMAP;
|
||||||
import static androidx.media3.common.VideoFrameProcessor.INPUT_TYPE_SURFACE;
|
import static androidx.media3.common.VideoFrameProcessor.INPUT_TYPE_SURFACE;
|
||||||
import static androidx.media3.common.VideoFrameProcessor.INPUT_TYPE_TEXTURE_ID;
|
import static androidx.media3.common.VideoFrameProcessor.INPUT_TYPE_TEXTURE_ID;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
@ -60,6 +61,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
|||||||
long durationUs,
|
long durationUs,
|
||||||
@Nullable Format decodedFormat,
|
@Nullable Format decodedFormat,
|
||||||
boolean isLast) {
|
boolean isLast) {
|
||||||
|
checkArgument(!editedMediaItem.isGap());
|
||||||
boolean isSurfaceAssetLoaderMediaItem = isMediaItemForSurfaceAssetLoader(editedMediaItem);
|
boolean isSurfaceAssetLoaderMediaItem = isMediaItemForSurfaceAssetLoader(editedMediaItem);
|
||||||
durationUs = editedMediaItem.getDurationAfterEffectsApplied(durationUs);
|
durationUs = editedMediaItem.getDurationAfterEffectsApplied(durationUs);
|
||||||
if (decodedFormat != null) {
|
if (decodedFormat != null) {
|
||||||
|
@ -32,8 +32,10 @@ import static androidx.media3.transformer.TestUtil.getDumpFileName;
|
|||||||
import static androidx.media3.transformer.TestUtil.getSequenceDumpFilePath;
|
import static androidx.media3.transformer.TestUtil.getSequenceDumpFilePath;
|
||||||
import static androidx.media3.transformer.TestUtil.removeEncodersAndDecoders;
|
import static androidx.media3.transformer.TestUtil.removeEncodersAndDecoders;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.audio.SonicAudioProcessor;
|
import androidx.media3.common.audio.SonicAudioProcessor;
|
||||||
@ -44,6 +46,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
@ -499,6 +502,81 @@ public final class SequenceExportTest {
|
|||||||
"silenceHighPitch"));
|
"silenceHighPitch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void transmuxAudio_itemGap_throws() throws Exception {
|
||||||
|
Transformer transformer =
|
||||||
|
createTransformerBuilder(new DefaultMuxer.Factory(), /* enableFallback= */ false).build();
|
||||||
|
EditedMediaItem audioItem =
|
||||||
|
new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW)).build();
|
||||||
|
EditedMediaItemSequence sequence =
|
||||||
|
new EditedMediaItemSequence.Builder().addItem(audioItem).addGap(500_000).build();
|
||||||
|
Composition composition = new Composition.Builder(sequence).setTransmuxAudio(true).build();
|
||||||
|
|
||||||
|
transformer.start(composition, outputDir.newFile().getPath());
|
||||||
|
|
||||||
|
ExportException exception =
|
||||||
|
assertThrows(ExportException.class, () -> TransformerTestRunner.runLooper(transformer));
|
||||||
|
assertThat(getRootCause(exception)).hasMessageThat().isEqualTo("Gaps can not be transmuxed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - b/369154363: Enable test after shouldTranscode inconsistency is resolved.
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
public void transmuxAudio_gapItem_throws() throws Exception {
|
||||||
|
Transformer transformer =
|
||||||
|
createTransformerBuilder(new DefaultMuxer.Factory(), /* enableFallback= */ false).build();
|
||||||
|
EditedMediaItem audioItem =
|
||||||
|
new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW))
|
||||||
|
.setRemoveVideo(true)
|
||||||
|
.build();
|
||||||
|
EditedMediaItemSequence sequence =
|
||||||
|
new EditedMediaItemSequence.Builder().addGap(500_000).addItem(audioItem).build();
|
||||||
|
Composition composition = new Composition.Builder(sequence).setTransmuxAudio(true).build();
|
||||||
|
|
||||||
|
transformer.start(composition, outputDir.newFile().getPath());
|
||||||
|
|
||||||
|
ExportException exception =
|
||||||
|
assertThrows(ExportException.class, () -> TransformerTestRunner.runLooper(transformer));
|
||||||
|
assertThat(getRootCause(exception)).hasMessageThat().isEqualTo("Gaps can not be transmuxed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void start_videoGap_throws() throws Exception {
|
||||||
|
Transformer transformer =
|
||||||
|
createTransformerBuilder(new DefaultMuxer.Factory(), /* enableFallback= */ false).build();
|
||||||
|
EditedMediaItem audioVideoItem =
|
||||||
|
new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW_VIDEO))
|
||||||
|
.build();
|
||||||
|
EditedMediaItemSequence sequence =
|
||||||
|
new EditedMediaItemSequence.Builder().addItem(audioVideoItem).addGap(500_000).build();
|
||||||
|
|
||||||
|
transformer.start(new Composition.Builder(sequence).build(), outputDir.newFile().getPath());
|
||||||
|
|
||||||
|
ExportException exception =
|
||||||
|
assertThrows(ExportException.class, () -> TransformerTestRunner.runLooper(transformer));
|
||||||
|
assertThat(getRootCause(exception))
|
||||||
|
.hasMessageThat()
|
||||||
|
.isEqualTo("Gaps in video sequences are not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void start_gapVideo_throws() throws Exception {
|
||||||
|
Transformer transformer =
|
||||||
|
createTransformerBuilder(new DefaultMuxer.Factory(), /* enableFallback= */ false).build();
|
||||||
|
EditedMediaItem audioVideoItem =
|
||||||
|
new EditedMediaItem.Builder(MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW_VIDEO))
|
||||||
|
.build();
|
||||||
|
EditedMediaItemSequence sequence =
|
||||||
|
new EditedMediaItemSequence.Builder().addGap(500_000).addItem(audioVideoItem).build();
|
||||||
|
Composition composition = new Composition.Builder(sequence).build();
|
||||||
|
|
||||||
|
transformer.start(composition, outputDir.newFile().getPath());
|
||||||
|
|
||||||
|
// Transformer throws because the first item in the sequence (the gap) does not have a video
|
||||||
|
// track.
|
||||||
|
assertThrows(ExportException.class, () -> TransformerTestRunner.runLooper(transformer));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void start_gapGap_completesSuccessfully() throws Exception {
|
public void start_gapGap_completesSuccessfully() throws Exception {
|
||||||
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
|
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
|
||||||
@ -915,4 +993,16 @@ public final class SequenceExportTest {
|
|||||||
assertThat(exportResult.sampleRate).isEqualTo(48_000);
|
assertThat(exportResult.sampleRate).isEqualTo(48_000);
|
||||||
assertThat(exportResult.channelCount).isEqualTo(2);
|
assertThat(exportResult.channelCount).isEqualTo(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Throwable getRootCause(Throwable throwable) {
|
||||||
|
@Nullable Throwable node = throwable;
|
||||||
|
@Nullable Throwable nodeCause;
|
||||||
|
do {
|
||||||
|
nodeCause = node.getCause();
|
||||||
|
if (nodeCause != null) {
|
||||||
|
node = nodeCause;
|
||||||
|
}
|
||||||
|
} while (nodeCause != null);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user