diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/mp3/XingSeekerTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/mp3/XingSeekerTest.java new file mode 100644 index 0000000000..ca5051b1ce --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/mp3/XingSeekerTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.extractor.mp3; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.testutil.TestUtil; +import com.google.android.exoplayer.util.MpegAudioHeader; +import com.google.android.exoplayer.util.ParsableByteArray; + +import android.test.InstrumentationTestCase; + +/** + * Tests for {@link XingSeeker} + */ +public final class XingSeekerTest extends InstrumentationTestCase { + + // XING header/payload from http://storage.googleapis.com/exoplayer-test-media-0/play.mp3. + private static final int XING_FRAME_HEADER_DATA = 0xFFFB3000; + private static final byte[] XING_FRAME_PAYLOAD = TestUtil.createByteArray( + "00000007000008dd000e7919000205080a0d0f1214171a1c1e212426292c2e303336383b3d404245484a4c4f5254" + + "575a5c5e616466696b6e707376787a7d808285878a8c8f929496999c9ea1a4a6a8abaeb0b3b5b8babdc0c2c4c7" + + "cacccfd2d4d6d9dcdee1e3e6e8ebeef0f2f5f8fafd"); + private static final int XING_FRAME_POSITION = 157; + + /** + * Size of the audio stream, encoded in {@link #XING_FRAME_PAYLOAD}. + */ + private static final int STREAM_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. + */ + private static final int INPUT_LENGTH = 948662; + + private XingSeeker seeker; + private XingSeeker seekerWithInputLength; + private int xingFrameSize; + + @Override + 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.UNKNOWN_TIME_US); + seekerWithInputLength = XingSeeker.create(xingFrameHeader, + new ParsableByteArray(XING_FRAME_PAYLOAD), XING_FRAME_POSITION, INPUT_LENGTH); + xingFrameSize = xingFrameHeader.frameSize; + } + + public void testGetTimeUsBeforeFirstAudioFrame() { + assertEquals(0, seeker.getTimeUs(-1)); + assertEquals(0, seekerWithInputLength.getTimeUs(-1)); + } + + public void testGetTimeUsAtFirstAudioFrame() { + assertEquals(0, seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize)); + assertEquals(0, seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize)); + } + + public void testGetTimeUsAtEndOfStream() { + assertEquals(STREAM_DURATION_US, + seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)); + assertEquals(STREAM_DURATION_US, + seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)); + } + + public void testGetPositionAtStartOfStream() { + assertEquals(XING_FRAME_POSITION + xingFrameSize, seeker.getPosition(0)); + assertEquals(XING_FRAME_POSITION + xingFrameSize, seekerWithInputLength.getPosition(0)); + } + + public void testGetPositionAtEndOfStream() { + assertEquals(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1, + seeker.getPosition(STREAM_DURATION_US)); + assertEquals(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1, + seekerWithInputLength.getPosition(STREAM_DURATION_US)); + } + + public void testGetTimeForAllPositions() { + for (int offset = xingFrameSize; offset < STREAM_SIZE_BYTES; offset++) { + int position = XING_FRAME_POSITION + offset; + long timeUs = seeker.getTimeUs(position); + assertEquals(position, seeker.getPosition(timeUs)); + timeUs = seekerWithInputLength.getTimeUs(position); + assertEquals(position, seekerWithInputLength.getPosition(timeUs)); + } + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java index a7a572d3cd..d4f796c3bc 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/mp4/Mp4ExtractorTest.java @@ -39,50 +39,51 @@ import java.util.List; public final class Mp4ExtractorTest extends TestCase { /** String of hexadecimal bytes containing the video stsd payload from an AVC video. */ - private static final byte[] VIDEO_STSD_PAYLOAD = getByteArray( - "00000000000000010000009961766331000000000000000100000000000000000000000000000000050002" - + "d00048000000480000000000000001000000000000000000000000000000000000000000000000000000" - + "00000000000018ffff0000002f617663430164001fffe100186764001facb402802dd808800000030080" - + "00001e078c195001000468ee3cb000000014627472740000e35c0042a61000216cb8"); - private static final byte[] VIDEO_HDLR_PAYLOAD = getByteArray("000000000000000076696465"); - private static final byte[] VIDEO_MDHD_PAYLOAD = - getByteArray("0000000000000000cf6c48890000001e00001c8a55c40000"); + private static final byte[] VIDEO_STSD_PAYLOAD = TestUtil.createByteArray( + "00000000000000010000009961766331000000000000000100000000000000000000000000000000050002d00048" + + "000000480000000000000001000000000000000000000000000000000000000000000000000000000000000000" + + "18ffff0000002f617663430164001fffe100186764001facb402802dd80880000003008000001e078c19500100" + + "0468ee3cb000000014627472740000e35c0042a61000216cb8"); + private static final byte[] VIDEO_HDLR_PAYLOAD = TestUtil.createByteArray( + "000000000000000076696465"); + private static final byte[] VIDEO_MDHD_PAYLOAD = TestUtil.createByteArray( + "0000000000000000cf6c48890000001e00001c8a55c40000"); private static final int TIMESCALE = 30; private static final int VIDEO_WIDTH = 1280; private static final int VIDEO_HEIGHT = 720; /** String of hexadecimal bytes containing the video stsd payload for an mp4v track. */ - private static final byte[] VIDEO_STSD_MP4V_PAYLOAD = getByteArray( - "0000000000000001000000A36D703476000000000000000100000000000000000000000000000000014000" - + "B40048000000480000000000000001000000000000000000000000000000000000000000000000000000" - + "00000000000018FFFF0000004D6573647300000000033F00000004372011001A400004CF280002F11805" - + "28000001B001000001B58913000001000000012000C48D8800F50A04169463000001B2476F6F676C6506" - + "0102"); + private static final byte[] VIDEO_STSD_MP4V_PAYLOAD = TestUtil.createByteArray( + "0000000000000001000000A36D703476000000000000000100000000000000000000000000000000014000B40048" + + "000000480000000000000001000000000000000000000000000000000000000000000000000000000000000000" + + "18FFFF0000004D6573647300000000033F00000004372011001A400004CF280002F1180528000001B001000001" + + "B58913000001000000012000C48D8800F50A04169463000001B2476F6F676C65060102"); private static final int VIDEO_MP4V_WIDTH = 320; private static final int VIDEO_MP4V_HEIGHT = 180; /** String of hexadecimal bytes containing the audio stsd payload from an AAC track. */ - private static final byte[] AUDIO_STSD_PAYLOAD = getByteArray( - "0000000000000001000000596d703461000000000000000100000000000000000001001000000000ac4400" - + "000000003565736473000000000327000000041f401500023e00024bc000023280051012080000000000" - + "000000000000000000060102"); - private static final byte[] AUDIO_HDLR_PAYLOAD = getByteArray("0000000000000000736f756e"); - private static final byte[] AUDIO_MDHD_PAYLOAD = - getByteArray("00000000cf6c4889cf6c488a0000ac4400a3e40055c40000"); + private static final byte[] AUDIO_STSD_PAYLOAD = TestUtil.createByteArray( + "0000000000000001000000596d703461000000000000000100000000000000000001001000000000ac4400000000" + + "003565736473000000000327000000041f401500023e00024bc000023280051012080000000000000000000000" + + "000000060102"); + private static final byte[] AUDIO_HDLR_PAYLOAD = TestUtil.createByteArray( + "0000000000000000736f756e"); + private static final byte[] AUDIO_MDHD_PAYLOAD = TestUtil.createByteArray( + "00000000cf6c4889cf6c488a0000ac4400a3e40055c40000"); /** String of hexadecimal bytes for an ftyp payload with major_brand mp41 and minor_version 0. **/ - private static final byte[] FTYP_PAYLOAD = getByteArray("6d70343100000000"); + private static final byte[] FTYP_PAYLOAD = TestUtil.createByteArray("6d70343100000000"); /** String of hexadecimal bytes containing an mvhd payload from an AVC/AAC video. */ - private static final byte[] MVHD_PAYLOAD = getByteArray( - "00000000cf6c4888cf6c48880000025800023ad40001000001000000000000000000000000010000000000" - + "000000000000000000000100000000000000000000000000004000000000000000000000000000000000" - + "000000000000000000000000000003"); + private static final byte[] MVHD_PAYLOAD = TestUtil.createByteArray( + "00000000cf6c4888cf6c48880000025800023ad40001000001000000000000000000000000010000000000000000" + + "000000000000000100000000000000000000000000004000000000000000000000000000000000000000000000" + + "000000000000000003"); /** String of hexadecimal bytes containing a tkhd payload with an unknown duration. */ - private static final byte[] TKHD_PAYLOAD = getByteArray( - "00000007D1F0C7BFD1F0C7BF0000000000000000FFFFFFFF00000000000000000000000000000000000100" - + "0000000000000000000000000000010000000000000000000000000000400000000780000004380000"); + private static final byte[] TKHD_PAYLOAD = TestUtil.createByteArray( + "00000007D1F0C7BFD1F0C7BF0000000000000000FFFFFFFF00000000000000000000000000000000000100000000" + + "0000000000000000000000010000000000000000000000000000400000000780000004380000"); /** Video frame timestamps in time units. */ private static final int[] SAMPLE_TIMESTAMPS = {0, 2, 3, 5, 6, 7}; @@ -450,15 +451,6 @@ public final class Mp4ExtractorTest extends TestCase { return new Mp4Atom(type, payload); } - private static byte[] getByteArray(String hexBytes) { - byte[] result = new byte[hexBytes.length() / 2]; - for (int i = 0; i < result.length; i++) { - result[i] = (byte) ((Character.digit(hexBytes.charAt(i * 2), 16) << 4) - + Character.digit(hexBytes.charAt(i * 2 + 1), 16)); - } - return result; - } - /** * MP4 atom that can be serialized as a byte array. */ diff --git a/library/src/androidTest/java/com/google/android/exoplayer/testutil/TestUtil.java b/library/src/androidTest/java/com/google/android/exoplayer/testutil/TestUtil.java index 77fc5fa12a..073b67dd8a 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/testutil/TestUtil.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/testutil/TestUtil.java @@ -77,6 +77,15 @@ public class TestUtil { return source; } + public static byte[] createByteArray(String hexBytes) { + byte[] result = new byte[hexBytes.length() / 2]; + for (int i = 0; i < result.length; i++) { + result[i] = (byte) ((Character.digit(hexBytes.charAt(i * 2), 16) << 4) + + Character.digit(hexBytes.charAt(i * 2 + 1), 16)); + } + return result; + } + public static byte[] createByteArray(int... intArray) { byte[] byteArray = new byte[intArray.length]; for (int i = 0; i < byteArray.length; i++) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java index bef49dfa89..8a482a3112 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java @@ -54,7 +54,7 @@ import com.google.android.exoplayer.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(inputLength, firstFramePosition, durationUs); + return new XingSeeker(firstFramePosition, durationUs, inputLength); } long sizeBytes = frame.readUnsignedIntToInt(); @@ -67,29 +67,32 @@ import com.google.android.exoplayer.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(inputLength, firstFramePosition, durationUs, tableOfContents, sizeBytes); + return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents, + sizeBytes, mpegAudioHeader.frameSize); } + private final long firstFramePosition; + private final long durationUs; + private final long inputLength; /** * Entries are in the range [0, 255], but are stored as long integers for convenience. */ private final long[] tableOfContents; - private final long firstFramePosition; private final long sizeBytes; - private final long durationUs; - private final long inputLength; + private final int headerSize; - private XingSeeker(long inputLength, long firstFramePosition, long durationUs) { - this(inputLength, firstFramePosition, durationUs, null, 0); + private XingSeeker(long firstFramePosition, long durationUs, long inputLength) { + this(firstFramePosition, durationUs, inputLength, null, 0, 0); } - private XingSeeker(long inputLength, long firstFramePosition, long durationUs, - long[] tableOfContents, long sizeBytes) { - this.tableOfContents = tableOfContents; + private XingSeeker(long firstFramePosition, long durationUs, long inputLength, + long[] tableOfContents, long sizeBytes, int headerSize) { this.firstFramePosition = firstFramePosition; - this.sizeBytes = sizeBytes; this.durationUs = durationUs; this.inputLength = inputLength; + this.tableOfContents = tableOfContents; + this.sizeBytes = sizeBytes; + this.headerSize = headerSize; } @Override @@ -124,8 +127,10 @@ import com.google.android.exoplayer.util.Util; fx = fa + (fb - fa) * (percent - a); } - long position = (long) ((1.0 / 256) * fx * sizeBytes) + firstFramePosition; - return inputLength != C.LENGTH_UNBOUNDED ? Math.min(position, inputLength - 1) : position; + long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition; + long maximumPosition = inputLength != C.LENGTH_UNBOUNDED ? inputLength - 1 + : firstFramePosition - headerSize + sizeBytes - 1; + return Math.min(position, maximumPosition); } @Override @@ -134,16 +139,14 @@ import com.google.android.exoplayer.util.Util; return 0L; } double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes; - int previousIndex = Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false); - long previousTime = getTimeUsForTocIndex(previousIndex); - if (previousIndex == 98) { - return previousTime; - } + int previousTocPosition = + Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1; + long previousTime = getTimeUsForTocPosition(previousTocPosition); // Linearly interpolate the time taking into account the next entry. - long previousByte = previousIndex == -1 ? 0 : tableOfContents[previousIndex]; - long nextByte = tableOfContents[previousIndex + 1]; - long nextTime = getTimeUsForTocIndex(previousIndex + 1); + long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1]; + long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition]; + long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) * (offsetByte - previousByte) / (nextByte - previousByte)); return previousTime + timeOffset; @@ -155,10 +158,11 @@ import com.google.android.exoplayer.util.Util; } /** - * Returns the time in microseconds corresponding to an index in the table of contents. + * 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. */ - private long getTimeUsForTocIndex(int tocIndex) { - return durationUs * (tocIndex + 1) / 100; + private long getTimeUsForTocPosition(int tocPosition) { + return durationUs * tocPosition / 100; } }