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

View File

@ -27,155 +27,155 @@ track 0:
initializationData: initializationData:
sample count = 38 sample count = 38
sample 0: sample 0:
time = 1858196 time = 1871586
flags = 1 flags = 1
data = length 384, hash E801184A data = length 384, hash E801184A
sample 1: sample 1:
time = 1882196 time = 1895586
flags = 1 flags = 1
data = length 384, hash 53C6CF9C data = length 384, hash 53C6CF9C
sample 2: sample 2:
time = 1906196 time = 1919586
flags = 1 flags = 1
data = length 384, hash 19A8D99F data = length 384, hash 19A8D99F
sample 3: sample 3:
time = 1930196 time = 1943586
flags = 1 flags = 1
data = length 384, hash E47EB43F data = length 384, hash E47EB43F
sample 4: sample 4:
time = 1954196 time = 1967586
flags = 1 flags = 1
data = length 384, hash 4EA329E7 data = length 384, hash 4EA329E7
sample 5: sample 5:
time = 1978196 time = 1991586
flags = 1 flags = 1
data = length 384, hash 1CCAAE62 data = length 384, hash 1CCAAE62
sample 6: sample 6:
time = 2002196 time = 2015586
flags = 1 flags = 1
data = length 384, hash ED3F8C66 data = length 384, hash ED3F8C66
sample 7: sample 7:
time = 2026196 time = 2039586
flags = 1 flags = 1
data = length 384, hash D3D646B6 data = length 384, hash D3D646B6
sample 8: sample 8:
time = 2050196 time = 2063586
flags = 1 flags = 1
data = length 384, hash 68CD1574 data = length 384, hash 68CD1574
sample 9: sample 9:
time = 2074196 time = 2087586
flags = 1 flags = 1
data = length 384, hash 8CEAB382 data = length 384, hash 8CEAB382
sample 10: sample 10:
time = 2098196 time = 2111586
flags = 1 flags = 1
data = length 384, hash D54B1C48 data = length 384, hash D54B1C48
sample 11: sample 11:
time = 2122196 time = 2135586
flags = 1 flags = 1
data = length 384, hash FFE2EE90 data = length 384, hash FFE2EE90
sample 12: sample 12:
time = 2146196 time = 2159586
flags = 1 flags = 1
data = length 384, hash BFE8A673 data = length 384, hash BFE8A673
sample 13: sample 13:
time = 2170196 time = 2183586
flags = 1 flags = 1
data = length 384, hash 978B1C92 data = length 384, hash 978B1C92
sample 14: sample 14:
time = 2194196 time = 2207586
flags = 1 flags = 1
data = length 384, hash 810CC71E data = length 384, hash 810CC71E
sample 15: sample 15:
time = 2218196 time = 2231586
flags = 1 flags = 1
data = length 384, hash 44FE42D9 data = length 384, hash 44FE42D9
sample 16: sample 16:
time = 2242196 time = 2255586
flags = 1 flags = 1
data = length 384, hash 2F5BB02C data = length 384, hash 2F5BB02C
sample 17: sample 17:
time = 2266196 time = 2279586
flags = 1 flags = 1
data = length 384, hash 77DDB90 data = length 384, hash 77DDB90
sample 18: sample 18:
time = 2290196 time = 2303586
flags = 1 flags = 1
data = length 384, hash 24FB5EDA data = length 384, hash 24FB5EDA
sample 19: sample 19:
time = 2314196 time = 2327586
flags = 1 flags = 1
data = length 384, hash E73203C6 data = length 384, hash E73203C6
sample 20: sample 20:
time = 2338196 time = 2351586
flags = 1 flags = 1
data = length 384, hash 14B525F1 data = length 384, hash 14B525F1
sample 21: sample 21:
time = 2362196 time = 2375586
flags = 1 flags = 1
data = length 384, hash 5E0F4E2E data = length 384, hash 5E0F4E2E
sample 22: sample 22:
time = 2386196 time = 2399586
flags = 1 flags = 1
data = length 384, hash 67EE4E31 data = length 384, hash 67EE4E31
sample 23: sample 23:
time = 2410196 time = 2423586
flags = 1 flags = 1
data = length 384, hash 2E04EC4C data = length 384, hash 2E04EC4C
sample 24: sample 24:
time = 2434196 time = 2447586
flags = 1 flags = 1
data = length 384, hash 852CABA7 data = length 384, hash 852CABA7
sample 25: sample 25:
time = 2458196 time = 2471586
flags = 1 flags = 1
data = length 384, hash 19928903 data = length 384, hash 19928903
sample 26: sample 26:
time = 2482196 time = 2495586
flags = 1 flags = 1
data = length 384, hash 5DA42021 data = length 384, hash 5DA42021
sample 27: sample 27:
time = 2506196 time = 2519586
flags = 1 flags = 1
data = length 384, hash 45B20B7C data = length 384, hash 45B20B7C
sample 28: sample 28:
time = 2530196 time = 2543586
flags = 1 flags = 1
data = length 384, hash D108A215 data = length 384, hash D108A215
sample 29: sample 29:
time = 2554196 time = 2567586
flags = 1 flags = 1
data = length 384, hash BD25DB7C data = length 384, hash BD25DB7C
sample 30: sample 30:
time = 2578196 time = 2591586
flags = 1 flags = 1
data = length 384, hash DA7F9861 data = length 384, hash DA7F9861
sample 31: sample 31:
time = 2602196 time = 2615586
flags = 1 flags = 1
data = length 384, hash CCD576F data = length 384, hash CCD576F
sample 32: sample 32:
time = 2626196 time = 2639586
flags = 1 flags = 1
data = length 384, hash 405C1EB5 data = length 384, hash 405C1EB5
sample 33: sample 33:
time = 2650196 time = 2663586
flags = 1 flags = 1
data = length 384, hash 6640B74E data = length 384, hash 6640B74E
sample 34: sample 34:
time = 2674196 time = 2687586
flags = 1 flags = 1
data = length 384, hash B4E5937A data = length 384, hash B4E5937A
sample 35: sample 35:
time = 2698196 time = 2711586
flags = 1 flags = 1
data = length 384, hash CEE17733 data = length 384, hash CEE17733
sample 36: sample 36:
time = 2722196 time = 2735586
flags = 1 flags = 1
data = length 384, hash 2A0DA733 data = length 384, hash 2A0DA733
sample 37: sample 37:
time = 2746196 time = 2759586
flags = 1 flags = 1
data = length 384, hash 97F4129B data = length 384, hash 97F4129B
tracksEnded = true tracksEnded = true

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp3; package com.google.android.exoplayer2.extractor.mp3;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.Util; 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 static final int BITS_PER_BYTE = 8;
private final long firstFramePosition; private final long firstFramePosition;
private final long dataSize;
private final int frameSize; private final int frameSize;
private final long dataSize;
private final int bitrate; private final int bitrate;
private final long durationUs; private final long durationUs;
/** /**
* @param firstFramePosition The position (byte offset) of the first frame. * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown.
* @param inputLength The length of the stream. * @param firstFramePosition The position of the first frame in the stream.
* @param frameSize The size of a single frame in the stream. * @param mpegAudioHeader The MPEG audio header associated with the first frame.
* @param bitrate The stream's bitrate.
*/ */
public ConstantBitrateSeeker(long firstFramePosition, long inputLength, int frameSize, public ConstantBitrateSeeker(long inputLength, long firstFramePosition,
int bitrate) { MpegAudioHeader mpegAudioHeader) {
this.firstFramePosition = firstFramePosition; this.firstFramePosition = firstFramePosition;
this.frameSize = frameSize; this.frameSize = mpegAudioHeader.frameSize;
this.bitrate = bitrate; this.bitrate = mpegAudioHeader.bitrate;
if (inputLength == C.LENGTH_UNSET) { if (inputLength == C.LENGTH_UNSET) {
dataSize = C.LENGTH_UNSET; dataSize = C.LENGTH_UNSET;
durationUs = C.TIME_UNSET; durationUs = C.TIME_UNSET;

View File

@ -360,7 +360,7 @@ public final class Mp3Extractor implements Extractor {
int seekHeader = getSeekFrameHeader(frame, xingBase); int seekHeader = getSeekFrameHeader(frame, xingBase);
Seeker seeker; Seeker seeker;
if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { 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 (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) {
// If there is a Xing header, read gapless playback metadata at a fixed offset. // If there is a Xing header, read gapless playback metadata at a fixed offset.
input.resetPeekPosition(); input.resetPeekPosition();
@ -375,7 +375,7 @@ public final class Mp3Extractor implements Extractor {
return getConstantBitrateSeeker(input); return getConstantBitrateSeeker(input);
} }
} else if (seekHeader == SEEK_HEADER_VBRI) { } 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); input.skipFully(synchronizedHeader.frameSize);
} else { // seekerHeader == SEEK_HEADER_UNSET } else { // seekerHeader == SEEK_HEADER_UNSET
// This frame doesn't contain seeking information, so reset the peek position. // 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); input.peekFully(scratch.data, 0, 4);
scratch.setPosition(0); scratch.setPosition(0);
MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
return new ConstantBitrateSeeker(input.getPosition(), input.getLength(), return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader);
synchronizedHeader.frameSize, synchronizedHeader.bitrate);
} }
/** /**

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.mp3; package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.ParsableByteArray; 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 { /* 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 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 * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
* caller should reset it. * 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 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 * @param frame The data in this audio frame, with its position set to immediately after the
* 'VBRI' tag. * '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 * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required
* information is not present. * information is not present.
*/ */
public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, public static VbriSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader,
long position, long inputLength) { ParsableByteArray frame) {
frame.skipBytes(10); frame.skipBytes(10);
int numFrames = frame.readInt(); int numFrames = frame.readInt();
if (numFrames <= 0) { if (numFrames <= 0) {
@ -53,15 +56,15 @@ import com.google.android.exoplayer2.util.Util;
int entrySize = frame.readUnsignedShort(); int entrySize = frame.readUnsignedShort();
frame.skipBytes(2); frame.skipBytes(2);
// Skip the frame containing the VBRI header. long minPosition = position + mpegAudioHeader.frameSize;
position += mpegAudioHeader.frameSize;
// Read table of contents entries. // Read table of contents entries.
long[] timesUs = new long[entryCount + 1]; long[] timesUs = new long[entryCount];
long[] positions = new long[entryCount + 1]; long[] positions = new long[entryCount];
timesUs[0] = 0L; for (int index = 0; index < entryCount; index++) {
positions[0] = position; timesUs[index] = (index * durationUs) / entryCount;
for (int index = 1; index < timesUs.length; index++) { // 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; int segmentSize;
switch (entrySize) { switch (entrySize) {
case 1: case 1:
@ -80,9 +83,9 @@ import com.google.android.exoplayer2.util.Util;
return null; return null;
} }
position += segmentSize * scale; position += segmentSize * scale;
timesUs[index] = index * durationUs / entryCount; }
positions[index] = if (inputLength != C.LENGTH_UNSET && inputLength != position) {
inputLength == C.LENGTH_UNSET ? position : Math.min(inputLength, position); Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position);
} }
return new VbriSeeker(timesUs, positions, durationUs); return new VbriSeeker(timesUs, positions, durationUs);
} }

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.mp3; package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.ParsableByteArray; 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 { /* 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 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 * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the
* caller should reset it. * 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 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 * @param frame The data in this audio frame, with its position set to immediately after the
* 'Xing' or 'Info' tag. * '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 * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required
* information is not present. * information is not present.
*/ */
public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, public static XingSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader,
long position, long inputLength) { ParsableByteArray frame) {
int samplesPerFrame = mpegAudioHeader.samplesPerFrame; int samplesPerFrame = mpegAudioHeader.samplesPerFrame;
int sampleRate = mpegAudioHeader.sampleRate; int sampleRate = mpegAudioHeader.sampleRate;
long firstFramePosition = position + mpegAudioHeader.frameSize;
int flags = frame.readInt(); int flags = frame.readInt();
int frameCount; int frameCount;
@ -54,10 +56,10 @@ import com.google.android.exoplayer2.util.Util;
sampleRate); sampleRate);
if ((flags & 0x06) != 0x06) { if ((flags & 0x06) != 0x06) {
// If the size in bytes or table of contents is missing, the stream is not seekable. // 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]; long[] tableOfContents = new long[100];
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {
tableOfContents[i] = frame.readUnsignedByte(); 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: // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes:
// delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4); // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4);
// padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte(); // 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 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. * Entries are in the range [0, 255], but are stored as long integers for convenience.
*/ */
private final long[] tableOfContents; private final long[] tableOfContents;
private final long sizeBytes;
private final int headerSize;
private XingSeeker(long firstFramePosition, long durationUs, long inputLength) { private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) {
this(firstFramePosition, durationUs, inputLength, null, 0, 0); this(dataStartPosition, xingFrameSize, durationUs, C.LENGTH_UNSET, null);
} }
private XingSeeker(long firstFramePosition, long durationUs, long inputLength, private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs, long dataSize,
long[] tableOfContents, long sizeBytes, int headerSize) { long[] tableOfContents) {
this.firstFramePosition = firstFramePosition; this.dataStartPosition = dataStartPosition;
this.xingFrameSize = xingFrameSize;
this.durationUs = durationUs; this.durationUs = durationUs;
this.inputLength = inputLength; this.dataSize = dataSize;
this.tableOfContents = tableOfContents; this.tableOfContents = tableOfContents;
this.sizeBytes = sizeBytes;
this.headerSize = headerSize;
} }
@Override @Override
@ -102,44 +109,45 @@ import com.google.android.exoplayer2.util.Util;
@Override @Override
public long getPosition(long timeUs) { public long getPosition(long timeUs) {
if (!isSeekable()) { if (!isSeekable()) {
return firstFramePosition; return dataStartPosition + xingFrameSize;
} }
double percent = (timeUs * 100d) / durationUs; double percent = (timeUs * 100d) / durationUs;
double fx; double scaledPosition;
if (percent <= 0) { if (percent <= 0) {
fx = 0; scaledPosition = 0;
} else if (percent >= 100) { } else if (percent >= 100) {
fx = 256; scaledPosition = 256;
} else { } else {
int a = (int) percent; int prevTableIndex = (int) percent;
float fa = tableOfContents[a]; double prevScaledPosition = tableOfContents[prevTableIndex];
float fb = a == 99 ? 256 : tableOfContents[a + 1]; double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];
fx = fa + (fb - fa) * (percent - a); // Linearly interpolate between the two scaled positions.
double interpolateFraction = percent - prevTableIndex;
scaledPosition = prevScaledPosition
+ (interpolateFraction * (nextScaledPosition - prevScaledPosition));
} }
long positionOffset = Math.round((scaledPosition / 256) * dataSize);
long position = Math.round((fx / 256) * sizeBytes) + firstFramePosition; // Ensure returned positions skip the frame containing the XING header.
long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1 positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1);
: firstFramePosition - headerSize + sizeBytes - 1; return dataStartPosition + positionOffset;
return Math.min(position, maximumPosition);
} }
@Override @Override
public long getTimeUs(long position) { public long getTimeUs(long position) {
if (!isSeekable() || position < firstFramePosition) { long positionOffset = position - dataStartPosition;
if (!isSeekable() || positionOffset <= xingFrameSize) {
return 0L; return 0L;
} }
double offsetByte = (256d * (position - firstFramePosition)) / sizeBytes; double scaledPosition = (positionOffset * 256d) / dataSize;
int previousTocPosition = int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true);
Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, true); long prevTimeUs = getTimeUsForTableIndex(prevTableIndex);
long previousTime = getTimeUsForTocPosition(previousTocPosition); long prevScaledPosition = tableOfContents[prevTableIndex];
long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1);
// Linearly interpolate the time taking into account the next entry. long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1];
long previousByte = tableOfContents[previousTocPosition]; // Linearly interpolate between the two table entries.
long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition + 1]; double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0
long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); : ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition));
long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs));
* (offsetByte - previousByte) / (nextByte - previousByte));
return previousTime + timeOffset;
} }
@Override @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 * Returns the time in microseconds for a given table index.
* interpreted as a percentage of the stream's duration between 0 and 100. *
* @param tableIndex A table index in the range [0, 100].
* @return The corresponding time in microseconds.
*/ */
private long getTimeUsForTocPosition(int tocPosition) { private long getTimeUsForTableIndex(int tableIndex) {
return (durationUs * tocPosition) / 100; return (durationUs * tableIndex) / 100;
} }
} }

View File

@ -43,17 +43,17 @@ public final class XingSeekerTest {
private static final int XING_FRAME_POSITION = 157; 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}. * Duration of the audio stream in microseconds, encoded in {@link #XING_FRAME_PAYLOAD}.
*/ */
private static final int STREAM_DURATION_US = 59271836; 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 seeker;
private XingSeeker seekerWithInputLength; private XingSeeker seekerWithInputLength;
@ -63,10 +63,10 @@ public final class XingSeekerTest {
public void setUp() throws Exception { public void setUp() throws Exception {
MpegAudioHeader xingFrameHeader = new MpegAudioHeader(); MpegAudioHeader xingFrameHeader = new MpegAudioHeader();
MpegAudioHeader.populateHeader(XING_FRAME_HEADER_DATA, xingFrameHeader); MpegAudioHeader.populateHeader(XING_FRAME_HEADER_DATA, xingFrameHeader);
seeker = XingSeeker.create(xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD), seeker = XingSeeker.create(C.LENGTH_UNSET, XING_FRAME_POSITION, xingFrameHeader,
XING_FRAME_POSITION, C.LENGTH_UNSET); new ParsableByteArray(XING_FRAME_PAYLOAD));
seekerWithInputLength = XingSeeker.create(xingFrameHeader, seekerWithInputLength = XingSeeker.create(STREAM_LENGTH,
new ParsableByteArray(XING_FRAME_PAYLOAD), XING_FRAME_POSITION, INPUT_LENGTH); XING_FRAME_POSITION, xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD));
xingFrameSize = xingFrameHeader.frameSize; xingFrameSize = xingFrameHeader.frameSize;
} }
@ -84,10 +84,10 @@ public final class XingSeekerTest {
@Test @Test
public void testGetTimeUsAtEndOfStream() { public void testGetTimeUsAtEndOfStream() {
assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) assertThat(seeker.getTimeUs(STREAM_LENGTH))
.isEqualTo(STREAM_DURATION_US); .isEqualTo(STREAM_DURATION_US);
assertThat( assertThat(
seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) seekerWithInputLength.getTimeUs(STREAM_LENGTH))
.isEqualTo(STREAM_DURATION_US); .isEqualTo(STREAM_DURATION_US);
} }
@ -100,14 +100,14 @@ public final class XingSeekerTest {
@Test @Test
public void testGetPositionAtEndOfStream() { public void testGetPositionAtEndOfStream() {
assertThat(seeker.getPosition(STREAM_DURATION_US)) assertThat(seeker.getPosition(STREAM_DURATION_US))
.isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); .isEqualTo(STREAM_LENGTH - 1);
assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US)) assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US))
.isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); .isEqualTo(STREAM_LENGTH - 1);
} }
@Test @Test
public void testGetTimeForAllPositions() { 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; int position = XING_FRAME_POSITION + offset;
long timeUs = seeker.getTimeUs(position); long timeUs = seeker.getTimeUs(position);
assertThat(seeker.getPosition(timeUs)).isEqualTo(position); assertThat(seeker.getPosition(timeUs)).isEqualTo(position);