content: /*
   * Copyright (C) 2016 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.ogg;
  
  import static com.google.android.exoplayer2.extractor.ExtractorUtil.skipFullyQuietly;
  
  import androidx.annotation.Nullable;
  import androidx.annotation.VisibleForTesting;
  import com.google.android.exoplayer2.C;
  import com.google.android.exoplayer2.ParserException;
  import com.google.android.exoplayer2.extractor.ExtractorInput;
  import com.google.android.exoplayer2.extractor.SeekMap;
  import com.google.android.exoplayer2.extractor.SeekPoint;
  import com.google.android.exoplayer2.util.Assertions;
  import com.google.android.exoplayer2.util.Util;
  import java.io.EOFException;
  import java.io.IOException;
  
  /** Seeks in an Ogg stream. */
  /* package */ final class DefaultOggSeeker implements OggSeeker {
  
    private static final int MATCH_RANGE = 72_000;
    private static final int MATCH_BYTE_RANGE = 100_000;
    private static final int DEFAULT_OFFSET = 30_000;
  
    private static final int STATE_SEEK_TO_END = 0;
    private static final int STATE_READ_LAST_PAGE = 1;
    private static final int STATE_SEEK = 2;
    private static final int STATE_SKIP = 3;
    private static final int STATE_IDLE = 4;
  
    private final OggPageHeader pageHeader;
    private final long payloadStartPosition;
    private final long payloadEndPosition;
    private final StreamReader streamReader;
  
    private int state;
    private long totalGranules;
    private long positionBeforeSeekToEnd;
    private long targetGranule;
  
    private long start;
    private long end;
    private long startGranule;
    private long endGranule;
  
    /**
     * Constructs an OggSeeker.
     *
     * @param streamReader The {@link StreamReader} that owns this seeker.
     * @param payloadStartPosition Start position of the payload (inclusive).
     * @param payloadEndPosition End position of the payload (exclusive).
     * @param firstPayloadPageSize The total size of the first payload page, in bytes.
     * @param firstPayloadPageGranulePosition The granule position of the first payload page.
     * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page.
     */
    public DefaultOggSeeker(
        StreamReader streamReader,
        long payloadStartPosition,
        long payloadEndPosition,
        long firstPayloadPageSize,
        long firstPayloadPageGranulePosition,
        boolean firstPayloadPageIsLastPage) {
      Assertions.checkArgument(
          payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition);
      this.streamReader = streamReader;
      this.payloadStartPosition = payloadStartPosition;
      this.payloadEndPosition = payloadEndPosition;
      if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition
          || firstPayloadPageIsLastPage) {
        totalGranules = firstPayloadPageGranulePosition;
        state = STATE_IDLE;
      } else {
        state = STATE_SEEK_TO_END;
      }
      pageHeader = new OggPageHeader();
    }
  
    @Override
    public long read(ExtractorInput input) throws IOException {
      switch (state) {
        case STATE_IDLE:
          return -1;
        case STATE_SEEK_TO_END:
          positionBeforeSeekToEnd = input.getPosition();
          state = STATE_READ_LAST_PAGE;
          // Seek to the end just before the last page of stream to get the duration.
          long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE;
          if (lastPageSearchPosition > positionBeforeSeekToEnd) {
            return lastPageSearchPosition;
          }
          // Fall through.
        case STATE_READ_LAST_PAGE:
          totalGranules = readGranuleOfLastPage(input);
          state = STATE_IDLE;
          return positionBeforeSeekToEnd;
        case STATE_SEEK:
          long position = getNextSeekPosition(input);
          if (position != C.POSITION_UNSET) {
            return position;
          }
          state = STATE_SKIP;
          // Fall through.
        case STATE_SKIP:
          skipToPageOfTargetGranule(input);
          state = STATE_IDLE;
          return -(startGranule + 2);
        default:
          // Never happens.
          throw new IllegalStateException();
      }
    }
  
    @Override
    @Nullable
    public OggSeekMap createSeekMap() {
      return totalGranules != 0 ? new OggSeekMap() : null;
    }
  
    @Override
    public void startSeek(long targetGranule) {
      this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1);
      state = STATE_SEEK;
      start = payloadStartPosition;
      end = payloadEndPosition;
      startGranule = 0;
      endGranule = totalGranules;
    }
  
    /**
     * Performs a single step of a seeking binary search, returning the byte position from which data
     * should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged.
     * If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be
     * called to skip to the target page.
     *
     * @param input The {@link ExtractorInput} to read from.
     * @return The byte position from which data should be provided for the next step, or {@link
     *     C#POSITION_UNSET} if the search has converged.
     * @throws IOException If reading from the input fails.
     */
    private long getNextSeekPosition(ExtractorInput input) throws IOException {
      if (start == end) {
        return C.POSITION_UNSET;
      }
  
      long currentPosition = input.getPosition();
      if (!pageHeader.skipToNextPage(input, end)) {
        if (start == currentPosition) {
          throw new IOException("No ogg page can be found.");
        }
        return start;
      }
  
      pageHeader.populate(input, /* quiet= */ false);
      input.resetPeekPosition();
  
      long granuleDistance = targetGranule - pageHeader.granulePosition;
      int pageSize = pageHeader.headerSize + pageHeader.bodySize;
      if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) {
        return C.POSITION_UNSET;
      }
  
      if (granuleDistance < 0) {
        end = currentPosition;
        endGranule = pageHeader.granulePosition;
      } else {
        start = input.getPosition() + pageSize;
        startGranule = pageHeader.granulePosition;
      }
  
      if (end - start < MATCH_BYTE_RANGE) {
        end = start;
        return start;
      }
  
      long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L);
      long nextPosition =
          input.getPosition()
              - offset
              + (granuleDistance * (end - start) / (endGranule - startGranule));
      return Util.constrainValue(nextPosition, start, end - 1);
    }
  
    /**
     * Skips forward to the start of the page containing the {@code targetGranule}.
     *
     * @param input The {@link ExtractorInput} to read from.
     * @throws ParserException If populating the page header fails.
     * @throws IOException If reading from the input fails.
     */
    private void skipToPageOfTargetGranule(ExtractorInput input) throws IOException {
      while (true) {
        // If pageHeader.skipToNextPage fails to find a page it will advance input.position to the
        // end of the file, so pageHeader.populate will throw EOFException (because quiet=false).
        pageHeader.skipToNextPage(input);
        pageHeader.populate(input, /* quiet= */ false);
        if (pageHeader.granulePosition > targetGranule) {
          break;
        }
        input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
        start = input.getPosition();
        startGranule = pageHeader.granulePosition;
      }
      input.resetPeekPosition();
    }
  
    /**
     * Skips to the last Ogg page in the stream and reads the header's granule field which is the
     * total number of samples per channel.
     *
     * @param input The {@link ExtractorInput} to read from.
     * @return The total number of samples of this input.
     * @throws IOException If reading from the input fails.
     */
    @VisibleForTesting
    long readGranuleOfLastPage(ExtractorInput input) throws IOException {
      pageHeader.reset();
      if (!pageHeader.skipToNextPage(input)) {
        throw new EOFException();
      }
      pageHeader.populate(input, /* quiet= */ false);
      input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
      long granulePosition = pageHeader.granulePosition;
      while ((pageHeader.type & 0x04) != 0x04
          && pageHeader.skipToNextPage(input)
          && input.getPosition() < payloadEndPosition) {
        boolean hasPopulated = pageHeader.populate(input, /* quiet= */ true);
        if (!hasPopulated || !skipFullyQuietly(input, pageHeader.headerSize + pageHeader.bodySize)) {
          // The input file contains a partial page at the end. Ignore it and return the granule
          // position of the last complete page.
          return granulePosition;
        }
        granulePosition = pageHeader.granulePosition;
      }
      return granulePosition;
    }
  
    private final class OggSeekMap implements SeekMap {
  
      @Override
      public boolean isSeekable() {
        return true;
      }
  
      @Override
      public SeekPoints getSeekPoints(long timeUs) {
        long targetGranule = streamReader.convertTimeToGranule(timeUs);
        long estimatedPosition =
            payloadStartPosition
                + (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules)
                - DEFAULT_OFFSET;
        estimatedPosition =
            Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1);
        return new SeekPoints(new SeekPoint(timeUs, estimatedPosition));
      }
  
      @Override
      public long getDurationUs() {
        return streamReader.convertGranuleToTime(totalGranules);
      }
    }
  }
  