Commit 64c0e5c9 by Oliver Woodman

Use XING headers without size/table of contents.

These MP3s are unseekable but allow calculating the VBR duration correctly.

Treat streams as live only if they are unseekable and lack a duration.

Issue: #713
parent 6799d6da
...@@ -574,11 +574,11 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe ...@@ -574,11 +574,11 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe
sampleQueues.valueAt(i).clear(); sampleQueues.valueAt(i).clear();
} }
loadable = createLoadableFromStart(); loadable = createLoadableFromStart();
} else if (!seekMap.isSeekable()) { } else if (!seekMap.isSeekable() && maxTrackDurationUs == C.UNKNOWN_TIME_US) {
// We're playing a non-seekable stream. Assume it's live, and therefore that the data at // We're playing a non-seekable stream with unknown duration. Assume it's live, and
// the uri is a continuously shifting window of the latest available media. For this case // therefore that the data at the uri is a continuously shifting window of the latest
// there's no way to continue loading from where a previous load finished, and hence it's // available media. For this case there's no way to continue loading from where a previous
// necessary to load from the start whenever commencing a new load. // load finished, so it's necessary to load from the start whenever commencing a new load.
for (int i = 0; i < sampleQueues.size(); i++) { for (int i = 0; i < sampleQueues.size(); i++) {
sampleQueues.valueAt(i).clear(); sampleQueues.valueAt(i).clear();
} }
......
...@@ -191,7 +191,9 @@ public final class Mp3Extractor implements Extractor { ...@@ -191,7 +191,9 @@ public final class Mp3Extractor implements Extractor {
return RESULT_CONTINUE; return RESULT_CONTINUE;
} }
/** Attempts to read an MPEG audio header at the current offset, resynchronizing if necessary. */ /**
* Attempts to read an MPEG audio header at the current offset, resynchronizing if necessary.
*/
private long maybeResynchronize(ExtractorInput extractorInput) private long maybeResynchronize(ExtractorInput extractorInput)
throws IOException, InterruptedException { throws IOException, InterruptedException {
inputBuffer.mark(); inputBuffer.mark();
......
...@@ -45,23 +45,18 @@ import com.google.android.exoplayer.util.Util; ...@@ -45,23 +45,18 @@ import com.google.android.exoplayer.util.Util;
long firstFramePosition = position + mpegAudioHeader.frameSize; long firstFramePosition = position + mpegAudioHeader.frameSize;
int flags = frame.readInt(); int flags = frame.readInt();
// Frame count, size and table of contents are required to use this header. int frameCount;
if ((flags & 0x07) != 0x07) { if ((flags & 0x01) != 0x01 || (frameCount = frame.readUnsignedIntToInt()) == 0) {
// If the frame count is missing/invalid, the header can't be used to determine the duration.
return null; return null;
} }
long durationUs = Util.scaleLargeTimestamp(frameCount, samplesPerFrame * 1000000L, sampleRate);
// Read frame count, as (flags & 1) == 1. if ((flags & 0x06) != 0x06) {
int frameCount = frame.readUnsignedIntToInt(); // If the size in bytes or table of contents is missing, the stream is not seekable.
if (frameCount == 0) { return new XingSeeker(inputLength, firstFramePosition, durationUs);
return null;
} }
long durationUs =
Util.scaleLargeTimestamp(frameCount, samplesPerFrame * 1000000L, sampleRate);
// Read size in bytes, as (flags & 2) == 2.
long sizeBytes = frame.readUnsignedIntToInt(); long sizeBytes = frame.readUnsignedIntToInt();
// Read table-of-contents as (flags & 4) == 4.
frame.skipBytes(1); frame.skipBytes(1);
long[] tableOfContents = new long[99]; long[] tableOfContents = new long[99];
for (int i = 0; i < 99; i++) { for (int i = 0; i < 99; i++) {
...@@ -71,18 +66,24 @@ import com.google.android.exoplayer.util.Util; ...@@ -71,18 +66,24 @@ 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(tableOfContents, firstFramePosition, sizeBytes, durationUs, inputLength); return new XingSeeker(inputLength, firstFramePosition, durationUs, tableOfContents, sizeBytes);
} }
/** 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 firstFramePosition;
private final long sizeBytes; private final long sizeBytes;
private final long durationUs; private final long durationUs;
private final long inputLength; private final long inputLength;
private XingSeeker(long[] tableOfContents, long firstFramePosition, long sizeBytes, private XingSeeker(long inputLength, long firstFramePosition, long durationUs) {
long durationUs, long inputLength) { this(inputLength, firstFramePosition, durationUs, null, 0);
}
private XingSeeker(long inputLength, long firstFramePosition, long durationUs,
long[] tableOfContents, long sizeBytes) {
this.tableOfContents = tableOfContents; this.tableOfContents = tableOfContents;
this.firstFramePosition = firstFramePosition; this.firstFramePosition = firstFramePosition;
this.sizeBytes = sizeBytes; this.sizeBytes = sizeBytes;
...@@ -92,11 +93,14 @@ import com.google.android.exoplayer.util.Util; ...@@ -92,11 +93,14 @@ import com.google.android.exoplayer.util.Util;
@Override @Override
public boolean isSeekable() { public boolean isSeekable() {
return true; return tableOfContents != null;
} }
@Override @Override
public long getPosition(long timeUs) { public long getPosition(long timeUs) {
if (!isSeekable()) {
return firstFramePosition;
}
float percent = timeUs * 100f / durationUs; float percent = timeUs * 100f / durationUs;
float fx; float fx;
if (percent <= 0f) { if (percent <= 0f) {
...@@ -125,6 +129,9 @@ import com.google.android.exoplayer.util.Util; ...@@ -125,6 +129,9 @@ import com.google.android.exoplayer.util.Util;
@Override @Override
public long getTimeUs(long position) { public long getTimeUs(long position) {
if (!isSeekable()) {
return 0L;
}
long offsetByte = 256 * (position - firstFramePosition) / sizeBytes; long offsetByte = 256 * (position - firstFramePosition) / sizeBytes;
int previousIndex = Util.binarySearchFloor(tableOfContents, offsetByte, true, false); int previousIndex = Util.binarySearchFloor(tableOfContents, offsetByte, true, false);
long previousTime = getTimeUsForTocIndex(previousIndex); long previousTime = getTimeUsForTocIndex(previousIndex);
...@@ -146,7 +153,9 @@ import com.google.android.exoplayer.util.Util; ...@@ -146,7 +153,9 @@ import com.google.android.exoplayer.util.Util;
return durationUs; return durationUs;
} }
/** Returns the time in microseconds corresponding to an index in the table of contents. */ /**
* Returns the time in microseconds corresponding to an index in the table of contents.
*/
private long getTimeUsForTocIndex(int tocIndex) { private long getTimeUsForTocIndex(int tocIndex) {
return durationUs * (tocIndex + 1) / 100; return durationUs * (tocIndex + 1) / 100;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment