From 64c0e5c9971e9d907ae334856961a481dc068871 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 1 Sep 2015 13:59:34 +0100 Subject: [PATCH] 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 --- .../extractor/ExtractorSampleSource.java | 10 ++--- .../exoplayer/extractor/mp3/Mp3Extractor.java | 4 +- .../exoplayer/extractor/mp3/XingSeeker.java | 45 +++++++++++-------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java index a2c60422e7..8f0bc4977b 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java @@ -574,11 +574,11 @@ public final class ExtractorSampleSource implements SampleSource, SampleSourceRe sampleQueues.valueAt(i).clear(); } loadable = createLoadableFromStart(); - } else if (!seekMap.isSeekable()) { - // We're playing a non-seekable stream. Assume it's live, and therefore that the data at - // the uri is a continuously shifting window of the latest available media. For this case - // there's no way to continue loading from where a previous load finished, and hence it's - // necessary to load from the start whenever commencing a new load. + } else if (!seekMap.isSeekable() && maxTrackDurationUs == C.UNKNOWN_TIME_US) { + // We're playing a non-seekable stream with unknown duration. Assume it's live, and + // therefore that the data at the uri is a continuously shifting window of the latest + // available media. For this case there's no way to continue loading from where a previous + // load finished, so it's necessary to load from the start whenever commencing a new load. for (int i = 0; i < sampleQueues.size(); i++) { sampleQueues.valueAt(i).clear(); } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java index 2af4af6913..6ca643dac2 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java @@ -191,7 +191,9 @@ public final class Mp3Extractor implements Extractor { 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) throws IOException, InterruptedException { inputBuffer.mark(); 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 b6d2e9c966..b8d86bcedc 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 @@ -45,23 +45,18 @@ import com.google.android.exoplayer.util.Util; long firstFramePosition = position + mpegAudioHeader.frameSize; int flags = frame.readInt(); - // Frame count, size and table of contents are required to use this header. - if ((flags & 0x07) != 0x07) { + int frameCount; + 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; } - - // Read frame count, as (flags & 1) == 1. - int frameCount = frame.readUnsignedIntToInt(); - if (frameCount == 0) { - return null; + long durationUs = Util.scaleLargeTimestamp(frameCount, samplesPerFrame * 1000000L, 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); } - long durationUs = - Util.scaleLargeTimestamp(frameCount, samplesPerFrame * 1000000L, sampleRate); - // Read size in bytes, as (flags & 2) == 2. long sizeBytes = frame.readUnsignedIntToInt(); - - // Read table-of-contents as (flags & 4) == 4. frame.skipBytes(1); long[] tableOfContents = new long[99]; for (int i = 0; i < 99; i++) { @@ -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: // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4); // 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 firstFramePosition; private final long sizeBytes; private final long durationUs; private final long inputLength; - private XingSeeker(long[] tableOfContents, long firstFramePosition, long sizeBytes, - long durationUs, long inputLength) { + private XingSeeker(long inputLength, long firstFramePosition, long durationUs) { + this(inputLength, firstFramePosition, durationUs, null, 0); + } + + private XingSeeker(long inputLength, long firstFramePosition, long durationUs, + long[] tableOfContents, long sizeBytes) { this.tableOfContents = tableOfContents; this.firstFramePosition = firstFramePosition; this.sizeBytes = sizeBytes; @@ -92,11 +93,14 @@ import com.google.android.exoplayer.util.Util; @Override public boolean isSeekable() { - return true; + return tableOfContents != null; } @Override public long getPosition(long timeUs) { + if (!isSeekable()) { + return firstFramePosition; + } float percent = timeUs * 100f / durationUs; float fx; if (percent <= 0f) { @@ -125,6 +129,9 @@ import com.google.android.exoplayer.util.Util; @Override public long getTimeUs(long position) { + if (!isSeekable()) { + return 0L; + } long offsetByte = 256 * (position - firstFramePosition) / sizeBytes; int previousIndex = Util.binarySearchFloor(tableOfContents, offsetByte, true, false); long previousTime = getTimeUsForTocIndex(previousIndex); @@ -146,7 +153,9 @@ import com.google.android.exoplayer.util.Util; 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) { return durationUs * (tocIndex + 1) / 100; }