Commit 6a099f1c by andrewlewis Committed by Oliver Woodman

Clean up MP3 synchronization and fix handling < 4 frames.

Also add a test MP3 stream with one frame.

Make FakeExtractorInput's end of input detection to apply also for peekFully, and
make its skip and read methods read at least one byte.

Issue: #1732

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=133241641
parent b1f9798b
seekMap:
isSeekable = true
duration = 26125
getPosition(0) = 0
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = null
containerMimeType = null
sampleMimeType = audio/mpeg
maxInputSize = 4096
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = 2
sampleRate = 44100
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
sample count = 1
sample 0:
time = 0
flags = 1
data = length 418, hash B819987
tracksEnded = true
seekMap:
isSeekable = true
duration = 26125
getPosition(0) = 0
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = null
containerMimeType = null
sampleMimeType = audio/mpeg
maxInputSize = 4096
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = 2
sampleRate = 44100
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
sample count = 0
tracksEnded = true
seekMap:
isSeekable = true
duration = 26125
getPosition(0) = 0
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = null
containerMimeType = null
sampleMimeType = audio/mpeg
maxInputSize = 4096
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = 2
sampleRate = 44100
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
sample count = 0
tracksEnded = true
seekMap:
isSeekable = true
duration = 26125
getPosition(0) = 0
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = null
containerMimeType = null
sampleMimeType = audio/mpeg
maxInputSize = 4096
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = 2
sampleRate = 44100
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
sample count = 0
tracksEnded = true
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
numberOfTracks = 1
track 0:
format:
bitrate = -1
id = null
containerMimeType = null
sampleMimeType = audio/mpeg
maxInputSize = 4096
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = 2
sampleRate = 44100
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
sample count = 1
sample 0:
time = 0
flags = 1
data = length 418, hash B819987
tracksEnded = true
......@@ -33,4 +33,13 @@ public final class Mp3ExtractorTest extends InstrumentationTestCase {
}, "mp3/bear.mp3", getInstrumentation());
}
public void testTrimmedMp3Sample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
@Override
public Extractor create() {
return new Mp3Extractor();
}
}, "mp3/play-trimmed.mp3", getInstrumentation());
}
}
......@@ -131,8 +131,12 @@ public final class Mp3Extractor implements Extractor {
@Override
public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
if (synchronizedHeaderData == 0 && !synchronizeCatchingEndOfInput(input)) {
return RESULT_END_OF_INPUT;
if (synchronizedHeaderData == 0) {
try {
synchronize(input, false);
} catch (EOFException e) {
return RESULT_END_OF_INPUT;
}
}
if (seeker == null) {
seeker = setupSeeker(input);
......@@ -147,9 +151,20 @@ public final class Mp3Extractor implements Extractor {
private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
if (sampleBytesRemaining == 0) {
if (!maybeResynchronize(extractorInput)) {
extractorInput.resetPeekPosition();
if (!extractorInput.peekFully(scratch.data, 0, 4, true)) {
return RESULT_END_OF_INPUT;
}
scratch.setPosition(0);
int sampleHeaderData = scratch.readInt();
if ((sampleHeaderData & HEADER_MASK) != (synchronizedHeaderData & HEADER_MASK)
|| MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) {
// We have lost synchronization, so attempt to resynchronize starting at the next byte.
extractorInput.skipFully(1);
synchronizedHeaderData = 0;
return RESULT_CONTINUE;
}
MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader);
if (basisTimeUs == C.TIME_UNSET) {
basisTimeUs = seeker.getTimeUs(extractorInput.getPosition());
if (forcedFirstSampleTimestampUs != C.TIME_UNSET) {
......@@ -175,49 +190,13 @@ public final class Mp3Extractor implements Extractor {
return RESULT_CONTINUE;
}
/**
* Attempts to read an MPEG audio header at the current offset, resynchronizing if necessary.
*/
private boolean maybeResynchronize(ExtractorInput extractorInput)
throws IOException, InterruptedException {
extractorInput.resetPeekPosition();
if (!extractorInput.peekFully(scratch.data, 0, 4, true)) {
return false;
}
scratch.setPosition(0);
int sampleHeaderData = scratch.readInt();
if ((sampleHeaderData & HEADER_MASK) == (synchronizedHeaderData & HEADER_MASK)) {
int frameSize = MpegAudioHeader.getFrameSize(sampleHeaderData);
if (frameSize != C.LENGTH_UNSET) {
MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader);
return true;
}
}
synchronizedHeaderData = 0;
extractorInput.skipFully(1);
return synchronizeCatchingEndOfInput(extractorInput);
}
private boolean synchronizeCatchingEndOfInput(ExtractorInput input)
throws IOException, InterruptedException {
// An EOFException will be raised if any peek operation was partially satisfied. If a seek
// operation resulted in reading from within the last frame, we may try to peek past the end of
// the file in a partially-satisfied read operation, so we need to catch the exception.
try {
return synchronize(input, false);
} catch (EOFException e) {
return false;
}
}
private boolean synchronize(ExtractorInput input, boolean sniffing)
throws IOException, InterruptedException {
int searched = 0;
int validFrameCount = 0;
int candidateSynchronizedHeaderData = 0;
int peekedId3Bytes = 0;
int searchedBytes = 0;
int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES;
input.resetPeekPosition();
if (input.getPosition() == 0) {
Id3Util.parseId3(input, gaplessInfoHolder);
......@@ -227,14 +206,9 @@ public final class Mp3Extractor implements Extractor {
}
}
while (true) {
if (sniffing && searched == MAX_SNIFF_BYTES) {
return false;
}
if (!sniffing && searched == MAX_SYNC_BYTES) {
throw new ParserException("Searched too many bytes.");
}
if (!input.peekFully(scratch.data, 0, 4, true)) {
return false;
if (!input.peekFully(scratch.data, 0, 4, validFrameCount > 0)) {
// We reached the end of the stream but found at least one valid frame.
break;
}
scratch.setPosition(0);
int headerData = scratch.readInt();
......@@ -242,18 +216,23 @@ public final class Mp3Extractor implements Extractor {
if ((candidateSynchronizedHeaderData != 0
&& (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK))
|| (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) {
// The header is invalid or doesn't match the candidate header. Try the next byte offset.
// The header doesn't match the candidate header or is invalid. Try the next byte offset.
if (searchedBytes++ == searchLimitBytes) {
if (!sniffing) {
throw new ParserException("Searched too many bytes.");
}
return false;
}
validFrameCount = 0;
candidateSynchronizedHeaderData = 0;
searched++;
if (sniffing) {
input.resetPeekPosition();
input.advancePeekPosition(peekedId3Bytes + searched);
input.advancePeekPosition(peekedId3Bytes + searchedBytes);
} else {
input.skipFully(1);
}
} else {
// The header is valid and matches the candidate header.
// The header matches the candidate header and/or is valid.
validFrameCount++;
if (validFrameCount == 1) {
MpegAudioHeader.populateHeader(headerData, synchronizedHeader);
......@@ -266,7 +245,7 @@ public final class Mp3Extractor implements Extractor {
}
// Prepare to read the synchronized frame.
if (sniffing) {
input.skipFully(peekedId3Bytes + searched);
input.skipFully(peekedId3Bytes + searchedBytes);
} else {
input.resetPeekPosition();
}
......
......@@ -84,7 +84,7 @@ public final class FakeExtractorInput implements ExtractorInput {
* @param position The position to set.
*/
public void setPosition(int position) {
Assert.assertTrue(0 <= position && position < data.length);
Assert.assertTrue(0 <= position && position <= data.length);
readPosition = position;
peekPosition = position;
}
......@@ -203,7 +203,7 @@ public final class FakeExtractorInput implements ExtractorInput {
peekPosition = readPosition;
throw new SimulatedIOException("Simulated IO error at position: " + position);
}
if (isEof()) {
if (length > 0 && position == data.length) {
if (allowEndOfInput) {
return false;
}
......@@ -217,6 +217,10 @@ public final class FakeExtractorInput implements ExtractorInput {
}
private int getReadLength(int requestedLength) {
if (readPosition == data.length) {
// If the requested length is non-zero, the end of the input will be read.
return requestedLength == 0 ? 0 : Integer.MAX_VALUE;
}
int targetPosition = readPosition + requestedLength;
if (simulatePartialReads && requestedLength > 1
&& !partiallySatisfiedTargetPositions.get(targetPosition)) {
......@@ -226,10 +230,6 @@ public final class FakeExtractorInput implements ExtractorInput {
return Math.min(requestedLength, data.length - readPosition);
}
private boolean isEof() {
return readPosition == data.length;
}
/**
* Builder of {@link FakeExtractorInput} instances.
*/
......
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