Further cleanup to FLV extractor

This commit is contained in:
Oliver Woodman 2015-10-27 18:23:00 +00:00
parent f91ea9039d
commit 4422e8a015
6 changed files with 290 additions and 338 deletions

View file

@ -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() {}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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);
}
/**