EditedMediaItem.java

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

import androidx.annotation.IntRange;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.extractor.mp4.Mp4Extractor;
import com.google.errorprone.annotations.CanIgnoreReturnValue;

/** A {@link MediaItem} with the transformations to apply to it. */
@UnstableApi
public final class EditedMediaItem {

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

    private final MediaItem mediaItem;

    private boolean removeAudio;
    private boolean removeVideo;
    private boolean flattenForSlowMotion;
    private long durationUs;
    private int frameRate;
    private Effects effects;

    /**
     * Creates an instance.
     *
     * <p>For image inputs, the values passed into {@link #setRemoveAudio}, {@link #setRemoveVideo}
     * and {@link #setFlattenForSlowMotion} will be ignored.
     *
     * @param mediaItem The {@link MediaItem} on which transformations are applied.
     */
    public Builder(MediaItem mediaItem) {
      this.mediaItem = mediaItem;
      durationUs = C.TIME_UNSET;
      frameRate = C.RATE_UNSET_INT;
      effects = Effects.EMPTY;
    }

    /**
     * Sets whether to remove the audio from the {@link MediaItem}.
     *
     * <p>The default value is {@code false}.
     *
     * <p>The audio and video cannot both be removed because the output would not contain any
     * samples.
     *
     * @param removeAudio Whether to remove the audio.
     * @return This builder.
     */
    @CanIgnoreReturnValue
    public Builder setRemoveAudio(boolean removeAudio) {
      this.removeAudio = removeAudio;
      return this;
    }

    /**
     * Sets whether to remove the video from the {@link MediaItem}.
     *
     * <p>The default value is {@code false}.
     *
     * <p>The audio and video cannot both be removed because the output would not contain any
     * samples.
     *
     * @param removeVideo Whether to remove the video.
     * @return This builder.
     */
    @CanIgnoreReturnValue
    public Builder setRemoveVideo(boolean removeVideo) {
      this.removeVideo = removeVideo;
      return this;
    }

    /**
     * Sets whether to flatten the {@link MediaItem} if it contains slow motion markers.
     *
     * <p>The default value is {@code false}.
     *
     * <p>See {@link #flattenForSlowMotion} for more information about slow motion flattening.
     *
     * <p>If using an {@link ExoPlayerAssetLoader.Factory} with a provided {@link
     * 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.
     *
     * <p>Slow motion flattening is only supported when the {@link Composition} contains exactly one
     * {@link MediaItem}.
     *
     * <p>Using slow motion flattening together with {@link MediaItem.ClippingConfiguration} is not
     * supported yet.
     *
     * @param flattenForSlowMotion Whether to flatten for slow motion.
     * @return This builder.
     */
    @CanIgnoreReturnValue
    public Builder setFlattenForSlowMotion(boolean flattenForSlowMotion) {
      // TODO(b/233986762): Support clipping with SEF flattening.
      checkArgument(
          mediaItem.clippingConfiguration.equals(MediaItem.ClippingConfiguration.UNSET)
              || !flattenForSlowMotion,
          "Slow motion flattening is not supported when clipping is requested");
      this.flattenForSlowMotion = flattenForSlowMotion;
      return this;
    }

    /**
     * Sets the duration of the output video in microseconds.
     *
     * <p>This should be set for inputs that don't have an implicit duration (e.g. images). It will
     * be ignored for inputs that do have an implicit duration (e.g. video).
     *
     * <p>No duration is set by default.
     */
    @CanIgnoreReturnValue
    public Builder setDurationUs(long durationUs) {
      checkArgument(durationUs > 0);
      this.durationUs = durationUs;
      return this;
    }

    /**
     * Sets the frame rate of the output video in frames per second.
     *
     * <p>This should be set for inputs that don't have an implicit frame rate (e.g. images). It
     * will be ignored for inputs that do have an implicit frame rate (e.g. video).
     *
     * <p>No frame rate is set by default.
     */
    // TODO(b/210593170): Remove/deprecate frameRate parameter when frameRate parameter is added to
    //     transformer.
    @CanIgnoreReturnValue
    public Builder setFrameRate(@IntRange(from = 0) int frameRate) {
      checkArgument(frameRate > 0);
      this.frameRate = frameRate;
      return this;
    }

    /**
     * Sets the {@link Effects} to apply to the {@link MediaItem}.
     *
     * <p>The default value is {@link Effects#EMPTY}.
     *
     * @param effects The {@link Effects} to apply.
     * @return This builder.
     */
    @CanIgnoreReturnValue
    public Builder setEffects(Effects effects) {
      this.effects = effects;
      return this;
    }

    /** Builds an {@link EditedMediaItem} instance. */
    public EditedMediaItem build() {
      return new EditedMediaItem(
          mediaItem,
          removeAudio,
          removeVideo,
          flattenForSlowMotion,
          durationUs,
          frameRate,
          effects);
    }
  }

  /** The {@link MediaItem} on which transformations are applied. */
  public final MediaItem mediaItem;
  /** Whether to remove the audio from the {@link #mediaItem}. */
  public final boolean removeAudio;
  /** Whether to remove the video from the {@link #mediaItem}. */
  public final boolean removeVideo;
  /**
   * Whether to flatten the {@link #mediaItem} if it contains slow motion markers.
   *
   * <p>The flattened 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.
   *
   * <p>Only Samsung Extension Format (SEF) slow motion metadata type is supported. Flattening 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>
   */
  public final boolean flattenForSlowMotion;
  /** The duration of the image in the output video, in microseconds. */
  public final long durationUs;
  /** The frame rate of the image in the output video, in frames per second. */
  @IntRange(from = 0)
  public final int frameRate;
  /** The {@link Effects} to apply to the {@link #mediaItem}. */
  public final Effects effects;

  private EditedMediaItem(
      MediaItem mediaItem,
      boolean removeAudio,
      boolean removeVideo,
      boolean flattenForSlowMotion,
      long durationUs,
      int frameRate,
      Effects effects) {
    checkState(!removeAudio || !removeVideo, "Audio and video cannot both be removed");
    this.mediaItem = mediaItem;
    this.removeAudio = removeAudio;
    this.removeVideo = removeVideo;
    this.flattenForSlowMotion = flattenForSlowMotion;
    this.durationUs = durationUs;
    this.frameRate = frameRate;
    this.effects = effects;
  }
}