Add TransformationRequest.

PiperOrigin-RevId: 417786661
This commit is contained in:
hschlueter 2021-12-22 11:16:53 +00:00 committed by tonihei
parent 7166e091cd
commit 0e61f44d4d
14 changed files with 488 additions and 252 deletions

View File

@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import android.content.Context;
import android.graphics.Matrix;
import androidx.media3.common.MimeTypes;
import androidx.media3.transformer.TransformationRequest;
import androidx.media3.transformer.Transformer;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -43,9 +44,12 @@ public final class RepeatedTranscodeTransformationTest {
transformationMatrix.postTranslate((float) 0.1, (float) 0.1);
Transformer transformer =
new Transformer.Builder(context)
.setVideoMimeType(MimeTypes.VIDEO_H265)
.setTransformationMatrix(transformationMatrix)
.setAudioMimeType(MimeTypes.AUDIO_AMR_NB)
.setTransformationRequest(
new TransformationRequest.Builder()
.setVideoMimeType(MimeTypes.VIDEO_H265)
.setTransformationMatrix(transformationMatrix)
.setAudioMimeType(MimeTypes.AUDIO_AMR_NB)
.build())
.build();
Set<Long> differentOutputSizesBytes = new HashSet<>();
@ -74,9 +78,12 @@ public final class RepeatedTranscodeTransformationTest {
transformationMatrix.postTranslate((float) 0.1, (float) 0.1);
Transformer transformer =
new Transformer.Builder(context)
.setVideoMimeType(MimeTypes.VIDEO_H265)
.setTransformationMatrix(transformationMatrix)
.setRemoveAudio(true)
.setTransformationRequest(
new TransformationRequest.Builder()
.setVideoMimeType(MimeTypes.VIDEO_H265)
.setTransformationMatrix(transformationMatrix)
.build())
.build();
Set<Long> differentOutputSizesBytes = new HashSet<>();
@ -103,8 +110,11 @@ public final class RepeatedTranscodeTransformationTest {
Context context = ApplicationProvider.getApplicationContext();
Transformer transcodingTransformer =
new Transformer.Builder(context)
.setAudioMimeType(MimeTypes.AUDIO_AMR_NB)
.setRemoveVideo(true)
.setTransformationRequest(
new TransformationRequest.Builder()
.setAudioMimeType(MimeTypes.AUDIO_AMR_NB)
.build())
.build();
Set<Long> differentOutputSizesBytes = new HashSet<>();

View File

@ -19,6 +19,7 @@ import static androidx.media3.transformer.mh.AndroidTestUtil.SEF_ASSET_URI_STRIN
import static androidx.media3.transformer.mh.AndroidTestUtil.runTransformer;
import android.content.Context;
import androidx.media3.transformer.TransformationRequest;
import androidx.media3.transformer.Transformer;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -32,7 +33,10 @@ public class SefTransformationTest {
public void sefTransform() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
Transformer transformer =
new Transformer.Builder(context).setFlattenForSlowMotion(true).build();
new Transformer.Builder(context)
.setTransformationRequest(
new TransformationRequest.Builder().setFlattenForSlowMotion(true).build())
.build();
runTransformer(
context,
/* testId = */ "sefTransform",

View File

@ -20,6 +20,7 @@ import static androidx.media3.transformer.mh.AndroidTestUtil.runTransformer;
import android.content.Context;
import android.graphics.Matrix;
import androidx.media3.transformer.TransformationRequest;
import androidx.media3.transformer.Transformer;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -35,7 +36,12 @@ public class SetTransformationMatrixTransformationTest {
Matrix transformationMatrix = new Matrix();
transformationMatrix.postTranslate(/* dx= */ .2f, /* dy= */ .1f);
Transformer transformer =
new Transformer.Builder(context).setTransformationMatrix(transformationMatrix).build();
new Transformer.Builder(context)
.setTransformationRequest(
new TransformationRequest.Builder()
.setTransformationMatrix(transformationMatrix)
.build())
.build();
runTransformer(
context,

View File

@ -43,7 +43,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private static final int DEFAULT_ENCODER_BITRATE = 128 * 1024;
private final Format inputFormat;
private final Transformation transformation;
private final TransformationRequest transformationRequest;
private final Codec.EncoderFactory encoderFactory;
private final Codec decoder;
@ -66,12 +66,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
public AudioSamplePipeline(
Format inputFormat,
Transformation transformation,
TransformationRequest transformationRequest,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory)
throws TransformationException {
this.inputFormat = inputFormat;
this.transformation = transformation;
this.transformationRequest = transformationRequest;
this.encoderFactory = encoderFactory;
decoderInputBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
@ -294,7 +294,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
decoderOutputFormat.sampleRate,
decoderOutputFormat.channelCount,
decoderOutputFormat.pcmEncoding);
if (transformation.flattenForSlowMotion) {
if (transformationRequest.flattenForSlowMotion) {
try {
outputAudioFormat = sonicAudioProcessor.configure(outputAudioFormat);
flushSonicAndSetSpeed(currentSpeed);
@ -310,9 +310,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
encoderFactory.createForAudioEncoding(
new Format.Builder()
.setSampleMimeType(
transformation.audioMimeType == null
transformationRequest.audioMimeType == null
? inputFormat.sampleMimeType
: transformation.audioMimeType)
: transformationRequest.audioMimeType)
.setSampleRate(outputAudioFormat.sampleRate)
.setChannelCount(outputAudioFormat.channelCount)
.setAverageBitrate(DEFAULT_ENCODER_BITRATE)
@ -322,7 +322,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
private boolean isSpeedChanging(BufferInfo bufferInfo) {
if (!transformation.flattenForSlowMotion) {
if (!transformationRequest.flattenForSlowMotion) {
return false;
}
float newSpeed = speedProvider.getSpeed(bufferInfo.presentationTimeUs);

View File

@ -1,52 +0,0 @@
/*
* Copyright 2020 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 androidx.media3.transformer;
import android.graphics.Matrix;
import androidx.annotation.Nullable;
/** A media transformation configuration. */
/* package */ final class Transformation {
public final boolean removeAudio;
public final boolean removeVideo;
public final boolean flattenForSlowMotion;
public final int outputHeight;
public final Matrix transformationMatrix;
public final String containerMimeType;
@Nullable public final String audioMimeType;
@Nullable public final String videoMimeType;
public Transformation(
boolean removeAudio,
boolean removeVideo,
boolean flattenForSlowMotion,
int outputHeight,
Matrix transformationMatrix,
String containerMimeType,
@Nullable String audioMimeType,
@Nullable String videoMimeType) {
this.removeAudio = removeAudio;
this.removeVideo = removeVideo;
this.flattenForSlowMotion = flattenForSlowMotion;
this.outputHeight = outputHeight;
this.transformationMatrix = transformationMatrix;
this.containerMimeType = containerMimeType;
this.audioMimeType = audioMimeType;
this.videoMimeType = videoMimeType;
}
}

View File

@ -0,0 +1,241 @@
/*
* Copyright 2020 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 androidx.media3.transformer;
import android.graphics.Matrix;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.source.MediaSourceFactory;
import androidx.media3.extractor.mp4.Mp4Extractor;
/** A media transformation request. */
@UnstableApi
public final class TransformationRequest {
/** A builder for {@link TransformationRequest} instances. */
public static final class Builder {
private Matrix transformationMatrix;
private boolean flattenForSlowMotion;
private int outputHeight;
@Nullable private String audioMimeType;
@Nullable private String videoMimeType;
/**
* Creates a new instance with default values.
*
* <p>Use {@link TransformationRequest#buildUpon()} to obtain a builder representing an existing
* {@link TransformationRequest}.
*/
public Builder() {
transformationMatrix = new Matrix();
outputHeight = C.LENGTH_UNSET;
}
private Builder(TransformationRequest transformationRequest) {
this.transformationMatrix = transformationRequest.transformationMatrix;
this.flattenForSlowMotion = transformationRequest.flattenForSlowMotion;
this.outputHeight = transformationRequest.outputHeight;
this.audioMimeType = transformationRequest.audioMimeType;
this.videoMimeType = transformationRequest.videoMimeType;
}
/**
* Sets the transformation matrix. The default value is to apply no change.
*
* <p>This can be used to perform operations supported by {@link Matrix}, like scaling and
* rotating the video.
*
* <p>For now, resolution will not be affected by this method.
*
* @param transformationMatrix The transformation to apply to video frames.
* @return This builder.
*/
public Builder setTransformationMatrix(Matrix transformationMatrix) {
// TODO(b/201293185): After {@link #setResolution} supports arbitrary resolutions,
// allow transformations to change the resolution, by scaling to the appropriate min/max
// values. This will also be required to create the VertexTransformation class, in order to
// have aspect ratio helper methods (which require resolution to change).
this.transformationMatrix = transformationMatrix;
return this;
}
/**
* Sets whether the input should be flattened for media containing slow motion markers. The
* transformed output is obtained by removing the slow motion metadata and by actually slowing
* down the parts of the video and audio streams defined in this metadata. The default value for
* {@code flattenForSlowMotion} is {@code false}.
*
* <p>Only Samsung Extension Format (SEF) slow motion metadata type is supported. The
* transformation has no effect if the input does not contain this metadata type.
*
* <p>For SEF slow motion media, the following assumptions are made on the input:
*
* <ul>
* <li>The input container format is (unfragmented) MP4.
* <li>The input contains an AVC video elementary stream with temporal SVC.
* <li>The recording frame rate of the video is 120 or 240 fps.
* </ul>
*
* <p>If specifying a {@link MediaSourceFactory} using {@link
* Transformer.Builder#setMediaSourceFactory(MediaSourceFactory)}, make sure that {@link
* Mp4Extractor#FLAG_READ_SEF_DATA} is set on the {@link Mp4Extractor} used. Otherwise, the slow
* motion metadata will be ignored and the input won't be flattened.
*
* @param flattenForSlowMotion Whether to flatten for slow motion.
* @return This builder.
*/
public Builder setFlattenForSlowMotion(boolean flattenForSlowMotion) {
this.flattenForSlowMotion = flattenForSlowMotion;
return this;
}
/**
* Sets the output resolution using the output height. The default value is the same height as
* the input. Output width will scale to preserve the input video's aspect ratio.
*
* <p>For now, only "popular" heights like 144, 240, 360, 480, 720, 1080, 1440, or 2160 are
* supported, to ensure compatibility on different devices.
*
* <p>For example, a 1920x1440 video can be scaled to 640x480 by calling setResolution(480).
*
* @param outputHeight The output height in pixels.
* @return This builder.
*/
public Builder setResolution(int outputHeight) {
// TODO(b/201293185): Restructure to input a Presentation class.
// TODO(b/201293185): Check encoder codec capabilities in order to allow arbitrary
// resolutions and reasonable fallbacks.
if (outputHeight != 144
&& outputHeight != 240
&& outputHeight != 360
&& outputHeight != 480
&& outputHeight != 720
&& outputHeight != 1080
&& outputHeight != 1440
&& outputHeight != 2160) {
throw new IllegalArgumentException(
"Please use a height of 144, 240, 360, 480, 720, 1080, 1440, or 2160.");
}
this.outputHeight = outputHeight;
return this;
}
/**
* Sets the video MIME type of the output. The default value is to use the same MIME type as the
* input. Supported values are:
*
* <ul>
* <li>{@link MimeTypes#VIDEO_H263}
* <li>{@link MimeTypes#VIDEO_H264}
* <li>{@link MimeTypes#VIDEO_H265} from API level 24
* <li>{@link MimeTypes#VIDEO_MP4V}
* </ul>
*
* @param videoMimeType The MIME type of the video samples in the output.
* @return This builder.
*/
public Builder setVideoMimeType(String videoMimeType) {
// TODO(b/209469847): Validate videoMimeType here once deprecated
// Transformer.Builder#setOuputMimeType(String) has been removed.
this.videoMimeType = videoMimeType;
return this;
}
/**
* Sets the audio MIME type of the output. The default value is to use the same MIME type as the
* input. Supported values are:
*
* <ul>
* <li>{@link MimeTypes#AUDIO_AAC}
* <li>{@link MimeTypes#AUDIO_AMR_NB}
* <li>{@link MimeTypes#AUDIO_AMR_WB}
* </ul>
*
* @param audioMimeType The MIME type of the audio samples in the output.
* @return This builder.
*/
public Builder setAudioMimeType(String audioMimeType) {
// TODO(b/209469847): Validate audioMimeType here once deprecated
// Transformer.Builder#setOuputMimeType(String) has been removed.
this.audioMimeType = audioMimeType;
return this;
}
/** Builds a {@link TransformationRequest} instance. */
public TransformationRequest build() {
return new TransformationRequest(
transformationMatrix, flattenForSlowMotion, outputHeight, audioMimeType, videoMimeType);
}
}
public final Matrix transformationMatrix;
public final boolean flattenForSlowMotion;
public final int outputHeight;
@Nullable public final String audioMimeType;
@Nullable public final String videoMimeType;
private TransformationRequest(
Matrix transformationMatrix,
boolean flattenForSlowMotion,
int outputHeight,
@Nullable String audioMimeType,
@Nullable String videoMimeType) {
this.transformationMatrix = transformationMatrix;
this.flattenForSlowMotion = flattenForSlowMotion;
this.outputHeight = outputHeight;
this.audioMimeType = audioMimeType;
this.videoMimeType = videoMimeType;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof TransformationRequest)) {
return false;
}
TransformationRequest that = (TransformationRequest) o;
return transformationMatrix.equals(that.transformationMatrix)
&& flattenForSlowMotion == that.flattenForSlowMotion
&& outputHeight == that.outputHeight
&& Util.areEqual(audioMimeType, that.audioMimeType)
&& Util.areEqual(videoMimeType, that.videoMimeType);
}
@Override
public int hashCode() {
int result = transformationMatrix.hashCode();
result = 31 * result + (flattenForSlowMotion ? 1 : 0);
result = 31 * result + outputHeight;
result = 31 * result + (audioMimeType != null ? audioMimeType.hashCode() : 0);
result = 31 * result + (videoMimeType != null ? videoMimeType.hashCode() : 0);
return result;
}
/**
* Returns a new {@link TransformationRequest.Builder} initialized with the values of this
* instance.
*/
public Builder buildUpon() {
return new Builder(this);
}
}

View File

@ -99,12 +99,9 @@ public final class Transformer {
private Muxer.Factory muxerFactory;
private boolean removeAudio;
private boolean removeVideo;
private boolean flattenForSlowMotion;
private int outputHeight;
private Matrix transformationMatrix;
private String containerMimeType;
@Nullable private String audioMimeType;
@Nullable private String videoMimeType;
// TODO(b/204869912): Make final once deprecated setters are removed.
private TransformationRequest transformationRequest;
private Transformer.Listener listener;
private DebugViewProvider debugViewProvider;
private Looper looper;
@ -115,14 +112,13 @@ public final class Transformer {
@Deprecated
public Builder() {
muxerFactory = new FrameworkMuxer.Factory();
outputHeight = C.LENGTH_UNSET;
transformationMatrix = new Matrix();
containerMimeType = MimeTypes.VIDEO_MP4;
listener = new Listener() {};
looper = Util.getCurrentOrMainLooper();
clock = Clock.DEFAULT;
encoderFactory = Codec.EncoderFactory.DEFAULT;
debugViewProvider = DebugViewProvider.NONE;
containerMimeType = MimeTypes.VIDEO_MP4;
transformationRequest = new TransformationRequest.Builder().build();
}
/**
@ -133,14 +129,13 @@ public final class Transformer {
public Builder(Context context) {
this.context = context.getApplicationContext();
muxerFactory = new FrameworkMuxer.Factory();
outputHeight = C.LENGTH_UNSET;
transformationMatrix = new Matrix();
containerMimeType = MimeTypes.VIDEO_MP4;
listener = new Listener() {};
looper = Util.getCurrentOrMainLooper();
clock = Clock.DEFAULT;
encoderFactory = Codec.EncoderFactory.DEFAULT;
debugViewProvider = DebugViewProvider.NONE;
containerMimeType = MimeTypes.VIDEO_MP4;
this.transformationRequest = new TransformationRequest.Builder().build();
}
/** Creates a builder with the values of the provided {@link Transformer}. */
@ -148,14 +143,10 @@ public final class Transformer {
this.context = transformer.context;
this.mediaSourceFactory = transformer.mediaSourceFactory;
this.muxerFactory = transformer.muxerFactory;
this.removeAudio = transformer.transformation.removeAudio;
this.removeVideo = transformer.transformation.removeVideo;
this.flattenForSlowMotion = transformer.transformation.flattenForSlowMotion;
this.outputHeight = transformer.transformation.outputHeight;
this.transformationMatrix = transformer.transformation.transformationMatrix;
this.containerMimeType = transformer.transformation.containerMimeType;
this.audioMimeType = transformer.transformation.audioMimeType;
this.videoMimeType = transformer.transformation.videoMimeType;
this.removeAudio = transformer.removeAudio;
this.removeVideo = transformer.removeVideo;
this.containerMimeType = transformer.containerMimeType;
this.transformationRequest = transformer.transformationRequest;
this.listener = transformer.listener;
this.looper = transformer.looper;
this.encoderFactory = transformer.encoderFactory;
@ -170,6 +161,17 @@ public final class Transformer {
return this;
}
/**
* Sets the {@link TransformationRequest} which configures the editing and transcoding options.
*
* @param transformationRequest The {@link TransformationRequest}.
* @return This builder.
*/
public Builder setTransformationRequest(TransformationRequest transformationRequest) {
this.transformationRequest = transformationRequest;
return this;
}
/**
* Sets the {@link MediaSourceFactory} to be used to retrieve the inputs to transform. The
* default value is a {@link DefaultMediaSourceFactory} built with the context provided in
@ -212,83 +214,31 @@ public final class Transformer {
}
/**
* Sets whether the input should be flattened for media containing slow motion markers. The
* transformed output is obtained by removing the slow motion metadata and by actually slowing
* down the parts of the video and audio streams defined in this metadata. The default value for
* {@code flattenForSlowMotion} is {@code false}.
*
* <p>Only Samsung Extension Format (SEF) slow motion metadata type is supported. The
* transformation has no effect if the input does not contain this metadata type.
*
* <p>For SEF slow motion media, the following assumptions are made on the input:
*
* <ul>
* <li>The input container format is (unfragmented) MP4.
* <li>The input contains an AVC video elementary stream with temporal SVC.
* <li>The recording frame rate of the video is 120 or 240 fps.
* </ul>
*
* <p>If specifying a {@link MediaSourceFactory} using {@link
* #setMediaSourceFactory(MediaSourceFactory)}, make sure that {@link
* Mp4Extractor#FLAG_READ_SEF_DATA} is set on the {@link Mp4Extractor} used. Otherwise, the slow
* motion metadata will be ignored and the input won't be flattened.
*
* @param flattenForSlowMotion Whether to flatten for slow motion.
* @return This builder.
* @deprecated Use {@link TransformationRequest.Builder#setFlattenForSlowMotion(boolean)}
* instead.
*/
@Deprecated
public Builder setFlattenForSlowMotion(boolean flattenForSlowMotion) {
this.flattenForSlowMotion = flattenForSlowMotion;
transformationRequest =
transformationRequest.buildUpon().setFlattenForSlowMotion(flattenForSlowMotion).build();
return this;
}
/**
* Sets the output resolution using the output height. The default value is the same height as
* the input. Output width will scale to preserve the input video's aspect ratio.
*
* <p>For now, only "popular" heights like 144, 240, 360, 480, 720, 1080, 1440, or 2160 are
* supported, to ensure compatibility on different devices.
*
* <p>For example, a 1920x1440 video can be scaled to 640x480 by calling setResolution(480).
*
* @param outputHeight The output height in pixels.
* @return This builder.
*/
/** @deprecated Use {@link TransformationRequest.Builder#setResolution(int)} instead. */
@Deprecated
public Builder setResolution(int outputHeight) {
// TODO(Internal b/201293185): Restructure to input a Presentation class.
// TODO(Internal b/201293185): Check encoder codec capabilities in order to allow arbitrary
// resolutions and reasonable fallbacks.
if (outputHeight != 144
&& outputHeight != 240
&& outputHeight != 360
&& outputHeight != 480
&& outputHeight != 720
&& outputHeight != 1080
&& outputHeight != 1440
&& outputHeight != 2160) {
throw new IllegalArgumentException(
"Please use a height of 144, 240, 360, 480, 720, 1080, 1440, or 2160.");
}
this.outputHeight = outputHeight;
transformationRequest = transformationRequest.buildUpon().setResolution(outputHeight).build();
return this;
}
/**
* Sets the transformation matrix. The default value is to apply no change.
*
* <p>This can be used to perform operations supported by {@link Matrix}, like scaling and
* rotating the video.
*
* <p>For now, resolution will not be affected by this method.
*
* @param transformationMatrix The transformation to apply to video frames.
* @return This builder.
* @deprecated Use {@link TransformationRequest.Builder#setTransformationMatrix(Matrix)}
* instead.
*/
@Deprecated
public Builder setTransformationMatrix(Matrix transformationMatrix) {
// TODO(Internal b/201293185): After {@link #setResolution} supports arbitrary resolutions,
// allow transformations to change the resolution, by scaling to the appropriate min/max
// values. This will also be required to create the VertexTransformation class, in order to
// have aspect ratio helper methods (which require resolution to change).
this.transformationMatrix = transformationMatrix;
transformationRequest =
transformationRequest.buildUpon().setTransformationMatrix(transformationMatrix).build();
return this;
}
@ -302,40 +252,19 @@ public final class Transformer {
return this;
}
/**
* Sets the video MIME type of the output. The default value is to use the same MIME type as the
* input. Supported values are:
*
* <ul>
* <li>{@link MimeTypes#VIDEO_H263}
* <li>{@link MimeTypes#VIDEO_H264}
* <li>{@link MimeTypes#VIDEO_H265} from API level 24
* <li>{@link MimeTypes#VIDEO_MP4V}
* </ul>
*
* @param videoMimeType The MIME type of the video samples in the output.
* @return This builder.
*/
/** @deprecated Use {@link TransformationRequest.Builder#setVideoMimeType(String)} instead. */
@Deprecated
public Builder setVideoMimeType(String videoMimeType) {
this.videoMimeType = videoMimeType;
transformationRequest =
transformationRequest.buildUpon().setVideoMimeType(videoMimeType).build();
return this;
}
/**
* Sets the audio MIME type of the output. The default value is to use the same MIME type as the
* input. Supported values are:
*
* <ul>
* <li>{@link MimeTypes#AUDIO_AAC}
* <li>{@link MimeTypes#AUDIO_AMR_NB}
* <li>{@link MimeTypes#AUDIO_AMR_WB}
* </ul>
*
* @param audioMimeType The MIME type of the audio samples in the output.
* @return This builder.
*/
/** @deprecated Use {@link TransformationRequest.Builder#setAudioMimeType(String)} instead. */
@Deprecated
public Builder setAudioMimeType(String audioMimeType) {
this.audioMimeType = audioMimeType;
transformationRequest =
transformationRequest.buildUpon().setAudioMimeType(audioMimeType).build();
return this;
}
@ -434,7 +363,7 @@ public final class Transformer {
checkNotNull(context);
if (mediaSourceFactory == null) {
DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
if (flattenForSlowMotion) {
if (transformationRequest.flattenForSlowMotion) {
defaultExtractorsFactory.setMp4ExtractorFlags(Mp4Extractor.FLAG_READ_SEF_DATA);
}
mediaSourceFactory = new DefaultMediaSourceFactory(context, defaultExtractorsFactory);
@ -442,27 +371,20 @@ public final class Transformer {
checkState(
muxerFactory.supportsOutputMimeType(containerMimeType),
"Unsupported container MIME type: " + containerMimeType);
if (audioMimeType != null) {
checkSampleMimeType(audioMimeType);
if (transformationRequest.audioMimeType != null) {
checkSampleMimeType(transformationRequest.audioMimeType);
}
if (videoMimeType != null) {
checkSampleMimeType(videoMimeType);
if (transformationRequest.videoMimeType != null) {
checkSampleMimeType(transformationRequest.videoMimeType);
}
Transformation transformation =
new Transformation(
removeAudio,
removeVideo,
flattenForSlowMotion,
outputHeight,
transformationMatrix,
containerMimeType,
audioMimeType,
videoMimeType);
return new Transformer(
context,
mediaSourceFactory,
muxerFactory,
transformation,
removeAudio,
removeVideo,
containerMimeType,
transformationRequest,
listener,
looper,
clock,
@ -553,7 +475,10 @@ public final class Transformer {
private final Context context;
private final MediaSourceFactory mediaSourceFactory;
private final Muxer.Factory muxerFactory;
private final Transformation transformation;
private final boolean removeAudio;
private final boolean removeVideo;
private final String containerMimeType;
private final TransformationRequest transformationRequest;
private final Looper looper;
private final Clock clock;
private final Codec.EncoderFactory encoderFactory;
@ -569,20 +494,24 @@ public final class Transformer {
Context context,
MediaSourceFactory mediaSourceFactory,
Muxer.Factory muxerFactory,
Transformation transformation,
boolean removeAudio,
boolean removeVideo,
String containerMimeType,
TransformationRequest transformationRequest,
Transformer.Listener listener,
Looper looper,
Clock clock,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory,
Transformer.DebugViewProvider debugViewProvider) {
checkState(
!transformation.removeAudio || !transformation.removeVideo,
"Audio and video cannot both be removed.");
checkState(!removeAudio || !removeVideo, "Audio and video cannot both be removed.");
this.context = context;
this.mediaSourceFactory = mediaSourceFactory;
this.muxerFactory = muxerFactory;
this.transformation = transformation;
this.removeAudio = removeAudio;
this.removeVideo = removeVideo;
this.containerMimeType = containerMimeType;
this.transformationRequest = transformationRequest;
this.listener = listener;
this.looper = looper;
this.clock = clock;
@ -628,7 +557,7 @@ public final class Transformer {
* @throws IOException If an error occurs opening the output file for writing.
*/
public void startTransformation(MediaItem mediaItem, String path) throws IOException {
startTransformation(mediaItem, muxerFactory.create(path, transformation.containerMimeType));
startTransformation(mediaItem, muxerFactory.create(path, containerMimeType));
}
/**
@ -656,8 +585,7 @@ public final class Transformer {
@RequiresApi(26)
public void startTransformation(MediaItem mediaItem, ParcelFileDescriptor parcelFileDescriptor)
throws IOException {
startTransformation(
mediaItem, muxerFactory.create(parcelFileDescriptor, transformation.containerMimeType));
startTransformation(mediaItem, muxerFactory.create(parcelFileDescriptor, containerMimeType));
}
private void startTransformation(MediaItem mediaItem, Muxer muxer) {
@ -666,8 +594,7 @@ public final class Transformer {
throw new IllegalStateException("There is already a transformation in progress.");
}
MuxerWrapper muxerWrapper =
new MuxerWrapper(muxer, muxerFactory, transformation.containerMimeType);
MuxerWrapper muxerWrapper = new MuxerWrapper(muxer, muxerFactory, containerMimeType);
this.muxerWrapper = muxerWrapper;
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
trackSelector.setParameters(
@ -690,7 +617,9 @@ public final class Transformer {
new TransformerRenderersFactory(
context,
muxerWrapper,
transformation,
removeAudio,
removeVideo,
transformationRequest,
encoderFactory,
decoderFactory,
debugViewProvider))
@ -781,7 +710,9 @@ public final class Transformer {
private final Context context;
private final MuxerWrapper muxerWrapper;
private final TransformerMediaClock mediaClock;
private final Transformation transformation;
private final boolean removeAudio;
private final boolean removeVideo;
private final TransformationRequest transformationRequest;
private final Codec.EncoderFactory encoderFactory;
private final Codec.DecoderFactory decoderFactory;
private final Transformer.DebugViewProvider debugViewProvider;
@ -789,13 +720,17 @@ public final class Transformer {
public TransformerRenderersFactory(
Context context,
MuxerWrapper muxerWrapper,
Transformation transformation,
boolean removeAudio,
boolean removeVideo,
TransformationRequest transformationRequest,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory,
Transformer.DebugViewProvider debugViewProvider) {
this.context = context;
this.muxerWrapper = muxerWrapper;
this.transformation = transformation;
this.removeAudio = removeAudio;
this.removeVideo = removeVideo;
this.transformationRequest = transformationRequest;
this.encoderFactory = encoderFactory;
this.decoderFactory = decoderFactory;
this.debugViewProvider = debugViewProvider;
@ -809,22 +744,22 @@ public final class Transformer {
AudioRendererEventListener audioRendererEventListener,
TextOutput textRendererOutput,
MetadataOutput metadataRendererOutput) {
int rendererCount = transformation.removeAudio || transformation.removeVideo ? 1 : 2;
int rendererCount = removeAudio || removeVideo ? 1 : 2;
Renderer[] renderers = new Renderer[rendererCount];
int index = 0;
if (!transformation.removeAudio) {
if (!removeAudio) {
renderers[index] =
new TransformerAudioRenderer(
muxerWrapper, mediaClock, transformation, encoderFactory, decoderFactory);
muxerWrapper, mediaClock, transformationRequest, encoderFactory, decoderFactory);
index++;
}
if (!transformation.removeVideo) {
if (!removeVideo) {
renderers[index] =
new TransformerVideoRenderer(
context,
muxerWrapper,
mediaClock,
transformation,
transformationRequest,
encoderFactory,
decoderFactory,
debugViewProvider);

View File

@ -39,10 +39,10 @@ import androidx.media3.extractor.metadata.mp4.SlowMotionData;
public TransformerAudioRenderer(
MuxerWrapper muxerWrapper,
TransformerMediaClock mediaClock,
Transformation transformation,
TransformationRequest transformationRequest,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory) {
super(C.TRACK_TYPE_AUDIO, muxerWrapper, mediaClock, transformation);
super(C.TRACK_TYPE_AUDIO, muxerWrapper, mediaClock, transformationRequest);
this.encoderFactory = encoderFactory;
this.decoderFactory = decoderFactory;
decoderInputBuffer =
@ -69,7 +69,8 @@ import androidx.media3.extractor.metadata.mp4.SlowMotionData;
Format inputFormat = checkNotNull(formatHolder.format);
if (shouldTranscode(inputFormat)) {
samplePipeline =
new AudioSamplePipeline(inputFormat, transformation, encoderFactory, decoderFactory);
new AudioSamplePipeline(
inputFormat, transformationRequest, encoderFactory, decoderFactory);
} else {
samplePipeline = new PassthroughSamplePipeline(inputFormat);
}
@ -77,11 +78,11 @@ import androidx.media3.extractor.metadata.mp4.SlowMotionData;
}
private boolean shouldTranscode(Format inputFormat) {
if (transformation.audioMimeType != null
&& !transformation.audioMimeType.equals(inputFormat.sampleMimeType)) {
if (transformationRequest.audioMimeType != null
&& !transformationRequest.audioMimeType.equals(inputFormat.sampleMimeType)) {
return true;
}
if (transformation.flattenForSlowMotion && isSlowMotion(inputFormat)) {
if (transformationRequest.flattenForSlowMotion && isSlowMotion(inputFormat)) {
return true;
}
return false;

View File

@ -38,7 +38,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
protected final MuxerWrapper muxerWrapper;
protected final TransformerMediaClock mediaClock;
protected final Transformation transformation;
protected final TransformationRequest transformationRequest;
protected boolean isRendererStarted;
protected boolean muxerWrapperTrackAdded;
@ -50,11 +50,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
int trackType,
MuxerWrapper muxerWrapper,
TransformerMediaClock mediaClock,
Transformation transformation) {
TransformationRequest transformationRequest) {
super(trackType);
this.muxerWrapper = muxerWrapper;
this.mediaClock = mediaClock;
this.transformation = transformation;
this.transformationRequest = transformationRequest;
}
@Override
@ -65,14 +65,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
} else if ((MimeTypes.isAudio(sampleMimeType)
&& muxerWrapper.supportsSampleMimeType(
transformation.audioMimeType == null
transformationRequest.audioMimeType == null
? sampleMimeType
: transformation.audioMimeType))
: transformationRequest.audioMimeType))
|| (MimeTypes.isVideo(sampleMimeType)
&& muxerWrapper.supportsSampleMimeType(
transformation.videoMimeType == null
transformationRequest.videoMimeType == null
? sampleMimeType
: transformation.videoMimeType))) {
: transformationRequest.videoMimeType))) {
return RendererCapabilities.create(C.FORMAT_HANDLED);
} else {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE);

View File

@ -45,11 +45,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
Context context,
MuxerWrapper muxerWrapper,
TransformerMediaClock mediaClock,
Transformation transformation,
TransformationRequest transformationRequest,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory,
Transformer.DebugViewProvider debugViewProvider) {
super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformation);
super(C.TRACK_TYPE_VIDEO, muxerWrapper, mediaClock, transformationRequest);
this.context = context;
this.encoderFactory = encoderFactory;
this.decoderFactory = decoderFactory;
@ -81,29 +81,29 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
new VideoSamplePipeline(
context,
inputFormat,
transformation,
transformationRequest,
encoderFactory,
decoderFactory,
debugViewProvider);
} else {
samplePipeline = new PassthroughSamplePipeline(inputFormat);
}
if (transformation.flattenForSlowMotion) {
if (transformationRequest.flattenForSlowMotion) {
sefSlowMotionFlattener = new SefSlowMotionFlattener(inputFormat);
}
return true;
}
private boolean shouldTranscode(Format inputFormat) {
if (transformation.videoMimeType != null
&& !transformation.videoMimeType.equals(inputFormat.sampleMimeType)) {
if (transformationRequest.videoMimeType != null
&& !transformationRequest.videoMimeType.equals(inputFormat.sampleMimeType)) {
return true;
}
if (transformation.outputHeight != C.LENGTH_UNSET
&& transformation.outputHeight != inputFormat.height) {
if (transformationRequest.outputHeight != C.LENGTH_UNSET
&& transformationRequest.outputHeight != inputFormat.height) {
return true;
}
if (!transformation.transformationMatrix.isIdentity()) {
if (!transformationRequest.transformationMatrix.isIdentity()) {
return true;
}
return false;

View File

@ -50,7 +50,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public VideoSamplePipeline(
Context context,
Format inputFormat,
Transformation transformation,
TransformationRequest transformationRequest,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory,
Transformer.DebugViewProvider debugViewProvider)
@ -63,10 +63,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// TODO(internal b/209781577): Think about which edge length should be set for portrait videos.
int outputWidth = inputFormat.width;
int outputHeight = inputFormat.height;
if (transformation.outputHeight != C.LENGTH_UNSET
&& transformation.outputHeight != inputFormat.height) {
outputWidth = inputFormat.width * transformation.outputHeight / inputFormat.height;
outputHeight = transformation.outputHeight;
if (transformationRequest.outputHeight != C.LENGTH_UNSET
&& transformationRequest.outputHeight != inputFormat.height) {
outputWidth = inputFormat.width * transformationRequest.outputHeight / inputFormat.height;
outputHeight = transformationRequest.outputHeight;
}
if (inputFormat.height > inputFormat.width) {
@ -83,7 +83,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// them back for improved encoder compatibility.
// TODO(internal b/201293185): After fragment shader transformations are implemented, put
// postrotation in a later vertex shader.
transformation.transformationMatrix.postRotate(outputRotationDegrees);
transformationRequest.transformationMatrix.postRotate(outputRotationDegrees);
encoder =
encoderFactory.createForVideoEncoding(
@ -92,19 +92,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
.setHeight(outputHeight)
.setRotationDegrees(0)
.setSampleMimeType(
transformation.videoMimeType != null
? transformation.videoMimeType
transformationRequest.videoMimeType != null
? transformationRequest.videoMimeType
: inputFormat.sampleMimeType)
.build());
if (inputFormat.height != outputHeight
|| inputFormat.width != outputWidth
|| !transformation.transformationMatrix.isIdentity()) {
|| !transformationRequest.transformationMatrix.isIdentity()) {
frameEditor =
FrameEditor.create(
context,
outputWidth,
outputHeight,
transformation.transformationMatrix,
transformationRequest.transformationMatrix,
/* outputSurface= */ checkNotNull(encoder.getInputSurface()),
debugViewProvider);
}

View File

@ -0,0 +1,49 @@
/*
* 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 androidx.media3.transformer;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.Matrix;
import androidx.media3.common.MimeTypes;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link TransformationRequest}. */
@RunWith(AndroidJUnit4.class)
public class TransformationRequestTest {
@Test
public void buildUponTransformationRequest_createsEqualTransformationRequest() {
TransformationRequest request = createTestTransformationRequest();
assertThat(request.buildUpon().build()).isEqualTo(request);
}
private static TransformationRequest createTestTransformationRequest() {
Matrix transformationMatrix = new Matrix();
transformationMatrix.preRotate(36);
transformationMatrix.postTranslate((float) 0.5, (float) -0.2);
return new TransformationRequest.Builder()
.setFlattenForSlowMotion(true)
.setAudioMimeType(MimeTypes.AUDIO_AAC)
.setVideoMimeType(MimeTypes.VIDEO_H264)
.setTransformationMatrix(transformationMatrix)
.build();
}
}

View File

@ -51,4 +51,36 @@ public class TransformerBuilderTest {
IllegalStateException.class,
() -> new Transformer.Builder(context).setRemoveAudio(true).setRemoveVideo(true).build());
}
// TODO(b/209469847): Move this test to TransformationRequestBuilderTest once deprecated
// Transformer.Builder#setOuputMimeType(String) has been removed.
@Test
public void build_withUnsupportedAudioMimeType_throws() {
Context context = ApplicationProvider.getApplicationContext();
TransformationRequest transformationRequest =
new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_UNKNOWN).build();
assertThrows(
IllegalStateException.class,
() ->
new Transformer.Builder(context)
.setTransformationRequest(transformationRequest)
.build());
}
// TODO(b/209469847): Move this test to TransformationRequestBuilderTest once deprecated
// Transformer.Builder#setOuputMimeType(String) has been removed.
@Test
public void build_withUnsupportedVideoMimeType_throws() {
Context context = ApplicationProvider.getApplicationContext();
TransformationRequest transformationRequest =
new TransformationRequest.Builder().setVideoMimeType(MimeTypes.VIDEO_UNKNOWN).build();
assertThrows(
IllegalStateException.class,
() ->
new Transformer.Builder(context)
.setTransformationRequest(transformationRequest)
.build());
}
}

View File

@ -224,9 +224,10 @@ public final class TransformerTest {
public void startTransformation_flattenForSlowMotion_completesSuccessfully() throws Exception {
Transformer transformer =
new Transformer.Builder(context)
.setFlattenForSlowMotion(true)
.setClock(clock)
.setMuxerFactory(new TestMuxerFactory())
.setTransformationRequest(
new TransformationRequest.Builder().setFlattenForSlowMotion(true).build())
.build();
MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_SEF_SLOW_MOTION);
@ -243,7 +244,10 @@ public final class TransformerTest {
new Transformer.Builder(context)
.setClock(clock)
.setMuxerFactory(new TestMuxerFactory())
.setAudioMimeType(MimeTypes.AUDIO_AMR_WB) // unsupported encoder MIME type
.setTransformationRequest(
new TransformationRequest.Builder()
.setAudioMimeType(MimeTypes.AUDIO_AMR_WB) // unsupported encoder MIME type
.build())
.build();
MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_ONLY);
@ -262,7 +266,10 @@ public final class TransformerTest {
new Transformer.Builder(context)
.setClock(clock)
.setMuxerFactory(new TestMuxerFactory())
.setAudioMimeType(MimeTypes.AUDIO_AAC) // supported encoder MIME type
.setTransformationRequest(
new TransformationRequest.Builder()
.setAudioMimeType(MimeTypes.AUDIO_AAC) // supported encoder MIME type
.build())
.build();
MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_WITH_ALL_SAMPLE_FORMATS_UNSUPPORTED);
@ -281,7 +288,10 @@ public final class TransformerTest {
new Transformer.Builder(context)
.setClock(clock)
.setMuxerFactory(new TestMuxerFactory())
.setVideoMimeType(MimeTypes.VIDEO_H263) // unsupported encoder MIME type
.setTransformationRequest(
new TransformationRequest.Builder()
.setVideoMimeType(MimeTypes.VIDEO_H263) // unsupported encoder MIME type
.build())
.build();
MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY);