IndexSeeker.java

/*
 * Copyright (C) 2020 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.mp3;

import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.util.LongArray;
import androidx.media3.common.util.Util;
import androidx.media3.extractor.SeekPoint;

/** MP3 seeker that builds a time-to-byte mapping as the stream is read. */
/* package */ final class IndexSeeker implements Seeker {

  @VisibleForTesting
  /* package */ static final long MIN_TIME_BETWEEN_POINTS_US = C.MICROS_PER_SECOND / 10;

  private final long dataEndPosition;
  private final LongArray timesUs;
  private final LongArray positions;

  private long durationUs;

  public IndexSeeker(long durationUs, long dataStartPosition, long dataEndPosition) {
    this.durationUs = durationUs;
    this.dataEndPosition = dataEndPosition;
    timesUs = new LongArray();
    positions = new LongArray();
    timesUs.add(0L);
    positions.add(dataStartPosition);
  }

  @Override
  public long getTimeUs(long position) {
    int targetIndex =
        Util.binarySearchFloor(
            positions, position, /* inclusive= */ true, /* stayInBounds= */ true);
    return timesUs.get(targetIndex);
  }

  @Override
  public long getDataEndPosition() {
    return dataEndPosition;
  }

  @Override
  public boolean isSeekable() {
    return true;
  }

  @Override
  public long getDurationUs() {
    return durationUs;
  }

  @Override
  public SeekPoints getSeekPoints(long timeUs) {
    int targetIndex =
        Util.binarySearchFloor(timesUs, timeUs, /* inclusive= */ true, /* stayInBounds= */ true);
    SeekPoint seekPoint = new SeekPoint(timesUs.get(targetIndex), positions.get(targetIndex));
    if (seekPoint.timeUs == timeUs || targetIndex == timesUs.size() - 1) {
      return new SeekPoints(seekPoint);
    } else {
      SeekPoint nextSeekPoint =
          new SeekPoint(timesUs.get(targetIndex + 1), positions.get(targetIndex + 1));
      return new SeekPoints(seekPoint, nextSeekPoint);
    }
  }

  /**
   * Adds a seek point to the index if it is sufficiently distant from the other points.
   *
   * <p>Seek points must be added in order.
   *
   * @param timeUs The time corresponding to the seek point to add in microseconds.
   * @param position The position corresponding to the seek point to add in bytes.
   */
  public void maybeAddSeekPoint(long timeUs, long position) {
    if (isTimeUsInIndex(timeUs)) {
      return;
    }
    timesUs.add(timeUs);
    positions.add(position);
  }

  /**
   * Returns whether {@code timeUs} (in microseconds) is included in the index.
   *
   * <p>A point is included in the index if it is equal to another point, between 2 points, or
   * sufficiently close to the last point.
   */
  public boolean isTimeUsInIndex(long timeUs) {
    long lastIndexedTimeUs = timesUs.get(timesUs.size() - 1);
    return timeUs - lastIndexedTimeUs < MIN_TIME_BETWEEN_POINTS_US;
  }

  /* package */ void setDurationUs(long durationUs) {
    this.durationUs = durationUs;
  }
}