Commit 0ea8c8bf by olly Committed by Oliver Woodman

Fix VBRI and XING seekers

- Remove skipping of the VBRI/XING frame before calculating position
  offsets. This was incorrect. Instead, a constraint is used to ensure
  we don't return positions within these frames, the difference being
  that the constraint adjusts only positions that would fall within
  the frames, where-as the previous approach shifted positions through
  the whole stream.
- Excluded last entry in the VBRI table because it has an invalid
  position (the length of the stream).
- Give variables in XingSeeker descriptive names.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=177451295
parent 022b85a6
......@@ -27,155 +27,155 @@ track 0:
initializationData:
sample count = 38
sample 0:
time = 1858196
time = 1871586
flags = 1
data = length 384, hash E801184A
sample 1:
time = 1882196
time = 1895586
flags = 1
data = length 384, hash 53C6CF9C
sample 2:
time = 1906196
time = 1919586
flags = 1
data = length 384, hash 19A8D99F
sample 3:
time = 1930196
time = 1943586
flags = 1
data = length 384, hash E47EB43F
sample 4:
time = 1954196
time = 1967586
flags = 1
data = length 384, hash 4EA329E7
sample 5:
time = 1978196
time = 1991586
flags = 1
data = length 384, hash 1CCAAE62
sample 6:
time = 2002196
time = 2015586
flags = 1
data = length 384, hash ED3F8C66
sample 7:
time = 2026196
time = 2039586
flags = 1
data = length 384, hash D3D646B6
sample 8:
time = 2050196
time = 2063586
flags = 1
data = length 384, hash 68CD1574
sample 9:
time = 2074196
time = 2087586
flags = 1
data = length 384, hash 8CEAB382
sample 10:
time = 2098196
time = 2111586
flags = 1
data = length 384, hash D54B1C48
sample 11:
time = 2122196
time = 2135586
flags = 1
data = length 384, hash FFE2EE90
sample 12:
time = 2146196
time = 2159586
flags = 1
data = length 384, hash BFE8A673
sample 13:
time = 2170196
time = 2183586
flags = 1
data = length 384, hash 978B1C92
sample 14:
time = 2194196
time = 2207586
flags = 1
data = length 384, hash 810CC71E
sample 15:
time = 2218196
time = 2231586
flags = 1
data = length 384, hash 44FE42D9
sample 16:
time = 2242196
time = 2255586
flags = 1
data = length 384, hash 2F5BB02C
sample 17:
time = 2266196
time = 2279586
flags = 1
data = length 384, hash 77DDB90
sample 18:
time = 2290196
time = 2303586
flags = 1
data = length 384, hash 24FB5EDA
sample 19:
time = 2314196
time = 2327586
flags = 1
data = length 384, hash E73203C6
sample 20:
time = 2338196
time = 2351586
flags = 1
data = length 384, hash 14B525F1
sample 21:
time = 2362196
time = 2375586
flags = 1
data = length 384, hash 5E0F4E2E
sample 22:
time = 2386196
time = 2399586
flags = 1
data = length 384, hash 67EE4E31
sample 23:
time = 2410196
time = 2423586
flags = 1
data = length 384, hash 2E04EC4C
sample 24:
time = 2434196
time = 2447586
flags = 1
data = length 384, hash 852CABA7
sample 25:
time = 2458196
time = 2471586
flags = 1
data = length 384, hash 19928903
sample 26:
time = 2482196
time = 2495586
flags = 1
data = length 384, hash 5DA42021
sample 27:
time = 2506196
time = 2519586
flags = 1
data = length 384, hash 45B20B7C
sample 28:
time = 2530196
time = 2543586
flags = 1
data = length 384, hash D108A215
sample 29:
time = 2554196
time = 2567586
flags = 1
data = length 384, hash BD25DB7C
sample 30:
time = 2578196
time = 2591586
flags = 1
data = length 384, hash DA7F9861
sample 31:
time = 2602196
time = 2615586
flags = 1
data = length 384, hash CCD576F
sample 32:
time = 2626196
time = 2639586
flags = 1
data = length 384, hash 405C1EB5
sample 33:
time = 2650196
time = 2663586
flags = 1
data = length 384, hash 6640B74E
sample 34:
time = 2674196
time = 2687586
flags = 1
data = length 384, hash B4E5937A
sample 35:
time = 2698196
time = 2711586
flags = 1
data = length 384, hash CEE17733
sample 36:
time = 2722196
time = 2735586
flags = 1
data = length 384, hash 2A0DA733
sample 37:
time = 2746196
time = 2759586
flags = 1
data = length 384, hash 97F4129B
tracksEnded = true
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp3;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.Util;
/**
......@@ -26,22 +27,21 @@ import com.google.android.exoplayer2.util.Util;
private static final int BITS_PER_BYTE = 8;
private final long firstFramePosition;
private final long dataSize;
private final int frameSize;
private final long dataSize;
private final int bitrate;
private final long durationUs;
/**
* @param firstFramePosition The position (byte offset) of the first frame.
* @param inputLength The length of the stream.
* @param frameSize The size of a single frame in the stream.
* @param bitrate The stream's bitrate.
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param firstFramePosition The position of the first frame in the stream.
* @param mpegAudioHeader The MPEG audio header associated with the first frame.
*/
public ConstantBitrateSeeker(long firstFramePosition, long inputLength, int frameSize,
int bitrate) {
public ConstantBitrateSeeker(long inputLength, long firstFramePosition,
MpegAudioHeader mpegAudioHeader) {
this.firstFramePosition = firstFramePosition;
this.frameSize = frameSize;
this.bitrate = bitrate;
this.frameSize = mpegAudioHeader.frameSize;
this.bitrate = mpegAudioHeader.bitrate;
if (inputLength == C.LENGTH_UNSET) {
dataSize = C.LENGTH_UNSET;
durationUs = C.TIME_UNSET;
......
......@@ -360,7 +360,7 @@ public final class Mp3Extractor implements Extractor {
int seekHeader = getSeekFrameHeader(frame, xingBase);
Seeker seeker;
if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) {
seeker = XingSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength());
seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame);
if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
// If there is a Xing header, read gapless playback metadata at a fixed offset.
input.resetPeekPosition();
......@@ -375,7 +375,7 @@ public final class Mp3Extractor implements Extractor {
return getConstantBitrateSeeker(input);
}
} else if (seekHeader == SEEK_HEADER_VBRI) {
seeker = VbriSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength());
seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame);
input.skipFully(synchronizedHeader.frameSize);
} else { // seekerHeader == SEEK_HEADER_UNSET
// This frame doesn't contain seeking information, so reset the peek position.
......@@ -393,8 +393,7 @@ public final class Mp3Extractor implements Extractor {
input.peekFully(scratch.data, 0, 4);
scratch.setPosition(0);
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
return new ConstantBitrateSeeker(input.getPosition(), input.getLength(),
synchronizedHeader.frameSize, synchronizedHeader.bitrate);
return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader);
}
/**
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.ParsableByteArray;
......@@ -25,21 +26,23 @@ import com.google.android.exoplayer2.util.Util;
*/
/* package */ final class VbriSeeker implements Mp3Extractor.Seeker {
private static final String TAG = "VbriSeeker";
/**
* Returns a {@link VbriSeeker} for seeking in the stream, if required information is present.
* Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
* caller should reset it.
*
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param position The position of the start of this frame in the stream.
* @param mpegAudioHeader The MPEG audio header associated with the frame.
* @param frame The data in this audio frame, with its position set to immediately after the
* 'VBRI' tag.
* @param position The position (byte offset) of the start of this frame in the stream.
* @param inputLength The length of the stream in bytes.
* @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required
* information is not present.
*/
public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame,
long position, long inputLength) {
public static VbriSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader,
ParsableByteArray frame) {
frame.skipBytes(10);
int numFrames = frame.readInt();
if (numFrames <= 0) {
......@@ -53,15 +56,15 @@ import com.google.android.exoplayer2.util.Util;
int entrySize = frame.readUnsignedShort();
frame.skipBytes(2);
// Skip the frame containing the VBRI header.
position += mpegAudioHeader.frameSize;
long minPosition = position + mpegAudioHeader.frameSize;
// Read table of contents entries.
long[] timesUs = new long[entryCount + 1];
long[] positions = new long[entryCount + 1];
timesUs[0] = 0L;
positions[0] = position;
for (int index = 1; index < timesUs.length; index++) {
long[] timesUs = new long[entryCount];
long[] positions = new long[entryCount];
for (int index = 0; index < entryCount; index++) {
timesUs[index] = (index * durationUs) / entryCount;
// Ensure positions do not fall within the frame containing the VBRI header. This constraint
// will normally only apply to the first entry in the table.
positions[index] = Math.max(position, minPosition);
int segmentSize;
switch (entrySize) {
case 1:
......@@ -80,9 +83,9 @@ import com.google.android.exoplayer2.util.Util;
return null;
}
position += segmentSize * scale;
timesUs[index] = index * durationUs / entryCount;
positions[index] =
inputLength == C.LENGTH_UNSET ? position : Math.min(inputLength, position);
}
if (inputLength != C.LENGTH_UNSET && inputLength != position) {
Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position);
}
return new VbriSeeker(timesUs, positions, durationUs);
}
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.ParsableByteArray;
......@@ -25,24 +26,25 @@ import com.google.android.exoplayer2.util.Util;
*/
/* package */ final class XingSeeker implements Mp3Extractor.Seeker {
private static final String TAG = "XingSeeker";
/**
* Returns a {@link XingSeeker} for seeking in the stream, if required information is present.
* Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
* caller should reset it.
*
* @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param position The position of the start of this frame in the stream.
* @param mpegAudioHeader The MPEG audio header associated with the frame.
* @param frame The data in this audio frame, with its position set to immediately after the
* 'Xing' or 'Info' tag.
* @param position The position (byte offset) of the start of this frame in the stream.
* @param inputLength The length of the stream in bytes.
* @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required
* information is not present.
*/
public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame,
long position, long inputLength) {
public static XingSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader,
ParsableByteArray frame) {
int samplesPerFrame = mpegAudioHeader.samplesPerFrame;
int sampleRate = mpegAudioHeader.sampleRate;
long firstFramePosition = position + mpegAudioHeader.frameSize;
int flags = frame.readInt();
int frameCount;
......@@ -54,10 +56,10 @@ import com.google.android.exoplayer2.util.Util;
sampleRate);
if ((flags & 0x06) != 0x06) {
// If the size in bytes or table of contents is missing, the stream is not seekable.
return new XingSeeker(firstFramePosition, durationUs, inputLength);
return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs);
}
long sizeBytes = frame.readUnsignedIntToInt();
long dataSize = frame.readUnsignedIntToInt();
long[] tableOfContents = new long[100];
for (int i = 0; i < 100; i++) {
tableOfContents[i] = frame.readUnsignedByte();
......@@ -66,32 +68,37 @@ import com.google.android.exoplayer2.util.Util;
// TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes:
// delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4);
// padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte();
return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents,
sizeBytes, mpegAudioHeader.frameSize);
if (inputLength != C.LENGTH_UNSET && inputLength != position + dataSize) {
Log.w(TAG, "XING data size mismatch: " + inputLength + ", " + (position + dataSize));
}
return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs, dataSize,
tableOfContents);
}
private final long firstFramePosition;
private final long dataStartPosition;
private final int xingFrameSize;
private final long durationUs;
private final long inputLength;
/**
* Data size, including the XING frame.
*/
private final long dataSize;
/**
* Entries are in the range [0, 255], but are stored as long integers for convenience.
*/
private final long[] tableOfContents;
private final long sizeBytes;
private final int headerSize;
private XingSeeker(long firstFramePosition, long durationUs, long inputLength) {
this(firstFramePosition, durationUs, inputLength, null, 0, 0);
private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) {
this(dataStartPosition, xingFrameSize, durationUs, C.LENGTH_UNSET, null);
}
private XingSeeker(long firstFramePosition, long durationUs, long inputLength,
long[] tableOfContents, long sizeBytes, int headerSize) {
this.firstFramePosition = firstFramePosition;
private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs, long dataSize,
long[] tableOfContents) {
this.dataStartPosition = dataStartPosition;
this.xingFrameSize = xingFrameSize;
this.durationUs = durationUs;
this.inputLength = inputLength;
this.dataSize = dataSize;
this.tableOfContents = tableOfContents;
this.sizeBytes = sizeBytes;
this.headerSize = headerSize;
}
@Override
......@@ -102,44 +109,45 @@ import com.google.android.exoplayer2.util.Util;
@Override
public long getPosition(long timeUs) {
if (!isSeekable()) {
return firstFramePosition;
return dataStartPosition + xingFrameSize;
}
double percent = (timeUs * 100d) / durationUs;
double fx;
double scaledPosition;
if (percent <= 0) {
fx = 0;
scaledPosition = 0;
} else if (percent >= 100) {
fx = 256;
scaledPosition = 256;
} else {
int a = (int) percent;
float fa = tableOfContents[a];
float fb = a == 99 ? 256 : tableOfContents[a + 1];
fx = fa + (fb - fa) * (percent - a);
int prevTableIndex = (int) percent;
double prevScaledPosition = tableOfContents[prevTableIndex];
double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];
// Linearly interpolate between the two scaled positions.
double interpolateFraction = percent - prevTableIndex;
scaledPosition = prevScaledPosition
+ (interpolateFraction * (nextScaledPosition - prevScaledPosition));
}
long position = Math.round((fx / 256) * sizeBytes) + firstFramePosition;
long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1
: firstFramePosition - headerSize + sizeBytes - 1;
return Math.min(position, maximumPosition);
long positionOffset = Math.round((scaledPosition / 256) * dataSize);
// Ensure returned positions skip the frame containing the XING header.
positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1);
return dataStartPosition + positionOffset;
}
@Override
public long getTimeUs(long position) {
if (!isSeekable() || position < firstFramePosition) {
long positionOffset = position - dataStartPosition;
if (!isSeekable() || positionOffset <= xingFrameSize) {
return 0L;
}
double offsetByte = (256d * (position - firstFramePosition)) / sizeBytes;
int previousTocPosition =
Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, true);
long previousTime = getTimeUsForTocPosition(previousTocPosition);
// Linearly interpolate the time taking into account the next entry.
long previousByte = tableOfContents[previousTocPosition];
long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition + 1];
long nextTime = getTimeUsForTocPosition(previousTocPosition + 1);
long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime)
* (offsetByte - previousByte) / (nextByte - previousByte));
return previousTime + timeOffset;
double scaledPosition = (positionOffset * 256d) / dataSize;
int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true);
long prevTimeUs = getTimeUsForTableIndex(prevTableIndex);
long prevScaledPosition = tableOfContents[prevTableIndex];
long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1);
long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];
// Linearly interpolate between the two table entries.
double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0
: ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition));
return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs));
}
@Override
......@@ -148,11 +156,13 @@ import com.google.android.exoplayer2.util.Util;
}
/**
* Returns the time in microseconds corresponding to a table of contents position, which is
* interpreted as a percentage of the stream's duration between 0 and 100.
* Returns the time in microseconds for a given table index.
*
* @param tableIndex A table index in the range [0, 100].
* @return The corresponding time in microseconds.
*/
private long getTimeUsForTocPosition(int tocPosition) {
return (durationUs * tocPosition) / 100;
private long getTimeUsForTableIndex(int tableIndex) {
return (durationUs * tableIndex) / 100;
}
}
......@@ -43,17 +43,17 @@ public final class XingSeekerTest {
private static final int XING_FRAME_POSITION = 157;
/**
* Size of the audio stream, encoded in {@link #XING_FRAME_PAYLOAD}.
* Data size, as encoded in {@link #XING_FRAME_PAYLOAD}.
*/
private static final int STREAM_SIZE_BYTES = 948505;
private static final int DATA_SIZE_BYTES = 948505;
/**
* Duration of the audio stream in microseconds, encoded in {@link #XING_FRAME_PAYLOAD}.
*/
private static final int STREAM_DURATION_US = 59271836;
/**
* The length of the file in bytes.
* The length of the stream in bytes.
*/
private static final int INPUT_LENGTH = 948662;
private static final int STREAM_LENGTH = XING_FRAME_POSITION + DATA_SIZE_BYTES;
private XingSeeker seeker;
private XingSeeker seekerWithInputLength;
......@@ -63,10 +63,10 @@ public final class XingSeekerTest {
public void setUp() throws Exception {
MpegAudioHeader xingFrameHeader = new MpegAudioHeader();
MpegAudioHeader.populateHeader(XING_FRAME_HEADER_DATA, xingFrameHeader);
seeker = XingSeeker.create(xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD),
XING_FRAME_POSITION, C.LENGTH_UNSET);
seekerWithInputLength = XingSeeker.create(xingFrameHeader,
new ParsableByteArray(XING_FRAME_PAYLOAD), XING_FRAME_POSITION, INPUT_LENGTH);
seeker = XingSeeker.create(C.LENGTH_UNSET, XING_FRAME_POSITION, xingFrameHeader,
new ParsableByteArray(XING_FRAME_PAYLOAD));
seekerWithInputLength = XingSeeker.create(STREAM_LENGTH,
XING_FRAME_POSITION, xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD));
xingFrameSize = xingFrameHeader.frameSize;
}
......@@ -84,10 +84,10 @@ public final class XingSeekerTest {
@Test
public void testGetTimeUsAtEndOfStream() {
assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES))
assertThat(seeker.getTimeUs(STREAM_LENGTH))
.isEqualTo(STREAM_DURATION_US);
assertThat(
seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES))
seekerWithInputLength.getTimeUs(STREAM_LENGTH))
.isEqualTo(STREAM_DURATION_US);
}
......@@ -100,14 +100,14 @@ public final class XingSeekerTest {
@Test
public void testGetPositionAtEndOfStream() {
assertThat(seeker.getPosition(STREAM_DURATION_US))
.isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1);
.isEqualTo(STREAM_LENGTH - 1);
assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US))
.isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1);
.isEqualTo(STREAM_LENGTH - 1);
}
@Test
public void testGetTimeForAllPositions() {
for (int offset = xingFrameSize; offset < STREAM_SIZE_BYTES; offset++) {
for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) {
int position = XING_FRAME_POSITION + offset;
long timeUs = seeker.getTimeUs(position);
assertThat(seeker.getPosition(timeUs)).isEqualTo(position);
......
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