纠纷奇闻社交美文家庭
投稿投诉
家庭城市
爱好生活
创业男女
能力餐饮
美文职业
心理周易
母婴奇趣
两性技能
社交传统
新闻范文
工作个人
思考社会
作文职场
家居中考
兴趣安全
解密魅力
奇闻笑话
写作笔记
阅读企业
饮食时事
纠纷案例
初中历史
说说童话
乐趣治疗

Android中使用ffmpeg编码进行rtmp推流

1月9日 暗影泪投稿
  要理解RTMP推流,我们就要知道详细原理。
  本文将详细的来给大家介绍RTMP推流原理以及如何推送到服务器,首先我们了解一下推流的全过程:
  我们将会分为几个小节来展开:一。本文用到的库文件:
  1。1本项目用到的库文件如下图所示,用到了ffmpeg库,以及编码视频的x264,编码音频的fdkaac,推流使用的rtmp等:
  使用静态链接库,最终把这些。a文件打包到libstream中,Android。mk如下LOCALPATH:(callmydir)include(CLEARVARS)LOCALMODULE:avformatLOCALSRCFILES:(LOCALPATH)liblibavformat。ainclude(PREBUILTSTATICLIBRARY)include(CLEARVARS)LOCALMODULE:avcodecLOCALSRCFILES:(LOCALPATH)liblibavcodec。ainclude(PREBUILTSTATICLIBRARY)include(CLEARVARS)LOCALMODULE:swscaleLOCALSRCFILES:(LOCALPATH)liblibswscale。ainclude(PREBUILTSTATICLIBRARY)include(CLEARVARS)LOCALMODULE:avutilLOCALSRCFILES:(LOCALPATH)liblibavutil。ainclude(PREBUILTSTATICLIBRARY)include(CLEARVARS)LOCALMODULE:swresampleLOCALSRCFILES:(LOCALPATH)liblibswresample。ainclude(PREBUILTSTATICLIBRARY)include(CLEARVARS)LOCALMODULE:postprocLOCALSRCFILES:(LOCALPATH)liblibpostproc。ainclude(PREBUILTSTATICLIBRARY)include(CLEARVARS)LOCALMODULE:x264LOCALSRCFILES:(LOCALPATH)liblibx264。ainclude(PREBUILTSTATICLIBRARY)include(CLEARVARS)LOCALMODULE:libyuvLOCALSRCFILES:(LOCALPATH)liblibyuv。ainclude(PREBUILTSTATICLIBRARY)include(CLEARVARS)LOCALMODULE:libfdkaacLOCALCINCLUDES(LOCALPATH)includeLOCALSRCFILES:(LOCALPATH)liblibfdkaac。ainclude(PREBUILTSTATICLIBRARY)include(CLEARVARS)LOCALMODULE:polarsslLOCALSRCFILES:(LOCALPATH)liblibpolarssl。ainclude(PREBUILTSTATICLIBRARY)include(CLEARVARS)LOCALMODULE:rtmpLOCALSRCFILES:(LOCALPATH)liblibrtmp。ainclude(PREBUILTSTATICLIBRARY)include(CLEARVARS)LOCALMODULE:libstreamLOCALSRCFILES:StreamProcess。cppFrameEncoder。cppAudioEncoder。cppwavreader。cRtmpLivePublish。cppLOCALCINCLUDES(LOCALPATH)includeLOCALSTATICLIBRARIES:libyuvavformatavcodecswscaleavutilswresamplepostprocx264libfdkaacpolarsslrtmpLOCALLDLIBSL(LOCALPATH)prebuiltlloglzIpthreadinclude(BUILDSHAREDLIBRARY)
  具体使用到哪些库中的接口我们将再下面进行细节展示。
  【相关学习资料推荐,点击下方链接免费报名,报名后会弹出学习资料免费领取地址】
  【免费】FFmpegWebRTCRTMPNDKAndroid音视频流媒体高级开发学习视频教程腾讯课堂
  C音视频更多学习资料:点击莬费领取音视频开发(资料文档视频教程面试题)(FFmpegWebRTCRTMPRTSPHLSRTP)
  二。如何从Camera摄像头获取视频流:
  2。1Camera获取视频流,这个就不用多说了,只需要看到这个回调就行了,我们需要获取到这个数据:CameraSurfaceView。java中OverridepublicvoidonPreviewFrame(byte〔〕data,Cameracamera){camera。addCallbackBuffer(data);if(listener!null){listener。onCallback(data);}}阻塞线程安全队列,生产者和消费者privateLinkedBlockingQueuebyte〔〕mQueuenewLinkedBlockingQueue();。。。。。。。。。。。OverridepublicvoidonCallback(finalbyte〔〕srcData){if(srcData!null){try{mQueue。put(srcData);}catch(InterruptedExceptione){e。printStackTrace();}}。。。。。。。
  2。2NV21转化为YUV420P数据我们知道一般的摄像头的数据都是NV21或者是NV12,接下来我们会用到第一个编码库libyuv库,我们先来看看这个消费者怎么从NV21的数据转化为YUV的workThreadnewThread(){Overridepublicvoidrun(){while(loop!Thread。interrupted()){try{获取阻塞队列中的数据,没有数据的时候阻塞byte〔〕srcDatamQueue。take();生成I420(YUV标准格式数据及YUV420P)目标数据,生成后的数据长度widthheight32finalbyte〔〕dstDatanewbyte〔scaleWidthscaleHeight32〕;finalintmorientationmCameraUtil。getMorientation();压缩NV21(YUV420SP)数据,元素数据位10801920,很显然这样的数据推流会很占用带宽,我们压缩成480640的YUV数据为啥要转化为YUV420P数据?因为是在为转化为H264数据在做准备,NV21不是标准的,只能先通过转换,生成标准YUV420P数据,然后把标准数据encode为H264流StreamProcessManager。compressYUV(srcData,mCameraUtil。getCameraWidth(),mCameraUtil。getCameraHeight(),dstData,scaleHeight,scaleWidth,0,morientation,morientation270);进行YUV420P数据裁剪的操作,测试下这个借口,我们可以对数据进行裁剪,裁剪后的数据也是I420数据,我们采用的是libyuv库文件这个libyuv库效率非常高,这也是我们用它的原因finalbyte〔〕cropDatanewbyte〔cropWidthcropHeight32〕;StreamProcessManager。cropYUV(dstData,scaleWidth,scaleHeight,cropData,cropWidth,cropHeight,cropStartX,cropStartY);自此,我们得到了YUV420P标准数据,这个过程实际上就是NV21转化为YUV420P数据注意,有些机器是NV12格式,只是数据存储不一样,我们一样可以用libyuv库的接口转化if(yuvDataListener!null){yuvDataListener。onYUVDataReceiver(cropData,cropWidth,cropHeight);}设置为true,我们把生成的YUV文件用播放器播放一下,看我们的数据是否有误,起调试作用if(SAVEFILEFORTEST){fileManager。saveFileData(cropData);}}catch(InterruptedExceptione){e。printStackTrace();}}}};
  2。3介绍一下摄像头的数据流格式
  视频流的转换,android中一般摄像头的格式是NV21或者是NV12,它们都是YUV420sp的一种,那么什么是YUV格式呢?
  何为YUV格式,有三个分量,Y表示明亮度,也就是灰度值,U和V则表示色度,即影像色彩饱和度,用于指定像素的颜色,(直接点就是Y是亮度信息,UV是色彩信息),YUV格式分为两大类,planar和packed两种:对于planar的YUV格式,先连续存储所有像素点Y,紧接着存储所有像素点U,随后所有像素点V对于packed的YUV格式,每个像素点YUV是连续交替存储的
  YUV格式为什么后面还带数字呢,比如YUV420,444,442YUV444:每一个Y对应一组UV分量YUV422:每两个Y共用一组UV分量YUV420:每四个Y公用一组UV分量
  实际上NV21,NV12就是属于YUV420,是一种twoplane模式,即Y和UV分为两个Plane,UV为交错存储,他们都属于YUV420SP,举个例子就会很清晰了NV21格式数据排列方式是YYYYYYYY(wh)VUVUVUVU(wh2),对于NV12的格式,排列方式是YYYYYYYY(wh)UVUVUVUV(wh2)
  正如代码注释中所说的那样,我们以标准的YUV420P为例,对于这样的格式,我们要取出Y,U,V这三个分量,我们看怎么取?比如480640大小的图片,其字节数为48064031个字节Y分量:480640个字节U分量:4806402个字节V分量:4806402个字节,加起来就为48064031个字节存储都是行优先存储,三部分之间顺序是YUV依次存储,即0480640是Y分量;48064048064054为U分量;4806405448064032是V分量,
  记住这个计算方法,等下在JNI中马上会体现出来
  那么YUV420SP和YUV420P的区别在哪里呢?显然Y的排序是完全相同的,但是UV排列上原理是完全不同的,420P它是先吧U存放完后,再放V,也就是说UV是连续的,而420SP它是UV,UV这样交替存放:YUV420SP格式:
  YUV420P格式:
  所以NV21(YUV420SP)的数据如下:同样的以480640大小的图片为例,其字节数为48064031个字节Y分量:480640个字节UV分量:4806401个字节(注意,我们没有把UV分量分开)加起来就为48064031个字节
  【相关学习资料推荐,点击下方链接免费报名,报名后会弹出学习资料免费领取地址】
  【免费】FFmpegWebRTCRTMPNDKAndroid音视频流媒体高级开发学习视频教程腾讯课堂
  下面我们来看看两个JNI函数,这个是摄像头转化的两个最关键的函数NV21转化为YUV420P数据paramsrc原始数据paramwidth原始数据宽度paramheight原始数据高度paramdst生成数据paramdstwidth生成数据宽度paramdstheight生成数据高度parammode模式paramdegree角度paramisMirror是否镜像returnpublicstaticnativeintcompressYUV(byte〔〕src,intwidth,intheight,byte〔〕dst,intdstwidth,intdstheight,intmode,intdegree,booleanisMirror);YUV420P数据的裁剪paramsrc原始数据paramwidth原始数据宽度paramheight原始数据高度paramdst生成数据paramdstwidth生成数据宽度paramdstheight生成数据高度paramleft裁剪的起始x点paramtop裁剪的起始y点returnpublicstaticnativeintcropYUV(byte〔〕src,intwidth,intheight,byte〔〕dst,intdstwidth,intdstheight,intleft,inttop);
  再看一看具体实现JNIEXPORTjintJNICALLJavacomriemannleeliveprojectStreamProcessManagercompressYUV(JNIEnvenv,jclasstype,jbyteArraysrc,jintwidth,jintheight,jbyteArraydst,jintdstwidth,jintdstheight,jintmode,jintdegree,jbooleanisMirror){jbyteSrcdataenvGetByteArrayElements(src,NULL);jbyteDstdataenvGetByteArrayElements(dst,NULL);nv21转化为i420(标准YUV420P数据)这个tempi420data大小是和Srcdata是一样的nv21ToI420(Srcdata,width,height,tempi420data);进行缩放的操作,这个缩放,会把数据压缩scaleI420(tempi420data,width,height,tempi420datascale,dstwidth,dstheight,mode);如果是前置摄像头,进行镜像操作if(isMirror){进行旋转的操作rotateI420(tempi420datascale,dstwidth,dstheight,tempi420datarotate,degree);因为旋转的角度都是90和270,那后面的数据width和height是相反的mirrorI420(tempi420datarotate,dstheight,dstwidth,Dstdata);}else{进行旋转的操作rotateI420(tempi420datascale,dstwidth,dstheight,Dstdata,degree);}envReleaseByteArrayElements(dst,Dstdata,0);envReleaseByteArrayElements(src,Srcdata,0);return0;}
  我们从java层传递过来的参数可以看到,原始数据是10801920,先转为10801920的标准的YUV420P的数据,下面的代码就是上面我举的例子,如何拆分YUV420P的Y,U,V分量和如何拆分YUV420SP的Y,UV分量,最后调用libyuv库的libyuv::NV21ToI420数据就完成了转换;然后进行缩放,调用了libyuv::I420Scale的函数完成转换NV21转化为YUV420P数据voidnv21ToI420(jbytesrcnv21data,jintwidth,jintheight,jbytesrci420data){Y通道数据大小U通道数据大小jintsrcusize(width1)(height1);NV21中Y通道数据jbytesrcnv21ydatasrcnv21由于是连续存储的Y通道数据后即为VU数据,它们的存储方式是交叉存储的jbytesrcnv21vudatasrcnv21YUV420P中Y通道数据jbytesrci420ydatasrci420YUV420P中U通道数据jbytesrci420udatasrci420YUV420P中V通道数据jbytesrci420vdatasrci420直接调用libyuv中接口,把NV21数据转化为YUV420P标准数据,此时,它们的存储大小是不变的libyuv::NV21ToI420((constuint8)srcnv21ydata,width,(constuint8)srcnv21vudata,width,(uint8)srci420ydata,width,(uint8)srci420udata,width1,(uint8)srci420vdata,width1,width,height);}进行缩放操作,此时是把10801920的YUV420P的数据480640的YUV420P的数据voidscaleI420(jbytesrci420data,jintwidth,jintheight,jbytedsti420data,jintdstwidth,jintdstheight,jintmode){Y数据大小widthheight,U数据大小为14的widthheight,V大小和U一样,一共是32的widthheight大小jintsrci420jintsrci420usize(width1)(height1);由于是标准的YUV420P的数据,我们可以把三个通道全部分离出来jbytesrci420ydatasrci420jbytesrci420udatasrci420datasrci420jbytesrci420vdatasrci420datasrci420ysizesrci420由于是标准的YUV420P的数据,我们可以把三个通道全部分离出来jintdsti420jintdsti420usize(dstwidth1)(dstheight1);jbytedsti420ydatadsti420jbytedsti420udatadsti420datadsti420jbytedsti420vdatadsti420datadsti420ysizedsti420调用libyuv库,进行缩放操作libyuv::I420Scale((constuint8)srci420ydata,width,(constuint8)srci420udata,width1,(constuint8)srci420vdata,width1,width,height,(uint8)dsti420ydata,dstwidth,(uint8)dsti420udata,dstwidth1,(uint8)dsti420vdata,dstwidth1,dstwidth,dstheight,(libyuv::FilterMode)mode);}
  至此,我们就把摄像头的NV21数据转化为YUV420P的标准数据了,这样,我们就可以把这个数据流转化为H264了,接下来,我们来看看如何把YUV420P流数据转化为h264数据,从而为推流做准备三标准YUV420P数据编码为H264
  多说无用,直接上代码
  3。1代码如何实现h264编码的:编码类MediaEncoder,主要是把视频流YUV420P格式编码为h264格式,把PCM裸音频转化为AAC格式publicclassMediaEncoder{privatestaticfinalStringTAGMediaEprivateThreadvideoEncoderThread,audioEncoderTprivatebooleanvideoEncoderLoop,audioEncoderL视频流队列privateLinkedBlockingQueueVideoDatavideoQ音频流队列privateLinkedBlockingQueueaudioQ。。。。。。。。。摄像头的YUV420P数据,put到队列中,生产者模型publicvoidputVideoData(VideoDatavideoData){try{videoQueue。put(videoData);}catch(InterruptedExceptione){e。printStackTrace();}}。。。。。。。。。videoEncoderThreadnewThread(){Overridepublicvoidrun(){视频消费者模型,不断从队列中取出视频流来进行h264编码while(videoEncoderLoop!Thread。interrupted()){try{队列中取视频数据VideoDatavideoDatavideoQueue。take();byte〔〕outbuffernewbyte〔videoData。widthvideoData。height〕;int〔〕buffLengthnewint〔10〕;对YUV420P进行h264编码,返回一个数据大小,里面是编码出来的h264数据intnumNalsStreamProcessManager。encoderVideoEncode(videoData。videoData,videoData。videoData。length,fps,outbuffer,buffLength);Log。e(RiemannLee,data。lengthvideoData。videoData。lengthh264encodelengthbuffLength〔0〕);if(numNals0){int〔〕segmentnewint〔numNals〕;System。arraycopy(buffLength,0,segment,0,numNals);inttotalLength0;for(inti0;isegment。i){totalLengthsegment〔i〕;}Log。i(RiemannLee,totalLengthtotalLength);编码后的h264数据byte〔〕encodeDatanewbyte〔totalLength〕;System。arraycopy(outbuffer,0,encodeData,0,encodeData。length);if(sMediaEncoderCallback!null){sMediaEncoderCallback。receiveEncoderVideoData(encodeData,encodeData。length,segment);}我们可以把数据在java层保存到文件中,看看我们编码的h264数据是否能播放,h264裸数据可以在VLC播放器中播放if(SAVEFILEFORTEST){videoFileManager。saveFileData(encodeData);}}}catch(InterruptedExceptione){e。printStackTrace();}}}};videoEncoderLvideoEncoderThread。start();}
  至此,我们就把摄像头的NV21数据转化为YUV420P的标准数据了,这样,我们就可以把这个数据流转化为H264了,接下来,我们来看看如何把YUV420P流数据转化为h264数据,从而为推流做准备三标准YUV420P数据编码为H264
  多说无用,直接上代码
  3。1代码如何实现h264编码的:编码类MediaEncoder,主要是把视频流YUV420P格式编码为h264格式,把PCM裸音频转化为AAC格式publicclassMediaEncoder{privatestaticfinalStringTAGMediaEprivateThreadvideoEncoderThread,audioEncoderTprivatebooleanvideoEncoderLoop,audioEncoderL视频流队列privateLinkedBlockingQueueVideoDatavideoQ音频流队列privateLinkedBlockingQueueaudioQ。。。。。。。。。摄像头的YUV420P数据,put到队列中,生产者模型publicvoidputVideoData(VideoDatavideoData){try{videoQueue。put(videoData);}catch(InterruptedExceptione){e。printStackTrace();}}。。。。。。。。。videoEncoderThreadnewThread(){Overridepublicvoidrun(){视频消费者模型,不断从队列中取出视频流来进行h264编码while(videoEncoderLoop!Thread。interrupted()){try{队列中取视频数据VideoDatavideoDatavideoQueue。take();byte〔〕outbuffernewbyte〔videoData。widthvideoData。height〕;int〔〕buffLengthnewint〔10〕;对YUV420P进行h264编码,返回一个数据大小,里面是编码出来的h264数据intnumNalsStreamProcessManager。encoderVideoEncode(videoData。videoData,videoData。videoData。length,fps,outbuffer,buffLength);Log。e(RiemannLee,data。lengthvideoData。videoData。lengthh264encodelengthbuffLength〔0〕);if(numNals0){int〔〕segmentnewint〔numNals〕;System。arraycopy(buffLength,0,segment,0,numNals);inttotalLength0;for(inti0;isegment。i){totalLengthsegment〔i〕;}Log。i(RiemannLee,totalLengthtotalLength);编码后的h264数据byte〔〕encodeDatanewbyte〔totalLength〕;System。arraycopy(outbuffer,0,encodeData,0,encodeData。length);if(sMediaEncoderCallback!null){sMediaEncoderCallback。receiveEncoderVideoData(encodeData,encodeData。length,segment);}我们可以把数据在java层保存到文件中,看看我们编码的h264数据是否能播放,h264裸数据可以在VLC播放器中播放if(SAVEFILEFORTEST){videoFileManager。saveFileData(encodeData);}}}catch(InterruptedExceptione){e。printStackTrace();}}}};videoEncoderLvideoEncoderThread。start();}
  这个就是如何把YUV420P数据转化为h264流,主要代码是这个JNI函数,接下来我们看是如何编码成h264的,编码函数如下:编码视频数据接口paramsrcFrame原始数据(YUV420P数据)paramframeSize帧大小paramfpsfpsparamdstFrame编码后的数据存储paramoutFramewSize编码后的数据大小returnpublicstaticnativeintencoderVideoEncode(byte〔〕srcFrame,intframeSize,intfps,byte〔〕dstFrame,int〔〕outFramewSize);
  JNI中视频流的编码接口,我们看到的是初始化一个FrameEncoder类,然后调用这个类的encodeFrame接口去编码初始化视频编码JNIEXPORTjintJNICALLJavacomriemannleeliveprojectStreamProcessManagerencoderVideoinit(JNIEnvenv,jclasstype,jintjwidth,jintjheight,jintjoutwidth,jintjoutheight){frameEncodernewFrameEncoder();frameEncodersetInWidth(jwidth);frameEncodersetInHeight(jheight);frameEncodersetOutWidth(joutwidth);frameEncodersetOutHeight(joutheight);frameEncodersetBitrate(128);frameEncoderopen();return0;}视频编码主要函数,注意JNI函数GetByteArrayElements和ReleaseByteArrayElements成对出现,否则回内存泄露JNIEXPORTjintJNICALLJavacomriemannleeliveprojectStreamProcessManagerencoderVideoEncode(JNIEnvenv,jclasstype,jbyteArrayjsrcFrame,jintjframeSize,jintcounter,jbyteArrayjdstFrame,jintArrayjdstFrameSize){jbyteSrcdataenvGetByteArrayElements(jsrcFrame,NULL);jbyteDstdataenvGetByteArrayElements(jdstFrame,NULL);jintdstFrameSizeenvGetIntArrayElements(jdstFrameSize,NULL);intnumNalsframeEncoderencodeFrame((char)Srcdata,jframeSize,counter,(char)Dstdata,dstFrameSize);envReleaseByteArrayElements(jdstFrame,Dstdata,0);envReleaseByteArrayElements(jsrcFrame,Srcdata,0);envReleaseIntArrayElements(jdstFrameSize,dstFrameSize,0);returnnumN}
  下面我们来详细的分析FrameEncoder这个C类,这里我们用到了多个库,第一个就是鼎鼎大名的ffmpeg库,还有就是X264库,下面我们先来了解一下h264的文件结构,这样有利于我们理解h264的编码流程
  3。2h264我们必须知道的一些概念:首先我们来介绍h264字节流,先来了解下面几个概念,h264由哪些东西组成呢?1。VCLvideocodinglayer视频编码层;2。NALnetworkabstractionlayer网络提取层;其中,VCL层是对核心算法引擎,块,宏块及片的语法级别的定义,他最终输出编码完的数据SODBSODB:StringofDataBits,数据比特串,它是最原始的编码数据RBSP:RawByteSequencePayload,原始字节序载荷,它是在SODB的后面添加了结尾比特和若干比特0,以便字节对齐EBSP:EncapsulateByteSequencePayload,扩展字节序列载荷,它是在RBSP基础上添加了防校验字节0x03后得到的。关系大致如下:SODBRBSPSTOPbit0bitsRBSPRBSPpart10x03RBSPpart20x03RBSPpartnEBSPNALUHeaderEBSPNALU(NAL单元)startcodeNALUstartcodeNALUH。264ByteStream
  NALU头结构长度:1byte(1个字节)forbiddenbit(1bit)nalreferencebit(2bit)nalunittype(5bit)1。forbiddenbit:禁止位,初始为0,当网络发现NAL单元有比特错误时可设置该比特为1,以便接收方纠错或丢掉该单元。2。nalreferencebit:nal重要性指示,标志该NAL单元的重要性,值越大,越重要,解码器在解码处理不过来的时候,可以丢掉重要性为0的NALU。
  NALU类型结构图:
  其中,nalunittype为1,2,3,4,5及12的NAL单元称为VCL的NAL单元,其他类型的NAL单元为非VCL的NAL单元。
  【相关学习资料推荐,点击下方链接免费报名,报名后会弹出学习资料免费领取地址】
  【免费】FFmpegWebRTCRTMPNDKAndroid音视频流媒体高级开发学习视频教程腾讯课堂
  C音视频更多学习资料:点击莬费领取音视频开发(资料文档视频教程面试题)(FFmpegWebRTCRTMPRTSPHLSRTP)
  对应的代码定义如下publicstaticfinalintNALUNKNOWN0;publicstaticfinalintNALSLICE1;非关键帧publicstaticfinalintNALSLICEDPA2;publicstaticfinalintNALSLICEDPB3;publicstaticfinalintNALSLICEDPC4;publicstaticfinalintNALSLICEIDR5;关键帧publicstaticfinalintNALSEI6;publicstaticfinalintNALSPS7;SPSpublicstaticfinalintNALPPS8;PPSpublicstaticfinalintNALAUD9;publicstaticfinalintNALFILLER12;
  由上面我们可以知道,h264字节流,就是由一些startcodeNALU组成的,要组成一个NALU单元,首先要有原始数据,称之为SODB,它是原始的H264数据编码得到到,不包括3字节(0x000001)4字节(0x00000001)的startcode,也不会包括1字节的NALU头,NALU头部信息包括了一些基础信息,比如NALU类型。ps:起始码包括两种,3字节0x000001和4字节0x00000001,在sps和pps和AccessUnit的第一个NALU使用4字节起始码,其余情况均使用3字节起始码
  在H264SPEC中,RBSP定义如下:在SODB结束处添加表示结束的bit1来表示SODB已经结束,因此添加的bit1成为rbspstoponebit,RBSP也需要字节对齐,为此需要在rbspstoponebit后添加若干0补齐,简单来说,要在SODB后面追加两样东西就形成了RBSPrbspstoponebit1rbspalignmentzerobit(s)0(s)
  RBSP的生成过程:
  即RBSP最后一个字节包含SODB最后几个比特,以及trailingbits其中,第一个比特位1,其余的比特位0,保证字节对齐,最后再结尾处添加0x0000,即CABACZEROWORD,从而形成RBSP。
  EBSP的生成过程:NALU数据起始码就形成了AnnexB格式(下面有介绍H264的两种格式,AnnexB为常用的格式),起始码包括两种,0x000001和0x00000001,为了不让NALU的主体和起始码之间产生竞争,在对RBSP进行扫描的时候,如果遇到连续两个0x00字节,则在该两个字节后面添加一个0x03字节,在解码的时候将该0x03字节去掉,也称为脱壳操作。解码器在解码时,首先逐个字节读取NAL的数据,统计NAL的长度,然后再开始解码。替换规则如下:0x0000000x000003000x0000010x000003010x0000020x000003020x0000030x00000303
  3。3下面我们找一个h264文件来看看
  0000000167。。。这个为SPS,67为NALUHeader,有type信息,后面即为我们说的EBSP0000000168。。。这个为PPS00000106。。。为SEI补充增强信息00000165。。。为IDR关键帧,图像中的编码slice
  对于这个SPS集合,从67type后开始计算,即42c033a680b41e6840000003004000000ca3c60ca8正如前面的描述,解码的时候直接03这个03是竞争检测
  从前面我们分析知道,VCL层出来的是编码完的视频帧数据,这些帧可能是I,B,P帧,而且这些帧可能属于不同的序列,在这同一个序列还有相对应的一套序列参数集和图片参数集,所以要完成视频的解码,不仅需要传输VCL层编码出来的视频帧数据,还需要传输序列参数集,图像参数集等数据。
  参数集:包括序列参数集SPS和图像参数集PPS
  SPS:包含的是针对一连续编码视频序列的参数,如标识符seqparametersetid,帧数以及POC的约束,参数帧数目,解码图像尺寸和帧场编码模式选择标识等等PPS:对应的是一个序列中某一副图像或者某几幅图像,其参数如标识符picparametersetid、可选的seqparametersetid、熵编码模式选择标识,片组数目,初始量化参数和去方块滤波系数调整标识等等数据分割:组成片的编码数据存放在3个独立的DP(数据分割A,B,C)中,各自包含一个编码片的子集,分割A包含片头和片中宏块头数据分割包含帧内和SI片宏块的编码残差数据。分割C包含帧间宏块的编码残差数据。每个分割可放在独立的NAL单元并独立传输。
  NALU的顺序要求H264AVC标准对送到解码器的NAL单元是由严格要求的,如果NAL单元的顺序是混乱的,必须将其重新依照规范组织后送入解码器,否则不能正确解码1。序列参数集NAL单元必须在传送所有以此参数集为参考的其它NAL单元之前传送,不过允许这些NAL单元中中间出现重复的序列参数集合NAL单元。所谓重复的详细解释为:序列参数集NAL单元都有其专门的标识,如果两个序列参数集NAL单元的标识相同,就可以认为后一个只不过是前一个的拷贝,而非新的序列参数集2。图像参数集NAL单元必须在所有此参数集为参考的其它NAL单元之前传送,不过允许这些NAL单元中间出现重复的图像参数集NAL单元,这一点与上述的序列参数集NAL单元是相同的。3。不同基本编码图像中的片段(slice)单元和数据划分片段(datapartition)单元在顺序上不可以相互交叉,即不允许属于某一基本编码图像的一系列片段(slice)单元和数据划分片段(datapartition)单元中忽然出现另一个基本编码图像的片段(slice)单元片段和数据划分片段(datapartition)单元。4。参考图像的影响:如果一幅图像以另一幅图像为参考,则属于前者的所有片段(slice)单元和数据划分片段(datapartition)单元必须在属于后者的片段和数据划分片段之后,无论是基本编码图像还是冗余编码图像都必须遵守这个规则。5。基本编码图像的所有片段(slice)单元和数据划分片段(datapartition)单元必须在属于相应冗余编码图像的片段(slice)单元和数据划分片段(datapartition)单元之前。6。如果数据流中出现了连续的无参考基本编码图像,则图像序号小的在前面。7。如果arbitrarysliceorderallowedflag置为1,一个基本编码图像中的片段(slice)单元和数据划分片段(datapartition)单元的顺序是任意的,如果arbitrarysliceorderallowedflag置为零,则要按照片段中第一个宏块的位置来确定片段的顺序,若使用数据划分,则A类数据划分片段在B类数据划分片段之前,B类数据划分片段在C类数据划分片段之前,而且对应不同片段的数据划分片段不能相互交叉,也不能与没有数据划分的片段相互交叉。8。如果存在SEI(补充增强信息)单元的话,它必须在它所对应的基本编码图像的片段(slice)单元和数据划分片段(datapartition)单元之前,并同时必须紧接在上一个基本编码图像的所有片段(slice)单元和数据划分片段(datapartition)单元后边。假如SEI属于多个基本编码图像,其顺序仅以第一个基本编码图像为参照。9。如果存在图像分割符的话,它必须在所有SEI单元、基本编码图像的所有片段slice)单元和数据划分片段(datapartition)单元之前,并且紧接着上一个基本编码图像那些NAL单元。10。如果存在序列结束符,且序列结束符后还有图像,则该图像必须是IDR(即时解码器刷新)图像。序列结束符的位置应当在属于这个IDR图像的分割符、SEI单元等数据之前,且紧接着前面那些图像的NAL单元。如果序列结束符后没有图像了,那么它的就在比特流中所有图像数据之后。11。流结束符在比特流中的最后。
  h264有两种封装,一种是Annexb模式,传统模式,有startcode,SPS和PPS是在ES中一种是mp4模式,一般mp4mkv会有,没有startcode,SPS和PPS以及其它信息被封装在container中,每一个frame前面是这个frame的长度很多解码器只支持annexb这种模式,因此需要将mp4做转换我们讨论的是第一种Annexb传统模式,
  3。4下面我们直接看代码,了解一下如何使用X264来编码h264文件x264paramdefaultpreset():为了方便使用x264,只需要根据编码速度的要求和视频质量的要求选择模型,并修改部分视频参数即可x264picturealloc():为图像结构体x264picturet分配内存。x264encoderopen():打开编码器。x264encoderencode():编码一帧图像。x264encoderclose():关闭编码器。x264pictureclean():释放x264picturealloc()申请的资源。存储数据的结构体如下所示。x264picturet:存储压缩编码前的像素数据。x264nalt:存储压缩编码后的码流数据。下面介绍几个重要的结构体x264imaget结构用于存放一帧图像实际像素数据。该结构体定义在x264。h中typedefstruct{设置彩色空间,通常取值X264CSPI420,所有可能取值定义在x264。h中图像平面个数,例如彩色空间是YUV420格式的,此处取值3intistride〔4〕;每个图像平面的跨度,也就是每一行数据的字节数uint8tplane〔4〕;每个图像平面存放数据的起始地址,plane〔0〕是Y平面,plane〔1〕和plane〔2〕分别代表U和V平面}x264x264picturet结构体描述视频帧的特征,该结构体定义在x264。h中。typedefstruct{帧的类型,取值有X264TYPEKEYFRAMEX264TYPEPX264TYPEAUTO等。初始化为auto,则在编码过程自行控制。intiqpplus1;此参数减1代表当前帧的量化参数值帧的结构类型,表示是帧还是场,是逐行还是隔行,取值为枚举值picstructe,定义在x264。h中输出:是否是关键帧int64一帧的显示时间戳int64输出:解码时间戳。当一帧的pts非常接近0时,该dts值可能为负。编码器参数设置,如果为NULL则表示继续使用前一帧的设置。某些参数(例如aspectratio)由于收到H264本身的限制,只能每隔一个GOP才能改变。这种情况下,如果想让这些改变的参数立即生效,则必须强制生成一个IDR帧。x264x264存放一帧图像的真实数据x264x264输出:HRD时间信息,仅当inalhrd设置了才有效私有数据存放区,将输入数据拷贝到输出帧中}x264x264nalt中的数据在下一次调用x264encoderencode之后就无效了,因此必须在调用x264encoderencode或x264encoderheaders之前使用或拷贝其中的数据。typedefstruct{Nal的优先级Nal的类型是否采用长前缀码0x00000001如果Nal为一条带,则表示该条带第一个宏块的指数如果Nal为一条带,则表示该条带最后一个宏块的指数payload的字节大小uint8存放编码后的数据,已经封装成Nal单元}x264
  再来看看编码h264源码初始化视频编码JNIEXPORTjintJNICALLJavacomriemannleeliveprojectStreamProcessManagerencoderVideoinit(JNIEnvenv,jclasstype,jintjwidth,jintjheight,jintjoutwidth,jintjoutheight){frameEncodernewFrameEncoder();frameEncodersetInWidth(jwidth);frameEncodersetInHeight(jheight);frameEncodersetOutWidth(joutwidth);frameEncodersetOutHeight(joutheight);frameEncodersetBitrate(128);frameEncoderopen();return0;}FrameEncoder。cpp源文件供测试文件使用,测试的时候打开defineENCODEOUTFILE1供测试文件使用defineENCODEOUTFILE2FrameEncoder::FrameEncoder():inwidth(0),inheight(0),outwidth(0),outheight(0),fps(0),encoder(NULL),numnals(0){ifdefENCODEOUTFILE1constcharoutfile1sdcard2222。h264;out1fopen(outfile1,wb);endififdefENCODEOUTFILE2constcharoutfile2sdcard3333。h264;out2fopen(outfile2,wb);endif}boolFrameEncoder::open(){intr0;intnheader0;intheadersize0;if(!validateSettings()){}if(encoder){LOGI(Alreadyopened。firstcallclose());}setencoderparameterssetParams();按照色度空间分配内存,即为图像结构体x264picturet分配内存,并返回内存的首地址作为指针icsp(图像颜色空间参数,目前只支持I420YUV420)为X264CSPI420x264picturealloc(picin,params。icsp,params。iwidth,params。iheight);createtheencoderusingourparams打开编码器encoderx264encoderopen(ms);if(!encoder){LOGI(Cannotopentheencoder);close();}writeheadersrx264encoderheaders(encoder,nals,nheader);if(r0){LOGI(x264encoderheaders()failed);}}编码h264帧intFrameEncoder::encodeFrame(charinBytes,intframeSize,intpts,charoutBytes,intoutFrameSize){YUV420P数据转化为h264inti420inti420usize(inwidth1)(inheight1);inti420vsizei420uint8ti420ydata(uint8t)inBuint8ti420udata(uint8t)inBytesi420uint8ti420vdata(uint8t)inBytesi420ysizei420将Y,U,V数据保存到picin。img的对应的分量中,还有一种方法是用AVfillPicture和swsscale来进行变换memcpy(picin。img。plane〔0〕,i420ydata,i420ysize);memcpy(picin。img。plane〔1〕,i420udata,i420usize);memcpy(picin。img。plane〔2〕,i420vdata,i420vsize);andencodeandstoreintopicoutpicin。最主要的函数,x264编码,picin为x264输入,picout为x264输出intframesizex264encoderencode(encoder,nals,numnals,picin,picout);if(framesize){Herefirstfourbytesproceedingthenalunitindicatesframelengthinthavecopy0;编码后,h264数据保存为nal了,我们可以获取到nals〔i〕。type的类型判断是sps还是pps或者是否是关键帧,nals〔i〕。ipayload表示数据长度,nals〔i〕。ppayload表示存储的数据编码后,我们按照nals〔i〕。ipayload的长度来保存copyh264数据的,然后抛给java端用作rtmp发送数据,outFrameSize是变长的,当有spspps的时候大于1,其它时候值为1for(inti0;i){outFrameSize〔i〕nals〔i〕。memcpy(outByteshavecopy,nals〔i〕。ppayload,nals〔i〕。ipayload);havecopynals〔i〕。}ifdefENCODEOUTFILE1fwrite(outBytes,1,framesize,out1);endififdefENCODEOUTFILE2for(inti0;i){outBytes〔i〕(char)nals〔0〕。ppayload〔i〕;}fwrite(outBytes,1,framesize,out2);outFrameS}return1;}
  最后,我们来看看抛往java层的h264数据,在MediaEncoder。java中,函数startVideoEncode:publicvoidstartVideoEncode(){if(videoEncoderLoop){thrownewRuntimeException(必须先停止);}videoEncoderThreadnewThread(){Overridepublicvoidrun(){视频消费者模型,不断从队列中取出视频流来进行h264编码while(videoEncoderLoop!Thread。interrupted()){try{队列中取视频数据VideoDatavideoDatavideoQueue。take();byte〔〕outbuffernewbyte〔videoData。widthvideoData。height〕;int〔〕buffLengthnewint〔10〕;对YUV420P进行h264编码,返回一个数据大小,里面是编码出来的h264数据intnumNalsStreamProcessManager。encoderVideoEncode(videoData。videoData,videoData。videoData。length,fps,outbuffer,buffLength);Log。e(RiemannLee,data。lengthvideoData。videoData。lengthh264encodelengthbuffLength〔0〕);if(numNals0){int〔〕segmentnewint〔numNals〕;System。arraycopy(buffLength,0,segment,0,numNals);inttotalLength0;for(inti0;isegment。i){totalLengthsegment〔i〕;}Log。i(RiemannLee,totalLengthtotalLength);编码后的h264数据byte〔〕encodeDatanewbyte〔totalLength〕;System。arraycopy(outbuffer,0,encodeData,0,encodeData。length);if(sMediaEncoderCallback!null){sMediaEncoderCallback。receiveEncoderVideoData(encodeData,encodeData。length,segment);}我们可以把数据在java层保存到文件中,看看我们编码的h264数据是否能播放,h264裸数据可以在VLC播放器中播放if(SAVEFILEFORTEST){videoFileManager。saveFileData(encodeData);}}}catch(InterruptedExceptione){e。printStackTrace();}}}};videoEncoderLvideoEncoderThread。start();}
  此时,h264数据已经出来了,我们就实现了YUV420P的数据到H264数据的编码,接下来,我们再来看看音频数据。
  3。5android音频数据如何使用fdkaac库来编码音频,转化为AAC数据的,直接上代码publicclassAudioRecoderManager{privatestaticfinalStringTAGAudioRecoderM音频获取privatefinalstaticintSOURCEMediaRecorder。AudioSource。MIC;设置音频采样率,44100是目前的标准,但是某些设备仍然支205060001025privatefinalstaticintSAMPLEHZ44100;设置音频的录制的声道CHANNELINSTEREO为双声道,CHANNELCONFIGURATIONMONO为单声道privatefinalstaticintCHANNELCONFIGAudioFormat。CHANNELINSTEREO;音频数据格式:PCM16位每个样本保证设备支持。PCM8位每个样本不一定能得到设备支持privatefinalstaticintFORMATAudioFormat。ENCODINGPCM16BIT;privateintmBufferSprivateAudioRecordmAudioRprivateintbufferSizeInBytes0;。。。。。。。。。。。。publicAudioRecoderManager(){if(SAVEFILEFORTEST){fileManagernewFileManager(FileManager。TESTPCMFILE);}bufferSizeInBytesAudioRecord。getMinBufferSize(SAMPLEHZ,CHANNELCONFIG,FORMAT);mAudioRecordnewAudioRecord(SOURCE,SAMPLEHZ,CHANNELCONFIG,FORMAT,bufferSizeInBytes);mBufferSize41024;}publicvoidstartAudioIn(){workThreadnewThread(){Overridepublicvoidrun(){mAudioRecord。startRecording();byte〔〕audioDatanewbyte〔mBufferSize〕;intreadsize0;录音,获取PCM裸音频,这个音频数据文件很大,我们必须编码成AAC,这样才能rtmp传输while(loop!Thread。interrupted()){try{readsizemAudioRecord。read(audioData,readsize,mBufferSize);byte〔〕ralAudionewbyte〔readsize〕;每次录音读取4K数据System。arraycopy(audioData,0,ralAudio,0,readsize);if(audioDataListener!null){把录音的数据抛给MediaEncoder去编码AAC音频数据audioDataListener。audioData(ralAudio);}我们可以把裸音频以文件格式存起来,判断这个音频是否是好的,只需要加一个WAV头即形成WAV无损音频格式if(SAVEFILEFORTEST){fileManager。saveFileData(ralAudio);}readsize0;Arrays。fill(audioData,(byte)0);}catch(Exceptione){e。printStackTrace();}}}};workThread。start();}publicvoidstopAudioIn(){workThread。interrupt();mAudioRecord。stop();mAudioRecord。release();mAudioRif(SAVEFILEFORTEST){fileManager。closeFile();测试代码,以WAV格式保存数据啊PcmToWav。copyWaveFile(FileManager。TESTPCMFILE,FileManager。TESTWAVFILE,SAMPLEHZ,bufferSizeInBytes);}}
  我们再来看看MediaEncoder是如何编码PCM裸音频的publicMediaEncoder(){if(SAVEFILEFORTEST){videoFileManagernewFileManager(FileManager。TESTH264FILE);audioFileManagernewFileManager(FileManager。TESTAACFILE);}videoQueuenewLinkedBlockingQueue();audioQueuenewLinkedBlockingQueue();这里我们初始化音频数据,为什么要初始化音频数据呢?音频数据里面我们做了什么事情?audioEncodeBufferStreamProcessManager。encoderAudioInit(Contacts。SAMPLERATE,Contacts。CHANNELS,Contacts。BITRATE);}。。。。。。。。。。。。publicvoidstartAudioEncode(){if(audioEncoderLoop){thrownewRuntimeException(必须先停止);}audioEncoderThreadnewThread(){Overridepublicvoidrun(){byte〔〕outbuffernewbyte〔1024〕;inthaveCopyLength0;byte〔〕inbuffernewbyte〔audioEncodeBuffer〕;while(audioEncoderLoop!Thread。interrupted()){try{AudioDataaudioaudioQueue。take();Log。e(RiemannLee,audio。audioData。lengthaudio。audioData。lengthaudioEncodeBufferaudioEncodeBuffer);finalintaudioGetLengthaudio。audioData。if(haveCopyLengthaudioEncodeBuffer){System。arraycopy(audio。audioData,0,inbuffer,haveCopyLength,audioGetLength);haveCopyLengthaudioGetLintremainaudioEncodeBufferhaveCopyLif(remain0){intvalidLengthStreamProcessManager。encoderAudioEncode(inbuffer,audioEncodeBuffer,outbuffer,outbuffer。length);Log。e(lihuzi,validLengthvalidLength);finalintVALIDLENGTHvalidLif(VALIDLENGTH0){byte〔〕encodeDatanewbyte〔VALIDLENGTH〕;System。arraycopy(outbuffer,0,encodeData,0,VALIDLENGTH);if(sMediaEncoderCallback!null){sMediaEncoderCallback。receiveEncoderAudioData(encodeData,VALIDLENGTH);}if(SAVEFILEFORTEST){audioFileManager。saveFileData(encodeData);}}haveCopyLength0;}}}catch(InterruptedExceptione){e。printStackTrace();}}}};audioEncoderLaudioEncoderThread。start();}
  【相关学习资料推荐,点击下方链接免费报名,报名后会弹出学习资料免费领取地址】
  【免费】FFmpegWebRTCRTMPNDKAndroid音视频流媒体高级开发学习视频教程腾讯课堂
  C音视频更多学习资料:点击莬费领取音视频开发(资料文档视频教程面试题)(FFmpegWebRTCRTMPRTSPHLSRTP)
  进入audio的jni编码音频初始化JNIEXPORTjintJNICALLJavacomriemannleeliveprojectStreamProcessManagerencoderAudioInit(JNIEnvenv,jclasstype,jintjsampleRate,jintjchannels,jintjbitRate){audioEncodernewAudioEncoder(jchannels,jsampleRate,jbitRate);intvalueaudioEncoderinit();}
  现在,我们进入了AudioEncoder,进入了音频编码的世界AudioEncoder::AudioEncoder(intchannels,intsampleRate,intbitRate){thissampleRatesampleRthisbitRatebitR}。。。。。。。。。。。。初始化fdkaac的参数,设置相关接口使得returnintAudioEncoder::init(){打开AAC音频编码引擎,创建AAC编码句柄if(aacEncOpen(handle,0,channels)!AACENCOK){LOGI(Unabletoopenfdkaacencoder);return1;}下面都是利用aacEncoderSetParam设置参数AACENCAOT设置为aaclcif(aacEncoderSetParam(handle,AACENCAOT,2)!AACENCOK){LOGI(UnabletosettheAOT);return1;}if(aacEncoderSetParam(handle,AACENCSAMPLERATE,sampleRate)!AACENCOK){LOGI(UnabletosetthesampleRate);return1;}AACENCCHANNELMODE设置为双通道if(aacEncoderSetParam(handle,AACENCCHANNELMODE,MODE2)!AACENCOK){LOGI(Unabletosetthechannelmode);return1;}if(aacEncoderSetParam(handle,AACENCCHANNELORDER,1)!AACENCOK){LOGI(Unabletosetthewavchannelorder);return1;}if(aacEncoderSetParam(handle,AACENCBITRATE,bitRate)!AACENCOK){LOGI(Unabletosetthebitrate);return1;}if(aacEncoderSetParam(handle,AACENCTRANSMUX,2)!AACENCOK){0raw2adtsLOGI(UnabletosettheADTStransmux);return1;}if(aacEncoderSetParam(handle,AACENCAFTERBURNER,1)!AACENCOK){LOGI(UnabletosettheADTSAFTERBURNER);return1;}if(aacEncEncode(handle,NULL,NULL,NULL,NULL)!AACENCOK){LOGI(Unabletoinitializetheencoder);return1;}AACENCInfoStructinfo{0};if(aacEncInfo(handle,info)!AACENCOK){LOGI(Unabletogettheencoderinfo);return1;}返回数据给上层,表示每次传递多少个数据最佳,这样encode效率最高intinputSizechannels2info。frameLLOGI(inputSized,inputSize);returninputS}
  我们终于知道MediaEncoder构造函数中初始化音频数据的用意了,它会返回设备中传递多少inputSize为最佳,这样,我们每次只需要传递相应的数据,就可以使得音频效率更优化publicvoidstartAudioEncode(){if(audioEncoderLoop){thrownewRuntimeException(必须先停止);}audioEncoderThreadnewThread(){Overridepublicvoidrun(){byte〔〕outbuffernewbyte〔1024〕;inthaveCopyLength0;byte〔〕inbuffernewbyte〔audioEncodeBuffer〕;while(audioEncoderLoop!Thread。interrupted()){try{AudioDataaudioaudioQueue。take();我们通过fdkaac接口获取到了audioEncodeBuffer的数据,即每次编码多少数据为最优这里我这边的手机每次都是返回的4096即4K的数据,其实为了简单点,我们每次可以让MIC录取4K大小的数据,然后把录取的数据传递到AudioEncoder。cpp中取编码Log。e(RiemannLee,audio。audioData。lengthaudio。audioData。lengthaudioEncodeBufferaudioEncodeBuffer);finalintaudioGetLengthaudio。audioData。if(haveCopyLengthaudioEncodeBuffer){System。arraycopy(audio。audioData,0,inbuffer,haveCopyLength,audioGetLength);haveCopyLengthaudioGetLintremainaudioEncodeBufferhaveCopyLif(remain0){fdkaac编码PCM裸音频数据,返回可用长度的有效字段intvalidLengthStreamProcessManager。encoderAudioEncode(inbuffer,audioEncodeBuffer,outbuffer,outbuffer。length);Log。e(lihuzi,validLengthvalidLength);finalintVALIDLENGTHvalidLif(VALIDLENGTH0){byte〔〕encodeDatanewbyte〔VALIDLENGTH〕;System。arraycopy(outbuffer,0,encodeData,0,VALIDLENGTH);if(sMediaEncoderCallback!null){编码后,把数据抛给rtmp去推流sMediaEncoderCallback。receiveEncoderAudioData(encodeData,VALIDLENGTH);}我们可以把Fdkaac编码后的数据保存到文件中,然后用播放器听一下,音频文件是否编码正确if(SAVEFILEFORTEST){audioFileManager。saveFileData(encodeData);}}haveCopyLength0;}}}catch(InterruptedExceptione){e。printStackTrace();}}}};audioEncoderLaudioEncoderThread。start();}
  我们看AudioEncoder是如何利用fdkaac编码的FdkAAC库压缩裸音频PCM数据,转化为AAC,这里为什么用fdkaac,这个库相比普通的aac库,压缩效率更高paraminBytesparamlengthparamoutBytesparamoutLengthreturnintAudioEncoder::encodeAudio(unsignedcharinBytes,intlength,unsignedcharoutBytes,intoutLength){voidinptr,AACENCBufDescinbuf{0};intinidentifierINAUDIODATA;intinelemsize2;传递input数据给inbufinptrinBinbuf。inbuf。numBufs1;inbuf。bufferIinbuf。bufSinbuf。bufElSAACENCBufDescoutbuf{0};intoutidentifierOUTBITSTREAMDATA;intelSize1;out数据放到outbuf中outptroutBoutbuf。outbuf。numBufs1;outbuf。bufferIoutbuf。bufSizesoutLoutbuf。bufElSizeselSAACENCInArgsinargs{0};inargs。numInSampleslength2;size为pcm字节数AACENCOutArgsoutargs{0};AACENCERROR利用aacEncEncode来编码PCM裸音频数据,上面的代码都是fdkaac的流程步骤if((erraacEncEncode(handle,inbuf,outbuf,inargs,outargs))!AACENCOK){LOGI(Encodingaacfailed);}返回编码后的有效字段长度returnoutargs。numOutB}
  至此,我们终于把视频数据和音频数据编码成功了视频数据:NV21YUV420PH264音频数据:PCM裸音频AAC四。RTMP如何推送音视频流最后我们看看rtmp是如何推流的:我们看看MediaPublisher这个类publicMediaPublisher(){mediaEncodernewMediaEncoder();MediaEncoder。setsMediaEncoderCallback(newMediaEncoder。MediaEncoderCallback(){OverridepublicvoidreceiveEncoderVideoData(byte〔〕videoData,inttotalLength,int〔〕segment){onEncoderVideoData(videoData,totalLength,segment);}OverridepublicvoidreceiveEncoderAudioData(byte〔〕audioData,intsize){onEncoderAudioData(audioData,size);}});rtmpThreadnewThread(publishthread){Overridepublicvoidrun(){while(loop!Thread。interrupted()){try{RunnablerunnablemRunnables。take();runnable。run();}catch(InterruptedExceptione){e。printStackTrace();}}}};rtmpThread。start();}。。。。。。。。。。。。privatevoidonEncoderVideoData(byte〔〕encodeVideoData,inttotalLength,int〔〕segment){intspsLen0;intppsLen0;byte〔〕byte〔〕inthaveCopy0;segment为C传递上来的数组,当为SPS,PPS的时候,视频NALU数组大于1,其它时候等于1for(inti0;isegment。i){intsegmentLengthsegment〔i〕;byte〔〕segmentBytenewbyte〔segmentLength〕;System。arraycopy(encodeVideoData,haveCopy,segmentByte,0,segmentLength);haveCopysegmentLintoffset4;if(segmentByte〔2〕0x01){offset3;}inttypesegmentByte〔offset〕0x1f;Log。d(RiemannLee,typetype);获取到NALU的type,SPS,PPS,SEI,还是关键帧if(typeNALSPS){spsLensegment〔i〕4;spsnewbyte〔spsLen〕;System。arraycopy(segmentByte,4,sps,0,spsLen);Log。e(RiemannLee,NALSPSspsLenspsLen);}elseif(typeNALPPS){ppsLensegment〔i〕4;ppsnewbyte〔ppsLen〕;System。arraycopy(segmentByte,4,pps,0,ppsLen);Log。e(RiemannLee,NALPPSppsLenppsLen);sendVideoSpsAndPPS(sps,spsLen,pps,ppsLen,0);}else{sendVideoData(segmentByte,segmentLength,videoID);}}}。。。。。。。。。。。。privatevoidonEncoderAudioData(byte〔〕encodeAudioData,intsize){if(!isSendAudioSpec){Log。e(RiemannLee,sendAudioSpec);sendAudioSpec(0);isSendAudioS}sendAudioData(encodeAudioData,size,audioID);}
  向rtmp发送视频和音频数据的时候,实际上就是下面几个JNI函数初始化RMTP,建立RTMP与RTMP服务器连接paramurlreturnpublicstaticnativeintinitRtmpData(Stringurl);发送SPS,PPS数据paramspssps数据paramspsLensps长度paramppspps数据paramppsLenpps长度paramtimeStamp时间戳returnpublicstaticnativeintsendRtmpVideoSpsPPS(byte〔〕sps,intspsLen,byte〔〕pps,intppsLen,longtimeStamp);发送视频数据,再发送sps,pps之后paramdataparamdataLenparamtimeStampreturnpublicstaticnativeintsendRtmpVideoData(byte〔〕data,intdataLen,longtimeStamp);发送AACSequenceHEAD头数据paramtimeStampreturnpublicstaticnativeintsendRtmpAudioSpec(longtimeStamp);发送AAC音频数据paramdataparamdataLenparamtimeStampreturnpublicstaticnativeintsendRtmpAudioData(byte〔〕data,intdataLen,longtimeStamp);释放RTMP连接returnpublicstaticnativeintreleaseRtmp();
  再来看看RtmpLivePublish是如何完成这几个jni函数的初始化rtmp,主要是在RtmpLivePublish类完成的JNIEXPORTjintJNICALLJavacomriemannleeliveprojectStreamProcessManagerinitRtmpData(JNIEnvenv,jclasstype,jstringjurl){constcharurlcstrenvGetStringUTFChars(jurl,NULL);复制urlcstr内容到rtmppathcharrtmppath(char)malloc(strlen(urlcstr)1);memset(rtmppath,0,strlen(urlcstr)1);memcpy(rtmppath,urlcstr,strlen(urlcstr));rtmpLivePublishnewRtmpLivePublish();rtmpLivePublishinit((unsignedchar)rtmppath);return0;}发送sps,pps数据JNIEXPORTjintJNICALLJavacomriemannleeliveprojectStreamProcessManagersendRtmpVideoSpsPPS(JNIEnvenv,jclasstype,jbyteArrayjspsArray,jintspsLen,jbyteArrayppsArray,jintppsLen,jlongjstamp){if(rtmpLivePublish){jbytespsdataenvGetByteArrayElements(jspsArray,NULL);jbyteppsdataenvGetByteArrayElements(ppsArray,NULL);rtmpLivePublishaddSequenceH264Header((unsignedchar)spsdata,spsLen,(unsignedchar)ppsdata,ppsLen);envReleaseByteArrayElements(jspsArray,spsdata,0);envReleaseByteArrayElements(ppsArray,ppsdata,0);}return0;}发送视频数据JNIEXPORTjintJNICALLJavacomriemannleeliveprojectStreamProcessManagersendRtmpVideoData(JNIEnvenv,jclasstype,jbyteArrayjvideoData,jintdataLen,jlongjstamp){if(rtmpLivePublish){jbytevideodataenvGetByteArrayElements(jvideoData,NULL);rtmpLivePublishaddH264Body((unsignedchar)videodata,dataLen,jstamp);envReleaseByteArrayElements(jvideoData,videodata,0);}return0;}发送音频Sequence头数据JNIEXPORTjintJNICALLJavacomriemannleeliveprojectStreamProcessManagersendRtmpAudioSpec(JNIEnvenv,jclasstype,jlongjstamp){if(rtmpLivePublish){rtmpLivePublishaddSequenceAacHeader(44100,2,0);}return0;}发送音频Audio数据JNIEXPORTjintJNICALLJavacomriemannleeliveprojectStreamProcessManagersendRtmpAudioData(JNIEnvenv,jclasstype,jbyteArrayjaudiodata,jintdataLen,jlongjstamp){if(rtmpLivePublish){jbyteaudiodataenvGetByteArrayElements(jaudiodata,NULL);rtmpLivePublishaddAccBody((unsignedchar)audiodata,dataLen,jstamp);envReleaseByteArrayElements(jaudiodata,audiodata,0);}return0;}释放RTMP连接JNIEXPORTjintJNICALLJavacomriemannleeliveprojectStreamProcessManagerreleaseRtmp(JNIEnvenv,jclasstype){if(rtmpLivePublish){rtmpLivePublishrelease();}return0;}
  最后再来看看RtmpLivePublish这个推流类是如何推送音视频的,rtmp的音视频流的推送有一个前提,需要首先发送AVCsequenceheader视频同步包的构造AACsequenceheader音频同步包的构造
  下面我们来看看AVCsequence的结构,AVCsequenceheader就是AVCDecoderConfigurationRecord结构
  这个协议对应于下面的代码:AVCDecoderConfigurationRecordconfigurationVersion版本号,1body〔i〕0x01;AVCProfileIndicationsps〔1〕body〔i〕sps〔1〕;profilecompatibilitysps〔2〕body〔i〕sps〔2〕;AVCLevelIndicationsps〔3〕body〔i〕sps〔3〕;6bit的reserved为二进制位111111和2bitlengthSizeMinusOne一般为3,二进制位11,合并起来为11111111,即为0xffbody〔i〕0sps3bit的reserved,二进制位111,5bit的numOfSequenceParameterSets,sps个数,一般为1,及合起来二进制位11100001,即为0xe1body〔i〕0xe1;SequenceParametersSetNALUnits(spssizesps)的数组body〔i〕(spslen8)0body〔i〕spslen0memcpy(body〔i〕,sps,spslen);ppsnumOfPictureParameterSets一般为1,即为0x01body〔i〕0x01;SequenceParametersSetNALUnits(ppssizepps)的数组body〔i〕(ppslen8)0body〔i〕(ppslen)0memcpy(body〔i〕,pps,ppslen);
  对于AACsequenceheader存放的是AudioSpecificConfig结构,该结构则在ISO144963Audio中描述。AudioSpecificConfig结构的描述非常复杂,这里我做一下简化,事先设定要将要编码的音频格式,其中,选择AACLC为音频编码,音频采样率为44100,于是AudioSpecificConfig简化为下表:
  这个协议对应于下面的代码:如上图所示5bitaudioObjectType编码结构类型,AACLC为2二进制位000104bitsamplingFrequencyIndex音频采样索引值,44100对应值是4,二进制位01004bitchannelConfiguration音频输出声道,对应的值是2,二进制位00101bitframeLengthFlag标志位用于表明IMDCT窗口长度0二进制位01bitdependsOnCoreCoder标志位,表面是否依赖与corecoder0二进制位01bitextensionFlag选择了AACLC,这里必须是0二进制位0上面都合成二进制0001001000010000uint16taudioConfig0;这里的2表示对应的是AACLC由于是5个bit,左移11位,变为16bit,2个字节与上一个1111100000000000(0xF800),即只保留前5个bitaudioConfig((211)0xF800);intsampleRateIndexgetSampleRateIndex(sampleRate);if(1sampleRateIndex){free(packet);packetNULL;LOGE(addSequenceAacHeader:nosupportcurrentsampleRate〔d〕,sampleRate);}sampleRateIndex为4,二进制位00000010000000000000011110000000(0x0780)(只保留5bit后4位)audioConfig((sampleRateIndex7)0x0780);sampleRateIndex为4,二进制位0000000000000000000000001111000(0x78)(只保留54后4位)audioConfig((channel3)0x78);最后三个bit都为0保留最后三位111(0x07)audioConfig(00x07);最后得到合成后的数据0001001000010000,然后分别取这两个字节body〔2〕(audioConfig8)0xFF;body〔3〕(audioConfig0xFF);
  至此,我们就分别构造了AVCsequenceheader和AACsequenceheader,这两个结构是推流的先决条件,没有这两个东西,解码器是无法解码的,最后我们再来看看我们把解码的音视频如何rtmp推送发送H264数据parambufparamlenparamtimeStampvoidRtmpLivePublish::addH264Body(unsignedcharbuf,intlen,longtimeStamp){去掉起始码(界定符)if(buf〔2〕0x00){00000001buf4;len4;}elseif(buf〔2〕0x01){000001buf3;len3;}intbodysizelen9;RTMPPacketpacket(RTMPPacket)malloc(RTMPHEADSIZE9len);memset(packet,0,RTMPHEADSIZE);packetmbody(char)packetRTMPHEADSIZE;unsignedcharbody(unsignedchar)当NAL头信息中,type(5位)等于5,说明这是关键帧NAL单元buf〔0〕NALHeader与运算,获取type,根据type判断关键帧和普通帧0000010100011111(0x1f)00000101inttypebuf〔0〕0x1f;Pframe7:AVCbody〔0〕0x27;IDRI帧图像Iframe7:AVCif(typeNALSLICEIDR){body〔0〕0x17;}AVCPacketType1nalunit,NALUs(AVCPacketType1)body〔1〕0x01;compositiontime0x00000024bitbody〔2〕0x00;body〔3〕0x00;body〔4〕0x00;写入NALU信息,右移8位,一个字节的读取body〔5〕(len24)0body〔6〕(len16)0body〔7〕(len8)0body〔8〕(len)0copydatamemcpy(body〔9〕,buf,len);packetmhasAbsTimestamp0;packetmnBodyS当前packet的类型:VideopacketmpacketTypeRTMPPACKETTYPEVIDEO;packetmnChannel0x04;packetmheaderTypeRTMPPACKETSIZELARGE;packetmnInfoField2记录了每一个tag相对于第一个tag(FileHeader)的相对时间packetmnTimeStampRTMPGetTime()sendrtmph264bodydataif(RTMPIsConnected(rtmp)){RTMPSendPacket(rtmp,packet,TRUE);LOGD(sendpacketsendVideoData);}free(packet);}发送rtmpAACdataparambufparamlenparamtimeStampvoidRtmpLivePublish::addAccBody(unsignedcharbuf,intlen,longtimeStamp){intbodysize2RTMPPacketpacket(RTMPPacket)malloc(RTMPHEADSIZElen2);memset(packet,0,RTMPHEADSIZE);packetmbody(char)packetRTMPHEADSIZE;unsignedcharbody(unsignedchar)头信息配置AF00AACRAWdatabody〔0〕0xAF;AACPacketType:1表示AACrawbody〔1〕0x01;specbuf是AACraw数据memcpy(body〔2〕,buf,len);packetmpacketTypeRTMPPACKETTYPEAUDIO;packetmnBodySpacketmnChannel0x04;packetmhasAbsTimestamp0;packetmheaderTypeRTMPPACKETSIZELARGE;packetmnTimeStampRTMPGetTime()LOGI(aacmnTimeStampd,packetmnTimeStamp);packetmnInfoField2sendrtmpaacdataif(RTMPIsConnected(rtmp)){RTMPSendPacket(rtmp,packet,TRUE);LOGD(sendpacketsendAccBody);}free(packet);}
  我们推送RTMP都是调用的libRtmp库的RTMPSendPacket接口,先判断是否rtmp是通的,是的话推流即可,最后,我们看看rtmp是如何连接服务器的:初始化RTMP数据,与rtmp连接paramurlvoidRtmpLivePublish::init(unsignedcharurl){rtmpRTMPAlloc();RTMPInit(rtmp);rtmpLink。timeout5;RTMPSetupURL(rtmp,(char)url);RTMPEnableWrite(rtmp);if(!RTMPConnect(rtmp,NULL)){LOGI(RTMPConnecterror);}else{LOGI(RTMPConnectsuccess。);}if(!RTMPConnectStream(rtmp,0)){LOGI(RTMPConnectStreamerror);}else{LOGI(RTMPConnectStreamsuccess。);}starttimeRTMPGetTime();LOGI(starttimed,starttime);}
  至此,我们终于完成了rtmp推流的整个过程。
  如果你对音视频开发感兴趣,觉得文章对您有帮助,别忘了点赞、收藏哦!或者对本文的一些阐述有自己的看法,有任何问题,欢迎在下方评论区与我讨论!
投诉 评论

亿纬锂能动力储能电池生产基地项目签约落地成都投资约100亿元2月7日,亿纬锂能动力储能电池生产基地项目签约仪式在成都举行,总投资约100亿元的20GWh动力储能电池生产基地项目正式落地成都。据悉,该项目正式签约,不仅将开启双方优势互补、……梅西国家队近12场比赛进球数据打进18球,除对阵波兰均有进球直播吧3月24日讯阿根廷在刚刚结束的友谊赛中20战胜巴拿马,梅西在比赛最后时刻取得进球。VarskySports也统计了梅西过去十二场比赛,在阿根廷国家队的进球数据。对阵……盘点今年最令人失望的签约牛宝加入FPX上榜,欧成结局最惨2021年行将结束,在这一年中,许多战队的表现都没有达到预期,尤其是一些在转会期重金引援,结果却不尽如人意的战队。对此,外媒Dotesports评选了2021年休赛期最令人失望……五星如厕感受,开箱云米智能马桶Nano2Pro想安装智能马桶,结果去了两趟附近家私城都被劝退,卫浴老板一听说是我们小区里的高层就直摇头,说水压不足,要么另加增压泵改水路,要么找物业调压,但无法保证一劳永逸。其实因为这事,业……济南泺口服装城稳字当头逆风扬帆!携手实体经济共克时艰记者张晓燕通讯员程建政峥嵘岁月蓝图绘,烂漫江山锦绣融。2022年,济南泺口服装城以承担泺口商圈区域性经济中心、营商环境提升和疫情风险防控为己任,鼎扛天桥区经济增长的……排空奶水好处多其实排空乳房是指将乳房中多余的乳汁及时排放出来,主要功能就是预防堵奶的发生。但是对于如何判断奶水是否已经排空,很多妈妈都抱有疑问。如何判断乳房是否排空呢?其实判断乳……初瑞雪勇当女性创业先锋,开展文明城市创建进乡镇宣传活动谁说女子不如男?选对行业,女性也能爆发出让常人惊叹的力量。在这个女性觉醒、崛起的新消费时代,无数女性勇于突破,自信绽放,活出精彩,初瑞雪雪大的故事就是最好的例子。作为一名……27寸2K办公更省心!新晋千元爆款显示器T2752Q杀到相信大家在购买东西时,也会抱着追求性价比的心态,毕竟买到物美价廉的产品也证明了我们拥有不错的眼光,对自己也是一个肯定。作为一个职场办公人士来说,在购买显示器时,自然也想在众多品……Android中使用ffmpeg编码进行rtmp推流要理解RTMP推流,我们就要知道详细原理。本文将详细的来给大家介绍RTMP推流原理以及如何推送到服务器,首先我们了解一下推流的全过程:我们将会分为几个小节来展开:一……上海青浦一季度开工36个重点项目,总投资608亿元1月9日上午,青浦区举行赋能长三角共创大未来2023年重大项目建设暨西岑科创中心启动仪式,西岑科创中心、上达河中央公园、华新镇凤溪社区城中村改造等一批重点项目在当天集中启动,全……2022年中国氢能源汽车行业市场现状及发展趋势分析中商情报网讯:氢能源汽车,是指以蓄电池作为辅助能源在普通汽车的基础上,安装了电机、控制器、蓄电池、转把闸把等操纵部件和显示仪表系统的机电一体化的个人交通工具。我国氢能源汽车在力……气质女生穿衣都很简约比如杨采钰倪妮,大方知性,值得借鉴大多数女人的穿搭都追求美丽,而走气质路线的女人穿衣则讲究简约实穿。时尚和潮流会随着时间的推移而不断变化,但气质永远摆在那里,不会改变永远,拥有气质的女人永远是最闪耀、最迷人的。……
老话说脾胃好,百病安!建议降温后多吃4样,脾胃好身体棒海外网友热议GEN击败T1AD差距!Ruler就是Gumay进入4月昴星团伴金星等精彩天象将接连上演图找准时机就出发今夏巴厘岛自由行攻略图来栖霞山景区赏自然美景吸奶器将母乳吸出存水箱,随时加热喂婴儿不影响产妇睡觉,好吗?图出境游推荐带你去雄伟发达的美国看看图全面千岛湖自由行攻略带你游遍你想去的每一个角落生化学家竟是间谍,潜伏十四年才被发现,十八年前的今天终被正法新疆男篮下赛季能否崛起?阵容重组,阿的江面临考验图梵净山海拔是多少人间仙境佛教寺庙多图少林寺旅游攻略推荐带你认识威震世界的中国武术男人弱精症的五大原因它竟居首位军民融合发展基金创立首期规模亿元吃榴莲的大禁忌榴莲不能和什么东西一起吃桓姓宝宝起名字大全最新苹果全新27寸iMac外媒汇总性能很硬核,适合在家办公山西五台山为什么这么出名?预防蠕虫病毒的软件有哪些男性性功能障碍不育怎样进行自我治疗名誉侵权起诉状怎么写无所适从造句用无所适从造句大全儿童辫子最新编法小女孩长发扎发设计另一种暴发户

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找美丽时装彩妆资讯历史明星乐活安卓数码常识驾车健康苹果问答网络发型电视车载室内电影游戏科学音乐整形