SilenceMediaSource.java

/*
 * Copyright (C) 2019 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.exoplayer.source;

import static java.lang.Math.min;

import android.net.Uri;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackGroupArray;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener;
import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.FormatHolder;
import androidx.media3.exoplayer.SeekParameters;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.Allocator;
import java.util.ArrayList;
import org.checkerframework.checker.nullness.compatqual.NullableType;

/** Media source with a single period consisting of silent raw audio of a given duration. */
@UnstableApi
public final class SilenceMediaSource extends BaseMediaSource {

  /** Factory for {@link SilenceMediaSource SilenceMediaSources}. */
  public static final class Factory {

    private long durationUs;
    @Nullable private Object tag;

    /**
     * Sets the duration of the silent audio. The value needs to be a positive value.
     *
     * @param durationUs The duration of silent audio to output, in microseconds.
     * @return This factory, for convenience.
     */
    public Factory setDurationUs(@IntRange(from = 1) long durationUs) {
      this.durationUs = durationUs;
      return this;
    }

    /**
     * Sets a tag for the media source which will be published in the {@link Timeline} of the source
     * as {@link MediaItem.LocalConfiguration#tag Window#mediaItem.localConfiguration.tag}.
     *
     * @param tag A tag for the media source.
     * @return This factory, for convenience.
     */
    public Factory setTag(@Nullable Object tag) {
      this.tag = tag;
      return this;
    }

    /**
     * Creates a new {@link SilenceMediaSource}.
     *
     * @throws IllegalStateException if the duration is a non-positive value.
     */
    public SilenceMediaSource createMediaSource() {
      Assertions.checkState(durationUs > 0);
      return new SilenceMediaSource(durationUs, MEDIA_ITEM.buildUpon().setTag(tag).build());
    }
  }

  /** The media id used by any media item of silence media sources. */
  public static final String MEDIA_ID = "SilenceMediaSource";

  private static final int SAMPLE_RATE_HZ = 44100;
  @C.PcmEncoding private static final int PCM_ENCODING = C.ENCODING_PCM_16BIT;
  private static final int CHANNEL_COUNT = 2;
  private static final Format FORMAT =
      new Format.Builder()
          .setSampleMimeType(MimeTypes.AUDIO_RAW)
          .setChannelCount(CHANNEL_COUNT)
          .setSampleRate(SAMPLE_RATE_HZ)
          .setPcmEncoding(PCM_ENCODING)
          .build();
  private static final MediaItem MEDIA_ITEM =
      new MediaItem.Builder()
          .setMediaId(MEDIA_ID)
          .setUri(Uri.EMPTY)
          .setMimeType(FORMAT.sampleMimeType)
          .build();
  private static final byte[] SILENCE_SAMPLE =
      new byte[Util.getPcmFrameSize(PCM_ENCODING, CHANNEL_COUNT) * 1024];

  private final long durationUs;
  private final MediaItem mediaItem;

  /**
   * Creates a new media source providing silent audio of the given duration.
   *
   * @param durationUs The duration of silent audio to output, in microseconds.
   */
  public SilenceMediaSource(long durationUs) {
    this(durationUs, MEDIA_ITEM);
  }

  /**
   * Creates a new media source providing silent audio of the given duration.
   *
   * @param durationUs The duration of silent audio to output, in microseconds.
   * @param mediaItem The media item associated with this media source.
   */
  private SilenceMediaSource(long durationUs, MediaItem mediaItem) {
    Assertions.checkArgument(durationUs >= 0);
    this.durationUs = durationUs;
    this.mediaItem = mediaItem;
  }

  @Override
  protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
    refreshSourceInfo(
        new SinglePeriodTimeline(
            durationUs,
            /* isSeekable= */ true,
            /* isDynamic= */ false,
            /* useLiveConfiguration= */ false,
            /* manifest= */ null,
            mediaItem));
  }

  @Override
  public void maybeThrowSourceInfoRefreshError() {}

  @Override
  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
    return new SilenceMediaPeriod(durationUs);
  }

  @Override
  public void releasePeriod(MediaPeriod mediaPeriod) {}

  @Override
  public MediaItem getMediaItem() {
    return mediaItem;
  }

  @Override
  protected void releaseSourceInternal() {}

  private static final class SilenceMediaPeriod implements MediaPeriod {

    private static final TrackGroupArray TRACKS = new TrackGroupArray(new TrackGroup(FORMAT));

    private final long durationUs;
    private final ArrayList<SampleStream> sampleStreams;

    public SilenceMediaPeriod(long durationUs) {
      this.durationUs = durationUs;
      sampleStreams = new ArrayList<>();
    }

    @Override
    public void prepare(Callback callback, long positionUs) {
      callback.onPrepared(/* mediaPeriod= */ this);
    }

    @Override
    public void maybeThrowPrepareError() {}

    @Override
    public TrackGroupArray getTrackGroups() {
      return TRACKS;
    }

    @Override
    public long selectTracks(
        @NullableType ExoTrackSelection[] selections,
        boolean[] mayRetainStreamFlags,
        @NullableType SampleStream[] streams,
        boolean[] streamResetFlags,
        long positionUs) {
      positionUs = constrainSeekPosition(positionUs);
      for (int i = 0; i < selections.length; i++) {
        if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
          sampleStreams.remove(streams[i]);
          streams[i] = null;
        }
        if (streams[i] == null && selections[i] != null) {
          SilenceSampleStream stream = new SilenceSampleStream(durationUs);
          stream.seekTo(positionUs);
          sampleStreams.add(stream);
          streams[i] = stream;
          streamResetFlags[i] = true;
        }
      }
      return positionUs;
    }

    @Override
    public void discardBuffer(long positionUs, boolean toKeyframe) {}

    @Override
    public long readDiscontinuity() {
      return C.TIME_UNSET;
    }

    @Override
    public long seekToUs(long positionUs) {
      positionUs = constrainSeekPosition(positionUs);
      for (int i = 0; i < sampleStreams.size(); i++) {
        ((SilenceSampleStream) sampleStreams.get(i)).seekTo(positionUs);
      }
      return positionUs;
    }

    @Override
    public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
      return constrainSeekPosition(positionUs);
    }

    @Override
    public long getBufferedPositionUs() {
      return C.TIME_END_OF_SOURCE;
    }

    @Override
    public long getNextLoadPositionUs() {
      return C.TIME_END_OF_SOURCE;
    }

    @Override
    public boolean continueLoading(long positionUs) {
      return false;
    }

    @Override
    public boolean isLoading() {
      return false;
    }

    @Override
    public void reevaluateBuffer(long positionUs) {}

    private long constrainSeekPosition(long positionUs) {
      return Util.constrainValue(positionUs, 0, durationUs);
    }
  }

  private static final class SilenceSampleStream implements SampleStream {

    private final long durationBytes;

    private boolean sentFormat;
    private long positionBytes;

    public SilenceSampleStream(long durationUs) {
      durationBytes = getAudioByteCount(durationUs);
      seekTo(0);
    }

    public void seekTo(long positionUs) {
      positionBytes = Util.constrainValue(getAudioByteCount(positionUs), 0, durationBytes);
    }

    @Override
    public boolean isReady() {
      return true;
    }

    @Override
    public void maybeThrowError() {}

    @Override
    public int readData(
        FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
      if (!sentFormat || (readFlags & FLAG_REQUIRE_FORMAT) != 0) {
        formatHolder.format = FORMAT;
        sentFormat = true;
        return C.RESULT_FORMAT_READ;
      }

      long bytesRemaining = durationBytes - positionBytes;
      if (bytesRemaining == 0) {
        buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
        return C.RESULT_BUFFER_READ;
      }

      buffer.timeUs = getAudioPositionUs(positionBytes);
      buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME);
      int bytesToWrite = (int) min(SILENCE_SAMPLE.length, bytesRemaining);
      if ((readFlags & FLAG_OMIT_SAMPLE_DATA) == 0) {
        buffer.ensureSpaceForWrite(bytesToWrite);
        buffer.data.put(SILENCE_SAMPLE, /* offset= */ 0, bytesToWrite);
      }
      if ((readFlags & FLAG_PEEK) == 0) {
        positionBytes += bytesToWrite;
      }
      return C.RESULT_BUFFER_READ;
    }

    @Override
    public int skipData(long positionUs) {
      long oldPositionBytes = positionBytes;
      seekTo(positionUs);
      return (int) ((positionBytes - oldPositionBytes) / SILENCE_SAMPLE.length);
    }
  }

  private static long getAudioByteCount(long durationUs) {
    long audioSampleCount = durationUs * SAMPLE_RATE_HZ / C.MICROS_PER_SECOND;
    return Util.getPcmFrameSize(PCM_ENCODING, CHANNEL_COUNT) * audioSampleCount;
  }

  private static long getAudioPositionUs(long bytes) {
    long audioSampleCount = bytes / Util.getPcmFrameSize(PCM_ENCODING, CHANNEL_COUNT);
    return audioSampleCount * C.MICROS_PER_SECOND / SAMPLE_RATE_HZ;
  }
}