Presentation.java

/*
 * 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 androidx.media3.effect;

import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.graphics.Matrix;
import android.util.Pair;
import androidx.annotation.IntDef;
import androidx.media3.common.C;
import androidx.media3.common.util.UnstableApi;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;

/**
 * Controls how a frame is presented with options to set the output resolution and choose how to map
 * the input pixels onto the output frame geometry (for example, by stretching the input frame to
 * match the specified output frame, or fitting the input frame using letterboxing).
 *
 * <p>The background color of the output frame will be black, with alpha = 0 if applicable.
 */
@UnstableApi
public final class Presentation implements MatrixTransformation {

  /**
   * Strategies controlling the layout of input pixels in the output frame.
   *
   * <p>One of {@link #LAYOUT_SCALE_TO_FIT}, {@link #LAYOUT_SCALE_TO_FIT_WITH_CROP}, or {@link
   * #LAYOUT_STRETCH_TO_FIT}.
   *
   * <p>May scale either width or height, leaving the other output dimension equal to its input.
   */
  @Documented
  @Retention(SOURCE)
  @Target(TYPE_USE)
  @IntDef({LAYOUT_SCALE_TO_FIT, LAYOUT_SCALE_TO_FIT_WITH_CROP, LAYOUT_STRETCH_TO_FIT})
  public @interface Layout {}
  /**
   * Empty pixels added above and below the input frame (for letterboxing), or to the left and right
   * of the input frame (for pillarboxing), until the desired aspect ratio is achieved. All input
   * frame pixels will be within the output frame.
   *
   * <p>When applying:
   *
   * <ul>
   *   <li>letterboxing, the output width will default to the input width, and the output height
   *       will be scaled appropriately.
   *   <li>pillarboxing, the output height will default to the input height, and the output width
   *       will be scaled appropriately.
   * </ul>
   */
  public static final int LAYOUT_SCALE_TO_FIT = 0;
  /**
   * Pixels cropped from the input frame, until the desired aspect ratio is achieved. Pixels may be
   * cropped either from the bottom and top, or from the left and right sides, of the input frame.
   *
   * <p>When cropping from the:
   *
   * <ul>
   *   <li>bottom and top, the output width will default to the input width, and the output height
   *       will be scaled appropriately.
   *   <li>left and right, the output height will default to the input height, and the output width
   *       will be scaled appropriately.
   * </ul>
   */
  public static final int LAYOUT_SCALE_TO_FIT_WITH_CROP = 1;
  /**
   * Frame stretched larger on the x or y axes to fit the desired aspect ratio.
   *
   * <p>When stretching to a:
   *
   * <ul>
   *   <li>taller aspect ratio, the output width will default to the input width, and the output
   *       height will be scaled appropriately.
   *   <li>narrower aspect ratio, the output height will default to the input height, and the output
   *       width will be scaled appropriately.
   * </ul>
   */
  public static final int LAYOUT_STRETCH_TO_FIT = 2;

  private static final float ASPECT_RATIO_UNSET = -1f;

  private static void checkLayout(@Layout int layout) {
    checkArgument(
        layout == LAYOUT_SCALE_TO_FIT
            || layout == LAYOUT_SCALE_TO_FIT_WITH_CROP
            || layout == LAYOUT_STRETCH_TO_FIT,
        "invalid layout " + layout);
  }

  /**
   * Creates a new {@link Presentation} instance.
   *
   * <p>The output frame will have the given aspect ratio (width/height ratio). Width or height will
   * be resized to conform to this {@code aspectRatio}, given a {@link Layout}.
   *
   * @param aspectRatio The aspect ratio (width/height ratio) of the output frame. Must be positive.
   * @param layout The layout of the output frame.
   */
  public static Presentation createForAspectRatio(float aspectRatio, @Layout int layout) {
    checkArgument(
        aspectRatio == C.LENGTH_UNSET || aspectRatio > 0,
        "aspect ratio " + aspectRatio + " must be positive or unset");
    checkLayout(layout);
    return new Presentation(
        /* width= */ C.LENGTH_UNSET, /* height= */ C.LENGTH_UNSET, aspectRatio, layout);
  }

  /**
   * Creates a new {@link Presentation} instance.
   *
   * <p>The output frame will have the given height. Width will scale to preserve the input aspect
   * ratio.
   *
   * @param height The height of the output frame, in pixels.
   */
  public static Presentation createForHeight(int height) {
    return new Presentation(
        /* width= */ C.LENGTH_UNSET, height, ASPECT_RATIO_UNSET, LAYOUT_SCALE_TO_FIT);
  }

  /**
   * Creates a new {@link Presentation} instance.
   *
   * <p>The output frame will have the given width and height, given a {@link Layout}.
   *
   * <p>Width and height must be positive integers representing the output frame's width and height.
   *
   * @param width The width of the output frame, in pixels.
   * @param height The height of the output frame, in pixels.
   * @param layout The layout of the output frame.
   */
  public static Presentation createForWidthAndHeight(int width, int height, @Layout int layout) {
    checkArgument(width > 0, "width " + width + " must be positive");
    checkArgument(height > 0, "height " + height + " must be positive");
    checkLayout(layout);
    return new Presentation(width, height, ASPECT_RATIO_UNSET, layout);
  }

  private final int requestedWidthPixels;
  private final int requestedHeightPixels;
  private float requestedAspectRatio;
  private final @Layout int layout;

  private float outputWidth;
  private float outputHeight;
  private @MonotonicNonNull Matrix transformationMatrix;

  private Presentation(int width, int height, float aspectRatio, @Layout int layout) {
    checkArgument(
        (aspectRatio == C.LENGTH_UNSET) || (width == C.LENGTH_UNSET),
        "width and aspect ratio should not both be set");

    this.requestedWidthPixels = width;
    this.requestedHeightPixels = height;
    this.requestedAspectRatio = aspectRatio;
    this.layout = layout;

    outputWidth = C.LENGTH_UNSET;
    outputHeight = C.LENGTH_UNSET;
    transformationMatrix = new Matrix();
  }

  @Override
  public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
    checkArgument(inputWidth > 0, "inputWidth must be positive");
    checkArgument(inputHeight > 0, "inputHeight must be positive");

    transformationMatrix = new Matrix();
    outputWidth = inputWidth;
    outputHeight = inputHeight;

    if ((requestedWidthPixels != C.LENGTH_UNSET) && (requestedHeightPixels != C.LENGTH_UNSET)) {
      requestedAspectRatio = (float) requestedWidthPixels / requestedHeightPixels;
    }

    if (requestedAspectRatio != C.LENGTH_UNSET) {
      applyAspectRatio();
    }

    // Scale output width and height to requested values.
    if (requestedHeightPixels != C.LENGTH_UNSET) {
      if (requestedWidthPixels != C.LENGTH_UNSET) {
        outputWidth = requestedWidthPixels;
      } else {
        outputWidth = requestedHeightPixels * outputWidth / outputHeight;
      }
      outputHeight = requestedHeightPixels;
    }
    return Pair.create(Math.round(outputWidth), Math.round(outputHeight));
  }

  @Override
  public Matrix getMatrix(long presentationTimeUs) {
    return checkStateNotNull(transformationMatrix, "configure must be called first");
  }

  @RequiresNonNull("transformationMatrix")
  private void applyAspectRatio() {
    float inputAspectRatio = outputWidth / outputHeight;
    if (layout == LAYOUT_SCALE_TO_FIT) {
      if (requestedAspectRatio > inputAspectRatio) {
        transformationMatrix.setScale(inputAspectRatio / requestedAspectRatio, 1f);
        outputWidth = outputHeight * requestedAspectRatio;
      } else {
        transformationMatrix.setScale(1f, requestedAspectRatio / inputAspectRatio);
        outputHeight = outputWidth / requestedAspectRatio;
      }
    } else if (layout == LAYOUT_SCALE_TO_FIT_WITH_CROP) {
      if (requestedAspectRatio > inputAspectRatio) {
        transformationMatrix.setScale(1f, requestedAspectRatio / inputAspectRatio);
        outputHeight = outputWidth / requestedAspectRatio;
      } else {
        transformationMatrix.setScale(inputAspectRatio / requestedAspectRatio, 1f);
        outputWidth = outputHeight * requestedAspectRatio;
      }
    } else if (layout == LAYOUT_STRETCH_TO_FIT) {
      if (requestedAspectRatio > inputAspectRatio) {
        outputWidth = outputHeight * requestedAspectRatio;
      } else {
        outputHeight = outputWidth / requestedAspectRatio;
      }
    }
  }
}