/* * myLib - https://github.com/taktod/myLib * Copyright (c) 2014 ttProject. All rights reserved. * * Licensed under The MIT license. */ package com.ttProject.container.flv.type; import java.nio.ByteBuffer; import org.apache.log4j.Logger; import com.ttProject.container.flv.FlvCodecType; import com.ttProject.container.flv.FlvTag; import com.ttProject.frame.IFrame; import com.ttProject.frame.IVideoFrame; import com.ttProject.frame.NullFrame; import com.ttProject.frame.VideoAnalyzer; import com.ttProject.frame.VideoFrame; import com.ttProject.frame.extra.VideoMultiFrame; import com.ttProject.frame.flv1.Flv1Frame; import com.ttProject.frame.flv1.type.DisposableInterFrame; import com.ttProject.frame.h264.ConfigData; import com.ttProject.frame.h264.DataNalAnalyzer; import com.ttProject.frame.h264.H264Frame; import com.ttProject.frame.h264.H264FrameSelector; import com.ttProject.frame.h264.SliceFrame; import com.ttProject.frame.h264.type.PictureParameterSet; import com.ttProject.frame.h264.type.SequenceParameterSet; import com.ttProject.frame.vp6.Vp6Frame; import com.ttProject.nio.channels.ByteReadChannel; import com.ttProject.nio.channels.IReadChannel; import com.ttProject.unit.extra.BitConnector; import com.ttProject.unit.extra.BitLoader; import com.ttProject.unit.extra.bit.Bit24; import com.ttProject.unit.extra.bit.Bit32; import com.ttProject.unit.extra.bit.Bit4; import com.ttProject.unit.extra.bit.Bit8; import com.ttProject.util.BufferUtil; /** * videoTag * @author taktod * TODO check the vp6 frame. how should I deal with first byte of frame data? */ public class VideoTag extends FlvTag { /** logger */ private Logger logger = Logger.getLogger(VideoTag.class); private Bit4 frameType = new Bit4(); // 1 keyFrame 2 inner frame 3 disposable inner frame(h263 only) 4 generated key frame? 5 video info(found on rtmp streaming.) private Bit4 codecId = new Bit4(); private Bit4 horizontalAdjustment = null; // vp6 only private Bit4 verticalAdjustment = null; // vp6 only private Bit32 offsetToAlpha = null; // vp6a only private Bit8 packetType = null; // avc only private Bit24 dts = null; // avc only private ByteBuffer frameBuffer = null; // frameBuffer private ByteBuffer alphaData = null; // alphaData for vp6a private IVideoFrame frame = null; // targetFrame. private VideoAnalyzer frameAnalyzer = null; private boolean frameAppendFlag = false; // flg for frame append. /** * constructor * @param tagType */ public VideoTag(Bit8 tagType) { super(tagType); } /** * constructor */ public VideoTag() { this(new Bit8(0x09)); } /** * set frameAnalyzer * @param analyzer */ public void setFrameAnalyzer(VideoAnalyzer analyzer) { this.frameAnalyzer = analyzer; } /** * {@inheritDoc} */ @Override public void load(IReadChannel channel) throws Exception { if(codecId != null) { BitLoader loader = null; switch(getCodec()) { case H264: channel.position(getPosition() + 16); frameBuffer = BufferUtil.safeRead(channel, getSize() - 16 - 4); if(packetType.get() == 0) { if(frameAnalyzer == null || !(frameAnalyzer instanceof DataNalAnalyzer)) { throw new Exception("frameAnalyzer is not suitable for h264 flv."); } DataNalAnalyzer dataNalAnalyzer = (DataNalAnalyzer)frameAnalyzer; ConfigData configData = new ConfigData(); configData.setSelector((H264FrameSelector)frameAnalyzer.getSelector()); configData.analyzeData(new ByteReadChannel(frameBuffer)); dataNalAnalyzer.setConfigData(configData); } break; case ON2VP6: horizontalAdjustment = new Bit4(); verticalAdjustment = new Bit4(); loader = new BitLoader(channel); loader.load(horizontalAdjustment, verticalAdjustment); frameBuffer = BufferUtil.safeRead(channel, getSize() - 13 - 4); break; case ON2VP6_ALPHA: offsetToAlpha = new Bit32(); loader = new BitLoader(channel); loader.load(offsetToAlpha); int offset = offsetToAlpha.get(); frameBuffer = BufferUtil.safeRead(channel, offset); alphaData = BufferUtil.safeRead(channel, getSize() - 16 - 4 - offset); break; default: channel.position(getPosition() + 12); frameBuffer = BufferUtil.safeRead(channel, getSize() - 12 - 4); break; } } // check the prevTagSize. if(getPrevTagSize() != BufferUtil.safeRead(channel, 4).getInt()) { throw new Exception("end size data is incorrect."); } } /** * ref the codec * @return */ public FlvCodecType getCodec() { return FlvCodecType.getVideoCodecType(codecId.get()); } /** * {@inheritDoc} */ @Override public void minimumLoad(IReadChannel channel) throws Exception { super.minimumLoad(channel); if(getSize() == 15) { // data size can be 0.(h264 end tag and so on...) logger.warn("get the no data tag."); return; } BitLoader loader = new BitLoader(channel); loader.load(frameType, codecId); if(getCodec() == FlvCodecType.H264) { // load the h264 extra data. packetType = new Bit8(); dts = new Bit24(); loader.load(packetType, dts); } } /** * {@inheritDoc} */ @Override protected void requestUpdate() throws Exception { if(frameBuffer == null && frame == null) { throw new Exception("frame data is undefined."); } ByteBuffer frameBuffer = null; if(frameAppendFlag) { // in the case of frame append.(re-establish the data.) IVideoFrame codecCheckFrame = frame; if(frame instanceof VideoMultiFrame) { codecCheckFrame = ((VideoMultiFrame) frame).getFrameList().get(0); } // frameType; // codecId; int sizeEx = 0; if(codecCheckFrame instanceof Flv1Frame) { codecId.set(FlvCodecType.getVideoCodecNum(FlvCodecType.FLV1)); sizeEx = 0; } else if(codecCheckFrame instanceof Vp6Frame) { // TODO need to think about vp6a horizontalAdjustment = new Bit4(); verticalAdjustment = new Bit4(); codecId.set(FlvCodecType.getVideoCodecNum(FlvCodecType.ON2VP6)); sizeEx = 1; } else if(codecCheckFrame instanceof H264Frame) { codecId.set(FlvCodecType.getVideoCodecNum(FlvCodecType.H264)); packetType = new Bit8(1); dts = new Bit24((int)(1.0D * frame.getDts() / frame.getTimebase() * 1000)); sizeEx = 4; } else { throw new Exception("unsuitable frame for flv.:" + frame); } if(frame instanceof DisposableInterFrame) { frameType.set(3); } else { if(frame.isKeyFrame()) { frameType.set(1); } else { frameType.set(2); } } frameBuffer = getFrameBuffer(); // need to update pts, timebase, and size setPts((long)(1.0D * frame.getPts() / frame.getTimebase() * 1000)); setTimebase(1000); setSize(11 + 1 + sizeEx + frameBuffer.remaining() + 4); } else { // no frame append. frameBuffer = getFrameBuffer(); } BitConnector connector = new BitConnector(); ByteBuffer startBuffer = getStartBuffer(); ByteBuffer videoInfoBuffer = connector.connect( frameType, codecId, horizontalAdjustment, verticalAdjustment, // vp6 offsetToAlpha, // vp6a packetType, dts // avc ); ByteBuffer tailBuffer = getTailBuffer(); setData(BufferUtil.connect( startBuffer, videoInfoBuffer, frameBuffer, alphaData, tailBuffer )); } /** * ref the frameBuffer * @return */ private ByteBuffer getFrameBuffer() throws Exception { if(frameBuffer == null) { if(FlvCodecType.getVideoCodecType(codecId.get()) == FlvCodecType.H264) { if(frame instanceof SliceFrame) { frameBuffer = ((SliceFrame) frame).getDataPackBuffer(); } else { // can be h264 multiframe(in the case of sei + sliceIDR.) throw new Exception("unexpected frame for h264.:" + frame); } } else { if(frame instanceof VideoMultiFrame) { throw new Exception("unexpected multiframe.:" + FlvCodecType.getVideoCodecType(codecId.get())); } else { frameBuffer = frame.getData(); } } } return frameBuffer.duplicate(); } /** * analyze frame.0 * @throws Exception */ private void analyzeFrame() throws Exception { if(frameBuffer == null) { throw new Exception("frameBuffer is undefined."); } if(getCodec() == FlvCodecType.H264 && packetType.get() != 1) { // in the case of h264 msh or h264 endOfSequence. no frame. return; } ByteBuffer buffer = frameBuffer; if(frameAnalyzer == null) { throw new Exception("frameAnalyzer is unknown."); } IReadChannel channel = new ByteReadChannel(buffer); // for video, container doesn't have information of width, height. do { IFrame analyzedFrame = frameAnalyzer.analyze(channel); if(analyzedFrame instanceof NullFrame) { continue; } VideoFrame videoFrame = (VideoFrame)analyzedFrame; videoFrame.setPts(getPts()); videoFrame.setTimebase(getTimebase()); if(dts != null) { videoFrame.setDts(dts.get()); } if(frame != null) { if(!(frame instanceof VideoMultiFrame)) { VideoMultiFrame multiFrame = new VideoMultiFrame(); multiFrame.addFrame(frame); frame = multiFrame; } ((VideoMultiFrame)frame).addFrame((IVideoFrame)videoFrame); } else { frame = (IVideoFrame)videoFrame; } } while(channel.size() != channel.position()); // remainFrame from frameAnalyzer IFrame lastFrame = frameAnalyzer.getRemainFrame(); if(lastFrame != null && !(lastFrame instanceof NullFrame)) { VideoFrame videoFrame = (VideoFrame)lastFrame; videoFrame.setPts(getPts()); videoFrame.setTimebase(getTimebase()); if(dts != null) { videoFrame.setDts(dts.get()); } if(frame != null) { if(!(frame instanceof VideoMultiFrame)) { VideoMultiFrame multiFrame = new VideoMultiFrame(); multiFrame.addFrame(frame); frame = multiFrame; } ((VideoMultiFrame)frame).addFrame((IVideoFrame)videoFrame); } else { frame = (IVideoFrame)videoFrame; } } } /** * ref the frame. * @return * @throws Exception */ public IVideoFrame getFrame() throws Exception { if(frame == null) { analyzeFrame(); } return frame; } /** * width * @return * @throws Exception */ public int getWidth() throws Exception { if(frame == null) { analyzeFrame(); } return frame.getWidth(); } /** * height * @return * @throws Exception */ public int getHeight() throws Exception { if(frame == null) { analyzeFrame(); } return frame.getHeight(); } /** * add the frame. * @param frame */ public void addFrame(IVideoFrame tmpFrame) throws Exception { if(tmpFrame == null) { return; } if(!(tmpFrame instanceof IVideoFrame)) { throw new Exception("try to append non-videoFrame for videoTag."); } frameAppendFlag = true; if(frame == null) { frame = tmpFrame; } else if(frame instanceof VideoMultiFrame) { ((VideoMultiFrame) frame).addFrame(tmpFrame); } else { VideoMultiFrame multiFrame = new VideoMultiFrame(); multiFrame.addFrame(frame); multiFrame.addFrame(tmpFrame); frame = multiFrame; } // frameから各情報を復元しないとだめ // 時間情報 // size情報 // streamId(0固定) // tagデータ(frameType, codecId) // (vp6,vp6a,h264の場合の特殊データ) // frameデータ実体 // tail size super.update(); } /** * check is msh? * @return */ public boolean isSequenceHeader() { return getCodec() == FlvCodecType.H264 && packetType.get() == 0; } /** * is key? * @return */ public boolean isKeyFrame() { return frameType.get() == 1; } /** * is disposableInner(flv1) * @return */ public boolean isDisposableInner() { return frameType.get() == 3; } /** * initialize as h264 msh. * @param frame * @param sps * @param pps * @throws Exception */ public void setH264MediaSequenceHeader(H264Frame frame, SequenceParameterSet sps, PictureParameterSet pps) throws Exception { codecId.set(FlvCodecType.getVideoCodecNum(FlvCodecType.H264)); frameType.set(1); // keyFrame packetType = new Bit8(0); dts = new Bit24(0); ConfigData configData = new ConfigData(); frameBuffer = configData.makeConfigData(sps, pps); setPts((long)(1.0D * frame.getPts() / frame.getTimebase() * 1000)); setTimebase(1000); setSize(11 + 1 + 4 + frameBuffer.remaining() + 4); super.update(); } @Override public void setPts(long pts) { if(frame != null && frame instanceof VideoFrame) { VideoFrame vFrame = (VideoFrame) frame; vFrame.setPts(pts * vFrame.getTimebase() / 1000); } super.setPts(pts); } /** * {@inheritDoc} */ @Override public String toString() { StringBuilder data = new StringBuilder(); data.append("VideoTag:"); data.append(" timestamp:").append(getPts()); if(dts != null) { data.append(" dts:").append(dts.get()); } data.append(" codec:").append(getCodec()); try { int width = getWidth(); int height = getHeight(); data.append(" size:").append(width).append("x").append(height); } catch(Exception e) { } return data.toString(); } }