/*
* myLib - https://github.com/taktod/myLib
* Copyright (c) 2014 ttProject. All rights reserved.
*
* Licensed under The MIT license.
*/
package com.ttProject.packet.mpegts.nw;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import com.ttProject.media.IAudioData;
import com.ttProject.media.mpegts.IPacketAnalyzer;
import com.ttProject.media.mpegts.Packet;
import com.ttProject.media.mpegts.PacketAnalyzer;
import com.ttProject.media.mpegts.field.AdaptationField;
import com.ttProject.media.mpegts.field.PmtElementaryField;
import com.ttProject.media.mpegts.packet.Pat;
import com.ttProject.media.mpegts.packet.Pes;
import com.ttProject.media.mpegts.packet.Pmt;
import com.ttProject.media.mpegts.packet.Sdt;
import com.ttProject.nio.channels.ByteReadChannel;
import com.ttProject.nio.channels.IReadChannel;
import com.ttProject.packet.IMediaPacket;
import com.ttProject.packet.MediaPacketManager;
/**
* 基本的にデータを受け取ったらそのデータをメモリーにとっておいて、必要な秒数分の音声と映像データが入手できたらOKみたいな感じ。
* 音声データは必要があれば、分解して再構築する必要あり。
* このパケットデータが指定された秒数分のファイルデータとなります。
* Sdt Pat Pmt [keyFrame Audio innerFrame] [keyFrame Audio innerFrame]
* となるようにしておきたいと思います。
* この動作って、mpegtsの入力からmpegtsを作り出すものなので、xuggleとかつかった場合は
* rawDataからmpegtsをつくる動作がほしくなるわけか・・・
* @author taktod
*/
public class MpegtsPacketManager extends MediaPacketManager {
private final Logger logger = Logger.getLogger(MpegtsPacketManager.class);
/** sdtはねつ造します。 */
private final Sdt sdt;
/** patは本家のものを利用します(ただし更新はしません。) */
private Pat pat;
/** pmtも本家のものを利用します(ただし更新はしません。) */
private Pmt pmt;
/** 映像用データのpes保持 */
private final VideoData videoData = new VideoData();
/** 音声用データのpes保持 */
private final AudioData audioData = new AudioData();
/** 処理済みpts値を保持 */
private long passedPts = -1; // 処理済みpts値保持
/**
* コンストラクタ
*/
public MpegtsPacketManager() throws Exception {
sdt = new Sdt();
sdt.writeDefaultProvider("taktodTools", "mpegtsMuxer");
}
// analyzerは外にだしておかないと、初期化時のデータがなくなってエラーになることがあるっぽいですね。
private IPacketAnalyzer analyzer = new PacketAnalyzer();
/** 動作カウンター */
@SuppressWarnings("unused")
private int counter = 0;
/** 出力ターゲットファイル */
// private FileOutputStream fos = null;
/**
* 全体停止時のため対処
* /
protected void finalize() throws Throwable {
if(fos != null) {
try {
fos.close();
}
catch(Exception e) {
}
fos = null;
}
}
/**
* パケットの内容を解析して必要な時間分のデータ(Packet)を応答します。
*/
protected IMediaPacket analizePacket(ByteBuffer buffer) {
IReadChannel readChannel = new ByteReadChannel(buffer);
Packet packet = null;
MpegtsPacket mediaPacket = null;
try {
// mpegtsのパケットについて調査しておく
while((packet = analyzer.analyze(readChannel)) != null) {
if(packet instanceof Pat) {
analyzePat((Pat)packet);
}
else if(packet instanceof Pmt) {
analyzePmt((Pmt)packet);
}
else if(packet instanceof Pes) {
mediaPacket = analyzePes((Pes)packet);
if(mediaPacket != null) {
break;
}
}
}
buffer.position(readChannel.position());
}
catch(Exception e) {
logger.error("aiueo", e);
}
return mediaPacket;
}
/**
* patについて処理する
* @param pat
*/
private void analyzePat(Pat pat) {
// pat指定がはじめての場合のみ受け入れる
if(this.pat != null) {
return;
}
this.pat = pat;
}
/**
* pmtについて処理する
* @param pmt
*/
private void analyzePmt(Pmt pmt) throws Exception {
// pmt指定がはじめての場合のみ受け入れる
if(this.pmt != null) {
return;
}
this.pmt = pmt;
for(PmtElementaryField field : pmt.getFields()) {
// pidとコーデック情報を保持
switch(field.getCodecType()) {
case VIDEO_H264:
videoData.analyzePmt(pmt, field);
break;
case AUDIO_AAC:
case AUDIO_MPEG1:
audioData.analyzePmt(pmt, field);
break;
default:
break;
}
}
}
/**
* pesについて解析する。
* @param pes
* @throws Exception
*/
private MpegtsPacket analyzePes(Pes pes) throws Exception {
// 処理済みpts値がまだ決まっていない場合は現在値を代入しておく。
videoData.analyzePes(pes);
audioData.analyzePes(pes);
if(passedPts == -1) {
if(!pes.hasPts()) {
throw new Exception("ptsのない状態から開始しました。");
}
passedPts = pes.getPts().getPts();
}
long targetDuration = passedPts + (long)(90000L * getDuration()); // targetDuration以上たまっている場合は作成が成功する可能性があるので、作り始める。
if(targetDuration < videoData.getLastDataPts()
&& targetDuration < audioData.getLastDataPts()) {
logger.info("必要以上にデータがたまったので書き込み実行");
// keyframeとそれに従うデータをパケット化しておく。
MpegtsPacket packet = (MpegtsPacket)getCurrentPacket();
if(packet == null) {
packet = new MpegtsPacket();
// 先頭にあるべきデータをいれておく
packet.analize(sdt.getBuffer());
packet.analize(pat.getBuffer());
packet.analize(pmt.getBuffer());
setCurrentPacket(packet);
}
// keyframeとそれに従うデータをパケット化しておく。
packet = makeKeyFrameUnit();
if(packet != null) {
return packet;
}
}
return null;
}
/**
* keyFrameから次のkeyFrameまでのunitを設定しておきます。
* @throws Exception
*/
private MpegtsPacket makeKeyFrameUnit() throws Exception {
// 映像のフレームを書き込む
boolean isFirst = true; // 発動作フラグ
// 補完する音声フレームを書き込む(ある程度以上にならない場合はスキップ)
long audioStartPts = audioData.getFirstDataPts();
List<IAudioData> audioDataList = new ArrayList<IAudioData>();
int audioSize = 0;
Pes videoPes = null;
logger.info("フレーム書き込み開始");
while((videoPes = videoData.shift()) != null) {
if(videoPes.isPayloadUnitStart()) {
logger.info("動画のペイロード");
// payloadUnitの開始位置の場合
if(!isFirst) {
logger.info("音声書き込み");
while(true) {
IAudioData aData = audioData.shift();
if(aData == null || audioData.getFirstDataPts() > videoPes.getPts().getPts()) {
if(aData != null) {
audioData.unshift(aData);
}
// たまったサイズを確認して書き込みを実行
if(audioSize > 0x1000) {
// 書き込み実行
makeAudioPes(audioSize, audioDataList, audioStartPts);
audioDataList.clear();
audioSize = 0;
audioStartPts = audioData.getFirstDataPts();
}
break;
}
audioSize += aData.getSize();
audioDataList.add(aData);
}
// キーフレームでない場合はaudioDataを挿入したい。
if(videoPes.isAdaptationFieldExist() && videoPes.getAdaptationField().getRandomAccessIndicator() == 1) {
logger.info("キーフレーム発見");
// 挿入処理後に確認してkeyFrameだったら、次のデータまできたことになる。
videoData.unshift(videoPes);
break;
}
}
isFirst = false;
}
if(videoPes.isAdaptationFieldExist() && videoPes.hasPts()) {
AdaptationField aField = videoPes.getAdaptationField();
aField.setPcrBase(videoPes.getPts().getPts());
}
// videoPesの値はここで書き込みしてしまえばOK
getCurrentPacket().analize(videoPes.getBuffer());
// fos.getChannel().write(videoPes.getBuffer());
}
logger.info("ループをぬけました。");
passedPts = audioData.getFirstDataPts(); // audioDataに残っているデータの終端位置をしっておきたい?
// たまっているaudioデータがある場合は最後尾に追加しておく。
if(audioSize > 0) {
logger.info("音声の残りデータがあるので、書き込み実施します。");
logger.info(audioSize);
logger.info(audioDataList);
logger.info(audioStartPts);
// 書き込み実行
makeAudioPes(audioSize, audioDataList, audioStartPts);
}
logger.info("フレーム書き込み完了。");
// audioSampleの長さを確認して、対象データより大きくなっていたら完了として扱う。
if(getCurrentPacket().getDuration() > getDuration()) {
// duration以上にデータがたまっているなら、出来上がったことになります。
MpegtsPacket packet = (MpegtsPacket)getCurrentPacket();
setCurrentPacket(null);
return packet;
}
else {
return null;
}
}
/**
* audio用のpesを作成します。
* @param audioSize
* @param audioDataList
* @param audioStartPts
* @throws Exception
*/
private void makeAudioPes(int audioSize, List<IAudioData> audioDataList, long audioStartPts) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate(audioSize);
MpegtsPacket mediaPacket = (MpegtsPacket)getCurrentPacket();
for(IAudioData audioData : audioDataList) {
mediaPacket.addSampleNum(audioData.getSampleNum());
mediaPacket.setAudioSampleRate(audioData.getSampleRate());
buffer.put(audioData.getRawData());
}
buffer.flip();
Pes audioPes = new Pes(audioData.getCodecType(),
audioData.isPcr(), // pcrであるかはフラグ次第
true, // randomAccessは絶対にOK(音声なので)
audioData.getPid(), // pid
buffer, // 実データ
audioStartPts); // 開始pts
do {
mediaPacket.analize(audioPes.getBuffer());
} while((audioPes = audioPes.nextPes()) != null);
}
/**
* 拡張子応答
*/
@Override
public String getExt() {
return ".ts";
}
/**
* リストファイルの拡張子応答
*/
@Override
public String getHeaderExt() {
return ".m3u8";
}
}