blob: 29a0f5932abbbc33e05934aef02a531450c51c95 [file] [log] [blame]
/*
* 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 static com.google.android.exoplayer2.extractor.ExtractorUtil.skipFullyQuietly;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
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.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
/** Seeks in an Ogg stream. */
/* package */ final class DefaultOggSeeker implements OggSeeker {
private static final int MATCH_RANGE = 72_000;
private static final int MATCH_BYTE_RANGE = 100_000;
private static final int DEFAULT_OFFSET = 30_000;
private static final int STATE_SEEK_TO_END = 0;
private static final int STATE_READ_LAST_PAGE = 1;
private static final int STATE_SEEK = 2;
private static final int STATE_SKIP = 3;
private static final int STATE_IDLE = 4;
private final OggPageHeader pageHeader;
private final long payloadStartPosition;
private final long payloadEndPosition;
private final StreamReader streamReader;
private int state;
private long totalGranules;
private long positionBeforeSeekToEnd;
private long targetGranule;
private long start;
private long end;
private long startGranule;
private long endGranule;
/**
* Constructs an OggSeeker.
*
* @param streamReader The {@link StreamReader} that owns this seeker.
* @param payloadStartPosition Start position of the payload (inclusive).
* @param payloadEndPosition End position of the payload (exclusive).
* @param firstPayloadPageSize The total size of the first payload page, in bytes.
* @param firstPayloadPageGranulePosition The granule position of the first payload page.
* @param firstPayloadPageIsLastPage Whether the first payload page is also the last page.
*/
public DefaultOggSeeker(
StreamReader streamReader,
long payloadStartPosition,
long payloadEndPosition,
long firstPayloadPageSize,
long firstPayloadPageGranulePosition,
boolean firstPayloadPageIsLastPage) {
Assertions.checkArgument(
payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition);
this.streamReader = streamReader;
this.payloadStartPosition = payloadStartPosition;
this.payloadEndPosition = payloadEndPosition;
if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition
|| firstPayloadPageIsLastPage) {
totalGranules = firstPayloadPageGranulePosition;
state = STATE_IDLE;
} else {
state = STATE_SEEK_TO_END;
}
pageHeader = new OggPageHeader();
}
@Override
public long read(ExtractorInput input) throws IOException {
switch (state) {
case STATE_IDLE:
return -1;
case STATE_SEEK_TO_END:
positionBeforeSeekToEnd = input.getPosition();
state = STATE_READ_LAST_PAGE;
// Seek to the end just before the last page of stream to get the duration.
long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE;
if (lastPageSearchPosition > positionBeforeSeekToEnd) {
return lastPageSearchPosition;
}
// Fall through.
case STATE_READ_LAST_PAGE:
totalGranules = readGranuleOfLastPage(input);
state = STATE_IDLE;
return positionBeforeSeekToEnd;
case STATE_SEEK:
long position = getNextSeekPosition(input);
if (position != C.POSITION_UNSET) {
return position;
}
state = STATE_SKIP;
// Fall through.
case STATE_SKIP:
skipToPageOfTargetGranule(input);
state = STATE_IDLE;
return -(startGranule + 2);
default:
// Never happens.
throw new IllegalStateException();
}
}
@Override
@Nullable
public OggSeekMap createSeekMap() {
return totalGranules != 0 ? new OggSeekMap() : null;
}
@Override
public void startSeek(long targetGranule) {
this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1);
state = STATE_SEEK;
start = payloadStartPosition;
end = payloadEndPosition;
startGranule = 0;
endGranule = totalGranules;
}
/**
* Performs a single step of a seeking binary search, returning the byte position from which data
* should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged.
* If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be
* called to skip to the target page.
*
* @param input The {@link ExtractorInput} to read from.
* @return The byte position from which data should be provided for the next step, or {@link
* C#POSITION_UNSET} if the search has converged.
* @throws IOException If reading from the input fails.
*/
private long getNextSeekPosition(ExtractorInput input) throws IOException {
if (start == end) {
return C.POSITION_UNSET;
}
long currentPosition = input.getPosition();
if (!pageHeader.skipToNextPage(input, end)) {
if (start == currentPosition) {
throw new IOException("No ogg page can be found.");
}
return start;
}
pageHeader.populate(input, /* quiet= */ false);
input.resetPeekPosition();
long granuleDistance = targetGranule - pageHeader.granulePosition;
int pageSize = pageHeader.headerSize + pageHeader.bodySize;
if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) {
return C.POSITION_UNSET;
}
if (granuleDistance < 0) {
end = currentPosition;
endGranule = pageHeader.granulePosition;
} else {
start = input.getPosition() + pageSize;
startGranule = pageHeader.granulePosition;
}
if (end - start < MATCH_BYTE_RANGE) {
end = start;
return start;
}
long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L);
long nextPosition =
input.getPosition()
- offset
+ (granuleDistance * (end - start) / (endGranule - startGranule));
return Util.constrainValue(nextPosition, start, end - 1);
}
/**
* Skips forward to the start of the page containing the {@code targetGranule}.
*
* @param input The {@link ExtractorInput} to read from.
* @throws ParserException If populating the page header fails.
* @throws IOException If reading from the input fails.
*/
private void skipToPageOfTargetGranule(ExtractorInput input) throws IOException {
while (true) {
// If pageHeader.skipToNextPage fails to find a page it will advance input.position to the
// end of the file, so pageHeader.populate will throw EOFException (because quiet=false).
pageHeader.skipToNextPage(input);
pageHeader.populate(input, /* quiet= */ false);
if (pageHeader.granulePosition > targetGranule) {
break;
}
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
start = input.getPosition();
startGranule = pageHeader.granulePosition;
}
input.resetPeekPosition();
}
/**
* Skips to the last Ogg page in the stream and reads the header's granule field which is the
* total number of samples per channel.
*
* @param input The {@link ExtractorInput} to read from.
* @return The total number of samples of this input.
* @throws IOException If reading from the input fails.
*/
@VisibleForTesting
long readGranuleOfLastPage(ExtractorInput input) throws IOException {
pageHeader.reset();
if (!pageHeader.skipToNextPage(input)) {
throw new EOFException();
}
pageHeader.populate(input, /* quiet= */ false);
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
long granulePosition = pageHeader.granulePosition;
while ((pageHeader.type & 0x04) != 0x04
&& pageHeader.skipToNextPage(input)
&& input.getPosition() < payloadEndPosition) {
boolean hasPopulated = pageHeader.populate(input, /* quiet= */ true);
if (!hasPopulated || !skipFullyQuietly(input, pageHeader.headerSize + pageHeader.bodySize)) {
// The input file contains a partial page at the end. Ignore it and return the granule
// position of the last complete page.
return granulePosition;
}
granulePosition = pageHeader.granulePosition;
}
return granulePosition;
}
private final class OggSeekMap implements SeekMap {
@Override
public boolean isSeekable() {
return true;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
long targetGranule = streamReader.convertTimeToGranule(timeUs);
long estimatedPosition =
payloadStartPosition
+ (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules)
- DEFAULT_OFFSET;
estimatedPosition =
Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1);
return new SeekPoints(new SeekPoint(timeUs, estimatedPosition));
}
@Override
public long getDurationUs() {
return streamReader.convertGranuleToTime(totalGranules);
}
}
}