/*
* myLib - https://github.com/taktod/myLib
* Copyright (c) 2014 ttProject. All rights reserved.
*
* Licensed under The MIT license.
*/
package com.ttProject.packet.flv;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import org.apache.log4j.Logger;
import com.ttProject.media.flv.CodecType;
import com.ttProject.util.HexUtil;
public class FlvMediaPacket extends FlvPacket {
private Logger logger = Logger.getLogger(FlvMediaPacket.class);
private final FlvHeaderPacket headerPacket;
private final long startPos;
public FlvMediaPacket(FlvPacketManager manager, FlvHeaderPacket headerPacket) {
super(manager);
this.headerPacket = headerPacket;
this.startPos = manager.getCurrentPos();
}
@Override
public boolean isHeader() {
return false;
}
@Override
public boolean analize(ByteBuffer buffer) {
while(buffer.remaining() > 0) {
int position = buffer.position();
Boolean result = null;
byte header = buffer.get();
buffer.position(position);
switch(header) {
case FlvPacketManager.AUDIO_TAG:
result = analizeAudioData(buffer);
break;
case FlvPacketManager.VIDEO_TAG:
result = analizeVideoData(buffer);
break;
case FlvPacketManager.META_TAG:
result = analizeMetaData(buffer);
break;
case FlvPacketManager.FLV_TAG:
result = analizeFlvHeader(buffer);
break;
default:
logger.warn("解析不能なデータがきました。");
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
logger.warn("position:" + position);
logger.warn(HexUtil.toHex(data, true));
throw new RuntimeException("解析不能なデータがきました。" + header);
}
if(result != null) {
buffer.position(position);
return result;
}
}
return false;
}
/**
* 音声データを解析する。
* @param buffer
* @return
*/
protected Boolean analizeAudioData(ByteBuffer buffer) {
if(buffer.remaining() < 11) {
// header部分取得に満たない場合
return false;
}
// ヘッダ情報を取得
byte[] header = new byte[11];
buffer.get(header);
// データサイズを確認する。
int size = getSizeFromHeader(header);
if(buffer.remaining() < size + 4) {
// 十分な量のデータがない。
return false;
}
long time = getTimeFromHeader(header);
getManager().setCurrentPos(time);
// データを取り出す
byte[] body = new byte[size];
buffer.get(body);
// 4byte終端データを確認する。
byte[] tail = new byte[4];
buffer.get(tail);
headerPacket.setAudioCodec(CodecType.getAudioCodecType(body[0]));
// コーデック確認
boolean isSequenceHeader = false;
if(headerPacket.getAudioCodec() == CodecType.AAC) {
// AACなら次のパケットを確認して、headerであるか確認する。
if(body[1] == 0x00) {
// headerだった
ByteBuffer sequenceHeader = ByteBuffer.allocate(size + 4 + 11);
sequenceHeader.put(header);
sequenceHeader.putInt(4, 0); // timestampの位置を強制的に0にしておく
sequenceHeader.put(body);
sequenceHeader.put(tail);
sequenceHeader.flip();
headerPacket.analize(sequenceHeader);
isSequenceHeader = true;
}
}
// sequenceデータではなく
// キーフレームだった場合はパケットの境目と判定しなければいけない。
if(headerPacket.getVideoCodec() == CodecType.NONE && !isSequenceHeader) {
float passedTime = (getManager().getCurrentPos() - startPos) / 1000;
if(passedTime >= getManager().getDuration()) {
// バッファサイズがたまっている場合は、終端がきたことになるので、分割する。
setDuration(passedTime);
// 記録済み時間について記録しておく。
getManager().addPassedTime(getDuration());
return true;
}
}
// シーケンスヘッダも書き込んでおく。(書き込んでおかないと、中途で変更があったときに困る。)
ByteBuffer saveBuffer = getBuffer(size+ 4 + 11);
saveBuffer.put(header);
saveBuffer.put(body);
saveBuffer.put(tail);
return null;
}
/**
* 映像データを解析する。
* @param buffer
* @return
*/
protected Boolean analizeVideoData(ByteBuffer buffer) {
if(buffer.remaining() < 11) {
// header部分取得に満たない場合
return false;
}
// ヘッダ情報を取得
byte[] header = new byte[11];
buffer.get(header);
// データサイズを確認する。
int size = getSizeFromHeader(header);
if(buffer.remaining() < size + 4) {
// 十分な量のデータがない。
return false;
}
long time = getTimeFromHeader(header);
getManager().setCurrentPos(time);
// データを取り出す
byte[] body = new byte[size];
// データの先頭1文字目を確認することでコーデック情報とかがわかります。
// 0xF0でキーフレームかどうか判定できる。
// 0x0Fで、フレームタイプがわかる。
// H.264の場合は、先頭の4バイトは、拡張データになります。
// TODO 本当にそうなっているか確認しなければいけない。0:AVCのヘッダデータ
buffer.get(body);
// 4byte終端データを確認する。
byte[] tail = new byte[4];
buffer.get(tail);
boolean isSequenceHeader = false;
// コーデック確認
headerPacket.setVideoCodec(CodecType.getVideoCodecType(body[0]));
if(headerPacket.getVideoCodec() == CodecType.AVC) {
// AVCなら次のパケットを確認して、headerであるか確認する。
if((body[0] & 0x10) == 0x10 && body[1] == 0x00) {
// headerだった
ByteBuffer sequenceHeader = ByteBuffer.allocate(size + 4 + 11);
sequenceHeader.put(header);
sequenceHeader.putInt(4, 0); // timestampの位置を強制0にしておく。
sequenceHeader.put(body);
sequenceHeader.put(tail);
sequenceHeader.flip();
headerPacket.analize(sequenceHeader);
isSequenceHeader = true;
}
}
// sequenceデータではなく
// キーフレームだった場合はパケットの境目と判定しなければいけない。
if(/*(body[0] & 0x10) == 0x10 && */!isSequenceHeader) {
float passedTime = (getManager().getCurrentPos() - startPos) / 1000f;
if(passedTime >= getManager().getDuration()) {
// バッファサイズがたまっている場合は、終端がきたことになるので、分割する。
setDuration(passedTime);
// 記録済み時間について記録しておく。
getManager().addPassedTime(getDuration());
return true;
}
}
// ここでバッファにカキコする。
ByteBuffer saveBuffer = getBuffer(size+ 4 + 11);
saveBuffer.put(header);
saveBuffer.put(body);
saveBuffer.put(tail);
return null;
}
/**
* metaタグ解析動作
* @param buffer 解析するバッファ(flvデータの先頭であることを期待します。)
* @return true:今回のパケットの解析が完了した。false:中途でデータがたりなくなった。null:解析は完了し、次のデータを解析する必要がある場合
*/
protected Boolean analizeMetaData(ByteBuffer buffer) {
if(buffer.remaining() < 11) {
// header部分取得に満たない場合
return false;
}
// ヘッダ情報を取得
byte[] header = new byte[11];
buffer.get(header);
// データサイズを確認する。
int size = getSizeFromHeader(header);
if(buffer.remaining() < size + 4) {
// 十分な量のデータがない。
return false;
}
long time = getTimeFromHeader(header);
getManager().setCurrentPos(time);
// データを取り出す
byte[] body = new byte[size];
// metaデータは特に興味ないので、すてておく。
buffer.get(body);
// 4byte終端データを確認する。
byte[] tail = new byte[4];
buffer.get(tail);
return null;
}
/**
* Flvのヘッダであるか確認します。
* @return
*/
protected Boolean analizeFlvHeader(ByteBuffer buffer) {
// バッファにデータがきちんと存在しているか確認
if(buffer.remaining() < 13) {
// 足りない。
return false;
}
byte[] data = new byte[13];
buffer.get(data);
for(int i = 0;i < 13;i ++) {
if(i != 4) {
if(data[i] != FlvPacketManager.flvHeader[i]) {
throw new RuntimeException("flvHeaderデータが不正です。");
}
}
else {
if(data[i] != 1 && data[i] != 4 && data[i] != 5) {
throw new RuntimeException("flvHeaderデータのメディア指定が不正です。");
}
}
}
ByteBuffer headerBuffer = ByteBuffer.allocate(FlvPacketManager.flvHeader.length);
headerBuffer.put(FlvPacketManager.flvHeader);
headerBuffer.flip();
headerPacket.analize(headerBuffer);
// MediaTagには書き込まない。
return null;
}
public void writeData(String targetFile, int number, boolean append) {
ByteBuffer buffer = getBuffer(0);
try {
WritableByteChannel channel = Channels.newChannel(new FileOutputStream(targetFile, append));
// 先頭にサイズ crc値 index値をいれておく必要あり。
ByteBuffer header = ByteBuffer.allocate(12);
buffer.flip();
header.putInt(buffer.remaining());
header.putInt(getManager().getCRC());
header.putInt(number);
header.flip();
channel.write(header);
// データ実体を書き込む
channel.write(buffer);
}
catch (Exception e) {
}
}
}