Improve seeking in MP3 files with XING headers.

Fix behavior of getTimeUs when seeking after the last entry in the table of
contents. Round correctly in getPosition, clipping to the stream duration based
on the input length (if known), falling back to the stream size from the header.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=109993852
This commit is contained in:
andrewlewis 2015-12-11 07:34:23 -08:00 committed by Oliver Woodman
parent cd1b991c48
commit 63dc769bff
4 changed files with 172 additions and 62 deletions

View File

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

View File

@ -39,50 +39,51 @@ import java.util.List;
public final class Mp4ExtractorTest extends TestCase { public final class Mp4ExtractorTest extends TestCase {
/** String of hexadecimal bytes containing the video stsd payload from an AVC video. */ /** String of hexadecimal bytes containing the video stsd payload from an AVC video. */
private static final byte[] VIDEO_STSD_PAYLOAD = getByteArray( private static final byte[] VIDEO_STSD_PAYLOAD = TestUtil.createByteArray(
"00000000000000010000009961766331000000000000000100000000000000000000000000000000050002" "00000000000000010000009961766331000000000000000100000000000000000000000000000000050002d00048"
+ "d00048000000480000000000000001000000000000000000000000000000000000000000000000000000" + "000000480000000000000001000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000018ffff0000002f617663430164001fffe100186764001facb402802dd808800000030080" + "18ffff0000002f617663430164001fffe100186764001facb402802dd80880000003008000001e078c19500100"
+ "00001e078c195001000468ee3cb000000014627472740000e35c0042a61000216cb8"); + "0468ee3cb000000014627472740000e35c0042a61000216cb8");
private static final byte[] VIDEO_HDLR_PAYLOAD = getByteArray("000000000000000076696465"); private static final byte[] VIDEO_HDLR_PAYLOAD = TestUtil.createByteArray(
private static final byte[] VIDEO_MDHD_PAYLOAD = "000000000000000076696465");
getByteArray("0000000000000000cf6c48890000001e00001c8a55c40000"); private static final byte[] VIDEO_MDHD_PAYLOAD = TestUtil.createByteArray(
"0000000000000000cf6c48890000001e00001c8a55c40000");
private static final int TIMESCALE = 30; private static final int TIMESCALE = 30;
private static final int VIDEO_WIDTH = 1280; private static final int VIDEO_WIDTH = 1280;
private static final int VIDEO_HEIGHT = 720; private static final int VIDEO_HEIGHT = 720;
/** String of hexadecimal bytes containing the video stsd payload for an mp4v track. */ /** String of hexadecimal bytes containing the video stsd payload for an mp4v track. */
private static final byte[] VIDEO_STSD_MP4V_PAYLOAD = getByteArray( private static final byte[] VIDEO_STSD_MP4V_PAYLOAD = TestUtil.createByteArray(
"0000000000000001000000A36D703476000000000000000100000000000000000000000000000000014000" "0000000000000001000000A36D703476000000000000000100000000000000000000000000000000014000B40048"
+ "B40048000000480000000000000001000000000000000000000000000000000000000000000000000000" + "000000480000000000000001000000000000000000000000000000000000000000000000000000000000000000"
+ "00000000000018FFFF0000004D6573647300000000033F00000004372011001A400004CF280002F11805" + "18FFFF0000004D6573647300000000033F00000004372011001A400004CF280002F1180528000001B001000001"
+ "28000001B001000001B58913000001000000012000C48D8800F50A04169463000001B2476F6F676C6506" + "B58913000001000000012000C48D8800F50A04169463000001B2476F6F676C65060102");
+ "0102");
private static final int VIDEO_MP4V_WIDTH = 320; private static final int VIDEO_MP4V_WIDTH = 320;
private static final int VIDEO_MP4V_HEIGHT = 180; private static final int VIDEO_MP4V_HEIGHT = 180;
/** String of hexadecimal bytes containing the audio stsd payload from an AAC track. */ /** String of hexadecimal bytes containing the audio stsd payload from an AAC track. */
private static final byte[] AUDIO_STSD_PAYLOAD = getByteArray( private static final byte[] AUDIO_STSD_PAYLOAD = TestUtil.createByteArray(
"0000000000000001000000596d703461000000000000000100000000000000000001001000000000ac4400" "0000000000000001000000596d703461000000000000000100000000000000000001001000000000ac4400000000"
+ "000000003565736473000000000327000000041f401500023e00024bc000023280051012080000000000" + "003565736473000000000327000000041f401500023e00024bc000023280051012080000000000000000000000"
+ "000000000000000000060102"); + "000000060102");
private static final byte[] AUDIO_HDLR_PAYLOAD = getByteArray("0000000000000000736f756e"); private static final byte[] AUDIO_HDLR_PAYLOAD = TestUtil.createByteArray(
private static final byte[] AUDIO_MDHD_PAYLOAD = "0000000000000000736f756e");
getByteArray("00000000cf6c4889cf6c488a0000ac4400a3e40055c40000"); 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. **/ /** 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. */ /** String of hexadecimal bytes containing an mvhd payload from an AVC/AAC video. */
private static final byte[] MVHD_PAYLOAD = getByteArray( private static final byte[] MVHD_PAYLOAD = TestUtil.createByteArray(
"00000000cf6c4888cf6c48880000025800023ad40001000001000000000000000000000000010000000000" "00000000cf6c4888cf6c48880000025800023ad40001000001000000000000000000000000010000000000000000"
+ "000000000000000000000100000000000000000000000000004000000000000000000000000000000000" + "000000000000000100000000000000000000000000004000000000000000000000000000000000000000000000"
+ "000000000000000000000000000003"); + "000000000000000003");
/** String of hexadecimal bytes containing a tkhd payload with an unknown duration. */ /** String of hexadecimal bytes containing a tkhd payload with an unknown duration. */
private static final byte[] TKHD_PAYLOAD = getByteArray( private static final byte[] TKHD_PAYLOAD = TestUtil.createByteArray(
"00000007D1F0C7BFD1F0C7BF0000000000000000FFFFFFFF00000000000000000000000000000000000100" "00000007D1F0C7BFD1F0C7BF0000000000000000FFFFFFFF00000000000000000000000000000000000100000000"
+ "0000000000000000000000000000010000000000000000000000000000400000000780000004380000"); + "0000000000000000000000010000000000000000000000000000400000000780000004380000");
/** Video frame timestamps in time units. */ /** Video frame timestamps in time units. */
private static final int[] SAMPLE_TIMESTAMPS = {0, 2, 3, 5, 6, 7}; 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); 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. * MP4 atom that can be serialized as a byte array.
*/ */

View File

@ -77,6 +77,15 @@ public class TestUtil {
return source; 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) { public static byte[] createByteArray(int... intArray) {
byte[] byteArray = new byte[intArray.length]; byte[] byteArray = new byte[intArray.length];
for (int i = 0; i < byteArray.length; i++) { for (int i = 0; i < byteArray.length; i++) {

View File

@ -54,7 +54,7 @@ import com.google.android.exoplayer.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(inputLength, firstFramePosition, durationUs); return new XingSeeker(firstFramePosition, durationUs, inputLength);
} }
long sizeBytes = frame.readUnsignedIntToInt(); 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: // 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(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. * 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 firstFramePosition;
private final long sizeBytes; private final long sizeBytes;
private final long durationUs; private final int headerSize;
private final long inputLength;
private XingSeeker(long inputLength, long firstFramePosition, long durationUs) { private XingSeeker(long firstFramePosition, long durationUs, long inputLength) {
this(inputLength, firstFramePosition, durationUs, null, 0); this(firstFramePosition, durationUs, inputLength, null, 0, 0);
} }
private XingSeeker(long inputLength, long firstFramePosition, long durationUs, private XingSeeker(long firstFramePosition, long durationUs, long inputLength,
long[] tableOfContents, long sizeBytes) { long[] tableOfContents, long sizeBytes, int headerSize) {
this.tableOfContents = tableOfContents;
this.firstFramePosition = firstFramePosition; this.firstFramePosition = firstFramePosition;
this.sizeBytes = sizeBytes;
this.durationUs = durationUs; this.durationUs = durationUs;
this.inputLength = inputLength; this.inputLength = inputLength;
this.tableOfContents = tableOfContents;
this.sizeBytes = sizeBytes;
this.headerSize = headerSize;
} }
@Override @Override
@ -124,8 +127,10 @@ import com.google.android.exoplayer.util.Util;
fx = fa + (fb - fa) * (percent - a); fx = fa + (fb - fa) * (percent - a);
} }
long position = (long) ((1.0 / 256) * fx * sizeBytes) + firstFramePosition; long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition;
return inputLength != C.LENGTH_UNBOUNDED ? Math.min(position, inputLength - 1) : position; long maximumPosition = inputLength != C.LENGTH_UNBOUNDED ? inputLength - 1
: firstFramePosition - headerSize + sizeBytes - 1;
return Math.min(position, maximumPosition);
} }
@Override @Override
@ -134,16 +139,14 @@ import com.google.android.exoplayer.util.Util;
return 0L; return 0L;
} }
double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes; double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes;
int previousIndex = Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false); int previousTocPosition =
long previousTime = getTimeUsForTocIndex(previousIndex); Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1;
if (previousIndex == 98) { long previousTime = getTimeUsForTocPosition(previousTocPosition);
return previousTime;
}
// Linearly interpolate the time taking into account the next entry. // Linearly interpolate the time taking into account the next entry.
long previousByte = previousIndex == -1 ? 0 : tableOfContents[previousIndex]; long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1];
long nextByte = tableOfContents[previousIndex + 1]; long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition];
long nextTime = getTimeUsForTocIndex(previousIndex + 1); long nextTime = getTimeUsForTocPosition(previousTocPosition + 1);
long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime)
* (offsetByte - previousByte) / (nextByte - previousByte)); * (offsetByte - previousByte) / (nextByte - previousByte));
return previousTime + timeOffset; 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) { private long getTimeUsForTocPosition(int tocPosition) {
return durationUs * (tocIndex + 1) / 100; return durationUs * tocPosition / 100;
} }
} }