Commit 3c8c5a33 by olly Committed by Oliver Woodman

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
parent 80bc50b6
......@@ -148,9 +148,9 @@ import java.io.IOException;
boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream.
oggSeeker =
new DefaultOggSeeker(
this,
payloadStartPosition,
input.getLength(),
this,
firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize,
firstPayloadPageHeader.granulePosition,
isLastPage);
......
......@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.extractor.ogg;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import androidx.test.ext.junit.runners.AndroidJUnit4;
......@@ -36,9 +35,9 @@ public final class DefaultOggSeekerTest {
public void testSetupWithUnsetEndPositionFails() {
try {
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ C.LENGTH_UNSET,
/* streamReader= */ new TestStreamReader(),
/* payloadStartPosition= */ 0,
/* payloadEndPosition= */ C.LENGTH_UNSET,
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 1,
/* firstPayloadPageIsLastPage= */ false);
......@@ -62,9 +61,9 @@ public final class DefaultOggSeekerTest {
TestStreamReader streamReader = new TestStreamReader();
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ testFile.data.length,
/* streamReader= */ streamReader,
/* payloadStartPosition= */ 0,
/* payloadEndPosition= */ testFile.data.length,
/* firstPayloadPageSize= */ testFile.firstPayloadPageSize,
/* firstPayloadPageGranulePosition= */ testFile.firstPayloadPageGranulePosition,
/* firstPayloadPageIsLastPage= */ false);
......@@ -78,70 +77,56 @@ public final class DefaultOggSeekerTest {
input.setPosition((int) nextSeekPosition);
}
// Test granule 0 from file start
assertThat(seekTo(input, oggSeeker, 0, 0)).isEqualTo(0);
// Test granule 0 from file start.
long granule = seekTo(input, oggSeeker, 0, 0);
assertThat(granule).isEqualTo(0);
assertThat(input.getPosition()).isEqualTo(0);
// Test granule 0 from file end
assertThat(seekTo(input, oggSeeker, 0, testFile.data.length - 1)).isEqualTo(0);
// Test granule 0 from file end.
granule = seekTo(input, oggSeeker, 0, testFile.data.length - 1);
assertThat(granule).isEqualTo(0);
assertThat(input.getPosition()).isEqualTo(0);
{ // Test last granule
long currentGranule = seekTo(input, oggSeeker, testFile.lastGranule, 0);
long position = testFile.data.length;
assertThat(
(testFile.lastGranule > currentGranule && position > input.getPosition())
|| (testFile.lastGranule == currentGranule && position == input.getPosition()))
.isTrue();
}
{ // Test exact granule
input.setPosition(testFile.data.length / 2);
oggSeeker.skipToNextPage(input);
assertThat(pageHeader.populate(input, true)).isTrue();
long position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize;
long currentGranule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0);
assertThat(
(pageHeader.granulePosition > currentGranule && position > input.getPosition())
|| (pageHeader.granulePosition == currentGranule
&& position == input.getPosition()))
.isTrue();
}
// Test last granule.
granule = seekTo(input, oggSeeker, testFile.lastGranule, 0);
long position = testFile.data.length;
// TODO: Simplify this.
assertThat(
(testFile.lastGranule > granule && position > input.getPosition())
|| (testFile.lastGranule == granule && position == input.getPosition()))
.isTrue();
// Test exact granule.
input.setPosition(testFile.data.length / 2);
oggSeeker.skipToNextPage(input);
assertThat(pageHeader.populate(input, true)).isTrue();
position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize;
granule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0);
// TODO: Simplify this.
assertThat(
(pageHeader.granulePosition > granule && position > input.getPosition())
|| (pageHeader.granulePosition == granule && position == input.getPosition()))
.isTrue();
for (int i = 0; i < 100; i += 1) {
long targetGranule = (long) (random.nextDouble() * testFile.lastGranule);
int initialPosition = random.nextInt(testFile.data.length);
long currentGranule = seekTo(input, oggSeeker, targetGranule, initialPosition);
granule = seekTo(input, oggSeeker, targetGranule, initialPosition);
long currentPosition = input.getPosition();
assertWithMessage("getNextSeekPosition() didn't leave input on a page start.")
.that(pageHeader.populate(input, true))
.isTrue();
if (currentGranule == 0) {
if (granule == 0) {
assertThat(currentPosition).isEqualTo(0);
} else {
int previousPageStart = testFile.findPreviousPageStart(currentPosition);
input.setPosition(previousPageStart);
assertThat(pageHeader.populate(input, true)).isTrue();
assertThat(currentGranule).isEqualTo(pageHeader.granulePosition);
pageHeader.populate(input, false);
assertThat(granule).isEqualTo(pageHeader.granulePosition);
}
input.setPosition((int) currentPosition);
oggSeeker.skipToPageOfGranule(input, targetGranule, -1);
long positionDiff = Math.abs(input.getPosition() - currentPosition);
long granuleDiff = currentGranule - targetGranule;
if ((granuleDiff > DefaultOggSeeker.MATCH_RANGE || granuleDiff < 0)
&& positionDiff > DefaultOggSeeker.MATCH_BYTE_RANGE) {
fail(
"granuleDiff ("
+ granuleDiff
+ ") or positionDiff ("
+ positionDiff
+ ") is more than allowed.");
}
pageHeader.populate(input, false);
// The target granule should be within the current page.
assertThat(granule).isAtMost(targetGranule);
assertThat(targetGranule).isLessThan(pageHeader.granulePosition);
}
}
......@@ -149,18 +134,15 @@ public final class DefaultOggSeekerTest {
FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition)
throws IOException, InterruptedException {
long nextSeekPosition = initialPosition;
oggSeeker.startSeek(targetGranule);
int count = 0;
oggSeeker.resetSeeking();
do {
input.setPosition((int) nextSeekPosition);
nextSeekPosition = oggSeeker.getNextSeekPosition(targetGranule, input);
while (nextSeekPosition >= 0) {
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);
}
......@@ -171,8 +153,7 @@ public final class DefaultOggSeekerTest {
}
@Override
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
throws IOException, InterruptedException {
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
return false;
}
}
......
......@@ -85,9 +85,9 @@ public final class DefaultOggSeekerUtilMethodsTest {
throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ extractorInput.getLength(),
/* streamReader= */ new FlacReader(),
/* payloadStartPosition= */ 0,
/* payloadEndPosition= */ extractorInput.getLength(),
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
......@@ -100,87 +100,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
public void testReadGranuleOfLastPage() throws IOException, InterruptedException {
FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays(
TestUtil.buildTestData(100, random),
......@@ -204,7 +123,7 @@ public final class DefaultOggSeekerUtilMethodsTest {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (EOFException e) {
// ignored
// Ignored.
}
}
......@@ -216,7 +135,7 @@ public final class DefaultOggSeekerUtilMethodsTest {
assertReadGranuleOfLastPage(input, 60000);
fail();
} catch (IllegalArgumentException e) {
// ignored
// Ignored.
}
}
......@@ -224,9 +143,9 @@ public final class DefaultOggSeekerUtilMethodsTest {
throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker =
new DefaultOggSeeker(
/* startPosition= */ 0,
/* endPosition= */ input.getLength(),
/* streamReader= */ new FlacReader(),
/* payloadStartPosition= */ 0,
/* payloadEndPosition= */ input.getLength(),
/* firstPayloadPageSize= */ 1,
/* firstPayloadPageGranulePosition= */ 2,
/* firstPayloadPageIsLastPage= */ false);
......@@ -235,7 +154,7 @@ public final class DefaultOggSeekerUtilMethodsTest {
assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected);
break;
} catch (FakeExtractorInput.SimulatedIOException e) {
// ignored
// Ignored.
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment