Commit d20f6849 by hschlueter Committed by Marc Baechinger

Use FrameProcessorChain#SurfaceProvider for encoder compat transform.

This change adds a SurfaceProvider interface which is necessary to
allow for texture processors whose output size becomes available
asynchronously in follow-ups.
VTSP's implementation of this interface wraps the encoder and provides
its input surface together with the output frame width, height, and
orientation as used for encoder configuration.
The FrameProcessorChain converts the output frames to the provided
orientation and resolution using a ScaleToFitTransformation and
Presentation replacing EncoderCompatibilityTransformation.

PiperOrigin-RevId: 455112598
parent e5260bee
......@@ -63,6 +63,8 @@ public final class FrameProcessorChainPixelTest {
"media/bitmap/sample_mp4_first_frame/translate_right.png";
public static final String ROTATE_THEN_TRANSLATE_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/rotate_then_translate.png";
public static final String ROTATE_THEN_SCALE_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/rotate45_then_scale2w.png";
public static final String TRANSLATE_THEN_ROTATE_PNG_ASSET_PATH =
"media/bitmap/sample_mp4_first_frame/translate_then_rotate.png";
public static final String REQUEST_OUTPUT_HEIGHT_PNG_ASSET_PATH =
......@@ -88,7 +90,7 @@ public final class FrameProcessorChainPixelTest {
new AtomicReference<>();
private @MonotonicNonNull FrameProcessorChain frameProcessorChain;
private @MonotonicNonNull ImageReader outputImageReader;
private volatile @MonotonicNonNull ImageReader outputImageReader;
private @MonotonicNonNull MediaFormat mediaFormat;
@After
......@@ -261,6 +263,30 @@ public final class FrameProcessorChainPixelTest {
}
@Test
public void processData_withTwoWrappedScaleToFitTransformations_producesExpectedOutput()
throws Exception {
String testId = "processData_withTwoWrappedScaleToFitTransformations";
setUpAndPrepareFirstFrame(
DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO,
new GlEffectWrapper(new ScaleToFitTransformation.Builder().setRotationDegrees(45).build()),
new GlEffectWrapper(
new ScaleToFitTransformation.Builder()
.setScale(/* scaleX= */ 2, /* scaleY= */ 1)
.build()));
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_THEN_SCALE_PNG_ASSET_PATH);
Bitmap actualBitmap = processFirstFrameAndEnd();
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
testId, /* bitmapLabel= */ "actual", actualBitmap);
// TODO(b/207848601): switch to using proper tooling for testing against golden data.
float averagePixelAbsoluteDifference =
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void
processData_withManyComposedMatrixTransformations_producesSameOutputAsCombinedTransformation()
throws Exception {
......@@ -325,27 +351,27 @@ public final class FrameProcessorChainPixelTest {
int inputWidth = checkNotNull(mediaFormat).getInteger(MediaFormat.KEY_WIDTH);
int inputHeight = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
frameProcessorChain =
FrameProcessorChain.create(
context,
/* listener= */ this.frameProcessingException::set,
pixelWidthHeightRatio,
inputWidth,
inputHeight,
/* streamOffsetUs= */ 0L,
effects,
/* enableExperimentalHdrEditing= */ false);
Size outputSize = frameProcessorChain.getOutputSize();
outputImageReader =
ImageReader.newInstance(
outputSize.getWidth(),
outputSize.getHeight(),
PixelFormat.RGBA_8888,
/* maxImages= */ 1);
frameProcessorChain.setOutputSurface(
outputImageReader.getSurface(),
outputSize.getWidth(),
outputSize.getHeight(),
/* debugSurfaceView= */ null);
checkNotNull(
FrameProcessorChain.create(
context,
/* listener= */ this.frameProcessingException::set,
pixelWidthHeightRatio,
inputWidth,
inputHeight,
/* streamOffsetUs= */ 0L,
effects,
/* outputSurfaceProvider= */ (requestedWidth, requestedHeight) -> {
outputImageReader =
ImageReader.newInstance(
requestedWidth,
requestedHeight,
PixelFormat.RGBA_8888,
/* maxImages= */ 1);
return new SurfaceInfo(
outputImageReader.getSurface(), requestedWidth, requestedHeight);
},
Transformer.DebugViewProvider.NONE,
/* enableExperimentalHdrEditing= */ false));
frameProcessorChain.registerInputFrame();
// Queue the first video frame from the extractor.
......@@ -437,4 +463,27 @@ public final class FrameProcessorChainPixelTest {
return checkStateNotNull(adjustedTransformationMatrix);
}
}
/**
* Wraps a {@link GlEffect} to prevent the {@link FrameProcessorChain} from detecting its class
* and optimizing it.
*
* <p>This ensures that {@link FrameProcessorChain} uses a separate {@link GlTextureProcessor} for
* the wrapped {@link GlEffect} rather than merging it with preceding or subsequent {@link
* GlEffect} instances and applying them in one combined {@link GlTextureProcessor}.
*/
private static final class GlEffectWrapper implements GlEffect {
private final GlEffect effect;
public GlEffectWrapper(GlEffect effect) {
this.effect = effect;
}
@Override
public SingleFrameGlTextureProcessor toGlTextureProcessor(Context context)
throws FrameProcessingException {
return effect.toGlTextureProcessor(context);
}
}
}
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.transformer;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.util.Size;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Tests for creating and configuring a {@link FrameProcessorChain}.
*
* <p>See {@link FrameProcessorChainPixelTest} for data processing tests.
*/
@RunWith(AndroidJUnit4.class)
public final class FrameProcessorChainTest {
private final AtomicReference<FrameProcessingException> frameProcessingException =
new AtomicReference<>();
@Test
public void getOutputSize_noOperation_returnsInputSize() throws Exception {
Size inputSize = new Size(200, 100);
FrameProcessorChain frameProcessorChain =
createFrameProcessorChainWithFakeTextureProcessors(
/* pixelWidthHeightRatio= */ 1f,
inputSize,
/* textureProcessorOutputSizes= */ ImmutableList.of());
Size outputSize = frameProcessorChain.getOutputSize();
assertThat(outputSize).isEqualTo(inputSize);
assertThat(frameProcessingException.get()).isNull();
}
@Test
public void getOutputSize_withWidePixels_returnsWiderOutputSize() throws Exception {
Size inputSize = new Size(200, 100);
FrameProcessorChain frameProcessorChain =
createFrameProcessorChainWithFakeTextureProcessors(
/* pixelWidthHeightRatio= */ 2f,
inputSize,
/* textureProcessorOutputSizes= */ ImmutableList.of());
Size outputSize = frameProcessorChain.getOutputSize();
assertThat(outputSize).isEqualTo(new Size(400, 100));
assertThat(frameProcessingException.get()).isNull();
}
@Test
public void getOutputSize_withTallPixels_returnsTallerOutputSize() throws Exception {
Size inputSize = new Size(200, 100);
FrameProcessorChain frameProcessorChain =
createFrameProcessorChainWithFakeTextureProcessors(
/* pixelWidthHeightRatio= */ .5f,
inputSize,
/* textureProcessorOutputSizes= */ ImmutableList.of());
Size outputSize = frameProcessorChain.getOutputSize();
assertThat(outputSize).isEqualTo(new Size(200, 200));
assertThat(frameProcessingException.get()).isNull();
}
@Test
public void getOutputSize_withOneTextureProcessor_returnsItsOutputSize() throws Exception {
Size inputSize = new Size(200, 100);
Size textureProcessorOutputSize = new Size(300, 250);
FrameProcessorChain frameProcessorChain =
createFrameProcessorChainWithFakeTextureProcessors(
/* pixelWidthHeightRatio= */ 1f,
inputSize,
/* textureProcessorOutputSizes= */ ImmutableList.of(textureProcessorOutputSize));
Size frameProcessorChainOutputSize = frameProcessorChain.getOutputSize();
assertThat(frameProcessorChainOutputSize).isEqualTo(textureProcessorOutputSize);
assertThat(frameProcessingException.get()).isNull();
}
@Test
public void getOutputSize_withThreeTextureProcessors_returnsLastOutputSize() throws Exception {
Size inputSize = new Size(200, 100);
Size outputSize1 = new Size(300, 250);
Size outputSize2 = new Size(400, 244);
Size outputSize3 = new Size(150, 160);
FrameProcessorChain frameProcessorChain =
createFrameProcessorChainWithFakeTextureProcessors(
/* pixelWidthHeightRatio= */ 1f,
inputSize,
/* textureProcessorOutputSizes= */ ImmutableList.of(
outputSize1, outputSize2, outputSize3));
Size frameProcessorChainOutputSize = frameProcessorChain.getOutputSize();
assertThat(frameProcessorChainOutputSize).isEqualTo(outputSize3);
assertThat(frameProcessingException.get()).isNull();
}
private FrameProcessorChain createFrameProcessorChainWithFakeTextureProcessors(
float pixelWidthHeightRatio, Size inputSize, List<Size> textureProcessorOutputSizes)
throws FrameProcessingException {
ImmutableList.Builder<GlEffect> effects = new ImmutableList.Builder<>();
for (Size element : textureProcessorOutputSizes) {
effects.add((Context context) -> new FakeTextureProcessor(element));
}
return FrameProcessorChain.create(
getApplicationContext(),
/* listener= */ this.frameProcessingException::set,
pixelWidthHeightRatio,
inputSize.getWidth(),
inputSize.getHeight(),
/* streamOffsetUs= */ 0L,
effects.build(),
/* enableExperimentalHdrEditing= */ false);
}
private static class FakeTextureProcessor extends SingleFrameGlTextureProcessor {
private final Size outputSize;
private FakeTextureProcessor(Size outputSize) {
this.outputSize = outputSize;
}
@Override
public Size configure(int inputWidth, int inputHeight) {
return outputSize;
}
@Override
public void drawFrame(int inputTexId, long presentationTimeNs) {}
}
}
......@@ -18,12 +18,15 @@ package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING;
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_URI_STRING;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.content.Context;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
......@@ -34,9 +37,10 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class TransformerEndToEndTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void videoEditing_completesWithConsistentFrameCount() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
Transformer transformer =
new Transformer.Builder(context)
.setTransformationRequest(
......@@ -61,7 +65,6 @@ public class TransformerEndToEndTest {
@Test
public void videoOnly_completesWithConsistentDuration() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
Transformer transformer =
new Transformer.Builder(context)
.setRemoveAudio(true)
......@@ -85,7 +88,6 @@ public class TransformerEndToEndTest {
@Test
public void clippedMedia_completesWithClippedDuration() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
Transformer transformer = new Transformer.Builder(context).build();
long clippingStartMs = 10_000;
long clippingEndMs = 11_000;
......@@ -106,4 +108,65 @@ public class TransformerEndToEndTest {
assertThat(result.transformationResult.durationMs).isAtMost(clippingEndMs - clippingStartMs);
}
@Test
public void videoEncoderFormatUnsupported_completesWithError() {
Transformer transformer =
new Transformer.Builder(context)
.setEncoderFactory(new VideoUnsupportedEncoderFactory(context))
.setRemoveAudio(true)
.build();
TransformationException exception =
assertThrows(
TransformationException.class,
() ->
new TransformerAndroidTestRunner.Builder(context, transformer)
.build()
.run(
/* testId= */ "videoEncoderFormatUnsupported_completesWithError",
MediaItem.fromUri(Uri.parse(MP4_ASSET_URI_STRING))));
assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
assertThat(exception.errorCode)
.isEqualTo(TransformationException.ERROR_CODE_ENCODER_INIT_FAILED);
assertThat(exception).hasMessageThat().contains("video");
}
private static final class VideoUnsupportedEncoderFactory implements Codec.EncoderFactory {
private final Codec.EncoderFactory encoderFactory;
public VideoUnsupportedEncoderFactory(Context context) {
encoderFactory = new DefaultEncoderFactory(context);
}
@Override
public Codec createForAudioEncoding(Format format, List<String> allowedMimeTypes)
throws TransformationException {
return encoderFactory.createForAudioEncoding(format, allowedMimeTypes);
}
@Override
public Codec createForVideoEncoding(Format format, List<String> allowedMimeTypes)
throws TransformationException {
throw TransformationException.createForCodec(
new IllegalArgumentException(),
/* isVideo= */ true,
/* isDecoder= */ false,
format,
/* mediaCodecName= */ null,
TransformationException.ERROR_CODE_ENCODER_INIT_FAILED);
}
@Override
public boolean audioNeedsEncoding() {
return false;
}
@Override
public boolean videoNeedsEncoding() {
return true;
}
}
}
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import android.graphics.Matrix;
import android.util.Size;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Specifies a {@link Format#rotationDegrees} to apply to each frame for encoder compatibility, if
* needed.
*
* <p>Encoders commonly support higher maximum widths than maximum heights. This may rotate the
* decoded frame before encoding, so the encoded frame's width >= height, and set {@link
* Format#rotationDegrees} to ensure the frame is displayed in the correct orientation.
*/
/* package */ class EncoderCompatibilityTransformation implements MatrixTransformation {
// TODO(b/218488308): Allow reconfiguration of the output size, as encoders may not support the
// requested output resolution.
private int outputRotationDegrees;
private @MonotonicNonNull Matrix transformationMatrix;
/** Creates a new instance. */
public EncoderCompatibilityTransformation() {
outputRotationDegrees = C.LENGTH_UNSET;
}
@Override
public Size configure(int inputWidth, int inputHeight) {
checkArgument(inputWidth > 0, "inputWidth must be positive");
checkArgument(inputHeight > 0, "inputHeight must be positive");
transformationMatrix = new Matrix();
if (inputHeight > inputWidth) {
outputRotationDegrees = 90;
transformationMatrix.postRotate(outputRotationDegrees);
return new Size(inputHeight, inputWidth);
} else {
outputRotationDegrees = 0;
return new Size(inputWidth, inputHeight);
}
}
@Override
public Matrix getMatrix(long presentationTimeUs) {
return checkStateNotNull(transformationMatrix, "configure must be called first");
}
/**
* Returns {@link Format#rotationDegrees} for the output frame.
*
* <p>Return values may be {@code 0} or {@code 90} degrees.
*
* <p>Should only be called after {@linkplain #configure(int, int) configuration}.
*/
public int getOutputRotationDegrees() {
checkState(
outputRotationDegrees != C.LENGTH_UNSET,
"configure must be called before getOutputRotationDegrees");
return outputRotationDegrees;
}
}
......@@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.content.Context;
......@@ -131,16 +130,7 @@ import java.util.Arrays;
@Override
public Size configure(int inputWidth, int inputHeight) {
checkArgument(inputWidth > 0, "inputWidth must be positive");
checkArgument(inputHeight > 0, "inputHeight must be positive");
Size outputSize = new Size(inputWidth, inputHeight);
for (int i = 0; i < matrixTransformations.size(); i++) {
outputSize =
matrixTransformations.get(i).configure(outputSize.getWidth(), outputSize.getHeight());
}
return outputSize;
return MatrixUtils.configureAndGetOutputSize(inputWidth, inputHeight, matrixTransformations);
}
@Override
......
......@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import android.opengl.Matrix;
import android.util.Size;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
......@@ -217,6 +218,26 @@ import java.util.Arrays;
return transformedPoints.build();
}
/**
* Returns the output frame {@link Size} after applying the given list of {@link
* GlMatrixTransformation GlMatrixTransformations} to an input frame with the given size.
*/
public static Size configureAndGetOutputSize(
int inputWidth,
int inputHeight,
ImmutableList<GlMatrixTransformation> matrixTransformations) {
checkArgument(inputWidth > 0, "inputWidth must be positive");
checkArgument(inputHeight > 0, "inputHeight must be positive");
Size outputSize = new Size(inputWidth, inputHeight);
for (int i = 0; i < matrixTransformations.size(); i++) {
outputSize =
matrixTransformations.get(i).configure(outputSize.getWidth(), outputSize.getHeight());
}
return outputSize;
}
/** Class only contains static methods. */
private MatrixUtils() {}
}
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import android.view.Surface;
import androidx.annotation.Nullable;
/** Immutable value class for a {@link Surface} and supporting information. */
/* package */ final class SurfaceInfo {
/** The {@link Surface}. */
public final Surface surface;
/** The width of frames rendered to the {@link #surface}, in pixels. */
public final int width;
/** The height of frames rendered to the {@link #surface}, in pixels. */
public final int height;
/**
* A counter-clockwise rotation to apply to frames before rendering them to the {@link #surface}.
*
* <p>Must be 0, 90, 180, or 270 degrees. Default is 0.
*/
public final int orientationDegrees;
/** Creates a new instance. */
public SurfaceInfo(Surface surface, int width, int height) {
this(surface, width, height, /* orientationDegrees= */ 0);
}
/** Creates a new instance. */
public SurfaceInfo(Surface surface, int width, int height, int orientationDegrees) {
checkArgument(
orientationDegrees == 0
|| orientationDegrees == 90
|| orientationDegrees == 180
|| orientationDegrees == 270,
"orientationDegrees must be 0, 90, 180, or 270");
this.surface = surface;
this.width = width;
this.height = height;
this.orientationDegrees = orientationDegrees;
}
/** A provider for a {@link SurfaceInfo} instance. */
public interface Provider {
/**
* Provides a {@linkplain SurfaceInfo surface} for the requested dimensions.
*
* <p>The dimensions given in the provided {@link SurfaceInfo} may differ from the requested
* dimensions. It is up to the caller to transform frames from the requested dimensions to the
* provided dimensions before rendering them to the {@link SurfaceInfo#surface}.
*/
@Nullable
SurfaceInfo getSurfaceInfo(int requestedWidth, int requestedHeight);
}
}
......@@ -106,10 +106,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
encoderFactory,
muxerWrapper.getSupportedSampleMimeTypes(getTrackType()),
fallbackListener,
/* frameProcessorChainListener= */ exception ->
asyncErrorListener.onTransformationException(
TransformationException.createForFrameProcessorChain(
exception, TransformationException.ERROR_CODE_GL_PROCESSING_FAILED)),
asyncErrorListener,
debugViewProvider);
}
if (transformationRequest.flattenForSlowMotion) {
......
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.transformer;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.util.Size;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link EncoderCompatibilityTransformation}. */
@RunWith(AndroidJUnit4.class)
public final class EncoderCompatibilityTransformationTest {
@Test
public void configure_noEditsLandscape_leavesOrientationUnchanged() {
int inputWidth = 200;
int inputHeight = 150;
EncoderCompatibilityTransformation encoderCompatibilityTransformation =
new EncoderCompatibilityTransformation();
Size outputSize = encoderCompatibilityTransformation.configure(inputWidth, inputHeight);
assertThat(encoderCompatibilityTransformation.getOutputRotationDegrees()).isEqualTo(0);
assertThat(outputSize.getWidth()).isEqualTo(inputWidth);
assertThat(outputSize.getHeight()).isEqualTo(inputHeight);
}
@Test
public void configure_noEditsSquare_leavesOrientationUnchanged() {
int inputWidth = 150;
int inputHeight = 150;
EncoderCompatibilityTransformation encoderCompatibilityTransformation =
new EncoderCompatibilityTransformation();
Size outputSize = encoderCompatibilityTransformation.configure(inputWidth, inputHeight);
assertThat(encoderCompatibilityTransformation.getOutputRotationDegrees()).isEqualTo(0);
assertThat(outputSize.getWidth()).isEqualTo(inputWidth);
assertThat(outputSize.getHeight()).isEqualTo(inputHeight);
}
@Test
public void configure_noEditsPortrait_flipsOrientation() {
int inputWidth = 150;
int inputHeight = 200;
EncoderCompatibilityTransformation encoderCompatibilityTransformation =
new EncoderCompatibilityTransformation();
Size outputSize = encoderCompatibilityTransformation.configure(inputWidth, inputHeight);
assertThat(encoderCompatibilityTransformation.getOutputRotationDegrees()).isEqualTo(90);
assertThat(outputSize.getWidth()).isEqualTo(inputHeight);
assertThat(outputSize.getHeight()).isEqualTo(inputWidth);
}
@Test
public void getOutputRotationDegreesBeforeConfigure_throwsIllegalStateException() {
EncoderCompatibilityTransformation encoderCompatibilityTransformation =
new EncoderCompatibilityTransformation();
// configure not called before getOutputRotationDegrees.
assertThrows(
IllegalStateException.class, encoderCompatibilityTransformation::getOutputRotationDegrees);
}
}
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.transformer;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.net.Uri;
import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ListenerSet;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link VideoTranscodingSamplePipeline.EncoderWrapper}. */
@RunWith(AndroidJUnit4.class)
public final class VideoEncoderWrapperTest {
private final TransformationRequest emptyTransformationRequest =
new TransformationRequest.Builder().build();
private final FakeVideoEncoderFactory fakeEncoderFactory = new FakeVideoEncoderFactory();
private final FallbackListener fallbackListener =
new FallbackListener(
MediaItem.fromUri(Uri.EMPTY),
new ListenerSet<>(Looper.myLooper(), Clock.DEFAULT, (listener, flags) -> {}),
emptyTransformationRequest);
private final VideoTranscodingSamplePipeline.EncoderWrapper encoderWrapper =
new VideoTranscodingSamplePipeline.EncoderWrapper(
fakeEncoderFactory,
/* inputFormat= */ new Format.Builder().build(),
/* allowedOutputMimeTypes= */ ImmutableList.of(),
emptyTransformationRequest,
fallbackListener,
new AtomicReference<>());
@Before
public void registerTrack() {
fallbackListener.registerTrack();
}
@Test
public void getSurfaceInfo_landscape_leavesOrientationUnchanged() {
int inputWidth = 200;
int inputHeight = 150;
SurfaceInfo surfaceInfo = encoderWrapper.getSurfaceInfo(inputWidth, inputHeight);
assertThat(surfaceInfo.orientationDegrees).isEqualTo(0);
assertThat(surfaceInfo.width).isEqualTo(inputWidth);
assertThat(surfaceInfo.height).isEqualTo(inputHeight);
}
@Test
public void getSurfaceInfo_square_leavesOrientationUnchanged() {
int inputWidth = 150;
int inputHeight = 150;
SurfaceInfo surfaceInfo = encoderWrapper.getSurfaceInfo(inputWidth, inputHeight);
assertThat(surfaceInfo.orientationDegrees).isEqualTo(0);
assertThat(surfaceInfo.width).isEqualTo(inputWidth);
assertThat(surfaceInfo.height).isEqualTo(inputHeight);
}
@Test
public void getSurfaceInfo_portrait_flipsOrientation() {
int inputWidth = 150;
int inputHeight = 200;
SurfaceInfo surfaceInfo = encoderWrapper.getSurfaceInfo(inputWidth, inputHeight);
assertThat(surfaceInfo.orientationDegrees).isEqualTo(90);
assertThat(surfaceInfo.width).isEqualTo(inputHeight);
assertThat(surfaceInfo.height).isEqualTo(inputWidth);
}
@Test
public void getSurfaceInfo_withEncoderFallback_usesFallbackResolution() {
int inputWidth = 200;
int inputHeight = 150;
int fallbackWidth = 100;
int fallbackHeight = 75;
fakeEncoderFactory.setFallbackResolution(fallbackWidth, fallbackHeight);
SurfaceInfo surfaceInfo = encoderWrapper.getSurfaceInfo(inputWidth, inputHeight);
assertThat(surfaceInfo.orientationDegrees).isEqualTo(0);
assertThat(surfaceInfo.width).isEqualTo(fallbackWidth);
assertThat(surfaceInfo.height).isEqualTo(fallbackHeight);
}
private static class FakeVideoEncoderFactory implements Codec.EncoderFactory {
private int fallbackWidth;
private int fallbackHeight;
public FakeVideoEncoderFactory() {
fallbackWidth = C.LENGTH_UNSET;
fallbackHeight = C.LENGTH_UNSET;
}
public void setFallbackResolution(int fallbackWidth, int fallbackHeight) {
this.fallbackWidth = fallbackWidth;
this.fallbackHeight = fallbackHeight;
}
@Override
public Codec createForAudioEncoding(Format format, List<String> allowedMimeTypes) {
throw new UnsupportedOperationException();
}
@Override
public Codec createForVideoEncoding(Format format, List<String> allowedMimeTypes) {
Codec mockEncoder = mock(Codec.class);
if (fallbackWidth != C.LENGTH_UNSET) {
format = format.buildUpon().setWidth(fallbackWidth).build();
}
if (fallbackHeight != C.LENGTH_UNSET) {
format = format.buildUpon().setHeight(fallbackHeight).build();
}
when(mockEncoder.getConfigurationFormat()).thenReturn(format);
return mockEncoder;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment