diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java new file mode 100644 index 0000000000..61568aac93 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp3; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.C; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.MpegAudioHeader; +import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekPoint; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; + +/** MP3 seeker that uses metadata from a Xing header. */ +/* package */ final class XingSeeker implements 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. + * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required + * information is not present. + */ + public static @Nullable XingSeeker create( + long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) { + int samplesPerFrame = mpegAudioHeader.samplesPerFrame; + int sampleRate = mpegAudioHeader.sampleRate; + + int flags = frame.readInt(); + 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; + } + long durationUs = Util.scaleLargeTimestamp(frameCount, samplesPerFrame * C.MICROS_PER_SECOND, + sampleRate); + if ((flags & 0x06) != 0x06) { + // If the size in bytes or table of contents is missing, the stream is not seekable. + return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs); + } + + long dataSize = frame.readUnsignedIntToInt(); + long[] tableOfContents = new long[100]; + for (int i = 0; i < 100; i++) { + tableOfContents[i] = frame.readUnsignedByte(); + } + + // 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(); + + 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 dataStartPosition; + private final int xingFrameSize; + private final long durationUs; + /** Data size, including the XING frame. */ + private final long dataSize; + + private final long dataEndPosition; + /** + * Entries are in the range [0, 255], but are stored as long integers for convenience. Null if the + * table of contents was missing from the header, in which case seeking is not be supported. + */ + @Nullable private final long[] tableOfContents; + + private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) { + this( + dataStartPosition, + xingFrameSize, + durationUs, + /* dataSize= */ C.LENGTH_UNSET, + /* tableOfContents= */ null); + } + + private XingSeeker( + long dataStartPosition, + int xingFrameSize, + long durationUs, + long dataSize, + @Nullable long[] tableOfContents) { + this.dataStartPosition = dataStartPosition; + this.xingFrameSize = xingFrameSize; + this.durationUs = durationUs; + this.tableOfContents = tableOfContents; + this.dataSize = dataSize; + dataEndPosition = dataSize == C.LENGTH_UNSET ? C.POSITION_UNSET : dataStartPosition + dataSize; + } + + @Override + public boolean isSeekable() { + return tableOfContents != null; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + if (!isSeekable()) { + return new SeekPoints(new SeekPoint(0, dataStartPosition + xingFrameSize)); + } + timeUs = Util.constrainValue(timeUs, 0, durationUs); + double percent = (timeUs * 100d) / durationUs; + double scaledPosition; + if (percent <= 0) { + scaledPosition = 0; + } else if (percent >= 100) { + scaledPosition = 256; + } else { + int prevTableIndex = (int) percent; + long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents); + 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 positionOffset = Math.round((scaledPosition / 256) * dataSize); + // Ensure returned positions skip the frame containing the XING header. + positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1); + return new SeekPoints(new SeekPoint(timeUs, dataStartPosition + positionOffset)); + } + + @Override + public long getTimeUs(long position) { + long positionOffset = position - dataStartPosition; + if (!isSeekable() || positionOffset <= xingFrameSize) { + return 0L; + } + long[] tableOfContents = Assertions.checkNotNull(this.tableOfContents); + 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 + public long getDurationUs() { + return durationUs; + } + + @Override + public long getDataEndPosition() { + return dataEndPosition; + } + + /** + * 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 getTimeUsForTableIndex(int tableIndex) { + return (durationUs * tableIndex) / 100; + } + +} |