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
This commit is contained in:
olly 2017-11-30 07:20:45 -08:00 committed by Oliver Woodman
parent 022b85a625
commit 0ea8c8bfa0
7 changed files with 298 additions and 282 deletions

View File

@ -25,309 +25,313 @@ track 0:
language = null
drmInitData = -
initializationData:
sample count = 76
sample count = 77
sample 0:
time = 945782
time = 928567
flags = 1
data = length 384, hash F7E344F4
sample 1:
time = 952567
flags = 1
data = length 384, hash 14EF6AFD
sample 1:
time = 969782
sample 2:
time = 976567
flags = 1
data = length 384, hash 61C9B92C
sample 2:
time = 993782
sample 3:
time = 1000567
flags = 1
data = length 384, hash ABE1368
sample 3:
time = 1017782
sample 4:
time = 1024567
flags = 1
data = length 384, hash 6A3B8547
sample 4:
time = 1041782
sample 5:
time = 1048567
flags = 1
data = length 384, hash 30E905FA
sample 5:
time = 1065782
sample 6:
time = 1072567
flags = 1
data = length 384, hash 21A267CD
sample 6:
time = 1089782
sample 7:
time = 1096567
flags = 1
data = length 384, hash D96A2651
sample 7:
time = 1113782
sample 8:
time = 1120567
flags = 1
data = length 384, hash 72340177
sample 8:
time = 1137782
sample 9:
time = 1144567
flags = 1
data = length 384, hash 9345E744
sample 9:
time = 1161782
sample 10:
time = 1168567
flags = 1
data = length 384, hash FDE39E3A
sample 10:
time = 1185782
sample 11:
time = 1192567
flags = 1
data = length 384, hash F0B7465
sample 11:
time = 1209782
sample 12:
time = 1216567
flags = 1
data = length 384, hash 3693AB86
sample 12:
time = 1233782
sample 13:
time = 1240567
flags = 1
data = length 384, hash F39719B1
sample 13:
time = 1257782
sample 14:
time = 1264567
flags = 1
data = length 384, hash DA3958DC
sample 14:
time = 1281782
sample 15:
time = 1288567
flags = 1
data = length 384, hash FDC7599F
sample 15:
time = 1305782
sample 16:
time = 1312567
flags = 1
data = length 384, hash AEFF8471
sample 16:
time = 1329782
sample 17:
time = 1336567
flags = 1
data = length 384, hash 89C92C19
sample 17:
time = 1353782
sample 18:
time = 1360567
flags = 1
data = length 384, hash 5C786A4B
sample 18:
time = 1377782
sample 19:
time = 1384567
flags = 1
data = length 384, hash 5ACA8B
sample 19:
time = 1401782
sample 20:
time = 1408567
flags = 1
data = length 384, hash 7755974C
sample 20:
time = 1425782
sample 21:
time = 1432567
flags = 1
data = length 384, hash 3934B73C
sample 21:
time = 1449782
sample 22:
time = 1456567
flags = 1
data = length 384, hash DDD70A2F
sample 22:
time = 1473782
sample 23:
time = 1480567
flags = 1
data = length 384, hash 8FACE2EF
sample 23:
time = 1497782
sample 24:
time = 1504567
flags = 1
data = length 384, hash 4A602591
sample 24:
time = 1521782
sample 25:
time = 1528567
flags = 1
data = length 384, hash D019AA2D
sample 25:
time = 1545782
sample 26:
time = 1552567
flags = 1
data = length 384, hash 8A680B9D
sample 26:
time = 1569782
sample 27:
time = 1576567
flags = 1
data = length 384, hash B655C959
sample 27:
time = 1593782
sample 28:
time = 1600567
flags = 1
data = length 384, hash 2168336B
sample 28:
time = 1617782
sample 29:
time = 1624567
flags = 1
data = length 384, hash D77F6D31
sample 29:
time = 1641782
sample 30:
time = 1648567
flags = 1
data = length 384, hash 524B4B2F
sample 30:
time = 1665782
sample 31:
time = 1672567
flags = 1
data = length 384, hash 4752DDFC
sample 31:
time = 1689782
sample 32:
time = 1696567
flags = 1
data = length 384, hash E786727F
sample 32:
time = 1713782
sample 33:
time = 1720567
flags = 1
data = length 384, hash 5DA6FB8C
sample 33:
time = 1737782
sample 34:
time = 1744567
flags = 1
data = length 384, hash 92F24269
sample 34:
time = 1761782
sample 35:
time = 1768567
flags = 1
data = length 384, hash CD0A3BA1
sample 35:
time = 1785782
sample 36:
time = 1792567
flags = 1
data = length 384, hash 7D00409F
sample 36:
time = 1809782
sample 37:
time = 1816567
flags = 1
data = length 384, hash D7ADB5FA
sample 37:
time = 1833782
sample 38:
time = 1840567
flags = 1
data = length 384, hash 4A140209
sample 38:
time = 1857782
sample 39:
time = 1864567
flags = 1
data = length 384, hash E801184A
sample 39:
time = 1881782
sample 40:
time = 1888567
flags = 1
data = length 384, hash 53C6CF9C
sample 40:
time = 1905782
sample 41:
time = 1912567
flags = 1
data = length 384, hash 19A8D99F
sample 41:
time = 1929782
sample 42:
time = 1936567
flags = 1
data = length 384, hash E47EB43F
sample 42:
time = 1953782
sample 43:
time = 1960567
flags = 1
data = length 384, hash 4EA329E7
sample 43:
time = 1977782
sample 44:
time = 1984567
flags = 1
data = length 384, hash 1CCAAE62
sample 44:
time = 2001782
sample 45:
time = 2008567
flags = 1
data = length 384, hash ED3F8C66
sample 45:
time = 2025782
sample 46:
time = 2032567
flags = 1
data = length 384, hash D3D646B6
sample 46:
time = 2049782
sample 47:
time = 2056567
flags = 1
data = length 384, hash 68CD1574
sample 47:
time = 2073782
sample 48:
time = 2080567
flags = 1
data = length 384, hash 8CEAB382
sample 48:
time = 2097782
sample 49:
time = 2104567
flags = 1
data = length 384, hash D54B1C48
sample 49:
time = 2121782
sample 50:
time = 2128567
flags = 1
data = length 384, hash FFE2EE90
sample 50:
time = 2145782
sample 51:
time = 2152567
flags = 1
data = length 384, hash BFE8A673
sample 51:
time = 2169782
sample 52:
time = 2176567
flags = 1
data = length 384, hash 978B1C92
sample 52:
time = 2193782
sample 53:
time = 2200567
flags = 1
data = length 384, hash 810CC71E
sample 53:
time = 2217782
sample 54:
time = 2224567
flags = 1
data = length 384, hash 44FE42D9
sample 54:
time = 2241782
sample 55:
time = 2248567
flags = 1
data = length 384, hash 2F5BB02C
sample 55:
time = 2265782
sample 56:
time = 2272567
flags = 1
data = length 384, hash 77DDB90
sample 56:
time = 2289782
sample 57:
time = 2296567
flags = 1
data = length 384, hash 24FB5EDA
sample 57:
time = 2313782
sample 58:
time = 2320567
flags = 1
data = length 384, hash E73203C6
sample 58:
time = 2337782
sample 59:
time = 2344567
flags = 1
data = length 384, hash 14B525F1
sample 59:
time = 2361782
sample 60:
time = 2368567
flags = 1
data = length 384, hash 5E0F4E2E
sample 60:
time = 2385782
sample 61:
time = 2392567
flags = 1
data = length 384, hash 67EE4E31
sample 61:
time = 2409782
sample 62:
time = 2416567
flags = 1
data = length 384, hash 2E04EC4C
sample 62:
time = 2433782
sample 63:
time = 2440567
flags = 1
data = length 384, hash 852CABA7
sample 63:
time = 2457782
sample 64:
time = 2464567
flags = 1
data = length 384, hash 19928903
sample 64:
time = 2481782
sample 65:
time = 2488567
flags = 1
data = length 384, hash 5DA42021
sample 65:
time = 2505782
sample 66:
time = 2512567
flags = 1
data = length 384, hash 45B20B7C
sample 66:
time = 2529782
sample 67:
time = 2536567
flags = 1
data = length 384, hash D108A215
sample 67:
time = 2553782
sample 68:
time = 2560567
flags = 1
data = length 384, hash BD25DB7C
sample 68:
time = 2577782
sample 69:
time = 2584567
flags = 1
data = length 384, hash DA7F9861
sample 69:
time = 2601782
sample 70:
time = 2608567
flags = 1
data = length 384, hash CCD576F
sample 70:
time = 2625782
sample 71:
time = 2632567
flags = 1
data = length 384, hash 405C1EB5
sample 71:
time = 2649782
sample 72:
time = 2656567
flags = 1
data = length 384, hash 6640B74E
sample 72:
time = 2673782
sample 73:
time = 2680567
flags = 1
data = length 384, hash B4E5937A
sample 73:
time = 2697782
sample 74:
time = 2704567
flags = 1
data = length 384, hash CEE17733
sample 74:
time = 2721782
sample 75:
time = 2728567
flags = 1
data = length 384, hash 2A0DA733
sample 75:
time = 2745782
sample 76:
time = 2752567
flags = 1
data = length 384, hash 97F4129B
tracksEnded = true

View File

@ -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

View File

@ -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;

View File

@ -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);
}
/**

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);