mirror of
https://github.com/samsonjs/media.git
synced 2026-04-03 10:55:48 +00:00
Further cleanup to FLV extractor
This commit is contained in:
parent
f91ea9039d
commit
4422e8a015
6 changed files with 290 additions and 338 deletions
|
|
@ -148,7 +148,6 @@ import java.util.Locale;
|
|||
"http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", PlayerActivity.TYPE_OTHER),
|
||||
new Sample("Big Buck Bunny (FLV Video)",
|
||||
"http://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0", PlayerActivity.TYPE_OTHER),
|
||||
|
||||
};
|
||||
|
||||
private Samples() {}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import android.util.Pair;
|
|||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Parses audio tags of from an FLV stream and extracts AAC frames.
|
||||
* Parses audio tags from an FLV stream and extracts AAC frames.
|
||||
*/
|
||||
/* package */ final class AudioTagPayloadReader extends TagPayloadReader {
|
||||
|
||||
|
|
@ -59,29 +59,22 @@ import java.util.Collections;
|
|||
|
||||
@Override
|
||||
protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException {
|
||||
// Parse audio data header, if it was not done, to extract information about the audio codec
|
||||
// and audio configuration.
|
||||
if (!hasParsedAudioDataHeader) {
|
||||
int header = data.readUnsignedByte();
|
||||
int audioFormat = (header >> 4) & 0x0F;
|
||||
int sampleRateIndex = (header >> 2) & 0x03;
|
||||
if (sampleRateIndex < 0 || sampleRateIndex >= AUDIO_SAMPLING_RATE_TABLE.length) {
|
||||
throw new UnsupportedFormatException("Invalid sample rate for the audio track");
|
||||
throw new UnsupportedFormatException("Invalid sample rate index: " + sampleRateIndex);
|
||||
}
|
||||
// TODO: Add support for MP3 and PCM.
|
||||
if (audioFormat != AUDIO_FORMAT_AAC) {
|
||||
// TODO: Adds support for MP3 and PCM
|
||||
if (audioFormat != AUDIO_FORMAT_AAC) {
|
||||
throw new UnsupportedFormatException("Audio format not supported: " + audioFormat);
|
||||
}
|
||||
throw new UnsupportedFormatException("Audio format not supported: " + audioFormat);
|
||||
}
|
||||
hasParsedAudioDataHeader = true;
|
||||
} else {
|
||||
// Skip header if it was parsed previously.
|
||||
data.skipBytes(1);
|
||||
}
|
||||
|
||||
// In all the cases we will be managing AAC format (otherwise an exception would be fired so we
|
||||
// can just always return true.
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,63 +21,61 @@ import com.google.android.exoplayer.extractor.ExtractorInput;
|
|||
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer.extractor.SeekMap;
|
||||
import com.google.android.exoplayer.util.Assertions;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Facilitates the extraction of data from the FLV container format.
|
||||
*/
|
||||
public final class FlvExtractor implements Extractor, SeekMap {
|
||||
// Header sizes
|
||||
private static final int FLV_MIN_HEADER_SIZE = 9;
|
||||
|
||||
// Header sizes.
|
||||
private static final int FLV_HEADER_SIZE = 9;
|
||||
private static final int FLV_TAG_HEADER_SIZE = 11;
|
||||
|
||||
// Parser states.
|
||||
private static final int STATE_READING_TAG_HEADER = 1;
|
||||
private static final int STATE_READING_SAMPLE = 2;
|
||||
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;
|
||||
|
||||
// Tag types
|
||||
// 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
|
||||
// FLV container identifier.
|
||||
private static final int FLV_TAG = Util.getIntegerCodeForString("FLV");
|
||||
|
||||
// Temporary buffers
|
||||
// Temporary buffers.
|
||||
private final ParsableByteArray scratch;
|
||||
private final ParsableByteArray headerBuffer;
|
||||
private final ParsableByteArray tagHeaderBuffer;
|
||||
private ParsableByteArray tagData;
|
||||
private final ParsableByteArray tagData;
|
||||
|
||||
// Extractor outputs.
|
||||
private ExtractorOutput extractorOutput;
|
||||
|
||||
// State variables.
|
||||
private int parserState;
|
||||
private int dataOffset;
|
||||
private TagHeader currentTagHeader;
|
||||
private int bytesToNextTagHeader;
|
||||
public int tagType;
|
||||
public int tagDataSize;
|
||||
public long tagTimestampUs;
|
||||
|
||||
// Tags readers
|
||||
// Tags readers.
|
||||
private AudioTagPayloadReader audioReader;
|
||||
private VideoTagPayloadReader videoReader;
|
||||
private ScriptTagPayloadReader metadataReader;
|
||||
|
||||
public FlvExtractor() {
|
||||
scratch = new ParsableByteArray(4);
|
||||
headerBuffer = new ParsableByteArray(FLV_MIN_HEADER_SIZE);
|
||||
headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE);
|
||||
tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);
|
||||
dataOffset = 0;
|
||||
currentTagHeader = new TagHeader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ExtractorOutput output) {
|
||||
this.extractorOutput = output;
|
||||
tagData = new ParsableByteArray();
|
||||
parserState = STATE_READING_FLV_HEADER;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -112,151 +110,133 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
|
||||
InterruptedException {
|
||||
if (dataOffset == 0
|
||||
&& !readHeader(input)) {
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
if (parserState == STATE_READING_TAG_HEADER) {
|
||||
if (!readTagHeader(input)) {
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
} else {
|
||||
return readSample(input);
|
||||
}
|
||||
}
|
||||
} catch (AudioTagPayloadReader.UnsupportedFormatException unsupportedTrack) {
|
||||
unsupportedTrack.printStackTrace();
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
public void init(ExtractorOutput output) {
|
||||
this.extractorOutput = output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek() {
|
||||
dataOffset = 0;
|
||||
parserState = STATE_READING_FLV_HEADER;
|
||||
bytesToNextTagHeader = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
|
||||
InterruptedException {
|
||||
while (true) {
|
||||
switch (parserState) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads FLV container header from the provided {@link ExtractorInput}.
|
||||
* 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. Otherwise, false.
|
||||
* @throws IOException If an error occurred reading from the source.
|
||||
* @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 readHeader(ExtractorInput input) throws IOException, InterruptedException {
|
||||
try {
|
||||
input.readFully(headerBuffer.data, 0, FLV_MIN_HEADER_SIZE);
|
||||
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));
|
||||
}
|
||||
if (hasVideo && videoReader == null) {
|
||||
videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO));
|
||||
}
|
||||
if (metadataReader == null) {
|
||||
metadataReader = new ScriptTagPayloadReader(null);
|
||||
}
|
||||
extractorOutput.endTracks();
|
||||
extractorOutput.seekMap(this);
|
||||
|
||||
// Store payload start position and start extended header (if there is one)
|
||||
dataOffset = headerBuffer.readInt();
|
||||
|
||||
input.skipFully(dataOffset - FLV_MIN_HEADER_SIZE);
|
||||
parserState = STATE_READING_TAG_HEADER;
|
||||
} catch (EOFException eof) {
|
||||
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));
|
||||
}
|
||||
if (hasVideo && videoReader == null) {
|
||||
videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO));
|
||||
}
|
||||
if (metadataReader == null) {
|
||||
metadataReader = new ScriptTagPayloadReader(null);
|
||||
}
|
||||
extractorOutput.endTracks();
|
||||
extractorOutput.seekMap(this);
|
||||
|
||||
// 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;
|
||||
parserState = 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;
|
||||
parserState = 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 from the source.
|
||||
* @throws IOException If an error occurred reading or parsing data from the source.
|
||||
* @throws InterruptedException If the thread was interrupted.
|
||||
* @throws TagPayloadReader.UnsupportedFormatException If payload of the tag is using a codec non
|
||||
* supported codec.
|
||||
*/
|
||||
private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException,
|
||||
TagPayloadReader.UnsupportedFormatException {
|
||||
try {
|
||||
// skipping previous tag size field
|
||||
input.skipFully(4);
|
||||
|
||||
// Read the tag header from the input.
|
||||
input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE);
|
||||
|
||||
tagHeaderBuffer.setPosition(0);
|
||||
int type = tagHeaderBuffer.readUnsignedByte();
|
||||
int dataSize = tagHeaderBuffer.readUnsignedInt24();
|
||||
long timestamp = tagHeaderBuffer.readUnsignedInt24();
|
||||
timestamp = (tagHeaderBuffer.readUnsignedByte() << 24) | timestamp;
|
||||
int streamId = tagHeaderBuffer.readUnsignedInt24();
|
||||
|
||||
currentTagHeader.type = type;
|
||||
currentTagHeader.dataSize = dataSize;
|
||||
currentTagHeader.timestamp = timestamp * 1000;
|
||||
currentTagHeader.streamId = streamId;
|
||||
|
||||
// Sanity checks.
|
||||
Assertions.checkState(type == TAG_TYPE_AUDIO || type == TAG_TYPE_VIDEO
|
||||
|| type == TAG_TYPE_SCRIPT_DATA);
|
||||
// Reuse tagData buffer to avoid lot of memory allocation (performance penalty).
|
||||
if (tagData == null || dataSize > tagData.capacity()) {
|
||||
tagData = new ParsableByteArray(dataSize);
|
||||
} else {
|
||||
tagData.setPosition(0);
|
||||
}
|
||||
tagData.setLimit(dataSize);
|
||||
parserState = STATE_READING_SAMPLE;
|
||||
|
||||
} catch (EOFException eof) {
|
||||
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
|
||||
parserState = STATE_READING_TAG_DATA;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads payload of an FLV tag from the provided {@link ExtractorInput}.
|
||||
* Reads the body of a tag from the provided {@link ExtractorInput}.
|
||||
*
|
||||
* @param input The {@link ExtractorInput} from which to read.
|
||||
* @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}.
|
||||
* @throws IOException If an error occurred reading from the source.
|
||||
* @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.
|
||||
* @throws TagPayloadReader.UnsupportedFormatException If payload of the tag is using a codec non
|
||||
* supported codec.
|
||||
*/
|
||||
private int readSample(ExtractorInput input) throws IOException,
|
||||
InterruptedException, AudioTagPayloadReader.UnsupportedFormatException {
|
||||
if (tagData != null) {
|
||||
if (!input.readFully(tagData.data, 0, currentTagHeader.dataSize, true)) {
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
tagData.setPosition(0);
|
||||
} else {
|
||||
input.skipFully(currentTagHeader.dataSize);
|
||||
return RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
// Pass payload to the right payload reader.
|
||||
if (currentTagHeader.type == TAG_TYPE_AUDIO && audioReader != null) {
|
||||
audioReader.consume(tagData, currentTagHeader.timestamp);
|
||||
} else if (currentTagHeader.type == TAG_TYPE_VIDEO && videoReader != null) {
|
||||
videoReader.consume(tagData, currentTagHeader.timestamp);
|
||||
} else if (currentTagHeader.type == TAG_TYPE_SCRIPT_DATA && metadataReader != null) {
|
||||
metadataReader.consume(tagData, currentTagHeader.timestamp);
|
||||
private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException {
|
||||
boolean wasConsumed = true;
|
||||
if (tagType == TAG_TYPE_AUDIO && audioReader != null) {
|
||||
audioReader.consume(prepareTagData(input), tagTimestampUs);
|
||||
} else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {
|
||||
videoReader.consume(prepareTagData(input), tagTimestampUs);
|
||||
} else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) {
|
||||
metadataReader.consume(prepareTagData(input), tagTimestampUs);
|
||||
if (metadataReader.getDurationUs() != C.UNKNOWN_TIME_US) {
|
||||
if (audioReader != null) {
|
||||
audioReader.setDurationUs(metadataReader.getDurationUs());
|
||||
|
|
@ -266,16 +246,28 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
tagData.reset();
|
||||
input.skipFully(tagDataSize);
|
||||
wasConsumed = false;
|
||||
}
|
||||
bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header.
|
||||
parserState = STATE_SKIPPING_TO_TAG_HEADER;
|
||||
return wasConsumed;
|
||||
}
|
||||
|
||||
parserState = STATE_READING_TAG_HEADER;
|
||||
|
||||
return RESULT_CONTINUE;
|
||||
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;
|
||||
}
|
||||
|
||||
// SeekMap implementation.
|
||||
// TODO: Add seeking support
|
||||
|
||||
@Override
|
||||
public boolean isSeekable() {
|
||||
return false;
|
||||
|
|
@ -286,16 +278,4 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
|||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines header of a FLV tag
|
||||
*/
|
||||
final class TagHeader {
|
||||
public int type;
|
||||
public int dataSize;
|
||||
public long timestamp;
|
||||
public int streamId;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer.extractor.flv;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
|
|
@ -55,17 +56,16 @@ import java.util.Map;
|
|||
}
|
||||
|
||||
@Override
|
||||
protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException {
|
||||
protected boolean parseHeader(ParsableByteArray data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected void parsePayload(ParsableByteArray data, long timeUs) {
|
||||
protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
|
||||
int nameType = readAmfType(data);
|
||||
if (nameType != AMF_TYPE_STRING) {
|
||||
// Should never happen.
|
||||
return;
|
||||
throw new ParserException();
|
||||
}
|
||||
String name = readAmfString(data);
|
||||
if (!NAME_METADATA.equals(name)) {
|
||||
|
|
@ -75,21 +75,118 @@ import java.util.Map;
|
|||
int type = readAmfType(data);
|
||||
if (type != AMF_TYPE_ECMA_ARRAY) {
|
||||
// Should never happen.
|
||||
return;
|
||||
throw new ParserException();
|
||||
}
|
||||
// Set the duration to the value contained in the metadata, if present.
|
||||
Map<String, Object> metadata = (Map<String, Object>) readAmfData(data, type);
|
||||
Map<String, Object> metadata = readAmfEcmaArray(data);
|
||||
if (metadata.containsKey(KEY_DURATION)) {
|
||||
double durationSeconds = (double) metadata.get(KEY_DURATION);
|
||||
setDurationUs((long) (durationSeconds * C.MICROS_PER_SECOND));
|
||||
}
|
||||
}
|
||||
|
||||
private int readAmfType(ParsableByteArray data) {
|
||||
private static int readAmfType(ParsableByteArray data) {
|
||||
return data.readUnsignedByte();
|
||||
}
|
||||
|
||||
private Object readAmfData(ParsableByteArray data, int type) {
|
||||
/**
|
||||
* Read a boolean from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private static Boolean readAmfBoolean(ParsableByteArray data) {
|
||||
return data.readUnsignedByte() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a double number from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private static Double readAmfDouble(ParsableByteArray data) {
|
||||
return Double.longBitsToDouble(data.readLong());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a string from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private static String readAmfString(ParsableByteArray data) {
|
||||
int size = data.readUnsignedShort();
|
||||
int position = data.getPosition();
|
||||
data.skipBytes(size);
|
||||
return new String(data.data, position, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an array from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private static ArrayList<Object> readAmfStrictArray(ParsableByteArray data) {
|
||||
int count = data.readUnsignedIntToInt();
|
||||
ArrayList<Object> list = new ArrayList<>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
int type = readAmfType(data);
|
||||
list.add(readAmfData(data, type));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an object from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private static HashMap<String, Object> readAmfObject(ParsableByteArray data) {
|
||||
HashMap<String, Object> array = new HashMap<>();
|
||||
while (true) {
|
||||
String key = readAmfString(data);
|
||||
int type = readAmfType(data);
|
||||
if (type == AMF_TYPE_END_MARKER) {
|
||||
break;
|
||||
}
|
||||
array.put(key, readAmfData(data, type));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an ECMA array from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private static HashMap<String, Object> readAmfEcmaArray(ParsableByteArray data) {
|
||||
int count = data.readUnsignedIntToInt();
|
||||
HashMap<String, Object> array = new HashMap<>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
String key = readAmfString(data);
|
||||
int type = readAmfType(data);
|
||||
array.put(key, readAmfData(data, type));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a date from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private static Date readAmfDate(ParsableByteArray data) {
|
||||
Date date = new Date((long) readAmfDouble(data).doubleValue());
|
||||
data.skipBytes(2); // Skip reserved bytes.
|
||||
return date;
|
||||
}
|
||||
|
||||
private static Object readAmfData(ParsableByteArray data, int type) {
|
||||
switch (type) {
|
||||
case AMF_TYPE_NUMBER:
|
||||
return readAmfDouble(data);
|
||||
|
|
@ -110,101 +207,4 @@ import java.util.Map;
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a boolean from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private Boolean readAmfBoolean(ParsableByteArray data) {
|
||||
return data.readUnsignedByte() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a double number from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private Double readAmfDouble(ParsableByteArray data) {
|
||||
return Double.longBitsToDouble(data.readLong());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a string from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private String readAmfString(ParsableByteArray data) {
|
||||
int size = data.readUnsignedShort();
|
||||
int position = data.getPosition();
|
||||
data.skipBytes(size);
|
||||
return new String(data.data, position, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an array from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private Object readAmfStrictArray(ParsableByteArray data) {
|
||||
long count = data.readUnsignedInt();
|
||||
ArrayList<Object> list = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
int type = readAmfType(data);
|
||||
list.add(readAmfData(data, type));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an object from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private Object readAmfObject(ParsableByteArray data) {
|
||||
HashMap<String, Object> array = new HashMap<>();
|
||||
while (true) {
|
||||
String key = readAmfString(data);
|
||||
int type = readAmfType(data);
|
||||
if (type == AMF_TYPE_END_MARKER) {
|
||||
break;
|
||||
}
|
||||
array.put(key, readAmfData(data, type));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an ECMA array from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private Object readAmfEcmaArray(ParsableByteArray data) {
|
||||
long count = data.readUnsignedInt();
|
||||
HashMap<String, Object> array = new HashMap<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
String key = readAmfString(data);
|
||||
int type = readAmfType(data);
|
||||
array.put(key, readAmfData(data, type));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a date from an AMF encoded buffer.
|
||||
*
|
||||
* @param data The buffer from which to read.
|
||||
* @return The value read from the buffer.
|
||||
*/
|
||||
private Date readAmfDate(ParsableByteArray data) {
|
||||
Date date = new Date((long) readAmfDouble(data).doubleValue());
|
||||
data.readUnsignedShort(); // Skip reserved bytes.
|
||||
return date;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer.extractor.flv;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.ParserException;
|
||||
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
|||
/**
|
||||
* Thrown when the format is not supported.
|
||||
*/
|
||||
public static final class UnsupportedFormatException extends Exception {
|
||||
public static final class UnsupportedFormatException extends ParserException {
|
||||
|
||||
public UnsupportedFormatException(String msg) {
|
||||
super(msg);
|
||||
|
|
@ -79,8 +80,9 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
|||
*
|
||||
* @param data The payload data to consume.
|
||||
* @param timeUs The timestamp associated with the payload.
|
||||
* @throws ParserException If an error occurs parsing the data.
|
||||
*/
|
||||
public final void consume(ParsableByteArray data, long timeUs) throws UnsupportedFormatException {
|
||||
public final void consume(ParsableByteArray data, long timeUs) throws ParserException {
|
||||
if (parseHeader(data)) {
|
||||
parsePayload(data, timeUs);
|
||||
}
|
||||
|
|
@ -92,16 +94,17 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
|||
* @param data Buffer where the tag header is stored.
|
||||
* @return True if the header was parsed successfully and the payload should be read. False
|
||||
* otherwise.
|
||||
* @throws UnsupportedFormatException
|
||||
* @throws ParserException If an error occurs parsing the header.
|
||||
*/
|
||||
protected abstract boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException;
|
||||
protected abstract boolean parseHeader(ParsableByteArray data) throws ParserException;
|
||||
|
||||
/**
|
||||
* Parses tag payload.
|
||||
*
|
||||
* @param data Buffer where tag payload is stored
|
||||
* @param timeUs Time position of the frame
|
||||
* @throws ParserException If an error occurs parsing the payload.
|
||||
*/
|
||||
protected abstract void parsePayload(ParsableByteArray data, long timeUs);
|
||||
protected abstract void parsePayload(ParsableByteArray data, long timeUs) throws ParserException;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ import com.google.android.exoplayer.util.NalUnitUtil;
|
|||
import com.google.android.exoplayer.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -35,24 +33,22 @@ import java.util.List;
|
|||
* Parses video tags from an FLV stream and extracts H.264 nal units.
|
||||
*/
|
||||
/* package */ final class VideoTagPayloadReader extends TagPayloadReader {
|
||||
private static final String TAG = "VideoTagPayloadReader";
|
||||
|
||||
// Video codec
|
||||
// Video codec.
|
||||
private static final int VIDEO_CODEC_AVC = 7;
|
||||
|
||||
// FRAME TYPE
|
||||
// Frame types.
|
||||
private static final int VIDEO_FRAME_KEYFRAME = 1;
|
||||
private static final int VIDEO_FRAME_VIDEO_INFO = 5;
|
||||
|
||||
// PACKET TYPE
|
||||
// Packet types.
|
||||
private static final int AVC_PACKET_TYPE_SEQUENCE_HEADER = 0;
|
||||
private static final int AVC_PACKET_TYPE_AVC_NALU = 1;
|
||||
private static final int AVC_PACKET_TYPE_AVC_END_OF_SEQUENCE = 2;
|
||||
|
||||
// Temporary arrays.
|
||||
private final ParsableByteArray nalStartCode;
|
||||
private final ParsableByteArray nalLength;
|
||||
private int nalUnitsLength;
|
||||
private int nalUnitLengthFieldLength;
|
||||
|
||||
// State variables.
|
||||
private boolean hasOutputFormat;
|
||||
|
|
@ -86,28 +82,17 @@ import java.util.List;
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void parsePayload(ParsableByteArray data, long timeUs) {
|
||||
protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
|
||||
int packetType = data.readUnsignedByte();
|
||||
int compositionTime = data.readUnsignedInt24();
|
||||
// If there is a composition time, adjust timeUs accordingly
|
||||
// Note: compositionTime within AVCVIDEOPACKET is provided in milliseconds
|
||||
// and timeUs is in microseconds.
|
||||
if (compositionTime > 0) {
|
||||
timeUs += compositionTime * 1000;
|
||||
}
|
||||
int compositionTimeMs = data.readUnsignedInt24();
|
||||
timeUs += compositionTimeMs * 1000L;
|
||||
// Parse avc sequence header in case this was not done before.
|
||||
if (packetType == AVC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {
|
||||
ParsableByteArray videoSequence = new ParsableByteArray(new byte[data.bytesLeft()]);
|
||||
data.readBytes(videoSequence.data, 0, data.bytesLeft());
|
||||
|
||||
AvcSequenceHeaderData avcData;
|
||||
try {
|
||||
avcData = parseAvcCodecPrivate(videoSequence);
|
||||
nalUnitsLength = avcData.nalUnitLengthFieldLength;
|
||||
} catch (ParserException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
AvcSequenceHeaderData avcData = parseAvcCodecPrivate(videoSequence);
|
||||
nalUnitLengthFieldLength = avcData.nalUnitLengthFieldLength;
|
||||
|
||||
// Construct and output the format.
|
||||
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.NO_VALUE,
|
||||
|
|
@ -124,8 +109,7 @@ import java.util.List;
|
|||
nalLengthData[0] = 0;
|
||||
nalLengthData[1] = 0;
|
||||
nalLengthData[2] = 0;
|
||||
int nalUnitLengthFieldLength = nalUnitsLength;
|
||||
int nalUnitLengthFieldLengthDiff = 4 - nalUnitsLength;
|
||||
int nalUnitLengthFieldLengthDiff = 4 - nalUnitLengthFieldLength;
|
||||
// NAL units are length delimited, but the decoder requires start code delimited units.
|
||||
// Loop until we've written the sample to the track output, replacing length delimiters with
|
||||
// start codes as we encounter them.
|
||||
|
|
@ -137,65 +121,58 @@ import java.util.List;
|
|||
nalLength.setPosition(0);
|
||||
bytesToWrite = nalLength.readUnsignedIntToInt();
|
||||
|
||||
// First, write nal start code (replacing length field by nal delimiter codes)
|
||||
// Write a start code for the current NAL unit.
|
||||
nalStartCode.setPosition(0);
|
||||
output.sampleData(nalStartCode, 4);
|
||||
bytesWritten += 4;
|
||||
|
||||
// Then write nal unit itsef
|
||||
// Write the payload of the NAL unit.
|
||||
output.sampleData(data, bytesToWrite);
|
||||
bytesWritten += bytesToWrite;
|
||||
}
|
||||
output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.SAMPLE_FLAG_SYNC : 0,
|
||||
bytesWritten, 0, null);
|
||||
} else if (packetType == AVC_PACKET_TYPE_AVC_END_OF_SEQUENCE) {
|
||||
Log.d(TAG, "End of seq!!!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds initialization data for a {@link MediaFormat} from H.264 (AVC) codec private data.
|
||||
*
|
||||
* @return The AvcSequenceHeader data with all the information needed to initialize
|
||||
* the video codec.
|
||||
* @return The AvcSequenceHeader data needed to initialize the video codec.
|
||||
* @throws ParserException If the initialization data could not be built.
|
||||
*/
|
||||
private AvcSequenceHeaderData parseAvcCodecPrivate(ParsableByteArray buffer)
|
||||
throws ParserException {
|
||||
try {
|
||||
// TODO: Deduplicate with AtomParsers.parseAvcCFromParent.
|
||||
buffer.setPosition(4);
|
||||
int nalUnitLengthFieldLength = (buffer.readUnsignedByte() & 0x03) + 1;
|
||||
Assertions.checkState(nalUnitLengthFieldLength != 3);
|
||||
List<byte[]> initializationData = new ArrayList<>();
|
||||
int numSequenceParameterSets = buffer.readUnsignedByte() & 0x1F;
|
||||
for (int i = 0; i < numSequenceParameterSets; i++) {
|
||||
initializationData.add(NalUnitUtil.parseChildNalUnit(buffer));
|
||||
}
|
||||
int numPictureParameterSets = buffer.readUnsignedByte();
|
||||
for (int j = 0; j < numPictureParameterSets; j++) {
|
||||
initializationData.add(NalUnitUtil.parseChildNalUnit(buffer));
|
||||
}
|
||||
|
||||
float pixelWidthAspectRatio = 1;
|
||||
int width = MediaFormat.NO_VALUE;
|
||||
int height = MediaFormat.NO_VALUE;
|
||||
if (numSequenceParameterSets > 0) {
|
||||
// Parse the first sequence parameter set to obtain pixelWidthAspectRatio.
|
||||
ParsableBitArray spsDataBitArray = new ParsableBitArray(initializationData.get(0));
|
||||
// Skip the NAL header consisting of the nalUnitLengthField and the type (1 byte).
|
||||
spsDataBitArray.setPosition(8 * (nalUnitLengthFieldLength + 1));
|
||||
CodecSpecificDataUtil.SpsData sps = CodecSpecificDataUtil.parseSpsNalUnit(spsDataBitArray);
|
||||
width = sps.width;
|
||||
height = sps.height;
|
||||
pixelWidthAspectRatio = sps.pixelWidthAspectRatio;
|
||||
}
|
||||
|
||||
return new AvcSequenceHeaderData(initializationData, nalUnitLengthFieldLength,
|
||||
width, height, pixelWidthAspectRatio);
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
throw new ParserException("Error parsing AVC codec private");
|
||||
// TODO: Deduplicate with AtomParsers.parseAvcCFromParent.
|
||||
buffer.setPosition(4);
|
||||
int nalUnitLengthFieldLength = (buffer.readUnsignedByte() & 0x03) + 1;
|
||||
Assertions.checkState(nalUnitLengthFieldLength != 3);
|
||||
List<byte[]> initializationData = new ArrayList<>();
|
||||
int numSequenceParameterSets = buffer.readUnsignedByte() & 0x1F;
|
||||
for (int i = 0; i < numSequenceParameterSets; i++) {
|
||||
initializationData.add(NalUnitUtil.parseChildNalUnit(buffer));
|
||||
}
|
||||
int numPictureParameterSets = buffer.readUnsignedByte();
|
||||
for (int j = 0; j < numPictureParameterSets; j++) {
|
||||
initializationData.add(NalUnitUtil.parseChildNalUnit(buffer));
|
||||
}
|
||||
|
||||
float pixelWidthAspectRatio = 1;
|
||||
int width = MediaFormat.NO_VALUE;
|
||||
int height = MediaFormat.NO_VALUE;
|
||||
if (numSequenceParameterSets > 0) {
|
||||
// Parse the first sequence parameter set to obtain pixelWidthAspectRatio.
|
||||
ParsableBitArray spsDataBitArray = new ParsableBitArray(initializationData.get(0));
|
||||
// Skip the NAL header consisting of the nalUnitLengthField and the type (1 byte).
|
||||
spsDataBitArray.setPosition(8 * (nalUnitLengthFieldLength + 1));
|
||||
CodecSpecificDataUtil.SpsData sps = CodecSpecificDataUtil.parseSpsNalUnit(spsDataBitArray);
|
||||
width = sps.width;
|
||||
height = sps.height;
|
||||
pixelWidthAspectRatio = sps.pixelWidthAspectRatio;
|
||||
}
|
||||
|
||||
return new AvcSequenceHeaderData(initializationData, nalUnitLengthFieldLength,
|
||||
width, height, pixelWidthAspectRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue