Commit 4422e8a0 by Oliver Woodman

Further cleanup to FLV extractor

parent f91ea903
......@@ -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;
}
......
......@@ -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,48 +75,27 @@ 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) {
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.
*
* @param data The buffer from which to read.
* @return The value read from the buffer.
*/
private Boolean readAmfBoolean(ParsableByteArray data) {
private static Boolean readAmfBoolean(ParsableByteArray data) {
return data.readUnsignedByte() == 1;
}
......@@ -126,7 +105,7 @@ import java.util.Map;
* @param data The buffer from which to read.
* @return The value read from the buffer.
*/
private Double readAmfDouble(ParsableByteArray data) {
private static Double readAmfDouble(ParsableByteArray data) {
return Double.longBitsToDouble(data.readLong());
}
......@@ -136,7 +115,7 @@ import java.util.Map;
* @param data The buffer from which to read.
* @return The value read from the buffer.
*/
private String readAmfString(ParsableByteArray data) {
private static String readAmfString(ParsableByteArray data) {
int size = data.readUnsignedShort();
int position = data.getPosition();
data.skipBytes(size);
......@@ -149,9 +128,9 @@ import java.util.Map;
* @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<>();
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));
......@@ -165,7 +144,7 @@ import java.util.Map;
* @param data The buffer from which to read.
* @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<>();
while (true) {
String key = readAmfString(data);
......@@ -184,9 +163,9 @@ import java.util.Map;
* @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<>();
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);
......@@ -201,10 +180,31 @@ import java.util.Map;
* @param data The buffer from which to read.
* @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());
data.readUnsignedShort(); // Skip reserved bytes.
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);
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 @@
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;
}
// 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));
}
return new AvcSequenceHeaderData(initializationData, nalUnitLengthFieldLength,
width, height, pixelWidthAspectRatio);
} catch (ArrayIndexOutOfBoundsException e) {
throw new ParserException("Error parsing AVC codec private");
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);
}
/**
......
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