diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index 2e0b21050c..7b6fe9db37 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -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 diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump index b3cb117cb2..3f393e768e 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump @@ -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 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java index e02e99e139..442e62deca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java @@ -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; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 7c579504c3..5c56dc460a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -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); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java index c43f065592..cc631d9f7e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java @@ -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); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index 55888066e7..e532249a64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -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; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java index b43949b7c2..e644abc7ef 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java @@ -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);