Commit 4422e8a0 by Oliver Woodman

Further cleanup to FLV extractor

parent f91ea903
...@@ -148,7 +148,6 @@ import java.util.Locale; ...@@ -148,7 +148,6 @@ import java.util.Locale;
"http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", PlayerActivity.TYPE_OTHER), "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", PlayerActivity.TYPE_OTHER),
new Sample("Big Buck Bunny (FLV Video)", new Sample("Big Buck Bunny (FLV Video)",
"http://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0", PlayerActivity.TYPE_OTHER), "http://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0", PlayerActivity.TYPE_OTHER),
}; };
private Samples() {} private Samples() {}
......
...@@ -28,7 +28,7 @@ import android.util.Pair; ...@@ -28,7 +28,7 @@ import android.util.Pair;
import java.util.Collections; 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 { /* package */ final class AudioTagPayloadReader extends TagPayloadReader {
...@@ -59,29 +59,22 @@ import java.util.Collections; ...@@ -59,29 +59,22 @@ import java.util.Collections;
@Override @Override
protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException { 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) { if (!hasParsedAudioDataHeader) {
int header = data.readUnsignedByte(); int header = data.readUnsignedByte();
int audioFormat = (header >> 4) & 0x0F; int audioFormat = (header >> 4) & 0x0F;
int sampleRateIndex = (header >> 2) & 0x03; int sampleRateIndex = (header >> 2) & 0x03;
if (sampleRateIndex < 0 || sampleRateIndex >= AUDIO_SAMPLING_RATE_TABLE.length) { 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) { if (audioFormat != AUDIO_FORMAT_AAC) {
// TODO: Adds support for MP3 and PCM throw new UnsupportedFormatException("Audio format not supported: " + audioFormat);
if (audioFormat != AUDIO_FORMAT_AAC) {
throw new UnsupportedFormatException("Audio format not supported: " + audioFormat);
}
} }
hasParsedAudioDataHeader = true; hasParsedAudioDataHeader = true;
} else { } else {
// Skip header if it was parsed previously. // Skip header if it was parsed previously.
data.skipBytes(1); 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; return true;
} }
......
...@@ -21,63 +21,61 @@ import com.google.android.exoplayer.extractor.ExtractorInput; ...@@ -21,63 +21,61 @@ import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput; import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.PositionHolder; import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap; 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.ParsableByteArray;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
/** /**
* Facilitates the extraction of data from the FLV container format. * Facilitates the extraction of data from the FLV container format.
*/ */
public final class FlvExtractor implements Extractor, SeekMap { 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; private static final int FLV_TAG_HEADER_SIZE = 11;
// Parser states. // Parser states.
private static final int STATE_READING_TAG_HEADER = 1; private static final int STATE_READING_FLV_HEADER = 1;
private static final int STATE_READING_SAMPLE = 2; 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_AUDIO = 8;
private static final int TAG_TYPE_VIDEO = 9; private static final int TAG_TYPE_VIDEO = 9;
private static final int TAG_TYPE_SCRIPT_DATA = 18; private static final int TAG_TYPE_SCRIPT_DATA = 18;
// FLV container identifier // FLV container identifier.
private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); private static final int FLV_TAG = Util.getIntegerCodeForString("FLV");
// Temporary buffers // Temporary buffers.
private final ParsableByteArray scratch; private final ParsableByteArray scratch;
private final ParsableByteArray headerBuffer; private final ParsableByteArray headerBuffer;
private final ParsableByteArray tagHeaderBuffer; private final ParsableByteArray tagHeaderBuffer;
private ParsableByteArray tagData; private final ParsableByteArray tagData;
// Extractor outputs. // Extractor outputs.
private ExtractorOutput extractorOutput; private ExtractorOutput extractorOutput;
// State variables. // State variables.
private int parserState; private int parserState;
private int dataOffset; private int bytesToNextTagHeader;
private TagHeader currentTagHeader; public int tagType;
public int tagDataSize;
public long tagTimestampUs;
// Tags readers // Tags readers.
private AudioTagPayloadReader audioReader; private AudioTagPayloadReader audioReader;
private VideoTagPayloadReader videoReader; private VideoTagPayloadReader videoReader;
private ScriptTagPayloadReader metadataReader; private ScriptTagPayloadReader metadataReader;
public FlvExtractor() { public FlvExtractor() {
scratch = new ParsableByteArray(4); scratch = new ParsableByteArray(4);
headerBuffer = new ParsableByteArray(FLV_MIN_HEADER_SIZE); headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE);
tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);
dataOffset = 0; tagData = new ParsableByteArray();
currentTagHeader = new TagHeader(); parserState = STATE_READING_FLV_HEADER;
}
@Override
public void init(ExtractorOutput output) {
this.extractorOutput = output;
} }
@Override @Override
...@@ -112,151 +110,133 @@ public final class FlvExtractor implements Extractor, SeekMap { ...@@ -112,151 +110,133 @@ public final class FlvExtractor implements Extractor, SeekMap {
} }
@Override @Override
public void init(ExtractorOutput output) {
this.extractorOutput = output;
}
@Override
public void seek() {
parserState = STATE_READING_FLV_HEADER;
bytesToNextTagHeader = 0;
}
@Override
public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
InterruptedException { InterruptedException {
if (dataOffset == 0 while (true) {
&& !readHeader(input)) { switch (parserState) {
return RESULT_END_OF_INPUT; case STATE_READING_FLV_HEADER:
} if (!readFlvHeader(input)) {
return RESULT_END_OF_INPUT;
try { }
while (true) { break;
if (parserState == STATE_READING_TAG_HEADER) { case STATE_SKIPPING_TO_TAG_HEADER:
skipToTagHeader(input);
break;
case STATE_READING_TAG_HEADER:
if (!readTagHeader(input)) { if (!readTagHeader(input)) {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
} }
} else { break;
return readSample(input); case STATE_READING_TAG_DATA:
} if (readTagData(input)) {
return RESULT_CONTINUE;
}
break;
} }
} catch (AudioTagPayloadReader.UnsupportedFormatException unsupportedTrack) {
unsupportedTrack.printStackTrace();
return RESULT_END_OF_INPUT;
} }
} }
@Override
public void seek() {
dataOffset = 0;
}
/** /**
* 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. * @param input The {@link ExtractorInput} from which to read.
* @return True if header was read successfully. Otherwise, false. * @return True if header was read successfully. False if the end of stream was reached.
* @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 InterruptedException If the thread was interrupted.
*/ */
private boolean readHeader(ExtractorInput input) throws IOException, InterruptedException { private boolean readFlvHeader(ExtractorInput input) throws IOException, InterruptedException {
try { if (!input.readFully(headerBuffer.data, 0, FLV_HEADER_SIZE, true)) {
input.readFully(headerBuffer.data, 0, FLV_MIN_HEADER_SIZE); // We've reached the end of the stream.
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) {
return false; 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; 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}. * Reads a tag header from the provided {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read. * @param input The {@link ExtractorInput} from which to read.
* @return True if tag header was read successfully. Otherwise, false. * @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 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, private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException {
TagPayloadReader.UnsupportedFormatException { if (!input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE, true)) {
try { // We've reached the end of the stream.
// 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) {
return false; 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; 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. * @param input The {@link ExtractorInput} from which to read.
* @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}. * @return True if the data was consumed by a reader. False if it was skipped.
* @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 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, private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException {
InterruptedException, AudioTagPayloadReader.UnsupportedFormatException { boolean wasConsumed = true;
if (tagData != null) { if (tagType == TAG_TYPE_AUDIO && audioReader != null) {
if (!input.readFully(tagData.data, 0, currentTagHeader.dataSize, true)) { audioReader.consume(prepareTagData(input), tagTimestampUs);
return RESULT_END_OF_INPUT; } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {
} videoReader.consume(prepareTagData(input), tagTimestampUs);
tagData.setPosition(0); } else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) {
} else { metadataReader.consume(prepareTagData(input), tagTimestampUs);
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);
if (metadataReader.getDurationUs() != C.UNKNOWN_TIME_US) { if (metadataReader.getDurationUs() != C.UNKNOWN_TIME_US) {
if (audioReader != null) { if (audioReader != null) {
audioReader.setDurationUs(metadataReader.getDurationUs()); audioReader.setDurationUs(metadataReader.getDurationUs());
...@@ -266,16 +246,28 @@ public final class FlvExtractor implements Extractor, SeekMap { ...@@ -266,16 +246,28 @@ public final class FlvExtractor implements Extractor, SeekMap {
} }
} }
} else { } 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; private ParsableByteArray prepareTagData(ExtractorInput input) throws IOException,
InterruptedException {
return RESULT_CONTINUE; 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. // SeekMap implementation.
// TODO: Add seeking support
@Override @Override
public boolean isSeekable() { public boolean isSeekable() {
return false; return false;
...@@ -286,16 +278,4 @@ public final class FlvExtractor implements Extractor, SeekMap { ...@@ -286,16 +278,4 @@ public final class FlvExtractor implements Extractor, SeekMap {
return 0; 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 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer.extractor.flv; package com.google.android.exoplayer.extractor.flv;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
...@@ -55,17 +56,16 @@ import java.util.Map; ...@@ -55,17 +56,16 @@ import java.util.Map;
} }
@Override @Override
protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException { protected boolean parseHeader(ParsableByteArray data) {
return true; return true;
} }
@SuppressWarnings("unchecked")
@Override @Override
protected void parsePayload(ParsableByteArray data, long timeUs) { protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
int nameType = readAmfType(data); int nameType = readAmfType(data);
if (nameType != AMF_TYPE_STRING) { if (nameType != AMF_TYPE_STRING) {
// Should never happen. // Should never happen.
return; throw new ParserException();
} }
String name = readAmfString(data); String name = readAmfString(data);
if (!NAME_METADATA.equals(name)) { if (!NAME_METADATA.equals(name)) {
...@@ -75,48 +75,27 @@ import java.util.Map; ...@@ -75,48 +75,27 @@ import java.util.Map;
int type = readAmfType(data); int type = readAmfType(data);
if (type != AMF_TYPE_ECMA_ARRAY) { if (type != AMF_TYPE_ECMA_ARRAY) {
// Should never happen. // Should never happen.
return; throw new ParserException();
} }
// Set the duration to the value contained in the metadata, if present. // 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)) { if (metadata.containsKey(KEY_DURATION)) {
double durationSeconds = (double) metadata.get(KEY_DURATION); double durationSeconds = (double) metadata.get(KEY_DURATION);
setDurationUs((long) (durationSeconds * C.MICROS_PER_SECOND)); setDurationUs((long) (durationSeconds * C.MICROS_PER_SECOND));
} }
} }
private int readAmfType(ParsableByteArray data) { private static int readAmfType(ParsableByteArray data) {
return data.readUnsignedByte(); return data.readUnsignedByte();
} }
private Object readAmfData(ParsableByteArray data, int type) {
switch (type) {
case AMF_TYPE_NUMBER:
return readAmfDouble(data);
case AMF_TYPE_BOOLEAN:
return readAmfBoolean(data);
case AMF_TYPE_STRING:
return readAmfString(data);
case AMF_TYPE_OBJECT:
return readAmfObject(data);
case AMF_TYPE_ECMA_ARRAY:
return readAmfEcmaArray(data);
case AMF_TYPE_STRICT_ARRAY:
return readAmfStrictArray(data);
case AMF_TYPE_DATE:
return readAmfDate(data);
default:
return null;
}
}
/** /**
* Read a boolean from an AMF encoded buffer. * Read a boolean from an AMF encoded buffer.
* *
* @param data The buffer from which to read. * @param data The buffer from which to read.
* @return The value read from the buffer. * @return The value read from the buffer.
*/ */
private Boolean readAmfBoolean(ParsableByteArray data) { private static Boolean readAmfBoolean(ParsableByteArray data) {
return data.readUnsignedByte() == 1; return data.readUnsignedByte() == 1;
} }
...@@ -126,7 +105,7 @@ import java.util.Map; ...@@ -126,7 +105,7 @@ import java.util.Map;
* @param data The buffer from which to read. * @param data The buffer from which to read.
* @return The value read from the buffer. * @return The value read from the buffer.
*/ */
private Double readAmfDouble(ParsableByteArray data) { private static Double readAmfDouble(ParsableByteArray data) {
return Double.longBitsToDouble(data.readLong()); return Double.longBitsToDouble(data.readLong());
} }
...@@ -136,7 +115,7 @@ import java.util.Map; ...@@ -136,7 +115,7 @@ import java.util.Map;
* @param data The buffer from which to read. * @param data The buffer from which to read.
* @return The value read from the buffer. * @return The value read from the buffer.
*/ */
private String readAmfString(ParsableByteArray data) { private static String readAmfString(ParsableByteArray data) {
int size = data.readUnsignedShort(); int size = data.readUnsignedShort();
int position = data.getPosition(); int position = data.getPosition();
data.skipBytes(size); data.skipBytes(size);
...@@ -149,9 +128,9 @@ import java.util.Map; ...@@ -149,9 +128,9 @@ import java.util.Map;
* @param data The buffer from which to read. * @param data The buffer from which to read.
* @return The value read from the buffer. * @return The value read from the buffer.
*/ */
private Object readAmfStrictArray(ParsableByteArray data) { private static ArrayList<Object> readAmfStrictArray(ParsableByteArray data) {
long count = data.readUnsignedInt(); int count = data.readUnsignedIntToInt();
ArrayList<Object> list = new ArrayList<>(); ArrayList<Object> list = new ArrayList<>(count);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
int type = readAmfType(data); int type = readAmfType(data);
list.add(readAmfData(data, type)); list.add(readAmfData(data, type));
...@@ -165,7 +144,7 @@ import java.util.Map; ...@@ -165,7 +144,7 @@ import java.util.Map;
* @param data The buffer from which to read. * @param data The buffer from which to read.
* @return The value read from the buffer. * @return The value read from the buffer.
*/ */
private Object readAmfObject(ParsableByteArray data) { private static HashMap<String, Object> readAmfObject(ParsableByteArray data) {
HashMap<String, Object> array = new HashMap<>(); HashMap<String, Object> array = new HashMap<>();
while (true) { while (true) {
String key = readAmfString(data); String key = readAmfString(data);
...@@ -184,9 +163,9 @@ import java.util.Map; ...@@ -184,9 +163,9 @@ import java.util.Map;
* @param data The buffer from which to read. * @param data The buffer from which to read.
* @return The value read from the buffer. * @return The value read from the buffer.
*/ */
private Object readAmfEcmaArray(ParsableByteArray data) { private static HashMap<String, Object> readAmfEcmaArray(ParsableByteArray data) {
long count = data.readUnsignedInt(); int count = data.readUnsignedIntToInt();
HashMap<String, Object> array = new HashMap<>(); HashMap<String, Object> array = new HashMap<>(count);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
String key = readAmfString(data); String key = readAmfString(data);
int type = readAmfType(data); int type = readAmfType(data);
...@@ -201,10 +180,31 @@ import java.util.Map; ...@@ -201,10 +180,31 @@ import java.util.Map;
* @param data The buffer from which to read. * @param data The buffer from which to read.
* @return The value read from the buffer. * @return The value read from the buffer.
*/ */
private Date readAmfDate(ParsableByteArray data) { private static Date readAmfDate(ParsableByteArray data) {
Date date = new Date((long) readAmfDouble(data).doubleValue()); Date date = new Date((long) readAmfDouble(data).doubleValue());
data.readUnsignedShort(); // Skip reserved bytes. data.skipBytes(2); // Skip reserved bytes.
return date; return date;
} }
private static Object readAmfData(ParsableByteArray data, int type) {
switch (type) {
case AMF_TYPE_NUMBER:
return readAmfDouble(data);
case AMF_TYPE_BOOLEAN:
return readAmfBoolean(data);
case AMF_TYPE_STRING:
return readAmfString(data);
case AMF_TYPE_OBJECT:
return readAmfObject(data);
case AMF_TYPE_ECMA_ARRAY:
return readAmfEcmaArray(data);
case AMF_TYPE_STRICT_ARRAY:
return readAmfStrictArray(data);
case AMF_TYPE_DATE:
return readAmfDate(data);
default:
return null;
}
}
} }
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer.extractor.flv; package com.google.android.exoplayer.extractor.flv;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
...@@ -27,7 +28,7 @@ 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. * 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) { public UnsupportedFormatException(String msg) {
super(msg); super(msg);
...@@ -79,8 +80,9 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -79,8 +80,9 @@ import com.google.android.exoplayer.util.ParsableByteArray;
* *
* @param data The payload data to consume. * @param data The payload data to consume.
* @param timeUs The timestamp associated with the payload. * @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)) { if (parseHeader(data)) {
parsePayload(data, timeUs); parsePayload(data, timeUs);
} }
...@@ -92,16 +94,17 @@ import com.google.android.exoplayer.util.ParsableByteArray; ...@@ -92,16 +94,17 @@ import com.google.android.exoplayer.util.ParsableByteArray;
* @param data Buffer where the tag header is stored. * @param data Buffer where the tag header is stored.
* @return True if the header was parsed successfully and the payload should be read. False * @return True if the header was parsed successfully and the payload should be read. False
* otherwise. * 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. * Parses tag payload.
* *
* @param data Buffer where tag payload is stored * @param data Buffer where tag payload is stored
* @param timeUs Time position of the frame * @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; ...@@ -26,8 +26,6 @@ import com.google.android.exoplayer.util.NalUnitUtil;
import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableBitArray;
import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.ParsableByteArray;
import android.util.Log;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
...@@ -35,24 +33,22 @@ 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. * Parses video tags from an FLV stream and extracts H.264 nal units.
*/ */
/* package */ final class VideoTagPayloadReader extends TagPayloadReader { /* package */ final class VideoTagPayloadReader extends TagPayloadReader {
private static final String TAG = "VideoTagPayloadReader";
// Video codec // Video codec.
private static final int VIDEO_CODEC_AVC = 7; 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_KEYFRAME = 1;
private static final int VIDEO_FRAME_VIDEO_INFO = 5; 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_SEQUENCE_HEADER = 0;
private static final int AVC_PACKET_TYPE_AVC_NALU = 1; private static final int AVC_PACKET_TYPE_AVC_NALU = 1;
private static final int AVC_PACKET_TYPE_AVC_END_OF_SEQUENCE = 2;
// Temporary arrays. // Temporary arrays.
private final ParsableByteArray nalStartCode; private final ParsableByteArray nalStartCode;
private final ParsableByteArray nalLength; private final ParsableByteArray nalLength;
private int nalUnitsLength; private int nalUnitLengthFieldLength;
// State variables. // State variables.
private boolean hasOutputFormat; private boolean hasOutputFormat;
...@@ -86,28 +82,17 @@ import java.util.List; ...@@ -86,28 +82,17 @@ import java.util.List;
} }
@Override @Override
protected void parsePayload(ParsableByteArray data, long timeUs) { protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
int packetType = data.readUnsignedByte(); int packetType = data.readUnsignedByte();
int compositionTime = data.readUnsignedInt24(); int compositionTimeMs = data.readUnsignedInt24();
// If there is a composition time, adjust timeUs accordingly timeUs += compositionTimeMs * 1000L;
// Note: compositionTime within AVCVIDEOPACKET is provided in milliseconds
// and timeUs is in microseconds.
if (compositionTime > 0) {
timeUs += compositionTime * 1000;
}
// Parse avc sequence header in case this was not done before. // Parse avc sequence header in case this was not done before.
if (packetType == AVC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { if (packetType == AVC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {
ParsableByteArray videoSequence = new ParsableByteArray(new byte[data.bytesLeft()]); ParsableByteArray videoSequence = new ParsableByteArray(new byte[data.bytesLeft()]);
data.readBytes(videoSequence.data, 0, data.bytesLeft()); data.readBytes(videoSequence.data, 0, data.bytesLeft());
AvcSequenceHeaderData avcData; AvcSequenceHeaderData avcData = parseAvcCodecPrivate(videoSequence);
try { nalUnitLengthFieldLength = avcData.nalUnitLengthFieldLength;
avcData = parseAvcCodecPrivate(videoSequence);
nalUnitsLength = avcData.nalUnitLengthFieldLength;
} catch (ParserException e) {
e.printStackTrace();
return;
}
// Construct and output the format. // Construct and output the format.
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.NO_VALUE, MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.NO_VALUE,
...@@ -124,8 +109,7 @@ import java.util.List; ...@@ -124,8 +109,7 @@ import java.util.List;
nalLengthData[0] = 0; nalLengthData[0] = 0;
nalLengthData[1] = 0; nalLengthData[1] = 0;
nalLengthData[2] = 0; nalLengthData[2] = 0;
int nalUnitLengthFieldLength = nalUnitsLength; int nalUnitLengthFieldLengthDiff = 4 - nalUnitLengthFieldLength;
int nalUnitLengthFieldLengthDiff = 4 - nalUnitsLength;
// NAL units are length delimited, but the decoder requires start code delimited units. // 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 // Loop until we've written the sample to the track output, replacing length delimiters with
// start codes as we encounter them. // start codes as we encounter them.
...@@ -137,65 +121,58 @@ import java.util.List; ...@@ -137,65 +121,58 @@ import java.util.List;
nalLength.setPosition(0); nalLength.setPosition(0);
bytesToWrite = nalLength.readUnsignedIntToInt(); 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); nalStartCode.setPosition(0);
output.sampleData(nalStartCode, 4); output.sampleData(nalStartCode, 4);
bytesWritten += 4; bytesWritten += 4;
// Then write nal unit itsef // Write the payload of the NAL unit.
output.sampleData(data, bytesToWrite); output.sampleData(data, bytesToWrite);
bytesWritten += bytesToWrite; bytesWritten += bytesToWrite;
} }
output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.SAMPLE_FLAG_SYNC : 0, output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.SAMPLE_FLAG_SYNC : 0,
bytesWritten, 0, null); 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. * 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 * @return The AvcSequenceHeader data needed to initialize the video codec.
* the video codec.
* @throws ParserException If the initialization data could not be built. * @throws ParserException If the initialization data could not be built.
*/ */
private AvcSequenceHeaderData parseAvcCodecPrivate(ParsableByteArray buffer) private AvcSequenceHeaderData parseAvcCodecPrivate(ParsableByteArray buffer)
throws ParserException { throws ParserException {
try { // TODO: Deduplicate with AtomParsers.parseAvcCFromParent.
// TODO: Deduplicate with AtomParsers.parseAvcCFromParent. buffer.setPosition(4);
buffer.setPosition(4); int nalUnitLengthFieldLength = (buffer.readUnsignedByte() & 0x03) + 1;
int nalUnitLengthFieldLength = (buffer.readUnsignedByte() & 0x03) + 1; Assertions.checkState(nalUnitLengthFieldLength != 3);
Assertions.checkState(nalUnitLengthFieldLength != 3); List<byte[]> initializationData = new ArrayList<>();
List<byte[]> initializationData = new ArrayList<>(); int numSequenceParameterSets = buffer.readUnsignedByte() & 0x1F;
int numSequenceParameterSets = buffer.readUnsignedByte() & 0x1F; for (int i = 0; i < numSequenceParameterSets; i++) {
for (int i = 0; i < numSequenceParameterSets; i++) { initializationData.add(NalUnitUtil.parseChildNalUnit(buffer));
initializationData.add(NalUnitUtil.parseChildNalUnit(buffer)); }
} int numPictureParameterSets = buffer.readUnsignedByte();
int numPictureParameterSets = buffer.readUnsignedByte(); for (int j = 0; j < numPictureParameterSets; j++) {
for (int j = 0; j < numPictureParameterSets; j++) { initializationData.add(NalUnitUtil.parseChildNalUnit(buffer));
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, float pixelWidthAspectRatio = 1;
width, height, pixelWidthAspectRatio); int width = MediaFormat.NO_VALUE;
} catch (ArrayIndexOutOfBoundsException e) { int height = MediaFormat.NO_VALUE;
throw new ParserException("Error parsing AVC codec private"); 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);
} }
/** /**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment