1、概述 本文首先以FFmpeg视频解码为主题,主要介绍了FFmpeg进行解码视频时的主要流程、基本原理;其次,文章还讲述了与FFmpeg视频解码有关的简单应用,包括如何在原有的FFmpeg视频解码的基础上按照一定时间轴顺序播放视频、如何在播放视频时加入seek的逻辑;除此之外,文章重点介绍了解码视频时可能容易遗漏的细节,最后是简单地阐述了下如何封装一个具有基本的视频解码功能的VideoDecoder。2、前言2。1FFmpeg FFmpeg是一套可以用来录制、转换数字音频、视频,并能将其转化为流的开源计算机程序,它可生成用于处理和操作多媒体数据的库,其中包含了先进的音视频解码库libavcodec和音视频格式转换库libavformat。2。2FFmpeg六大常用功能模块libavformat:多媒体文件或协议的封装和解封装库,如mp4、flv等文件封装格式,rtmp、rtsp等网络协议封装格式;libavcodec:音视频解码核心库;libavfilter:音视频、字幕滤镜库;libswscale:图像格式转换库;libswresample:音频重采样库;libavutil:工具库2。3视频解码基础入门解复用(Demux):解复用也可叫解封装。这里有一个概念叫封装格式,封装格式指的是音视频的组合格式,常见的有mp4、flv、mkv等。通俗来讲,封装是将音频流、视频流、字幕流以及其他附件按一定规则组合成一个封装的产物。而解封装起着与封装相反的作用,将一个流媒体文件拆解成音频数据和视频数据等。此时拆分后数据是经过压缩编码的,常见的视频压缩数据格式有h264。 解码(Decode):简单来说,就是对压缩的编码数据解压成原始的视频像素数据,常用的原始视频像素数据格式有yuv。 色彩空间转换(ColorSpaceConvert):通常对于图像显示器来说,它是通过RGB模型来显示图像的,但在传输图像数据时使用YUV模型可以节省带宽。因此在显示图像时就需要将yuv像素格式的数据转换成rgb的像素格式后再进行渲染。渲染(Render):将前面已经解码和进行色彩空间转换的每一个视频帧的数据发送给显卡以绘制在屏幕画面上。3、引入FFmpeg前的准备工作3。1FFmpegso库编译在FFmpeg官网下载源码库并解压;下载NDK库并解压;配置解压后的FFmpeg源码库目录中的configure,修改高亮部分几个参数为以下的内容,主要目的是生成Android可使用的名称版本。so文件的格式;buildsettingsSHFLAGSsharedWl,soname,(F)LIBPREFlibLIBSUF。aFULLNAME(NAME)(BUILDSUF)LIBNAME(LIBPREF)(FULLNAME)(LIBSUF)SLIBPREFlibSLIBSUF。soSLIBNAME(SLIBPREF)(FULLNAME)(SLIBSUF)SLIBNAMEWITHVERSION(SLIBNAME)。(LIBVERSION)已修改配置SLIBNAMEWITHMAJOR(SLIBNAME)(FULLNAME)(LIBMAJOR)(SLIBSUF)LIBINSTALLEXTRACMD(RANLIB)(LIBDIR)(LIBNAME)SLIBINSTALLNAME(SLIBNAMEWITHMAJOR)SLIBINSTALLLINKS(SLIBNAME)在FFmpeg源码库目录下新建脚本文件buildandroidarmv8a。sh,在文件中配置NDK的路径,并输入下面其他的内容;清空上次的编译makeclean这里先配置你的NDK路径exportNDKUsersbytedanceLibraryAndroidsdkndk21。4。7075529TOOLCHAINNDKtoolchainsllvmprebuiltdarwinx8664functionbuildandroid{。configureprefixPREFIXdisablepostprocdisabledebugdisabledocenableFFmpegdisabledocdisablesymverdisablestaticenablesharedcrossprefixCROSSPREFIXtargetosandroidarchARCHcpuCPUccCCcxxCXXenablecrosscompilesysrootSYSROOTextracflagsOsfpicOPTIMIZECFLAGSextraldflagsADDILDFLAGSmakecleanmakej16makeinstallechobuildandroidarm64v8asuccess}arm64v8aARCHarm64CPUarmv8aAPI21CCTOOLCHAINbinaarch64linuxandroidAPIclangCXXTOOLCHAINbinaarch64linuxandroidAPIclangSYSROOTNDKtoolchainsllvmprebuiltdarwinx8664sysrootCROSSPREFIXTOOLCHAINbinaarch64linuxandroidPREFIX(pwd)androidCPUOPTIMIZECFLAGSmarchCPUechoCCbuildandroid设置NDK文件夹中所有文件的权限chmod777RNDK;终端执行脚本。buildandroidarmv8a。sh,开始编译FFmpeg。编译成功后的文件会在FFmpeg下的android目录中,会出现多个。so文件; 若要编译armv7a,只需要拷贝修改以上的脚本为以下buildandroidarmv7a。sh的内容。armv7aARCHarmCPUarmv7aAPI21CCTOOLCHAINbinarmv7alinuxandroideabiAPIclangCXXTOOLCHAINbinarmv7alinuxandroideabiAPIclangSYSROOTNDKtoolchainsllvmprebuiltdarwinx8664sysrootCROSSPREFIXTOOLCHAINbinarmlinuxandroideabiPREFIX(pwd)androidCPUOPTIMIZECFLAGSmfloatabisoftfpmfpuvfpmarmmarchCPU3。2在Android中引入FFmpeg的so库NDK环境、CMake构建工具、LLDB(CC代码调试工具);新建Cmodule,一般会生成以下几个重要的文件:CMakeLists。txt、nativelib。cpp、MainActivity;在appsrcmain目录下,新建目录,并命名jniLibs,这是AndroidStudio默认放置so动态库的目录;接着在jniLibs目录下,新建arm64v8a目录,然后将编译好的。so文件粘贴至此目录下;然后再将编译时生成的。h头文件(FFmpeg对外暴露的接口)粘贴至cpp目录下的include中。以上的。so动态库目录和。h头文件目录都会在CMakeLists。txt中显式声明和链接进来;最上层的MainActivity,在这里面加载CC代码编译的库:nativelib。nativelib在CMakeLists。txt中被添加到名为ffmpeg的library中,所以在System。loadLibrary()中输入的是ffmpeg;classMainActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle?){super。onCreate(savedInstanceState)setContentView(R。layout。activitymain)Exampleofacalltoanativemethodsampletext。textstringFromJNI()}声明一个外部引用的方法,此方法和CC层的代码是对应的。externalfunstringFromJNI():Stringcompanionobject{在init{}中加载CC编译成的library:ffmpeglibrary名称的定义和添加在CMakeLists。txt中完成init{System。loadLibrary(ffmpeg)}}}nativelib。cpp是一个C接口文件,Java层中声明的external方法在这里得到实现;includejni。hincludestringexternCJNIEXPORTjstringJNICALLJavacombytedanceexampleMainActivitystringFromJNI(JNIEnvenv,jobjectthis){std::stringhelloHellofromC;returnenvNewStringUTF(hello。cstr());}CMakeLists。txt是一个构建脚本,目的是配置可以编译出nativelib此so库的构建信息;FormoreinformationaboutusingCMakewithAndroidStudio,readthedocumentation:https:d。android。comstudioprojectsaddnativecode。htmlSetstheminimumversionofCMakerequiredtobuildthenativelibrary。cmakeminimumrequired(VERSION3。10。2)Declaresandnamestheproject。project(ffmpeg)Createsandnamesalibrary,setsitaseitherSTATICorSHARED,andprovidestherelativepathstoitssourcecode。Youcandefinemultiplelibraries,andCMakebuildsthemforyou。GradleautomaticallypackagessharedlibrarieswithyourAPK。定义so库和头文件所在目录,方便后面使用set(FFmpeglibdir{CMAKESOURCEDIR}。。jniLibs{ANDROIDABI})set(FFmpegheaddir{CMAKESOURCEDIR}FFmpeg)添加头文件目录includedirectories(FFmpeginclude)addlibrary(Setsthenameofthelibrary。ffmmpegSetsthelibraryasasharedlibrary。SHAREDProvidesarelativepathtoyoursourcefile(s)。nativelib。cpp)Searchesforaspecifiedprebuiltlibraryandstoresthepathasavariable。BecauseCMakeincludessystemlibrariesinthesearchpathbydefault,youonlyneedtospecifythenameofthepublicNDKlibraryyouwanttoadd。CMakeverifiesthatthelibraryexistsbeforecompletingitsbuild。添加FFmpeg相关的so库addlibrary(avutilSHAREDIMPORTED)settargetproperties(avutilPROPERTIESIMPORTEDLOCATION{FFmpeglibdir}libavutil。so)addlibrary(swresampleSHAREDIMPORTED)settargetproperties(swresamplePROPERTIESIMPORTEDLOCATION{FFmpeglibdir}libswresample。so)addlibrary(avcodecSHAREDIMPORTED)settargetproperties(avcodecPROPERTIESIMPORTEDLOCATION{FFmpeglibdir}libavcodec。so)findlibrary(Setsthenameofthepathvariable。loglibSpecifiesthenameoftheNDKlibrarythatyouwantCMaketolocate。log)SpecifieslibrariesCMakeshouldlinktoyourtargetlibrary。Youcanlinkmultiplelibraries,suchaslibrariesyoudefineinthisbuildscript,prebuiltthirdpartylibraries,orsystemlibraries。targetlinklibraries(Specifiesthetargetlibrary。audioffmmpeg把前面添加进来的FFmpeg。so库都链接到目标库nativelib上avutilswresampleavcodeclandroidLinksthetargetlibrarytotheloglibraryincludedintheNDK。{loglib})以上的操作就将FFmpeg引入Android项目。 C音视频学习资料免费获取方法:关注音视频开发T哥,点击链接即可免费获取2023年最新C音视频开发进阶独家免费学习大礼包!4、FFmpeg解码视频的原理和细节4。1主要流程 4。2基本原理4。2。1常用的ffmpeg接口1分配AVFormatContextavformatalloccontext();2打开文件输入流avformatopeninput(AVFormatContextps,constcharurl,constAVInputFormatfmt,AVDictionaryoptions);3提取输入文件中的数据流信息avformatfindstreaminfo(AVFormatContextic,AVDictionaryoptions);4分配编解码上下文avcodecalloccontext3(constAVCodeccodec);5基于与数据流相关的编解码参数来填充编解码器上下文avcodecparameterstocontext(AVCodecContextcodec,constAVCodecParameterspar);6查找对应已注册的编解码器avcodecfinddecoder(enumAVCodecIDid);7打开编解码器avcodecopen2(AVCodecContextavctx,constAVCodeccodec,AVDictionaryoptions);8不停地从码流中提取压缩帧数据,获取的是一帧视频的压缩数据avreadframe(AVFormatContexts,AVPacketpkt);9发送原生的压缩数据输入到解码器(compresseddata)avcodecsendpacket(AVCodecContextavctx,constAVPacketavpkt);10接收解码器输出的解码数据avcodecreceiveframe(AVCodecContextavctx,AVFrameframe);4。2。2视频解码的整体思路首先要注册libavformat并且注册所有的编解码器、复用解复用组、协议等。它是所有基于FFmpeg的应用程序中第一个被调用的函数,只有调用了该函数,才能正常使用FFmpeg的各项功能。另外,在最新版本的FFmpeg中目前已经可以不用加入这行代码;avregisterall();打开视频文件,提取文件中的数据流信息;autoavformatcontextavformatalloccontext();avformatopeninput(avformatcontext,path。cstr(),nullptr,nullptr);avformatfindstreaminfo(avformatcontext,nullptr);然后获取视频媒体流的下标,才能找到文件中的视频媒体流;intvideostreamindex1;for(inti0;iavformatcontextnbstreams;i){匹配找到视频媒体流的下标,if(avformatcontextstreams〔i〕codecparcodectypeAVMEDIATYPEVIDEO){videostreamindexi;LOGD(TAG,findvideostreamindexd,videostreamindex);break;}}获取视频媒体流、获取解码器上下文、获取解码器上下文、配置解码器上下文的参数值、打开解码器;获取视频媒体流autostreamavformatcontextstreams〔videostreamindex〕;找到已注册的解码器autocodecavcodecfinddecoder(streamcodecparcodecid);获取解码器上下文AVCodecContextcodecctxavcodecalloccontext3(codec);将视频媒体流的参数配置到解码器上下文autoretavcodecparameterstocontext(codecctx,streamcodecpar);if(ret0){打开解码器avcodecopen2(codecctx,codec,nullptr);}通过指定像素格式、图像宽、图像高来计算所需缓冲区需要的内存大小,分配设置缓冲区;并且由于是上屏绘制,因此我们需要用到ANativeWindow,使用ANativeWindowsetBuffersGeometry设置此绘制窗口的属性;videowidthcodecctxwidth;videoheightcodecctxheight;intbuffersizeavimagegetbuffersize(AVPIXFMTRGBA,videowidth,videoheight,1);输出bufferoutbuffer(uint8t)avmalloc(buffersizesizeof(uint8t));通过设置宽高来限制缓冲区中的像素数量,而非显示屏幕的尺寸。如果缓冲区与显示的屏幕尺寸不相符,则实际显示的可能会是拉伸,或者被压缩的图像intresultANativeWindowsetBuffersGeometry(nativewindow,videowidth,videoheight,WINDOWFORMATRGBA8888);分配内存空间给像素格式为RGBA的AVFrame,用于存放转换成RGBA后的帧数据;设置rgbaframe缓冲区,使其与outbuffer相关联;autorgbaframeavframealloc();avimagefillarrays(rgbaframedata,rgbaframelinesize,outbuffer,AVPIXFMTRGBA,videowidth,videoheight,1);获取SwsContext,它在调用swsscale()进行图像格式转换和图像缩放时会使用到。YUV420P转换为RGBA时可能会在调用swsscale时格式转换失败而无法返回正确的高度值,原因跟调用swsgetContext时flags有关,需要将SWSBICUBIC换成SWSFULLCHRHINTSWSACCURATERND;structSwsContextdataconvertcontextswsgetContext(videowidth,videoheight,codecctxpixfmt,videowidth,videoheight,AVPIXFMTRGBA,SWSBICUBIC,nullptr,nullptr,nullptr);分配内存空间给用于存储原始数据的AVFrame,指向原始帧数据;并且分配内存空间给用于存放视频解码前数据的AVPacket;autoframeavframealloc();autopacketavpacketalloc();从视频码流中循环读取压缩帧数据,然后开始解码;retavreadframe(avformatcontext,packet);if(packetsize){Decode(codecctx,packet,frame,stream,lock,dataconvertcontext,rgbaframe);}在Decode()函数中将装有原生压缩数据的packet作为输入发送给解码器;sendthepacketwiththecompresseddatatothedecoderretavcodecsendpacket(codecctx,pkt);解码器返回解码后的帧数据到指定的frame上,后续可对已解码frame的pts换算为时间戳,按时间轴的显示顺序逐帧绘制到播放的画面上;while(ret0!isstop){返回解码后的数据到frameretavcodecreceiveframe(codecctx,frame);if(retAVERROR(EAGAIN)retAVERROREOF){return;}elseif(ret0){return;}拿到当前解码后的frame,对其pts换算成时间戳,以便于跟传入的指定时间戳进行比autodecodetimemsframepts1000streamtimebase。den;if(decodetimemstimems){lastdecodetimemsdecodetimems;isseekingfalse;图片数据格式转换把转换后的数据绘制到屏幕上}avpacketunref(pkt);}绘制画面之前,要进行图片数据格式的转换,这里就要用到前面获取到的SwsContext;图片数据格式转换intresultswsscale(swscontext,(constuint8tconst)framedata,framelinesize,0,videoheight,rgbaframedata,rgbaframelinesize);if(result0){LOGE(TAG,PlayerError:dataconvertfail);return;}因为是上屏绘制,所以用到了ANativeWindow和ANativeWindowBuffer。在绘制画面之前,需要使用锁定窗口的下一个绘图surface以进行绘制,然后将要显示的帧数据写入到缓冲区中,最后解锁窗口的绘图surface,将缓冲区的数据发布到屏幕显示上;播放resultANativeWindowlock(nativewindow,windowbuffer,nullptr);if(result0){LOGE(TAG,PlayerError:Cannotlocknativewindow);}else{将图像绘制到界面上注意:这里rgbaframe一行的像素和windowbuffer一行的像素长度可能不一致需要转换好否则可能花屏autobits(uint8t)windowbuffer。bits;for(inth0;hvideoheight;h){memcpy(bitshwindowbuffer。stride4,outbufferhrgbaframelinesize〔0〕,rgbaframelinesize〔0〕);}ANativeWindowunlockAndPost(nativewindow);}以上就是主要的解码过程。除此之外,因为C使用资源和内存空间时需要自行释放,所以解码结束后还需要调用释放的接口释放资源,以免造成内存泄漏。swsfreeContext(dataconvertcontext);avfree(outbuffer);avframefree(rgbaframe);avframefree(frame);avpacketfree(packet);avcodecclose(codecctx);avcodecfreecontext(codecctx);avformatcloseinput(avformatcontext);avformatfreecontext(avformatcontext);ANativeWindowrelease(nativewindow);4。3简单应用 为了更好地理解视频解码的过程,这里封装一个视频解码器VideoDecoder,解码器初步会有以下几个函数:VideoDecoder(constcharpath,std::functionvoid(longtimestamp)ondecodeframe);voidPrepare(ANativeWindowwindow);boolDecodeFrame(longtimems);voidRelease(); 在这个视频解码器中,输入指定时间戳后会返回解码的这一帧数据。其中较为重要的是DecodeFrame(longtimems)函数,它可以由使用者自行调用,传入指定帧的时间戳,进而解码对应的帧数据。此外,可以增加同步锁以实现解码线程和使用线程分离。4。3。1加入同步锁实现视频播放 若只要对视频进行解码,是不需要使用同步等待的; 但若是要实现视频的播放,那么每解码绘制完一帧就需使用锁进行同步等待,这是因为播放视频时需要让解码和绘制分离、且按照一定的时间轴顺序和速度进行解码和绘制。condition。wait(lock); 在上层调用DecodeFrame函数传入解码的时间戳时唤醒同步锁,让解码绘制的循环继续执行。boolVideoDecoder::DecodeFrame(longtimems){timemstimems;condition。notifyall();returntrue;}4。3。2播放时加入seekframe 在正常播放情况下,视频是一帧一帧逐帧解码播放;但在拖动进度条到达指定的seek点的情况下,如果还是从头到尾逐帧解码到seek点的话,效率可能不太高。这时候就需要在一定规则内对seek点的时间戳做检查,符合条件的直接seek到指定的时间戳。 FFmpeg中的avseekframeavseekframe可以定位到关键帧和非关键帧,这取决于选择的flag值。因为视频的解码需要依赖关键帧,所以一般我们需要定位到关键帧;intavseekframe(AVFormatContexts,intstreamindex,int64ttimestamp,intflags);avseekframe中的flag是用来指定寻找的I帧和传入的时间戳之间的位置关系。当要seek已过去的时间戳时,时间戳不一定会刚好处在I帧的位置,但因为解码需要依赖I帧,所以需要先找到此时间戳附近一个的I帧,此时flag就表明要seek到当前时间戳的前一个I帧还是后一个I帧;flag有四个选项: flag选项 描述 AVSEEKFLAGBACKWARD 第一个Flag是seek到请求的时间戳之前最近的关键帧。通常情况下,seek以ms为单位,若指定的ms时间戳刚好不是关键帧(大几率),会自动往回seek到最近的关键帧。虽然这种flag定位并不是非常精确,但能够较好地处理掉马赛克的问题,因为BACKWARD的方式会向回查找关键帧处,定位到关键帧处。 AVSEEKFLAGBYTE 第二个Flag是seek到文件中对应的位置(字节表示),和AVSEEKFLAGFRAME完全一致,但查找算法不同。 AVSEEKFLAGANY 第三个Flag是可以seek到任意帧,不一定是关键帧,因此使用时可能出现花屏(马赛克),但进度和手滑完全一致。 AVSEEKFLAGFRAME 第四个Flag是seek的时间戳对应frame序号,可以理解为向后找到最近的关键帧,与BACKWARD的方向是相反的。flag可能同时包含以上的多个值。比如AVSEEKFLAGBACKWARDAVSEEKFLAGBYTE;FRAME和BACKWARD是按帧之间的间隔推算出seek的目标位置,适合快进快退;BYTE则适合大幅度滑动。 seek的场景解码时传入的时间戳若是往前进的方向,并且超过上一帧时间戳有一定距离就需要seek,这里的一定距离是通过多次实验估算所得,并非都是以下代码中使用的1000ms;如果是往后退的方向且小于上一次解码时间戳,但与上一次解码时间戳的距离比较大(比如已超过50ms),就要seek到上一个关键帧;使用bool变量isseeking是为了防止其他干扰当前seeking的操作,目的是控制当前只有一个seek操作在进行。if(!isseeking(timemslastdecodetimems1000timemslastdecodetimems50)){isseekingtrue;seek时传入的是指定帧带有timebase的时间戳,因此要用timesms进行推算LOGD(TAG,seekframetimemsld,lastdecodetimemsld,timems,lastdecodetimems);avseekframe(avformatcontext,videostreamindex,timemsstreamtimebase。den1000,AVSEEKFLAGBACKWARD);} 插入seek的逻辑 因为在解码前要检查是否seek,所以要在avreadframe函数(返回视频媒体流下一帧)之前插入seek的逻辑,符合seek条件时使用avseekframe到达指定I帧,接着avreadframe后再继续解码到目的时间戳的位置。是否进行seek的逻辑写在这接下来是读取视频流的下一帧intretavreadframe(avformatcontext,packet);4。4解码过程中的细节4。4。1DecodeFrame时seek的条件 使用avseekframe函数时需要指定正确的flag,并且还要约定进行seek操作时的条件,否则视频可能会出现花屏(马赛克)。if(!isseeking(timemslastdecodetimems1000timemslastdecodetimems50)){isseekingtrue;avseekframe(,,,AVSEEKFLAGBACKWARD);}4。4。2减少解码的次数 在视频解码时,在有些条件下是可以不用对传入时间戳的帧数据进行解码的。比如:当前解码时间戳若是前进方向并且与上一次的解码时间戳相同或者与当前正在解码的时间戳相同,则不需要进行解码;当前解码时间戳若不大于上一次的解码时间戳并且与上一次的解码时间戳之间的距离相差较小(比如未超过50ms),则不需要进行解码。boolVideoDecoder::DecodeFrame(longtimems){LOGD(TAG,DecodeFrametimemsld,timems);if(lastdecodetimemstimemstimemstimems){LOGD(TAG,DecodeFramelastdecodetimemstimems);returnfalse;}if(timemslastdecodetimemstimems50lastdecodetimems){returnfalse;}timemstimems;condition。notifyall();returntrue;} 有了以上这些条件的约束后,会减少一些不必要的解码操作。4。4。3使用AVFrame的ptsAVPacket存储解码前的数据(编码数据:H264AAC等),保存的是解封装之后、解码前的数据,仍然是压缩数据;AVFrame存储解码后的数据(像素数据:YUVRGBPCM等);AVPacket的pts和AVFrame的pts意义存在差异。前者表示这个解压包何时显示,后者表示帧数据何时显示;AVPacket的ptsPresentationtimestampinAVStreamtimebaseunits;thetimeatwhichthedecompressedpacketwillbepresentedtotheuser。CanbeAVNOPTSVALUEifitisnotstoredinthefile。ptsMUSTbelargerorequaltodtsaspresentationcannothappenbeforedecompression,unlessonewantstoviewhexdumps。Someformatsmisusethetermsdtsandptsctstomeansomethingdifferent。SuchtimestampsmustbeconvertedtotrueptsdtsbeforetheyarestoredinAVPacket。int64tpts;AVFrame的ptsPresentationtimestampintimebaseunits(timewhenframeshouldbeshowntouser)。int64tpts;是否将当前解码的帧数据绘制到画面上,取决于传入到解码时间戳与当前解码器返回的已解码帧的时间戳的比较结果。这里不可使用AVPacket的pts,它很可能不是一个递增的时间戳;需要进行画面绘制的前提是:当传入指定的解码时间戳不大于当前已解码frame的pts换算后的时间戳时进行画面绘制。autodecodetimemsframepts1000streamtimebase。den;LOGD(TAG,decodetimemsld,decodetimems);if(decodetimemstimems){lastdecodetimemsdecodetimems;isseekingfalse;画面绘制}4。4。4解码最后一帧时视频已经没有数据 使用avreadframe(avformatcontext,packet)返回视频媒体流下一帧到AVPacket中。如果函数返回的int值是0则是Success,如果小于0则是Error或者EOF。 因此如果在播放视频时返回的是小于0的值,调用avcodecflushbuffers函数重置解码器的状态,flush缓冲区中的内容,然后再seek到当前传入的时间戳处,完成解码后的回调,再让同步锁进行等待。读取码流中的音频若干帧或者视频一帧,这里是读取视频一帧(完整的一帧),获取的是一帧视频的压缩数据,接下来才能对其进行解码retavreadframe(avformatcontext,packet);if(ret0){avcodecflushbuffers(codecctx);avseekframe(avformatcontext,videostreamindex,timemsstreamtimebase。den1000,AVSEEKFLAGBACKWARD);LOGD(TAG,ret0,condition。wait(lock));防止解最后一帧时视频已经没有数据ondecodeframe(lastdecodetimems);condition。wait(lock);}4。5上层封装解码器VideoDecoder 如果要在上层封装一个VideoDecoder,只需要将C层VideoDecoder的接口暴露在nativelib。cpp中,然后上层通过JNI的方式调用C的接口。 比如上层要传入指定的解码时间戳进行解码时,写一个deocodeFrame方法,然后把时间戳传到C层的nativeDecodeFrame进行解码,而nativeDecodeFrame这个方法的实现就写在nativelib。cpp中。FFmpegVideoDecoder。ktclassFFmpegVideoDecoder(path:String,valonDecodeFrame:(timestamp:Long,texture:SurfaceTexture,needRender:Boolean)Unit){抽第timeMs帧,根据sync是否同步等待fundecodeFrame(timeMS:Long,sync:Booleanfalse){若当前不需要抽帧时不进行等待if(nativeDecodeFrame(decoderPtr,timeMS)sync){}else{}}privateexternalfunnativeDecodeFrame(decoder:Long,timeMS:Long):Booleancompanionobject{constvalTAGFFmpegVideoDecoderinit{System。loadLibrary(ffmmpeg)}}} 然后在nativelib。cpp中调用C层VideoDecoder的接口DecodeFrame,这样就通过JNI的方式建立起了上层和C底层之间的联系nativelib。cppexternCJNIEXPORTjbooleanJNICALLJavacomexampledecodervideoFFmpegVideoDecodernativeDecodeFrame(JNIEnvenv,jobjectthiz,jlongdecoder,jlongtimems){autovideoDecoder(codec::VideoDecoder)decoder;returnvideoDecoderDecodeFrame(timems);}5、心得 技术经验FFmpeg编译后与Android结合起来实现视频的解码播放,便捷性很高。由于是用C层实现具体的解码流程,会有学习难度,最好有一定的C基础。6、附录 C封装的VideoDecoderVideoDecoder。hincludejni。hincludemutexincludeincludeincludetime。hexternC{includelibavformatavformat。hincludelibavcodecavcodec。hincludelibswresampleswresample。hincludelibswscaleswscale。h}includestringVideoDecoder可用于解码某个音视频文件(比如。mp4)中视频媒体流的数据。Java层传入指定文件的路径后,可以按一定fps循环传入指定的时间戳进行解码(抽帧),这一实现由C提供的DecodeFrame来完成。在每次解码结束时,将解码某一帧的时间戳回调给上层的解码器,以供其他操作使用。namespacecodec{classVideoDecoder{private:std::stringpath;longtimems1;longlastdecodetimems1;boolisseekingfalse;ANativeWindownativewindownullptr;ANativeWindowBufferwindowbuffer{};、视频宽高属性intvideowidth0;intvideoheight0;uint8toutbuffernullptr;ondecodeframe用于将抽取指定帧的时间戳回调给上层解码器,以供上层解码器进行其他操作。std::functionvoid(longtimestamp)ondecodeframenullptr;boolisstopfalse;会与在循环同步时用的锁std::uniquelockstd::mutex配合使用std::mutexworkqueuemtx;真正在进行同步等待和唤醒的属性std::conditionvariablecondition;解码器真正进行解码的函数voidDecode(AVCodecContextcodecctx,AVPacketpkt,AVFrameframe,AVStreamstream,std::uniquelockstd::mutexlock,SwsContextswscontext,AVFramepFrame);public:新建解码器时要传入媒体文件路径和一个解码后的回调ondecodeframe。VideoDecoder(constcharpath,std::functionvoid(longtimestamp)ondecodeframe);在JNI层将上层传入的Surface包装后新建一个ANativeWindow传入,在后面解码后绘制帧数据时需要用到voidPrepare(ANativeWindowwindow);抽取指定时间戳的视频帧,可由上层调用boolDecodeFrame(longtimems);释放解码器资源voidRelease();获取当前系统毫秒时间staticint64tGetCurrentMilliTime(void);};}VideoDecoder。cppincludeVideoDecoder。hinclude。。logLogger。hincludethreadincludeutilityexternC{includelibavutilimgutils。h}defineTAGVideoDecodernamespacecodec{VideoDecoder::VideoDecoder(constcharpath,std::functionvoid(longtimestamp)ondecodeframe):ondecodeframe(std::move(ondecodeframe)){pathstd::string(path);}voidVideoDecoder::Decode(AVCodecContextcodecctx,AVPacketpkt,AVFrameframe,AVStreamstream,std::uniquelockstd::mutexlock,SwsContextswscontext,AVFramergbaframe){intret;sendthepacketwiththecompresseddatatothedecoderretavcodecsendpacket(codecctx,pkt);if(retAVERROR(EAGAIN)){LOGE(TAG,Decode:ReceiveframeandsendpacketbothreturnedEAGAIN,whichisanAPIviolation。);}elseif(ret0){return;}readalltheoutputframes(infilegeneraltheremaybeanynumberofthemwhile(ret0!isstop){对于frame,avcodecreceiveframe内部每次都先调用retavcodecreceiveframe(codecctx,frame);if(retAVERROR(EAGAIN)retAVERROREOF){return;}elseif(ret0){return;}int64tstartTimeGetCurrentMilliTime();LOGD(TAG,decodeStartTime:ld,startTime);换算当前解码的frame时间戳autodecodetimemsframepts1000streamtimebase。den;LOGD(TAG,decodetimemsld,decodetimems);if(decodetimemstimems){LOGD(TAG,decodedecodetimemsld,timemsld,decodetimems,timems);lastdecodetimemsdecodetimems;isseekingfalse;数据格式转换intresultswsscale(swscontext,(constuint8tconst)framedata,framelinesize,0,videoheight,rgbaframedata,rgbaframelinesize);if(result0){LOGE(TAG,PlayerError:dataconvertfail);return;}播放resultANativeWindowlock(nativewindow,windowbuffer,nullptr);if(result0){LOGE(TAG,PlayerError:Cannotlocknativewindow);}else{将图像绘制到界面上autobits(uint8t)windowbuffer。bits;for(inth0;hvideoheight;h){memcpy(bitshwindowbuffer。stride4,outbufferhrgbaframelinesize〔0〕,rgbaframelinesize〔0〕);}ANativeWindowunlockAndPost(nativewindow);}ondecodeframe(decodetimems);int64tendTimeGetCurrentMilliTime();LOGD(TAG,decodeEndTimedecodeStartTime:ld,endTimestartTime);LOGD(TAG,finishdecodeframe);condition。wait(lock);}主要作用是清理AVPacket中的所有空间数据,清理完毕后进行初始化操作,并且将data与size置为0,方便下次调用。释放packet引用avpacketunref(pkt);}}voidVideoDecoder::Prepare(ANativeWindowwindow){nativewindowwindow;avregisterall();autoavformatcontextavformatalloccontext();avformatopeninput(avformatcontext,path。cstr(),nullptr,nullptr);avformatfindstreaminfo(avformatcontext,nullptr);intvideostreamindex1;for(inti0;iavformatcontextnbstreams;i){找到视频媒体流的下标if(avformatcontextstreams〔i〕codecparcodectypeAVMEDIATYPEVIDEO){videostreamindexi;LOGD(TAG,findvideostreamindexd,videostreamindex);break;}}runoncedo{if(videostreamindex1){codec::LOGE(TAG,PlayerError:Cannotfindvideostream);break;}std::uniquelockstd::mutexlock(workqueuemtx);获取视频媒体流autostreamavformatcontextstreams〔videostreamindex〕;找到已注册的解码器autocodecavcodecfinddecoder(streamcodecparcodecid);获取解码器上下文AVCodecContextcodecctxavcodecalloccontext3(codec);autoretavcodecparameterstocontext(codecctx,streamcodecpar);if(ret0){打开avcodecopen2(codecctx,codec,nullptr);解码器打开后才有宽高的值videowidthcodecctxwidth;videoheightcodecctxheight;AVFramergbaframeavframealloc();intbuffersizeavimagegetbuffersize(AVPIXFMTRGBA,videowidth,videoheight,1);分配内存空间给输出bufferoutbuffer(uint8t)avmalloc(buffersizesizeof(uint8t));avimagefillarrays(rgbaframedata,rgbaframelinesize,outbuffer,AVPIXFMTRGBA,videowidth,videoheight,1);通过设置宽高限制缓冲区中的像素数量,而非屏幕的物理显示尺寸。如果缓冲区与物理屏幕的显示尺寸不相符,则实际显示可能会是拉伸,或者被压缩的图像intresultANativeWindowsetBuffersGeometry(nativewindow,videowidth,videoheight,WINDOWFORMATRGBA8888);if(result0){LOGE(TAG,PlayerError:Cannotsetnativewindowbuffer);avcodecclose(codecctx);avcodecfreecontext(codecctx);avfree(outbuffer);break;}autoframeavframealloc();autopacketavpacketalloc();structSwsContextdataconvertcontextswsgetContext(videowidth,videoheight,codecctxpixfmt,videowidth,videoheight,AVPIXFMTRGBA,SWSBICUBIC,nullptr,nullptr,nullptr);while(!isstop){LOGD(TAG,frontseektimemsld,lastdecodetimemsld,timems,lastdecodetimems);if(!isseeking(timemslastdecodetimems1000timemslastdecodetimems50)){isseekingtrue;LOGD(TAG,seekframetimemsld,lastdecodetimemsld,timems,lastdecodetimems);传进去的是指定帧带有timebase的时间戳,所以是要将原来的timesms按照上面获取时的计算方式反推算出时间戳avseekframe(avformatcontext,videostreamindex,timemsstreamtimebase。den1000,AVSEEKFLAGBACKWARD);}读取视频一帧(完整的一帧),获取的是一帧视频的压缩数据,接下来才能对其进行解码retavreadframe(avformatcontext,packet);if(ret0){avcodecflushbuffers(codecctx);avseekframe(avformatcontext,videostreamindex,timemsstreamtimebase。den1000,AVSEEKFLAGBACKWARD);LOGD(TAG,ret0,condition。wait(lock));防止解码最后一帧时视频已经没有数据ondecodeframe(lastdecodetimems);condition。wait(lock);}if(packetsize){Decode(codecctx,packet,frame,stream,lock,dataconvertcontext,rgbaframe);}}释放资源swsfreeContext(dataconvertcontext);avfree(outbuffer);avframefree(rgbaframe);avframefree(frame);avpacketfree(packet);}avcodecclose(codecctx);avcodecfreecontext(codecctx);}while(false);avformatcloseinput(avformatcontext);avformatfreecontext(avformatcontext);ANativeWindowrelease(nativewindow);deletethis;}boolVideoDecoder::DecodeFrame(longtimems){LOGD(TAG,DecodeFrametimemsld,timems);if(lastdecodetimemstimemstimemstimems){LOGD(TAG,DecodeFramelastdecodetimemstimems);returnfalse;}if(lastdecodetimemstimemslastdecodetimemstimems50){returnfalse;}timemstimems;condition。notifyall();returntrue;}voidVideoDecoder::Release(){isstoptrue;condition。notifyall();}获取当前的毫秒级时间int64tVideoDecoder::GetCurrentMilliTime(void){structtimevaltv{};gettimeofday(tv,nullptr);returntv。tvsec1000。0tv。tvusec1000。0;}} 原文链接:一文读懂AndroidFFmpeg视频解码过程与实战分析