From 4422e8a0155642a4708e1f50b1fb2c340c78f56d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 27 Oct 2015 18:23:00 +0000 Subject: [PATCH] Further cleanup to FLV extractor --- .../android/exoplayer/demo/Samples.java | 1 - .../extractor/flv/AudioTagPayloadReader.java | 15 +- .../exoplayer/extractor/flv/FlvExtractor.java | 282 ++++++++---------- .../extractor/flv/ScriptTagPayloadReader.java | 210 ++++++------- .../extractor/flv/TagPayloadReader.java | 13 +- .../extractor/flv/VideoTagPayloadReader.java | 107 +++---- 6 files changed, 290 insertions(+), 338 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index 0162e043aa..f82b0649ed 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -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() {} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagPayloadReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagPayloadReader.java index 8d13de415a..0aa42cded7 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagPayloadReader.java @@ -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; } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/FlvExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/FlvExtractor.java index ebf3efe8ef..d1c6c766bb 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/flv/FlvExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/FlvExtractor.java @@ -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; - } - - } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/ScriptTagPayloadReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/ScriptTagPayloadReader.java index 3c9bbd8c57..b28f422d67 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/flv/ScriptTagPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/ScriptTagPayloadReader.java @@ -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 metadata = (Map) readAmfData(data, type); + Map 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 readAmfStrictArray(ParsableByteArray data) { + int count = data.readUnsignedIntToInt(); + ArrayList 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 readAmfObject(ParsableByteArray data) { + HashMap 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 readAmfEcmaArray(ParsableByteArray data) { + int count = data.readUnsignedIntToInt(); + HashMap 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 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 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 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; - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagPayloadReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagPayloadReader.java index da259bb4b7..a4bbbd0123 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagPayloadReader.java @@ -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; } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagPayloadReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagPayloadReader.java index 620aa18e58..897913d647 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagPayloadReader.java @@ -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 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 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); } /**