Implement trim optimization in Transformer

PiperOrigin-RevId: 584622392
This commit is contained in:
tofunmi 2023-11-22 07:25:56 -08:00 committed by Copybara-Service
parent 6435ddb89e
commit 2d77e4d22c
8 changed files with 6627 additions and 23 deletions

View File

@ -0,0 +1,560 @@
format video:
id = 1
sampleMimeType = video/avc
codecs = avc1.42C015
maxInputSize = 14839
width = 320
height = 240
frameRate = 59.997425
colorInfo:
colorSpace = 2
colorRange = 1
colorTransfer = 3
lumaBitdepth = 8
chromaBitdepth = 8
metadata = entries=[TSSE: description=null: values=[Lavf58.76.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000]
initializationData:
data = length 31, hash 4B108214
data = length 9, hash FBA158BB
container metadata = entries=[TSSE: description=null: values=[Lavf58.76.100], Mp4Timestamp: creation time=0, modification time=0, timescale=1000]
sample:
trackType = video
dataHashCode = 983000500
size = 13539
isKeyFrame = true
presentationTimeUs = 0
sample:
trackType = video
dataHashCode = -1834230781
size = 32
isKeyFrame = false
presentationTimeUs = 16666
sample:
trackType = video
dataHashCode = 521720738
size = 1534
isKeyFrame = false
presentationTimeUs = 33333
sample:
trackType = video
dataHashCode = 722836039
size = 123
isKeyFrame = false
presentationTimeUs = 50000
sample:
trackType = video
dataHashCode = -1702585381
size = 2061
isKeyFrame = false
presentationTimeUs = 66666
sample:
trackType = video
dataHashCode = -365856396
size = 147
isKeyFrame = false
presentationTimeUs = 83333
sample:
trackType = video
dataHashCode = 1258185334
size = 2534
isKeyFrame = false
presentationTimeUs = 100000
sample:
trackType = video
dataHashCode = -179623006
size = 87
isKeyFrame = false
presentationTimeUs = 116666
sample:
trackType = video
dataHashCode = -541393824
size = 2762
isKeyFrame = false
presentationTimeUs = 133333
sample:
trackType = video
dataHashCode = -1912932514
size = 57
isKeyFrame = false
presentationTimeUs = 150000
sample:
trackType = video
dataHashCode = 485634444
size = 2833
isKeyFrame = false
presentationTimeUs = 166666
sample:
trackType = video
dataHashCode = 570625802
size = 189
isKeyFrame = false
presentationTimeUs = 183333
sample:
trackType = video
dataHashCode = 1819668957
size = 3153
isKeyFrame = false
presentationTimeUs = 200000
sample:
trackType = video
dataHashCode = 1004398066
size = 104
isKeyFrame = false
presentationTimeUs = 216666
sample:
trackType = video
dataHashCode = 2087741113
size = 2304
isKeyFrame = false
presentationTimeUs = 233333
sample:
trackType = video
dataHashCode = -419782502
size = 222
isKeyFrame = false
presentationTimeUs = 250000
sample:
trackType = video
dataHashCode = -1867110345
size = 2306
isKeyFrame = false
presentationTimeUs = 266666
sample:
trackType = video
dataHashCode = 1908323737
size = 257
isKeyFrame = false
presentationTimeUs = 283333
sample:
trackType = video
dataHashCode = 884063337
size = 2201
isKeyFrame = false
presentationTimeUs = 300000
sample:
trackType = video
dataHashCode = -1308458590
size = 174
isKeyFrame = false
presentationTimeUs = 316666
sample:
trackType = video
dataHashCode = -1686938678
size = 2524
isKeyFrame = false
presentationTimeUs = 333333
sample:
trackType = video
dataHashCode = -1372845971
size = 171
isKeyFrame = false
presentationTimeUs = 350000
sample:
trackType = video
dataHashCode = 1130876644
size = 2306
isKeyFrame = false
presentationTimeUs = 366666
sample:
trackType = video
dataHashCode = 1707671352
size = 188
isKeyFrame = false
presentationTimeUs = 383333
sample:
trackType = video
dataHashCode = 300233313
size = 2529
isKeyFrame = false
presentationTimeUs = 400000
sample:
trackType = video
dataHashCode = -1284013406
size = 182
isKeyFrame = false
presentationTimeUs = 416666
sample:
trackType = video
dataHashCode = -2088617828
size = 2047
isKeyFrame = false
presentationTimeUs = 433333
sample:
trackType = video
dataHashCode = 2116374999
size = 259
isKeyFrame = false
presentationTimeUs = 450000
sample:
trackType = video
dataHashCode = -2123019940
size = 2234
isKeyFrame = false
presentationTimeUs = 466666
sample:
trackType = video
dataHashCode = 1901454757
size = 138
isKeyFrame = false
presentationTimeUs = 483333
sample:
trackType = video
dataHashCode = 1576638059
size = 2088
isKeyFrame = false
presentationTimeUs = 500000
sample:
trackType = video
dataHashCode = 1120133924
size = 151
isKeyFrame = false
presentationTimeUs = 516666
sample:
trackType = video
dataHashCode = 264118578
size = 2235
isKeyFrame = false
presentationTimeUs = 533333
sample:
trackType = video
dataHashCode = 64254117
size = 164
isKeyFrame = false
presentationTimeUs = 550000
sample:
trackType = video
dataHashCode = -1000078879
size = 2231
isKeyFrame = false
presentationTimeUs = 566666
sample:
trackType = video
dataHashCode = 286919946
size = 123
isKeyFrame = false
presentationTimeUs = 583333
sample:
trackType = video
dataHashCode = -320312658
size = 2303
isKeyFrame = false
presentationTimeUs = 600000
sample:
trackType = video
dataHashCode = 1057750590
size = 175
isKeyFrame = false
presentationTimeUs = 616666
sample:
trackType = video
dataHashCode = 1961415074
size = 2165
isKeyFrame = false
presentationTimeUs = 633333
sample:
trackType = video
dataHashCode = 667267023
size = 260
isKeyFrame = false
presentationTimeUs = 650000
sample:
trackType = video
dataHashCode = 979033489
size = 1924
isKeyFrame = false
presentationTimeUs = 666666
sample:
trackType = video
dataHashCode = -1974473017
size = 286
isKeyFrame = false
presentationTimeUs = 683333
sample:
trackType = video
dataHashCode = -962519103
size = 1992
isKeyFrame = false
presentationTimeUs = 700000
sample:
trackType = video
dataHashCode = -1312094075
size = 204
isKeyFrame = false
presentationTimeUs = 716666
sample:
trackType = video
dataHashCode = 2068151127
size = 1826
isKeyFrame = false
presentationTimeUs = 733333
sample:
trackType = video
dataHashCode = -1531967506
size = 284
isKeyFrame = false
presentationTimeUs = 750000
sample:
trackType = video
dataHashCode = -778066699
size = 1940
isKeyFrame = false
presentationTimeUs = 766666
sample:
trackType = video
dataHashCode = -1219952117
size = 129
isKeyFrame = false
presentationTimeUs = 783333
sample:
trackType = video
dataHashCode = -1218204223
size = 1947
isKeyFrame = false
presentationTimeUs = 800000
sample:
trackType = video
dataHashCode = -1816247511
size = 147
isKeyFrame = false
presentationTimeUs = 816666
sample:
trackType = video
dataHashCode = 299686318
size = 2066
isKeyFrame = false
presentationTimeUs = 833333
sample:
trackType = video
dataHashCode = -1520242765
size = 185
isKeyFrame = false
presentationTimeUs = 850000
sample:
trackType = video
dataHashCode = -1702498409
size = 2159
isKeyFrame = false
presentationTimeUs = 866666
sample:
trackType = video
dataHashCode = 345202950
size = 189
isKeyFrame = false
presentationTimeUs = 883333
sample:
trackType = video
dataHashCode = 220746796
size = 2098
isKeyFrame = false
presentationTimeUs = 900000
sample:
trackType = video
dataHashCode = -32341189
size = 159
isKeyFrame = false
presentationTimeUs = 916666
sample:
trackType = video
dataHashCode = -1838476361
size = 1914
isKeyFrame = false
presentationTimeUs = 933333
sample:
trackType = video
dataHashCode = -1322093590
size = 99
isKeyFrame = false
presentationTimeUs = 950000
sample:
trackType = video
dataHashCode = -1391064751
size = 2168
isKeyFrame = false
presentationTimeUs = 966666
sample:
trackType = video
dataHashCode = 1479204931
size = 129
isKeyFrame = false
presentationTimeUs = 983333
sample:
trackType = video
dataHashCode = 1131230500
size = 2327
isKeyFrame = false
presentationTimeUs = 1000000
sample:
trackType = video
dataHashCode = -393815961
size = 160
isKeyFrame = false
presentationTimeUs = 1016666
sample:
trackType = video
dataHashCode = -242739025
size = 2136
isKeyFrame = false
presentationTimeUs = 1033333
sample:
trackType = video
dataHashCode = 65238903
size = 163
isKeyFrame = false
presentationTimeUs = 1050000
sample:
trackType = video
dataHashCode = 1720840922
size = 2043
isKeyFrame = false
presentationTimeUs = 1066666
sample:
trackType = video
dataHashCode = -1006231050
size = 178
isKeyFrame = false
presentationTimeUs = 1083333
sample:
trackType = video
dataHashCode = 1742965952
size = 2022
isKeyFrame = false
presentationTimeUs = 1100000
sample:
trackType = video
dataHashCode = -971065365
size = 240
isKeyFrame = false
presentationTimeUs = 1116666
sample:
trackType = video
dataHashCode = 1757434551
size = 1887
isKeyFrame = false
presentationTimeUs = 1133333
sample:
trackType = video
dataHashCode = 1501849116
size = 252
isKeyFrame = false
presentationTimeUs = 1150000
sample:
trackType = video
dataHashCode = 825501977
size = 1816
isKeyFrame = false
presentationTimeUs = 1166666
sample:
trackType = video
dataHashCode = -1616223509
size = 246
isKeyFrame = false
presentationTimeUs = 1183333
sample:
trackType = video
dataHashCode = 457119646
size = 1817
isKeyFrame = false
presentationTimeUs = 1200000
sample:
trackType = video
dataHashCode = -1382929639
size = 146
isKeyFrame = false
presentationTimeUs = 1216666
sample:
trackType = video
dataHashCode = -1580853131
size = 1929
isKeyFrame = false
presentationTimeUs = 1233333
sample:
trackType = video
dataHashCode = 1758706551
size = 196
isKeyFrame = false
presentationTimeUs = 1250000
sample:
trackType = video
dataHashCode = 207289556
size = 2154
isKeyFrame = false
presentationTimeUs = 1266666
sample:
trackType = video
dataHashCode = -981284942
size = 182
isKeyFrame = false
presentationTimeUs = 1283333
sample:
trackType = video
dataHashCode = 855103964
size = 2144
isKeyFrame = false
presentationTimeUs = 1300000
sample:
trackType = video
dataHashCode = 380479426
size = 90
isKeyFrame = false
presentationTimeUs = 1316666
sample:
trackType = video
dataHashCode = -1677996152
size = 2005
isKeyFrame = false
presentationTimeUs = 1333333
sample:
trackType = video
dataHashCode = 1516852008
size = 156
isKeyFrame = false
presentationTimeUs = 1350000
sample:
trackType = video
dataHashCode = -1602805193
size = 1772
isKeyFrame = false
presentationTimeUs = 1366666
sample:
trackType = video
dataHashCode = -1720426556
size = 162
isKeyFrame = false
presentationTimeUs = 1383333
sample:
trackType = video
dataHashCode = -1392260423
size = 1865
isKeyFrame = false
presentationTimeUs = 1400000
sample:
trackType = video
dataHashCode = -1842432151
size = 151
isKeyFrame = false
presentationTimeUs = 1416666
sample:
trackType = video
dataHashCode = -537063215
size = 1848
isKeyFrame = false
presentationTimeUs = 1433333
sample:
trackType = video
dataHashCode = 2089388394
size = 206
isKeyFrame = false
presentationTimeUs = 1450000
sample:
trackType = video
dataHashCode = -1761777019
size = 1934
isKeyFrame = false
presentationTimeUs = 1466666
sample:
trackType = video
dataHashCode = 235471194
size = 119
isKeyFrame = false
presentationTimeUs = 1483333
released = true

View File

@ -0,0 +1,31 @@
format audio:
averageBitrate = 131072
sampleMimeType = audio/mp4a-latm
channelCount = 1
sampleRate = 44100
pcmEncoding = 2
sample:
trackType = audio
dataHashCode = -8136122
size = 8820
isKeyFrame = true
presentationTimeUs = 0
sample:
trackType = audio
dataHashCode = 1750866613
size = 8820
isKeyFrame = true
presentationTimeUs = 100000
sample:
trackType = audio
dataHashCode = -1100753636
size = 8820
isKeyFrame = true
presentationTimeUs = 200000
sample:
trackType = audio
dataHashCode = 507833230
size = 8820
isKeyFrame = true
presentationTimeUs = 300000
released = true

View File

@ -16,7 +16,9 @@
package androidx.media3.transformer; package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.ColorInfo; import androidx.media3.common.ColorInfo;
@ -24,6 +26,10 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -46,6 +52,7 @@ public final class ExportResult {
private int width; private int width;
private int videoFrameCount; private int videoFrameCount;
@Nullable private String videoEncoderName; @Nullable private String videoEncoderName;
private @OptimizationResult int optimizationResult;
@Nullable private ExportException exportException; @Nullable private ExportException exportException;
/** Creates a builder. */ /** Creates a builder. */
@ -192,6 +199,21 @@ public final class ExportResult {
return this; return this;
} }
/**
* Sets {@link OptimizationResult} to indicate an optimization as been successful, or has failed
* and normal export proceeded instead.
*
* <p>The default value is {@link #OPTIMIZATION_NONE}.
*
* @param optimizationResult The {@link OptimizationResult}.
* @return This {@link Builder}.
*/
@CanIgnoreReturnValue
public Builder setOptimizationResult(@OptimizationResult int optimizationResult) {
this.optimizationResult = optimizationResult;
return this;
}
/** Sets the {@link ExportException} that caused the export to fail. */ /** Sets the {@link ExportException} that caused the export to fail. */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setExportException(@Nullable ExportException exportException) { public Builder setExportException(@Nullable ExportException exportException) {
@ -215,6 +237,7 @@ public final class ExportResult {
width, width,
videoFrameCount, videoFrameCount,
videoEncoderName, videoEncoderName,
optimizationResult,
exportException); exportException);
} }
@ -263,6 +286,38 @@ public final class ExportResult {
} }
} }
/**
* Specifies the result of an optimized operation, such as {@link
* Transformer.Builder#experimentalSetTrimOptimizationEnabled}. One of {@link #OPTIMIZATION_NONE},
* {@link #OPTIMIZATION_SUCCEEDED}, {@link #OPTIMIZATION_FAILED_NO_VIDEO_TRACK_TO_TRIM} or {@link
* #OPTIMIZATION_FAILED_EXTRACTION_FAILED}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
OPTIMIZATION_NONE,
OPTIMIZATION_SUCCEEDED,
OPTIMIZATION_FAILED_NO_VIDEO_TRACK_TO_TRIM,
OPTIMIZATION_FAILED_EXTRACTION_FAILED
})
@interface OptimizationResult {}
/** No optimizations were applied since none were requested. */
public static final int OPTIMIZATION_NONE = 0;
/** The optimization was successfully applied. */
public static final int OPTIMIZATION_SUCCEEDED = 1;
/** The trim optimization failed because there was no video track. Normal export proceeded. */
public static final int OPTIMIZATION_FAILED_NO_VIDEO_TRACK_TO_TRIM = 2;
/**
* The optimization failed because mp4 metadata extraction failed (possibly because the file
* wasn't an mp4 file). Normal export proceeded.
*/
public static final int OPTIMIZATION_FAILED_EXTRACTION_FAILED = 3;
/** The list of {@linkplain ProcessedInput processed inputs}. */ /** The list of {@linkplain ProcessedInput processed inputs}. */
public final ImmutableList<ProcessedInput> processedInputs; public final ImmutableList<ProcessedInput> processedInputs;
@ -306,6 +361,9 @@ public final class ExportResult {
/** The name of the video encoder used, or {@code null} if none were used. */ /** The name of the video encoder used, or {@code null} if none were used. */
@Nullable public final String videoEncoderName; @Nullable public final String videoEncoderName;
/** The result of any requested optimizations. */
public final @OptimizationResult int optimizationResult;
/** /**
* The {@link ExportException} that caused the export to fail, or {@code null} if the export was a * The {@link ExportException} that caused the export to fail, or {@code null} if the export was a
* success. * success.
@ -326,6 +384,7 @@ public final class ExportResult {
int width, int width,
int videoFrameCount, int videoFrameCount,
@Nullable String videoEncoderName, @Nullable String videoEncoderName,
@OptimizationResult int optimizationResult,
@Nullable ExportException exportException) { @Nullable ExportException exportException) {
this.processedInputs = processedInputs; this.processedInputs = processedInputs;
this.durationMs = durationMs; this.durationMs = durationMs;
@ -340,6 +399,7 @@ public final class ExportResult {
this.width = width; this.width = width;
this.videoFrameCount = videoFrameCount; this.videoFrameCount = videoFrameCount;
this.videoEncoderName = videoEncoderName; this.videoEncoderName = videoEncoderName;
this.optimizationResult = optimizationResult;
this.exportException = exportException; this.exportException = exportException;
} }

View File

@ -20,6 +20,9 @@ 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 static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.transformer.Composition.HDR_MODE_EXPERIMENTAL_FORCE_INTERPRET_HDR_AS_SDR; import static androidx.media3.transformer.Composition.HDR_MODE_EXPERIMENTAL_FORCE_INTERPRET_HDR_AS_SDR;
import static androidx.media3.transformer.ExportResult.OPTIMIZATION_FAILED_EXTRACTION_FAILED;
import static androidx.media3.transformer.ExportResult.OPTIMIZATION_FAILED_NO_VIDEO_TRACK_TO_TRIM;
import static androidx.media3.transformer.TransmuxTranscodeHelper.buildNewCompositionWithClipTimes;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context; import android.content.Context;
@ -99,6 +102,7 @@ public final class Transformer {
private boolean removeAudio; private boolean removeAudio;
private boolean removeVideo; private boolean removeVideo;
private boolean flattenForSlowMotion; private boolean flattenForSlowMotion;
private boolean trimOptimizationEnabled;
private ListenerSet<Transformer.Listener> listeners; private ListenerSet<Transformer.Listener> listeners;
private AssetLoader.@MonotonicNonNull Factory assetLoaderFactory; private AssetLoader.@MonotonicNonNull Factory assetLoaderFactory;
private AudioMixer.Factory audioMixerFactory; private AudioMixer.Factory audioMixerFactory;
@ -138,6 +142,7 @@ public final class Transformer {
this.videoEffects = transformer.videoEffects; this.videoEffects = transformer.videoEffects;
this.removeAudio = transformer.removeAudio; this.removeAudio = transformer.removeAudio;
this.removeVideo = transformer.removeVideo; this.removeVideo = transformer.removeVideo;
this.trimOptimizationEnabled = transformer.trimOptimizationEnabled;
this.listeners = transformer.listeners; this.listeners = transformer.listeners;
this.assetLoaderFactory = transformer.assetLoaderFactory; this.assetLoaderFactory = transformer.assetLoaderFactory;
this.audioMixerFactory = transformer.audioMixerFactory; this.audioMixerFactory = transformer.audioMixerFactory;
@ -282,6 +287,39 @@ public final class Transformer {
return this; return this;
} }
// TODO: b/304476154 - Support audio and progress updates in trim optimization.
/**
* Sets whether to attempt to optimize trims from the start of the {@link EditedMediaItem} by
* transcoding as little of the file as possible and transmuxing the rest.
*
* <p>This optimization has the following limitations:
*
* <ul>
* <li>Only supported for single-asset (i.e. only one {@link EditedMediaItem} in the whole
* {@link Composition}) exports of mp4 files.
* <li>Not guaranteed to work with any effects.
* <li>Video track only (removes audio from the file).
* <li>Progress updates will be unavailable.
* </ul>
*
* <p>{@link ExportResult#optimizationResult} will indicate whether the optimization was
* applied.
*
* <p>This process relies on the given {@linkplain #setEncoderFactory EncoderFactory} providing
* the right encoder level and profiles when transcoding, so that the transcoded and transmuxed
* segments of the file can be stitched together. If the file segments can't be stitched
* together, the {@linkplain #start(Composition, String) export operation} will throw an
* exception.
*
* @param enabled Whether to enable trim optimization.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder experimentalSetTrimOptimizationEnabled(boolean enabled) {
trimOptimizationEnabled = enabled;
return this;
}
/** /**
* @deprecated Use {@link #addListener(Listener)}, {@link #removeListener(Listener)} or {@link * @deprecated Use {@link #addListener(Listener)}, {@link #removeListener(Listener)} or {@link
* #removeAllListeners()} instead. * #removeAllListeners()} instead.
@ -497,6 +535,7 @@ public final class Transformer {
removeAudio, removeAudio,
removeVideo, removeVideo,
flattenForSlowMotion, flattenForSlowMotion,
trimOptimizationEnabled,
listeners, listeners,
assetLoaderFactory, assetLoaderFactory,
audioMixerFactory, audioMixerFactory,
@ -668,7 +707,9 @@ public final class Transformer {
TRANSFORMER_STATE_REMUX_PROCESSED_VIDEO, TRANSFORMER_STATE_REMUX_PROCESSED_VIDEO,
TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO, TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO,
TRANSFORMER_STATE_PROCESS_AUDIO, TRANSFORMER_STATE_PROCESS_AUDIO,
TRANSFORMER_STATE_COPY_OUTPUT TRANSFORMER_STATE_COPY_OUTPUT,
TRANSFORMER_STATE_TRANSCODE_VIDEO_START,
TRANSFORMER_STATE_TRANSMUX_REMAINING_VIDEO
}) })
private @interface TransformerState {} private @interface TransformerState {}
@ -677,6 +718,8 @@ public final class Transformer {
private static final int TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO = 2; private static final int TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO = 2;
private static final int TRANSFORMER_STATE_PROCESS_AUDIO = 3; private static final int TRANSFORMER_STATE_PROCESS_AUDIO = 3;
private static final int TRANSFORMER_STATE_COPY_OUTPUT = 4; private static final int TRANSFORMER_STATE_COPY_OUTPUT = 4;
private static final int TRANSFORMER_STATE_TRANSCODE_VIDEO_START = 5;
private static final int TRANSFORMER_STATE_TRANSMUX_REMAINING_VIDEO = 6;
private final Context context; private final Context context;
private final TransformationRequest transformationRequest; private final TransformationRequest transformationRequest;
@ -685,6 +728,7 @@ public final class Transformer {
private final boolean removeAudio; private final boolean removeAudio;
private final boolean removeVideo; private final boolean removeVideo;
private final boolean flattenForSlowMotion; private final boolean flattenForSlowMotion;
private final boolean trimOptimizationEnabled;
private final ListenerSet<Transformer.Listener> listeners; private final ListenerSet<Transformer.Listener> listeners;
@Nullable private final AssetLoader.Factory assetLoaderFactory; @Nullable private final AssetLoader.Factory assetLoaderFactory;
private final AudioMixer.Factory audioMixerFactory; private final AudioMixer.Factory audioMixerFactory;
@ -704,10 +748,12 @@ public final class Transformer {
private @MonotonicNonNull String outputFilePath; private @MonotonicNonNull String outputFilePath;
private @MonotonicNonNull String oldFilePath; private @MonotonicNonNull String oldFilePath;
private @TransformerState int transformerState; private @TransformerState int transformerState;
private ExportResumeHelper.@MonotonicNonNull ResumeMetadata resumeMetadata; private TransmuxTranscodeHelper.@MonotonicNonNull ResumeMetadata resumeMetadata;
private @MonotonicNonNull ListenableFuture<ExportResumeHelper.ResumeMetadata> private @MonotonicNonNull ListenableFuture<TransmuxTranscodeHelper.ResumeMetadata>
getResumeMetadataFuture; getResumeMetadataFuture;
private @MonotonicNonNull ListenableFuture<Void> copyOutputFuture; private @MonotonicNonNull ListenableFuture<Void> copyOutputFuture;
private @MonotonicNonNull ListenableFuture<Mp4MetadataInfo> getMp4MetadataInfoFuture;
private @MonotonicNonNull Mp4MetadataInfo mp4MetadataInfo;
private Transformer( private Transformer(
Context context, Context context,
@ -717,6 +763,7 @@ public final class Transformer {
boolean removeAudio, boolean removeAudio,
boolean removeVideo, boolean removeVideo,
boolean flattenForSlowMotion, boolean flattenForSlowMotion,
boolean trimOptimizationEnabled,
ListenerSet<Listener> listeners, ListenerSet<Listener> listeners,
@Nullable AssetLoader.Factory assetLoaderFactory, @Nullable AssetLoader.Factory assetLoaderFactory,
AudioMixer.Factory audioMixerFactory, AudioMixer.Factory audioMixerFactory,
@ -734,6 +781,7 @@ public final class Transformer {
this.removeAudio = removeAudio; this.removeAudio = removeAudio;
this.removeVideo = removeVideo; this.removeVideo = removeVideo;
this.flattenForSlowMotion = flattenForSlowMotion; this.flattenForSlowMotion = flattenForSlowMotion;
this.trimOptimizationEnabled = trimOptimizationEnabled;
this.listeners = listeners; this.listeners = listeners;
this.assetLoaderFactory = assetLoaderFactory; this.assetLoaderFactory = assetLoaderFactory;
this.audioMixerFactory = audioMixerFactory; this.audioMixerFactory = audioMixerFactory;
@ -868,12 +916,17 @@ public final class Transformer {
* @throws IllegalStateException If an export is already in progress. * @throws IllegalStateException If an export is already in progress.
*/ */
public void start(Composition composition, String path) { public void start(Composition composition, String path) {
verifyApplicationThread();
initialize(composition, path); initialize(composition, path);
startInternal( if (!trimOptimizationEnabled || isMultiAsset()) {
composition, startInternal(
new MuxerWrapper(path, muxerFactory, componentListener, MuxerWrapper.MUXER_MODE_DEFAULT), composition,
componentListener, new MuxerWrapper(path, muxerFactory, componentListener, MuxerWrapper.MUXER_MODE_DEFAULT),
/* initialTimestampOffsetUs= */ 0); componentListener,
/* initialTimestampOffsetUs= */ 0);
} else {
transcodeVideoBeforeFirstSyncSampleAfterTrimStartTime();
}
} }
/** /**
@ -1026,6 +1079,10 @@ public final class Transformer {
* <li>The output is an MP4 file. * <li>The output is an MP4 file.
* </ul> * </ul>
* *
* <p>Note that export optimizations (such as {@linkplain
* Builder#experimentalSetTrimOptimizationEnabled trim optimization}) will not be applied upon
* resumption.
*
* @param composition The {@link Composition} to resume export. * @param composition The {@link Composition} to resume export.
* @param outputFilePath The path to the output file. This must be different from the output path * @param outputFilePath The path to the output file. This must be different from the output path
* of the cancelled export. * of the cancelled export.
@ -1060,13 +1117,13 @@ public final class Transformer {
private void remuxProcessedVideo() { private void remuxProcessedVideo() {
transformerState = TRANSFORMER_STATE_REMUX_PROCESSED_VIDEO; transformerState = TRANSFORMER_STATE_REMUX_PROCESSED_VIDEO;
getResumeMetadataFuture = getResumeMetadataFuture =
ExportResumeHelper.getResumeMetadataAsync( TransmuxTranscodeHelper.getResumeMetadataAsync(
context, checkNotNull(oldFilePath), checkNotNull(composition)); context, checkNotNull(oldFilePath), checkNotNull(composition));
Futures.addCallback( Futures.addCallback(
getResumeMetadataFuture, getResumeMetadataFuture,
new FutureCallback<ExportResumeHelper.ResumeMetadata>() { new FutureCallback<TransmuxTranscodeHelper.ResumeMetadata>() {
@Override @Override
public void onSuccess(ExportResumeHelper.ResumeMetadata resumeMetadata) { public void onSuccess(TransmuxTranscodeHelper.ResumeMetadata resumeMetadata) {
// If there is no video track to remux or the last sync sample is actually the first // If there is no video track to remux or the last sync sample is actually the first
// sample, then start the normal Export. // sample, then start the normal Export.
if (resumeMetadata.lastSyncSampleTimestampUs == C.TIME_UNSET if (resumeMetadata.lastSyncSampleTimestampUs == C.TIME_UNSET
@ -1085,7 +1142,7 @@ public final class Transformer {
MuxerWrapper.MUXER_MODE_MUX_PARTIAL_VIDEO); MuxerWrapper.MUXER_MODE_MUX_PARTIAL_VIDEO);
startInternal( startInternal(
ExportResumeHelper.createVideoOnlyComposition( TransmuxTranscodeHelper.createVideoOnlyComposition(
oldFilePath, oldFilePath,
/* clippingEndPositionUs= */ resumeMetadata.lastSyncSampleTimestampUs), /* clippingEndPositionUs= */ resumeMetadata.lastSyncSampleTimestampUs),
checkNotNull(remuxingMuxerWrapper), checkNotNull(remuxingMuxerWrapper),
@ -1105,7 +1162,7 @@ public final class Transformer {
private void processRemainingVideo() { private void processRemainingVideo() {
transformerState = TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO; transformerState = TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO;
Composition videoOnlyComposition = Composition videoOnlyComposition =
ExportResumeHelper.buildUponComposition( TransmuxTranscodeHelper.buildUponComposition(
checkNotNull(composition), checkNotNull(composition),
/* removeAudio= */ true, /* removeAudio= */ true,
/* removeVideo= */ false, /* removeVideo= */ false,
@ -1125,7 +1182,7 @@ public final class Transformer {
transformerState = TRANSFORMER_STATE_PROCESS_AUDIO; transformerState = TRANSFORMER_STATE_PROCESS_AUDIO;
startInternal( startInternal(
ExportResumeHelper.createAudioTranscodeAndVideoTransmuxComposition( TransmuxTranscodeHelper.createAudioTranscodeAndVideoTransmuxComposition(
checkNotNull(composition), checkNotNull(outputFilePath)), checkNotNull(composition), checkNotNull(outputFilePath)),
new MuxerWrapper( new MuxerWrapper(
checkNotNull(oldFilePath), checkNotNull(oldFilePath),
@ -1140,7 +1197,7 @@ public final class Transformer {
private void copyOutput() { private void copyOutput() {
transformerState = TRANSFORMER_STATE_COPY_OUTPUT; transformerState = TRANSFORMER_STATE_COPY_OUTPUT;
copyOutputFuture = copyOutputFuture =
ExportResumeHelper.copyFileAsync( TransmuxTranscodeHelper.copyFileAsync(
new File(checkNotNull(oldFilePath)), new File(checkNotNull(outputFilePath))); new File(checkNotNull(oldFilePath)), new File(checkNotNull(outputFilePath)));
Futures.addCallback( Futures.addCallback(
@ -1161,6 +1218,102 @@ public final class Transformer {
applicationHandler::post); applicationHandler::post);
} }
private void transcodeVideoBeforeFirstSyncSampleAfterTrimStartTime() {
transformerState = TRANSFORMER_STATE_TRANSCODE_VIDEO_START;
MediaItem firstMediaItem =
checkNotNull(composition).sequences.get(0).editedMediaItems.get(0).mediaItem;
long trimStartTimeUs = firstMediaItem.clippingConfiguration.startPositionUs;
getMp4MetadataInfoFuture =
TransmuxTranscodeHelper.getMp4MetadataInfo(
context,
checkNotNull(firstMediaItem.localConfiguration).uri.toString(),
trimStartTimeUs);
Futures.addCallback(
getMp4MetadataInfoFuture,
new FutureCallback<Mp4MetadataInfo>() {
@Override
public void onSuccess(Mp4MetadataInfo mp4MetadataInfo) {
if (mp4MetadataInfo.firstSyncSampleTimestampUsAfterTimeUs == C.TIME_UNSET) {
exportResultBuilder.setOptimizationResult(OPTIMIZATION_FAILED_NO_VIDEO_TRACK_TO_TRIM);
processFullInput();
return;
}
if (mp4MetadataInfo.firstSyncSampleTimestampUsAfterTimeUs == trimStartTimeUs) {
Transformer.this.composition =
buildNewCompositionWithClipTimes(
composition,
trimStartTimeUs,
firstMediaItem.clippingConfiguration.endPositionUs,
mp4MetadataInfo.durationUs,
/* startsAtKeyFrame= */ true);
processFullInput();
return;
}
Transformer.this.mp4MetadataInfo = mp4MetadataInfo;
Composition trancodeComposition =
buildNewCompositionWithClipTimes(
composition,
trimStartTimeUs,
mp4MetadataInfo.firstSyncSampleTimestampUsAfterTimeUs,
mp4MetadataInfo.durationUs,
/* startsAtKeyFrame= */ false);
// TODO: b/304476154 - Check for cases where we shouldTranscode anyway and proceed with
// normal export instead.
remuxingMuxerWrapper =
new MuxerWrapper(
checkNotNull(outputFilePath),
muxerFactory,
componentListener,
MuxerWrapper.MUXER_MODE_MUX_PARTIAL_VIDEO);
startInternal(
trancodeComposition,
remuxingMuxerWrapper,
componentListener,
/* initialTimestampOffsetUs= */ 0);
}
@Override
public void onFailure(Throwable t) {
exportResultBuilder.setOptimizationResult(OPTIMIZATION_FAILED_EXTRACTION_FAILED);
processFullInput();
}
},
applicationHandler::post);
}
private void transmuxRemainingVideo() {
transformerState = TRANSFORMER_STATE_TRANSMUX_REMAINING_VIDEO;
// TODO: b/304476154 - check original file format against transcode file format here to fail
// fast if necessary.
MediaItem firstMediaItem =
checkNotNull(composition).sequences.get(0).editedMediaItems.get(0).mediaItem;
long trimStartTimeUs = firstMediaItem.clippingConfiguration.startPositionUs;
long trimEndTimeUs = firstMediaItem.clippingConfiguration.endPositionUs;
checkNotNull(mp4MetadataInfo);
Composition transmuxComposition =
buildNewCompositionWithClipTimes(
composition,
mp4MetadataInfo.firstSyncSampleTimestampUsAfterTimeUs,
trimEndTimeUs,
mp4MetadataInfo.durationUs,
/* startsAtKeyFrame= */ true);
checkNotNull(remuxingMuxerWrapper);
remuxingMuxerWrapper.changeToAppendVideoMode();
startInternal(
transmuxComposition,
remuxingMuxerWrapper,
componentListener,
/* initialTimestampOffsetUs= */ mp4MetadataInfo.firstSyncSampleTimestampUsAfterTimeUs
- trimStartTimeUs);
}
private boolean isMultiAsset() {
return checkNotNull(composition).sequences.size() > 1
|| composition.sequences.get(0).editedMediaItems.size() > 1;
}
private void verifyApplicationThread() { private void verifyApplicationThread() {
if (Looper.myLooper() != looper) { if (Looper.myLooper() != looper) {
throw new IllegalStateException("Transformer is accessed on the wrong thread."); throw new IllegalStateException("Transformer is accessed on the wrong thread.");
@ -1173,7 +1326,6 @@ public final class Transformer {
ComponentListener componentListener, ComponentListener componentListener,
long initialTimestampOffsetUs) { long initialTimestampOffsetUs) {
checkArgument(composition.effects.audioProcessors.isEmpty()); checkArgument(composition.effects.audioProcessors.isEmpty());
verifyApplicationThread();
checkState(transformerInternal == null, "There is already an export in progress."); checkState(transformerInternal == null, "There is already an export in progress.");
TransformationRequest transformationRequest = this.transformationRequest; TransformationRequest transformationRequest = this.transformationRequest;
if (composition.hdrMode != Composition.HDR_MODE_KEEP_HDR) { if (composition.hdrMode != Composition.HDR_MODE_KEEP_HDR) {
@ -1209,7 +1361,7 @@ public final class Transformer {
debugViewProvider, debugViewProvider,
clock, clock,
initialTimestampOffsetUs, initialTimestampOffsetUs,
/* matchInitializationData= */ false); /* matchInitializationData= */ trimOptimizationEnabled);
transformerInternal.start(); transformerInternal.start();
} }
@ -1258,6 +1410,11 @@ public final class Transformer {
processAudio(); processAudio();
} else if (transformerState == TRANSFORMER_STATE_PROCESS_AUDIO) { } else if (transformerState == TRANSFORMER_STATE_PROCESS_AUDIO) {
copyOutput(); copyOutput();
} else if (transformerState == TRANSFORMER_STATE_TRANSCODE_VIDEO_START) {
transmuxRemainingVideo();
} else if (transformerState == TRANSFORMER_STATE_TRANSMUX_REMAINING_VIDEO) {
exportResultBuilder.setOptimizationResult(ExportResult.OPTIMIZATION_SUCCEEDED);
onExportCompletedWithSuccess();
} else { } else {
onExportCompletedWithSuccess(); onExportCompletedWithSuccess();
} }

View File

@ -36,8 +36,8 @@ import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/** Utility methods for resuming an export. */ /** Utility methods used in transmux-transcode exports. */
/* package */ final class ExportResumeHelper { /* package */ final class TransmuxTranscodeHelper {
/** Provides metadata required to resume an export. */ /** Provides metadata required to resume an export. */
public static final class ResumeMetadata { public static final class ResumeMetadata {
@ -59,7 +59,59 @@ import java.util.List;
} }
} }
private ExportResumeHelper() {} public static ListenableFuture<Mp4MetadataInfo> getMp4MetadataInfo(
Context context, String filePath, long timeUs) {
SettableFuture<Mp4MetadataInfo> mp4MetadataInfoSettableFuture = SettableFuture.create();
new Thread("TransmuxTranscodeHelper:Mp4MetadataInfo") {
@Override
public void run() {
try {
mp4MetadataInfoSettableFuture.set(Mp4MetadataInfo.create(context, filePath, timeUs));
} catch (Exception ex) {
mp4MetadataInfoSettableFuture.setException(ex);
}
}
}.start();
return mp4MetadataInfoSettableFuture;
}
public static Composition buildNewCompositionWithClipTimes(
Composition oldComposition,
long startTimeUs,
long endTimeUs,
long mediaDurationUs,
boolean startsAtKeyFrame) {
EditedMediaItem firstEditedMediaItem = oldComposition.sequences.get(0).editedMediaItems.get(0);
MediaItem.ClippingConfiguration clippingConfiguration =
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionUs(startTimeUs)
.setEndPositionUs(endTimeUs)
.setStartsAtKeyFrame(startsAtKeyFrame)
.build();
MediaItem mediaItem =
firstEditedMediaItem
.mediaItem
.buildUpon()
.setClippingConfiguration(clippingConfiguration)
.build();
EditedMediaItem editedMediaItem =
firstEditedMediaItem
.buildUpon()
.setMediaItem(mediaItem)
.setDurationUs(mediaDurationUs)
// TODO: b/304476154 - Support audio in trim optimization.
.setRemoveAudio(true)
.build();
return oldComposition
.buildUpon()
.setSequences(ImmutableList.of(new EditedMediaItemSequence(editedMediaItem)))
.build();
}
private TransmuxTranscodeHelper() {}
/** /**
* Returns a video only {@link Composition} from the given {@code filePath} and {@code * Returns a video only {@link Composition} from the given {@code filePath} and {@code
@ -94,7 +146,7 @@ import java.util.List;
public static Composition createAudioTranscodeAndVideoTransmuxComposition( public static Composition createAudioTranscodeAndVideoTransmuxComposition(
Composition composition, String videoFilePath) { Composition composition, String videoFilePath) {
Composition audioOnlyComposition = Composition audioOnlyComposition =
ExportResumeHelper.buildUponComposition( TransmuxTranscodeHelper.buildUponComposition(
checkNotNull(composition), checkNotNull(composition),
/* removeAudio= */ false, /* removeAudio= */ false,
/* removeVideo= */ true, /* removeVideo= */ true,
@ -203,7 +255,7 @@ import java.util.List;
public static ListenableFuture<ResumeMetadata> getResumeMetadataAsync( public static ListenableFuture<ResumeMetadata> getResumeMetadataAsync(
Context context, String filePath, Composition composition) { Context context, String filePath, Composition composition) {
SettableFuture<ResumeMetadata> resumeMetadataSettableFuture = SettableFuture.create(); SettableFuture<ResumeMetadata> resumeMetadataSettableFuture = SettableFuture.create();
new Thread("ExportResumeHelper:ResumeMetadata") { new Thread("TransmuxTranscodeHelper:ResumeMetadata") {
@Override @Override
public void run() { public void run() {
try { try {
@ -256,7 +308,7 @@ import java.util.List;
/** Copies {@link File} content from source to destination asynchronously. */ /** Copies {@link File} content from source to destination asynchronously. */
public static ListenableFuture<Void> copyFileAsync(File source, File destination) { public static ListenableFuture<Void> copyFileAsync(File source, File destination) {
SettableFuture<Void> copyFileSettableFuture = SettableFuture.create(); SettableFuture<Void> copyFileSettableFuture = SettableFuture.create();
new Thread("ExportResumeHelper:CopyFile") { new Thread("TransmuxTranscodeHelper:CopyFile") {
@Override @Override
public void run() { public void run() {
if (copyFileSettableFuture.isCancelled()) { if (copyFileSettableFuture.isCancelled()) {

View File

@ -152,6 +152,87 @@ public final class MediaItemExportTest {
/* modifications...= */ "clipped")); /* modifications...= */ "clipped"));
} }
@Test
public void start_trimOptimizationEnabled_clippingConfigurationUnset_outputMatchesOriginal()
throws Exception {
Transformer transformer =
createTransformerBuilder(muxerFactory, /* enableFallback= */ false)
.experimentalSetTrimOptimizationEnabled(true)
.build();
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S)
.build();
transformer.start(mediaItem, outputDir.newFile().getPath());
ExportResult exportResult = TransformerTestRunner.runLooper(transformer);
assertThat(exportResult.optimizationResult).isEqualTo(ExportResult.OPTIMIZATION_NONE);
// Asserts against file generated when experimentalSetTrimOptimizationEnabled is set to false.
DumpFileAsserts.assertOutput(
context,
muxerFactory.getCreatedMuxer(),
getDumpFileName(/* originalFileName= */ FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S));
}
@Test
public void start_trimOptimizationEnabled_withClippingStartAtKeyFrame_completesSuccessfully()
throws Exception {
Transformer transformer =
createTransformerBuilder(muxerFactory, /* enableFallback= */ false)
.experimentalSetTrimOptimizationEnabled(true)
.build();
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S)
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(12_500)
.setEndPositionMs(14_000)
.build())
.build();
transformer.start(mediaItem, outputDir.newFile().getPath());
ExportResult exportResult = TransformerTestRunner.runLooper(transformer);
assertThat(exportResult.optimizationResult).isEqualTo(ExportResult.OPTIMIZATION_NONE);
// TODO: b/304476154 - When trim optimization supports audio, remove trim optimization specific
// file and use the pre-existing clipped file made from normal export path.
DumpFileAsserts.assertOutput(
context,
muxerFactory.getCreatedMuxer(),
getDumpFileName(
/* originalFileName= */ FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S,
/* modifications...= */ "trimOptimizedClippedAtKeyFrame"));
}
@Test
public void start_trimOptimizationEnabled_fileNotMp4_fallbackToNormalExport() throws Exception {
Transformer transformer =
createTransformerBuilder(muxerFactory, /* enableFallback= */ false)
.experimentalSetTrimOptimizationEnabled(true)
.build();
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(ASSET_URI_PREFIX + FILE_AUDIO_RAW)
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionUs(500_000)
.setEndPositionUs(900_000)
.build())
.build();
transformer.start(mediaItem, outputDir.newFile().getPath());
ExportResult exportResult = TransformerTestRunner.runLooper(transformer);
assertThat(exportResult.optimizationResult).isNotEqualTo(ExportResult.OPTIMIZATION_SUCCEEDED);
assertThat(exportResult.optimizationResult).isNotEqualTo(ExportResult.OPTIMIZATION_NONE);
DumpFileAsserts.assertOutput(
context,
muxerFactory.getCreatedMuxer(),
getDumpFileName(/* originalFileName= */ FILE_AUDIO_RAW, /* modifications...= */ "clipped"));
}
@Test @Test
public void start_withSubtitlesVideoOnly_completesSuccessfully() throws Exception { public void start_withSubtitlesVideoOnly_completesSuccessfully() throws Exception {
Transformer transformer = Transformer transformer =

View File

@ -29,6 +29,7 @@ import static androidx.media3.transformer.TestUtil.createPitchChangingAudioProce
import static androidx.media3.transformer.TestUtil.createTransformerBuilder; import static androidx.media3.transformer.TestUtil.createTransformerBuilder;
import static androidx.media3.transformer.TestUtil.getDumpFileName; import static androidx.media3.transformer.TestUtil.getDumpFileName;
import static androidx.media3.transformer.TestUtil.removeEncodersAndDecoders; import static androidx.media3.transformer.TestUtil.removeEncodersAndDecoders;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context; import android.content.Context;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
@ -177,6 +178,56 @@ public final class SequenceExportTest {
"transmux")); "transmux"));
} }
@Test
public void
start_trimOptimizationEnabled_concatenateClippedMediaItemsWithTransmux_completesSuccessfully()
throws Exception {
Transformer transformer =
createTransformerBuilder(muxerFactory, /* enableFallback= */ false)
.experimentalSetTrimOptimizationEnabled(true)
.build();
MediaItem.ClippingConfiguration clippingConfiguration1 =
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(0) // Corresponds to key frame.
.setEndPositionMs(500)
.build();
MediaItem mediaItem1 =
new MediaItem.Builder()
.setUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S)
.setClippingConfiguration(clippingConfiguration1)
.build();
EditedMediaItem editedMediaItem1 = new EditedMediaItem.Builder(mediaItem1).build();
MediaItem.ClippingConfiguration clippingConfiguration2 =
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(12_500) // Corresponds to key frame.
.setEndPositionMs(14_000)
.build();
MediaItem mediaItem2 =
new MediaItem.Builder()
.setUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S)
.setClippingConfiguration(clippingConfiguration2)
.build();
EditedMediaItem editedMediaItem2 = new EditedMediaItem.Builder(mediaItem2).build();
Composition composition =
new Composition.Builder(new EditedMediaItemSequence(editedMediaItem1, editedMediaItem2))
.setTransmuxAudio(true)
.setTransmuxVideo(true)
.build();
transformer.start(composition, outputDir.newFile().getPath());
ExportResult exportResult = TransformerTestRunner.runLooper(transformer);
assertThat(exportResult.optimizationResult).isEqualTo(ExportResult.OPTIMIZATION_NONE);
DumpFileAsserts.assertOutput(
context,
muxerFactory.getCreatedMuxer(),
getDumpFileName(
/* originalFileName= */ FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S,
/* modifications...= */ "clipped",
"clipped",
"transmux"));
}
@Test @Test
public void concatenateAudioAndSilence_withTransmuxVideo_completesSuccessfully() public void concatenateAudioAndSilence_withTransmuxVideo_completesSuccessfully()
throws Exception { throws Exception {