要理解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推流的整个过程。 如果你对音视频开发感兴趣,觉得文章对您有帮助,别忘了点赞、收藏哦!或者对本文的一些阐述有自己的看法,有任何问题,欢迎在下方评论区与我讨论!