OggOpusAudioPacketizer.java

/*
 * Copyright (C) 2023 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.exoplayer.audio;

import static androidx.media3.common.audio.AudioProcessor.EMPTY_BUFFER;
import static androidx.media3.common.util.Assertions.checkNotNull;

import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.extractor.OpusUtil;
import com.google.common.primitives.UnsignedBytes;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;

/** A packetizer that encapsulates Opus audio encodings in Ogg packets. */
@UnstableApi
public final class OggOpusAudioPacketizer {

  private static final int CHECKSUM_INDEX = 22;

  /** ID Header and Comment Header pages are 0 and 1 respectively */
  private static final int FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER = 2;

  private static final int OGG_PACKET_HEADER_LENGTH = 28;
  private static final int SERIAL_NUMBER = 0;
  private static final byte[] OGG_DEFAULT_ID_HEADER_PAGE =
      new byte[] {
        79, 103, 103, 83, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, -43, -59, -9, 1,
        19, 79, 112, 117, 115, 72, 101, 97, 100, 1, 2, 56, 1, -128, -69, 0, 0, 0, 0, 0
      };
  private static final byte[] OGG_DEFAULT_COMMENT_HEADER_PAGE =
      new byte[] {
        79, 103, 103, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 11, -103, 87, 83, 1,
        16, 79, 112, 117, 115, 84, 97, 103, 115, 0, 0, 0, 0, 0, 0, 0, 0
      };

  private ByteBuffer outputBuffer;
  private int pageSequenceNumber;
  private int granulePosition;

  /** Creates an instance. */
  public OggOpusAudioPacketizer() {
    outputBuffer = EMPTY_BUFFER;
    granulePosition = 0;
    pageSequenceNumber = FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER;
  }

  /**
   * Packetizes the audio data between the position and limit of the {@code inputBuffer}.
   *
   * @param inputBuffer The input buffer to packetize. It must be a direct {@link ByteBuffer} with
   *     LITTLE_ENDIAN order. The contents will be overwritten with the Ogg packet. The caller
   *     retains ownership of the provided buffer.
   * @param initializationData contains set-up data for the Opus Decoder. The data will be provided
   *     in an Ogg ID Header Page prepended to the bitstream. The list should contain either one or
   *     three byte arrays. The first item is the payload for the Ogg ID Header Page. If three
   *     items, then it also contains the Opus pre-skip and seek pre-roll values in that order.
   */
  public void packetize(DecoderInputBuffer inputBuffer, List<byte[]> initializationData) {
    checkNotNull(inputBuffer.data);
    if (inputBuffer.data.limit() - inputBuffer.data.position() == 0) {
      return;
    }
    @Nullable
    byte[] providedOggIdHeaderPayloadBytes =
        pageSequenceNumber == FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER
                && (initializationData.size() == 1 || initializationData.size() == 3)
            ? initializationData.get(0)
            : null;
    outputBuffer = packetizeInternal(inputBuffer.data, providedOggIdHeaderPayloadBytes);
    inputBuffer.clear();
    inputBuffer.ensureSpaceForWrite(outputBuffer.remaining());
    inputBuffer.data.put(outputBuffer);
    inputBuffer.flip();
  }

  /** Resets the packetizer. */
  public void reset() {
    outputBuffer = EMPTY_BUFFER;
    granulePosition = 0;
    pageSequenceNumber = FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER;
  }

  /**
   * Fill outputBuffer with an Ogg packet encapsulating the inputBuffer.
   *
   * <p>If {@code providedOggIdHeaderPayloadBytes} is {@code null} and {@link #pageSequenceNumber}
   * is {@link #FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER}, then {@link #OGG_DEFAULT_ID_HEADER_PAGE}
   * will be prepended to the Ogg Opus Audio packets for the Ogg ID Header Page.
   *
   * @param inputBuffer contains Opus to wrap in Ogg packet.
   * @param providedOggIdHeaderPayloadBytes containing the Ogg ID Header Page payload. Expected to
   *     be {@code null} if {@link #pageSequenceNumber} is not {@link
   *     #FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER}.
   * @return {@link ByteBuffer} containing Ogg packet
   */
  private ByteBuffer packetizeInternal(
      ByteBuffer inputBuffer, @Nullable byte[] providedOggIdHeaderPayloadBytes) {
    int position = inputBuffer.position();
    int limit = inputBuffer.limit();
    int inputBufferSize = limit - position;

    // inputBufferSize divisible by 255 requires extra '0' terminating lacing value
    int numSegments = (inputBufferSize + 255) / 255;
    int headerSize = 27 + numSegments;

    int outputPacketSize = headerSize + inputBufferSize;

    // If first audio sample in stream, then the packetizer will add Ogg ID Header and Comment
    // Header Pages. Include additional page lengths in buffer size calculation.
    int oggIdHeaderPageSize = 0;
    if (pageSequenceNumber == FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER) {
      oggIdHeaderPageSize =
          providedOggIdHeaderPayloadBytes != null
              ? OGG_PACKET_HEADER_LENGTH + providedOggIdHeaderPayloadBytes.length
              : OGG_DEFAULT_ID_HEADER_PAGE.length;
      outputPacketSize += oggIdHeaderPageSize + OGG_DEFAULT_COMMENT_HEADER_PAGE.length;
    }

    // Resample the little endian input and update the output buffers.
    ByteBuffer buffer = replaceOutputBuffer(outputPacketSize);

    // If first audio sample in stream then insert Ogg ID Header and Comment Header Pages
    if (pageSequenceNumber == FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER) {
      if (providedOggIdHeaderPayloadBytes != null) {
        writeOggIdHeaderPage(buffer, /* idHeaderPayloadBytes= */ providedOggIdHeaderPayloadBytes);
      } else {
        // Write default Ogg ID Header Payload
        buffer.put(OGG_DEFAULT_ID_HEADER_PAGE);
      }
      buffer.put(OGG_DEFAULT_COMMENT_HEADER_PAGE);
    }

    // granule_position
    int numSamples = OpusUtil.parsePacketAudioSampleCount(inputBuffer);
    granulePosition += numSamples;

    writeOggPacketHeader(
        buffer, granulePosition, pageSequenceNumber, numSegments, /* isIdHeaderPacket= */ false);

    // Segment_table
    int bytesLeft = inputBufferSize;
    for (int i = 0; i < numSegments; i++) {
      if (bytesLeft >= 255) {
        buffer.put((byte) 255);
        bytesLeft -= 255;
      } else {
        buffer.put((byte) bytesLeft);
        bytesLeft = 0;
      }
    }

    // Write Opus audio data
    for (int i = position; i < limit; i++) {
      buffer.put(inputBuffer.get(i));
    }

    inputBuffer.position(inputBuffer.limit());
    buffer.flip();

    int checksum;
    if (pageSequenceNumber == FIRST_AUDIO_SAMPLE_PAGE_SEQUENCE_NUMBER) {
      checksum =
          Util.crc32(
              buffer.array(),
              /* start= */ buffer.arrayOffset()
                  + oggIdHeaderPageSize
                  + OGG_DEFAULT_COMMENT_HEADER_PAGE.length,
              /* end= */ buffer.limit() - buffer.position(),
              /* initialValue= */ 0);
      buffer.putInt(
          oggIdHeaderPageSize + OGG_DEFAULT_COMMENT_HEADER_PAGE.length + CHECKSUM_INDEX, checksum);
    } else {
      checksum =
          Util.crc32(
              buffer.array(),
              /* start= */ buffer.arrayOffset(),
              /* end= */ buffer.limit() - buffer.position(),
              /* initialValue= */ 0);
      buffer.putInt(CHECKSUM_INDEX, checksum);
    }

    // Increase pageSequenceNumber for next packet
    pageSequenceNumber++;

    return buffer;
  }

  /**
   * Write Ogg ID Header Page packet to {@link ByteBuffer}.
   *
   * @param buffer to write into.
   * @param idHeaderPayloadBytes containing the Ogg ID Header Page payload.
   */
  private void writeOggIdHeaderPage(ByteBuffer buffer, byte[] idHeaderPayloadBytes) {
    //     TODO(b/290195621): Use starting position to calculate correct 'pre-skip' value
    writeOggPacketHeader(
        buffer,
        /* granulePosition= */ 0,
        /* pageSequenceNumber= */ 0,
        /* numberPageSegments= */ 1,
        /* isIdHeaderPacket= */ true);
    buffer.put(UnsignedBytes.checkedCast(idHeaderPayloadBytes.length));
    buffer.put(idHeaderPayloadBytes);
    int checksum =
        Util.crc32(
            buffer.array(),
            /* start= */ buffer.arrayOffset(),
            /* end= */ OGG_PACKET_HEADER_LENGTH + idHeaderPayloadBytes.length,
            /* initialValue= */ 0);
    buffer.putInt(/* index= */ CHECKSUM_INDEX, checksum);
    buffer.position(OGG_PACKET_HEADER_LENGTH + idHeaderPayloadBytes.length);
  }

  /**
   * Write header for an Ogg Page Packet to {@link ByteBuffer}.
   *
   * @param byteBuffer to write unto.
   * @param granulePosition is the number of audio samples in the stream up to and including this
   *     packet.
   * @param pageSequenceNumber of the page this header is for.
   * @param numberPageSegments the data of this Ogg page will span.
   * @param isIdHeaderPacket where if this header is start of the bitstream.
   */
  private void writeOggPacketHeader(
      ByteBuffer byteBuffer,
      long granulePosition,
      int pageSequenceNumber,
      int numberPageSegments,
      boolean isIdHeaderPacket) {
    // Capture Pattern for Ogg Page [OggS]
    byteBuffer.put((byte) 'O');
    byteBuffer.put((byte) 'g');
    byteBuffer.put((byte) 'g');
    byteBuffer.put((byte) 'S');

    // StreamStructure Version
    byteBuffer.put((byte) 0);

    // Header-type
    byteBuffer.put(isIdHeaderPacket ? (byte) 0x02 : (byte) 0x00);

    // Granule_position
    byteBuffer.putLong(granulePosition);

    // bitstream_serial_number
    byteBuffer.putInt(SERIAL_NUMBER);

    // Page_sequence_number
    byteBuffer.putInt(pageSequenceNumber);

    // CRC_checksum
    // Will be overwritten with calculated checksum after rest of page is written to buffer.
    byteBuffer.putInt(0);

    // Number_page_segments
    byteBuffer.put(UnsignedBytes.checkedCast(numberPageSegments));
  }

  /**
   * Replaces the current output buffer with a buffer of at least {@code size} bytes and returns it.
   * Callers should write to the returned buffer then {@link ByteBuffer#flip()} it so it can be read
   * via buffer.
   */
  private ByteBuffer replaceOutputBuffer(int size) {
    if (outputBuffer.capacity() < size) {
      outputBuffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
    } else {
      outputBuffer.clear();
    }
    return outputBuffer;
  }
}