/* * myLib - https://github.com/taktod/myLib * Copyright (c) 2014 ttProject. All rights reserved. * * Licensed under The MIT license. */ package com.ttProject.chunk.mpegts; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.log4j.Logger; import com.ttProject.chunk.IMediaChunk; import com.ttProject.chunk.MediaChunkManager; import com.ttProject.chunk.mpegts.analyzer.IPesAnalyzer; import com.ttProject.media.IAudioData; import com.ttProject.media.Unit; import com.ttProject.media.mpegts.CodecType; 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; /** * mpegtsのchunkを取り出すための動作マネージャー * 基本的にgetChunksにUnitデータ(flvのTagとかmpegtsのPacketとか)をいれると * 対応したMediaChunkがでてくる。 * * TODO このchunkManagerは中途で音声や映像のtrackが追加されることは想定していないので、そういう場合に誤動作する可能性があります。 * VLCの出力データは得意ではないということですね。 * audioがpcrになっているデータは想定済みになってます。 * またVLCの出力データでAC-3を扱ったのだが、myLib.media.ac3をつくらないと対応無理。 */ public class MpegtsChunkManager extends MediaChunkManager { /** ロガー */ private Logger logger = Logger.getLogger(MpegtsChunkManager.class); /** sdtデータ */ private final Sdt sdt; /** patデータ */ private Pat pat = null; /** pmtデータ */ private Pmt pmt = null; /** 処理中のaudioData保持オブジェクト */ private AudioDataList audioDataList = new AudioDataList(); /** 処理中のvideoData保持オブジェクト */ private VideoDataList videoDataList = new VideoDataList(); /** すでに処理済みのpts値 */ private long passedPts = 0; /** 現在処理中のchunkオブジェクト */ private MpegtsChunk chunk = null; /** 解析用のオブジェクト */ private Set<IPesAnalyzer> analyzers = new HashSet<IPesAnalyzer>(); /** * コンストラクタ * @throws Exception */ public MpegtsChunkManager() throws Exception { sdt = new Sdt(); sdt.writeDefaultProvider("taktodTools", "mpegtsChunkMuxer"); } /** * pesの解析オブジェクトを登録する。 * <note> * setupTracksを先に設定してください。(設定後のpmtをベースに処理するため) * </note> * @param pesAnalyzer */ public void addPesAnalyzer(IPesAnalyzer pesAnalyzer) { // pmtを設置してやらないと自分がpcrであるかわからない。 pesAnalyzer.setAudioDataList(audioDataList); pesAnalyzer.setVideoDataList(videoDataList); pesAnalyzer.analyze(pmt, 0); // pmtを先行して送っておきます。 analyzers.add(pesAnalyzer); } /** * トラック情報をいれておきます。(mpegtsの場合はpatやpmtから読み取るので必要ありません。) * あらかじめ宣言したいときにいれておくと、すんなり動作します。 * なお、すでに定義済みの場合は例外をなげるようにします。 * 再定義したい場合はpmtを作り直してください。 */ public void setupTracks(CodecType videoCodec, CodecType audioCodec) throws Exception { if(pat != null || pmt != null || pmt == null || pmt.getFields().size() != 0) { throw new Exception("すでにfield定義がおわっています。"); } Pat pat = new Pat(); analyzePat(pat); Pmt pmt = new Pmt(); if(videoCodec != null) { pmt.addNewField(PmtElementaryField.makeNewField(videoCodec)); } if(audioCodec != null) { pmt.addNewField(PmtElementaryField.makeNewField(audioCodec)); } analyzePmt(pmt); } /** * chunkを取り出します。 */ @Override public IMediaChunk getChunk(Unit unit) throws Exception { if(unit instanceof Pat) { // mpegtsのpatの場合 analyzePat((Pat) unit); } else if(unit instanceof Pmt) { // mpegtsのpmtの場合 analyzePmt((Pmt) unit); } // データをanalyzeしてchunkが取得できたらそこでおわり。 // それ以外の場合はnullを返す。 for(IPesAnalyzer analyzer : analyzers) { analyzer.analyze(unit, 0); } return checkCompleteChunk(); // 完了したchunkについて調査する。 } /** * patを解析します * @param pat */ private void analyzePat(Pat pat) { if(this.pat != null) { return; } this.pat = pat; } /** * pmtを解析します * @param pmt */ private void analyzePmt(Pmt pmt) throws Exception { // TODO あとで下記変わることがあるので、この点注意 if(this.pmt != null) { return; } this.pmt = pmt; for(PmtElementaryField field : pmt.getFields()) { switch(field.getCodecType()) { case VIDEO_H264: videoDataList.analyzePmt(pmt, field); break; case AUDIO_AAC: case AUDIO_MPEG1: audioDataList.analyzePmt(pmt, field); break; default: break; } } } /** * chunkが作成完了したか確認する。 * @return */ private IMediaChunk checkCompleteChunk() throws Exception { // 先頭情報が抜け落ちている場合は処理できない。 if(sdt == null || pat == null || pmt == null) { return null; } // 処理したいtimestampを求めておく long targetPts = passedPts + (long)(90000 * getDuration()); // 映像と音声のdurationについて確認しておく。 // 問題のduration以上データがのこっていることを確認しておく。 if((videoDataList.getCodecType() == null || (videoDataList.getCodecType() != null && videoDataList.getLastDataPts() > targetPts)) && (audioDataList.getCodecType() == null || (audioDataList.getCodecType() != null && audioDataList.getLastDataPts() > targetPts))) { // 映像音声ともにあるデータの場合は、keyFrame間の音声データが満了しているか確認しておく。 if(videoDataList.getCodecType() != null && audioDataList.getCodecType() != null // 両方メディアがある && videoDataList.getSecondDataPts() != -1 // videoDataのkeyFrameが2つ以上内包されている && videoDataList.getSecondDataPts() > audioDataList.getLastDataPts()) { // 2つ目までのaudioデータがcompleteしている // 満了していなさそうなので、処理をスキップ return null; } // すでにデータがたまっている。 // mpegtsChunkにデータをいれていく必要あり。 if(chunk == null) { chunk = makeNewChunk(); // 開始時の時刻を書き込んでおきたい。 if(pmt.getPcrPid() == audioDataList.getPid()) { // 音声のpidとpcrPidが一致する場合 chunk.setTimestamp(audioDataList.getFirstDataPts()); } else { // それ以外の場合は映像を採用します chunk.setTimestamp(videoDataList.getFirstDataPts()); } } // unitを作成する。 IMediaChunk resultChunk = makeFrameUnit(targetPts); if(resultChunk != null) { // 前のデータが完成しているので、次のデータにうつりたい。 chunk = null; } // 必要な長さのデータができていたら応答する。 return resultChunk; } return null; } /** * 動作用のchunkを生成します。 * @return */ protected MpegtsChunk makeNewChunk() throws Exception { MpegtsChunk chunk = new MpegtsChunk(); chunk.write(sdt.getBuffer()); chunk.write(pat.getBuffer()); chunk.write(pmt.getBuffer()); return chunk; } /** * frameunitを作成します。 * ただし、映像のあるなし、音声のあるなしによって変わります。 * @param targetPts -1ならすべて */ private IMediaChunk makeFrameUnit(long targetPts) throws Exception { if(videoDataList.getCodecType() == null) { // 音声のみの場合 return makeAudioOnlyFrameUnit(targetPts); } else if(audioDataList.getCodecType() == null) { // 映像のみの場合 return makeVideoOnlyFrameUnit(targetPts); } else { // 両方ある場合 return makeNormalFrameUnit(targetPts); } } /** * 音声のみのframeUnitをつくります。 * @param targetPts -1ならすべてのデータ * @throws Exception */ private IMediaChunk makeAudioOnlyFrameUnit(long targetPts) throws Exception { int audioSize = 0; List<IAudioData> audioList = new ArrayList<IAudioData>(); long audioStartPts = audioDataList.getFirstDataPts(); while(true) { IAudioData audioData = audioDataList.shift(); if(audioData == null || (targetPts != -1 && audioDataList.getFirstDataPts() > targetPts)) { // データがなくなった場合もしくは、データが問題のptsを超えた場合 if(audioData != null) { audioDataList.unshift(audioData); } if(audioSize != 0) { // 処理おわり ここまでこれたということはchunkができたということ。 makeAudioPes(audioSize, audioList, audioStartPts); } // getFirstDataPtsでデータがなくても音声に限っていえばduration値を応答することは可能。 long durationTimestamp = audioDataList.getFirstDataPts() - chunk.getTimestamp(); chunk.setDuration(durationTimestamp / 90000F); passedPts = audioDataList.getFirstDataPts(); break; } audioSize += audioData.getSize(); // データサイズを計算 audioList.add(audioData); // データを追加リストに登録 // ある程度以上データがたまっていたら追加計算しておく。 if(audioSize > 0x1000) { // 書き込み実行 makeAudioPes(audioSize, audioList, audioStartPts); audioList.clear(); audioSize = 0; audioStartPts = audioDataList.getFirstDataPts(); } } return chunk; } /** * 映像のみのframeUnitをつくります * @param targetPts -1ならすべて取り出します * @return */ private IMediaChunk makeVideoOnlyFrameUnit(long targetPts) throws Exception { // pesデータをvideoDataListから引き出していく。 List<Pes> videoList = new ArrayList<Pes>(); while(true) { Pes videoPes = videoDataList.shift(); if(videoPes != null && videoPes.isPayloadUnitStart()) { makeVideoPes(videoList); videoList.clear(); } if(videoPes == null || // もうvideoPesがない場合 (targetPts != -1 && (videoPes.isAdaptationFieldExist() && videoPes.getAdaptationField().getRandomAccessIndicator() == 1) && // keyFrameで (videoPes.hasPts() && videoPes.getPts().getPts() > targetPts))) { // pts値が目標のptsを超えている場合 if(videoPes != null) { videoDataList.unshift(videoPes); } if(videoList.size() != 0) { makeVideoPes(videoList); videoList.clear(); } // データが残っている場合は記録しなければいけな・・・いことないか long durationTimestamp = videoDataList.getFirstDataPts() - chunk.getTimestamp(); chunk.setDuration(durationTimestamp / 90000F); passedPts = videoDataList.getFirstDataPts(); break; } // pesがある場合は書き込んでいく。 videoList.add(videoPes); } return chunk; } /** * 通常のframeUnitを作ります。 * @param targetPts -1の場合はすべて引き出します * @return */ private IMediaChunk makeNormalFrameUnit(long targetPts) throws Exception { // pesデータをvideoDataListから引き出していく。 int audioSize = 0; List<IAudioData> audioList = new ArrayList<IAudioData>(); List<Pes> videoList = new ArrayList<Pes>(); long audioStartPts = audioDataList.getFirstDataPts(); // はじめのframeの処理をしたというフラグをいれます。(これをいれないとフレーム0で処理がおわることがあります。) boolean firstFlg = true; // 開始前のptsは必要ないか? while(true) { Pes videoPes = videoDataList.shift(); // payloadstartの段階でaudioデータの挿入を気にかける。 if(videoPes != null && videoPes.isPayloadUnitStart() && videoPes.hasPts()) { // いままでにたまったvideoPesについて書き出す makeVideoPes(videoList); videoList.clear(); while(true) { IAudioData audioData = audioDataList.shift(); if(audioData == null) { if(targetPts != -1) { logger.warn("audioDataがnullでした"); // TODO どうしてもaudioDataがnullにならないと動作しないといったことが発生したら、対処を考える throw new Exception("audioDataがnullになることは想定外としておきます。"); } else { break; } } if(audioDataList.getFirstDataPts() > videoPes.getPts().getPts()) { // 現在処理中の映像ptsを超えた場合 if(audioData != null) { audioDataList.unshift(audioData); } // ptsを超えた場合もしくはaudioDataがnullの場合 if(audioSize > 0x1000) { makeAudioPes(audioSize, audioList, audioStartPts); audioList.clear(); audioSize = 0; audioStartPts = audioDataList.getFirstDataPts(); } break; } audioSize += audioData.getSize(); audioList.add(audioData); } } if(videoPes == null || // もうvideoPesがない場合 (targetPts != -1 && (videoPes.isAdaptationFieldExist() && videoPes.getAdaptationField().getRandomAccessIndicator() == 1))) {// keyFrameである場合 if(firstFlg && videoPes != null) { // はじめのデータである場合はフラグをつけて放置する firstFlg = false; } else { // 今回の処理完了時 if(videoPes != null) { videoDataList.unshift(videoPes); } if(videoList.size() != 0) { makeVideoPes(videoList); videoList.clear(); } // ここまできたときにaudioデータがのこっている場合 if(audioSize != 0) { makeAudioPes(audioSize, audioList, audioStartPts); } if(targetPts != -1 && videoDataList.getFirstDataPts() < targetPts) { return null; } // データが残っている場合は記録しなければいけな・・・いことないか long durationTimestamp = videoDataList.getFirstDataPts() - chunk.getTimestamp(); chunk.setDuration(durationTimestamp / 90000F); passedPts = videoDataList.getFirstDataPts(); break; } } // videoPesをためておく。 videoList.add(videoPes); } return chunk; } /** * audio用のpesを作成します。 * @param audioSize * @param audioDataList * @param audioStartPts * @throws Exception */ protected void makeAudioPes(int audioSize, List<IAudioData> audioList, long audioStartPts) throws Exception { ByteBuffer buffer = ByteBuffer.allocate(audioSize); for(IAudioData audioData : audioList) { buffer.put(audioData.getRawData()); } buffer.flip(); Pes audioPes = new Pes(audioDataList.getCodecType(), pmt.getPcrPid() == audioDataList.getPid(), // pcrであるかはフラグ次第 true, // randomAccessは絶対にOK(音声なので) audioDataList.getPid(), // pid buffer, // 実データ audioStartPts); // 開始pts do { chunk.write(audioPes.getBuffer()); } while((audioPes = audioPes.nextPes()) != null); } /** * 動画のデータを書き込む * @param videoList */ protected void makeVideoPes(List<Pes> videoList) throws Exception { for(Pes videoPes : videoList) { chunk.write(videoPes.getBuffer()); } } /** * 現在処理中のchunkについて応答する。 */ @Override public IMediaChunk getCurrentChunk() { return chunk; } /** * 残りデータがある場合はここで応答しなければいけない。 */ @Override public IMediaChunk close() { // すでにデータが枯渇している場合は応答しない。 if(videoDataList.getListCount() == 0 && audioDataList.getListCount() == 0) { return null; } try { // chunkからデータを作って作成しなおす必要あり。 if(chunk == null) { chunk = makeNewChunk(); // 開始時の時刻を書き込んでおきたい。 if(pmt.getPcrPid() == audioDataList.getPid()) { // 音声のpidとpcrPidが一致する場合 chunk.setTimestamp(audioDataList.getFirstDataPts()); } else { // それ以外の場合は映像を採用します chunk.setTimestamp(videoDataList.getFirstDataPts()); } } // 残っているデータをすべて投入しておく。 IMediaChunk resultChunk = makeFrameUnit(-1); if(resultChunk != null) { chunk = null; } return resultChunk; } catch(Exception e) { logger.warn("例外が発生しました。", e); } return null; } /** * {@inheritDoc} */ @Override public String getExt() { return "ts"; } /** * {@inheritDoc} */ @Override @Deprecated public String getHeaderExt() { return "ts"; } /** * {@inheritDoc} */ @Override public long getPassedTic() { return passedPts; } }