/* * myLib - https://github.com/taktod/myLib * Copyright (c) 2014 ttProject. All rights reserved. * * Licensed under The MIT license. */ package com.ttProject.media.extra.flv; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import org.apache.log4j.Logger; import com.ttProject.media.extra.mp4.Meta; import com.ttProject.media.extra.mp4.Sond; import com.ttProject.media.extra.mp4.Vdeo; import com.ttProject.media.flv.CodecType; import com.ttProject.media.flv.FlvHeader; import com.ttProject.media.flv.FlvTagOrderManager; import com.ttProject.media.flv.Tag; import com.ttProject.media.flv.amf.Amf0Object; import com.ttProject.media.flv.tag.AudioTag; import com.ttProject.media.flv.tag.MetaTag; import com.ttProject.media.flv.tag.VideoTag; import com.ttProject.nio.channels.IReadChannel; import com.ttProject.util.BufferUtil; /** * mp4からflvのデータを取り出すモデル * flvTagの状態で取り出したい。 * 一度つかったら2度とつかえませんので、2度目やる場合はnewしなおしてください。 * * keyFramesのデータに嘘をいれて、flowplayerで動作できるように調整しておきました。 * @author taktod */ public class FlvOrderModel { private Logger logger = Logger.getLogger(FlvOrderModel.class); /** 終了イベントの取得動作用 */ private IFlvStartEventListener startEventListener = null; /** 処理途上で捨てたデータのサイズ保持 */ private int disposeDataSize = 0; /** 分割用 */ private int divCount = 30; // 解析をすすめたい。 private Vdeo vdeo = null; private Sond sond = null; private Meta meta = null; private int startMilliSeconds; private FlvTagOrderManager orderManager = new FlvTagOrderManager(); private VideoTag videoMshTag = null; private AudioTag audioMshTag = null; /** * コンストラクタ */ public FlvOrderModel(IReadChannel idxFile, boolean videoFlg, boolean soundFlg, int startMilliSecond) throws Exception { this.startMilliSeconds = startMilliSecond; initialize(idxFile, videoFlg, soundFlg); } /** * 開始位置イベントリスナーを設定しておきます。 * @param startEventListener */ public void addStartEvent(IFlvStartEventListener startEventListener) { this.startEventListener = startEventListener; } /** * flvのheaderを応答しておく * なお、開始位置が遅すぎて対象パケットがない場合もありうるので、ほんとうは注意が必要 * @return */ public FlvHeader getFlvHeader() { FlvHeader flvHeader = new FlvHeader(); flvHeader.setVideoFlg(vdeo != null); flvHeader.setAudioFlg(sond != null); return flvHeader; } /** * 映像のmshデータを参照します * @return */ public VideoTag getVideoMsh() { return videoMshTag; } /** * 音声のmshデータを参照します * @return */ public AudioTag getAudioMsh() { return audioMshTag; } /** * 開始準備をしておく。 */ private void initialize(IReadChannel tmp, boolean videoFlg, boolean soundFlg) throws Exception { while(tmp.position() < tmp.size()) { int position = tmp.position(); ByteBuffer buffer = BufferUtil.safeRead(tmp, 8); int size = buffer.getInt(); String tag = BufferUtil.getDwordText(buffer); if("vdeo".equals(tag)) { if(videoFlg) { vdeo = new Vdeo(position, size); vdeo.analyze(tmp); } tmp.position(position + size); } else if("sond".equals(tag)) { if(soundFlg) { sond = new Sond(position, size); sond.analyze(tmp); } tmp.position(position + size); } else if("meta".equals(tag)) { meta = new Meta(position, size); meta.analyze(tmp); tmp.position(position + size); } } if(vdeo == null && meta != null) { meta.setHeight(0); meta.setWidth(0); } if(sond != null) { sond.getStco().start(tmp, false); // dataPos sond.getStsc().start(tmp, false); // samples in chunk sond.getStsz().start(tmp, false); // sample size sond.getStts().start(tmp, false); // time sond.getStco().nextChunkPos(); // 次のchunkの位置を調べて置きます audioMshTag = sond.createFlvMshTag(tmp); } else { orderManager.setNomoreAudio(); } if(vdeo != null) { vdeo.getStco().start(tmp, false); // dataPos vdeo.getStsc().start(tmp, false); // samples in chunk vdeo.getStsz().start(tmp, false); // sample size vdeo.getStss().start(tmp, false); // keyFrame vdeo.getStts().start(tmp, false); // time vdeo.getStco().nextChunkPos(); // 次のchunkの位置を調べて置きます。 vdeo.getStss().nextKeyFrame(); // 初めのキーフレームの位置について調べておく videoMshTag = vdeo.createFlvMshTag(tmp); } else { orderManager.setNomoreVideo(); } } // すでに応答を返しているかフラグ private boolean startResponse = false; /** * もうデータがない場合はnullまだある場合はlistを返します。 * @return */ public List<Tag> nextTagList(IReadChannel source) throws Exception { // データを解析する。 // 映像のstcoと音声のstcoを比較して前にある方の処理をすすめる。 if(vdeo != null && (sond == null || vdeo.getStco().getChunkPos() < sond.getStco().getChunkPos())) { // 映像の方が前にあるので、映像の処理をすすめます。 analyzeVdeo(source); } else if(sond != null) { // 音声の方が前にあるので、音声の処理をすすめます。 analyzeSond(source); } else { // 両方nullだった場合はどうしようもないです。 return null; } // 応答すべきflvTagを集める。 List<Tag> result = orderManager.getCompleteTags(); if(startResponse) { // すでにデータの応答中ならそのまま返す。 return result; } while(result.size() > 0) { Tag tag = result.get(0); // 先頭をとってみる。 MetaTag metaTag = null; if(vdeo == null) { // vdeoデータがない場合はaudioのみの動作になる。 // 先頭にmshのデータを追加してあとは普通に応答すればOK if(audioMshTag != null) { audioMshTag.setTimestamp(tag.getTimestamp()); result.add(0, audioMshTag); } // メタデータ用のmshをくっつけておく。 if(meta != null) { metaTag = meta.createFlvMetaTag(); metaTag.setTimestamp(tag.getTimestamp()); // injectテスト Amf0Object<String, List<Double>> keyframes = new Amf0Object<String, List<Double>>(); List<Double> times = new ArrayList<Double>(); List<Double> filepositions = new ArrayList<Double>(); int delta = (int)(meta.getDuration() / divCount); for(int i = 0;i < meta.getDuration();i += delta) { times.add(i * 0.001); filepositions.add(i * 1.0); } keyframes.put("times", times); keyframes.put("filepositions", filepositions); metaTag.putData("keyframes", keyframes); result.add(0, metaTag); } startResponse = true; if(startEventListener != null) { startEventListener.start(13 + metaTag.getSize() + getSize()); } break; } else { // vdeoがある場合は動画のデータ // keyFrameが来るまでデータを捨てる必要がある。 if(!(tag instanceof VideoTag) || !((VideoTag)tag).isKeyFrame()) { disposeDataSize += tag.getSize(); result.remove(0); // 先頭のデータは必要ないので、捨てる continue; } // 第一キーフレームなので、ここからはじめることにする。 if(audioMshTag != null) { audioMshTag.setTimestamp(tag.getTimestamp()); result.add(0, audioMshTag); } if(videoMshTag != null) { videoMshTag.setTimestamp(tag.getTimestamp()); result.add(0, videoMshTag); } if(meta != null) { metaTag = meta.createFlvMetaTag(); metaTag.setTimestamp(tag.getTimestamp()); // injectテスト Amf0Object<String, List<Double>> keyframes = new Amf0Object<String, List<Double>>(); List<Double> times = new ArrayList<Double>(); List<Double> filepositions = new ArrayList<Double>(); // この部分がおおきすぎるとoverflowするらしい。こまったもんだ。 int delta = (int)(meta.getDuration() / divCount); logger.info("delta:" + delta); for(int i = 0;i < meta.getDuration();i += delta) { times.add(i * 0.001); filepositions.add(i * 1.0); } keyframes.put("times", times); keyframes.put("filepositions", filepositions); metaTag.putData("keyframes", keyframes); result.add(0, metaTag); } startResponse = true; if(startEventListener != null) { startEventListener.start(13 + metaTag.getSize() + getSize()); } break; } } // 応答 return result; } private long vTimePos = 0; private long sTimePos = 0; private int vSampleCount = 0; private void analyzeVdeo(IReadChannel source) throws Exception { int sourcePos = vdeo.getStco().getChunkPos(); vdeo.getStsc().nextChunk(); int chunkSampleCount = vdeo.getStsc().getSampleCount(); for(int i = 0;i < chunkSampleCount;i ++) { int sampleSize = vdeo.getStsz().nextSampleSize(); if(sampleSize == -1) { throw new Exception("sampleSizeが取得できませんでした。"); } vSampleCount ++; boolean isKeyFrame = (vdeo.getStss().getKeyFrame() == vSampleCount); if(isKeyFrame) { // キーフレーム vdeo.getStss().nextKeyFrame(); } if(vTimePos * 1000 / vdeo.getTimescale() >= startMilliSeconds) { // 書き込むべきデータ VideoTag tag = new VideoTag(); tag.setCodec(CodecType.AVC); tag.setFrameType(isKeyFrame); tag.setTimestamp((int)(vTimePos * 1000 / vdeo.getTimescale())); source.position(sourcePos); tag.setData(source, sampleSize); orderManager.addTag(tag); } else { disposeDataSize += 11 + 4 + 1 + 4 + sampleSize; } // 時間を更新しておく。 int delta = vdeo.getStts().nextDuration(); if(delta == -1) { // 最後まで読み込みが完了しているので、次のデータがとれない。 logger.info("delta値を最後まで読み取ったのでおわりとします。"); break; } sourcePos += sampleSize; vTimePos += delta; } // 一番最後まで処理して、データがもうない場合(stcoが切れた場合)vdeo = nullにしておく。 if(!vdeo.getStco().hasMore()) { logger.info("stcoのデータがこれ以上ないみたいです。"); orderManager.setNomoreVideo(); vdeo = null; } else { vdeo.getStco().nextChunkPos(); } } private void analyzeSond(IReadChannel source) throws Exception { int sourcePos = sond.getStco().getChunkPos(); sond.getStsc().nextChunk(); int chunkSampleCount = sond.getStsc().getSampleCount(); for(int i = 0;i < chunkSampleCount;i ++) { int sampleSize = sond.getStsz().nextSampleSize(); if(sampleSize == -1) { throw new Exception("sampleSizeが取得できませんでした。"); } if(sTimePos * 1000 / sond.getTimescale() > startMilliSeconds) { // 書き込むべきデータ AudioTag tag = new AudioTag(); if(sond.getMsh() == null) { tag.setCodec(CodecType.MP3); } else { tag.setCodec(CodecType.AAC); } tag.setChannels(sond.getChannelCount()); tag.setSampleRate(sond.getSampleRate()); tag.setTimestamp((int)(sTimePos * 1000 / sond.getTimescale())); source.position(sourcePos); tag.setData(source, sampleSize); orderManager.addTag(tag); } else { if(sond.getMsh() == null) { disposeDataSize += 11 + 4 + 1 + sampleSize; } else { disposeDataSize += 11 + 4 + 2 + sampleSize; } } int delta = sond.getStts().nextDuration(); if(delta == -1) { break; } sourcePos += sampleSize; sTimePos += delta; } if(!sond.getStco().hasMore()) { orderManager.setNomoreAudio(); sond = null; } else { sond.getStco().nextChunkPos(); } } public int getSize() { int size = 0; if(vdeo != null) { size += vdeo.getTotalFlvSize(); } if(sond != null) { size += sond.getTotalFlvSize(); } return size - disposeDataSize; } }