blob: ffb85d07f0580719e7c3784a0ee1c84463cc305b [file] [log] [blame]
/*
* 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 com.google.android.exoplayer2.extractor.flv;
import static java.lang.Math.max;
import androidx.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.IndexSeekMap;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions;
import 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;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** 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 @MonotonicNonNull 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 @MonotonicNonNull AudioTagPayloadReader audioReader;
private @MonotonicNonNull 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 {
// Check if file starts with "FLV" tag
input.peekFully(scratch.getData(), 0, 3);
scratch.setPosition(0);
if (scratch.readUnsignedInt24() != FLV_TAG) {
return false;
}
// Checking reserved flags are set to 0
input.peekFully(scratch.getData(), 0, 2);
scratch.setPosition(0);
if ((scratch.readUnsignedShort() & 0xFA) != 0) {
return false;
}
// Read data offset
input.peekFully(scratch.getData(), 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.getData(), 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) {
if (position == 0) {
state = STATE_READING_FLV_HEADER;
outputFirstSample = false;
} else {
state = STATE_READING_TAG_HEADER;
}
bytesToNextTagHeader = 0;
}
@Override
public void release() {
// Do nothing
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException {
Assertions.checkStateNotNull(extractorOutput); // Asserts that init has been called.
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.
*/
@RequiresNonNull("extractorOutput")
private boolean readFlvHeader(ExtractorInput input) throws IOException {
if (!input.readFully(headerBuffer.getData(), 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.
*/
private void skipToTagHeader(ExtractorInput input) throws IOException {
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.
*/
private boolean readTagHeader(ExtractorInput input) throws IOException {
if (!input.readFully(tagHeaderBuffer.getData(), 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.
*/
@RequiresNonNull("extractorOutput")
private boolean readTagData(ExtractorInput input) throws IOException {
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 IndexSeekMap(
metadataReader.getKeyFrameTagPositions(),
metadataReader.getKeyFrameTimesUs(),
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 {
if (tagDataSize > tagData.capacity()) {
tagData.reset(new byte[max(tagData.capacity() * 2, tagDataSize)], 0);
} else {
tagData.setPosition(0);
}
tagData.setLimit(tagDataSize);
input.readFully(tagData.getData(), 0, tagDataSize);
return tagData;
}
@RequiresNonNull("extractorOutput")
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);
}
}