FLV Support - Added Video Reader and parsing improvements
This commit is contained in:
parent
8ddc73511e
commit
3e36f529f8
@ -145,9 +145,9 @@ import java.util.Locale;
|
|||||||
"http://storage.googleapis.com/exoplayer-test-media-0/play.mp3", PlayerActivity.TYPE_OTHER),
|
"http://storage.googleapis.com/exoplayer-test-media-0/play.mp3", PlayerActivity.TYPE_OTHER),
|
||||||
new Sample("Google Glass (WebM Video with Vorbis Audio)",
|
new Sample("Google Glass (WebM Video with Vorbis Audio)",
|
||||||
"http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", PlayerActivity.TYPE_OTHER),
|
"http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", PlayerActivity.TYPE_OTHER),
|
||||||
new Sample("FLV Sample",
|
new Sample("Big Buck Bunny (FLV Video)",
|
||||||
"http://master255.org/res/%D0%9A%D0%BB%D0%B8%D0%BF%D1%8B/B/Black%20Eyed%20Peas/black%20ey"
|
"http://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0", PlayerActivity.TYPE_OTHER),
|
||||||
+ "ed%20peas-My%20Humps.flv", PlayerActivity.TYPE_OTHER),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private Samples() {}
|
private Samples() {}
|
||||||
|
@ -58,9 +58,10 @@ import java.util.List;
|
|||||||
* <li>MP3 ({@link com.google.android.exoplayer.extractor.mp3.Mp3Extractor})</li>
|
* <li>MP3 ({@link com.google.android.exoplayer.extractor.mp3.Mp3Extractor})</li>
|
||||||
* <li>AAC ({@link com.google.android.exoplayer.extractor.ts.AdtsExtractor})</li>
|
* <li>AAC ({@link com.google.android.exoplayer.extractor.ts.AdtsExtractor})</li>
|
||||||
* <li>MPEG TS ({@link com.google.android.exoplayer.extractor.ts.TsExtractor}</li>
|
* <li>MPEG TS ({@link com.google.android.exoplayer.extractor.ts.TsExtractor}</li>
|
||||||
|
* <li>FLV ({@link com.google.android.exoplayer.extractor.flv.FlvExtractor}</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p>Seeking in AAC and MPEG TS streams is not supported.
|
* <p>Seeking in AAC, MPEG TS and FLV streams is not supported.
|
||||||
*
|
*
|
||||||
* <p>To override the default extractors, pass one or more {@link Extractor} instances to the
|
* <p>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}
|
* constructor. When reading a new stream, the first {@link Extractor} that returns {@code true}
|
||||||
|
@ -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;
|
package com.google.android.exoplayer.extractor.flv;
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
import com.google.android.exoplayer.MediaFormat;
|
import com.google.android.exoplayer.MediaFormat;
|
||||||
import com.google.android.exoplayer.extractor.TrackOutput;
|
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||||
@ -14,48 +27,27 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
|||||||
import java.util.Collections;
|
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{
|
final class AudioTagPayloadReader extends TagPayloadReader {
|
||||||
|
|
||||||
private static final String TAG = "AudioTagReader";
|
|
||||||
|
|
||||||
// Sound format
|
// 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_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
|
// AAC PACKET TYPE
|
||||||
private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0;
|
private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0;
|
||||||
private static final int AAC_PACKET_TYPE_AAC_RAW = 1;
|
private static final int AAC_PACKET_TYPE_AAC_RAW = 1;
|
||||||
|
|
||||||
|
// SAMPLING RATES
|
||||||
private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] {
|
private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] {
|
||||||
5500, 11000, 22000, 44000
|
5500, 11000, 22000, 44000
|
||||||
};
|
};
|
||||||
|
|
||||||
private int format;
|
// State variables
|
||||||
private int sampleRate;
|
private boolean hasParsedAudioDataHeader;
|
||||||
private int bitsPerSample;
|
|
||||||
private int channels;
|
|
||||||
|
|
||||||
private boolean hasParsedAudioData;
|
|
||||||
private boolean hasOutputFormat;
|
private boolean hasOutputFormat;
|
||||||
|
|
||||||
/**
|
|
||||||
* @param output A {@link TrackOutput} to which samples should be written.
|
public AudioTagPayloadReader(TrackOutput output) {
|
||||||
*/
|
|
||||||
public AudioTagReader(TrackOutput output) {
|
|
||||||
super(output);
|
super(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,8 +57,10 @@ public class AudioTagReader extends TagReader{
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void parseHeader(ParsableByteArray data) throws UnsupportedTrack {
|
protected boolean parseHeader(ParsableByteArray data) throws UnsupportedTrack {
|
||||||
if (!hasParsedAudioData) {
|
// 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 header = data.readUnsignedByte();
|
||||||
int soundFormat = (header >> 4) & 0x0F;
|
int soundFormat = (header >> 4) & 0x0F;
|
||||||
int sampleRateIndex = (header >> 2) & 0x03;
|
int sampleRateIndex = (header >> 2) & 0x03;
|
||||||
@ -78,42 +72,29 @@ public class AudioTagReader extends TagReader{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasOutputFormat) {
|
if (!hasOutputFormat) {
|
||||||
switch (soundFormat) {
|
// TODO: Adds support for MP3 and PCM
|
||||||
// raw audio data. Just creates media format
|
if (soundFormat != AUDIO_FORMAT_AAC) {
|
||||||
case AUDIO_FORMAT_LINEAR_PCM_LITTLE_ENDIAN:
|
throw new UnsupportedTrack("Audio track not supported. Format: " + soundFormat +
|
||||||
output.format(MediaFormat.createAudioFormat(MimeTypes.AUDIO_RAW, MediaFormat.NO_VALUE,
|
", Sample rate: " + sampleRateIndex + ", bps: " + bitsPerSample + ", channels: " +
|
||||||
MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, channels,
|
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;
|
hasParsedAudioDataHeader = true;
|
||||||
this.sampleRate = AUDIO_SAMPLING_RATE_TABLE[sampleRateIndex];
|
|
||||||
this.bitsPerSample = bitsPerSample;
|
|
||||||
this.channels = channels;
|
|
||||||
|
|
||||||
hasParsedAudioData = true;
|
|
||||||
} else {
|
} else {
|
||||||
|
// Skip header if it was parsed previously.
|
||||||
data.skipBytes(1);
|
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
|
@Override
|
||||||
protected void parsePayload(ParsableByteArray data, long timeUs) {
|
protected void parsePayload(ParsableByteArray data, long timeUs) {
|
||||||
int packetType = data.readUnsignedByte();
|
int packetType = data.readUnsignedByte();
|
||||||
|
// Parse sequence header just in case it was not done before.
|
||||||
if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {
|
if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {
|
||||||
ParsableBitArray adtsScratch = new ParsableBitArray(new byte[data.bytesLeft()]);
|
ParsableBitArray adtsScratch = new ParsableBitArray(new byte[data.bytesLeft()]);
|
||||||
data.readBytes(adtsScratch.data, 0, data.bytesLeft());
|
data.readBytes(adtsScratch.data, 0, data.bytesLeft());
|
||||||
@ -134,17 +115,11 @@ public class AudioTagReader extends TagReader{
|
|||||||
output.format(mediaFormat);
|
output.format(mediaFormat);
|
||||||
hasOutputFormat = true;
|
hasOutputFormat = true;
|
||||||
} else if (packetType == AAC_PACKET_TYPE_AAC_RAW) {
|
} else if (packetType == AAC_PACKET_TYPE_AAC_RAW) {
|
||||||
|
// Sample audio AAC frames
|
||||||
int bytesToWrite = data.bytesLeft();
|
int bytesToWrite = data.bytesLeft();
|
||||||
output.sampleData(data, bytesToWrite);
|
output.sampleData(data, bytesToWrite);
|
||||||
output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, bytesToWrite, 0, null);
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -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;
|
package com.google.android.exoplayer.extractor.flv;
|
||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
@ -6,7 +21,6 @@ import com.google.android.exoplayer.extractor.ExtractorInput;
|
|||||||
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
import com.google.android.exoplayer.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer.extractor.PositionHolder;
|
import com.google.android.exoplayer.extractor.PositionHolder;
|
||||||
import com.google.android.exoplayer.extractor.SeekMap;
|
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.Assertions;
|
||||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
import com.google.android.exoplayer.util.Util;
|
import com.google.android.exoplayer.util.Util;
|
||||||
@ -15,9 +29,9 @@ import java.io.EOFException;
|
|||||||
import java.io.IOException;
|
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
|
// Header sizes
|
||||||
private static final int FLV_MIN_HEADER_SIZE = 9;
|
private static final int FLV_MIN_HEADER_SIZE = 9;
|
||||||
private static final int FLV_TAG_HEADER_SIZE = 11;
|
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_VIDEO = 9;
|
||||||
private static final int TAG_TYPE_SCRIPT_DATA = 18;
|
private static final int TAG_TYPE_SCRIPT_DATA = 18;
|
||||||
|
|
||||||
|
// FLV container identifier
|
||||||
private static final int FLV_TAG = Util.getIntegerCodeForString("FLV");
|
private static final int FLV_TAG = Util.getIntegerCodeForString("FLV");
|
||||||
|
|
||||||
|
// Temporary buffers
|
||||||
private final ParsableByteArray scratch;
|
private final ParsableByteArray scratch;
|
||||||
private final ParsableByteArray headerBuffer;
|
private final ParsableByteArray headerBuffer;
|
||||||
private final ParsableByteArray tagHeaderBuffer;
|
private final ParsableByteArray tagHeaderBuffer;
|
||||||
@ -40,36 +56,28 @@ public final class FlvExtractor implements Extractor {
|
|||||||
|
|
||||||
// Extractor outputs.
|
// Extractor outputs.
|
||||||
private ExtractorOutput extractorOutput;
|
private ExtractorOutput extractorOutput;
|
||||||
private TrackOutput trackOutput;
|
|
||||||
|
|
||||||
private boolean hasAudio;
|
|
||||||
private boolean hasVideo;
|
|
||||||
private int dataOffset;
|
|
||||||
|
|
||||||
|
// State variables.
|
||||||
private int parserState;
|
private int parserState;
|
||||||
|
private int dataOffset;
|
||||||
private TagHeader currentTagHeader;
|
private TagHeader currentTagHeader;
|
||||||
|
|
||||||
private AudioTagReader audioReader;
|
// Tags readers
|
||||||
private VideoTagReader videoReader;
|
private AudioTagPayloadReader audioReader;
|
||||||
private MetadataReader metadataReader;
|
private VideoTagPayloadReader videoReader;
|
||||||
|
private ScriptTagPayloadReader metadataReader;
|
||||||
|
|
||||||
public FlvExtractor() {
|
public FlvExtractor() {
|
||||||
scratch = new ParsableByteArray(4);
|
scratch = new ParsableByteArray(4);
|
||||||
headerBuffer = new ParsableByteArray(FLV_MIN_HEADER_SIZE);
|
headerBuffer = new ParsableByteArray(FLV_MIN_HEADER_SIZE);
|
||||||
tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);
|
tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);
|
||||||
dataOffset = 0;
|
dataOffset = 0;
|
||||||
hasAudio = false;
|
|
||||||
hasVideo = false;
|
|
||||||
currentTagHeader = new TagHeader();
|
currentTagHeader = new TagHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(ExtractorOutput output) {
|
public void init(ExtractorOutput output) {
|
||||||
this.extractorOutput = output;
|
this.extractorOutput = output;
|
||||||
trackOutput = extractorOutput.track(0);
|
|
||||||
extractorOutput.endTracks();
|
|
||||||
|
|
||||||
output.seekMap(SeekMap.UNSEEKABLE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -80,7 +88,6 @@ public final class FlvExtractor implements Extractor {
|
|||||||
if (scratch.readUnsignedInt24() != FLV_TAG) {
|
if (scratch.readUnsignedInt24() != FLV_TAG) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
|
|
||||||
// Checking reserved flags are set to 0
|
// Checking reserved flags are set to 0
|
||||||
input.peekFully(scratch.data, 0, 2);
|
input.peekFully(scratch.data, 0, 2);
|
||||||
@ -98,13 +105,10 @@ public final class FlvExtractor implements Extractor {
|
|||||||
input.advancePeekPosition(dataOffset);
|
input.advancePeekPosition(dataOffset);
|
||||||
|
|
||||||
// Checking first "previous tag size" is set to 0
|
// Checking first "previous tag size" is set to 0
|
||||||
input.peekFully(scratch.data, 0, 1);
|
input.peekFully(scratch.data, 0, 4);
|
||||||
scratch.setPosition(0);
|
scratch.setPosition(0);
|
||||||
if (scratch.readInt() != 0) {
|
|
||||||
return false;
|
return scratch.readInt() == 0;
|
||||||
}
|
|
||||||
*/
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -117,21 +121,17 @@ public final class FlvExtractor implements Extractor {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
switch (parserState) {
|
if (parserState == STATE_READING_TAG_HEADER) {
|
||||||
case STATE_READING_TAG_HEADER:
|
if (!readTagHeader(input)) {
|
||||||
if (!readTagHeader(input)) {
|
return RESULT_END_OF_INPUT;
|
||||||
return RESULT_END_OF_INPUT;
|
}
|
||||||
}
|
} else {
|
||||||
break;
|
return readSample(input);
|
||||||
|
|
||||||
default:
|
|
||||||
return readSample(input, seekPosition);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (AudioTagReader.UnsupportedTrack unsupportedTrack) {
|
} catch (AudioTagPayloadReader.UnsupportedTrack unsupportedTrack) {
|
||||||
unsupportedTrack.printStackTrace();
|
unsupportedTrack.printStackTrace();
|
||||||
return RESULT_END_OF_INPUT;
|
return RESULT_END_OF_INPUT;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,23 +140,35 @@ public final class FlvExtractor implements Extractor {
|
|||||||
dataOffset = 0;
|
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 {
|
private boolean readHeader(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
try {
|
try {
|
||||||
input.readFully(headerBuffer.data, 0, FLV_MIN_HEADER_SIZE);
|
input.readFully(headerBuffer.data, 0, FLV_MIN_HEADER_SIZE);
|
||||||
headerBuffer.setPosition(0);
|
headerBuffer.setPosition(0);
|
||||||
headerBuffer.skipBytes(4);
|
headerBuffer.skipBytes(4);
|
||||||
int flags = headerBuffer.readUnsignedByte();
|
int flags = headerBuffer.readUnsignedByte();
|
||||||
hasAudio = (flags & 0x04) != 0;
|
boolean hasAudio = (flags & 0x04) != 0;
|
||||||
hasVideo = (flags & 0x01) != 0;
|
boolean hasVideo = (flags & 0x01) != 0;
|
||||||
|
|
||||||
if (hasAudio) {
|
if (hasAudio && audioReader == null) {
|
||||||
audioReader = new AudioTagReader(trackOutput);
|
audioReader = new AudioTagPayloadReader(extractorOutput.track(TAG_TYPE_AUDIO));
|
||||||
}
|
}
|
||||||
if (hasVideo) {
|
if (hasVideo && videoReader == null) {
|
||||||
//videoReader = new VideoTagReader(trackOutput);
|
videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO));
|
||||||
}
|
}
|
||||||
metadataReader = new MetadataReader(trackOutput);
|
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();
|
dataOffset = headerBuffer.readInt();
|
||||||
|
|
||||||
input.skipFully(dataOffset - FLV_MIN_HEADER_SIZE);
|
input.skipFully(dataOffset - FLV_MIN_HEADER_SIZE);
|
||||||
@ -168,14 +180,25 @@ public final class FlvExtractor implements Extractor {
|
|||||||
return true;
|
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,
|
private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException,
|
||||||
TagReader.UnsupportedTrack {
|
TagPayloadReader.UnsupportedTrack {
|
||||||
try {
|
try {
|
||||||
|
// skipping previous tag size field
|
||||||
input.skipFully(4);
|
input.skipFully(4);
|
||||||
|
|
||||||
|
// Read the tag header from the input.
|
||||||
input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE);
|
input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE);
|
||||||
|
|
||||||
tagHeaderBuffer.setPosition(0);
|
tagHeaderBuffer.setPosition(0);
|
||||||
// skipping previous tag size field.
|
|
||||||
int type = tagHeaderBuffer.readUnsignedByte();
|
int type = tagHeaderBuffer.readUnsignedByte();
|
||||||
int dataSize = tagHeaderBuffer.readUnsignedInt24();
|
int dataSize = tagHeaderBuffer.readUnsignedInt24();
|
||||||
long timestamp = tagHeaderBuffer.readUnsignedInt24();
|
long timestamp = tagHeaderBuffer.readUnsignedInt24();
|
||||||
@ -187,8 +210,16 @@ public final class FlvExtractor implements Extractor {
|
|||||||
currentTagHeader.timestamp = timestamp * 1000;
|
currentTagHeader.timestamp = timestamp * 1000;
|
||||||
currentTagHeader.streamId = streamId;
|
currentTagHeader.streamId = streamId;
|
||||||
|
|
||||||
Assertions.checkState(dataSize <= Integer.MAX_VALUE);
|
// Sanity checks.
|
||||||
tagData = new ParsableByteArray((int) dataSize);
|
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;
|
parserState = STATE_READING_SAMPLE;
|
||||||
|
|
||||||
} catch (EOFException eof) {
|
} catch (EOFException eof) {
|
||||||
@ -198,8 +229,17 @@ public final class FlvExtractor implements Extractor {
|
|||||||
return true;
|
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 (tagData != null) {
|
||||||
if (!input.readFully(tagData.data, 0, currentTagHeader.dataSize, true)) {
|
if (!input.readFully(tagData.data, 0, currentTagHeader.dataSize, true)) {
|
||||||
return RESULT_END_OF_INPUT;
|
return RESULT_END_OF_INPUT;
|
||||||
@ -210,18 +250,19 @@ public final class FlvExtractor implements Extractor {
|
|||||||
return RESULT_CONTINUE;
|
return RESULT_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass payload to the right payload reader.
|
||||||
if (currentTagHeader.type == TAG_TYPE_AUDIO && audioReader != null) {
|
if (currentTagHeader.type == TAG_TYPE_AUDIO && audioReader != null) {
|
||||||
audioReader.consume(tagData, currentTagHeader.timestamp);
|
audioReader.consume(tagData, currentTagHeader.timestamp);
|
||||||
} else if (currentTagHeader.type == TAG_TYPE_VIDEO && videoReader != null) {
|
} else if (currentTagHeader.type == TAG_TYPE_VIDEO && videoReader != null) {
|
||||||
videoReader.consume(tagData, currentTagHeader.timestamp);
|
videoReader.consume(tagData, currentTagHeader.timestamp);
|
||||||
} else if (currentTagHeader.type == TAG_TYPE_SCRIPT_DATA && metadataReader != null) {
|
} else if (currentTagHeader.type == TAG_TYPE_SCRIPT_DATA && metadataReader != null) {
|
||||||
metadataReader.consume(tagData, currentTagHeader.timestamp);
|
metadataReader.consume(tagData, currentTagHeader.timestamp);
|
||||||
if (metadataReader.durationUs != C.UNKNOWN_TIME_US) {
|
if (metadataReader.getDurationUs() != C.UNKNOWN_TIME_US) {
|
||||||
if (audioReader != null) {
|
if (audioReader != null) {
|
||||||
audioReader.durationUs = metadataReader.durationUs;
|
audioReader.setDurationUs(metadataReader.getDurationUs());
|
||||||
}
|
}
|
||||||
if (videoReader != null) {
|
if (videoReader != null) {
|
||||||
videoReader.durationUs = metadataReader.durationUs;
|
videoReader.setDurationUs(metadataReader.getDurationUs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -233,4 +274,28 @@ public final class FlvExtractor implements Extractor {
|
|||||||
return RESULT_CONTINUE;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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<String, Object> extractedMetadata = (Map<String, Object>) obj;
|
|
||||||
for (Map.Entry<String, Object> 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<Object> list = new ArrayList<Object>();
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
list.add(readAMFData(data, METADATA_TYPE_UNKNOWN));
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object readAMFObject(ParsableByteArray data) {
|
|
||||||
HashMap<String, Object> array = new HashMap<String, Object>();
|
|
||||||
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<String, Object> array = new HashMap<String, Object>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<String, Object> extractedMetadata = (Map<String, Object>) obj;
|
||||||
|
for (Map.Entry<String, Object> 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<Object> 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<String, Object> 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<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
package com.google.android.exoplayer.extractor.flv;
|
||||||
|
|
||||||
import com.google.android.exoplayer.C;
|
import com.google.android.exoplayer.C;
|
||||||
@ -5,17 +20,19 @@ import com.google.android.exoplayer.extractor.TrackOutput;
|
|||||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
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;
|
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.
|
* @param output A {@link TrackOutput} to which samples should be written.
|
||||||
*/
|
*/
|
||||||
protected TagReader(TrackOutput output) {
|
protected TagPayloadReader(TrackOutput output) {
|
||||||
this.output = output;
|
this.output = output;
|
||||||
this.durationUs = C.UNKNOWN_TIME_US;
|
this.durationUs = C.UNKNOWN_TIME_US;
|
||||||
}
|
}
|
||||||
@ -32,8 +49,11 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
|||||||
/**
|
/**
|
||||||
* Parses tag header
|
* Parses tag header
|
||||||
* @param data Buffer where the tag header is stored
|
* @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
|
* Parses tag payload
|
||||||
@ -43,25 +63,28 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
|||||||
protected abstract void parsePayload(ParsableByteArray data, long timeUs);
|
protected abstract void parsePayload(ParsableByteArray data, long timeUs);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate if for the current tag, payload should be parsed
|
* Consumes payload data.
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
protected abstract boolean shouldParsePayload();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consumes (possibly partial) payload data.
|
|
||||||
*
|
*
|
||||||
* @param data The payload data to consume.
|
* @param data The payload data to consume.
|
||||||
* @param timeUs The timestamp associated with the payload.
|
* @param timeUs The timestamp associated with the payload.
|
||||||
*/
|
*/
|
||||||
public void consume(ParsableByteArray data, long timeUs) throws UnsupportedTrack {
|
public void consume(ParsableByteArray data, long timeUs) throws UnsupportedTrack {
|
||||||
parseHeader(data);
|
if (parseHeader(data)) {
|
||||||
|
|
||||||
if (shouldParsePayload()) {
|
|
||||||
parsePayload(data, timeUs);
|
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
|
* Thrown when format described in the AudioTrack is not supported
|
||||||
*/
|
*/
|
@ -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<byte[]> initializationData = new ArrayList<>();
|
||||||
|
int numSequenceParameterSets = buffer.readUnsignedByte() & 0x1F;
|
||||||
|
for (int i = 0; i < numSequenceParameterSets; i++) {
|
||||||
|
initializationData.add(NalUnitUtil.parseChildNalUnit(buffer));
|
||||||
|
}
|
||||||
|
int numPictureParameterSets = buffer.readUnsignedByte();
|
||||||
|
for (int j = 0; j < numPictureParameterSets; j++) {
|
||||||
|
initializationData.add(NalUnitUtil.parseChildNalUnit(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
float pixelWidthAspectRatio = 1;
|
||||||
|
int width = MediaFormat.NO_VALUE;
|
||||||
|
int height = MediaFormat.NO_VALUE;
|
||||||
|
if (numSequenceParameterSets > 0) {
|
||||||
|
// Parse the first sequence parameter set to obtain pixelWidthAspectRatio.
|
||||||
|
ParsableBitArray spsDataBitArray = new ParsableBitArray(initializationData.get(0));
|
||||||
|
// Skip the NAL header consisting of the nalUnitLengthField and the type (1 byte).
|
||||||
|
spsDataBitArray.setPosition(8 * (nalUnitLengthFieldLength + 1));
|
||||||
|
CodecSpecificDataUtil.SpsData sps = CodecSpecificDataUtil.parseSpsNalUnit(spsDataBitArray);
|
||||||
|
width = sps.width;
|
||||||
|
height = sps.height;
|
||||||
|
pixelWidthAspectRatio = sps.pixelWidthAspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AvcSequenceHeaderData(initializationData, nalUnitLengthFieldLength,
|
||||||
|
width, height, pixelWidthAspectRatio);
|
||||||
|
} catch (ArrayIndexOutOfBoundsException e) {
|
||||||
|
throw new ParserException("Error parsing AVC codec private");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds data parsed from an Sequence Header video tag atom.
|
||||||
|
*/
|
||||||
|
private static final class AvcSequenceHeaderData {
|
||||||
|
|
||||||
|
public final List<byte[]> initializationData;
|
||||||
|
public final int nalUnitLengthFieldLength;
|
||||||
|
public final float pixelWidthAspectRatio;
|
||||||
|
public final int width;
|
||||||
|
public final int height;
|
||||||
|
|
||||||
|
public AvcSequenceHeaderData(List<byte[]> initializationData, int nalUnitLengthFieldLength,
|
||||||
|
int width, int height, float pixelWidthAspectRatio) {
|
||||||
|
this.initializationData = initializationData;
|
||||||
|
this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;
|
||||||
|
this.pixelWidthAspectRatio = pixelWidthAspectRatio;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user