Have VideoSampleExporter output orientation match input
This should enable trim optimization to work correctly in more cases. PiperOrigin-RevId: 611096958
This commit is contained in:
parent
55bfe4f95c
commit
08993b6fb1
@ -501,6 +501,7 @@ public final class Util {
|
|||||||
String deviceName = Ascii.toLowerCase(Util.DEVICE);
|
String deviceName = Ascii.toLowerCase(Util.DEVICE);
|
||||||
return deviceName.contains("emulator")
|
return deviceName.contains("emulator")
|
||||||
|| deviceName.contains("emu64a")
|
|| deviceName.contains("emu64a")
|
||||||
|
|| deviceName.contains("emu64x")
|
||||||
|| deviceName.contains("generic");
|
|| deviceName.contains("generic");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binary file not shown.
Binary file not shown.
@ -72,6 +72,12 @@ public final class AndroidTestUtil {
|
|||||||
public static final String MP4_TRIM_OPTIMIZATION_URI_STRING =
|
public static final String MP4_TRIM_OPTIMIZATION_URI_STRING =
|
||||||
"asset:///media/mp4/internal_emulator_transformer_output.mp4";
|
"asset:///media/mp4/internal_emulator_transformer_output.mp4";
|
||||||
|
|
||||||
|
public static final String MP4_TRIM_OPTIMIZATION_270_URI_STRING =
|
||||||
|
"asset:///media/mp4/internal_emulator_transformer_output_270_rotated.mp4";
|
||||||
|
|
||||||
|
public static final String MP4_TRIM_OPTIMIZATION_180_URI_STRING =
|
||||||
|
"asset:///media/mp4/internal_emulator_transformer_output_180_rotated.mp4";
|
||||||
|
|
||||||
public static final String MP4_TRIM_OPTIMIZATION_PIXEL_URI_STRING =
|
public static final String MP4_TRIM_OPTIMIZATION_PIXEL_URI_STRING =
|
||||||
"asset:///media/mp4/pixel7_videoOnly_cleaned.mp4";
|
"asset:///media/mp4/pixel7_videoOnly_cleaned.mp4";
|
||||||
|
|
||||||
|
@ -25,6 +25,8 @@ import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING;
|
|||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_FORMAT;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_FORMAT;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_URI_STRING;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_URI_STRING;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION_180_URI_STRING;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION_270_URI_STRING;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION_URI_STRING;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION_URI_STRING;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.PNG_ASSET_URI_STRING;
|
import static androidx.media3.transformer.AndroidTestUtil.PNG_ASSET_URI_STRING;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.createOpenGlObjects;
|
import static androidx.media3.transformer.AndroidTestUtil.createOpenGlObjects;
|
||||||
@ -704,6 +706,80 @@ public class TransformerEndToEndTest {
|
|||||||
assertThat(result.exportResult.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED);
|
assertThat(result.exportResult.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
clippedMedia_trimOptimizationEnabled_inputFileRotated270_completesWithOptimizationApplied()
|
||||||
|
throws Exception {
|
||||||
|
if (!isRunningOnEmulator() || Util.SDK_INT < 33) {
|
||||||
|
// The trim optimization is only guaranteed to work on emulator for this (emulator-transcoded)
|
||||||
|
// file.
|
||||||
|
recordTestSkipped(context, testId, /* reason= */ "SDK 33 Emulator only test");
|
||||||
|
assumeTrue(false);
|
||||||
|
}
|
||||||
|
Transformer transformer =
|
||||||
|
new Transformer.Builder(context).experimentalSetTrimOptimizationEnabled(true).build();
|
||||||
|
MediaItem mediaItem =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri(MP4_TRIM_OPTIMIZATION_270_URI_STRING)
|
||||||
|
.setClippingConfiguration(
|
||||||
|
new MediaItem.ClippingConfiguration.Builder()
|
||||||
|
.setStartPositionMs(500)
|
||||||
|
.setEndPositionMs(2500)
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
EditedMediaItem editedMediaItem = new EditedMediaItem.Builder(mediaItem).build();
|
||||||
|
|
||||||
|
ExportTestResult result =
|
||||||
|
new TransformerAndroidTestRunner.Builder(context, transformer)
|
||||||
|
.build()
|
||||||
|
.run(testId, editedMediaItem);
|
||||||
|
|
||||||
|
assertThat(result.exportResult.optimizationResult).isEqualTo(OPTIMIZATION_SUCCEEDED);
|
||||||
|
assertThat(result.exportResult.durationMs).isAtMost(2000);
|
||||||
|
assertThat(result.exportResult.videoConversionProcess)
|
||||||
|
.isEqualTo(CONVERSION_PROCESS_TRANSMUXED_AND_TRANSCODED);
|
||||||
|
assertThat(result.exportResult.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED);
|
||||||
|
Format format = retrieveTrackFormat(context, result.filePath, C.TRACK_TYPE_VIDEO);
|
||||||
|
assertThat(format.rotationDegrees).isEqualTo(270);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
clippedMedia_trimOptimizationEnabled_inputFileRotated180_completesWithOptimizationApplied()
|
||||||
|
throws Exception {
|
||||||
|
if (!isRunningOnEmulator() || Util.SDK_INT < 33) {
|
||||||
|
// The trim optimization is only guaranteed to work on emulator for this (emulator-transcoded)
|
||||||
|
// file.
|
||||||
|
recordTestSkipped(context, testId, /* reason= */ "SDK 33 Emulator only test");
|
||||||
|
assumeTrue(false);
|
||||||
|
}
|
||||||
|
Transformer transformer =
|
||||||
|
new Transformer.Builder(context).experimentalSetTrimOptimizationEnabled(true).build();
|
||||||
|
MediaItem mediaItem =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri(MP4_TRIM_OPTIMIZATION_180_URI_STRING)
|
||||||
|
.setClippingConfiguration(
|
||||||
|
new MediaItem.ClippingConfiguration.Builder()
|
||||||
|
.setStartPositionMs(500)
|
||||||
|
.setEndPositionMs(2500)
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
EditedMediaItem editedMediaItem = new EditedMediaItem.Builder(mediaItem).build();
|
||||||
|
|
||||||
|
ExportTestResult result =
|
||||||
|
new TransformerAndroidTestRunner.Builder(context, transformer)
|
||||||
|
.build()
|
||||||
|
.run(testId, editedMediaItem);
|
||||||
|
|
||||||
|
assertThat(result.exportResult.optimizationResult).isEqualTo(OPTIMIZATION_SUCCEEDED);
|
||||||
|
assertThat(result.exportResult.durationMs).isAtMost(2000);
|
||||||
|
assertThat(result.exportResult.videoConversionProcess)
|
||||||
|
.isEqualTo(CONVERSION_PROCESS_TRANSMUXED_AND_TRANSCODED);
|
||||||
|
assertThat(result.exportResult.audioConversionProcess).isEqualTo(CONVERSION_PROCESS_TRANSMUXED);
|
||||||
|
Format format = retrieveTrackFormat(context, result.filePath, C.TRACK_TYPE_VIDEO);
|
||||||
|
assertThat(format.rotationDegrees).isEqualTo(180);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
clippedMediaAudioRemovedNoOpEffectAndRotated_trimOptimizationEnabled_completedWithOptimizationAppliedAndCorrectOrientation()
|
clippedMediaAudioRemovedNoOpEffectAndRotated_trimOptimizationEnabled_completedWithOptimizationAppliedAndCorrectOrientation()
|
||||||
|
@ -367,6 +367,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
checkArgument(
|
checkArgument(
|
||||||
trackType == C.TRACK_TYPE_AUDIO || trackType == C.TRACK_TYPE_VIDEO,
|
trackType == C.TRACK_TYPE_AUDIO || trackType == C.TRACK_TYPE_VIDEO,
|
||||||
"Unsupported track format: " + sampleMimeType);
|
"Unsupported track format: " + sampleMimeType);
|
||||||
|
if (trackType == C.TRACK_TYPE_VIDEO) {
|
||||||
|
format =
|
||||||
|
format
|
||||||
|
.buildUpon()
|
||||||
|
.setRotationDegrees((format.rotationDegrees + additionalRotationDegrees) % 360)
|
||||||
|
.build();
|
||||||
|
if (muxerMode == MUXER_MODE_MUX_PARTIAL) {
|
||||||
|
List<byte[]> mostCompatibleInitializationData =
|
||||||
|
getMostComatibleInitializationData(format, checkNotNull(appendVideoFormat));
|
||||||
|
if (mostCompatibleInitializationData == null) {
|
||||||
|
throw new AppendTrackFormatException("Switching to MUXER_MODE_APPEND will fail.");
|
||||||
|
}
|
||||||
|
format = format.buildUpon().setInitializationData(mostCompatibleInitializationData).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (muxerMode == MUXER_MODE_APPEND) {
|
if (muxerMode == MUXER_MODE_APPEND) {
|
||||||
if (trackType == C.TRACK_TYPE_VIDEO) {
|
if (trackType == C.TRACK_TYPE_VIDEO) {
|
||||||
checkState(contains(trackTypeToInfo, C.TRACK_TYPE_VIDEO));
|
checkState(contains(trackTypeToInfo, C.TRACK_TYPE_VIDEO));
|
||||||
@ -392,6 +408,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
throw new AppendTrackFormatException(
|
throw new AppendTrackFormatException(
|
||||||
"Video format mismatch - height: " + existingFormat.height + " != " + format.height);
|
"Video format mismatch - height: " + existingFormat.height + " != " + format.height);
|
||||||
}
|
}
|
||||||
|
if (existingFormat.rotationDegrees != format.rotationDegrees) {
|
||||||
|
throw new AppendTrackFormatException(
|
||||||
|
"Video format mismatch - rotationDegrees: "
|
||||||
|
+ existingFormat.rotationDegrees
|
||||||
|
+ " != "
|
||||||
|
+ format.rotationDegrees);
|
||||||
|
}
|
||||||
// The initialization data of the existing format is already compatible with
|
// The initialization data of the existing format is already compatible with
|
||||||
// appendVideoFormat.
|
// appendVideoFormat.
|
||||||
if (!format.initializationDataEquals(checkNotNull(appendVideoFormat))) {
|
if (!format.initializationDataEquals(checkNotNull(appendVideoFormat))) {
|
||||||
@ -439,22 +462,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
checkState(
|
checkState(
|
||||||
!contains(trackTypeToInfo, trackType), "There is already a track of type " + trackType);
|
!contains(trackTypeToInfo, trackType), "There is already a track of type " + trackType);
|
||||||
|
|
||||||
if (trackType == C.TRACK_TYPE_VIDEO) {
|
|
||||||
format =
|
|
||||||
format
|
|
||||||
.buildUpon()
|
|
||||||
.setRotationDegrees((format.rotationDegrees + additionalRotationDegrees) % 360)
|
|
||||||
.build();
|
|
||||||
if (muxerMode == MUXER_MODE_MUX_PARTIAL) {
|
|
||||||
List<byte[]> mostCompatibleInitializationData =
|
|
||||||
getMostComatibleInitializationData(format, checkNotNull(appendVideoFormat));
|
|
||||||
if (mostCompatibleInitializationData == null) {
|
|
||||||
throw new AppendTrackFormatException("Switching to MUXER_MODE_APPEND will fail.");
|
|
||||||
}
|
|
||||||
format = format.buildUpon().setInitializationData(mostCompatibleInitializationData).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureMuxerInitialized();
|
ensureMuxerInitialized();
|
||||||
TrackInfo trackInfo = new TrackInfo(format, muxer.addTrack(format));
|
TrackInfo trackInfo = new TrackInfo(format, muxer.addTrack(format));
|
||||||
trackTypeToInfo.put(trackType, trackInfo);
|
trackTypeToInfo.put(trackType, trackInfo);
|
||||||
|
@ -301,6 +301,10 @@ import org.checkerframework.dataflow.qual.Pure;
|
|||||||
// frame before encoding, so the encoded frame's width >= height, and sets
|
// frame before encoding, so the encoded frame's width >= height, and sets
|
||||||
// rotationDegrees in the output Format to ensure the frame is displayed in the correct
|
// rotationDegrees in the output Format to ensure the frame is displayed in the correct
|
||||||
// orientation.
|
// orientation.
|
||||||
|
// VideoGraph rotates the decoded video frames counter-clockwise by outputRotationDegrees.
|
||||||
|
// Instruct the muxer to signal clockwise rotation by outputRotationDegrees.
|
||||||
|
// When both VideoGraph and muxer rotations are applied, the video will be displayed the right
|
||||||
|
// way up.
|
||||||
if (requestedWidth < requestedHeight) {
|
if (requestedWidth < requestedHeight) {
|
||||||
int temp = requestedWidth;
|
int temp = requestedWidth;
|
||||||
requestedWidth = requestedHeight;
|
requestedWidth = requestedHeight;
|
||||||
@ -308,6 +312,15 @@ import org.checkerframework.dataflow.qual.Pure;
|
|||||||
outputRotationDegrees = 90;
|
outputRotationDegrees = 90;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to match the inputFormat's rotation, but preserve landscape mode.
|
||||||
|
// This is a best-effort attempt to preserve input video properties
|
||||||
|
// (helpful for trim optimization), but is not guaranteed to work when effects are applied.
|
||||||
|
if (inputFormat.rotationDegrees % 180 == outputRotationDegrees % 180) {
|
||||||
|
outputRotationDegrees = inputFormat.rotationDegrees;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotation is handled by this class. The encoder must see a landscape video with zero
|
||||||
|
// degrees rotation.
|
||||||
Format requestedEncoderFormat =
|
Format requestedEncoderFormat =
|
||||||
new Format.Builder()
|
new Format.Builder()
|
||||||
.setWidth(requestedWidth)
|
.setWidth(requestedWidth)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user