Fix DefaultOggSeeker seeking
- When in STATE_SEEK with targetGranule==0, seeking would exit without checking that the input was positioned at the correct place. - Seeking could fail due to trying to read beyond the end of the stream. - Seeking was not robust against IO errors during the skip phase that occurs after the binary search has sufficiently converged. PiperOrigin-RevId: 261317035
This commit is contained in:
parent
90ab05c574
commit
91c62ea26f
@ -16,6 +16,7 @@
|
|||||||
package com.google.android.exoplayer2.extractor.ogg;
|
package com.google.android.exoplayer2.extractor.ogg;
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
@ -35,11 +36,12 @@ import java.io.IOException;
|
|||||||
private static final int STATE_SEEK_TO_END = 0;
|
private static final int STATE_SEEK_TO_END = 0;
|
||||||
private static final int STATE_READ_LAST_PAGE = 1;
|
private static final int STATE_READ_LAST_PAGE = 1;
|
||||||
private static final int STATE_SEEK = 2;
|
private static final int STATE_SEEK = 2;
|
||||||
private static final int STATE_IDLE = 3;
|
private static final int STATE_SKIP = 3;
|
||||||
|
private static final int STATE_IDLE = 4;
|
||||||
|
|
||||||
private final OggPageHeader pageHeader = new OggPageHeader();
|
private final OggPageHeader pageHeader = new OggPageHeader();
|
||||||
private final long startPosition;
|
private final long payloadStartPosition;
|
||||||
private final long endPosition;
|
private final long payloadEndPosition;
|
||||||
private final StreamReader streamReader;
|
private final StreamReader streamReader;
|
||||||
|
|
||||||
private int state;
|
private int state;
|
||||||
@ -55,26 +57,27 @@ import java.io.IOException;
|
|||||||
/**
|
/**
|
||||||
* Constructs an OggSeeker.
|
* Constructs an OggSeeker.
|
||||||
*
|
*
|
||||||
* @param startPosition Start position of the payload (inclusive).
|
|
||||||
* @param endPosition End position of the payload (exclusive).
|
|
||||||
* @param streamReader The {@link StreamReader} that owns this seeker.
|
* @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 firstPayloadPageSize The total size of the first payload page, in bytes.
|
||||||
* @param firstPayloadPageGranulePosition The granule position of the first payload page.
|
* @param firstPayloadPageGranulePosition The granule position of the first payload page.
|
||||||
* @param firstPayloadPageIsLastPage Whether the first payload page is also the last page in the
|
* @param firstPayloadPageIsLastPage Whether the first payload page is also the last page.
|
||||||
* ogg stream.
|
|
||||||
*/
|
*/
|
||||||
public DefaultOggSeeker(
|
public DefaultOggSeeker(
|
||||||
long startPosition,
|
|
||||||
long endPosition,
|
|
||||||
StreamReader streamReader,
|
StreamReader streamReader,
|
||||||
|
long payloadStartPosition,
|
||||||
|
long payloadEndPosition,
|
||||||
long firstPayloadPageSize,
|
long firstPayloadPageSize,
|
||||||
long firstPayloadPageGranulePosition,
|
long firstPayloadPageGranulePosition,
|
||||||
boolean firstPayloadPageIsLastPage) {
|
boolean firstPayloadPageIsLastPage) {
|
||||||
Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition);
|
Assertions.checkArgument(
|
||||||
|
payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition);
|
||||||
this.streamReader = streamReader;
|
this.streamReader = streamReader;
|
||||||
this.startPosition = startPosition;
|
this.payloadStartPosition = payloadStartPosition;
|
||||||
this.endPosition = endPosition;
|
this.payloadEndPosition = payloadEndPosition;
|
||||||
if (firstPayloadPageSize == endPosition - startPosition || firstPayloadPageIsLastPage) {
|
if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition
|
||||||
|
|| firstPayloadPageIsLastPage) {
|
||||||
totalGranules = firstPayloadPageGranulePosition;
|
totalGranules = firstPayloadPageGranulePosition;
|
||||||
state = STATE_IDLE;
|
state = STATE_IDLE;
|
||||||
} else {
|
} else {
|
||||||
@ -91,7 +94,7 @@ import java.io.IOException;
|
|||||||
positionBeforeSeekToEnd = input.getPosition();
|
positionBeforeSeekToEnd = input.getPosition();
|
||||||
state = STATE_READ_LAST_PAGE;
|
state = STATE_READ_LAST_PAGE;
|
||||||
// Seek to the end just before the last page of stream to get the duration.
|
// Seek to the end just before the last page of stream to get the duration.
|
||||||
long lastPageSearchPosition = endPosition - OggPageHeader.MAX_PAGE_SIZE;
|
long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE;
|
||||||
if (lastPageSearchPosition > positionBeforeSeekToEnd) {
|
if (lastPageSearchPosition > positionBeforeSeekToEnd) {
|
||||||
return lastPageSearchPosition;
|
return lastPageSearchPosition;
|
||||||
}
|
}
|
||||||
@ -101,137 +104,110 @@ import java.io.IOException;
|
|||||||
state = STATE_IDLE;
|
state = STATE_IDLE;
|
||||||
return positionBeforeSeekToEnd;
|
return positionBeforeSeekToEnd;
|
||||||
case STATE_SEEK:
|
case STATE_SEEK:
|
||||||
long currentGranule;
|
long position = getNextSeekPosition(input);
|
||||||
if (targetGranule == 0) {
|
if (position != C.POSITION_UNSET) {
|
||||||
currentGranule = 0;
|
return position;
|
||||||
} else {
|
|
||||||
long position = getNextSeekPosition(targetGranule, input);
|
|
||||||
if (position >= 0) {
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
currentGranule = skipToPageOfGranule(input, targetGranule, -(position + 2));
|
|
||||||
}
|
}
|
||||||
|
state = STATE_SKIP;
|
||||||
|
// Fall through.
|
||||||
|
case STATE_SKIP:
|
||||||
|
skipToPageOfTargetGranule(input);
|
||||||
state = STATE_IDLE;
|
state = STATE_IDLE;
|
||||||
return -(currentGranule + 2);
|
return -(startGranule + 2);
|
||||||
default:
|
default:
|
||||||
// Never happens.
|
// Never happens.
|
||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startSeek(long targetGranule) {
|
|
||||||
Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK);
|
|
||||||
this.targetGranule = targetGranule;
|
|
||||||
state = STATE_SEEK;
|
|
||||||
resetSeeking();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OggSeekMap createSeekMap() {
|
public OggSeekMap createSeekMap() {
|
||||||
return totalGranules != 0 ? new OggSeekMap() : null;
|
return totalGranules != 0 ? new OggSeekMap() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@Override
|
||||||
public void resetSeeking() {
|
public void startSeek(long targetGranule) {
|
||||||
start = startPosition;
|
this.targetGranule = targetGranule;
|
||||||
end = endPosition;
|
state = STATE_SEEK;
|
||||||
|
start = payloadStartPosition;
|
||||||
|
end = payloadEndPosition;
|
||||||
startGranule = 0;
|
startGranule = 0;
|
||||||
endGranule = totalGranules;
|
endGranule = totalGranules;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput}
|
* Performs a single step of a seeking binary search, returning the byte position from which data
|
||||||
* has to seek and then be passed for another call until a negative number is returned. If a
|
* should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged.
|
||||||
* negative number is returned the input is at a position which is before the target page and at
|
* If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be
|
||||||
* which it is sensible to just skip pages to the target granule and pre-roll instead of doing
|
* called to skip to the target page.
|
||||||
* another seek request.
|
|
||||||
*
|
*
|
||||||
* @param targetGranule The target granule position to seek to.
|
|
||||||
* @param input The {@link ExtractorInput} to read from.
|
* @param input The {@link ExtractorInput} to read from.
|
||||||
* @return The position to seek the {@link ExtractorInput} to for a next call or -(currentGranule
|
* @return The byte position from which data should be provided for the next step, or {@link
|
||||||
* + 2) if it's close enough to skip to the target page.
|
* C#POSITION_UNSET} if the search has converged.
|
||||||
* @throws IOException If reading from the input fails.
|
* @throws IOException If reading from the input fails.
|
||||||
* @throws InterruptedException If interrupted while reading from the input.
|
* @throws InterruptedException If interrupted while reading from the input.
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
public long getNextSeekPosition(long targetGranule, ExtractorInput input)
|
|
||||||
throws IOException, InterruptedException {
|
|
||||||
if (start == end) {
|
if (start == end) {
|
||||||
return -(startGranule + 2);
|
return C.POSITION_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
long initialPosition = input.getPosition();
|
long currentPosition = input.getPosition();
|
||||||
if (!skipToNextPage(input, end)) {
|
if (!skipToNextPage(input, end)) {
|
||||||
if (start == initialPosition) {
|
if (start == currentPosition) {
|
||||||
throw new IOException("No ogg page can be found.");
|
throw new IOException("No ogg page can be found.");
|
||||||
}
|
}
|
||||||
return start;
|
return start;
|
||||||
}
|
}
|
||||||
|
|
||||||
pageHeader.populate(input, false);
|
pageHeader.populate(input, /* quiet= */ false);
|
||||||
input.resetPeekPosition();
|
input.resetPeekPosition();
|
||||||
|
|
||||||
long granuleDistance = targetGranule - pageHeader.granulePosition;
|
long granuleDistance = targetGranule - pageHeader.granulePosition;
|
||||||
int pageSize = pageHeader.headerSize + pageHeader.bodySize;
|
int pageSize = pageHeader.headerSize + pageHeader.bodySize;
|
||||||
if (granuleDistance < 0 || granuleDistance > MATCH_RANGE) {
|
if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) {
|
||||||
if (granuleDistance < 0) {
|
return C.POSITION_UNSET;
|
||||||
end = initialPosition;
|
|
||||||
endGranule = pageHeader.granulePosition;
|
|
||||||
} else {
|
|
||||||
start = input.getPosition() + pageSize;
|
|
||||||
startGranule = pageHeader.granulePosition;
|
|
||||||
if (end - start + pageSize < MATCH_BYTE_RANGE) {
|
|
||||||
input.skipFully(pageSize);
|
|
||||||
return -(startGranule + 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
nextPosition = Math.max(nextPosition, start);
|
|
||||||
nextPosition = Math.min(nextPosition, end - 1);
|
|
||||||
return nextPosition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// position accepted (before target granule and within MATCH_RANGE)
|
if (granuleDistance < 0) {
|
||||||
input.skipFully(pageSize);
|
end = currentPosition;
|
||||||
return -(pageHeader.granulePosition + 2);
|
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 to the position of the start of the page containing the {@code targetGranule} and returns
|
* Skips forward to the start of the page containing the {@code targetGranule}.
|
||||||
* the granule of the page previous to the target page.
|
|
||||||
*
|
*
|
||||||
* @param input The {@link ExtractorInput} to read from.
|
* @param input The {@link ExtractorInput} to read from.
|
||||||
* @param targetGranule The target granule.
|
|
||||||
* @param currentGranule The current granule or -1 if it's unknown.
|
|
||||||
* @return The granule of the prior page or the {@code currentGranule} if there isn't a prior
|
|
||||||
* page.
|
|
||||||
* @throws ParserException If populating the page header fails.
|
* @throws ParserException If populating the page header fails.
|
||||||
* @throws IOException If reading from the input fails.
|
* @throws IOException If reading from the input fails.
|
||||||
* @throws InterruptedException If interrupted while reading from the input.
|
* @throws InterruptedException If interrupted while reading from the input.
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
private void skipToPageOfTargetGranule(ExtractorInput input)
|
||||||
long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule)
|
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
pageHeader.populate(input, false);
|
pageHeader.populate(input, /* quiet= */ false);
|
||||||
while (pageHeader.granulePosition < targetGranule) {
|
while (pageHeader.granulePosition < targetGranule) {
|
||||||
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
|
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
|
||||||
// Store in a member field to be able to resume after IOExceptions.
|
start = input.getPosition();
|
||||||
currentGranule = pageHeader.granulePosition;
|
startGranule = pageHeader.granulePosition;
|
||||||
// Peek next header.
|
pageHeader.populate(input, /* quiet= */ false);
|
||||||
pageHeader.populate(input, false);
|
|
||||||
}
|
}
|
||||||
input.resetPeekPosition();
|
input.resetPeekPosition();
|
||||||
return currentGranule;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -244,7 +220,7 @@ import java.io.IOException;
|
|||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException {
|
void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
if (!skipToNextPage(input, endPosition)) {
|
if (!skipToNextPage(input, payloadEndPosition)) {
|
||||||
// Not found until eof.
|
// Not found until eof.
|
||||||
throw new EOFException();
|
throw new EOFException();
|
||||||
}
|
}
|
||||||
@ -261,7 +237,7 @@ import java.io.IOException;
|
|||||||
*/
|
*/
|
||||||
private boolean skipToNextPage(ExtractorInput input, long limit)
|
private boolean skipToNextPage(ExtractorInput input, long limit)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
limit = Math.min(limit + 3, endPosition);
|
limit = Math.min(limit + 3, payloadEndPosition);
|
||||||
byte[] buffer = new byte[2048];
|
byte[] buffer = new byte[2048];
|
||||||
int peekLength = buffer.length;
|
int peekLength = buffer.length;
|
||||||
while (true) {
|
while (true) {
|
||||||
@ -302,8 +278,8 @@ import java.io.IOException;
|
|||||||
long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException {
|
long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
skipToNextPage(input);
|
skipToNextPage(input);
|
||||||
pageHeader.reset();
|
pageHeader.reset();
|
||||||
while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < endPosition) {
|
while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) {
|
||||||
pageHeader.populate(input, false);
|
pageHeader.populate(input, /* quiet= */ false);
|
||||||
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
|
input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
|
||||||
}
|
}
|
||||||
return pageHeader.granulePosition;
|
return pageHeader.granulePosition;
|
||||||
@ -320,10 +296,11 @@ import java.io.IOException;
|
|||||||
public SeekPoints getSeekPoints(long timeUs) {
|
public SeekPoints getSeekPoints(long timeUs) {
|
||||||
long targetGranule = streamReader.convertTimeToGranule(timeUs);
|
long targetGranule = streamReader.convertTimeToGranule(timeUs);
|
||||||
long estimatedPosition =
|
long estimatedPosition =
|
||||||
startPosition
|
payloadStartPosition
|
||||||
+ (targetGranule * (endPosition - startPosition) / totalGranules)
|
+ (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules)
|
||||||
- DEFAULT_OFFSET;
|
- DEFAULT_OFFSET;
|
||||||
estimatedPosition = Util.constrainValue(estimatedPosition, startPosition, endPosition - 1);
|
estimatedPosition =
|
||||||
|
Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1);
|
||||||
return new SeekPoints(new SeekPoint(timeUs, estimatedPosition));
|
return new SeekPoints(new SeekPoint(timeUs, estimatedPosition));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,9 +148,9 @@ import java.io.IOException;
|
|||||||
boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream.
|
boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream.
|
||||||
oggSeeker =
|
oggSeeker =
|
||||||
new DefaultOggSeeker(
|
new DefaultOggSeeker(
|
||||||
|
this,
|
||||||
payloadStartPosition,
|
payloadStartPosition,
|
||||||
input.getLength(),
|
input.getLength(),
|
||||||
this,
|
|
||||||
firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize,
|
firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize,
|
||||||
firstPayloadPageHeader.granulePosition,
|
firstPayloadPageHeader.granulePosition,
|
||||||
isLastPage);
|
isLastPage);
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
package com.google.android.exoplayer2.extractor.ogg;
|
package com.google.android.exoplayer2.extractor.ogg;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static com.google.common.truth.Truth.assertWithMessage;
|
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
@ -36,9 +35,9 @@ public final class DefaultOggSeekerTest {
|
|||||||
public void testSetupWithUnsetEndPositionFails() {
|
public void testSetupWithUnsetEndPositionFails() {
|
||||||
try {
|
try {
|
||||||
new DefaultOggSeeker(
|
new DefaultOggSeeker(
|
||||||
/* startPosition= */ 0,
|
|
||||||
/* endPosition= */ C.LENGTH_UNSET,
|
|
||||||
/* streamReader= */ new TestStreamReader(),
|
/* streamReader= */ new TestStreamReader(),
|
||||||
|
/* payloadStartPosition= */ 0,
|
||||||
|
/* payloadEndPosition= */ C.LENGTH_UNSET,
|
||||||
/* firstPayloadPageSize= */ 1,
|
/* firstPayloadPageSize= */ 1,
|
||||||
/* firstPayloadPageGranulePosition= */ 1,
|
/* firstPayloadPageGranulePosition= */ 1,
|
||||||
/* firstPayloadPageIsLastPage= */ false);
|
/* firstPayloadPageIsLastPage= */ false);
|
||||||
@ -62,9 +61,9 @@ public final class DefaultOggSeekerTest {
|
|||||||
TestStreamReader streamReader = new TestStreamReader();
|
TestStreamReader streamReader = new TestStreamReader();
|
||||||
DefaultOggSeeker oggSeeker =
|
DefaultOggSeeker oggSeeker =
|
||||||
new DefaultOggSeeker(
|
new DefaultOggSeeker(
|
||||||
/* startPosition= */ 0,
|
|
||||||
/* endPosition= */ testFile.data.length,
|
|
||||||
/* streamReader= */ streamReader,
|
/* streamReader= */ streamReader,
|
||||||
|
/* payloadStartPosition= */ 0,
|
||||||
|
/* payloadEndPosition= */ testFile.data.length,
|
||||||
/* firstPayloadPageSize= */ testFile.firstPayloadPageSize,
|
/* firstPayloadPageSize= */ testFile.firstPayloadPageSize,
|
||||||
/* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranulePosition,
|
/* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranulePosition,
|
||||||
/* firstPayloadPageIsLastPage= */ false);
|
/* firstPayloadPageIsLastPage= */ false);
|
||||||
@ -78,70 +77,56 @@ public final class DefaultOggSeekerTest {
|
|||||||
input.setPosition((int) nextSeekPosition);
|
input.setPosition((int) nextSeekPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test granule 0 from file start
|
// Test granule 0 from file start.
|
||||||
assertThat(seekTo(input, oggSeeker, 0, 0)).isEqualTo(0);
|
long granule = seekTo(input, oggSeeker, 0, 0);
|
||||||
|
assertThat(granule).isEqualTo(0);
|
||||||
assertThat(input.getPosition()).isEqualTo(0);
|
assertThat(input.getPosition()).isEqualTo(0);
|
||||||
|
|
||||||
// Test granule 0 from file end
|
// Test granule 0 from file end.
|
||||||
assertThat(seekTo(input, oggSeeker, 0, testFile.data.length - 1)).isEqualTo(0);
|
granule = seekTo(input, oggSeeker, 0, testFile.data.length - 1);
|
||||||
|
assertThat(granule).isEqualTo(0);
|
||||||
assertThat(input.getPosition()).isEqualTo(0);
|
assertThat(input.getPosition()).isEqualTo(0);
|
||||||
|
|
||||||
{ // Test last granule
|
// Test last granule.
|
||||||
long currentGranule = seekTo(input, oggSeeker, testFile.lastGranule, 0);
|
granule = seekTo(input, oggSeeker, testFile.lastGranule, 0);
|
||||||
long position = testFile.data.length;
|
long position = testFile.data.length;
|
||||||
assertThat(
|
// TODO: Simplify this.
|
||||||
(testFile.lastGranule > currentGranule && position > input.getPosition())
|
assertThat(
|
||||||
|| (testFile.lastGranule == currentGranule && position == input.getPosition()))
|
(testFile.lastGranule > granule && position > input.getPosition())
|
||||||
.isTrue();
|
|| (testFile.lastGranule == granule && position == input.getPosition()))
|
||||||
}
|
.isTrue();
|
||||||
|
|
||||||
{ // Test exact granule
|
// Test exact granule.
|
||||||
input.setPosition(testFile.data.length / 2);
|
input.setPosition(testFile.data.length / 2);
|
||||||
oggSeeker.skipToNextPage(input);
|
oggSeeker.skipToNextPage(input);
|
||||||
assertThat(pageHeader.populate(input, true)).isTrue();
|
assertThat(pageHeader.populate(input, true)).isTrue();
|
||||||
long position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize;
|
position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize;
|
||||||
long currentGranule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0);
|
granule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0);
|
||||||
assertThat(
|
// TODO: Simplify this.
|
||||||
(pageHeader.granulePosition > currentGranule && position > input.getPosition())
|
assertThat(
|
||||||
|| (pageHeader.granulePosition == currentGranule
|
(pageHeader.granulePosition > granule && position > input.getPosition())
|
||||||
&& position == input.getPosition()))
|
|| (pageHeader.granulePosition == granule && position == input.getPosition()))
|
||||||
.isTrue();
|
.isTrue();
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < 100; i += 1) {
|
for (int i = 0; i < 100; i += 1) {
|
||||||
long targetGranule = (long) (random.nextDouble() * testFile.lastGranule);
|
long targetGranule = (long) (random.nextDouble() * testFile.lastGranule);
|
||||||
int initialPosition = random.nextInt(testFile.data.length);
|
int initialPosition = random.nextInt(testFile.data.length);
|
||||||
|
granule = seekTo(input, oggSeeker, targetGranule, initialPosition);
|
||||||
long currentGranule = seekTo(input, oggSeeker, targetGranule, initialPosition);
|
|
||||||
long currentPosition = input.getPosition();
|
long currentPosition = input.getPosition();
|
||||||
|
if (granule == 0) {
|
||||||
assertWithMessage("getNextSeekPosition() didn't leave input on a page start.")
|
|
||||||
.that(pageHeader.populate(input, true))
|
|
||||||
.isTrue();
|
|
||||||
|
|
||||||
if (currentGranule == 0) {
|
|
||||||
assertThat(currentPosition).isEqualTo(0);
|
assertThat(currentPosition).isEqualTo(0);
|
||||||
} else {
|
} else {
|
||||||
int previousPageStart = testFile.findPreviousPageStart(currentPosition);
|
int previousPageStart = testFile.findPreviousPageStart(currentPosition);
|
||||||
input.setPosition(previousPageStart);
|
input.setPosition(previousPageStart);
|
||||||
assertThat(pageHeader.populate(input, true)).isTrue();
|
pageHeader.populate(input, false);
|
||||||
assertThat(currentGranule).isEqualTo(pageHeader.granulePosition);
|
assertThat(granule).isEqualTo(pageHeader.granulePosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
input.setPosition((int) currentPosition);
|
input.setPosition((int) currentPosition);
|
||||||
oggSeeker.skipToPageOfGranule(input, targetGranule, -1);
|
pageHeader.populate(input, false);
|
||||||
long positionDiff = Math.abs(input.getPosition() - currentPosition);
|
// The target granule should be within the current page.
|
||||||
|
assertThat(granule).isAtMost(targetGranule);
|
||||||
long granuleDiff = currentGranule - targetGranule;
|
assertThat(targetGranule).isLessThan(pageHeader.granulePosition);
|
||||||
if ((granuleDiff > DefaultOggSeeker.MATCH_RANGE || granuleDiff < 0)
|
|
||||||
&& positionDiff > DefaultOggSeeker.MATCH_BYTE_RANGE) {
|
|
||||||
fail(
|
|
||||||
"granuleDiff ("
|
|
||||||
+ granuleDiff
|
|
||||||
+ ") or positionDiff ("
|
|
||||||
+ positionDiff
|
|
||||||
+ ") is more than allowed.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,18 +134,15 @@ public final class DefaultOggSeekerTest {
|
|||||||
FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition)
|
FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
long nextSeekPosition = initialPosition;
|
long nextSeekPosition = initialPosition;
|
||||||
|
oggSeeker.startSeek(targetGranule);
|
||||||
int count = 0;
|
int count = 0;
|
||||||
oggSeeker.resetSeeking();
|
while (nextSeekPosition >= 0) {
|
||||||
|
|
||||||
do {
|
|
||||||
input.setPosition((int) nextSeekPosition);
|
|
||||||
nextSeekPosition = oggSeeker.getNextSeekPosition(targetGranule, input);
|
|
||||||
|
|
||||||
if (count++ > 100) {
|
if (count++ > 100) {
|
||||||
fail("infinite loop?");
|
fail("Seek failed to converge in 100 iterations");
|
||||||
}
|
}
|
||||||
} while (nextSeekPosition >= 0);
|
input.setPosition((int) nextSeekPosition);
|
||||||
|
nextSeekPosition = oggSeeker.read(input);
|
||||||
|
}
|
||||||
return -(nextSeekPosition + 2);
|
return -(nextSeekPosition + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,8 +153,7 @@ public final class DefaultOggSeekerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
|
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
|
||||||
throws IOException, InterruptedException {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,9 +85,9 @@ public final class DefaultOggSeekerUtilMethodsTest {
|
|||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
DefaultOggSeeker oggSeeker =
|
DefaultOggSeeker oggSeeker =
|
||||||
new DefaultOggSeeker(
|
new DefaultOggSeeker(
|
||||||
/* startPosition= */ 0,
|
|
||||||
/* endPosition= */ extractorInput.getLength(),
|
|
||||||
/* streamReader= */ new FlacReader(),
|
/* streamReader= */ new FlacReader(),
|
||||||
|
/* payloadStartPosition= */ 0,
|
||||||
|
/* payloadEndPosition= */ extractorInput.getLength(),
|
||||||
/* firstPayloadPageSize= */ 1,
|
/* firstPayloadPageSize= */ 1,
|
||||||
/* firstPayloadPageGranulePosition= */ 2,
|
/* firstPayloadPageGranulePosition= */ 2,
|
||||||
/* firstPayloadPageIsLastPage= */ false);
|
/* firstPayloadPageIsLastPage= */ false);
|
||||||
@ -99,87 +99,6 @@ public final class DefaultOggSeekerUtilMethodsTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSkipToPageOfGranule() throws IOException, InterruptedException {
|
|
||||||
byte[] packet = TestUtil.buildTestData(3 * 254, random);
|
|
||||||
byte[] data = TestUtil.joinByteArrays(
|
|
||||||
OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03),
|
|
||||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
|
||||||
packet,
|
|
||||||
OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03),
|
|
||||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
|
||||||
packet,
|
|
||||||
OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03),
|
|
||||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
|
||||||
packet);
|
|
||||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
|
||||||
|
|
||||||
// expect to be granule of the previous page returned as elapsedSamples
|
|
||||||
skipToPageOfGranule(input, 54000, 40000);
|
|
||||||
// expect to be at the start of the third page
|
|
||||||
assertThat(input.getPosition()).isEqualTo(2 * (30 + (3 * 254)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException {
|
|
||||||
byte[] packet = TestUtil.buildTestData(3 * 254, random);
|
|
||||||
byte[] data = TestUtil.joinByteArrays(
|
|
||||||
OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03),
|
|
||||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
|
||||||
packet,
|
|
||||||
OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03),
|
|
||||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
|
||||||
packet,
|
|
||||||
OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03),
|
|
||||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
|
||||||
packet);
|
|
||||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
|
||||||
|
|
||||||
skipToPageOfGranule(input, 40000, 20000);
|
|
||||||
// expect to be at the start of the second page
|
|
||||||
assertThat(input.getPosition()).isEqualTo(30 + (3 * 254));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException {
|
|
||||||
byte[] packet = TestUtil.buildTestData(3 * 254, random);
|
|
||||||
byte[] data = TestUtil.joinByteArrays(
|
|
||||||
OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03),
|
|
||||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
|
||||||
packet,
|
|
||||||
OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03),
|
|
||||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
|
||||||
packet,
|
|
||||||
OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03),
|
|
||||||
TestUtil.createByteArray(254, 254, 254), // Laces.
|
|
||||||
packet);
|
|
||||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
|
||||||
|
|
||||||
skipToPageOfGranule(input, 10000, -1);
|
|
||||||
assertThat(input.getPosition()).isEqualTo(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void skipToPageOfGranule(ExtractorInput input, long granule,
|
|
||||||
long elapsedSamplesExpected) throws IOException, InterruptedException {
|
|
||||||
DefaultOggSeeker oggSeeker =
|
|
||||||
new DefaultOggSeeker(
|
|
||||||
/* startPosition= */ 0,
|
|
||||||
/* endPosition= */ input.getLength(),
|
|
||||||
/* streamReader= */ new FlacReader(),
|
|
||||||
/* firstPayloadPageSize= */ 1,
|
|
||||||
/* firstPayloadPageGranulePosition= */ 2,
|
|
||||||
/* firstPayloadPageIsLastPage= */ false);
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
assertThat(oggSeeker.skipToPageOfGranule(input, granule, -1))
|
|
||||||
.isEqualTo(elapsedSamplesExpected);
|
|
||||||
return;
|
|
||||||
} catch (FakeExtractorInput.SimulatedIOException e) {
|
|
||||||
input.resetPeekPosition();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadGranuleOfLastPage() throws IOException, InterruptedException {
|
public void testReadGranuleOfLastPage() throws IOException, InterruptedException {
|
||||||
FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays(
|
FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays(
|
||||||
@ -204,7 +123,7 @@ public final class DefaultOggSeekerUtilMethodsTest {
|
|||||||
assertReadGranuleOfLastPage(input, 60000);
|
assertReadGranuleOfLastPage(input, 60000);
|
||||||
fail();
|
fail();
|
||||||
} catch (EOFException e) {
|
} catch (EOFException e) {
|
||||||
// ignored
|
// Ignored.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,7 +135,7 @@ public final class DefaultOggSeekerUtilMethodsTest {
|
|||||||
assertReadGranuleOfLastPage(input, 60000);
|
assertReadGranuleOfLastPage(input, 60000);
|
||||||
fail();
|
fail();
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
// ignored
|
// Ignored.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,9 +143,9 @@ public final class DefaultOggSeekerUtilMethodsTest {
|
|||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
DefaultOggSeeker oggSeeker =
|
DefaultOggSeeker oggSeeker =
|
||||||
new DefaultOggSeeker(
|
new DefaultOggSeeker(
|
||||||
/* startPosition= */ 0,
|
|
||||||
/* endPosition= */ input.getLength(),
|
|
||||||
/* streamReader= */ new FlacReader(),
|
/* streamReader= */ new FlacReader(),
|
||||||
|
/* payloadStartPosition= */ 0,
|
||||||
|
/* payloadEndPosition= */ input.getLength(),
|
||||||
/* firstPayloadPageSize= */ 1,
|
/* firstPayloadPageSize= */ 1,
|
||||||
/* firstPayloadPageGranulePosition= */ 2,
|
/* firstPayloadPageGranulePosition= */ 2,
|
||||||
/* firstPayloadPageIsLastPage= */ false);
|
/* firstPayloadPageIsLastPage= */ false);
|
||||||
@ -235,7 +154,7 @@ public final class DefaultOggSeekerUtilMethodsTest {
|
|||||||
assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected);
|
assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected);
|
||||||
break;
|
break;
|
||||||
} catch (FakeExtractorInput.SimulatedIOException e) {
|
} catch (FakeExtractorInput.SimulatedIOException e) {
|
||||||
// ignored
|
// Ignored.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user