OggPacket.java

/*
 * 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 androidx.media3.extractor.ogg;

import static androidx.media3.extractor.ExtractorUtil.readFullyQuietly;
import static androidx.media3.extractor.ExtractorUtil.skipFullyQuietly;
import static java.lang.Math.max;

import androidx.media3.common.C;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.extractor.ExtractorInput;
import java.io.IOException;
import java.util.Arrays;

/** OGG packet class. */
/* package */ final class OggPacket {

  private final OggPageHeader pageHeader = new OggPageHeader();
  private final ParsableByteArray packetArray =
      new ParsableByteArray(new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0);

  private int currentSegmentIndex = C.INDEX_UNSET;
  private int segmentCount;
  private boolean populated;

  /** Resets this reader. */
  public void reset() {
    pageHeader.reset();
    packetArray.reset(/* limit= */ 0);
    currentSegmentIndex = C.INDEX_UNSET;
    populated = false;
  }

  /**
   * Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make
   * sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader
   * can resume properly from an error while reading a continued packet spanned across multiple
   * pages.
   *
   * @param input The {@link ExtractorInput} to read data from.
   * @return {@code true} if the read was successful. The read fails if the end of the input is
   *     encountered without reading the whole packet.
   * @throws IOException If reading from the input fails.
   */
  public boolean populate(ExtractorInput input) throws IOException {
    Assertions.checkState(input != null);

    if (populated) {
      populated = false;
      packetArray.reset(/* limit= */ 0);
    }

    while (!populated) {
      if (currentSegmentIndex < 0) {
        // We're at the start of a page.
        if (!pageHeader.skipToNextPage(input) || !pageHeader.populate(input, /* quiet= */ true)) {
          return false;
        }
        int segmentIndex = 0;
        int bytesToSkip = pageHeader.headerSize;
        if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) {
          // After seeking, the first packet may be the remainder
          // part of a continued packet which has to be discarded.
          bytesToSkip += calculatePacketSize(segmentIndex);
          segmentIndex += segmentCount;
        }
        if (!skipFullyQuietly(input, bytesToSkip)) {
          return false;
        }
        currentSegmentIndex = segmentIndex;
      }

      int size = calculatePacketSize(currentSegmentIndex);
      int segmentIndex = currentSegmentIndex + segmentCount;
      if (size > 0) {
        packetArray.ensureCapacity(packetArray.limit() + size);
        if (!readFullyQuietly(input, packetArray.getData(), packetArray.limit(), size)) {
          return false;
        }
        packetArray.setLimit(packetArray.limit() + size);
        populated = pageHeader.laces[segmentIndex - 1] != 255;
      }
      // Advance now since we are sure reading didn't throw an exception.
      currentSegmentIndex =
          segmentIndex == pageHeader.pageSegmentCount ? C.INDEX_UNSET : segmentIndex;
    }
    return true;
  }

  /**
   * An OGG Packet may span multiple pages. Returns the {@link OggPageHeader} of the last page read,
   * or an empty header if the packet has yet to be populated.
   *
   * <p>Note that the returned {@link OggPageHeader} is mutable and may be updated during subsequent
   * calls to {@link #populate(ExtractorInput)}.
   *
   * @return the {@code PageHeader} of the last page read or an empty header if the packet has yet
   *     to be populated.
   */
  public OggPageHeader getPageHeader() {
    return pageHeader;
  }

  /** Returns a {@link ParsableByteArray} containing the packet's payload. */
  public ParsableByteArray getPayload() {
    return packetArray;
  }

  /** Trims the packet data array. */
  public void trimPayload() {
    if (packetArray.getData().length == OggPageHeader.MAX_PAGE_PAYLOAD) {
      return;
    }
    packetArray.reset(
        Arrays.copyOf(
            packetArray.getData(), max(OggPageHeader.MAX_PAGE_PAYLOAD, packetArray.limit())),
        /* limit= */ packetArray.limit());
  }

  /**
   * Calculates the size of the packet starting from {@code startSegmentIndex}.
   *
   * @param startSegmentIndex the index of the first segment of the packet.
   * @return Size of the packet.
   */
  private int calculatePacketSize(int startSegmentIndex) {
    segmentCount = 0;
    int size = 0;
    while (startSegmentIndex + segmentCount < pageHeader.pageSegmentCount) {
      int segmentLength = pageHeader.laces[startSegmentIndex + segmentCount++];
      size += segmentLength;
      if (segmentLength != 255) {
        // packets end at first lace < 255
        break;
      }
    }
    return size;
  }
}