summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java308
1 files changed, 308 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
new file mode 100644
index 0000000000..a7438b190f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
@@ -0,0 +1,308 @@
+/*
+ * 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.flv;
+
+import androidx.annotation.IntDef;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+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.SeekMap;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Extracts data from the FLV container format.
+ */
+public final class FlvExtractor implements Extractor {
+
+ /** Factory for {@link FlvExtractor} instances. */
+ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlvExtractor()};
+
+ /** Extractor states. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATE_READING_FLV_HEADER,
+ STATE_SKIPPING_TO_TAG_HEADER,
+ STATE_READING_TAG_HEADER,
+ STATE_READING_TAG_DATA
+ })
+ private @interface States {}
+
+ private static final int STATE_READING_FLV_HEADER = 1;
+ private static final int STATE_SKIPPING_TO_TAG_HEADER = 2;
+ private static final int STATE_READING_TAG_HEADER = 3;
+ private static final int STATE_READING_TAG_DATA = 4;
+
+ // Header sizes.
+ private static final int FLV_HEADER_SIZE = 9;
+ private static final int FLV_TAG_HEADER_SIZE = 11;
+
+ // Tag types.
+ private static final int TAG_TYPE_AUDIO = 8;
+ private static final int TAG_TYPE_VIDEO = 9;
+ private static final int TAG_TYPE_SCRIPT_DATA = 18;
+
+ // FLV container identifier.
+ private static final int FLV_TAG = 0x00464c56;
+
+ private final ParsableByteArray scratch;
+ private final ParsableByteArray headerBuffer;
+ private final ParsableByteArray tagHeaderBuffer;
+ private final ParsableByteArray tagData;
+ private final ScriptTagPayloadReader metadataReader;
+
+ private ExtractorOutput extractorOutput;
+ private @States int state;
+ private boolean outputFirstSample;
+ private long mediaTagTimestampOffsetUs;
+ private int bytesToNextTagHeader;
+ private int tagType;
+ private int tagDataSize;
+ private long tagTimestampUs;
+ private boolean outputSeekMap;
+ private AudioTagPayloadReader audioReader;
+ private VideoTagPayloadReader videoReader;
+
+ public FlvExtractor() {
+ scratch = new ParsableByteArray(4);
+ headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE);
+ tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);
+ tagData = new ParsableByteArray();
+ metadataReader = new ScriptTagPayloadReader();
+ state = STATE_READING_FLV_HEADER;
+ }
+
+ @Override
+ public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+ // Check if file starts with "FLV" tag
+ input.peekFully(scratch.data, 0, 3);
+ scratch.setPosition(0);
+ if (scratch.readUnsignedInt24() != FLV_TAG) {
+ return false;
+ }
+
+ // Checking reserved flags are set to 0
+ input.peekFully(scratch.data, 0, 2);
+ scratch.setPosition(0);
+ if ((scratch.readUnsignedShort() & 0xFA) != 0) {
+ return false;
+ }
+
+ // Read data offset
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+ int dataOffset = scratch.readInt();
+
+ input.resetPeekPosition();
+ input.advancePeekPosition(dataOffset);
+
+ // Checking first "previous tag size" is set to 0
+ input.peekFully(scratch.data, 0, 4);
+ scratch.setPosition(0);
+
+ return scratch.readInt() == 0;
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.extractorOutput = output;
+ }
+
+ @Override
+ public void seek(long position, long timeUs) {
+ state = STATE_READING_FLV_HEADER;
+ outputFirstSample = false;
+ bytesToNextTagHeader = 0;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
+ InterruptedException {
+ while (true) {
+ switch (state) {
+ case STATE_READING_FLV_HEADER:
+ if (!readFlvHeader(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_SKIPPING_TO_TAG_HEADER:
+ skipToTagHeader(input);
+ break;
+ case STATE_READING_TAG_HEADER:
+ if (!readTagHeader(input)) {
+ return RESULT_END_OF_INPUT;
+ }
+ break;
+ case STATE_READING_TAG_DATA:
+ if (readTagData(input)) {
+ return RESULT_CONTINUE;
+ }
+ break;
+ default:
+ // Never happens.
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ /**
+ * Reads an FLV container header from the provided {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return True if header was read successfully. False if the end of stream was reached.
+ * @throws IOException If an error occurred reading or parsing data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private boolean readFlvHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (!input.readFully(headerBuffer.data, 0, FLV_HEADER_SIZE, true)) {
+ // We've reached the end of the stream.
+ return false;
+ }
+
+ headerBuffer.setPosition(0);
+ headerBuffer.skipBytes(4);
+ int flags = headerBuffer.readUnsignedByte();
+ boolean hasAudio = (flags & 0x04) != 0;
+ boolean hasVideo = (flags & 0x01) != 0;
+ if (hasAudio && audioReader == null) {
+ audioReader = new AudioTagPayloadReader(
+ extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO));
+ }
+ if (hasVideo && videoReader == null) {
+ videoReader = new VideoTagPayloadReader(
+ extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO));
+ }
+ extractorOutput.endTracks();
+
+ // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size.
+ bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4;
+ state = STATE_SKIPPING_TO_TAG_HEADER;
+ return true;
+ }
+
+ /**
+ * Skips over data to reach the next tag header.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @throws IOException If an error occurred skipping data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException {
+ input.skipFully(bytesToNextTagHeader);
+ bytesToNextTagHeader = 0;
+ state = STATE_READING_TAG_HEADER;
+ }
+
+ /**
+ * Reads a tag header from the provided {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return True if tag header was read successfully. Otherwise, false.
+ * @throws IOException If an error occurred reading or parsing data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException {
+ if (!input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE, true)) {
+ // We've reached the end of the stream.
+ return false;
+ }
+
+ tagHeaderBuffer.setPosition(0);
+ tagType = tagHeaderBuffer.readUnsignedByte();
+ tagDataSize = tagHeaderBuffer.readUnsignedInt24();
+ tagTimestampUs = tagHeaderBuffer.readUnsignedInt24();
+ tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L;
+ tagHeaderBuffer.skipBytes(3); // streamId
+ state = STATE_READING_TAG_DATA;
+ return true;
+ }
+
+ /**
+ * Reads the body of a tag from the provided {@link ExtractorInput}.
+ *
+ * @param input The {@link ExtractorInput} from which to read.
+ * @return True if the data was consumed by a reader. False if it was skipped.
+ * @throws IOException If an error occurred reading or parsing data from the source.
+ * @throws InterruptedException If the thread was interrupted.
+ */
+ private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException {
+ boolean wasConsumed = true;
+ boolean wasSampleOutput = false;
+ long timestampUs = getCurrentTimestampUs();
+ if (tagType == TAG_TYPE_AUDIO && audioReader != null) {
+ ensureReadyForMediaOutput();
+ wasSampleOutput = audioReader.consume(prepareTagData(input), timestampUs);
+ } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {
+ ensureReadyForMediaOutput();
+ wasSampleOutput = videoReader.consume(prepareTagData(input), timestampUs);
+ } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) {
+ wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs);
+ long durationUs = metadataReader.getDurationUs();
+ if (durationUs != C.TIME_UNSET) {
+ extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
+ outputSeekMap = true;
+ }
+ } else {
+ input.skipFully(tagDataSize);
+ wasConsumed = false;
+ }
+ if (!outputFirstSample && wasSampleOutput) {
+ outputFirstSample = true;
+ mediaTagTimestampOffsetUs =
+ metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0;
+ }
+ bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header.
+ state = STATE_SKIPPING_TO_TAG_HEADER;
+ return wasConsumed;
+ }
+
+ private ParsableByteArray prepareTagData(ExtractorInput input) throws IOException,
+ InterruptedException {
+ if (tagDataSize > tagData.capacity()) {
+ tagData.reset(new byte[Math.max(tagData.capacity() * 2, tagDataSize)], 0);
+ } else {
+ tagData.setPosition(0);
+ }
+ tagData.setLimit(tagDataSize);
+ input.readFully(tagData.data, 0, tagDataSize);
+ return tagData;
+ }
+
+ private void ensureReadyForMediaOutput() {
+ if (!outputSeekMap) {
+ extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+ outputSeekMap = true;
+ }
+ }
+
+ private long getCurrentTimestampUs() {
+ return outputFirstSample
+ ? (mediaTagTimestampOffsetUs + tagTimestampUs)
+ : (metadataReader.getDurationUs() == C.TIME_UNSET ? 0 : tagTimestampUs);
+ }
+}