diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java new file mode 100644 index 0000000000..8bb142f496 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java @@ -0,0 +1,136 @@ +/* + * 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.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 VBRI header. */ +/* package */ final class VbriSeeker implements Seeker { + + private static final String TAG = "VbriSeeker"; + + /** + * Returns a {@link VbriSeeker} 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 + * 'VBRI' tag. + * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required + * information is not present. + */ + public static @Nullable VbriSeeker create( + long inputLength, long position, MpegAudioHeader mpegAudioHeader, ParsableByteArray frame) { + frame.skipBytes(10); + int numFrames = frame.readInt(); + if (numFrames <= 0) { + return null; + } + int sampleRate = mpegAudioHeader.sampleRate; + long durationUs = Util.scaleLargeTimestamp(numFrames, + C.MICROS_PER_SECOND * (sampleRate >= 32000 ? 1152 : 576), sampleRate); + int entryCount = frame.readUnsignedShort(); + int scale = frame.readUnsignedShort(); + int entrySize = frame.readUnsignedShort(); + frame.skipBytes(2); + + long minPosition = position + mpegAudioHeader.frameSize; + // Read table of contents entries. + long[] timesUs = new long[entryCount]; + long[] positions = new long[entryCount]; + for (int index = 0; index < entryCount; index++) { + timesUs[index] = (index * durationUs) / entryCount; + // Ensure positions do not fall within the frame containing the VBRI header. This constraint + // will normally only apply to the first entry in the table. + positions[index] = Math.max(position, minPosition); + int segmentSize; + switch (entrySize) { + case 1: + segmentSize = frame.readUnsignedByte(); + break; + case 2: + segmentSize = frame.readUnsignedShort(); + break; + case 3: + segmentSize = frame.readUnsignedInt24(); + break; + case 4: + segmentSize = frame.readUnsignedIntToInt(); + break; + default: + return null; + } + position += segmentSize * scale; + } + if (inputLength != C.LENGTH_UNSET && inputLength != position) { + Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position); + } + return new VbriSeeker(timesUs, positions, durationUs, /* dataEndPosition= */ position); + } + + private final long[] timesUs; + private final long[] positions; + private final long durationUs; + private final long dataEndPosition; + + private VbriSeeker(long[] timesUs, long[] positions, long durationUs, long dataEndPosition) { + this.timesUs = timesUs; + this.positions = positions; + this.durationUs = durationUs; + this.dataEndPosition = dataEndPosition; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + int tableIndex = Util.binarySearchFloor(timesUs, timeUs, true, true); + SeekPoint seekPoint = new SeekPoint(timesUs[tableIndex], positions[tableIndex]); + if (seekPoint.timeUs >= timeUs || tableIndex == timesUs.length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint nextSeekPoint = new SeekPoint(timesUs[tableIndex + 1], positions[tableIndex + 1]); + return new SeekPoints(seekPoint, nextSeekPoint); + } + } + + @Override + public long getTimeUs(long position) { + return timesUs[Util.binarySearchFloor(positions, position, true, true)]; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public long getDataEndPosition() { + return dataEndPosition; + } +} |