IcyDataSource.java

/*
 * 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 androidx.media3.exoplayer.source;

import static java.lang.Math.min;

import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.TransferListener;
import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
 * Splits ICY stream metadata out from a stream.
 *
 * <p>Note: {@link #open(DataSpec)} and {@link #close()} are not supported. This implementation is
 * intended to wrap upstream {@link DataSource} instances that are opened and closed directly.
 */
/* package */ final class IcyDataSource implements DataSource {

  public interface Listener {

    /**
     * Called when ICY stream metadata has been split from the stream.
     *
     * @param metadata The stream metadata in binary form.
     */
    void onIcyMetadata(ParsableByteArray metadata);
  }

  private final DataSource upstream;
  private final int metadataIntervalBytes;
  private final Listener listener;
  private final byte[] metadataLengthByteHolder;
  private int bytesUntilMetadata;

  /**
   * @param upstream The upstream {@link DataSource}.
   * @param metadataIntervalBytes The interval between ICY stream metadata, in bytes.
   * @param listener A listener to which stream metadata is delivered.
   */
  public IcyDataSource(DataSource upstream, int metadataIntervalBytes, Listener listener) {
    Assertions.checkArgument(metadataIntervalBytes > 0);
    this.upstream = upstream;
    this.metadataIntervalBytes = metadataIntervalBytes;
    this.listener = listener;
    metadataLengthByteHolder = new byte[1];
    bytesUntilMetadata = metadataIntervalBytes;
  }

  @Override
  public void addTransferListener(TransferListener transferListener) {
    Assertions.checkNotNull(transferListener);
    upstream.addTransferListener(transferListener);
  }

  @Override
  public long open(DataSpec dataSpec) {
    throw new UnsupportedOperationException();
  }

  @Override
  public int read(byte[] buffer, int offset, int length) throws IOException {
    if (bytesUntilMetadata == 0) {
      if (readMetadata()) {
        bytesUntilMetadata = metadataIntervalBytes;
      } else {
        return C.RESULT_END_OF_INPUT;
      }
    }
    int bytesRead = upstream.read(buffer, offset, min(bytesUntilMetadata, length));
    if (bytesRead != C.RESULT_END_OF_INPUT) {
      bytesUntilMetadata -= bytesRead;
    }
    return bytesRead;
  }

  @Override
  @Nullable
  public Uri getUri() {
    return upstream.getUri();
  }

  @Override
  public Map<String, List<String>> getResponseHeaders() {
    return upstream.getResponseHeaders();
  }

  @Override
  public void close() {
    throw new UnsupportedOperationException();
  }

  /**
   * Reads an ICY stream metadata block, passing it to {@link #listener} unless the block is empty.
   *
   * @return True if the block was extracted, including if its length byte indicated a length of
   *     zero. False if the end of the stream was reached.
   * @throws IOException If an error occurs reading from the wrapped {@link DataSource}.
   */
  private boolean readMetadata() throws IOException {
    int bytesRead = upstream.read(metadataLengthByteHolder, 0, 1);
    if (bytesRead == C.RESULT_END_OF_INPUT) {
      return false;
    }
    int metadataLength = (metadataLengthByteHolder[0] & 0xFF) << 4;
    if (metadataLength == 0) {
      return true;
    }

    int offset = 0;
    int lengthRemaining = metadataLength;
    byte[] metadata = new byte[metadataLength];
    while (lengthRemaining > 0) {
      bytesRead = upstream.read(metadata, offset, lengthRemaining);
      if (bytesRead == C.RESULT_END_OF_INPUT) {
        return false;
      }
      offset += bytesRead;
      lengthRemaining -= bytesRead;
    }

    // Discard trailing zero bytes.
    while (metadataLength > 0 && metadata[metadataLength - 1] == 0) {
      metadataLength--;
    }

    if (metadataLength > 0) {
      listener.onIcyMetadata(new ParsableByteArray(metadata, metadataLength));
    }
    return true;
  }
}