Commit b6755c14 by eguven Committed by Oliver Woodman

DefaultOggSeeker loop fix.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=123191416
parent 9609302b
...@@ -17,92 +17,134 @@ package com.google.android.exoplayer.extractor.ogg; ...@@ -17,92 +17,134 @@ package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.testutil.FakeExtractorInput; import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.testutil.TestUtil; import com.google.android.exoplayer.util.ParsableByteArray;
import junit.framework.TestCase; import junit.framework.TestCase;
import java.io.IOException; import java.io.IOException;
import java.util.Random;
/** /**
* Unit test for {@link DefaultOggSeeker}. * Unit test for {@link DefaultOggSeeker}.
*/ */
public final class DefaultOggSeekerTest extends TestCase { public final class DefaultOggSeekerTest extends TestCase {
private static final long HEADER_GRANULE = 200000;
private static final int START_POSITION = 0;
private static final int END_POSITION = 1000000;
private static final int TOTAL_SAMPLES = END_POSITION - START_POSITION;
private DefaultOggSeeker oggSeeker;
@Override
public void setUp() throws Exception {
super.setUp();
oggSeeker = DefaultOggSeeker.createOggSeekerForTesting(START_POSITION, END_POSITION,
TOTAL_SAMPLES);
}
public void testSetupUnboundAudioLength() { public void testSetupUnboundAudioLength() {
try { try {
new DefaultOggSeeker(0, C.LENGTH_UNBOUNDED, new FlacReader()); new DefaultOggSeeker(0, C.LENGTH_UNBOUNDED, new TestStreamReader());
fail(); fail();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// ignored // ignored
} }
} }
public void testGetNextSeekPositionMatch() throws IOException, InterruptedException { public void testSeeking() throws IOException, InterruptedException {
assertGetNextSeekPosition(HEADER_GRANULE + DefaultOggSeeker.MATCH_RANGE); Random random = new Random(0);
assertGetNextSeekPosition(HEADER_GRANULE + 1); for (int i = 0; i < 100; i++) {
testSeeking(random);
}
} }
public void testGetNextSeekPositionTooHigh() throws IOException, InterruptedException { public void testSeeking(Random random) throws IOException, InterruptedException {
assertGetNextSeekPosition(HEADER_GRANULE - 100000); OggTestFile testFile = OggTestFile.generate(random, 1000);
assertGetNextSeekPosition(HEADER_GRANULE); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build();
} TestStreamReader streamReader = new TestStreamReader();
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, testFile.data.length, streamReader);
OggPageHeader pageHeader = new OggPageHeader();
public void testGetNextSeekPositionTooLow() throws IOException, InterruptedException { while (true) {
assertGetNextSeekPosition(HEADER_GRANULE + DefaultOggSeeker.MATCH_RANGE + 1); long nextSeekPosition = oggSeeker.read(input);
assertGetNextSeekPosition(HEADER_GRANULE + 100000); if (nextSeekPosition == -1) {
} break;
}
input.setPosition((int) nextSeekPosition);
}
public void testGetNextSeekPositionBounds() throws IOException, InterruptedException { // Test granule 0 from file start
assertGetNextSeekPosition(HEADER_GRANULE + TOTAL_SAMPLES); assertEquals(0, seekTo(input, oggSeeker, 0, 0));
assertGetNextSeekPosition(HEADER_GRANULE - TOTAL_SAMPLES); assertEquals(0, input.getPosition());
}
// Test granule 0 from file end
assertEquals(0, seekTo(input, oggSeeker, 0, testFile.data.length - 1));
assertEquals(0, input.getPosition());
private void assertGetNextSeekPosition(long targetGranule) { // Test last granule
throws IOException, InterruptedException { long currentGranule = seekTo(input, oggSeeker, testFile.lastGranule, 0);
int pagePosition = 500000; long position = testFile.data.length;
FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( assertTrue((testFile.lastGranule > currentGranule && position > input.getPosition())
new byte[pagePosition], || (testFile.lastGranule == currentGranule && position == input.getPosition()));
TestData.buildOggHeader(0x00, HEADER_GRANULE, 22, 2),
TestUtil.createByteArray(54, 55) // laces
), false);
input.setPosition(pagePosition);
long granuleDiff = targetGranule - HEADER_GRANULE;
long expectedPosition;
if (granuleDiff > 0 && granuleDiff <= DefaultOggSeeker.MATCH_RANGE) {
expectedPosition = -1;
} else {
long doublePageSize = (27 + 2 + 54 + 55) * (granuleDiff <= 0 ? 2 : 1);
expectedPosition = pagePosition - doublePageSize + granuleDiff;
expectedPosition = Math.max(expectedPosition, START_POSITION);
expectedPosition = Math.min(expectedPosition, END_POSITION - 1);
} }
assertGetNextSeekPosition(expectedPosition, targetGranule, input);
}
private void assertGetNextSeekPosition(long expectedPosition, long targetGranule, { // Test exact granule
FakeExtractorInput input) throws IOException, InterruptedException { input.setPosition(testFile.data.length / 2);
while (true) { oggSeeker.skipToNextPage(input);
try { assertTrue(pageHeader.populate(input, true));
assertEquals(expectedPosition, oggSeeker.getNextSeekPosition(targetGranule, input)); long position = input.getPosition() + pageHeader.headerSize + pageHeader.bodySize;
break; long currentGranule = seekTo(input, oggSeeker, pageHeader.granulePosition, 0);
} catch (FakeExtractorInput.SimulatedIOException e) { assertTrue((pageHeader.granulePosition > currentGranule && position > input.getPosition())
// ignored || (pageHeader.granulePosition == currentGranule && position == input.getPosition()));
}
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);
long currentPosition = input.getPosition();
assertTrue("getNextSeekPosition() didn't leave input on a page start.",
pageHeader.populate(input, true));
if (currentGranule == 0) {
assertEquals(0, currentPosition);
} else {
int previousPageStart = testFile.findPreviousPageStart(currentPosition);
input.setPosition(previousPageStart);
assertTrue(pageHeader.populate(input, true));
assertEquals(pageHeader.granulePosition, currentGranule);
}
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(String.format("granuleDiff (%d) or positionDiff (%d) is more than allowed.",
granuleDiff, positionDiff));
} }
} }
} }
private long seekTo(FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule,
int initialPosition) throws IOException, InterruptedException {
long nextSeekPosition = initialPosition;
int count = 0;
oggSeeker.resetSeeking();
do {
input.setPosition((int) nextSeekPosition);
nextSeekPosition = oggSeeker.getNextSeekPosition(targetGranule, input);
if (count++ > 100) {
fail("infinite loop?");
}
} while (nextSeekPosition >= 0);
return -(nextSeekPosition + 2);
}
private static class TestStreamReader extends StreamReader {
@Override
protected long preparePayload(ParsableByteArray packet) {
return 0;
}
@Override
protected boolean readHeaders(ParsableByteArray packet, long position,
SetupData setupData) throws IOException, InterruptedException {
return false;
}
}
} }
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer.extractor.ogg; package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.testutil.FakeExtractorInput; import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.testutil.TestUtil; import com.google.android.exoplayer.testutil.TestUtil;
...@@ -32,7 +31,6 @@ import java.util.Random; ...@@ -32,7 +31,6 @@ import java.util.Random;
public class DefaultOggSeekerUtilMethodsTest extends TestCase { public class DefaultOggSeekerUtilMethodsTest extends TestCase {
private Random random = new Random(0); private Random random = new Random(0);
private DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, 100, new FlacReader());
public void testSkipToNextPage() throws Exception { public void testSkipToNextPage() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput( FakeExtractorInput extractorInput = TestData.createInput(
...@@ -45,17 +43,6 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -45,17 +43,6 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
assertEquals(4000, extractorInput.getPosition()); assertEquals(4000, extractorInput.getPosition());
} }
public void testSkipToNextPageUnbounded() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(4000, random),
new byte[]{'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), true);
skipToNextPage(extractorInput);
assertEquals(4000, extractorInput.getPosition());
}
public void testSkipToNextPageOverlap() throws Exception { public void testSkipToNextPageOverlap() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput( FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays( TestUtil.joinByteArrays(
...@@ -67,17 +54,6 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -67,17 +54,6 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
assertEquals(2046, extractorInput.getPosition()); assertEquals(2046, extractorInput.getPosition());
} }
public void testSkipToNextPageOverlapUnbounded() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays(
TestUtil.buildTestData(2046, random),
new byte[]{'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random)
), true);
skipToNextPage(extractorInput);
assertEquals(2046, extractorInput.getPosition());
}
public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput( FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays( TestUtil.joinByteArrays(
...@@ -100,9 +76,11 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -100,9 +76,11 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
private static void skipToNextPage(ExtractorInput extractorInput) private static void skipToNextPage(ExtractorInput extractorInput)
throws IOException, InterruptedException { throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, extractorInput.getLength(),
new FlacReader());
while (true) { while (true) {
try { try {
DefaultOggSeeker.skipToNextPage(extractorInput); oggSeeker.skipToNextPage(extractorInput);
break; break;
} catch (FakeExtractorInput.SimulatedIOException e) { /* ignored */ } } catch (FakeExtractorInput.SimulatedIOException e) { /* ignored */ }
} }
...@@ -110,17 +88,17 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -110,17 +88,17 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
public void testSkipToPageOfGranule() throws IOException, InterruptedException { public void testSkipToPageOfGranule() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random); byte[] packet = TestUtil.buildTestData(3 * 254, random);
FakeExtractorInput input = TestData.createInput( byte[] data = TestUtil.joinByteArrays(
TestUtil.joinByteArrays( TestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestData.buildOggHeader(0x01, 20000, 1000, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces.
TestUtil.createByteArray(254, 254, 254), // Laces. packet,
packet, TestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestData.buildOggHeader(0x04, 40000, 1001, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces.
TestUtil.createByteArray(254, 254, 254), // Laces. packet,
packet, TestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestData.buildOggHeader(0x04, 60000, 1002, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces.
TestUtil.createByteArray(254, 254, 254), // Laces. packet);
packet), false); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
// expect to be granule of the previous page returned as elapsedSamples // expect to be granule of the previous page returned as elapsedSamples
skipToPageOfGranule(input, 54000, 40000); skipToPageOfGranule(input, 54000, 40000);
...@@ -130,17 +108,17 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -130,17 +108,17 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException { public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random); byte[] packet = TestUtil.buildTestData(3 * 254, random);
FakeExtractorInput input = TestData.createInput( byte[] data = TestUtil.joinByteArrays(
TestUtil.joinByteArrays( TestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestData.buildOggHeader(0x01, 20000, 1000, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces.
TestUtil.createByteArray(254, 254, 254), // Laces. packet,
packet, TestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestData.buildOggHeader(0x04, 40000, 1001, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces.
TestUtil.createByteArray(254, 254, 254), // Laces. packet,
packet, TestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestData.buildOggHeader(0x04, 60000, 1002, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces.
TestUtil.createByteArray(254, 254, 254), // Laces. packet);
packet), false); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
skipToPageOfGranule(input, 40000, 20000); skipToPageOfGranule(input, 40000, 20000);
// expect to be at the start of the second page // expect to be at the start of the second page
...@@ -149,32 +127,28 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -149,32 +127,28 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException { public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException {
byte[] packet = TestUtil.buildTestData(3 * 254, random); byte[] packet = TestUtil.buildTestData(3 * 254, random);
FakeExtractorInput input = TestData.createInput( byte[] data = TestUtil.joinByteArrays(
TestUtil.joinByteArrays( TestData.buildOggHeader(0x01, 20000, 1000, 0x03),
TestData.buildOggHeader(0x01, 20000, 1000, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces.
TestUtil.createByteArray(254, 254, 254), // Laces. packet,
packet, TestData.buildOggHeader(0x04, 40000, 1001, 0x03),
TestData.buildOggHeader(0x04, 40000, 1001, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces.
TestUtil.createByteArray(254, 254, 254), // Laces. packet,
packet, TestData.buildOggHeader(0x04, 60000, 1002, 0x03),
TestData.buildOggHeader(0x04, 60000, 1002, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces.
TestUtil.createByteArray(254, 254, 254), // Laces. packet);
packet), false); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
try { skipToPageOfGranule(input, 10000, -1);
skipToPageOfGranule(input, 10000, 20000);
fail();
} catch (ParserException e) {
// ignored
}
assertEquals(0, input.getPosition()); assertEquals(0, input.getPosition());
} }
private void skipToPageOfGranule(ExtractorInput input, long granule, private void skipToPageOfGranule(ExtractorInput input, long granule,
long elapsedSamplesExpected) throws IOException, InterruptedException { long elapsedSamplesExpected) throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader());
while (true) { while (true) {
try { try {
assertEquals(elapsedSamplesExpected, oggSeeker.skipToPageOfGranule(input, granule)); assertEquals(elapsedSamplesExpected, oggSeeker.skipToPageOfGranule(input, granule, -1));
return; return;
} catch (FakeExtractorInput.SimulatedIOException e) { } catch (FakeExtractorInput.SimulatedIOException e) {
input.resetPeekPosition(); input.resetPeekPosition();
...@@ -221,6 +195,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -221,6 +195,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected)
throws IOException, InterruptedException { throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader());
while (true) { while (true) {
try { try {
assertEquals(expected, oggSeeker.readGranuleOfLastPage(input)); assertEquals(expected, oggSeeker.readGranuleOfLastPage(input));
......
/*
* 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.exoplayer.extractor.ogg;
import com.google.android.exoplayer.testutil.TestUtil;
import junit.framework.Assert;
import java.util.ArrayList;
import java.util.Random;
/**
* Generates test data.
*/
class OggTestFile {
public static final int MAX_PACKET_LENGTH = 2048;
public static final int MAX_SEGMENT_COUNT = 10;
public static final int MAX_GRANULES_IN_PAGE = 100000;
byte[] data;
long lastGranule;
int packetCount;
int pageCount;
private OggTestFile(byte[] data, long lastGranule, int packetCount, int pageCount) {
this.data = data;
this.lastGranule = lastGranule;
this.packetCount = packetCount;
this.pageCount = pageCount;
}
static OggTestFile generate(Random random, int pageCount) {
ArrayList<byte[]> fileData = new ArrayList<>();
int fileSize = 0;
long granule = 0;
int packetLength = -1;
int packetCount = 0;
for (int i = 0; i < pageCount; i++) {
int headerType = 0x00;
if (packetLength >= 0) {
headerType |= 1;
}
if (i == 0) {
headerType |= 2;
}
if (i == pageCount - 1) {
headerType |= 4;
}
granule += random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1;
int pageSegmentCount = random.nextInt(MAX_SEGMENT_COUNT);
byte[] header = TestData.buildOggHeader(headerType, granule, 0, pageSegmentCount);
fileData.add(header);
fileSize += header.length;
byte[] laces = new byte[pageSegmentCount];
int bodySize = 0;
for (int j = 0; j < pageSegmentCount; j++) {
if (packetLength < 0) {
packetCount++;
if (i < pageCount - 1) {
packetLength = random.nextInt(MAX_PACKET_LENGTH);
} else {
int maxPacketLength = 255 * (pageSegmentCount - j) - 1;
packetLength = random.nextInt(maxPacketLength);
}
} else if (i == pageCount - 1 && j == pageSegmentCount - 1) {
packetLength = Math.min(packetLength, 254);
}
laces[j] = (byte) Math.min(packetLength, 255);
bodySize += laces[j] & 0xFF;
packetLength -= 255;
}
fileData.add(laces);
fileSize += laces.length;
byte[] payload = TestUtil.buildTestData(bodySize, random);
fileData.add(payload);
fileSize += payload.length;
}
byte[] file = new byte[fileSize];
int position = 0;
for (byte[] data : fileData) {
System.arraycopy(data, 0, file, position, data.length);
position += data.length;
}
return new OggTestFile(file, granule, packetCount, pageCount);
}
int findPreviousPageStart(long position) {
for (int i = (int) (position - 4); i >= 0; i--) {
if (data[i] == 'O' && data[i + 1] == 'g' && data[i + 2] == 'g' && data[i + 3] == 'S') {
return i;
}
}
Assert.fail();
return -1;
}
}
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer.extractor.ogg; package com.google.android.exoplayer.extractor.ogg;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.SeekMap;
...@@ -29,15 +28,17 @@ import java.io.IOException; ...@@ -29,15 +28,17 @@ import java.io.IOException;
*/ */
/* package */ final class DefaultOggSeeker implements OggSeeker { /* package */ final class DefaultOggSeeker implements OggSeeker {
//@VisibleForTesting
public static final int MATCH_RANGE = 72000;
//@VisibleForTesting
public static final int MATCH_BYTE_RANGE = 100000;
private static final int DEFAULT_OFFSET = 30000;
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_IDLE = 3;
//@VisibleForTesting
public static final int MATCH_RANGE = 72000;
private static final int DEFAULT_OFFSET = 30000;
private final OggPageHeader pageHeader = new OggPageHeader(); private final OggPageHeader pageHeader = new OggPageHeader();
private final long startPosition; private final long startPosition;
private final long endPosition; private final long endPosition;
...@@ -48,28 +49,23 @@ import java.io.IOException; ...@@ -48,28 +49,23 @@ import java.io.IOException;
private volatile long queriedGranule; private volatile long queriedGranule;
private long positionBeforeSeekToEnd; private long positionBeforeSeekToEnd;
private long targetGranule; private long targetGranule;
private long elapsedSamples;
private long start;
public static DefaultOggSeeker createOggSeekerForTesting(long startPosition, long endPosition, private long end;
long totalGranules) { private long startGranule;
Assertions.checkArgument(totalGranules > 0); private long endGranule;
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(startPosition, endPosition, null);
oggSeeker.totalGranules = totalGranules;
return oggSeeker;
}
/** /**
* Constructs an OggSeeker. * Constructs an OggSeeker.
* @param startPosition Start position of the payload. * @param startPosition Start position of the payload (inclusive).
* @param endPosition End position of the payload. * @param endPosition End position of the payload (exclusive).
* @param streamReader StreamReader instance which owns this OggSeeker * @param streamReader StreamReader instance which owns this OggSeeker
*/ */
public DefaultOggSeeker(long startPosition, long endPosition, StreamReader streamReader) { public DefaultOggSeeker(long startPosition, long endPosition, StreamReader streamReader) {
this.streamReader = streamReader;
Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition); Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition);
this.streamReader = streamReader;
this.startPosition = startPosition; this.startPosition = startPosition;
this.endPosition = endPosition; this.endPosition = endPosition;
this.queriedGranule = 0;
this.state = STATE_SEEK_TO_END; this.state = STATE_SEEK_TO_END;
} }
...@@ -83,9 +79,9 @@ import java.io.IOException; ...@@ -83,9 +79,9 @@ 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 lastPagePosition = input.getLength() - OggPageHeader.MAX_PAGE_SIZE; long lastPagePosition = endPosition - OggPageHeader.MAX_PAGE_SIZE;
if (lastPagePosition > 0) { if (lastPagePosition > positionBeforeSeekToEnd) {
return Math.max(lastPagePosition, 0); return lastPagePosition;
} }
// fall through // fall through
...@@ -100,14 +96,13 @@ import java.io.IOException; ...@@ -100,14 +96,13 @@ import java.io.IOException;
currentGranule = 0; currentGranule = 0;
} else { } else {
long position = getNextSeekPosition(targetGranule, input); long position = getNextSeekPosition(targetGranule, input);
if (position != -1) { if (position >= 0) {
return position; return position;
} else {
currentGranule = skipToPageOfGranule(input, targetGranule);
} }
currentGranule = skipToPageOfGranule(input, targetGranule, -(position + 2));
} }
state = STATE_IDLE; state = STATE_IDLE;
return -currentGranule - 2; return -(currentGranule + 2);
default: default:
// Never happens. // Never happens.
...@@ -120,6 +115,7 @@ import java.io.IOException; ...@@ -120,6 +115,7 @@ import java.io.IOException;
Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK); Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK);
targetGranule = queriedGranule; targetGranule = queriedGranule;
state = STATE_SEEK; state = STATE_SEEK;
resetSeeking();
return targetGranule; return targetGranule;
} }
...@@ -128,39 +124,78 @@ import java.io.IOException; ...@@ -128,39 +124,78 @@ import java.io.IOException;
return totalGranules != 0 ? new OggSeekMap() : null; return totalGranules != 0 ? new OggSeekMap() : null;
} }
//@VisibleForTesting
public void resetSeeking() {
start = startPosition;
end = endPosition;
startGranule = 0;
endGranule = totalGranules;
}
/** /**
* Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput} * Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput}
* has to seek and then be passed for another call until -1 is return. If -1 is returned the * has to seek and then be passed for another call until a negative number is returned. If a
* input is at a position which is before the start of the page before the target page and at * negative number is returned the input is at a position which is before the target page and at
* which it is sensible to just skip pages to the target granule and pre-roll instead of doing * which it is sensible to just skip pages to the target granule and pre-roll instead of doing
* another seek request. * another seek request.
* *
* @param targetGranule the target granule position to seek to. * @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 -1 if it's close * @return the position to seek the {@link ExtractorInput} to for a next call or
* enough to skip to the target page. * -(currentGranule + 2) if it's close enough to skip to the target page.
* @throws IOException thrown if reading from the input fails. * @throws IOException thrown if reading from the input fails.
* @throws InterruptedException thrown if interrupted while reading from the input. * @throws InterruptedException thrown if interrupted while reading from the input.
*/ */
//@VisibleForTesting //@VisibleForTesting
public long getNextSeekPosition(long targetGranule, ExtractorInput input) public long getNextSeekPosition(long targetGranule, ExtractorInput input)
throws IOException, InterruptedException { throws IOException, InterruptedException {
long previousPosition = input.getPosition(); if (start == end) {
skipToNextPage(input); return -(startGranule + 2);
}
long initialPosition = input.getPosition();
if (!skipToNextPage(input, end)) {
if (start == initialPosition) {
throw new IOException("No ogg page can be found.");
}
return start;
}
pageHeader.populate(input, false); pageHeader.populate(input, false);
input.resetPeekPosition();
long granuleDistance = targetGranule - pageHeader.granulePosition; long granuleDistance = targetGranule - pageHeader.granulePosition;
if (granuleDistance <= 0 || granuleDistance > MATCH_RANGE) { int pageSize = pageHeader.headerSize + pageHeader.bodySize;
// estimated position too high or too low if (granuleDistance < 0 || granuleDistance > MATCH_RANGE) {
long offset = (pageHeader.bodySize + pageHeader.headerSize) if (granuleDistance < 0) {
* (granuleDistance <= 0 ? 2 : 1); end = initialPosition;
long estimatedPosition = getEstimatedPosition(input.getPosition(), granuleDistance, offset); endGranule = pageHeader.granulePosition;
if (estimatedPosition != previousPosition) { // Temporary prevention for simple loops } else {
return estimatedPosition; start = input.getPosition() + pageSize;
startGranule = pageHeader.granulePosition;
if (end - start + pageSize < MATCH_BYTE_RANGE) {
input.skip(pageSize);
return -(startGranule + 2);
}
} }
if (end - start < MATCH_BYTE_RANGE) {
end = start;
return start;
}
long offset = pageSize * (granuleDistance <= 0 ? 2 : 1);
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 (below target granule and within MATCH_RANGE)
input.resetPeekPosition(); // position accepted (before target granule and within MATCH_RANGE)
return -1; input.skip(pageSize);
return -(pageHeader.granulePosition + 2);
} }
private long getEstimatedPosition(long position, long granuleDistance, long offset) { private long getEstimatedPosition(long position, long granuleDistance, long offset) {
...@@ -204,21 +239,38 @@ import java.io.IOException; ...@@ -204,21 +239,38 @@ import java.io.IOException;
* @param input The {@code ExtractorInput} to skip to the next page. * @param input The {@code ExtractorInput} to skip to the next page.
* @throws IOException thrown if peeking/reading from the input fails. * @throws IOException thrown if peeking/reading from the input fails.
* @throws InterruptedException thrown if interrupted while peeking/reading from the input. * @throws InterruptedException thrown if interrupted while peeking/reading from the input.
* @throws EOFException if the next page can't be found before the end of the input.
*/ */
//@VisibleForTesting //@VisibleForTesting
static void skipToNextPage(ExtractorInput input) void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException {
throws IOException, InterruptedException { if (!skipToNextPage(input, endPosition)) {
// Not found until eof.
throw new EOFException();
}
}
/**
* Skips to the next page. Searches for the next page header.
*
* @param input The {@code ExtractorInput} to skip to the next page.
* @param until Searches until this position.
* @return true if the next page is found.
* @throws IOException thrown if peeking/reading from the input fails.
* @throws InterruptedException thrown if interrupted while peeking/reading from the input.
*/
//@VisibleForTesting
boolean skipToNextPage(ExtractorInput input, long until)
throws IOException, InterruptedException {
until = Math.min(until + 3, endPosition);
byte[] buffer = new byte[2048]; byte[] buffer = new byte[2048];
int peekLength = buffer.length; int peekLength = buffer.length;
long length = input.getLength();
while (true) { while (true) {
if (length != C.LENGTH_UNBOUNDED && input.getPosition() + peekLength > length) { if (input.getPosition() + peekLength > until) {
// Make sure to not peek beyond the end of the input. // Make sure to not peek beyond the end of the input.
peekLength = (int) (length - input.getPosition()); peekLength = (int) (until - input.getPosition());
if (peekLength < 4) { if (peekLength < 4) {
// Not found until eof. // Not found until end.
throw new EOFException(); return false;
} }
} }
input.peekFully(buffer, 0, peekLength, false); input.peekFully(buffer, 0, peekLength, false);
...@@ -227,7 +279,7 @@ import java.io.IOException; ...@@ -227,7 +279,7 @@ import java.io.IOException;
&& buffer[i + 3] == 'S') { && buffer[i + 3] == 'S') {
// Match! Skip to the start of the pattern. // Match! Skip to the start of the pattern.
input.skipFully(i); input.skipFully(i);
return; return true;
} }
} }
// Overlap by not skipping the entire peekLength. // Overlap by not skipping the entire peekLength.
...@@ -247,10 +299,9 @@ import java.io.IOException; ...@@ -247,10 +299,9 @@ import java.io.IOException;
//@VisibleForTesting //@VisibleForTesting
long readGranuleOfLastPage(ExtractorInput input) long readGranuleOfLastPage(ExtractorInput input)
throws IOException, InterruptedException { throws IOException, InterruptedException {
Assertions.checkArgument(input.getLength() != C.LENGTH_UNBOUNDED); // never read forever!
skipToNextPage(input); skipToNextPage(input);
pageHeader.reset(); pageHeader.reset();
while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < input.getLength()) { while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < endPosition) {
pageHeader.populate(input, false); pageHeader.populate(input, false);
input.skipFully(pageHeader.headerSize + pageHeader.bodySize); input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
} }
...@@ -259,39 +310,31 @@ import java.io.IOException; ...@@ -259,39 +310,31 @@ import java.io.IOException;
/** /**
* Skips to the position of the start of the page containing the {@code targetGranule} and * Skips to the position of the start of the page containing the {@code targetGranule} and
* returns the elapsed samples which is the granule of the page previous to the target page. * returns the granule of the page previous to the target page.
* <p>
* Note that the position of the {@code input} must be before the start of the page previous to
* the page containing the targetGranule to get the correct number of elapsed samples.
* Which is in short like: {@code pos(input) <= pos(targetPage.pageSequence - 1)}.
* *
* @param input the {@link ExtractorInput} to read from. * @param input the {@link ExtractorInput} to read from.
* @param targetGranule the target granule (number of frames per channel). * @param targetGranule the target granule.
* @return the number of elapsed samples at the start of the target page. * @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 thrown if populating the page header fails. * @throws ParserException thrown if populating the page header fails.
* @throws IOException thrown if reading from the input fails. * @throws IOException thrown if reading from the input fails.
* @throws InterruptedException thrown if interrupted while reading from the input. * @throws InterruptedException thrown if interrupted while reading from the input.
*/ */
//@VisibleForTesting //@VisibleForTesting
long skipToPageOfGranule(ExtractorInput input, long targetGranule) long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule)
throws IOException, InterruptedException { throws IOException, InterruptedException {
skipToNextPage(input); skipToNextPage(input);
pageHeader.populate(input, false); pageHeader.populate(input, 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. // Store in a member field to be able to resume after IOExceptions.
elapsedSamples = pageHeader.granulePosition; currentGranule = pageHeader.granulePosition;
// Peek next header. // Peek next header.
pageHeader.populate(input, false); pageHeader.populate(input, false);
} }
if (elapsedSamples == 0) {
throw new ParserException();
}
input.resetPeekPosition(); input.resetPeekPosition();
long returnValue = elapsedSamples; return currentGranule;
// Reset member state.
elapsedSamples = 0;
return returnValue;
} }
} }
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