package org.jcodec.containers.flv;
import org.jcodec.common.AudioFormat;
import org.jcodec.common.Codec;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.logging.Logger;
import org.jcodec.common.tools.MathUtil;
import org.jcodec.containers.flv.FLVTag.AacAudioTagHeader;
import org.jcodec.containers.flv.FLVTag.AudioTagHeader;
import org.jcodec.containers.flv.FLVTag.AvcVideoTagHeader;
import org.jcodec.containers.flv.FLVTag.TagHeader;
import org.jcodec.containers.flv.FLVTag.Type;
import org.jcodec.containers.flv.FLVTag.VideoTagHeader;
import org.jcodec.platform.Platform;
import java.io.IOException;
import java.lang.System;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* This class is part of JCodec ( www.jcodec.org ) This software is distributed
* under FreeBSD License
*
* FLV ( Flash Media Video ) demuxer
*
* @author Stan Vitvitskyy
*
*/
public class FLVReader {
private static final int REPOSITION_BUFFER_READS = 10;
private static final int TAG_HEADER_SIZE = 15;
// Read buffer, 1M
private static final int READ_BUFFER_SIZE = 1 << 10;
private int frameNo;
private ByteBuffer readBuf;
private SeekableByteChannel ch;
private boolean eof;
private static boolean platformBigEndian = ByteBuffer.allocate(0).order() == ByteOrder.BIG_ENDIAN;
public static Codec[] audioCodecMapping = new Codec[] { Codec.PCM, Codec.ADPCM, Codec.MP3, Codec.PCM,
Codec.NELLYMOSER, Codec.NELLYMOSER, Codec.NELLYMOSER, Codec.G711, Codec.G711, null, Codec.AAC, Codec.SPEEX,
Codec.MP3, null };
public static Codec[] videoCodecMapping = new Codec[] { null, null, Codec.SORENSON, Codec.FLASH_SCREEN_VIDEO,
Codec.VP6, Codec.VP6, Codec.FLASH_SCREEN_V2, Codec.H264 };
public static int[] sampleRates = new int[] { 5500, 11000, 22000, 44100 };
public FLVReader(SeekableByteChannel ch) throws IOException {
this.ch = ch;
readBuf = ByteBuffer.allocate(READ_BUFFER_SIZE);
readBuf.order(ByteOrder.BIG_ENDIAN);
initialRead(ch);
if (!readHeader(readBuf)) {
// This file doesn't have an FLV header, maybe it's a portion of an
// FLV file and we can position at the tag start?
readBuf.position(0);
if (!repositionFile())
throw new RuntimeException("Invalid FLV file");
else {
Logger.warn(String.format("Parsing a corrupt FLV file, first tag found at %d. %s", readBuf.position(),
readBuf.position() == 0 ? "Did you forget the FLV 9-byte header?" : ""));
}
}
}
private void initialRead(ReadableByteChannel ch) throws IOException {
readBuf.clear();
if (ch.read(readBuf) == -1)
eof = true;
readBuf.flip();
}
public FLVTag readNextPacket() throws IOException {
if (eof)
return null;
FLVTag pkt = parsePacket(readBuf);
// No more pakets fit into the buffer, reading more data
if (pkt == null && !eof) {
moveRemainderToTheStart(readBuf);
if (ch.read(readBuf) == -1) {
eof = true;
return null;
}
while (MathUtil.log2(readBuf.capacity()) <= 22) {
readBuf.flip();
pkt = parsePacket(readBuf);
if (pkt != null || readBuf.position() > 0)
break;
// The buffer is too small, getting a bigger one
ByteBuffer newBuf = ByteBuffer.allocate(readBuf.capacity() << 2);
newBuf.put(readBuf);
readBuf = newBuf;
if (ch.read(readBuf) == -1) {
eof = true;
return null;
}
}
}
return pkt;
}
public FLVTag readPrevPacket() throws IOException {
int startOfLastPacket = readBuf.getInt();
readBuf.position(readBuf.position() - 4);
if (readBuf.position() > startOfLastPacket) {
// The previous frame is still in the buffer, so no need to fetch
readBuf.position(readBuf.position() - startOfLastPacket);
return parsePacket(readBuf);
} else {
// Now we need to fetch the new buffer, because we are unsure of the
// access pattern we are going to fetch only half of the buffer from
// the left side and the other half from the right side of the
// current position
long oldPos = ch.position() - readBuf.remaining();
if (oldPos <= 9) {
// We are at the first frame, there's nowhere to seek
return null;
}
long newPos = Math.max(0, oldPos - readBuf.capacity() / 2);
ch.setPosition(newPos);
readBuf.clear();
ch.read(readBuf);
readBuf.flip();
readBuf.position((int) (oldPos - newPos));
return readPrevPacket();
}
}
private static void moveRemainderToTheStart(ByteBuffer readBuf) {
int rem = readBuf.remaining();
for (int i = 0; i < rem; i++) {
readBuf.put(i, readBuf.get());
}
readBuf.clear();
readBuf.position(rem);
}
public FLVTag parsePacket(ByteBuffer readBuf) throws IOException {
for (;;) {
if (readBuf.remaining() < TAG_HEADER_SIZE) {
return null;
}
int pos = readBuf.position();
long packetPos = ch.position() - readBuf.remaining();
int prevPacketSize = readBuf.getInt();
int packetType = readBuf.get() & 0xff;
int payloadSize = ((readBuf.getShort() & 0xffff) << 8) | (readBuf.get() & 0xff);
int timestamp = ((readBuf.getShort() & 0xffff) << 8) | (readBuf.get() & 0xff)
| ((readBuf.get() & 0xff) << 24);
int streamId = ((readBuf.getShort() & 0xffff) << 8) | (readBuf.get() & 0xff);
// Sanity check and reposition
if (readBuf.remaining() >= payloadSize + 4) {
int thisPacketSize = readBuf.getInt(readBuf.position() + payloadSize);
if (thisPacketSize != payloadSize + 11) {
readBuf.position(readBuf.position() - TAG_HEADER_SIZE);
if (!repositionFile()) {
Logger.error(String.format("Corrupt FLV stream at %d, failed to reposition!", packetPos));
ch.setPosition(ch.size());
eof = true;
return null;
}
Logger.warn(String.format("Corrupt FLV stream at %d, repositioned to %d.", packetPos, ch.position()
- readBuf.remaining()));
continue;
}
}
if (readBuf.remaining() < payloadSize) {
readBuf.position(pos);
return null;
}
if (packetType != 0x8 && packetType != 0x9 && packetType != 0x12) {
NIOUtils.skip(readBuf, payloadSize);
continue;
}
ByteBuffer payload = NIOUtils.clone(NIOUtils.read(readBuf, payloadSize));
Type type;
TagHeader tagHeader;
if (packetType == 0x8) {
type = Type.AUDIO;
tagHeader = parseAudioTagHeader(payload.duplicate());
} else if (packetType == 0x9) {
type = Type.VIDEO;
tagHeader = parseVideoTagHeader(payload.duplicate());
} else if (packetType == 0x12) {
type = Type.SCRIPT;
tagHeader = null;
} else {
System.out.println("NON AV packet");
continue;
}
boolean keyFrame = packetType == 0x8 || packetType == 9 && ((VideoTagHeader) tagHeader).getFrameType() == 1;
return new FLVTag(type, packetPos, tagHeader, timestamp, payload, keyFrame, frameNo++, streamId,
prevPacketSize);
}
}
public static boolean readHeader(ByteBuffer readBuf) {
if (readBuf.remaining() < 9 || readBuf.get() != 'F' || readBuf.get() != 'L' || readBuf.get() != 'V'
|| readBuf.get() != 1 || (readBuf.get() & 0x5) == 0 || readBuf.getInt() != 9) {
return false;
}
return true;
}
public static FLVMetadata parseMetadata(ByteBuffer bb) {
if ("onMetaData".equals(readAMFData(bb, -1)))
return new FLVMetadata((Map<String, Object>) readAMFData(bb, -1));
return null;
}
private static Object readAMFData(ByteBuffer input, int type) {
if (type == -1) {
type = input.get() & 0xff;
}
switch (type) {
case 0:
return input.getDouble();
case 1:
return input.get() == 1;
case 2:
return readAMFString(input);
case 3:
return readAMFObject(input);
case 8:
return readAMFEcmaArray(input);
case 10:
return readAMFStrictArray(input);
case 11:
final Date date = new Date((long) input.getDouble());
input.getShort(); // time zone
return date;
case 13:
return "UNDEFINED";
default:
return null;
}
}
private static Object readAMFStrictArray(ByteBuffer input) {
int count = input.getInt();
Object[] result = new Object[count];
for (int i = 0; i < count; i++) {
result[i] = readAMFData(input, -1);
}
return result;
}
private static String readAMFString(ByteBuffer input) {
int size = input.getShort() & 0xffff;
return Platform.stringFromCharset(NIOUtils.toArray(NIOUtils.read(input, size)), Charset.forName("UTF-8"));
}
private static Object readAMFObject(ByteBuffer input) {
Map<String, Object> array = new HashMap<String, Object>();
while (true) {
String key = readAMFString(input);
int dataType = input.get() & 0xff;
if (dataType == 9) { // object end marker
break;
}
array.put(key, readAMFData(input, dataType));
}
return array;
}
private static Object readAMFEcmaArray(ByteBuffer input) {
long size = input.getInt();
Map<String, Object> array = new HashMap<String, Object>();
for (int i = 0; i < size; i++) {
String key = readAMFString(input);
int dataType = input.get() & 0xff;
array.put(key, readAMFData(input, dataType));
}
return array;
}
public static VideoTagHeader parseVideoTagHeader(ByteBuffer dup) {
byte b0 = dup.get();
int frameType = (b0 & 0xff) >> 4;
int codecId = (b0 & 0xf);
Codec codec = videoCodecMapping[codecId];
if (codecId == 7) {
byte avcPacketType = dup.get();
int compOffset = (dup.getShort() << 8) | (dup.get() & 0xff);
return new AvcVideoTagHeader(codec, frameType, avcPacketType, compOffset);
}
return new VideoTagHeader(codec, frameType);
}
public static TagHeader parseAudioTagHeader(ByteBuffer dup) {
byte b = dup.get();
int codecId = (b & 0xff) >> 4;
int sampleRate = sampleRates[(b >> 2) & 0x3];
if (codecId == 4 || codecId == 11)
sampleRate = 16000;
if (codecId == 5 || codecId == 14)
sampleRate = 8000;
int sampleSizeInBits = (b & 0x2) == 0 ? 8 : 16;
boolean signed = codecId != 3 && codecId != 0 || sampleSizeInBits == 16;
int channelCount = 1 + (b & 1);
if (codecId == 11)
channelCount = 1;
AudioFormat audioFormat = new AudioFormat(sampleRate, sampleSizeInBits, channelCount, signed,
codecId == 3 ? false : platformBigEndian);
Codec codec = audioCodecMapping[codecId];
if (codecId == 10) {
byte packetType = dup.get();
return new AacAudioTagHeader(codec, audioFormat, packetType);
}
return new AudioTagHeader(codec, audioFormat);
}
public static int probe(ByteBuffer buf) {
try {
readHeader(buf);
return 100;
} catch (RuntimeException e) {
return 0;
}
}
public void reset() throws IOException {
initialRead(ch);
}
public void reposition() throws IOException {
reset();
if (!positionAtPacket(readBuf)) {
throw new RuntimeException("Could not find at FLV tag start");
}
}
public static boolean positionAtPacket(ByteBuffer readBuf) {
// We will be using the fact that <payload size> = <start of last
// packet> - 15
ByteBuffer dup = readBuf.duplicate();
int payloadSize = 0;
NIOUtils.skip(dup, 5);
while (dup.hasRemaining()) {
payloadSize = ((payloadSize & 0xffff) << 8) | (dup.get() & 0xff);
int pointerPos = dup.position() + 7 + payloadSize;
if (dup.position() >= 8 && pointerPos < dup.limit() - 4 && dup.getInt(pointerPos) - payloadSize == 11) {
readBuf.position(dup.position() - 8);
return true;
}
}
return false;
}
/**
* Searching for the next tag in a file after corrupt segment
*
* @return
* @throws IOException
*/
public boolean repositionFile() throws IOException {
int payloadSize = 0;
for (int i = 0; i < REPOSITION_BUFFER_READS; i++) {
while (readBuf.hasRemaining()) {
payloadSize = ((payloadSize & 0xffff) << 8) | (readBuf.get() & 0xff);
int pointerPos = readBuf.position() + 7 + payloadSize;
if (readBuf.position() >= 8 && pointerPos < readBuf.limit() - 4
&& readBuf.getInt(pointerPos) - payloadSize == 11) {
readBuf.position(readBuf.position() - 8);
return true;
}
}
initialRead(ch);
if (!readBuf.hasRemaining())
break;
}
return false;
}
}