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.ts;
  
  import static java.lang.Math.min;
  
  import com.google.android.exoplayer2.C;
  import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
  import com.google.android.exoplayer2.extractor.ExtractorInput;
  import com.google.android.exoplayer2.util.ParsableByteArray;
  import com.google.android.exoplayer2.util.TimestampAdjuster;
  import com.google.android.exoplayer2.util.Util;
  import java.io.IOException;
  
  /**
   * A seeker that supports seeking within PS stream using binary search.
   *
   * <p>This seeker uses the first and last SCR values within the stream, as well as the stream
   * duration to interpolate the SCR value of the seeking position. Then it performs binary search
   * within the stream to find a packets whose SCR value is with in {@link #SEEK_TOLERANCE_US} from
   * the target SCR.
   */
  /* package */ final class PsBinarySearchSeeker extends BinarySearchSeeker {
  
    private static final long SEEK_TOLERANCE_US = 100_000;
    private static final int MINIMUM_SEARCH_RANGE_BYTES = 1000;
    private static final int TIMESTAMP_SEARCH_BYTES = 20_000;
  
    public PsBinarySearchSeeker(
        TimestampAdjuster scrTimestampAdjuster, long streamDurationUs, long inputLength) {
      super(
          new DefaultSeekTimestampConverter(),
          new PsScrSeeker(scrTimestampAdjuster),
          streamDurationUs,
          /* floorTimePosition= */ 0,
          /* ceilingTimePosition= */ streamDurationUs + 1,
          /* floorBytePosition= */ 0,
          /* ceilingBytePosition= */ inputLength,
          /* approxBytesPerFrame= */ TsExtractor.TS_PACKET_SIZE,
          MINIMUM_SEARCH_RANGE_BYTES);
    }
  
    /**
     * A seeker that looks for a given SCR timestamp at a given position in a PS stream.
     *
     * <p>Given a SCR timestamp, and a position within a PS stream, this seeker will peek up to {@link
     * #TIMESTAMP_SEARCH_BYTES} bytes from that stream position, look for all packs in that range, and
     * then compare the SCR timestamps (if available) of these packets to the target timestamp.
     */
    private static final class PsScrSeeker implements TimestampSeeker {
  
      private final TimestampAdjuster scrTimestampAdjuster;
      private final ParsableByteArray packetBuffer;
  
      private PsScrSeeker(TimestampAdjuster scrTimestampAdjuster) {
        this.scrTimestampAdjuster = scrTimestampAdjuster;
        packetBuffer = new ParsableByteArray();
      }
  
      @Override
      public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
          throws IOException {
        long inputPosition = input.getPosition();
        int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition);
  
        packetBuffer.reset(bytesToSearch);
        input.peekFully(packetBuffer.getData(), /* offset= */ 0, bytesToSearch);
  
        return searchForScrValueInBuffer(packetBuffer, targetTimestamp, inputPosition);
      }
  
      @Override
      public void onSeekFinished() {
        packetBuffer.reset(Util.EMPTY_BYTE_ARRAY);
      }
  
      private TimestampSearchResult searchForScrValueInBuffer(
          ParsableByteArray packetBuffer, long targetScrTimeUs, long bufferStartOffset) {
        int startOfLastPacketPosition = C.POSITION_UNSET;
        int endOfLastPacketPosition = C.POSITION_UNSET;
        long lastScrTimeUsInRange = C.TIME_UNSET;
  
        while (packetBuffer.bytesLeft() >= 4) {
          int nextStartCode = peekIntAtPosition(packetBuffer.getData(), packetBuffer.getPosition());
          if (nextStartCode != PsExtractor.PACK_START_CODE) {
            packetBuffer.skipBytes(1);
            continue;
          } else {
            packetBuffer.skipBytes(4);
          }
  
          // We found a pack.
          long scrValue = PsDurationReader.readScrValueFromPack(packetBuffer);
          if (scrValue != C.TIME_UNSET) {
            long scrTimeUs = scrTimestampAdjuster.adjustTsTimestamp(scrValue);
            if (scrTimeUs > targetScrTimeUs) {
              if (lastScrTimeUsInRange == C.TIME_UNSET) {
                // First SCR timestamp is already over target.
                return TimestampSearchResult.overestimatedResult(scrTimeUs, bufferStartOffset);
              } else {
                // Last SCR timestamp < target timestamp < this timestamp.
                return TimestampSearchResult.targetFoundResult(
                    bufferStartOffset + startOfLastPacketPosition);
              }
            } else if (scrTimeUs + SEEK_TOLERANCE_US > targetScrTimeUs) {
              long startOfPacketInStream = bufferStartOffset + packetBuffer.getPosition();
              return TimestampSearchResult.targetFoundResult(startOfPacketInStream);
            }
  
            lastScrTimeUsInRange = scrTimeUs;
            startOfLastPacketPosition = packetBuffer.getPosition();
          }
          skipToEndOfCurrentPack(packetBuffer);
          endOfLastPacketPosition = packetBuffer.getPosition();
        }
  
        if (lastScrTimeUsInRange != C.TIME_UNSET) {
          long endOfLastPacketPositionInStream = bufferStartOffset + endOfLastPacketPosition;
          return TimestampSearchResult.underestimatedResult(
              lastScrTimeUsInRange, endOfLastPacketPositionInStream);
        } else {
          return TimestampSearchResult.NO_TIMESTAMP_IN_RANGE_RESULT;
        }
      }
  
      /**
       * Skips the buffer position to the position after the end of the current PS pack in the buffer,
       * given the byte position right after the {@link PsExtractor#PACK_START_CODE} of the pack in
       * the buffer. If the pack ends after the end of the buffer, skips to the end of the buffer.
       */
      private static void skipToEndOfCurrentPack(ParsableByteArray packetBuffer) {
        int limit = packetBuffer.limit();
  
        if (packetBuffer.bytesLeft() < 10) {
          // We require at least 9 bytes for pack header to read SCR value + 1 byte for pack_stuffing
          // length.
          packetBuffer.setPosition(limit);
          return;
        }
        packetBuffer.skipBytes(9);
  
        int packStuffingLength = packetBuffer.readUnsignedByte() & 0x07;
        if (packetBuffer.bytesLeft() < packStuffingLength) {
          packetBuffer.setPosition(limit);
          return;
        }
        packetBuffer.skipBytes(packStuffingLength);
  
        if (packetBuffer.bytesLeft() < 4) {
          packetBuffer.setPosition(limit);
          return;
        }
  
        int nextStartCode = peekIntAtPosition(packetBuffer.getData(), packetBuffer.getPosition());
        if (nextStartCode == PsExtractor.SYSTEM_HEADER_START_CODE) {
          packetBuffer.skipBytes(4);
          int systemHeaderLength = packetBuffer.readUnsignedShort();
          if (packetBuffer.bytesLeft() < systemHeaderLength) {
            packetBuffer.setPosition(limit);
            return;
          }
          packetBuffer.skipBytes(systemHeaderLength);
        }
  
        // Find the position of the next PACK_START_CODE or MPEG_PROGRAM_END_CODE, which is right
        // after the end position of this pack.
        // If we couldn't find these codes within the buffer, return the buffer limit, or return
        // the first position which PES packets pattern does not match (some malformed packets).
        while (packetBuffer.bytesLeft() >= 4) {
          nextStartCode = peekIntAtPosition(packetBuffer.getData(), packetBuffer.getPosition());
          if (nextStartCode == PsExtractor.PACK_START_CODE
              || nextStartCode == PsExtractor.MPEG_PROGRAM_END_CODE) {
            break;
          }
          if (nextStartCode >>> 8 != PsExtractor.PACKET_START_CODE_PREFIX) {
            break;
          }
          packetBuffer.skipBytes(4);
  
          if (packetBuffer.bytesLeft() < 2) {
            // 2 bytes for PES_packet length.
            packetBuffer.setPosition(limit);
            return;
          }
          int pesPacketLength = packetBuffer.readUnsignedShort();
          packetBuffer.setPosition(
              min(packetBuffer.limit(), packetBuffer.getPosition() + pesPacketLength));
        }
      }
    }
  
    private static int peekIntAtPosition(byte[] data, int position) {
      return (data[position] & 0xFF) << 24
          | (data[position + 1] & 0xFF) << 16
          | (data[position + 2] & 0xFF) << 8
          | (data[position + 3] & 0xFF);
    }
  }
  