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;
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.media3.common.C;
import androidx.media3.common.ColorInfo;
@ -24,6 +26,10 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
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.Objects;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -46,6 +52,7 @@ public final class ExportResult {
private int width;
private int videoFrameCount;
@Nullable private String videoEncoderName;
private @OptimizationResult int optimizationResult;
@Nullable private ExportException exportException;
/** Creates a builder. */
@ -192,6 +199,21 @@ public final class ExportResult {
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. */
@CanIgnoreReturnValue
public Builder setExportException(@Nullable ExportException exportException) {
@ -215,6 +237,7 @@ public final class ExportResult {
width,
videoFrameCount,
videoEncoderName,
optimizationResult,
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}. */
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. */
@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
* success.
@ -326,6 +384,7 @@ public final class ExportResult {
int width,
int videoFrameCount,
@Nullable String videoEncoderName,
@OptimizationResult int optimizationResult,
@Nullable ExportException exportException) {
this.processedInputs = processedInputs;
this.durationMs = durationMs;
@ -340,6 +399,7 @@ public final class ExportResult {
this.width = width;
this.videoFrameCount = videoFrameCount;
this.videoEncoderName = videoEncoderName;
this.optimizationResult = optimizationResult;
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.checkState;
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 android.content.Context;
@ -99,6 +102,7 @@ public final class Transformer {
private boolean removeAudio;
private boolean removeVideo;
private boolean flattenForSlowMotion;
private boolean trimOptimizationEnabled;
private ListenerSet<Transformer.Listener> listeners;
private AssetLoader.@MonotonicNonNull Factory assetLoaderFactory;
private AudioMixer.Factory audioMixerFactory;
@ -138,6 +142,7 @@ public final class Transformer {
this.videoEffects = transformer.videoEffects;
this.removeAudio = transformer.removeAudio;
this.removeVideo = transformer.removeVideo;
this.trimOptimizationEnabled = transformer.trimOptimizationEnabled;
this.listeners = transformer.listeners;
this.assetLoaderFactory = transformer.assetLoaderFactory;
this.audioMixerFactory = transformer.audioMixerFactory;
@ -282,6 +287,39 @@ public final class Transformer {
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
* #removeAllListeners()} instead.
@ -497,6 +535,7 @@ public final class Transformer {
removeAudio,
removeVideo,
flattenForSlowMotion,
trimOptimizationEnabled,
listeners,
assetLoaderFactory,
audioMixerFactory,
@ -668,7 +707,9 @@ public final class Transformer {
TRANSFORMER_STATE_REMUX_PROCESSED_VIDEO,
TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO,
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 {}
@ -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_AUDIO = 3;
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 TransformationRequest transformationRequest;
@ -685,6 +728,7 @@ public final class Transformer {
private final boolean removeAudio;
private final boolean removeVideo;
private final boolean flattenForSlowMotion;
private final boolean trimOptimizationEnabled;
private final ListenerSet<Transformer.Listener> listeners;
@Nullable private final AssetLoader.Factory assetLoaderFactory;
private final AudioMixer.Factory audioMixerFactory;
@ -704,10 +748,12 @@ public final class Transformer {
private @MonotonicNonNull String outputFilePath;
private @MonotonicNonNull String oldFilePath;
private @TransformerState int transformerState;
private ExportResumeHelper.@MonotonicNonNull ResumeMetadata resumeMetadata;
private @MonotonicNonNull ListenableFuture<ExportResumeHelper.ResumeMetadata>
private TransmuxTranscodeHelper.@MonotonicNonNull ResumeMetadata resumeMetadata;
private @MonotonicNonNull ListenableFuture<TransmuxTranscodeHelper.ResumeMetadata>
getResumeMetadataFuture;
private @MonotonicNonNull ListenableFuture<Void> copyOutputFuture;
private @MonotonicNonNull ListenableFuture<Mp4MetadataInfo> getMp4MetadataInfoFuture;
private @MonotonicNonNull Mp4MetadataInfo mp4MetadataInfo;
private Transformer(
Context context,
@ -717,6 +763,7 @@ public final class Transformer {
boolean removeAudio,
boolean removeVideo,
boolean flattenForSlowMotion,
boolean trimOptimizationEnabled,
ListenerSet<Listener> listeners,
@Nullable AssetLoader.Factory assetLoaderFactory,
AudioMixer.Factory audioMixerFactory,
@ -734,6 +781,7 @@ public final class Transformer {
this.removeAudio = removeAudio;
this.removeVideo = removeVideo;
this.flattenForSlowMotion = flattenForSlowMotion;
this.trimOptimizationEnabled = trimOptimizationEnabled;
this.listeners = listeners;
this.assetLoaderFactory = assetLoaderFactory;
this.audioMixerFactory = audioMixerFactory;
@ -868,12 +916,17 @@ public final class Transformer {
* @throws IllegalStateException If an export is already in progress.
*/
public void start(Composition composition, String path) {
verifyApplicationThread();
initialize(composition, path);
startInternal(
composition,
new MuxerWrapper(path, muxerFactory, componentListener, MuxerWrapper.MUXER_MODE_DEFAULT),
componentListener,
/* initialTimestampOffsetUs= */ 0);
if (!trimOptimizationEnabled || isMultiAsset()) {
startInternal(
composition,
new MuxerWrapper(path, muxerFactory, componentListener, MuxerWrapper.MUXER_MODE_DEFAULT),
componentListener,
/* initialTimestampOffsetUs= */ 0);
} else {
transcodeVideoBeforeFirstSyncSampleAfterTrimStartTime();
}
}
/**
@ -1026,6 +1079,10 @@ public final class Transformer {
* <li>The output is an MP4 file.
* </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 outputFilePath The path to the output file. This must be different from the output path
* of the cancelled export.
@ -1060,13 +1117,13 @@ public final class Transformer {
private void remuxProcessedVideo() {
transformerState = TRANSFORMER_STATE_REMUX_PROCESSED_VIDEO;
getResumeMetadataFuture =
ExportResumeHelper.getResumeMetadataAsync(
TransmuxTranscodeHelper.getResumeMetadataAsync(
context, checkNotNull(oldFilePath), checkNotNull(composition));
Futures.addCallback(
getResumeMetadataFuture,
new FutureCallback<ExportResumeHelper.ResumeMetadata>() {
new FutureCallback<TransmuxTranscodeHelper.ResumeMetadata>() {
@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
// sample, then start the normal Export.
if (resumeMetadata.lastSyncSampleTimestampUs == C.TIME_UNSET
@ -1085,7 +1142,7 @@ public final class Transformer {
MuxerWrapper.MUXER_MODE_MUX_PARTIAL_VIDEO);
startInternal(
ExportResumeHelper.createVideoOnlyComposition(
TransmuxTranscodeHelper.createVideoOnlyComposition(
oldFilePath,
/* clippingEndPositionUs= */ resumeMetadata.lastSyncSampleTimestampUs),
checkNotNull(remuxingMuxerWrapper),
@ -1105,7 +1162,7 @@ public final class Transformer {
private void processRemainingVideo() {
transformerState = TRANSFORMER_STATE_PROCESS_REMAINING_VIDEO;
Composition videoOnlyComposition =
ExportResumeHelper.buildUponComposition(
TransmuxTranscodeHelper.buildUponComposition(
checkNotNull(composition),
/* removeAudio= */ true,
/* removeVideo= */ false,
@ -1125,7 +1182,7 @@ public final class Transformer {
transformerState = TRANSFORMER_STATE_PROCESS_AUDIO;
startInternal(
ExportResumeHelper.createAudioTranscodeAndVideoTransmuxComposition(
TransmuxTranscodeHelper.createAudioTranscodeAndVideoTransmuxComposition(
checkNotNull(composition), checkNotNull(outputFilePath)),
new MuxerWrapper(
checkNotNull(oldFilePath),
@ -1140,7 +1197,7 @@ public final class Transformer {
private void copyOutput() {
transformerState = TRANSFORMER_STATE_COPY_OUTPUT;
copyOutputFuture =
ExportResumeHelper.copyFileAsync(
TransmuxTranscodeHelper.copyFileAsync(
new File(checkNotNull(oldFilePath)), new File(checkNotNull(outputFilePath)));
Futures.addCallback(
@ -1161,6 +1218,102 @@ public final class Transformer {
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() {
if (Looper.myLooper() != looper) {
throw new IllegalStateException("Transformer is accessed on the wrong thread.");
@ -1173,7 +1326,6 @@ public final class Transformer {
ComponentListener componentListener,
long initialTimestampOffsetUs) {
checkArgument(composition.effects.audioProcessors.isEmpty());
verifyApplicationThread();
checkState(transformerInternal == null, "There is already an export in progress.");
TransformationRequest transformationRequest = this.transformationRequest;
if (composition.hdrMode != Composition.HDR_MODE_KEEP_HDR) {
@ -1209,7 +1361,7 @@ public final class Transformer {
debugViewProvider,
clock,
initialTimestampOffsetUs,
/* matchInitializationData= */ false);
/* matchInitializationData= */ trimOptimizationEnabled);
transformerInternal.start();
}
@ -1258,6 +1410,11 @@ public final class Transformer {
processAudio();
} else if (transformerState == TRANSFORMER_STATE_PROCESS_AUDIO) {
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 {
onExportCompletedWithSuccess();
}

View File

@ -36,8 +36,8 @@ import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
/** Utility methods for resuming an export. */
/* package */ final class ExportResumeHelper {
/** Utility methods used in transmux-transcode exports. */
/* package */ final class TransmuxTranscodeHelper {
/** Provides metadata required to resume an export. */
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
@ -94,7 +146,7 @@ import java.util.List;
public static Composition createAudioTranscodeAndVideoTransmuxComposition(
Composition composition, String videoFilePath) {
Composition audioOnlyComposition =
ExportResumeHelper.buildUponComposition(
TransmuxTranscodeHelper.buildUponComposition(
checkNotNull(composition),
/* removeAudio= */ false,
/* removeVideo= */ true,
@ -203,7 +255,7 @@ import java.util.List;
public static ListenableFuture<ResumeMetadata> getResumeMetadataAsync(
Context context, String filePath, Composition composition) {
SettableFuture<ResumeMetadata> resumeMetadataSettableFuture = SettableFuture.create();
new Thread("ExportResumeHelper:ResumeMetadata") {
new Thread("TransmuxTranscodeHelper:ResumeMetadata") {
@Override
public void run() {
try {
@ -256,7 +308,7 @@ import java.util.List;
/** Copies {@link File} content from source to destination asynchronously. */
public static ListenableFuture<Void> copyFileAsync(File source, File destination) {
SettableFuture<Void> copyFileSettableFuture = SettableFuture.create();
new Thread("ExportResumeHelper:CopyFile") {
new Thread("TransmuxTranscodeHelper:CopyFile") {
@Override
public void run() {
if (copyFileSettableFuture.isCancelled()) {

View File

@ -152,6 +152,87 @@ public final class MediaItemExportTest {
/* 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
public void start_withSubtitlesVideoOnly_completesSuccessfully() throws Exception {
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.getDumpFileName;
import static androidx.media3.transformer.TestUtil.removeEncodersAndDecoders;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import androidx.media3.common.MediaItem;
@ -177,6 +178,56 @@ public final class SequenceExportTest {
"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
public void concatenateAudioAndSilence_withTransmuxVideo_completesSuccessfully()
throws Exception {