TransformationRequest.java

/*
 * 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 static androidx.media3.common.util.Assertions.checkArgument;

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.MediaSource;
import androidx.media3.extractor.mp4.Mp4Extractor;
import com.google.common.collect.ImmutableSet;

/** A media transformation request. */
@UnstableApi
public final class TransformationRequest {

  /** A builder for {@link TransformationRequest} instances. */
  public static final class Builder {

    private static final ImmutableSet<Integer> SUPPORTED_OUTPUT_HEIGHTS =
        ImmutableSet.of(144, 240, 360, 480, 720, 1080, 1440, 2160);

    private Matrix transformationMatrix;
    private boolean flattenForSlowMotion;
    private int outputHeight;
    @Nullable private String audioMimeType;
    @Nullable private String videoMimeType;
    private boolean enableHdrEditing;

    /**
     * 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 = new Matrix(transformationRequest.transformationMatrix);
      this.flattenForSlowMotion = transformationRequest.flattenForSlowMotion;
      this.outputHeight = transformationRequest.outputHeight;
      this.audioMimeType = transformationRequest.audioMimeType;
      this.videoMimeType = transformationRequest.videoMimeType;
      this.enableHdrEditing = transformationRequest.enableHdrEditing;
    }

    /**
     * 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>The video dimensions will be on the x axis, from -aspectRatio to aspectRatio, and on the y
     * axis, from -1 to 1.
     *
     * <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).

      // TODO(b/213198690): Consider changing how transformationMatrix is applied, so that
      // dimensions will be from -1 to 1 on both x and y axes, but transformations will be applied
      // in a predictable manner.
      this.transformationMatrix = new Matrix(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 MediaSource.Factory} using {@link
     * Transformer.Builder#setMediaSourceFactory(MediaSource.Factory)}, 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 {@link C#LENGTH_UNSET}
     * corresponds to using 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.
     * @throws IllegalArgumentException If the {@code outputHeight} is not supported.
     */
    public Builder setResolution(int outputHeight) {
      // TODO(b/209781577): Define outputHeight in the javadoc as height can be ambiguous for videos
      // where rotationDegrees is set in the Format.
      // 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.
      checkArgument(
          outputHeight == C.LENGTH_UNSET || SUPPORTED_OUTPUT_HEIGHTS.contains(outputHeight),
          "Unsupported outputHeight: " + outputHeight);
      this.outputHeight = outputHeight;
      return this;
    }

    /**
     * Sets the video MIME type of the output. The default value is {@code null} which corresponds
     * to using the same MIME type as the input. Supported MIME types 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.
     * @throws IllegalArgumentException If the {@code videoMimeType} is non-null but not a video
     *     {@link MimeTypes MIME type}.
     */
    public Builder setVideoMimeType(@Nullable String videoMimeType) {
      checkArgument(
          videoMimeType == null || MimeTypes.isVideo(videoMimeType),
          "Not a video MIME type: " + videoMimeType);
      this.videoMimeType = videoMimeType;
      return this;
    }

    /**
     * Sets the audio MIME type of the output. The default value is {@code null} which corresponds
     * to using the same MIME type as the input. Supported MIME types 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.
     * @throws IllegalArgumentException If the {@code audioMimeType} is non-null but not an audio
     *     {@link MimeTypes MIME type}.
     */
    public Builder setAudioMimeType(@Nullable String audioMimeType) {
      checkArgument(
          audioMimeType == null || MimeTypes.isAudio(audioMimeType),
          "Not an audio MIME type: " + audioMimeType);
      this.audioMimeType = audioMimeType;
      return this;
    }

    /**
     * Sets whether to attempt to process any input video stream as a high dynamic range (HDR)
     * signal.
     *
     * <p>This method is experimental, and will be renamed or removed in a future release. The HDR
     * editing feature is under development and is intended for developing/testing HDR processing
     * and encoding support.
     *
     * @param enableHdrEditing Whether to attempt to process any input video stream as a high
     *     dynamic range (HDR) signal.
     * @return This builder.
     */
    public Builder experimental_setEnableHdrEditing(boolean enableHdrEditing) {
      this.enableHdrEditing = enableHdrEditing;
      return this;
    }

    /** Builds a {@link TransformationRequest} instance. */
    public TransformationRequest build() {
      return new TransformationRequest(
          transformationMatrix,
          flattenForSlowMotion,
          outputHeight,
          audioMimeType,
          videoMimeType,
          enableHdrEditing);
    }
  }

  /**
   * A {@link Matrix transformation matrix} to apply to video frames.
   *
   * @see Builder#setTransformationMatrix(Matrix)
   */
  public final Matrix transformationMatrix;
  /**
   * Whether the input should be flattened for media containing slow motion markers.
   *
   * @see Builder#setFlattenForSlowMotion(boolean)
   */
  public final boolean flattenForSlowMotion;
  /**
   * The requested height of the output video, or {@link C#LENGTH_UNSET} if inferred from the input.
   *
   * @see Builder#setResolution(int)
   */
  public final int outputHeight;
  /**
   * The requested output audio sample {@link MimeTypes MIME type}, or {@code null} if inferred from
   * the input.
   *
   * @see Builder#setAudioMimeType(String)
   */
  @Nullable public final String audioMimeType;
  /**
   * The requested output video sample {@link MimeTypes MIME type}, or {@code null} if inferred from
   * the input.
   *
   * @see Builder#setVideoMimeType(String)
   */
  @Nullable public final String videoMimeType;
  /**
   * Whether to attempt to process any input video stream as a high dynamic range (HDR) signal.
   *
   * @see Builder#experimental_setEnableHdrEditing(boolean)
   */
  public final boolean enableHdrEditing;

  private TransformationRequest(
      Matrix transformationMatrix,
      boolean flattenForSlowMotion,
      int outputHeight,
      @Nullable String audioMimeType,
      @Nullable String videoMimeType,
      boolean enableHdrEditing) {
    this.transformationMatrix = transformationMatrix;
    this.flattenForSlowMotion = flattenForSlowMotion;
    this.outputHeight = outputHeight;
    this.audioMimeType = audioMimeType;
    this.videoMimeType = videoMimeType;
    this.enableHdrEditing = enableHdrEditing;
  }

  @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)
        && enableHdrEditing == that.enableHdrEditing;
  }

  @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);
    result = 31 * result + (enableHdrEditing ? 1 : 0);
    return result;
  }

  /**
   * Returns a new {@link TransformationRequest.Builder} initialized with the values of this
   * instance.
   */
  public Builder buildUpon() {
    return new Builder(this);
  }
}