From 8ddc73511e0129f73b09f76d8028753c9b1f67d6 Mon Sep 17 00:00:00 2001 From: joli Date: Tue, 29 Sep 2015 03:30:57 +0200 Subject: [PATCH 1/2] FLV Support - Added Audio and Script Data readers --- .../android/exoplayer/demo/Samples.java | 3 + .../extractor/ExtractorSampleSource.java | 7 + .../extractor/flv/AudioTagReader.java | 150 +++++++++++ .../exoplayer/extractor/flv/FlvExtractor.java | 236 ++++++++++++++++++ .../extractor/flv/MetadataReader.java | 203 +++++++++++++++ .../exoplayer/extractor/flv/TagHeader.java | 15 ++ .../exoplayer/extractor/flv/TagReader.java | 75 ++++++ .../extractor/flv/VideoTagReader.java | 104 ++++++++ 8 files changed, 793 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagReader.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/flv/FlvExtractor.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/flv/MetadataReader.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/flv/TagHeader.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/flv/TagReader.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagReader.java 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 ef54b3ebc05..98c8bb2066e 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 @@ -145,6 +145,9 @@ public Sample(String name, String contentId, String uri, int type) { "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3", PlayerActivity.TYPE_OTHER), new Sample("Google Glass (WebM Video with Vorbis Audio)", "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", PlayerActivity.TYPE_OTHER), + new Sample("FLV Sample", + "http://master255.org/res/%D0%9A%D0%BB%D0%B8%D0%BF%D1%8B/B/Black%20Eyed%20Peas/black%20ey" + + "ed%20peas-My%20Humps.flv", PlayerActivity.TYPE_OTHER), }; private Samples() {} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java index 8f0bc4977b8..982894e4b53 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java @@ -146,6 +146,13 @@ public UnrecognizedInputFormatException(Extractor[] extractors) { } catch (ClassNotFoundException e) { // Extractor not found. } + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("com.google.android.exoplayer.extractor.flv.FlvExtractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } } private final ExtractorHolder extractorHolder; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagReader.java new file mode 100644 index 00000000000..bde1cbbdeaa --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagReader.java @@ -0,0 +1,150 @@ +package com.google.android.exoplayer.extractor.flv; + +import android.util.Log; +import android.util.Pair; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.extractor.TrackOutput; +import com.google.android.exoplayer.util.CodecSpecificDataUtil; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.util.Collections; + +/** + * Created by joliva on 9/27/15. + */ +public class AudioTagReader extends TagReader{ + + private static final String TAG = "AudioTagReader"; + + // Sound format + private static final int AUDIO_FORMAT_LINEAR_PCM_PLATFORM_ENDIAN = 0; + private static final int AUDIO_FORMAT_ADPCM = 1; + private static final int AUDIO_FORMAT_MP3 = 2; + private static final int AUDIO_FORMAT_LINEAR_PCM_LITTLE_ENDIAN = 3; + private static final int AUDIO_FORMAT_NELLYMOSER_16KHZ_MONO = 4; + private static final int AUDIO_FORMAT_NELLYMOSER_8KHZ_MONO = 5; + private static final int AUDIO_FORMAT_NELLYMOSER = 6; + private static final int AUDIO_FORMAT_G711_A_LAW = 7; + private static final int AUDIO_FORMAT_G711_MU_LAW = 8; + private static final int AUDIO_FORMAT_RESERVED = 9; + private static final int AUDIO_FORMAT_AAC = 10; + private static final int AUDIO_FORMAT_SPEEX = 11; + private static final int AUDIO_FORMAT_MP3_8KHZ = 14; + private static final int AUDIO_FORMAT_DEVICE_SPECIFIC = 15; + + // AAC PACKET TYPE + private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0; + private static final int AAC_PACKET_TYPE_AAC_RAW = 1; + + private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] { + 5500, 11000, 22000, 44000 + }; + + private int format; + private int sampleRate; + private int bitsPerSample; + private int channels; + + private boolean hasParsedAudioData; + private boolean hasOutputFormat; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + public AudioTagReader(TrackOutput output) { + super(output); + } + + @Override + public void seek() { + + } + + @Override + protected void parseHeader(ParsableByteArray data) throws UnsupportedTrack { + if (!hasParsedAudioData) { + int header = data.readUnsignedByte(); + int soundFormat = (header >> 4) & 0x0F; + int sampleRateIndex = (header >> 2) & 0x03; + int bitsPerSample = (header & 0x02) == 0x02 ? 16 : 8; + int channels = (header & 0x01) + 1; + + if (sampleRateIndex < 0 || sampleRateIndex >= AUDIO_SAMPLING_RATE_TABLE.length) { + throw new UnsupportedTrack("Invalid sample rate for the audio track"); + } + + if (!hasOutputFormat) { + switch (soundFormat) { + // raw audio data. Just creates media format + case AUDIO_FORMAT_LINEAR_PCM_LITTLE_ENDIAN: + output.format(MediaFormat.createAudioFormat(MimeTypes.AUDIO_RAW, MediaFormat.NO_VALUE, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, channels, + AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex], null, null)); + hasOutputFormat = true; + break; + + case AUDIO_FORMAT_AAC: + break; + + case AUDIO_FORMAT_MP3: + case AUDIO_FORMAT_MP3_8KHZ: + case AUDIO_FORMAT_LINEAR_PCM_PLATFORM_ENDIAN: + default: + throw new UnsupportedTrack("Audio track not supported. Format: " + soundFormat + + ", Sample rate: " + sampleRateIndex + ", bps: " + bitsPerSample + ", channels: " + + channels); + } + } + + this.format = soundFormat; + this.sampleRate = AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex]; + this.bitsPerSample = bitsPerSample; + this.channels = channels; + + hasParsedAudioData = true; + } else { + data.skipBytes(1); + } + } + + @Override + protected void parsePayload(ParsableByteArray data, long timeUs) { + int packetType = data.readUnsignedByte(); + if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { + ParsableBitArray adtsScratch = new ParsableBitArray(new byte[data.bytesLeft()]); + data.readBytes(adtsScratch.data, 0, data.bytesLeft()); + + int audioObjectType = adtsScratch.readBits(5); + int sampleRateIndex = adtsScratch.readBits(4); + int channelConfig = adtsScratch.readBits(4); + + byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAacAudioSpecificConfig( + audioObjectType, sampleRateIndex, channelConfig); + Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( + audioSpecificConfig); + + MediaFormat mediaFormat = MediaFormat.createAudioFormat(MimeTypes.AUDIO_AAC, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, durationUs, audioParams.second, + audioParams.first, Collections.singletonList(audioSpecificConfig), null); + + output.format(mediaFormat); + hasOutputFormat = true; + } else if (packetType == AAC_PACKET_TYPE_AAC_RAW) { + int bytesToWrite = data.bytesLeft(); + output.sampleData(data, bytesToWrite); + output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, bytesToWrite, 0, null); + + Log.d(TAG, "AAC TAG. Size: " + bytesToWrite + ", timeUs: " + timeUs); + } + } + + @Override + protected boolean shouldParsePayload() { + return (format == AUDIO_FORMAT_AAC); + } + +} 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 new file mode 100644 index 00000000000..e94be312b00 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/FlvExtractor.java @@ -0,0 +1,236 @@ +package com.google.android.exoplayer.extractor.flv; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.extractor.Extractor; +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.extractor.TrackOutput; +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; + +/** + * Created by joliva on 9/26/15. + */ +public final class FlvExtractor implements Extractor { + // Header sizes + private static final int FLV_MIN_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; + + // 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; + + private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); + + private final ParsableByteArray scratch; + private final ParsableByteArray headerBuffer; + private final ParsableByteArray tagHeaderBuffer; + private ParsableByteArray tagData; + + // Extractor outputs. + private ExtractorOutput extractorOutput; + private TrackOutput trackOutput; + + private boolean hasAudio; + private boolean hasVideo; + private int dataOffset; + + private int parserState; + private TagHeader currentTagHeader; + + private AudioTagReader audioReader; + private VideoTagReader videoReader; + private MetadataReader metadataReader; + + public FlvExtractor() { + scratch = new ParsableByteArray(4); + headerBuffer = new ParsableByteArray(FLV_MIN_HEADER_SIZE); + tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); + dataOffset = 0; + hasAudio = false; + hasVideo = false; + currentTagHeader = new TagHeader(); + } + + @Override + public void init(ExtractorOutput output) { + this.extractorOutput = output; + trackOutput = extractorOutput.track(0); + extractorOutput.endTracks(); + + output.seekMap(SeekMap.UNSEEKABLE); + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + // Check if file starts with "FLV" tag + input.peekFully(scratch.data, 0, 3); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != FLV_TAG) { + return false; + } +/* + + // Checking reserved flags are set to 0 + input.peekFully(scratch.data, 0, 2); + scratch.setPosition(0); + if ((scratch.readUnsignedShort() & 0xFA) != 0) { + return false; + } + + // Read data offset + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + int dataOffset = scratch.readInt(); + + input.resetPeekPosition(); + input.advancePeekPosition(dataOffset); + + // Checking first "previous tag size" is set to 0 + input.peekFully(scratch.data, 0, 1); + scratch.setPosition(0); + if (scratch.readInt() != 0) { + return false; + } +*/ + return true; + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, + InterruptedException { + if (dataOffset == 0 + && !readHeader(input)) { + return RESULT_END_OF_INPUT; + } + + try { + while (true) { + switch (parserState) { + case STATE_READING_TAG_HEADER: + if (!readTagHeader(input)) { + return RESULT_END_OF_INPUT; + } + break; + + default: + return readSample(input, seekPosition); + } + } + } catch (AudioTagReader.UnsupportedTrack unsupportedTrack) { + unsupportedTrack.printStackTrace(); + return RESULT_END_OF_INPUT; + + } + } + + @Override + public void seek() { + dataOffset = 0; + } + + 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(); + hasAudio = (flags & 0x04) != 0; + hasVideo = (flags & 0x01) != 0; + + if (hasAudio) { + audioReader = new AudioTagReader(trackOutput); + } + if (hasVideo) { + //videoReader = new VideoTagReader(trackOutput); + } + metadataReader = new MetadataReader(trackOutput); + + dataOffset = headerBuffer.readInt(); + + input.skipFully(dataOffset - FLV_MIN_HEADER_SIZE); + parserState = STATE_READING_TAG_HEADER; + } catch (EOFException eof) { + return false; + } + + return true; + } + + private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException, + TagReader.UnsupportedTrack { + try { + input.skipFully(4); + input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE); + + tagHeaderBuffer.setPosition(0); + // skipping previous tag size field. + 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; + + Assertions.checkState(dataSize <= Integer.MAX_VALUE); + tagData = new ParsableByteArray((int) dataSize); + parserState = STATE_READING_SAMPLE; + + } catch (EOFException eof) { + return false; + } + + return true; + } + + private int readSample(ExtractorInput input, PositionHolder seekPosition) throws IOException, + InterruptedException, AudioTagReader.UnsupportedTrack { + 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; + } + + 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.durationUs != C.UNKNOWN_TIME_US) { + if (audioReader != null) { + audioReader.durationUs = metadataReader.durationUs; + } + if (videoReader != null) { + videoReader.durationUs = metadataReader.durationUs; + } + } + } else { + tagData.reset(); + } + + parserState = STATE_READING_TAG_HEADER; + + return RESULT_CONTINUE; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/MetadataReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/MetadataReader.java new file mode 100644 index 00000000000..aac3ff6a4c4 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/MetadataReader.java @@ -0,0 +1,203 @@ +package com.google.android.exoplayer.extractor.flv; + +import android.util.Log; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.extractor.TrackOutput; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * Created by joliva on 9/28/15. + */ +public class MetadataReader extends TagReader{ + + private static final int METADATA_TYPE_UNKNOWN = -1; + private static final int METADATA_TYPE_NUMBER = 0; + private static final int METADATA_TYPE_BOOLEAN = 1; + private static final int METADATA_TYPE_STRING = 2; + private static final int METADATA_TYPE_OBJECT = 3; + private static final int METADATA_TYPE_MOVIE_CLIP = 4; + private static final int METADATA_TYPE_NULL = 5; + private static final int METADATA_TYPE_UNDEFINED = 6; + private static final int METADATA_TYPE_REFERENCE = 7; + private static final int METADATA_TYPE_ECMA_ARRAY = 8; + private static final int METADATA_TYPE_STRICT_ARRAY = 10; + private static final int METADATA_TYPE_DATE = 11; + private static final int METADATA_TYPE_LONG_STRING = 12; + + public long startTime = C.UNKNOWN_TIME_US; + public float frameRate; + public float videoDataRate; + public float audioDataRate; + public int height; + public int width; + public boolean canSeekOnTime; + public String httpHostHeader; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + public MetadataReader(TrackOutput output) { + super(output); + } + + @Override + public void seek() { + + } + + @Override + protected void parseHeader(ParsableByteArray data) throws UnsupportedTrack { + + } + + @Override + protected void parsePayload(ParsableByteArray data, long timeUs) { + Object messageName = readAMFData(data, METADATA_TYPE_UNKNOWN); + Object obj = readAMFData(data, METADATA_TYPE_UNKNOWN); + + if(obj instanceof Map) { + Map extractedMetadata = (Map) obj; + for (Map.Entry entry : extractedMetadata.entrySet()) { + if (entry.getValue() == null) { + continue; + } + Log.d("Metadata", "Key: " + entry.getKey() + ", Value: " + entry.getValue().toString()); + + switch (entry.getKey()) { + case "totalduration": + this.durationUs = (long)(C.MICROS_PER_SECOND * (Double)(entry.getValue())); + break; + + case "starttime": + this.startTime = (long)(C.MICROS_PER_SECOND * (Double)(entry.getValue())); + break; + + case "videodatarate": + this.videoDataRate = ((Double)entry.getValue()).floatValue(); + break; + + case "audiodatarate": + this.audioDataRate = ((Double)entry.getValue()).floatValue(); + break; + + case "framerate": + this.frameRate = ((Double)entry.getValue()).floatValue(); + break; + + case "width": + this.width = Math.round(((Double) entry.getValue()).floatValue()); + break; + + case "height": + this.height = Math.round(((Double) entry.getValue()).floatValue()); + break; + + case "canseekontime": + this.canSeekOnTime = (boolean) entry.getValue(); + break; + + case "httphostheader": + this.httpHostHeader = (String) entry.getValue(); + break; + + default: + break; + } + } + } + } + + @Override + protected boolean shouldParsePayload() { + return true; + } + + private Object readAMFData(ParsableByteArray data, int type) { + if (type == METADATA_TYPE_UNKNOWN) { + type = data.readUnsignedByte(); + } + byte [] b; + switch (type) { + case METADATA_TYPE_NUMBER: + return readAMFDouble(data); + case METADATA_TYPE_BOOLEAN: + return readAMFBoolean(data); + case METADATA_TYPE_STRING: + return readAMFString(data); + case METADATA_TYPE_OBJECT: + return readAMFObject(data); + case METADATA_TYPE_ECMA_ARRAY: + return readAMFEcmaArray(data); + case METADATA_TYPE_STRICT_ARRAY: + return readAMFStrictArray(data); + case METADATA_TYPE_DATE: + return readAMFDouble(data); + default: + return null; + } + } + + private Boolean readAMFBoolean(ParsableByteArray data) { + return Boolean.valueOf(data.readUnsignedByte() == 1); + } + + private Double readAMFDouble(ParsableByteArray data) { + byte []b = new byte[8]; + data.readBytes(b, 0, b.length); + return ByteBuffer.wrap(b).getDouble(); + } + + private String readAMFString(ParsableByteArray data) { + int size = data.readUnsignedShort(); + byte []b = new byte[size]; + data.readBytes(b, 0, b.length); + return new String(b); + } + + private Object readAMFStrictArray(ParsableByteArray data) { + long count = data.readUnsignedInt(); + ArrayList list = new ArrayList(); + for (int i = 0; i < count; i++) { + list.add(readAMFData(data, METADATA_TYPE_UNKNOWN)); + } + return list; + } + + private Object readAMFObject(ParsableByteArray data) { + HashMap array = new HashMap(); + while (true) { + String key = readAMFString(data); + int type = data.readUnsignedByte(); + if (type == 9) { // object end marker + break; + } + array.put(key, readAMFData(data, type)); + } + return array; + } + + 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 = data.readUnsignedByte(); + array.put(key, readAMFData(data, type)); + } + return array; + } + + private Date readAMFDate(ParsableByteArray data) { + final Date date = new Date((long) readAMFDouble(data).doubleValue()); + data.readUnsignedShort(); + return date; + } +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagHeader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagHeader.java new file mode 100644 index 00000000000..be0f9ef1a84 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagHeader.java @@ -0,0 +1,15 @@ +package com.google.android.exoplayer.extractor.flv; + +/** + * Created by joliva on 9/26/15. + */ +final class TagHeader { + public static final int TAG_TYPE_AUDIO = 8; + public static final int TAG_TYPE_VIDEO = 9; + public static final int TAG_TYPE_SCRIPT_DATA = 18; + + 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/TagReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagReader.java new file mode 100644 index 00000000000..9df4d8416a7 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagReader.java @@ -0,0 +1,75 @@ +package com.google.android.exoplayer.extractor.flv; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.extractor.TrackOutput; +import com.google.android.exoplayer.util.ParsableByteArray; + +/** + * Extracts individual samples from FLV tags. + */ +/* package */ abstract class TagReader { + + protected final TrackOutput output; + public long durationUs; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + protected TagReader(TrackOutput output) { + this.output = output; + this.durationUs = C.UNKNOWN_TIME_US; + } + + /** + * Notifies the reader that a seek has occurred. + *

+ * Following a call to this method, the data passed to the next invocation of + * {@link #consume(ParsableByteArray, long)} will not be a continuation of the data that + * was previously passed. Hence the reader should reset any internal state. + */ + public abstract void seek(); + + /** + * Parses tag header + * @param data Buffer where the tag header is stored + */ + protected abstract void parseHeader(ParsableByteArray data) throws UnsupportedTrack; + + /** + * Parses tag payload + * @param data Buffer where tag payload is stored + * @param timeUs Time position of the frame + */ + protected abstract void parsePayload(ParsableByteArray data, long timeUs); + + /** + * Evaluate if for the current tag, payload should be parsed + * @return + */ + protected abstract boolean shouldParsePayload(); + + /** + * Consumes (possibly partial) payload data. + * + * @param data The payload data to consume. + * @param timeUs The timestamp associated with the payload. + */ + public void consume(ParsableByteArray data, long timeUs) throws UnsupportedTrack { + parseHeader(data); + + if (shouldParsePayload()) { + parsePayload(data, timeUs); + } + } + + /** + * Thrown when format described in the AudioTrack is not supported + */ + public static final class UnsupportedTrack extends Exception { + + public UnsupportedTrack(String msg) { + super(msg); + } + + } +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagReader.java new file mode 100644 index 00000000000..3b2496e4a50 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagReader.java @@ -0,0 +1,104 @@ +package com.google.android.exoplayer.extractor.flv; + +import android.util.Log; +import android.util.Pair; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.extractor.TrackOutput; +import com.google.android.exoplayer.util.CodecSpecificDataUtil; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.util.Collections; + +/** + * Created by joliva on 9/27/15. + */ +public class VideoTagReader extends TagReader{ + private static final String TAG = "VideoTagReader"; + + // Video codec + private static final int VIDEO_CODEC_JPEG = 1; + private static final int VIDEO_CODEC_H263 = 2; + private static final int VIDEO_CODEC_SCREEN_VIDEO = 3; + private static final int VIDEO_CODEC_VP6 = 4; + private static final int VIDEO_CODEC_VP6_WITH_ALPHA_CHANNEL = 5; + private static final int VIDEO_CODEC_SCREEN_VIDEO_V2 = 6; + private static final int VIDEO_CODEC_AVC = 7; + + // FRAME TYPE + private static final int VIDEO_FRAME_KEYFRAME = 1; + private static final int VIDEO_FRAME_INTERFRAME = 2; + private static final int VIDEO_FRAME_DISPOSABLE_INTERFRAME = 3; + private static final int VIDEO_FRAME_GENERATED_KEYFRAME = 4; + private static final int VIDEO_FRAME_VIDEO_INFO = 5; + + // PACKET TYPE + 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; + + private boolean hasOutputFormat; + private int format; + private int frameType; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + public VideoTagReader(TrackOutput output) { + super(output); + } + + @Override + public void seek() { + + } + + @Override + protected void parseHeader(ParsableByteArray data) throws UnsupportedTrack { + int header = data.readUnsignedByte(); + int frameType = (header >> 4) & 0x0F; + int videoCodec = (header & 0x0F); + + if (videoCodec != VIDEO_CODEC_AVC) { + throw new UnsupportedTrack("Video codec not supported. Codec: " + videoCodec); + } + + this.format = videoCodec; + this.frameType = frameType; + } + + @Override + protected void parsePayload(ParsableByteArray data, long timeUs) { + int packetType = data.readUnsignedByte(); + int compositionTime = data.readUnsignedInt24(); + if (packetType == AVC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { + ParsableBitArray videoSequence = new ParsableBitArray(new byte[data.bytesLeft()]); + data.readBytes(videoSequence.data, 0, data.bytesLeft()); + + +/* + // Construct and output the format. + output.format(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, + MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US, parsedSpsData.width, parsedSpsData.height, + initializationData, MediaFormat.NO_VALUE, parsedSpsData.pixelWidthAspectRatio)); +*/ +// output.format(mediaFormat); + hasOutputFormat = true; + } else if (packetType == AVC_PACKET_TYPE_AVC_NALU) { + int bytesToWrite = data.bytesLeft(); + output.sampleData(data, bytesToWrite); + output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.SAMPLE_FLAG_SYNC : 0, + bytesToWrite, 0, null); + + Log.d(TAG, "AAC TAG. Size: " + bytesToWrite + ", timeUs: " + timeUs); + } + } + + @Override + protected boolean shouldParsePayload() { + return (format == VIDEO_CODEC_AVC && frameType != VIDEO_FRAME_VIDEO_INFO); + } +} From 3e36f529f8fe8ef4c3550dc00f16684c532d0da0 Mon Sep 17 00:00:00 2001 From: joli Date: Tue, 29 Sep 2015 16:20:41 +0200 Subject: [PATCH 2/2] FLV Support - Added Video Reader and parsing improvements --- .../android/exoplayer/demo/Samples.java | 6 +- .../extractor/ExtractorSampleSource.java | 3 +- ...Reader.java => AudioTagPayloadReader.java} | 103 +++----- .../exoplayer/extractor/flv/FlvExtractor.java | 169 +++++++++---- .../extractor/flv/MetadataReader.java | 203 ---------------- .../extractor/flv/ScriptTagPayloadReader.java | 201 ++++++++++++++++ .../exoplayer/extractor/flv/TagHeader.java | 15 -- .../{TagReader.java => TagPayloadReader.java} | 53 +++-- .../extractor/flv/VideoTagPayloadReader.java | 224 ++++++++++++++++++ .../extractor/flv/VideoTagReader.java | 104 -------- 10 files changed, 624 insertions(+), 457 deletions(-) rename library/src/main/java/com/google/android/exoplayer/extractor/flv/{AudioTagReader.java => AudioTagPayloadReader.java} (52%) delete mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/flv/MetadataReader.java create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/flv/ScriptTagPayloadReader.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/flv/TagHeader.java rename library/src/main/java/com/google/android/exoplayer/extractor/flv/{TagReader.java => TagPayloadReader.java} (53%) create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagPayloadReader.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagReader.java 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 98c8bb2066e..8560216d8b5 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 @@ -145,9 +145,9 @@ public Sample(String name, String contentId, String uri, int type) { "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3", PlayerActivity.TYPE_OTHER), new Sample("Google Glass (WebM Video with Vorbis Audio)", "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", PlayerActivity.TYPE_OTHER), - new Sample("FLV Sample", - "http://master255.org/res/%D0%9A%D0%BB%D0%B8%D0%BF%D1%8B/B/Black%20Eyed%20Peas/black%20ey" - + "ed%20peas-My%20Humps.flv", 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/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java index 982894e4b53..e13bb16422a 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java @@ -58,9 +58,10 @@ *

  • MP3 ({@link com.google.android.exoplayer.extractor.mp3.Mp3Extractor})
  • *
  • AAC ({@link com.google.android.exoplayer.extractor.ts.AdtsExtractor})
  • *
  • MPEG TS ({@link com.google.android.exoplayer.extractor.ts.TsExtractor}
  • + *
  • FLV ({@link com.google.android.exoplayer.extractor.flv.FlvExtractor}
  • * * - *

    Seeking in AAC and MPEG TS streams is not supported. + *

    Seeking in AAC, MPEG TS and FLV streams is not supported. * *

    To override the default extractors, pass one or more {@link Extractor} instances to the * constructor. When reading a new stream, the first {@link Extractor} that returns {@code true} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagPayloadReader.java similarity index 52% rename from library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagReader.java rename to library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagPayloadReader.java index bde1cbbdeaa..470a2eaf3f6 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagPayloadReader.java @@ -1,8 +1,21 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer.extractor.flv; -import android.util.Log; import android.util.Pair; - import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.extractor.TrackOutput; @@ -14,48 +27,27 @@ import java.util.Collections; /** - * Created by joliva on 9/27/15. + * Parses audio tags of from an FLV stream and extracts AAC frames. */ -public class AudioTagReader extends TagReader{ - - private static final String TAG = "AudioTagReader"; - +final class AudioTagPayloadReader extends TagPayloadReader { // Sound format - private static final int AUDIO_FORMAT_LINEAR_PCM_PLATFORM_ENDIAN = 0; - private static final int AUDIO_FORMAT_ADPCM = 1; - private static final int AUDIO_FORMAT_MP3 = 2; - private static final int AUDIO_FORMAT_LINEAR_PCM_LITTLE_ENDIAN = 3; - private static final int AUDIO_FORMAT_NELLYMOSER_16KHZ_MONO = 4; - private static final int AUDIO_FORMAT_NELLYMOSER_8KHZ_MONO = 5; - private static final int AUDIO_FORMAT_NELLYMOSER = 6; - private static final int AUDIO_FORMAT_G711_A_LAW = 7; - private static final int AUDIO_FORMAT_G711_MU_LAW = 8; - private static final int AUDIO_FORMAT_RESERVED = 9; private static final int AUDIO_FORMAT_AAC = 10; - private static final int AUDIO_FORMAT_SPEEX = 11; - private static final int AUDIO_FORMAT_MP3_8KHZ = 14; - private static final int AUDIO_FORMAT_DEVICE_SPECIFIC = 15; // AAC PACKET TYPE private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0; private static final int AAC_PACKET_TYPE_AAC_RAW = 1; + // SAMPLING RATES private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] { 5500, 11000, 22000, 44000 }; - private int format; - private int sampleRate; - private int bitsPerSample; - private int channels; - - private boolean hasParsedAudioData; + // State variables + private boolean hasParsedAudioDataHeader; private boolean hasOutputFormat; - /** - * @param output A {@link TrackOutput} to which samples should be written. - */ - public AudioTagReader(TrackOutput output) { + + public AudioTagPayloadReader(TrackOutput output) { super(output); } @@ -65,8 +57,10 @@ public void seek() { } @Override - protected void parseHeader(ParsableByteArray data) throws UnsupportedTrack { - if (!hasParsedAudioData) { + protected boolean parseHeader(ParsableByteArray data) throws UnsupportedTrack { + // 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 soundFormat = (header >> 4) & 0x0F; int sampleRateIndex = (header >> 2) & 0x03; @@ -78,42 +72,29 @@ protected void parseHeader(ParsableByteArray data) throws UnsupportedTrack { } if (!hasOutputFormat) { - switch (soundFormat) { - // raw audio data. Just creates media format - case AUDIO_FORMAT_LINEAR_PCM_LITTLE_ENDIAN: - output.format(MediaFormat.createAudioFormat(MimeTypes.AUDIO_RAW, MediaFormat.NO_VALUE, - MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, channels, - AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex], null, null)); - hasOutputFormat = true; - break; - - case AUDIO_FORMAT_AAC: - break; - - case AUDIO_FORMAT_MP3: - case AUDIO_FORMAT_MP3_8KHZ: - case AUDIO_FORMAT_LINEAR_PCM_PLATFORM_ENDIAN: - default: - throw new UnsupportedTrack("Audio track not supported. Format: " + soundFormat + - ", Sample rate: " + sampleRateIndex + ", bps: " + bitsPerSample + ", channels: " + - channels); + // TODO: Adds support for MP3 and PCM + if (soundFormat != AUDIO_FORMAT_AAC) { + throw new UnsupportedTrack("Audio track not supported. Format: " + soundFormat + + ", Sample rate: " + sampleRateIndex + ", bps: " + bitsPerSample + ", channels: " + + channels); } } - this.format = soundFormat; - this.sampleRate = AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex]; - this.bitsPerSample = bitsPerSample; - this.channels = channels; - - hasParsedAudioData = true; + 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; } @Override protected void parsePayload(ParsableByteArray data, long timeUs) { int packetType = data.readUnsignedByte(); + // Parse sequence header just in case it was not done before. if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { ParsableBitArray adtsScratch = new ParsableBitArray(new byte[data.bytesLeft()]); data.readBytes(adtsScratch.data, 0, data.bytesLeft()); @@ -134,17 +115,11 @@ protected void parsePayload(ParsableByteArray data, long timeUs) { output.format(mediaFormat); hasOutputFormat = true; } else if (packetType == AAC_PACKET_TYPE_AAC_RAW) { + // Sample audio AAC frames int bytesToWrite = data.bytesLeft(); output.sampleData(data, bytesToWrite); output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, bytesToWrite, 0, null); - - Log.d(TAG, "AAC TAG. Size: " + bytesToWrite + ", timeUs: " + timeUs); } } - @Override - protected boolean shouldParsePayload() { - return (format == AUDIO_FORMAT_AAC); - } - } 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 e94be312b00..563361d97d5 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 @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer.extractor.flv; import com.google.android.exoplayer.C; @@ -6,7 +21,6 @@ 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.extractor.TrackOutput; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.Util; @@ -15,9 +29,9 @@ import java.io.IOException; /** - * Created by joliva on 9/26/15. + * Facilitates the extraction of data from the FLV container format. */ -public final class FlvExtractor implements Extractor { +public final class FlvExtractor implements Extractor, SeekMap { // Header sizes private static final int FLV_MIN_HEADER_SIZE = 9; private static final int FLV_TAG_HEADER_SIZE = 11; @@ -31,8 +45,10 @@ public final class FlvExtractor implements Extractor { private static final int TAG_TYPE_VIDEO = 9; private static final int TAG_TYPE_SCRIPT_DATA = 18; + // FLV container identifier private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); + // Temporary buffers private final ParsableByteArray scratch; private final ParsableByteArray headerBuffer; private final ParsableByteArray tagHeaderBuffer; @@ -40,36 +56,28 @@ public final class FlvExtractor implements Extractor { // Extractor outputs. private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; - - private boolean hasAudio; - private boolean hasVideo; - private int dataOffset; + // State variables. private int parserState; + private int dataOffset; private TagHeader currentTagHeader; - private AudioTagReader audioReader; - private VideoTagReader videoReader; - private MetadataReader metadataReader; + // Tags readers + private AudioTagPayloadReader audioReader; + private VideoTagPayloadReader videoReader; + private ScriptTagPayloadReader metadataReader; public FlvExtractor() { scratch = new ParsableByteArray(4); headerBuffer = new ParsableByteArray(FLV_MIN_HEADER_SIZE); tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); dataOffset = 0; - hasAudio = false; - hasVideo = false; currentTagHeader = new TagHeader(); } @Override public void init(ExtractorOutput output) { this.extractorOutput = output; - trackOutput = extractorOutput.track(0); - extractorOutput.endTracks(); - - output.seekMap(SeekMap.UNSEEKABLE); } @Override @@ -80,7 +88,6 @@ public boolean sniff(ExtractorInput input) throws IOException, InterruptedExcept if (scratch.readUnsignedInt24() != FLV_TAG) { return false; } -/* // Checking reserved flags are set to 0 input.peekFully(scratch.data, 0, 2); @@ -98,13 +105,10 @@ public boolean sniff(ExtractorInput input) throws IOException, InterruptedExcept input.advancePeekPosition(dataOffset); // Checking first "previous tag size" is set to 0 - input.peekFully(scratch.data, 0, 1); + input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); - if (scratch.readInt() != 0) { - return false; - } -*/ - return true; + + return scratch.readInt() == 0; } @Override @@ -117,21 +121,17 @@ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOExce try { while (true) { - switch (parserState) { - case STATE_READING_TAG_HEADER: - if (!readTagHeader(input)) { - return RESULT_END_OF_INPUT; - } - break; - - default: - return readSample(input, seekPosition); + if (parserState == STATE_READING_TAG_HEADER) { + if (!readTagHeader(input)) { + return RESULT_END_OF_INPUT; + } + } else { + return readSample(input); } } - } catch (AudioTagReader.UnsupportedTrack unsupportedTrack) { + } catch (AudioTagPayloadReader.UnsupportedTrack unsupportedTrack) { unsupportedTrack.printStackTrace(); - return RESULT_END_OF_INPUT; - + return RESULT_END_OF_INPUT; } } @@ -140,23 +140,35 @@ public void seek() { dataOffset = 0; } + /** + * Reads 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. + * @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(); - hasAudio = (flags & 0x04) != 0; - hasVideo = (flags & 0x01) != 0; + boolean hasAudio = (flags & 0x04) != 0; + boolean hasVideo = (flags & 0x01) != 0; - if (hasAudio) { - audioReader = new AudioTagReader(trackOutput); + 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 (hasVideo) { - //videoReader = new VideoTagReader(trackOutput); + if (metadataReader == null) { + metadataReader = new ScriptTagPayloadReader(null); } - metadataReader = new MetadataReader(trackOutput); + 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); @@ -168,14 +180,25 @@ private boolean readHeader(ExtractorInput input) throws IOException, Interrupted return true; } + /** + * 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 InterruptedException If the thread was interrupted. + * @throws TagPayloadReader.UnsupportedTrack If payload of the tag is using a codec non + * supported codec. + */ private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException, - TagReader.UnsupportedTrack { + TagPayloadReader.UnsupportedTrack { 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); - // skipping previous tag size field. int type = tagHeaderBuffer.readUnsignedByte(); int dataSize = tagHeaderBuffer.readUnsignedInt24(); long timestamp = tagHeaderBuffer.readUnsignedInt24(); @@ -187,8 +210,16 @@ private boolean readTagHeader(ExtractorInput input) throws IOException, Interrup currentTagHeader.timestamp = timestamp * 1000; currentTagHeader.streamId = streamId; - Assertions.checkState(dataSize <= Integer.MAX_VALUE); - tagData = new ParsableByteArray((int) dataSize); + // 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) { @@ -198,8 +229,17 @@ private boolean readTagHeader(ExtractorInput input) throws IOException, Interrup return true; } - private int readSample(ExtractorInput input, PositionHolder seekPosition) throws IOException, - InterruptedException, AudioTagReader.UnsupportedTrack { + /** + * Reads payload of an FLV 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. + * @throws InterruptedException If the thread was interrupted. + * @throws TagPayloadReader.UnsupportedTrack If payload of the tag is using a codec non + * supported codec. + */ + private int readSample(ExtractorInput input) throws IOException, + InterruptedException, AudioTagPayloadReader.UnsupportedTrack { if (tagData != null) { if (!input.readFully(tagData.data, 0, currentTagHeader.dataSize, true)) { return RESULT_END_OF_INPUT; @@ -210,18 +250,19 @@ private int readSample(ExtractorInput input, PositionHolder seekPosition) throws 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.durationUs != C.UNKNOWN_TIME_US) { + if (metadataReader.getDurationUs() != C.UNKNOWN_TIME_US) { if (audioReader != null) { - audioReader.durationUs = metadataReader.durationUs; + audioReader.setDurationUs(metadataReader.getDurationUs()); } if (videoReader != null) { - videoReader.durationUs = metadataReader.durationUs; + videoReader.setDurationUs(metadataReader.getDurationUs()); } } } else { @@ -233,4 +274,28 @@ private int readSample(ExtractorInput input, PositionHolder seekPosition) throws return RESULT_CONTINUE; } + // SeekMap implementation. + // TODO: Add seeking support + @Override + public boolean isSeekable() { + return false; + } + + @Override + public long getPosition(long timeUs) { + 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/MetadataReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/MetadataReader.java deleted file mode 100644 index aac3ff6a4c4..00000000000 --- a/library/src/main/java/com/google/android/exoplayer/extractor/flv/MetadataReader.java +++ /dev/null @@ -1,203 +0,0 @@ -package com.google.android.exoplayer.extractor.flv; - -import android.util.Log; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.extractor.TrackOutput; -import com.google.android.exoplayer.util.ParsableBitArray; -import com.google.android.exoplayer.util.ParsableByteArray; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -/** - * Created by joliva on 9/28/15. - */ -public class MetadataReader extends TagReader{ - - private static final int METADATA_TYPE_UNKNOWN = -1; - private static final int METADATA_TYPE_NUMBER = 0; - private static final int METADATA_TYPE_BOOLEAN = 1; - private static final int METADATA_TYPE_STRING = 2; - private static final int METADATA_TYPE_OBJECT = 3; - private static final int METADATA_TYPE_MOVIE_CLIP = 4; - private static final int METADATA_TYPE_NULL = 5; - private static final int METADATA_TYPE_UNDEFINED = 6; - private static final int METADATA_TYPE_REFERENCE = 7; - private static final int METADATA_TYPE_ECMA_ARRAY = 8; - private static final int METADATA_TYPE_STRICT_ARRAY = 10; - private static final int METADATA_TYPE_DATE = 11; - private static final int METADATA_TYPE_LONG_STRING = 12; - - public long startTime = C.UNKNOWN_TIME_US; - public float frameRate; - public float videoDataRate; - public float audioDataRate; - public int height; - public int width; - public boolean canSeekOnTime; - public String httpHostHeader; - - /** - * @param output A {@link TrackOutput} to which samples should be written. - */ - public MetadataReader(TrackOutput output) { - super(output); - } - - @Override - public void seek() { - - } - - @Override - protected void parseHeader(ParsableByteArray data) throws UnsupportedTrack { - - } - - @Override - protected void parsePayload(ParsableByteArray data, long timeUs) { - Object messageName = readAMFData(data, METADATA_TYPE_UNKNOWN); - Object obj = readAMFData(data, METADATA_TYPE_UNKNOWN); - - if(obj instanceof Map) { - Map extractedMetadata = (Map) obj; - for (Map.Entry entry : extractedMetadata.entrySet()) { - if (entry.getValue() == null) { - continue; - } - Log.d("Metadata", "Key: " + entry.getKey() + ", Value: " + entry.getValue().toString()); - - switch (entry.getKey()) { - case "totalduration": - this.durationUs = (long)(C.MICROS_PER_SECOND * (Double)(entry.getValue())); - break; - - case "starttime": - this.startTime = (long)(C.MICROS_PER_SECOND * (Double)(entry.getValue())); - break; - - case "videodatarate": - this.videoDataRate = ((Double)entry.getValue()).floatValue(); - break; - - case "audiodatarate": - this.audioDataRate = ((Double)entry.getValue()).floatValue(); - break; - - case "framerate": - this.frameRate = ((Double)entry.getValue()).floatValue(); - break; - - case "width": - this.width = Math.round(((Double) entry.getValue()).floatValue()); - break; - - case "height": - this.height = Math.round(((Double) entry.getValue()).floatValue()); - break; - - case "canseekontime": - this.canSeekOnTime = (boolean) entry.getValue(); - break; - - case "httphostheader": - this.httpHostHeader = (String) entry.getValue(); - break; - - default: - break; - } - } - } - } - - @Override - protected boolean shouldParsePayload() { - return true; - } - - private Object readAMFData(ParsableByteArray data, int type) { - if (type == METADATA_TYPE_UNKNOWN) { - type = data.readUnsignedByte(); - } - byte [] b; - switch (type) { - case METADATA_TYPE_NUMBER: - return readAMFDouble(data); - case METADATA_TYPE_BOOLEAN: - return readAMFBoolean(data); - case METADATA_TYPE_STRING: - return readAMFString(data); - case METADATA_TYPE_OBJECT: - return readAMFObject(data); - case METADATA_TYPE_ECMA_ARRAY: - return readAMFEcmaArray(data); - case METADATA_TYPE_STRICT_ARRAY: - return readAMFStrictArray(data); - case METADATA_TYPE_DATE: - return readAMFDouble(data); - default: - return null; - } - } - - private Boolean readAMFBoolean(ParsableByteArray data) { - return Boolean.valueOf(data.readUnsignedByte() == 1); - } - - private Double readAMFDouble(ParsableByteArray data) { - byte []b = new byte[8]; - data.readBytes(b, 0, b.length); - return ByteBuffer.wrap(b).getDouble(); - } - - private String readAMFString(ParsableByteArray data) { - int size = data.readUnsignedShort(); - byte []b = new byte[size]; - data.readBytes(b, 0, b.length); - return new String(b); - } - - private Object readAMFStrictArray(ParsableByteArray data) { - long count = data.readUnsignedInt(); - ArrayList list = new ArrayList(); - for (int i = 0; i < count; i++) { - list.add(readAMFData(data, METADATA_TYPE_UNKNOWN)); - } - return list; - } - - private Object readAMFObject(ParsableByteArray data) { - HashMap array = new HashMap(); - while (true) { - String key = readAMFString(data); - int type = data.readUnsignedByte(); - if (type == 9) { // object end marker - break; - } - array.put(key, readAMFData(data, type)); - } - return array; - } - - 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 = data.readUnsignedByte(); - array.put(key, readAMFData(data, type)); - } - return array; - } - - private Date readAMFDate(ParsableByteArray data) { - final Date date = new Date((long) readAMFDouble(data).doubleValue()); - data.readUnsignedShort(); - return date; - } -} 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 new file mode 100644 index 00000000000..d11185207a9 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/ScriptTagPayloadReader.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.extractor.flv; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.extractor.TrackOutput; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * Parses Script Data tags from an FLV stream and extracts metadata information. + */ +final class ScriptTagPayloadReader extends TagPayloadReader { + + // AMF object types + private static final int AMF_TYPE_UNKNOWN = -1; + private static final int AMF_TYPE_NUMBER = 0; + private static final int AMF_TYPE_BOOLEAN = 1; + private static final int AMF_TYPE_STRING = 2; + private static final int AMF_TYPE_OBJECT = 3; + private static final int AMF_TYPE_ECMA_ARRAY = 8; + private static final int AMF_TYPE_END_MARKER = 9; + private static final int AMF_TYPE_STRICT_ARRAY = 10; + private static final int AMF_TYPE_DATE = 11; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + public ScriptTagPayloadReader(TrackOutput output) { + super(output); + } + + @Override + public void seek() { + + } + + @Override + protected boolean parseHeader(ParsableByteArray data) throws UnsupportedTrack { + return true; + } + + @SuppressWarnings("unchecked") + @Override + protected void parsePayload(ParsableByteArray data, long timeUs) { + // Read message name (don't storing it as we are not going to give it any use) + readAMFData(data, AMF_TYPE_UNKNOWN); + Object obj = readAMFData(data, AMF_TYPE_UNKNOWN); + + if (obj instanceof Map) { + Map extractedMetadata = (Map) obj; + for (Map.Entry entry : extractedMetadata.entrySet()) { + if (entry.getValue() == null) { + continue; + } + + switch (entry.getKey()) { + case "duration": + this.durationUs = (long)(C.MICROS_PER_SECOND * (Double)(entry.getValue())); + break; + + default: + break; + } + } + } + } + + private Object readAMFData(ParsableByteArray data, int type) { + if (type == AMF_TYPE_UNKNOWN) { + type = data.readUnsignedByte(); + } + 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 Buffer + * @return Boolean 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 Buffer + * @return Double number read from the buffer + */ + private Double readAMFDouble(ParsableByteArray data) { + byte []b = new byte[8]; + data.readBytes(b, 0, b.length); + return ByteBuffer.wrap(b).getDouble(); + } + + /** + * Read a string from an AMF encoded buffer + * @param data Buffer + * @return String read from the buffer + */ + private String readAMFString(ParsableByteArray data) { + int size = data.readUnsignedShort(); + byte []b = new byte[size]; + data.readBytes(b, 0, b.length); + return new String(b); + } + + /** + * Read an array from an AMF encoded buffer + * @param data Buffer + * @return Array read from the buffer + */ + private Object readAMFStrictArray(ParsableByteArray data) { + long count = data.readUnsignedInt(); + ArrayList list = new ArrayList<>(); + for (int i = 0; i < count; i++) { + list.add(readAMFData(data, AMF_TYPE_UNKNOWN)); + } + return list; + } + + /** + * Read an object from an AMF encoded buffer + * @param data Buffer + * @return Object read from the buffer + */ + private Object readAMFObject(ParsableByteArray data) { + HashMap array = new HashMap<>(); + while (true) { + String key = readAMFString(data); + int type = data.readUnsignedByte(); + if (type == AMF_TYPE_END_MARKER) { + break; + } + array.put(key, readAMFData(data, type)); + } + return array; + } + + /** + * Read am ecma array from an AMF encoded buffer + * @param data Buffer + * @return Ecma array 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 = data.readUnsignedByte(); + array.put(key, readAMFData(data, type)); + } + return array; + } + + /** + * Read a date from an AMF encoded buffer + * @param data Buffer + * @return Date read from the buffer + */ + private Date readAMFDate(ParsableByteArray data) { + final Date date = new Date((long) readAMFDouble(data).doubleValue()); + data.readUnsignedShort(); + return date; + } +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagHeader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagHeader.java deleted file mode 100644 index be0f9ef1a84..00000000000 --- a/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagHeader.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.google.android.exoplayer.extractor.flv; - -/** - * Created by joliva on 9/26/15. - */ -final class TagHeader { - public static final int TAG_TYPE_AUDIO = 8; - public static final int TAG_TYPE_VIDEO = 9; - public static final int TAG_TYPE_SCRIPT_DATA = 18; - - 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/TagReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagPayloadReader.java similarity index 53% rename from library/src/main/java/com/google/android/exoplayer/extractor/flv/TagReader.java rename to library/src/main/java/com/google/android/exoplayer/extractor/flv/TagPayloadReader.java index 9df4d8416a7..c032e3a8a19 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagReader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/TagPayloadReader.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.android.exoplayer.extractor.flv; import com.google.android.exoplayer.C; @@ -5,17 +20,19 @@ import com.google.android.exoplayer.util.ParsableByteArray; /** - * Extracts individual samples from FLV tags. + * Extracts individual samples from FLV tags, preserving original order. */ -/* package */ abstract class TagReader { +/* package */ abstract class TagPayloadReader { protected final TrackOutput output; - public long durationUs; + + // Duration of the track + protected long durationUs; /** * @param output A {@link TrackOutput} to which samples should be written. */ - protected TagReader(TrackOutput output) { + protected TagPayloadReader(TrackOutput output) { this.output = output; this.durationUs = C.UNKNOWN_TIME_US; } @@ -32,8 +49,11 @@ protected TagReader(TrackOutput output) { /** * Parses tag header * @param data Buffer where the tag header is stored + * @return True if header was parsed successfully and then payload should be read; + * Otherwise, false + * @throws UnsupportedTrack */ - protected abstract void parseHeader(ParsableByteArray data) throws UnsupportedTrack; + protected abstract boolean parseHeader(ParsableByteArray data) throws UnsupportedTrack; /** * Parses tag payload @@ -43,25 +63,28 @@ protected TagReader(TrackOutput output) { protected abstract void parsePayload(ParsableByteArray data, long timeUs); /** - * Evaluate if for the current tag, payload should be parsed - * @return - */ - protected abstract boolean shouldParsePayload(); - - /** - * Consumes (possibly partial) payload data. + * Consumes payload data. * * @param data The payload data to consume. * @param timeUs The timestamp associated with the payload. */ public void consume(ParsableByteArray data, long timeUs) throws UnsupportedTrack { - parseHeader(data); - - if (shouldParsePayload()) { + if (parseHeader(data)) { parsePayload(data, timeUs); } } + /** + * Sets duration in microseconds + * @param durationUs duration in microseconds + */ + public void setDurationUs(long durationUs) { + this.durationUs = durationUs; + } + + public long getDurationUs() { + return durationUs; + } /** * Thrown when format described in the AudioTrack is not supported */ 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 new file mode 100644 index 00000000000..19704f2b2fe --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagPayloadReader.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.extractor.flv; + +import android.util.Log; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.extractor.TrackOutput; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.CodecSpecificDataUtil; +import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.NalUnitUtil; +import com.google.android.exoplayer.util.ParsableBitArray; +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parses video tags from an FLV stream and extracts H.264 nal units. + */ +final class VideoTagPayloadReader extends TagPayloadReader { + private static final String TAG = "VideoTagPayloadReader"; + + // Video codec + private static final int VIDEO_CODEC_AVC = 7; + + // FRAME TYPE + private static final int VIDEO_FRAME_KEYFRAME = 1; + private static final int VIDEO_FRAME_VIDEO_INFO = 5; + + // PACKET TYPE + 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; + + // State variables. + private boolean hasOutputFormat; + private int frameType; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + public VideoTagPayloadReader(TrackOutput output) { + super(output); + + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); + nalLength = new ParsableByteArray(4); + } + + @Override + public void seek() { + + } + + @Override + protected boolean parseHeader(ParsableByteArray data) throws UnsupportedTrack { + int header = data.readUnsignedByte(); + int frameType = (header >> 4) & 0x0F; + int videoCodec = (header & 0x0F); + + // Support just H.264 encoded content. + if (videoCodec != VIDEO_CODEC_AVC) { + throw new UnsupportedTrack("Video codec not supported. Codec: " + videoCodec); + } + this.frameType = frameType; + return (frameType != VIDEO_FRAME_VIDEO_INFO); + } + + @Override + protected void parsePayload(ParsableByteArray data, long timeUs) { + 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; + } + // 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; + } + + // Construct and output the format. + MediaFormat mediaFormat = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, durationUs, avcData.width, avcData.height, + avcData.initializationData, MediaFormat.NO_VALUE, avcData.pixelWidthAspectRatio); + output.format(mediaFormat); + hasOutputFormat = true; + } else if (packetType == AVC_PACKET_TYPE_AVC_NALU) { + // TODO: Deduplicate with Mp4Extractor. + // Zero the top three bytes of the array that we'll use to parse nal unit lengths, in case + // they're only 1 or 2 bytes long. + byte[] nalLengthData = nalLength.data; + nalLengthData[0] = 0; + nalLengthData[1] = 0; + nalLengthData[2] = 0; + int nalUnitLengthFieldLength = nalUnitsLength; + int nalUnitLengthFieldLengthDiff = 4 - nalUnitsLength; + // 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. + int bytesWritten = 0; + int bytesToWrite; + while (data.bytesLeft() > 0) { + // Read the NAL length so that we know where we find the next one. + data.readBytes(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + nalLength.setPosition(0); + bytesToWrite = nalLength.readUnsignedIntToInt(); + + // First, write nal start code (replacing length field by nal delimiter codes) + nalStartCode.setPosition(0); + output.sampleData(nalStartCode, 4); + bytesWritten += 4; + + // Then write nal unit itsef + 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. + * @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"); + } + } + + /** + * Holds data parsed from an Sequence Header video tag atom. + */ + private static final class AvcSequenceHeaderData { + + public final List initializationData; + public final int nalUnitLengthFieldLength; + public final float pixelWidthAspectRatio; + public final int width; + public final int height; + + public AvcSequenceHeaderData(List initializationData, int nalUnitLengthFieldLength, + int width, int height, float pixelWidthAspectRatio) { + this.initializationData = initializationData; + this.nalUnitLengthFieldLength = nalUnitLengthFieldLength; + this.pixelWidthAspectRatio = pixelWidthAspectRatio; + this.width = width; + this.height = height; + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagReader.java deleted file mode 100644 index 3b2496e4a50..00000000000 --- a/library/src/main/java/com/google/android/exoplayer/extractor/flv/VideoTagReader.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.google.android.exoplayer.extractor.flv; - -import android.util.Log; -import android.util.Pair; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.extractor.TrackOutput; -import com.google.android.exoplayer.util.CodecSpecificDataUtil; -import com.google.android.exoplayer.util.MimeTypes; -import com.google.android.exoplayer.util.ParsableBitArray; -import com.google.android.exoplayer.util.ParsableByteArray; - -import java.util.Collections; - -/** - * Created by joliva on 9/27/15. - */ -public class VideoTagReader extends TagReader{ - private static final String TAG = "VideoTagReader"; - - // Video codec - private static final int VIDEO_CODEC_JPEG = 1; - private static final int VIDEO_CODEC_H263 = 2; - private static final int VIDEO_CODEC_SCREEN_VIDEO = 3; - private static final int VIDEO_CODEC_VP6 = 4; - private static final int VIDEO_CODEC_VP6_WITH_ALPHA_CHANNEL = 5; - private static final int VIDEO_CODEC_SCREEN_VIDEO_V2 = 6; - private static final int VIDEO_CODEC_AVC = 7; - - // FRAME TYPE - private static final int VIDEO_FRAME_KEYFRAME = 1; - private static final int VIDEO_FRAME_INTERFRAME = 2; - private static final int VIDEO_FRAME_DISPOSABLE_INTERFRAME = 3; - private static final int VIDEO_FRAME_GENERATED_KEYFRAME = 4; - private static final int VIDEO_FRAME_VIDEO_INFO = 5; - - // PACKET TYPE - 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; - - private boolean hasOutputFormat; - private int format; - private int frameType; - - /** - * @param output A {@link TrackOutput} to which samples should be written. - */ - public VideoTagReader(TrackOutput output) { - super(output); - } - - @Override - public void seek() { - - } - - @Override - protected void parseHeader(ParsableByteArray data) throws UnsupportedTrack { - int header = data.readUnsignedByte(); - int frameType = (header >> 4) & 0x0F; - int videoCodec = (header & 0x0F); - - if (videoCodec != VIDEO_CODEC_AVC) { - throw new UnsupportedTrack("Video codec not supported. Codec: " + videoCodec); - } - - this.format = videoCodec; - this.frameType = frameType; - } - - @Override - protected void parsePayload(ParsableByteArray data, long timeUs) { - int packetType = data.readUnsignedByte(); - int compositionTime = data.readUnsignedInt24(); - if (packetType == AVC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { - ParsableBitArray videoSequence = new ParsableBitArray(new byte[data.bytesLeft()]); - data.readBytes(videoSequence.data, 0, data.bytesLeft()); - - -/* - // Construct and output the format. - output.format(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, - MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US, parsedSpsData.width, parsedSpsData.height, - initializationData, MediaFormat.NO_VALUE, parsedSpsData.pixelWidthAspectRatio)); -*/ -// output.format(mediaFormat); - hasOutputFormat = true; - } else if (packetType == AVC_PACKET_TYPE_AVC_NALU) { - int bytesToWrite = data.bytesLeft(); - output.sampleData(data, bytesToWrite); - output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.SAMPLE_FLAG_SYNC : 0, - bytesToWrite, 0, null); - - Log.d(TAG, "AAC TAG. Size: " + bytesToWrite + ", timeUs: " + timeUs); - } - } - - @Override - protected boolean shouldParsePayload() { - return (format == VIDEO_CODEC_AVC && frameType != VIDEO_FRAME_VIDEO_INFO); - } -}