summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java313
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java143
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java114
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java155
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java135
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java57
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java132
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java268
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java198
9 files changed, 1515 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java
new file mode 100644
index 0000000000..5d3b27e294
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java
@@ -0,0 +1,313 @@
+/*
+ * 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.ogg;
+
+import androidx.annotation.VisibleForTesting;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+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.Util;
+import java.io.EOFException;
+import java.io.IOException;
+
+/** Seeks in an Ogg stream. */
+/* package */ final class DefaultOggSeeker implements OggSeeker {
+
+ private static final int MATCH_RANGE = 72000;
+ private static final int MATCH_BYTE_RANGE = 100000;
+ private static final int DEFAULT_OFFSET = 30000;
+
+ private static final int STATE_SEEK_TO_END = 0;
+ private static final int STATE_READ_LAST_PAGE = 1;
+ private static final int STATE_SEEK = 2;
+ private static final int STATE_SKIP = 3;
+ private static final int STATE_IDLE = 4;
+
+ private final OggPageHeader pageHeader = new OggPageHeader();
+ private final long payloadStartPosition;
+ private final long payloadEndPosition;
+ private final StreamReader streamReader;
+
+ private int state;
+ private long totalGranules;
+ private long positionBeforeSeekToEnd;
+ private long targetGranule;
+
+ private long start;
+ private long end;
+ private long startGranule;
+ private long endGranule;
+
+ /**
+ * Constructs an OggSeeker.
+ *
+ * @param streamReader The {@link StreamReader} that owns this seeker.
+ * @param payloadStartPosition Start position of the payload (inclusive).
+ * @param payloadEndPosition End position of the payload (exclusive).
+ * @param firstPayloadPageSize The total size of the first payload page, in bytes.
+ * @param firstPayloadPageGranulePosition The granule position of the first payload page.
+ * @param firstPayloadPageIsLastPage Whether the first payload page is also the last page.
+ */
+ public DefaultOggSeeker(
+ StreamReader streamReader,
+ long payloadStartPosition,
+ long payloadEndPosition,
+ long firstPayloadPageSize,
+ long firstPayloadPageGranulePosition,
+ boolean firstPayloadPageIsLastPage) {
+ Assertions.checkArgument(
+ payloadStartPosition >= 0 && payloadEndPosition > payloadStartPosition);
+ this.streamReader = streamReader;
+ this.payloadStartPosition = payloadStartPosition;
+ this.payloadEndPosition = payloadEndPosition;
+ if (firstPayloadPageSize == payloadEndPosition - payloadStartPosition
+ || firstPayloadPageIsLastPage) {
+ totalGranules = firstPayloadPageGranulePosition;
+ state = STATE_IDLE;
+ } else {
+ state = STATE_SEEK_TO_END;
+ }
+ }
+
+ @Override
+ @SuppressWarnings("fallthrough")
+ public long read(ExtractorInput input) throws IOException, InterruptedException {
+ switch (state) {
+ case STATE_IDLE:
+ return -1;
+ case STATE_SEEK_TO_END:
+ positionBeforeSeekToEnd = input.getPosition();
+ state = STATE_READ_LAST_PAGE;
+ // Seek to the end just before the last page of stream to get the duration.
+ long lastPageSearchPosition = payloadEndPosition - OggPageHeader.MAX_PAGE_SIZE;
+ if (lastPageSearchPosition > positionBeforeSeekToEnd) {
+ return lastPageSearchPosition;
+ }
+ // Fall through.
+ case STATE_READ_LAST_PAGE:
+ totalGranules = readGranuleOfLastPage(input);
+ state = STATE_IDLE;
+ return positionBeforeSeekToEnd;
+ case STATE_SEEK:
+ long position = getNextSeekPosition(input);
+ if (position != C.POSITION_UNSET) {
+ return position;
+ }
+ state = STATE_SKIP;
+ // Fall through.
+ case STATE_SKIP:
+ skipToPageOfTargetGranule(input);
+ state = STATE_IDLE;
+ return -(startGranule + 2);
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ }
+
+ @Override
+ public OggSeekMap createSeekMap() {
+ return totalGranules != 0 ? new OggSeekMap() : null;
+ }
+
+ @Override
+ public void startSeek(long targetGranule) {
+ this.targetGranule = Util.constrainValue(targetGranule, 0, totalGranules - 1);
+ state = STATE_SEEK;
+ start = payloadStartPosition;
+ end = payloadEndPosition;
+ startGranule = 0;
+ endGranule = totalGranules;
+ }
+
+ /**
+ * Performs a single step of a seeking binary search, returning the byte position from which data
+ * should be provided for the next step, or {@link C#POSITION_UNSET} if the search has converged.
+ * If the search has converged then {@link #skipToPageOfTargetGranule(ExtractorInput)} should be
+ * called to skip to the target page.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @return The byte position from which data should be provided for the next step, or {@link
+ * C#POSITION_UNSET} if the search has converged.
+ * @throws IOException If reading from the input fails.
+ * @throws InterruptedException If interrupted while reading from the input.
+ */
+ private long getNextSeekPosition(ExtractorInput input) throws IOException, InterruptedException {
+ if (start == end) {
+ return C.POSITION_UNSET;
+ }
+
+ long currentPosition = input.getPosition();
+ if (!skipToNextPage(input, end)) {
+ if (start == currentPosition) {
+ throw new IOException("No ogg page can be found.");
+ }
+ return start;
+ }
+
+ pageHeader.populate(input, /* quiet= */ false);
+ input.resetPeekPosition();
+
+ long granuleDistance = targetGranule - pageHeader.granulePosition;
+ int pageSize = pageHeader.headerSize + pageHeader.bodySize;
+ if (0 <= granuleDistance && granuleDistance < MATCH_RANGE) {
+ return C.POSITION_UNSET;
+ }
+
+ if (granuleDistance < 0) {
+ end = currentPosition;
+ endGranule = pageHeader.granulePosition;
+ } else {
+ start = input.getPosition() + pageSize;
+ startGranule = pageHeader.granulePosition;
+ }
+
+ if (end - start < MATCH_BYTE_RANGE) {
+ end = start;
+ return start;
+ }
+
+ long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L);
+ long nextPosition =
+ input.getPosition()
+ - offset
+ + (granuleDistance * (end - start) / (endGranule - startGranule));
+ return Util.constrainValue(nextPosition, start, end - 1);
+ }
+
+ /**
+ * Skips forward to the start of the page containing the {@code targetGranule}.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @throws ParserException If populating the page header fails.
+ * @throws IOException If reading from the input fails.
+ * @throws InterruptedException If interrupted while reading from the input.
+ */
+ private void skipToPageOfTargetGranule(ExtractorInput input)
+ throws IOException, InterruptedException {
+ pageHeader.populate(input, /* quiet= */ false);
+ while (pageHeader.granulePosition <= targetGranule) {
+ input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
+ start = input.getPosition();
+ startGranule = pageHeader.granulePosition;
+ pageHeader.populate(input, /* quiet= */ false);
+ }
+ input.resetPeekPosition();
+ }
+
+ /**
+ * Skips to the next page.
+ *
+ * @param input The {@code ExtractorInput} to skip to the next page.
+ * @throws IOException If peeking/reading from the input fails.
+ * @throws InterruptedException If the thread is interrupted.
+ * @throws EOFException If the next page can't be found before the end of the input.
+ */
+ @VisibleForTesting
+ void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException {
+ if (!skipToNextPage(input, payloadEndPosition)) {
+ // Not found until eof.
+ throw new EOFException();
+ }
+ }
+
+ /**
+ * Skips to the next page. Searches for the next page header.
+ *
+ * @param input The {@code ExtractorInput} to skip to the next page.
+ * @param limit The limit up to which the search should take place.
+ * @return Whether the next page was found.
+ * @throws IOException If peeking/reading from the input fails.
+ * @throws InterruptedException If interrupted while peeking/reading from the input.
+ */
+ private boolean skipToNextPage(ExtractorInput input, long limit)
+ throws IOException, InterruptedException {
+ limit = Math.min(limit + 3, payloadEndPosition);
+ byte[] buffer = new byte[2048];
+ int peekLength = buffer.length;
+ while (true) {
+ if (input.getPosition() + peekLength > limit) {
+ // Make sure to not peek beyond the end of the input.
+ peekLength = (int) (limit - input.getPosition());
+ if (peekLength < 4) {
+ // Not found until end.
+ return false;
+ }
+ }
+ input.peekFully(buffer, 0, peekLength, false);
+ for (int i = 0; i < peekLength - 3; i++) {
+ if (buffer[i] == 'O'
+ && buffer[i + 1] == 'g'
+ && buffer[i + 2] == 'g'
+ && buffer[i + 3] == 'S') {
+ // Match! Skip to the start of the pattern.
+ input.skipFully(i);
+ return true;
+ }
+ }
+ // Overlap by not skipping the entire peekLength.
+ input.skipFully(peekLength - 3);
+ }
+ }
+
+ /**
+ * Skips to the last Ogg page in the stream and reads the header's granule field which is the
+ * total number of samples per channel.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @return The total number of samples of this input.
+ * @throws IOException If reading from the input fails.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ @VisibleForTesting
+ long readGranuleOfLastPage(ExtractorInput input) throws IOException, InterruptedException {
+ skipToNextPage(input);
+ pageHeader.reset();
+ while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < payloadEndPosition) {
+ pageHeader.populate(input, /* quiet= */ false);
+ input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
+ }
+ return pageHeader.granulePosition;
+ }
+
+ private final class OggSeekMap implements SeekMap {
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ long targetGranule = streamReader.convertTimeToGranule(timeUs);
+ long estimatedPosition =
+ payloadStartPosition
+ + (targetGranule * (payloadEndPosition - payloadStartPosition) / totalGranules)
+ - DEFAULT_OFFSET;
+ estimatedPosition =
+ Util.constrainValue(estimatedPosition, payloadStartPosition, payloadEndPosition - 1);
+ return new SeekPoints(new SeekPoint(timeUs, estimatedPosition));
+ }
+
+ @Override
+ public long getDurationUs() {
+ return streamReader.convertGranuleToTime(totalGranules);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java
new file mode 100644
index 0000000000..449bf35f78
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java
@@ -0,0 +1,143 @@
+/*
+ * 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.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacFrameReader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacMetadataReader;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * {@link StreamReader} to extract Flac data out of Ogg byte stream.
+ */
+/* package */ final class FlacReader extends StreamReader {
+
+ private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF;
+
+ private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
+
+ private FlacStreamMetadata streamMetadata;
+ private FlacOggSeeker flacOggSeeker;
+
+ public static boolean verifyBitstreamType(ParsableByteArray data) {
+ return data.bytesLeft() >= 5 && data.readUnsignedByte() == 0x7F && // packet type
+ data.readUnsignedInt() == 0x464C4143; // ASCII signature "FLAC"
+ }
+
+ @Override
+ protected void reset(boolean headerData) {
+ super.reset(headerData);
+ if (headerData) {
+ streamMetadata = null;
+ flacOggSeeker = null;
+ }
+ }
+
+ private static boolean isAudioPacket(byte[] data) {
+ return data[0] == AUDIO_PACKET_TYPE;
+ }
+
+ @Override
+ protected long preparePayload(ParsableByteArray packet) {
+ if (!isAudioPacket(packet.data)) {
+ return -1;
+ }
+ return getFlacFrameBlockSize(packet);
+ }
+
+ @Override
+ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
+ byte[] data = packet.data;
+ if (streamMetadata == null) {
+ streamMetadata = new FlacStreamMetadata(data, 17);
+ byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
+ setupData.format = streamMetadata.getFormat(metadata, /* id3Metadata= */ null);
+ } else if ((data[0] & 0x7F) == FlacConstants.METADATA_TYPE_SEEK_TABLE) {
+ flacOggSeeker = new FlacOggSeeker();
+ FlacStreamMetadata.SeekTable seekTable =
+ FlacMetadataReader.readSeekTableMetadataBlock(packet);
+ streamMetadata = streamMetadata.copyWithSeekTable(seekTable);
+ } else if (isAudioPacket(data)) {
+ if (flacOggSeeker != null) {
+ flacOggSeeker.setFirstFrameOffset(position);
+ setupData.oggSeeker = flacOggSeeker;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private int getFlacFrameBlockSize(ParsableByteArray packet) {
+ int blockSizeKey = (packet.data[2] & 0xFF) >> 4;
+ if (blockSizeKey == 6 || blockSizeKey == 7) {
+ // Skip the sample number.
+ packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET);
+ packet.readUtf8EncodedLong();
+ }
+ int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(packet, blockSizeKey);
+ packet.setPosition(0);
+ return result;
+ }
+
+ private class FlacOggSeeker implements OggSeeker {
+
+ private long firstFrameOffset;
+ private long pendingSeekGranule;
+
+ public FlacOggSeeker() {
+ firstFrameOffset = -1;
+ pendingSeekGranule = -1;
+ }
+
+ public void setFirstFrameOffset(long firstFrameOffset) {
+ this.firstFrameOffset = firstFrameOffset;
+ }
+
+ @Override
+ public long read(ExtractorInput input) throws IOException, InterruptedException {
+ if (pendingSeekGranule >= 0) {
+ long result = -(pendingSeekGranule + 2);
+ pendingSeekGranule = -1;
+ return result;
+ }
+ return -1;
+ }
+
+ @Override
+ public void startSeek(long targetGranule) {
+ Assertions.checkNotNull(streamMetadata.seekTable);
+ long[] seekPointGranules = streamMetadata.seekTable.pointSampleNumbers;
+ int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true);
+ pendingSeekGranule = seekPointGranules[index];
+ }
+
+ @Override
+ public SeekMap createSeekMap() {
+ Assertions.checkState(firstFrameOffset != -1);
+ return new FlacSeekTableSeekMap(streamMetadata, firstFrameOffset);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
new file mode 100644
index 0000000000..da53a47dc0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
@@ -0,0 +1,114 @@
+/*
+ * 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.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * Extracts data from the Ogg container format.
+ */
+public class OggExtractor implements Extractor {
+
+ /** Factory for {@link OggExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new OggExtractor()};
+
+ private static final int MAX_VERIFICATION_BYTES = 8;
+
+ private ExtractorOutput output;
+ private StreamReader streamReader;
+ private boolean streamReaderInitialized;
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ try {
+ return sniffInternal(input);
+ } catch (ParserException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.output = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ if (streamReader != null) {
+ streamReader.seek(position, timeUs);
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ if (streamReader == null) {
+ if (!sniffInternal(input)) {
+ throw new ParserException("Failed to determine bitstream type");
+ }
+ input.resetPeekPosition();
+ }
+ if (!streamReaderInitialized) {
+ TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
+ output.endTracks();
+ streamReader.init(output, trackOutput);
+ streamReaderInitialized = true;
+ }
+ return streamReader.read(input, seekPosition);
+ }
+
+ private boolean sniffInternal(ExtractorInput input) throws IOException, InterruptedException {
+ OggPageHeader header = new OggPageHeader();
+ if (!header.populate(input, true) || (header.type & 0x02) != 0x02) {
+ return false;
+ }
+
+ int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES);
+ ParsableByteArray scratch = new ParsableByteArray(length);
+ input.peekFully(scratch.data, 0, length);
+
+ if (FlacReader.verifyBitstreamType(resetPosition(scratch))) {
+ streamReader = new FlacReader();
+ } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) {
+ streamReader = new VorbisReader();
+ } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) {
+ streamReader = new OpusReader();
+ } else {
+ return false;
+ }
+ return true;
+ }
+
+ private static ParsableByteArray resetPosition(ParsableByteArray scratch) {
+ scratch.setPosition(0);
+ return scratch;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java
new file mode 100644
index 0000000000..1f3bf38c73
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java
@@ -0,0 +1,155 @@
+/*
+ * 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.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * OGG packet class.
+ */
+/* package */ final class OggPacket {
+
+ private final OggPageHeader pageHeader = new OggPageHeader();
+ private final ParsableByteArray packetArray = new ParsableByteArray(
+ new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0);
+
+ private int currentSegmentIndex = C.INDEX_UNSET;
+ private int segmentCount;
+ private boolean populated;
+
+ /**
+ * Resets this reader.
+ */
+ public void reset() {
+ pageHeader.reset();
+ packetArray.reset();
+ currentSegmentIndex = C.INDEX_UNSET;
+ populated = false;
+ }
+
+ /**
+ * Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make
+ * sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader
+ * can resume properly from an error while reading a continued packet spanned across multiple
+ * pages.
+ *
+ * @param input The {@link ExtractorInput} to read data from.
+ * @return {@code true} if the read was successful. The read fails if the end of the input is
+ * encountered without reading data.
+ * @throws IOException If reading from the input fails.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public boolean populate(ExtractorInput input) throws IOException, InterruptedException {
+ Assertions.checkState(input != null);
+
+ if (populated) {
+ populated = false;
+ packetArray.reset();
+ }
+
+ while (!populated) {
+ if (currentSegmentIndex < 0) {
+ // We're at the start of a page.
+ if (!pageHeader.populate(input, true)) {
+ return false;
+ }
+ int segmentIndex = 0;
+ int bytesToSkip = pageHeader.headerSize;
+ if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) {
+ // After seeking, the first packet may be the remainder
+ // part of a continued packet which has to be discarded.
+ bytesToSkip += calculatePacketSize(segmentIndex);
+ segmentIndex += segmentCount;
+ }
+ input.skipFully(bytesToSkip);
+ currentSegmentIndex = segmentIndex;
+ }
+
+ int size = calculatePacketSize(currentSegmentIndex);
+ int segmentIndex = currentSegmentIndex + segmentCount;
+ if (size > 0) {
+ if (packetArray.capacity() < packetArray.limit() + size) {
+ packetArray.data = Arrays.copyOf(packetArray.data, packetArray.limit() + size);
+ }
+ input.readFully(packetArray.data, packetArray.limit(), size);
+ packetArray.setLimit(packetArray.limit() + size);
+ populated = pageHeader.laces[segmentIndex - 1] != 255;
+ }
+ // Advance now since we are sure reading didn't throw an exception.
+ currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? C.INDEX_UNSET
+ : segmentIndex;
+ }
+ return true;
+ }
+
+ /**
+ * An OGG Packet may span multiple pages. Returns the {@link OggPageHeader} of the last page read,
+ * or an empty header if the packet has yet to be populated.
+ *
+ * <p>Note that the returned {@link OggPageHeader} is mutable and may be updated during subsequent
+ * calls to {@link #populate(ExtractorInput)}.
+ *
+ * @return the {@code PageHeader} of the last page read or an empty header if the packet has yet
+ * to be populated.
+ */
+ public OggPageHeader getPageHeader() {
+ return pageHeader;
+ }
+
+ /**
+ * Returns a {@link ParsableByteArray} containing the packet's payload.
+ */
+ public ParsableByteArray getPayload() {
+ return packetArray;
+ }
+
+ /**
+ * Trims the packet data array.
+ */
+ public void trimPayload() {
+ if (packetArray.data.length == OggPageHeader.MAX_PAGE_PAYLOAD) {
+ return;
+ }
+ packetArray.data = Arrays.copyOf(packetArray.data, Math.max(OggPageHeader.MAX_PAGE_PAYLOAD,
+ packetArray.limit()));
+ }
+
+ /**
+ * Calculates the size of the packet starting from {@code startSegmentIndex}.
+ *
+ * @param startSegmentIndex the index of the first segment of the packet.
+ * @return Size of the packet.
+ */
+ private int calculatePacketSize(int startSegmentIndex) {
+ segmentCount = 0;
+ int size = 0;
+ while (startSegmentIndex + segmentCount < pageHeader.pageSegmentCount) {
+ int segmentLength = pageHeader.laces[startSegmentIndex + segmentCount++];
+ size += segmentLength;
+ if (segmentLength != 255) {
+ // packets end at first lace < 255
+ break;
+ }
+ }
+ return size;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java
new file mode 100644
index 0000000000..afdccf80fd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java
@@ -0,0 +1,135 @@
+/*
+ * 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.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Data object to store header information.
+ */
+/* package */ final class OggPageHeader {
+
+ public static final int EMPTY_PAGE_HEADER_SIZE = 27;
+ public static final int MAX_SEGMENT_COUNT = 255;
+ public static final int MAX_PAGE_PAYLOAD = 255 * 255;
+ public static final int MAX_PAGE_SIZE = EMPTY_PAGE_HEADER_SIZE + MAX_SEGMENT_COUNT
+ + MAX_PAGE_PAYLOAD;
+
+ private static final int TYPE_OGGS = 0x4f676753;
+
+ public int revision;
+ public int type;
+ /**
+ * The absolute granule position of the page. This is the total number of samples from the start
+ * of the file up to the <em>end</em> of the page. Samples partially in the page that continue on
+ * the next page do not count.
+ */
+ public long granulePosition;
+
+ public long streamSerialNumber;
+ public long pageSequenceNumber;
+ public long pageChecksum;
+ public int pageSegmentCount;
+ public int headerSize;
+ public int bodySize;
+ /**
+ * Be aware that {@code laces.length} is always {@link #MAX_SEGMENT_COUNT}. Instead use
+ * {@link #pageSegmentCount} to iterate.
+ */
+ public final int[] laces = new int[MAX_SEGMENT_COUNT];
+
+ private final ParsableByteArray scratch = new ParsableByteArray(MAX_SEGMENT_COUNT);
+
+ /**
+ * Resets all primitive member fields to zero.
+ */
+ public void reset() {
+ revision = 0;
+ type = 0;
+ granulePosition = 0;
+ streamSerialNumber = 0;
+ pageSequenceNumber = 0;
+ pageChecksum = 0;
+ pageSegmentCount = 0;
+ headerSize = 0;
+ bodySize = 0;
+ }
+
+ /**
+ * Peeks an Ogg page header and updates this {@link OggPageHeader}.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @param quiet Whether to return {@code false} rather than throwing an exception if the header
+ * cannot be populated.
+ * @return Whether the read was successful. The read fails if the end of the input is encountered
+ * without reading data.
+ * @throws IOException If reading data fails or the stream is invalid.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ public boolean populate(ExtractorInput input, boolean quiet)
+ throws IOException, InterruptedException {
+ scratch.reset();
+ reset();
+ boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNSET
+ || input.getLength() - input.getPeekPosition() >= EMPTY_PAGE_HEADER_SIZE;
+ if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, EMPTY_PAGE_HEADER_SIZE, true)) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new EOFException();
+ }
+ }
+ if (scratch.readUnsignedInt() != TYPE_OGGS) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("expected OggS capture pattern at begin of page");
+ }
+ }
+
+ revision = scratch.readUnsignedByte();
+ if (revision != 0x00) {
+ if (quiet) {
+ return false;
+ } else {
+ throw new ParserException("unsupported bit stream revision");
+ }
+ }
+ type = scratch.readUnsignedByte();
+
+ granulePosition = scratch.readLittleEndianLong();
+ streamSerialNumber = scratch.readLittleEndianUnsignedInt();
+ pageSequenceNumber = scratch.readLittleEndianUnsignedInt();
+ pageChecksum = scratch.readLittleEndianUnsignedInt();
+ pageSegmentCount = scratch.readUnsignedByte();
+ headerSize = EMPTY_PAGE_HEADER_SIZE + pageSegmentCount;
+
+ // calculate total size of header including laces
+ scratch.reset();
+ input.peekFully(scratch.data, 0, pageSegmentCount);
+ for (int i = 0; i < pageSegmentCount; i++) {
+ laces[i] = scratch.readUnsignedByte();
+ bodySize += laces[i];
+ }
+
+ return true;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java
new file mode 100644
index 0000000000..0a0be963f7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java
@@ -0,0 +1,57 @@
+/*
+ * 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.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import java.io.IOException;
+
+/**
+ * Used to seek in an Ogg stream. OggSeeker implementation may do direct seeking or progressive
+ * seeking. OggSeeker works together with a {@link SeekMap} instance to capture the queried position
+ * and start the seeking with an initial estimated position.
+ */
+/* package */ interface OggSeeker {
+
+ /**
+ * Returns a {@link SeekMap} that returns an initial estimated position for progressive seeking
+ * or the final position for direct seeking. Returns null if {@link #read} has yet to return -1.
+ */
+ SeekMap createSeekMap();
+
+ /**
+ * Starts a seek operation.
+ *
+ * @param targetGranule The target granule position.
+ */
+ void startSeek(long targetGranule);
+
+ /**
+ * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a seek.
+ * <p/>
+ * If more data is required or if the position of the input needs to be modified then a position
+ * from which data should be provided is returned. Else a negative value is returned. If a seek
+ * has been completed then the value returned is -(currentGranule + 2). Else it is -1.
+ *
+ * @param input The {@link ExtractorInput} to read from.
+ * @return A non-negative position to seek the {@link ExtractorInput} to, or -(currentGranule + 2)
+ * if the progressive seek has completed, or -1 otherwise.
+ * @throws IOException If reading from the {@link ExtractorInput} fails.
+ * @throws InterruptedException If the thread is interrupted.
+ */
+ long read(ExtractorInput input) throws IOException, InterruptedException;
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java
new file mode 100644
index 0000000000..c3f3a13d54
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java
@@ -0,0 +1,132 @@
+/*
+ * 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.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * {@link StreamReader} to extract Opus data out of Ogg byte stream.
+ */
+/* package */ final class OpusReader extends StreamReader {
+
+ private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840;
+
+ /**
+ * Opus streams are always decoded at 48000 Hz.
+ */
+ private static final int SAMPLE_RATE = 48000;
+
+ private static final int OPUS_CODE = 0x4f707573;
+ private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
+
+ private boolean headerRead;
+
+ public static boolean verifyBitstreamType(ParsableByteArray data) {
+ if (data.bytesLeft() < OPUS_SIGNATURE.length) {
+ return false;
+ }
+ byte[] header = new byte[OPUS_SIGNATURE.length];
+ data.readBytes(header, 0, OPUS_SIGNATURE.length);
+ return Arrays.equals(header, OPUS_SIGNATURE);
+ }
+
+ @Override
+ protected void reset(boolean headerData) {
+ super.reset(headerData);
+ if (headerData) {
+ headerRead = false;
+ }
+ }
+
+ @Override
+ protected long preparePayload(ParsableByteArray packet) {
+ return convertTimeToGranule(getPacketDurationUs(packet.data));
+ }
+
+ @Override
+ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
+ if (!headerRead) {
+ byte[] metadata = Arrays.copyOf(packet.data, packet.limit());
+ int channelCount = metadata[9] & 0xFF;
+ int preskip = ((metadata[11] & 0xFF) << 8) | (metadata[10] & 0xFF);
+
+ List<byte[]> initializationData = new ArrayList<>(3);
+ initializationData.add(metadata);
+ putNativeOrderLong(initializationData, preskip);
+ putNativeOrderLong(initializationData, DEFAULT_SEEK_PRE_ROLL_SAMPLES);
+
+ setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_OPUS, null,
+ Format.NO_VALUE, Format.NO_VALUE, channelCount, SAMPLE_RATE, initializationData, null, 0,
+ null);
+ headerRead = true;
+ } else {
+ boolean headerPacket = packet.readInt() == OPUS_CODE;
+ packet.setPosition(0);
+ return headerPacket;
+ }
+ return true;
+ }
+
+ private void putNativeOrderLong(List<byte[]> initializationData, int samples) {
+ long ns = (samples * C.NANOS_PER_SECOND) / SAMPLE_RATE;
+ byte[] array = ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(ns).array();
+ initializationData.add(array);
+ }
+
+ /**
+ * Returns the duration of the given audio packet.
+ *
+ * @param packet Contains audio data.
+ * @return Returns the duration of the given audio packet.
+ */
+ private long getPacketDurationUs(byte[] packet) {
+ int toc = packet[0] & 0xFF;
+ int frames;
+ switch (toc & 0x3) {
+ case 0:
+ frames = 1;
+ break;
+ case 1:
+ case 2:
+ frames = 2;
+ break;
+ default:
+ frames = packet[1] & 0x3F;
+ break;
+ }
+
+ int config = toc >> 3;
+ int length = config & 0x3;
+ if (config >= 16) {
+ length = 2500 << length;
+ } else if (config >= 12) {
+ length = 10000 << (length & 0x1);
+ } else if (length == 3) {
+ length = 60000;
+ } else {
+ length = 10000 << length;
+ }
+ return (long) frames * length;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
new file mode 100644
index 0000000000..067c8aef03
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
@@ -0,0 +1,268 @@
+/*
+ * 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.ogg;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.Extractor;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorInput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.PositionHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/** StreamReader abstract class. */
+@SuppressWarnings("UngroupedOverloads")
+/* package */ abstract class StreamReader {
+
+ private static final int STATE_READ_HEADERS = 0;
+ private static final int STATE_SKIP_HEADERS = 1;
+ private static final int STATE_READ_PAYLOAD = 2;
+ private static final int STATE_END_OF_INPUT = 3;
+
+ static class SetupData {
+ Format format;
+ OggSeeker oggSeeker;
+ }
+
+ private final OggPacket oggPacket;
+
+ private TrackOutput trackOutput;
+ private ExtractorOutput extractorOutput;
+ private OggSeeker oggSeeker;
+ private long targetGranule;
+ private long payloadStartPosition;
+ private long currentGranule;
+ private int state;
+ private int sampleRate;
+ private SetupData setupData;
+ private long lengthOfReadPacket;
+ private boolean seekMapSet;
+ private boolean formatSet;
+
+ public StreamReader() {
+ oggPacket = new OggPacket();
+ }
+
+ void init(ExtractorOutput output, TrackOutput trackOutput) {
+ this.extractorOutput = output;
+ this.trackOutput = trackOutput;
+ reset(true);
+ }
+
+ /**
+ * Resets the state of the {@link StreamReader}.
+ *
+ * @param headerData Resets parsed header data too.
+ */
+ protected void reset(boolean headerData) {
+ if (headerData) {
+ setupData = new SetupData();
+ payloadStartPosition = 0;
+ state = STATE_READ_HEADERS;
+ } else {
+ state = STATE_SKIP_HEADERS;
+ }
+ targetGranule = -1;
+ currentGranule = 0;
+ }
+
+ /**
+ * @see Extractor#seek(long, long)
+ */
+ final void seek(long position, long timeUs) {
+ oggPacket.reset();
+ if (position == 0) {
+ reset(!seekMapSet);
+ } else {
+ if (state != STATE_READ_HEADERS) {
+ targetGranule = convertTimeToGranule(timeUs);
+ oggSeeker.startSeek(targetGranule);
+ state = STATE_READ_PAYLOAD;
+ }
+ }
+ }
+
+ /**
+ * @see Extractor#read(ExtractorInput, PositionHolder)
+ */
+ final int read(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ switch (state) {
+ case STATE_READ_HEADERS:
+ return readHeaders(input);
+ case STATE_SKIP_HEADERS:
+ input.skipFully((int) payloadStartPosition);
+ state = STATE_READ_PAYLOAD;
+ return Extractor.RESULT_CONTINUE;
+ case STATE_READ_PAYLOAD:
+ return readPayload(input, seekPosition);
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ }
+
+ private int readHeaders(ExtractorInput input) throws IOException, InterruptedException {
+ boolean readingHeaders = true;
+ while (readingHeaders) {
+ if (!oggPacket.populate(input)) {
+ state = STATE_END_OF_INPUT;
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ lengthOfReadPacket = input.getPosition() - payloadStartPosition;
+
+ readingHeaders = readHeaders(oggPacket.getPayload(), payloadStartPosition, setupData);
+ if (readingHeaders) {
+ payloadStartPosition = input.getPosition();
+ }
+ }
+
+ sampleRate = setupData.format.sampleRate;
+ if (!formatSet) {
+ trackOutput.format(setupData.format);
+ formatSet = true;
+ }
+
+ if (setupData.oggSeeker != null) {
+ oggSeeker = setupData.oggSeeker;
+ } else if (input.getLength() == C.LENGTH_UNSET) {
+ oggSeeker = new UnseekableOggSeeker();
+ } else {
+ OggPageHeader firstPayloadPageHeader = oggPacket.getPageHeader();
+ boolean isLastPage = (firstPayloadPageHeader.type & 0x04) != 0; // Type 4 is end of stream.
+ oggSeeker =
+ new DefaultOggSeeker(
+ this,
+ payloadStartPosition,
+ input.getLength(),
+ firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize,
+ firstPayloadPageHeader.granulePosition,
+ isLastPage);
+ }
+
+ setupData = null;
+ state = STATE_READ_PAYLOAD;
+ // First payload packet. Trim the payload array of the ogg packet after headers have been read.
+ oggPacket.trimPayload();
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ private int readPayload(ExtractorInput input, PositionHolder seekPosition)
+ throws IOException, InterruptedException {
+ long position = oggSeeker.read(input);
+ if (position >= 0) {
+ seekPosition.position = position;
+ return Extractor.RESULT_SEEK;
+ } else if (position < -1) {
+ onSeekEnd(-(position + 2));
+ }
+ if (!seekMapSet) {
+ SeekMap seekMap = oggSeeker.createSeekMap();
+ extractorOutput.seekMap(seekMap);
+ seekMapSet = true;
+ }
+
+ if (lengthOfReadPacket > 0 || oggPacket.populate(input)) {
+ lengthOfReadPacket = 0;
+ ParsableByteArray payload = oggPacket.getPayload();
+ long granulesInPacket = preparePayload(payload);
+ if (granulesInPacket >= 0 && currentGranule + granulesInPacket >= targetGranule) {
+ // calculate time and send payload data to codec
+ long timeUs = convertGranuleToTime(currentGranule);
+ trackOutput.sampleData(payload, payload.limit());
+ trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, payload.limit(), 0, null);
+ targetGranule = -1;
+ }
+ currentGranule += granulesInPacket;
+ } else {
+ state = STATE_END_OF_INPUT;
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ return Extractor.RESULT_CONTINUE;
+ }
+
+ /**
+ * Converts granule value to time.
+ *
+ * @param granule The granule value.
+ * @return Time in milliseconds.
+ */
+ protected long convertGranuleToTime(long granule) {
+ return (granule * C.MICROS_PER_SECOND) / sampleRate;
+ }
+
+ /**
+ * Converts time value to granule.
+ *
+ * @param timeUs Time in milliseconds.
+ * @return The granule value.
+ */
+ protected long convertTimeToGranule(long timeUs) {
+ return (sampleRate * timeUs) / C.MICROS_PER_SECOND;
+ }
+
+ /**
+ * Prepares payload data in the packet for submitting to TrackOutput and returns number of
+ * granules in the packet.
+ *
+ * @param packet Ogg payload data packet.
+ * @return Number of granules in the packet or -1 if the packet doesn't contain payload data.
+ */
+ protected abstract long preparePayload(ParsableByteArray packet);
+
+ /**
+ * Checks if the given packet is a header packet and reads it.
+ *
+ * @param packet An ogg packet.
+ * @param position Position of the given header packet.
+ * @param setupData Setup data to be filled.
+ * @return Whether the packet contains header data.
+ */
+ protected abstract boolean readHeaders(ParsableByteArray packet, long position,
+ SetupData setupData) throws IOException, InterruptedException;
+
+ /**
+ * Called on end of seeking.
+ *
+ * @param currentGranule The granule at the current input position.
+ */
+ protected void onSeekEnd(long currentGranule) {
+ this.currentGranule = currentGranule;
+ }
+
+ private static final class UnseekableOggSeeker implements OggSeeker {
+
+ @Override
+ public long read(ExtractorInput input) {
+ return -1;
+ }
+
+ @Override
+ public void startSeek(long targetGranule) {
+ // Do nothing.
+ }
+
+ @Override
+ public SeekMap createSeekMap() {
+ return new SeekMap.Unseekable(C.TIME_UNSET);
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java
new file mode 100644
index 0000000000..cb0678a285
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java
@@ -0,0 +1,198 @@
+/*
+ * 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.ogg;
+
+import androidx.annotation.VisibleForTesting;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil.Mode;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * {@link StreamReader} to extract Vorbis data out of Ogg byte stream.
+ */
+/* package */ final class VorbisReader extends StreamReader {
+
+ private VorbisSetup vorbisSetup;
+ private int previousPacketBlockSize;
+ private boolean seenFirstAudioPacket;
+
+ private VorbisUtil.VorbisIdHeader vorbisIdHeader;
+ private VorbisUtil.CommentHeader commentHeader;
+
+ public static boolean verifyBitstreamType(ParsableByteArray data) {
+ try {
+ return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true);
+ } catch (ParserException e) {
+ return false;
+ }
+ }
+
+ @Override
+ protected void reset(boolean headerData) {
+ super.reset(headerData);
+ if (headerData) {
+ vorbisSetup = null;
+ vorbisIdHeader = null;
+ commentHeader = null;
+ }
+ previousPacketBlockSize = 0;
+ seenFirstAudioPacket = false;
+ }
+
+ @Override
+ protected void onSeekEnd(long currentGranule) {
+ super.onSeekEnd(currentGranule);
+ seenFirstAudioPacket = currentGranule != 0;
+ previousPacketBlockSize = vorbisIdHeader != null ? vorbisIdHeader.blockSize0 : 0;
+ }
+
+ @Override
+ protected long preparePayload(ParsableByteArray packet) {
+ // if this is not an audio packet...
+ if ((packet.data[0] & 0x01) == 1) {
+ return -1;
+ }
+
+ // ... we need to decode the block size
+ int packetBlockSize = decodeBlockSize(packet.data[0], vorbisSetup);
+ // a packet contains samples produced from overlapping the previous and current frame data
+ // (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2)
+ int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4
+ : 0;
+ // codec expects the number of samples appended to audio data
+ appendNumberOfSamples(packet, samplesInPacket);
+
+ // update state in members for next iteration
+ seenFirstAudioPacket = true;
+ previousPacketBlockSize = packetBlockSize;
+ return samplesInPacket;
+ }
+
+ @Override
+ protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
+ throws IOException, InterruptedException {
+ if (vorbisSetup != null) {
+ return false;
+ }
+
+ vorbisSetup = readSetupHeaders(packet);
+ if (vorbisSetup == null) {
+ return true;
+ }
+
+ ArrayList<byte[]> codecInitialisationData = new ArrayList<>();
+ codecInitialisationData.add(vorbisSetup.idHeader.data);
+ codecInitialisationData.add(vorbisSetup.setupHeaderData);
+
+ setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null,
+ this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE,
+ this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate,
+ codecInitialisationData, null, 0, null);
+ return true;
+ }
+
+ @VisibleForTesting
+ /* package */ VorbisSetup readSetupHeaders(ParsableByteArray scratch) throws IOException {
+
+ if (vorbisIdHeader == null) {
+ vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch);
+ return null;
+ }
+
+ if (commentHeader == null) {
+ commentHeader = VorbisUtil.readVorbisCommentHeader(scratch);
+ return null;
+ }
+
+ // the third packet contains the setup header
+ byte[] setupHeaderData = new byte[scratch.limit()];
+ // raw data of vorbis setup header has to be passed to decoder as CSD buffer #2
+ System.arraycopy(scratch.data, 0, setupHeaderData, 0, scratch.limit());
+ // partially decode setup header to get the modes
+ Mode[] modes = VorbisUtil.readVorbisModes(scratch, vorbisIdHeader.channels);
+ // we need the ilog of modes all the time when extracting, so we compute it once
+ int iLogModes = VorbisUtil.iLog(modes.length - 1);
+
+ return new VorbisSetup(vorbisIdHeader, commentHeader, setupHeaderData, modes, iLogModes);
+ }
+
+ /**
+ * Reads an int of {@code length} bits from {@code src} starting at {@code
+ * leastSignificantBitIndex}.
+ *
+ * @param src the {@code byte} to read from.
+ * @param length the length in bits of the int to read.
+ * @param leastSignificantBitIndex the index of the least significant bit of the int to read.
+ * @return the int value read.
+ */
+ @VisibleForTesting
+ /* package */ static int readBits(byte src, int length, int leastSignificantBitIndex) {
+ return (src >> leastSignificantBitIndex) & (255 >>> (8 - length));
+ }
+
+ @VisibleForTesting
+ /* package */ static void appendNumberOfSamples(
+ ParsableByteArray buffer, long packetSampleCount) {
+
+ buffer.setLimit(buffer.limit() + 4);
+ // The vorbis decoder expects the number of samples in the packet
+ // to be appended to the audio data as an int32
+ buffer.data[buffer.limit() - 4] = (byte) (packetSampleCount & 0xFF);
+ buffer.data[buffer.limit() - 3] = (byte) ((packetSampleCount >>> 8) & 0xFF);
+ buffer.data[buffer.limit() - 2] = (byte) ((packetSampleCount >>> 16) & 0xFF);
+ buffer.data[buffer.limit() - 1] = (byte) ((packetSampleCount >>> 24) & 0xFF);
+ }
+
+ private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) {
+ // read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1)
+ int modeNumber = readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1);
+ int currentBlockSize;
+ if (!vorbisSetup.modes[modeNumber].blockFlag) {
+ currentBlockSize = vorbisSetup.idHeader.blockSize0;
+ } else {
+ currentBlockSize = vorbisSetup.idHeader.blockSize1;
+ }
+ return currentBlockSize;
+ }
+
+ /**
+ * Class to hold all data read from Vorbis setup headers.
+ */
+ /* package */ static final class VorbisSetup {
+
+ public final VorbisUtil.VorbisIdHeader idHeader;
+ public final VorbisUtil.CommentHeader commentHeader;
+ public final byte[] setupHeaderData;
+ public final Mode[] modes;
+ public final int iLogModes;
+
+ public VorbisSetup(VorbisUtil.VorbisIdHeader idHeader, VorbisUtil.CommentHeader
+ commentHeader, byte[] setupHeaderData, Mode[] modes, int iLogModes) {
+ this.idHeader = idHeader;
+ this.commentHeader = commentHeader;
+ this.setupHeaderData = setupHeaderData;
+ this.modes = modes;
+ this.iLogModes = iLogModes;
+ }
+
+ }
+
+}