ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Eclipse集成ffmpeg与opencv的Android视频处理模板搭建指南

Eclipse集成ffmpeg与opencv的Android视频处理模板搭建指南 简介面向Android音视频开发者的ffmpegopencv工程模板专为EclipseJNI环境打造帮助解决从零编译ffmpeg与opencv动态库时遇到的NDK配置、交叉编译、链接错误等烦琐问题让开发重心回到业务逻辑本身。压缩包内共433个文件包含180个hpp、175个h头文件、32个so动态库以及少量java源码、mk构建脚本、xml配置和可直接安装的demo apk整体大小49.79MB。ffmpeg已集成x264、aac及mp3lame编码器opencv库也预先编译并配置妥当导入Eclipse后只需设置好本机SDK/NDK路径即可编译运行。包内附带的apk与编译中间产物可用于快速验证环境是否正常省去排查构建时报错的麻烦。已有490人学习该资源尤其适合初涉Android音视频开发、希望跳过环境搭建环节直接进入实际编码的读者。1. 为什么要做Eclipse版的ffmpegopencv模板公司里还有一堆Eclipse工程在跑新的需求却要往里塞视频抽帧、ROI检测、画面修复。直接迁移到Android Studio几十个模块连带升级风险太大甲方也不批。剩下一条路就是把这个标题里的事干完在Eclipse工程里把ffmpeg的解码能力和opencv的算力接进去。ffmpeg负责读流、解出每一帧opencv负责像素级处理两套库通过JNI暴露给Java层。这套模板把最折腾的交叉编译、JNI桥接、so库打包做成固定骨架后续开发只需要替换jni目录下的算法代码。适合两类人一类是被分配去维护老项目的苦力另一类是准备在Eclipse下快速验证视频处理算法、又不想每次都从头配NDK环境的人。2. 模板的技术底座NDK工具链与编译链路的选型2.1 为什么Eclipse时代还在用ndk-build而不是CMakeEclipse ADT对ndk-build的集成是原生的工程里放一个jni目录右键Project可以看到“Refresh”和“Build Project”选项编译信息直接输出到控制台。CMake虽然已经是Android Studio的默认方案但在Eclipse里用CMake要配置外部工具链把生成器指向Ninja或Unix Makefiles再从项目属性里绑定构建命令中间任何一个环节跟Eclipse的自动构建机制对不上就会遇到“Library not found”这类问题。更关键的是ffmpeg当初就是用configure生成Makefile的体系产物天然面向Makefile。在Android.mk里用PREBUILT_SHARED_LIBRARY声明那些so文件跟ffmpeg的编译产物直接衔接中间不需要再包一层CMakeLists。ndk-build本身也是基于Makefile的所以这条链路可以少一层抽象。模板骨架里就用ndk-build后面要迁移到Android Studio时加一个CMakeLists.txt把Android.mk里的模块声明翻译过去就行这种改动工作量可控。2.2 ffmpeg交叉编译configure脚本里的调和项交叉编译ffmpeg最考验人的是configure参数。网上的ffmpeg使用教程一大半是PC版直接把./configure搬过来的后果是编出一堆x86的obj文件放进Android里全部报“Exec format error”。模板里我维护了一个build_ffmpeg.sh核心参数如下#!/usr/bin/env bash NDK/opt/android-ndk-r16b TOOLCHAIN$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64 SYSROOT$NDK/platforms/android-21/arch-arm ./configure \ --target-osandroid \ --archarm \ --cross-prefix$TOOLCHAIN/bin/arm-linux-androideabi- \ --sysroot$SYSROOT \ --disable-programs \ --disable-doc \ --disable-avdevice \ --disable-swresample \ --disable-postproc \ --disable-avfilter \ --enable-encodermjpeg \ --enable-decodermjpeg \ --enable-protocolfile \ --enable-demuxerimage2 \ --disable-network这段参数里真正影响模板可用性的是几个开关。--disable-programs去掉ffmpeg、ffprobe等可执行文件因为Android上不需要它们--disable-avfilter把滤镜链关掉模板初期不做转场特效--enable-decodermjpeg和--enable-demuxerimage2组合起来可以保证模板在测试阶段用图片序列验证管线。--disable-network是给纯本地文件解码准备的如果后续要做直播拉流这里必须改成--enable-network --enable-protocoltcp --enable-protocolrtmp。编译完先不要急着拿进工程在命令行下用file命令检查产物file libavcodec.so输出里必须出现“ELF 32-bit LSB shared object, ARM”才算交叉编译成功。如果显示的是x86-64说明--cross-prefix没有生效去检查TOOLCHAIN路径下是否真的有arm-linux-androideabi-gcc这个文件。arm64-v8a设备的库需要在另一个目录下用aarch64-linux-android-4.9工具链再编一份。2.3 opencv的接入方式Java接口和Native接口怎么选opencv-2.4时代还有一张脸叫OpenCV Manager需要用户在手机上单独装一个APK。到了模板这个阶段建议直接放弃Manager把so文件放进工程的libs目录一起打包。两种方案的对比看这张表接入方式集成成本运行性能适用场景OpenCVManager Java API低几行代码即可调用低每帧要跨JNI传输Mat快速验证轮廓检测等单帧操作Native Mat操作高需要自己编译opencv高Mat在C侧直接运算视频解码后的逐帧处理模板选第二种因为ffmpeg解码出的AVFrame数据本质上是一块内存用cv::Mat对这块内存做包装只需要一次类型转换如果走Java API要先把AVFrame转成Bitmap再转成Mat一来一回内存多搬两趟。另外很多opencv算法比如LBP特征检测、LK光流本身是用C写的直接在JNI层调用可以减少JNI回调次数。opencv安装教程里教的纯Java玩法在视频场景下性能会拖后腿。Java入口处只需要一次静态加载static { System.loadLibrary(opencv_java4); System.loadLibrary(avcodec); System.loadLibrary(avformat); System.loadLibrary(avutil); System.loadLibrary(template); }加载顺序有讲究opencv的so不依赖ffmpeg但template.so链接了前面四个必须把依赖库放在前面。Eclipse打包APK时会把libs目录下的所有so都塞进去所以这里并不需要再单独执行install命令。3. 搭一个可编译通过的最小模板工程3.1 Eclipse工程的目录结构与NDK环境Eclipse工程比Android Studio简单直接。新建一个Android Application Project后包名建议用com.example.ffopentpl后面JNI的方法名会用到这个包名。工程根目录下建jni子目录里面放Android.mk、Application.mk和C源码libs目录下放编译好的ffmpeg和opencv的so按ABI分子目录存放。目录长这样ffmpeg-opencv-template/ ├── src/com/example/ffopentpl/ │ └── FFmpegOpenCvTemplate.java ├── jni/ │ ├── Android.mk │ ├── Application.mk │ ├── bridge.c │ └── include/ # ffmpeg头文件 │ ├── libavcodec/avcodec.h │ └── libavformat/avformat.h ├── libs/ │ ├── armeabi-v7a/ │ │ ├── libavcodec.so │ │ ├── libavformat.so │ │ ├── libavutil.so │ │ ├── libopencv_java4.so │ │ └── libtemplate.so │ └── arm64-v8a/ └── project.propertiesEclipse的NDK配置在工程属性里右键项目 - Properties - Builders - New新建一个Program类型的BuilderLocation指向ndk-build.cmdWorking Directory选项目根目录。ARGS留空因为ndk-build会自己找jni目录。这样每次点Build Project按钮Eclipse就会依次调用javac和ndk-build最终把so打进APK。3.2 Android.mk与Application.mk的写法模板的核心是jni/Android.mk它要把ffmpeg和opencv作为prebuilt库引入再把自己写的bridge.c编译成另一个soLOCAL_PATH : $(call my-dir) # ffmpeg 组件 include $(CLEAR_VARS) LOCAL_MODULE : avcodec LOCAL_SRC_FILES : ../libs/$(TARGET_ARCH_ABI)/libavcodec.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE : avformat LOCAL_SRC_FILES : ../libs/$(TARGET_ARCH_ABI)/libavformat.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE : avutil LOCAL_SRC_FILES : ../libs/$(TARGET_ARCH_ABI)/libavutil.so include $(PREBUILT_SHARED_LIBRARY) # opencv include $(CLEAR_VARS) LOCAL_MODULE : opencv_java4 LOCAL_SRC_FILES : ../libs/$(TARGET_ARCH_ABI)/libopencv_java4.so include $(PREBUILT_SHARED_LIBRARY) # 模板主模块 include $(CLEAR_VARS) LOCAL_MODULE : template LOCAL_SRC_FILES : bridge.c LOCAL_C_INCLUDES : $(LOCAL_PATH)/include \ $(LOCAL_PATH)/opencvInclude LOCAL_SHARED_LIBRARIES : avcodec avformat avutil opencv_java4 include $(BUILD_SHARED_LIBRARY)LOCAL_SHARED_LIBRARIES这一行的顺序建议保持依赖在前ffmpeg各库之间没有相互依赖可以不刻意排序但opencv放在最后面不影响链接。LOCAL_C_INCLUDES指向了ffmpeg头文件的目录如果opencv的路径写错编译bridge.c时会报“opencv2/core.hpp: No such file or directory”这个错误信息在Eclipse控制台里很容易定位。Application.mk控制ABI和平台级别APP_ABI : armeabi-v7a arm64-v8a APP_PLATFORM : android-21 APP_CFLAGS : -O2 -stdc99 -WallAPP_ABI不要随手写allx86用的so对手机没有意义反而会让APK体积变大。ARMeabi-v7a和arm64-v8a两种已经覆盖绝大多数Android设备模拟器调试再加x86。APP_PLATFORM取android-21也就是Android 5.0一个合理的底线这样用NDK r16b不会报平台过低的问题。3.3 JNI桥接层与Java端的调用顺序bridge.c的初始版本不需要写解码逻辑先让“so能加载、方法能被调用”这条链路通起来。JNI方法名硬编码了Java包名和类名#include jni.h #include android/log.h #define LOG_TAG FFOpenTpl #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) JNIEXPORT jint JNICALL Java_com_example_ffopentpl_FFmpegOpenCvTemplate_nativePing( JNIEnv *env, jobject thiz) { LOGD(nativePing called); return 2024; }Java端对应声明public class FFmpegOpenCvTemplate { static { System.loadLibrary(opencv_java4); System.loadLibrary(avcodec); System.loadLibrary(avformat); System.loadLibrary(avutil); System.loadLibrary(template); } public native int nativePing(); public String ping() { int code nativePing(); return native code code; } }跑一次Activity调用ping()logcat里能看到FFOpenTpl的debug日志就说明JNI签名、类加载、so依赖全部正常。这里有个新手常踩的坑如果Java包名改成3段以上例如com.company.video.algoJNI函数名必须写成Java_com_company_video_algo_FFmpegOpenCvTemplate_nativePing下划线一层都不能少。包名定了之后尽量别再改否则所有JNI方法都要同步改签名。4. 用模板跑通一个视频抽帧图像处理管线4.1 解码端循环AVFormatContext、AVCodecContext的初始化模板真正的价值在bridge.c里如何组织ffmpeg的解码循环。代码结构是固定的video解码的骨架如下#include libavformat/avformat.h #include libavcodec/avcodec.h #include libavutil/imgutils.h int decode_video_to_gray(const char *input_path, const char *output_path) { AVFormatContext *fmt_ctx NULL; AVCodecContext *codec_ctx NULL; AVFrame *frame NULL; AVPacket *pkt NULL; int video_stream_idx -1; avformat_open_input(fmt_ctx, input_path, NULL, NULL); avformat_find_stream_info(fmt_ctx, NULL); video_stream_idx av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); const AVCodec *codec avcodec_find_decoder( fmt_ctx-streams[video_stream_idx]-codecpar-codec_id); codec_ctx avcodec_alloc_context3(codec); avcodec_parameters_to_context(codec_ctx, fmt_ctx-streams[video_stream_idx]-codecpar); avcodec_open2(codec_ctx, codec, NULL); frame av_frame_alloc(); pkt av_packet_alloc(); while (av_read_frame(fmt_ctx, pkt) 0) { if (pkt-stream_index video_stream_idx) { avcodec_send_packet(codec_ctx, pkt); while (avcodec_receive_frame(codec_ctx, frame) 0) { process_frame_and_save(frame, codec_ctx, output_path); av_frame_unref(frame); } } av_packet_unref(pkt); } av_packet_free(pkt); av_frame_free(frame); avcodec_free_context(codec_ctx); avformat_close_input(fmt_ctx); return 0; }这段代码的典型错误是忘记av_frame_unref导致内存随着解码帧数线性上涨。av_read_frame每读一个packet都要av_packet_unref否则旧数据会累积。avcodec_send_packet和avcodec_receive_frame的搭配在新版本ffmpeg里是标准姿势发送一个、收干净所有帧再发下一个。模板里帧率控制用一个简单计数int frame_count 0; if (frame_count % 3 0) { process_frame_and_save(...); } frame_count;这样相当于每3帧取1帧也就是1/3抽帧率输出帧率本来是30fps的话就降成了10fps。4.2 AVFrame到Mat的转换共享buffer而不是拷贝process_frame_and_save里做两件事把AVFrame转成cv::Mat然后对这个Mat做灰度化处理。ffmpeg解码出来的原始格式大概率是YUV420P不能直接当作RGB用#include opencv2/core.hpp #include opencv2/imgproc.hpp void process_frame_and_save(AVFrame *frame, AVCodecContext *codec_ctx, const char *output_path) { int width codec_ctx-width; int height codec_ctx-height; // YUV420P 数据Y平面 sizewidth*height // U/V平面在数据区后部各占 width*height/4 cv::Mat yuv420p(height * 3 / 2, width, CV_8UC1, frame-data[0], frame-linesize[0]); cv::Mat bgr; cv::cvtColor(yuv420p, bgr, cv::COLOR_YUV2BGR_I420); cv::Mat gray; cv::cvtColor(bgr, gray, cv::COLOR_BGR2GRAY); // 保存或者交给后续算法 cv::imwrite(output_path, gray); }这里有一个必须注意的细节cv::Mat的构造传入了frame-linesize[0]作为每行字节数。ffmpeg解码输出时会对每一行做对齐行宽不一定等于width如果直接用默认的step构造Mat画面会出现斜线。一旦发现输出的图片沿45度斜向偏移就是linesize的参数没传。COLOR_YUV2BGR_I420这个枚举专门对应ffmpeg输出的YUV420P平面式布局如果用了COLOR_YUV2BGR_NV12虽然不会崩溃但颜色一定错乱。cv::Mat yuv420p只是包装了frame-data[0]这块内存没有发生数据拷贝。所以process_frame里必须保证AVFrame的生命周期比Mat长av_frame_unref调用之后再去访问Mat内容就是非法内存了。实测中如果发现某些视频抽出来的帧有花屏或者半绿半红多半是循环里提前unref了frame。4.3 抽帧参数表与内存回收陷阱模板运行起来后最需要关心的三个参数是抽帧步长、Mat的连续性和终态释放顺序。抽帧步长决定耗时连续性决定cvtColor的表现释放顺序决定有没有野指针。常用配置见下表参数推荐值说明frame_count取模取3或5视频有30fps时取3帧率减到10fpsYUV格式I420与ffmpeg默认输出一致避免额外转换Mat构造stepframe-linesize[0]必须原样传入不能填width帧处理多线程关闭模板先保证单线程稳定运行另一个常在EEclipse环境里被忽视的坑是avcodec_alloc_context3与avcodec_parameters_to_context的相对顺序。旧代码直接不用parameters_to_context而是靠codec_ctx-width decoder_ctx-width这样的赋值在新版ffmpeg中这些字段已经被移到AVCodecParameters里了直接访问codec_ctx-width编译倒是能过但运行时报空指针。模板统一走parameters_to_context就是避免旧接口的兼容性问题。内存回收的顺序按代码执行就不展开了但有一个原则谁申请谁释放。frame是av_frame_alloc分配的就用av_frame_freepacket是av_packet_alloc的就用av_packet_free。混用av_free或者free会导致libavutil的内置内存池计数错乱下一次循环分配帧时应用直接崩。5. 模板的进阶用法裁剪so库、调试与冒烟验证5.1 用--disable-everything把ffmpeg裁剪到能用直接拿完整ffmpeg编出来的so光是libavcodec就有20MB以上整包塞进模板APK既占空间又拖长编译时间。模板的进阶配置应该改成--disable-everything开头再按需打开组件。上一章的样本里只开了mjpeg一个10秒的短视频用mjpeg解码器根本解不开所以实际模板的默认配置至少需要H.264和H.265./configure \ --target-osandroid \ --archarm \ --cross-prefix$TOOLCHAIN/bin/arm-linux-androideabi- \ --sysroot$SYSROOT \ --disable-everything \ --enable-decoderh264,hevc,mpeg4 \ --enable-parserh264,hevc,mpeg4video \ --enable-demuxermov,flv,mpegts,avi \ --enable-protocolfile \ --disable-network裁剪后libavcodec.so能压在8MB以内libavformat在1MB以内。判断裁剪是否过度的方法是看avi格式的容器解析--enable-demuxeravi漏了ffmpeg会报Invalid data found when processing input注意排查av_find_best_stream之前有没有判定fmt_ctx是否为NULL。5.2 打日志alLog与logcat的配合JNI层里加日志比Java层多费一步要引入头文件并做一次带TAG的输出#include android/log.h #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, ffopentpl, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, ffopentpl, __VA_ARGS__)在avcodec_receive_frame成功后打一条日志记录当前帧号和pts值。ffmpeg输出的pts是以time_base为单位的换算成毫秒要乘以av_q2d(stream-time_base) * 1000。如果你在logcat里看到pts越跳越大或者突然跳回0通常是时间基设置不对先检查stream的time_base和codec_ctx的framerate是不是来自同一路的AVStream。调试期间日志必须能体现出“每秒多少帧”这个指标这样调线程优先级、抽帧步长时才有数据支撑。5.3 冒烟测试的验收标准模板交出去之前我习惯在jni目录下放一个smoke_test命令用adb push进去跑一次。冒烟测试的输入是一个用ffmpeg命令行生成的测试文件ffmpeg -f lavfi -i testsrcduration5:size640x480:rate30 \ -pix_fmt yuv420p /data/local/tmp/smoke.mp4然后用app_process调native方法处理这个文件检查输出目录下有哪些jpg文件产生。验收标准就两条5秒视频抽帧得到的jpeg数量不少于10张即抽帧率在2~3之间所有jpeg的尺寸都是640x432而不是640x480这个尺寸差就是时间基和容器尺寸对冲的结果在32位颜色深度下YUV420P要求宽高得是偶数正常正好退化到432行。如果高度变成208说明linesize的换算逻辑被改坏了。全部通过这个模板就可以真正投入算法开发了。本文还有配套的精品资源点击获取
返回列表