content: /*
   * Copyright (C) 2018 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 com.google.android.exoplayer2.extractor;
  
  import static java.lang.Math.max;
  import static java.lang.Math.min;
  
  import com.google.android.exoplayer2.C;
  
  /**
   * A {@link SeekMap} implementation that assumes the stream has a constant bitrate and consists of
   * multiple independent frames of the same size. Seek points are calculated to be at frame
   * boundaries.
   */
  public class ConstantBitrateSeekMap implements SeekMap {
  
    private final long inputLength;
    private final long firstFrameBytePosition;
    private final int frameSize;
    private final long dataSize;
    private final int bitrate;
    private final long durationUs;
    private final boolean allowSeeksIfLengthUnknown;
  
    /**
     * Creates an instance with {@code allowSeeksIfLengthUnknown} set to {@code false}.
     *
     * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
     * @param firstFrameBytePosition The byte-position of the first frame in the stream.
     * @param bitrate The bitrate (which is assumed to be constant in the stream).
     * @param frameSize The size of each frame in the stream in bytes. May be {@link C#LENGTH_UNSET}
     *     if unknown.
     */
    public ConstantBitrateSeekMap(
        long inputLength, long firstFrameBytePosition, int bitrate, int frameSize) {
      this(
          inputLength,
          firstFrameBytePosition,
          bitrate,
          frameSize,
          /* allowSeeksIfLengthUnknown= */ false);
    }
  
    /**
     * Creates an instance.
     *
     * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
     * @param firstFrameBytePosition The byte-position of the first frame in the stream.
     * @param bitrate The bitrate (which is assumed to be constant in the stream).
     * @param frameSize The size of each frame in the stream in bytes. May be {@link C#LENGTH_UNSET}
     *     if unknown.
     * @param allowSeeksIfLengthUnknown Whether to allow seeking even if the length of the content is
     *     unknown.
     */
    public ConstantBitrateSeekMap(
        long inputLength,
        long firstFrameBytePosition,
        int bitrate,
        int frameSize,
        boolean allowSeeksIfLengthUnknown) {
      this.inputLength = inputLength;
      this.firstFrameBytePosition = firstFrameBytePosition;
      this.frameSize = frameSize == C.LENGTH_UNSET ? 1 : frameSize;
      this.bitrate = bitrate;
      this.allowSeeksIfLengthUnknown = allowSeeksIfLengthUnknown;
  
      if (inputLength == C.LENGTH_UNSET) {
        dataSize = C.LENGTH_UNSET;
        durationUs = C.TIME_UNSET;
      } else {
        dataSize = inputLength - firstFrameBytePosition;
        durationUs = getTimeUsAtPosition(inputLength, firstFrameBytePosition, bitrate);
      }
    }
  
    @Override
    public boolean isSeekable() {
      return dataSize != C.LENGTH_UNSET || allowSeeksIfLengthUnknown;
    }
  
    @Override
    public SeekPoints getSeekPoints(long timeUs) {
      if (dataSize == C.LENGTH_UNSET && !allowSeeksIfLengthUnknown) {
        return new SeekPoints(new SeekPoint(0, firstFrameBytePosition));
      }
      long seekFramePosition = getFramePositionForTimeUs(timeUs);
      long seekTimeUs = getTimeUsAtPosition(seekFramePosition);
      SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekFramePosition);
      // We only return a single seek point if the length is unknown, to avoid generating a second
      // seek point beyond the end of the data in the case that the requested seek position is valid,
      // but very close to the end of the content.
      if (dataSize == C.LENGTH_UNSET
          || seekTimeUs >= timeUs
          || seekFramePosition + frameSize >= inputLength) {
        return new SeekPoints(seekPoint);
      } else {
        long secondSeekPosition = seekFramePosition + frameSize;
        long secondSeekTimeUs = getTimeUsAtPosition(secondSeekPosition);
        SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
        return new SeekPoints(seekPoint, secondSeekPoint);
      }
    }
  
    @Override
    public long getDurationUs() {
      return durationUs;
    }
  
    /**
     * Returns the stream time in microseconds for a given position.
     *
     * @param position The stream byte-position.
     * @return The stream time in microseconds for the given position.
     */
    public long getTimeUsAtPosition(long position) {
      return getTimeUsAtPosition(position, firstFrameBytePosition, bitrate);
    }
  
    // Internal methods
  
    /**
     * Returns the stream time in microseconds for a given stream position.
     *
     * @param position The stream byte-position.
     * @param firstFrameBytePosition The position of the first frame in the stream.
     * @param bitrate The bitrate (which is assumed to be constant in the stream).
     * @return The stream time in microseconds for the given stream position.
     */
    private static long getTimeUsAtPosition(long position, long firstFrameBytePosition, int bitrate) {
      return max(0, position - firstFrameBytePosition)
          * C.BITS_PER_BYTE
          * C.MICROS_PER_SECOND
          / bitrate;
    }
  
    private long getFramePositionForTimeUs(long timeUs) {
      long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * C.BITS_PER_BYTE);
      // Constrain to nearest preceding frame offset.
      positionOffset = (positionOffset / frameSize) * frameSize;
      if (dataSize != C.LENGTH_UNSET) {
        positionOffset = min(positionOffset, dataSize - frameSize);
      }
      positionOffset = max(positionOffset, 0);
      return firstFrameBytePosition + positionOffset;
    }
  }
  