找回密码
 会员注册
查看: 21|回复: 0

一文读懂AndroidFFmpeg视频解码过程与实战分析_UTF_8

[复制链接]

3

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2024-9-30 10:57:04 | 显示全部楼层 |阅读模式
动手点关注干货不迷路概述本文首先以 FFmpeg 视频解码为主题,主要介绍了 FFmpeg 进行解码视频时的主要流程、基本原理;其次,文章还讲述了与 FFmpeg 视频解码有关的简单应用,包括如何在原有的 FFmpeg 视频解码的基础上按照一定时间轴顺序播放视频、如何在播放视频时加入 seek 的逻辑;除此之外,文章重点介绍了解码视频时可能容易遗漏的细节,最后是简单地阐述了下如何封装一个具有基本的视频解码功能的 VideoDecoder。前言FFmpegFFmpeg 是一套可以用来录制、转换数字音频、视频,并能将其转化为流的开源计算机程序,它可生成用于处理和操作多媒体数据的库,其中包含了先进的音视频解码库 libavcodec 和音视频格式转换库 libavformat。FFmpeg 六大常用功能模块libavformat:多媒体文件或协议的封装和解封装库,如 mp4、flv 等文件封装格式,rtmp、rtsp 等网络协议封装格式;libavcodec:音视频解码核心库;libavfilter:音视频、字幕滤镜库;libswscale:图像格式转换库;libswresample:音频重采样库;libavutil:工具库视频解码基础入门解复用(Demux):解复用也可叫解封装。这里有一个概念叫封装格式,封装格式指的是音视频的组合格式,常见的有 mp4、flv、mkv 等。通俗来讲,封装是将音频流、视频流、字幕流以及其他附件按一定规则组合成一个封装的产物。而解封装起着与封装相反的作用,将一个流媒体文件拆解成音频数据和视频数据等。此时拆分后数据是经过压缩编码的,常见的视频压缩数据格式有 h264。解码(Decode):简单来说,就是对压缩的编码数据解压成原始的视频像素数据,常用的原始视频像素数据格式有 yuv。色彩空间转换(Color Space Convert):通常对于图像显示器来说,它是通过 RGB 模型来显示图像的,但在传输图像数据时使用 YUV 模型可以节省带宽。因此在显示图像时就需要将 yuv 像素格式的数据转换成 rgb 的像素格式后再进行渲染。渲染(Render):将前面已经解码和进行色彩空间转换的每一个视频帧的数据发送给显卡以绘制在屏幕画面上。一、 引入 FFmpeg 前的准备工作1.1 FFmpeg so 库编译在 FFmpeg 官网下载源码库并解压;下载 NDK 库并解压;配置解压后的 FFmpeg 源码库目录中的 configure,修改高亮部分几个参数为以下的内容,主要目的是生成 Android 可使用的 名称-版本.so 文件的格式;#······#buildsettingsSHFLAGS='-shared-Wl,-soname,$(@F)'LIBPREF="lib"LIBSUF=".a"FULLNAME='$(NAME)$(BUILDSUF)'LIBNAME='$(LIBPREF)$(FULLNAME)$(LIBSUF)'SLIBPREF="lib"SLIBSUF=".so"SLIBNAME='$(SLIBPREF)$(FULLNAME)$(SLIBSUF)'SLIBNAME_WITH_VERSION='$(SLIBNAME).$(LIBVERSION)'#已修改配置SLIBNAME_WITH_MAJOR='$(SLIBNAME)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'LIB_INSTALL_EXTRA_CMD='$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'SLIB_INSTALL_LINKS='$(SLIBNAME)'#······在 FFmpeg 源码库目录下新建脚本文件 build_android_arm_v8a.sh,在文件中配置 NDK 的路径,并输入下面其他的内容;#清空上次的编译makeclean#这里先配置你的NDK路径exportNDK=/Users/bytedance/Library/Android/sdk/ndk/21.4.7075529TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64functionbuild_android{./configure\--prefix=$PREFIX\--disable-postproc\--disable-debug\--disable-doc\--enable-FFmpeg\--disable-doc\--disable-symver\--disable-static\--enable-shared\--cross-prefix=$CROSS_PREFIX\--target-os=android\--arch=$ARCH\--cpu=$CPU\--cc=$CC\--cxx=$CXX\--enable-cross-compile\--sysroot=$SYSROOT\--extra-cflags="-Os-fpic$OPTIMIZE_CFLAGS"\--extra-ldflags="$ADDI_LDFLAGS"makecleanmake-j16makeinstallecho"============================buildandroidarm64-v8asuccess=========================="}#arm64-v8aARCH=arm64CPU=armv8-aAPI=21CC=$TOOLCHAIN/bin/aarch64-linux-android$API-clangCXX=$TOOLCHAIN/bin/aarch64-linux-android$API-clang++SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysrootCROSS_PREFIX=$TOOLCHAIN/bin/aarch64-linux-android-PREFIX=$(pwd)/android/$CPUOPTIMIZE_CFLAGS="-march=$CPU"echo$CCbuild_android设置 NDK 文件夹中所有文件的权限 chmod 777 -R NDK;终端执行脚本 ./build_android_arm_v8a.sh,开始编译 FFmpeg。编译成功后的文件会在 FFmpeg 下的 android 目录中,会出现多个 .so 文件;若要编译 arm-v7a,只需要拷贝修改以上的脚本为以下 build_android_arm_v7a.sh 的内容。#armv7-aARCH=armCPU=armv7-aAPI=21CC=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clangCXX=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang++SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysrootCROSS_PREFIX=$TOOLCHAIN/bin/arm-linux-androideabi-PREFIX=$(pwd)/android/$CPUOPTIMIZE_CFLAGS="-mfloat-abi=softfp-mfpu=vfp-marm-march=$CPU"1.2 在 Android 中引入 FFmpeg 的 so 库NDK 环境、CMake 构建工具、LLDB(C/C++ 代码调试工具);新建 C++ module,一般会生成以下几个重要的文件:CMakeLists.txt、native-lib.cpp、MainActivity;在 app/src/main/ 目录下,新建目录,并命名 jniLibs,这是 Android Studio 默认放置 so 动态库的目录;接着在 jniLibs 目录下,新建 arm64-v8a 目录,然后将编译好的 .so 文件粘贴至此目录下;然后再将编译时生成的 .h 头文件(FFmpeg 对外暴露的接口)粘贴至 cpp 目录下的 include 中。以上的 .so 动态库目录和 .h 头文件目录都会在 CMakeLists.txt 中显式声明和链接进来;最上层的 MainActivity,在这里面加载 C/C++ 代码编译的库:native-lib。native-lib 在 CMakeLists.txt 中被添加到名为 "ffmpeg" 的 library 中,所以在 System.loadLibrary()中输入的是 "ffmpeg";classMainActivity:AppCompatActivity(){overridefunonCreate(savedInstanceState:Bundle){super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)//Exampleofacalltoanativemethodsample_text.text=stringFromJNI()}//声明一个外部引用的方法,此方法和 C/C++层的代码是对应的。externalfunstringFromJNI():Stringcompanionobject{//在 init{}中加载 C/C++编译成的 library:ffmpeg//library名称的定义和添加在CMakeLists.txt中完成init{System.loadLibrary("ffmpeg")}}}native-lib.cpp 是一个 C++ 接口文件,Java 层中声明的 external 方法在这里得到实现;#include#includeextern"C"JNIEXPORTjstringJNICALLJava_com_bytedance_example_MainActivity_stringFromJNI(JNIEnv*env,jobject/*this*/){std::stringhello="HellofromC++";returnenv->NewStringUTF(hello.c_str());}CMakeLists.txt 是一个构建脚本,目的是配置可以编译出 native-lib 此 so 库的构建信息;#FormoreinformationaboutusingCMakewithAndroidStudio,readthe#documentation:https://d.android.com/studio/projects/add-native-code.html#SetstheminimumversionofCMakerequiredtobuildthenativelibrary.cmake_minimum_required(VERSION3.10.2)#Declaresandnamestheproject.project("ffmpeg")#Createsandnamesalibrary,setsitaseitherSTATIC#orSHARED,andprovidestherelativepathstoitssourcecode.#Youcandefinemultiplelibraries,andCMakebuildsthemforyou.#GradleautomaticallypackagessharedlibrarieswithyourAPK.#定义so库和头文件所在目录,方便后面使用set(FFmpeg_lib_dir${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})set(FFmpeg_head_dir${CMAKE_SOURCE_DIR}/FFmpeg)#添加头文件目录include_directories(FFmpeg/include)add_library(#Setsthenameofthelibrary.ffmmpeg#Setsthelibraryasasharedlibrary.SHARED#Providesarelativepathtoyoursourcefile(s).native-lib.cpp)#Searchesforaspecifiedprebuiltlibraryandstoresthepathasa#variable.BecauseCMakeincludessystemlibrariesinthesearchpathby#default,youonlyneedtospecifythenameofthepublicNDKlibrary#youwanttoadd.CMakeverifiesthatthelibraryexistsbefore#completingitsbuild.#添加FFmpeg相关的so库add_library(avutilSHAREDIMPORTED)set_target_properties(avutilPROPERTIESIMPORTED_LOCATION${FFmpeg_lib_dir}/libavutil.so)add_library(swresampleSHAREDIMPORTED)set_target_properties(swresamplePROPERTIESIMPORTED_LOCATION${FFmpeg_lib_dir}/libswresample.so)add_library(avcodecSHAREDIMPORTED)set_target_properties(avcodecPROPERTIESIMPORTED_LOCATION${FFmpeg_lib_dir}/libavcodec.so)find_library(#Setsthenameofthepathvariable.log-lib#SpecifiesthenameoftheNDKlibrarythat#youwantCMaketolocate.log)#SpecifieslibrariesCMakeshouldlinktoyourtargetlibrary.You#canlinkmultiplelibraries,suchaslibrariesyoudefineinthis#buildscript,prebuiltthird-partylibraries,orsystemlibraries.target_link_libraries(#Specifiesthetargetlibrary.audioffmmpeg#把前面添加进来的FFmpeg.so库都链接到目标库native-lib上avutilswresampleavcodec-landroid#Linksthetargetlibrarytotheloglibrary#includedintheNDK.${log-lib})以上的操作就将 FFmpeg 引入 Android 项目。二、FFmpeg 解码视频的原理和细节2.1 主要流程2.2 基本原理2.2.1 常用的 ffmpeg 接口//1分配AVFormatContextavformat_alloc_context();//2打开文件输入流avformat_open_input(AVFormatContext**ps,constchar*url,constAVInputFormat*fmt,AVDictionary**options);//3提取输入文件中的数据流信息avformat_find_stream_info(AVFormatContext*ic,AVDictionary**options);//4分配编解码上下文avcodec_alloc_context3(constAVCodec*codec);//5基于与数据流相关的编解码参数来填充编解码器上下文avcodec_parameters_to_context(AVCodecContext*codec,constAVCodecParameters*par);//6查找对应已注册的编解码器avcodec_find_decoder(enumAVCodecIDid);//7打开编解码器avcodec_open2(AVCodecContext*avctx,constAVCodec*codec,AVDictionary**options);//8不停地从码流中提取压缩帧数据,获取的是一帧视频的压缩数据av_read_frame(AVFormatContext*s,AVPacket*pkt);//9发送原生的压缩数据输入到解码器(compresseddata)avcodec_send_packet(AVCodecContext*avctx,constAVPacket*avpkt);//10接收解码器输出的解码数据avcodec_receive_frame(AVCodecContext*avctx,AVFrame*frame);2.2.2 视频解码的整体思路首先要注册 libavformat 并且注册所有的编解码器、复用/解复用组、协议等。它是所有基于 FFmpeg 的应用程序中第一个被调用的函数, 只有调用了该函数,才能正常使用 FFmpeg 的各项功能。另外,在最新版本的 FFmpeg 中目前已经可以不用加入这行代码;av_register_all();打开视频文件,提取文件中的数据流信息;autoav_format_context=avformat_alloc_context();avformat_open_input(&av_format_context,path_.c_str(),nullptr,nullptr);avformat_find_stream_info(av_format_context,nullptr);然后获取视频媒体流的下标,才能找到文件中的视频媒体流;intvideo_stream_index=-1;for(inti=0;inb_streams;i++){//匹配找到视频媒体流的下标,if(av_format_context->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO){video_stream_index=i;LOGD(TAG,"findvideostreamindex=%d",video_stream_index);break;}}获取视频媒体流、获取解码器上下文、获取解码器上下文、配置解码器上下文的参数值、打开解码器;//获取视频媒体流autostream=av_format_context->streams[video_stream_index];//找到已注册的解码器autocodec=avcodec_find_decoder(stream->codecpar->codec_id);//获取解码器上下文AVCodecContext*codec_ctx=avcodec_alloc_context3(codec);//将视频媒体流的参数配置到解码器上下文autoret=avcodec_parameters_to_context(codec_ctx,stream->codecpar);if(ret>=0){//打开解码器avcodec_open2(codec_ctx,codec,nullptr);//······}通过指定像素格式、图像宽、图像高来计算所需缓冲区需要的内存大小,分配设置缓冲区;并且由于是上屏绘制,因此我们需要用到 ANativeWindow,使用 ANativeWindow_setBuffersGeometry 设置此绘制窗口的属性;video_width_=codec_ctx->width;video_height_=codec_ctx->height;intbuffer_size=av_image_get_buffer_size(AV_PIX_FMT_RGBA,video_width_,video_height_,1);//输出bufferout_buffer_=(uint8_t*)av_malloc(buffer_size*sizeof(uint8_t));//通过设置宽高来限制缓冲区中的像素数量,而非显示屏幕的尺寸。//如果缓冲区与显示的屏幕尺寸不相符,则实际显示的可能会是拉伸,或者被压缩的图像intresult=ANativeWindow_setBuffersGeometry(native_window_,video_width_,video_height_,WINDOW_FORMAT_RGBA_8888);分配内存空间给像素格式为 RGBA 的 AVFrame,用于存放转换成 RGBA 后的帧数据;设置 rgba_frame 缓冲区,使其与 out_buffer_ 相关联;autorgba_frame=av_frame_alloc();av_image_fill_arrays(rgba_frame->data,rgba_frame->linesize,out_buffer_,AV_PIX_FMT_RGBA,video_width_,video_height_,1);获取 SwsContext,它在调用 sws_scale() 进行图像格式转换和图像缩放时会使用到。YUV420P 转换为 RGBA 时可能会在调用 sws_scale 时格式转换失败而无法返回正确的高度值,原因跟调用 sws_getContext 时 flags 有关,需要将 SWS_BICUBIC 换成 SWS_FULL_CHR_H_INT | SWS_ACCURATE_RND;structSwsContext*data_convert_context=sws_getContext(video_width_,video_height_,codec_ctx->pix_fmt,video_width_,video_height_,AV_PIX_FMT_RGBA,SWS_BICUBIC,nullptr,nullptr,nullptr);分配内存空间给用于存储原始数据的 AVFrame,指向原始帧数据;并且分配内存空间给用于存放视频解码前数据的 AVPacket;autoframe=av_frame_alloc();autopacket=av_packet_alloc();从视频码流中循环读取压缩帧数据,然后开始解码;ret=av_read_frame(av_format_context,packet);if(packet->size){Decode(codec_ctx,packet,frame,stream,lock,data_convert_context,rgba_frame);}在 Decode() 函数中将装有原生压缩数据的 packet 作为输入发送给解码器;/*sendthepacketwiththecompresseddatatothedecoder*/ret=avcodec_send_packet(codec_ctx,pkt);解码器返回解码后的帧数据到指定的 frame 上,后续可对已解码 frame 的 pts 换算为时间戳,按时间轴的显示顺序逐帧绘制到播放的画面上;while(ret>=0&!is_stop_){//返回解码后的数据到frameret=avcodec_receive_frame(codec_ctx,frame);if(ret==AVERROR(EAGAIN)||ret==AVERROR_EOF){return;}elseif(retpts*1000/stream->time_base.den;if(decode_time_ms>=time_ms_){last_decode_time_ms_=decode_time_ms;is_seeking_=false;//······//图片数据格式转换//······//把转换后的数据绘制到屏幕上}av_packet_unref(pkt);}绘制画面之前,要进行图片数据格式的转换,这里就要用到前面获取到的 SwsContext;//图片数据格式转换intresult=sws_scale(sws_context,(constuint8_t*const*)frame->data,frame->linesize,0,video_height_,rgba_frame->data,rgba_frame->linesize);if(resultlinesize[0],rgba_frame->linesize[0]);}ANativeWindow_unlockAndPost(native_window_);}以上就是主要的解码过程。除此之外,因为 C++ 使用资源和内存空间时需要自行释放,所以解码结束后还需要调用释放的接口释放资源,以免造成内存泄漏。sws_freeContext(data_convert_context);av_free(out_buffer_);av_frame_free(&rgba_frame);av_frame_free(&frame);av_packet_free(&packet);avcodec_close(codec_ctx);avcodec_free_context(&codec_ctx);avformat_close_input(&av_format_context);avformat_free_context(av_format_context);ANativeWindow_release(native_window_);2.3 简单应用为了更好地理解视频解码的过程,这里封装一个视频解码器 VideoDecoder ,解码器初步会有以下几个函数:VideoDecoder(constchar*path,std::functionon_decode_frame);voidPrepare(ANativeWindow*window);boolDecodeFrame(longtime_ms);voidRelease();在这个视频解码器中,输入指定时间戳后会返回解码的这一帧数据。其中较为重要的是 DecodeFrame(long time_ms) 函数,它可以由使用者自行调用,传入指定帧的时间戳,进而解码对应的帧数据。此外,可以增加同步锁以实现解码线程和使用线程分离。2.3.1 加入同步锁实现视频播放若只要对视频进行解码,是不需要使用同步等待的;但若是要实现视频的播放,那么每解码绘制完一帧就需使用锁进行同步等待,这是因为播放视频时需要让解码和绘制分离、且按照一定的时间轴顺序和速度进行解码和绘制。condition_.wait(lock);在上层调用 DecodeFrame 函数传入解码的时间戳时唤醒同步锁,让解码绘制的循环继续执行。boolVideoDecoder:ecodeFrame(longtime_ms){//······time_ms_=time_ms;condition_.notify_all();returntrue;}2.3.2 播放时加入 seek_frame在正常播放情况下,视频是一帧一帧逐帧解码播放;但在拖动进度条到达指定的 seek 点的情况下,如果还是从头到尾逐帧解码到 seek 点的话,效率可能不太高。这时候就需要在一定规则内对 seek 点的时间戳做检查,符合条件的直接 seek 到指定的时间戳。FFmpeg 中的 av_seek_frameav_seek_frame 可以定位到关键帧和非关键帧,这取决于选择的 flag 值。因为视频的解码需要依赖关键帧,所以一般我们需要定位到关键帧;intav_seek_frame(AVFormatContext*s,intstream_index,int64_ttimestamp,intflags);av_seek_frame 中的 flag 是用来指定寻找的 I 帧和传入的时间戳之间的位置关系。当要 seek 已过去的时间戳时,时间戳不一定会刚好处在 I 帧的位置,但因为解码需要依赖 I 帧,所以需要先找到此时间戳附近一个的 I 帧,此时 flag 就表明要 seek 到当前时间戳的前一个 I 帧还是后一个 I 帧;flag 有四个选项:flag 选项描述AVSEEK_FLAG_BACKWARD第一个 Flag 是 seek 到请求的时间戳之前最近的关键帧。通常情况下,seek 以 ms 为单位,若指定的 ms 时间戳刚好不是关键帧(大几率),会自动往回 seek 到最近的关键帧。虽然这种 flag 定位并不是非常精确,但能够较好地处理掉马赛克的问题,因为 BACKWARD 的方式会向回查找关键帧处,定位到关键帧处。AVSEEK_FLAG_BYTE第二个 Flag 是 seek 到文件中对应的位置(字节表示),和 AVSEEK_FLAG_FRAME 完全一致,但查找算法不同。AVSEEK_FLAG_ANY第三个 Flag 是可以 seek 到任意帧,不一定是关键帧,因此使用时可能出现花屏(马赛克),但进度和手滑完全一致。AVSEEK_FLAG_FRAME第四个 Flag 是 seek 的时间戳对应 frame 序号,可以理解为向后找到最近的关键帧,与 BACKWARD 的方向是相反的。flag 可能同时包含以上的多个值。比如 AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_BYTE;FRAME 和 BACKWARD 是按帧之间的间隔推算出 seek 的目标位置,适合快进快退;BYTE 则适合大幅度滑动。seek 的场景解码时传入的时间戳若是往前进的方向,并且超过上一帧时间戳有一定距离就需要 seek,这里的“一定距离”是通过多次实验估算所得,并非都是以下代码中使用的 1000ms;如果是往后退的方向且小于上一次解码时间戳,但与上一次解码时间戳的距离比较大(比如已超过 50ms),就要 seek 到上一个关键帧;使用 bool 变量 is_seeking_ 是为了防止其他干扰当前 seeking 的操作,目的是控制当前只有一个 seek 操作在进行。if(!is_seeking_&(time_ms_>last_decode_time_ms_+1000||time_ms_time_base.den/1000,AVSEEK_FLAG_BACKWARD);}插入 seek 的逻辑因为在解码前要检查是否 seek,所以要在 av_read_frame 函数(返回视频媒体流下一帧)之前插入 seek 的逻辑,符合 seek 条件时使用 av_seek_frame 到达指定 I 帧,接着 av_read_frame 后再继续解码到目的时间戳的位置。//是否进行seek的逻辑写在这//接下来是读取视频流的下一帧intret=av_read_frame(av_format_context,packet);2.4 解码过程中的细节2.4.1 DecodeFrame 时 seek 的条件使用 av_seek_frame 函数时需要指定正确的 flag,并且还要约定进行 seek 操作时的条件,否则视频可能会出现花屏(马赛克)。if(!is_seeking_&(time_ms_>last_decode_time_ms_+1000||time_ms_=last_decode_time_ms_){returnfalse;}time_ms_=time_ms;condition_.notify_all();returntrue;}有了以上这些条件的约束后,会减少一些不必要的解码操作。2.4.3 使用 AVFrame 的 ptsAVPacket 存储解码前的数据(编码数据:H264/AAC 等),保存的是解封装之后、解码前的数据,仍然是压缩数据;AVFrame 存储解码后的数据(像素数据:YUV/RGB/PCM 等);AVPacket 的 pts 和 AVFrame 的 pts 意义存在差异。前者表示这个解压包何时显示,后者表示帧数据何时显示;//AVPacket的pts/***PresentationtimestampinAVStream->time_baseunits;thetimeatwhich*thedecompressedpacketwillbepresentedtotheuser.*CanbeAV_NOPTS_VALUEifitisnotstoredinthefile.*ptsMUSTbelargerorequaltodtsaspresentationcannothappenbefore*decompression,unlessonewantstoviewhexdumps.Someformatsmisuse*thetermsdtsandpts/ctstomeansomethingdifferent.Suchtimestamps*mustbeconvertedtotruepts/dtsbeforetheyarestoredinAVPacket.*/int64_tpts;//AVFrame的pts/***Presentationtimestampintime_baseunits(timewhenframeshouldbeshowntouser).*/int64_tpts;是否将当前解码的帧数据绘制到画面上,取决于传入到解码时间戳与当前解码器返回的已解码帧的时间戳的比较结果。这里不可使用 AVPacket 的 pts,它很可能不是一个递增的时间戳;需要进行画面绘制的前提是:当传入指定的解码时间戳不大于当前已解码 frame 的 pts 换算后的时间戳时进行画面绘制。autodecode_time_ms=frame->pts*1000/stream->time_base.den;LOGD(TAG,"decode_time_ms=%ld",decode_time_ms);if(decode_time_ms>=time_ms_){last_decode_time_ms_=decode_time_ms;is_seeking=false;//画面绘制//····}2.4.4 解码最后一帧时视频已经没有数据使用 av_read_frame(av_format_context, packet)返回视频媒体流下一帧到 AVPacket 中。如果函数返回的 int 值是 0 则是 Success,如果小于 0 则是 Error 或者 EOF。因此如果在播放视频时返回的是小于 0 的值,调用 avcodec_flush_buffers 函数重置解码器的状态,flush 缓冲区中的内容,然后再 seek 到当前传入的时间戳处,完成解码后的回调,再让同步锁进行等待。//读取码流中的音频若干帧或者视频一帧,//这里是读取视频一帧(完整的一帧),获取的是一帧视频的压缩数据,接下来才能对其进行解码ret=av_read_frame(av_format_context,packet);if(rettime_base.den/1000,AVSEEK_FLAG_BACKWARD);LOGD(TAG,"retUnit){//抽第timeMs帧,根据sync是否同步等待fundecodeFrame(timeMSong,sync:Boolean=false){//若当前不需要抽帧时不进行等待if(nativeDecodeFrame(decoderPtr,timeMS)&sync){//······}else{//······}}privateexternalfunnativeDecodeFrame(decoderong,timeMSong):Booleancompanionobject{constvalTAG="FFmpegVideoDecoder"init{System.loadLibrary("ffmmpeg")}}}然后在 native-lib.cpp 中调用 C++ 层 VideoDecoder 的接口 DecodeFrame ,这样就通过 JNI 的方式建立起了上层和 C++ 底层之间的联系//native-lib.cppextern"C"JNIEXPORTjbooleanJNICALLJava_com_example_decoder_video_FFmpegVideoDecoder_nativeDecodeFrame(JNIEnv*env,jobjectthiz,jlongdecoder,jlongtime_ms){autovideoDecoder=(codec::VideoDecoder*)decoder;returnvideoDecoder->DecodeFrame(time_ms);}三、心得技术经验FFmpeg 编译后与 Android 结合起来实现视频的解码播放,便捷性很高。由于是用 C++ 层实现具体的解码流程,会有学习难度,最好有一定的 C++ 基础。四、附录C++ 封装的 VideoDecoderVideoDecoder.h#include#include#include#include#includeextern"C"{#include#include#include#include}#include/** VideoDecoder 可用于解码某个音视频文件(比如.mp4)中视频媒体流的数据。* Java 层传入指定文件的路径后,可以按一定 fps 循环传入指定的时间戳进行解码(抽帧),这一实现由 C++提供的 DecodeFrame 来完成。*在每次解码结束时,将解码某一帧的时间戳回调给上层的解码器,以供其他操作使用。*/namespacecodec{classVideoDecoder{private:std::stringpath_;longtime_ms_=-1;longlast_decode_time_ms_=-1;boolis_seeking_=false;ANativeWindow*native_window_=nullptr;ANativeWindow_Bufferwindow_buffer_{};、//视频宽高属性intvideo_width_=0;intvideo_height_=0;uint8_t*out_buffer_=nullptr;// on_decode_frame 用于将抽取指定帧的时间戳回调给上层解码器,以供上层解码器进行其他操作。std::functionon_decode_frame_=nullptr;boolis_stop_=false;//会与在循环同步时用的锁“std::unique_lock”配合使用std::mutexwork_queue_mtx;//真正在进行同步等待和唤醒的属性std::condition_variablecondition_;//解码器真正进行解码的函数voidDecode(AVCodecContext*codec_ctx,AVPacket*pkt,AVFrame*frame,AVStream*stream,std::unique_lock&lock,SwsContext*sws_context,AVFrame*pFrame);public://新建解码器时要传入媒体文件路径和一个解码后的回调 on_decode_frame。VideoDecoder(constchar*path,std::functionon_decode_frame);//在JNI层将上层传入的Surface包装后新建一个ANativeWindow传入,在后面解码后绘制帧数据时需要用到voidPrepare(ANativeWindow*window);//抽取指定时间戳的视频帧,可由上层调用boolDecodeFrame(longtime_ms);//释放解码器资源voidRelease();//获取当前系统毫秒时间staticint64_tGetCurrentMilliTime(void);};}VideoDecoder.cpp#include"VideoDecoder.h"#include"../log/Logger.h"#include#includeextern"C"{#include}#defineTAG"VideoDecoder"namespacecodec{VideoDecoder::VideoDecoder(constchar*path,std::functionon_decode_frame)n_decode_frame_(std::move(on_decode_frame)){path_=std::string(path);}voidVideoDecoder:ecode(AVCodecContext*codec_ctx,AVPacket*pkt,AVFrame*frame,AVStream*stream,std::unique_lock&lock,SwsContext*sws_context,AVFrame*rgba_frame){intret;/*sendthepacketwiththecompresseddatatothedecoder*/ret=avcodec_send_packet(codec_ctx,pkt);if(ret==AVERROR(EAGAIN)){LOGE(TAG,"Decode:Receive_frameandsend_packetbothreturnedEAGAIN,whichisanAPIviolation.");}elseif(ret=0&!is_stop_){//对于frame,avcodec_receive_frame内部每次都先调用ret=avcodec_receive_frame(codec_ctx,frame);if(ret==AVERROR(EAGAIN)||ret==AVERROR_EOF){return;}elseif(retpts*1000/stream->time_base.den;LOGD(TAG,"decode_time_ms=%ld",decode_time_ms);if(decode_time_ms>=time_ms_){LOGD(TAG,"decodedecode_time_ms=%ld,time_ms_=%ld",decode_time_ms,time_ms_);last_decode_time_ms_=decode_time_ms;is_seeking_=false;//数据格式转换intresult=sws_scale(sws_context,(constuint8_t*const*)frame->data,frame->linesize,0,video_height_,rgba_frame->data,rgba_frame->linesize);if(resultlinesize[0],rgba_frame->linesize[0]);}ANativeWindow_unlockAndPost(native_window_);}on_decode_frame_(decode_time_ms);int64_tendTime=GetCurrentMilliTime();LOGD(TAG,"decodeEndTime-decodeStartTime:%ld",endTime-startTime);LOGD(TAG,"finishdecodeframe");condition_.wait(lock);}//主要作用是清理AVPacket中的所有空间数据,清理完毕后进行初始化操作,并且将 data 与 size 置为0,方便下次调用。//释放packet引用av_packet_unref(pkt);}}voidVideoDecoder:repare(ANativeWindow*window){native_window_=window;av_register_all();autoav_format_context=avformat_alloc_context();avformat_open_input(&av_format_context,path_.c_str(),nullptr,nullptr);avformat_find_stream_info(av_format_context,nullptr);intvideo_stream_index=-1;for(inti=0;inb_streams;i++){//找到视频媒体流的下标if(av_format_context->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO){video_stream_index=i;LOGD(TAG,"findvideostreamindex=%d",video_stream_index);break;}}//runoncedo{if(video_stream_index==-1){codec:OGE(TAG,"PlayerError:Cannotfindvideostream");break;}std::unique_locklock(work_queue_mtx);//获取视频媒体流autostream=av_format_context->streams[video_stream_index];//找到已注册的解码器autocodec=avcodec_find_decoder(stream->codecpar->codec_id);//获取解码器上下文AVCodecContext*codec_ctx=avcodec_alloc_context3(codec);autoret=avcodec_parameters_to_context(codec_ctx,stream->codecpar);if(ret>=0){//打开avcodec_open2(codec_ctx,codec,nullptr);//解码器打开后才有宽高的值video_width_=codec_ctx->width;video_height_=codec_ctx->height;AVFrame*rgba_frame=av_frame_alloc();intbuffer_size=av_image_get_buffer_size(AV_PIX_FMT_RGBA,video_width_,video_height_,1);//分配内存空间给输出bufferout_buffer_=(uint8_t*)av_malloc(buffer_size*sizeof(uint8_t));av_image_fill_arrays(rgba_frame->data,rgba_frame->linesize,out_buffer_,AV_PIX_FMT_RGBA,video_width_,video_height_,1);//通过设置宽高限制缓冲区中的像素数量,而非屏幕的物理显示尺寸。//如果缓冲区与物理屏幕的显示尺寸不相符,则实际显示可能会是拉伸,或者被压缩的图像intresult=ANativeWindow_setBuffersGeometry(native_window_,video_width_,video_height_,WINDOW_FORMAT_RGBA_8888);if(resultpix_fmt,video_width_,video_height_,AV_PIX_FMT_RGBA,SWS_BICUBIC,nullptr,nullptr,nullptr);while(!is_stop_){LOGD(TAG,"frontseektime_ms_=%ld,last_decode_time_ms_=%ld",time_ms_,last_decode_time_ms_);if(!is_seeking_&(time_ms_>last_decode_time_ms_+1000||time_ms_time_base.den/1000,AVSEEK_FLAG_BACKWARD);}//读取视频一帧(完整的一帧),获取的是一帧视频的压缩数据,接下来才能对其进行解码ret=av_read_frame(av_format_context,packet);if(rettime_base.den/1000,AVSEEK_FLAG_BACKWARD);LOGD(TAG,"retsize){Decode(codec_ctx,packet,frame,stream,lock,data_convert_context,rgba_frame);}}//释放资源sws_freeContext(data_convert_context);av_free(out_buffer_);av_frame_free(&rgba_frame);av_frame_free(&frame);av_packet_free(&packet);}avcodec_close(codec_ctx);avcodec_free_context(&codec_ctx);}while(false);avformat_close_input(&av_format_context);avformat_free_context(av_format_context);ANativeWindow_release(native_window_);deletethis;}boolVideoDecoder:ecodeFrame(longtime_ms){LOGD(TAG,"DecodeFrametime_ms=%ld",time_ms);if(last_decode_time_ms_==time_ms||time_ms_==time_ms){LOGD(TAG,"DecodeFramelast_decode_time_ms_==time_ms");returnfalse;}if(last_decode_time_ms_>=time_ms&last_decode_time_ms_
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-15 14:29 , Processed in 0.499932 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表