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..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,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("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 8f0bc4977b8..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}
@@ -146,6 +147,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/AudioTagPayloadReader.java b/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagPayloadReader.java
new file mode 100644
index 00000000000..470a2eaf3f6
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/AudioTagPayloadReader.java
@@ -0,0 +1,125 @@
+/*
+ * 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.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;
+
+/**
+ * Parses audio tags of from an FLV stream and extracts AAC frames.
+ */
+final class AudioTagPayloadReader extends TagPayloadReader {
+ // Sound format
+ private static final int AUDIO_FORMAT_AAC = 10;
+
+ // 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
+ };
+
+ // State variables
+ private boolean hasParsedAudioDataHeader;
+ private boolean hasOutputFormat;
+
+
+ public AudioTagPayloadReader(TrackOutput output) {
+ super(output);
+ }
+
+ @Override
+ public void seek() {
+
+ }
+
+ @Override
+ 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;
+ 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) {
+ // 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);
+ }
+ }
+
+ 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());
+
+ 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) {
+ // Sample audio AAC frames
+ int bytesToWrite = data.bytesLeft();
+ output.sampleData(data, bytesToWrite);
+ output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, bytesToWrite, 0, null);
+ }
+ }
+
+}
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..563361d97d5
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/extractor/flv/FlvExtractor.java
@@ -0,0 +1,301 @@
+/*
+ * 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.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.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;
+ 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;
+
+ // 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;
+ private ParsableByteArray tagData;
+
+ // Extractor outputs.
+ private ExtractorOutput extractorOutput;
+
+ // State variables.
+ private int parserState;
+ private int dataOffset;
+ private TagHeader currentTagHeader;
+
+ // 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;
+ currentTagHeader = new TagHeader();
+ }
+
+ @Override
+ public void init(ExtractorOutput output) {
+ this.extractorOutput = output;
+ }
+
+ @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, 4);
+ scratch.setPosition(0);
+
+ return scratch.readInt() == 0;
+ }
+
+ @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.UnsupportedTrack unsupportedTrack) {
+ unsupportedTrack.printStackTrace();
+ return RESULT_END_OF_INPUT;
+ }
+ }
+
+ @Override
+ 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();
+ boolean hasAudio = (flags & 0x04) != 0;
+ boolean hasVideo = (flags & 0x01) != 0;
+
+ if (hasAudio && audioReader == null) {
+ audioReader = new AudioTagPayloadReader(extractorOutput.track(TAG_TYPE_AUDIO));
+ }
+ if (hasVideo && videoReader == null) {
+ videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO));
+ }
+ if (metadataReader == null) {
+ metadataReader = new ScriptTagPayloadReader(null);
+ }
+ extractorOutput.endTracks();
+ extractorOutput.seekMap(this);
+
+ // Store payload start position and start extended header (if there is one)
+ dataOffset = headerBuffer.readInt();
+
+ input.skipFully(dataOffset - FLV_MIN_HEADER_SIZE);
+ parserState = STATE_READING_TAG_HEADER;
+ } catch (EOFException eof) {
+ return false;
+ }
+
+ return 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,
+ 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);
+ int type = tagHeaderBuffer.readUnsignedByte();
+ int dataSize = tagHeaderBuffer.readUnsignedInt24();
+ long timestamp = tagHeaderBuffer.readUnsignedInt24();
+ timestamp = (tagHeaderBuffer.readUnsignedByte() << 24) | timestamp;
+ int streamId = tagHeaderBuffer.readUnsignedInt24();
+
+ currentTagHeader.type = type;
+ currentTagHeader.dataSize = dataSize;
+ currentTagHeader.timestamp = timestamp * 1000;
+ currentTagHeader.streamId = streamId;
+
+ // Sanity checks.
+ Assertions.checkState(type == TAG_TYPE_AUDIO || type == TAG_TYPE_VIDEO
+ || type == TAG_TYPE_SCRIPT_DATA);
+ // Reuse tagData buffer to avoid lot of memory allocation (performance penalty).
+ if (tagData == null || dataSize > tagData.capacity()) {
+ tagData = new ParsableByteArray(dataSize);
+ } else {
+ tagData.setPosition(0);
+ }
+ tagData.setLimit(dataSize);
+ parserState = STATE_READING_SAMPLE;
+
+ } catch (EOFException eof) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 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;
+ }
+ 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);
+ if (metadataReader.getDurationUs() != C.UNKNOWN_TIME_US) {
+ if (audioReader != null) {
+ audioReader.setDurationUs(metadataReader.getDurationUs());
+ }
+ if (videoReader != null) {
+ videoReader.setDurationUs(metadataReader.getDurationUs());
+ }
+ }
+ } else {
+ tagData.reset();
+ }
+
+ parserState = STATE_READING_TAG_HEADER;
+
+ 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/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