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 com.google.android.exoplayer2.Format;
  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.FlacStreamInfo;
  import com.google.android.exoplayer2.util.MimeTypes;
  import com.google.android.exoplayer2.util.ParsableByteArray;
  import com.google.android.exoplayer2.util.Util;
  import java.io.IOException;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.List;
  
  /**
   * {@link StreamReader} to extract Flac data out of Ogg byte stream.
   */
  /* package */ final class FlacReader extends StreamReader {
  
    private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF;
    private static final byte SEEKTABLE_PACKET_TYPE = 0x03;
  
    private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
  
    private FlacStreamInfo streamInfo;
    private FlacOggSeeker flacOggSeeker;
  
    public static boolean verifyBitstreamType(ParsableByteArray data) {
      return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type
          data.readUnsignedInt() == 0x464C4143; // ASCII signature "FLAC"
    }
  
    @Override
    protected void reset(boolean headerData) {
      super.reset(headerData);
      if (headerData) {
        streamInfo = null;
        flacOggSeeker = null;
      }
    }
  
    private static boolean isAudioPacket(byte[] data) {
      return data[0] == AUDIO_PACKET_TYPE;
    }
  
    @Override
    protected long preparePayload(ParsableByteArray packet) {
      if (!isAudioPacket(packet.data)) {
        return -1;
      }
      return getFlacFrameBlockSize(packet);
    }
  
    @Override
    protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
        throws IOException, InterruptedException {
      byte[] data = packet.data;
      if (streamInfo == null) {
        streamInfo = new FlacStreamInfo(data, 17);
        byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
        metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks
        List<byte[]> initializationData = Collections.singletonList(metadata);
        setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null,
            Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate,
            initializationData, null, 0, null);
      } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) {
        flacOggSeeker = new FlacOggSeeker();
        flacOggSeeker.parseSeekTable(packet);
      } else if (isAudioPacket(data)) {
        if (flacOggSeeker != null) {
          flacOggSeeker.setFirstFrameOffset(position);
          setupData.oggSeeker = flacOggSeeker;
        }
        return false;
      }
      return true;
    }
  
    private int getFlacFrameBlockSize(ParsableByteArray packet) {
      int blockSizeCode = (packet.data[2] & 0xFF) >> 4;
      switch (blockSizeCode) {
        case 1:
          return 192;
        case 2:
        case 3:
        case 4:
        case 5:
          return 576 << (blockSizeCode - 2);
        case 6:
        case 7:
          // skip the sample number
          packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET);
          packet.readUtf8EncodedLong();
          int value = blockSizeCode == 6 ? packet.readUnsignedByte() : packet.readUnsignedShort();
          packet.setPosition(0);
          return value + 1;
        case 8:
        case 9:
        case 10:
        case 11:
        case 12:
        case 13:
        case 14:
        case 15:
          return 256 << (blockSizeCode - 8);
        default:
          return -1;
      }
    }
  
    private class FlacOggSeeker implements OggSeeker, SeekMap {
  
      private static final int METADATA_LENGTH_OFFSET = 1;
      private static final int SEEK_POINT_SIZE = 18;
  
      private long[] seekPointGranules;
      private long[] seekPointOffsets;
      private long firstFrameOffset;
      private long pendingSeekGranule;
  
      public FlacOggSeeker() {
        firstFrameOffset = -1;
        pendingSeekGranule = -1;
      }
  
      public void setFirstFrameOffset(long firstFrameOffset) {
        this.firstFrameOffset = firstFrameOffset;
      }
  
      /**
       * Parses a FLAC file seek table metadata structure and initializes internal fields.
       *
       * @param data A {@link ParsableByteArray} including whole seek table metadata block. Its
       *     position should be set to the beginning of the block.
       * @see <a href="https://xiph.org/flac/format.html#metadata_block_seektable">FLAC format
       *     METADATA_BLOCK_SEEKTABLE</a>
       */
      public void parseSeekTable(ParsableByteArray data) {
        data.skipBytes(METADATA_LENGTH_OFFSET);
        int length = data.readUnsignedInt24();
        int numberOfSeekPoints = length / SEEK_POINT_SIZE;
        seekPointGranules = new long[numberOfSeekPoints];
        seekPointOffsets = new long[numberOfSeekPoints];
        for (int i = 0; i < numberOfSeekPoints; i++) {
          seekPointGranules[i] = data.readLong();
          seekPointOffsets[i] = data.readLong();
          data.skipBytes(2); // Skip "Number of samples in the target frame."
        }
      }
  
      @Override
      public long read(ExtractorInput input) throws IOException, InterruptedException {
        if (pendingSeekGranule >= 0) {
          long result = -(pendingSeekGranule + 2);
          pendingSeekGranule = -1;
          return result;
        }
        return -1;
      }
  
      @Override
      public long startSeek(long timeUs) {
        long granule = convertTimeToGranule(timeUs);
        int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);
        pendingSeekGranule = seekPointGranules[index];
        return granule;
      }
  
      @Override
      public SeekMap createSeekMap() {
        return this;
      }
  
      @Override
      public boolean isSeekable() {
        return true;
      }
  
      @Override
      public SeekPoints getSeekPoints(long timeUs) {
        long granule = convertTimeToGranule(timeUs);
        int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);
        long seekTimeUs = convertGranuleToTime(seekPointGranules[index]);
        long seekPosition = firstFrameOffset + seekPointOffsets[index];
        SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
        if (seekTimeUs >= timeUs || index == seekPointGranules.length - 1) {
          return new SeekPoints(seekPoint);
        } else {
          long secondSeekTimeUs = convertGranuleToTime(seekPointGranules[index + 1]);
          long secondSeekPosition = firstFrameOffset + seekPointOffsets[index + 1];
          SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
          return new SeekPoints(seekPoint, secondSeekPoint);
        }
      }
  
      @Override
      public long getDurationUs() {
        return streamInfo.durationUs();
      }
  
    }
  
  }
  