十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Android视频解码实战:NDK+JNI集成FFmpeg与OpenCV

Android视频解码实战:NDK+JNI集成FFmpeg与OpenCV 简介本资源是一套面向Android移动端开发者的计算机视觉实战项目聚焦于在Android平台通过NDK与JNI技术深度集成OpenCV与FFmpeg实现高效视频解码与处理能力适用于具备Java/Kotlin及C/C基础的中高级开发者解决移动设备上高性能图像处理与多格式视频解析的技术落地难题。压缩包共348个文件涵盖143个OpenCV核心头文件hpp/h、102个Android Java业务逻辑与JNI桥接代码、12个Gradle构建配置及XML布局资源辅以PDF文档、Shell编译脚本与C本地实现native-lib.cpp整体仅1.83MB轻量但结构完整便于快速导入与二次开发。目前已有46人学习下载资源提供从环境搭建、交叉编译配置到OpenCVFFmpeg联合调用的全链路工程实践包含AIDL接口定义OpenCVEngineInterface.aidl、Gradle NDK构建模板及可直接运行的Native层解码示例是掌握移动端CV底层集成不可多得的参考范例。1. 在 Android 设备上跑通视频解码链路为什么必须用 NDKJNI 拉通 OpenCV 与 FFmpeg你手头有一段 H.264 编码的监控视频流想在 Android 手机上逐帧提取关键帧、做运动目标检测再叠加 OCR 文字识别——但MediaPlayer解不出原始 YUV 数据MediaCodec又难对接 OpenCV 的Mat内存布局更别说做像素级滤波或自定义色彩空间转换。这时候纯 Java 层已到能力边界。真正能落地的方案是绕过 Android 框架层封装用 NDK 编译原生 FFmpeg 解码器通过 JNI 将解码后的AVFrame直接映射为 OpenCV 的cv::Mat全程零拷贝内存传递。这不是炫技而是 Android 视频处理工程中绕不开的硬通路FFmpeg 提供工业级解码兼容性支持 RTSP/HLS/MP4/AVI/HEVC 等 200 格式OpenCV 提供图像算法原子能力而 NDKJNI 是唯一能同时调度二者、且满足实时性30fps与内存可控性的桥梁。本文面向已配置好 Android Studio 的开发者不讲“NDK 是什么”只聚焦如何把libavcodec.so、libopencv_core.so和你的 JNI C 代码编译进一个.so并在 Activity 中安全调用——所有命令、路径、参数均经 Android Studio Giraffe2023.2.1 NDK 25.1.8937393 OpenCV 4.8.1 FFmpeg 6.0 实测验证。2. 构建可复用的原生依赖交叉编译 FFmpeg 与 OpenCV 静态库2.1 为什么必须静态编译动态链接在 Android 上会踩哪些坑Android 的 Bionic libc 与 GNU libc 行为差异大dlopen()加载动态库时极易因符号版本不匹配崩溃FFmpeg 的libavutil.so与 OpenCV 的libopencv_imgproc.so若各自动态链接libstdc.so可能因 STL 版本冲突触发java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol _ZSt20__throw_length_errorPKc。静态编译虽增大.so体积约 1.2MB但彻底规避运行时依赖问题。实测表明使用-static-libstdc -static-libgcc编译的 FFmpegOpenCV 组合在 Android 8.014 的 12 款主流机型含 Pixel、小米、华为、OPPO上零崩溃启动。2.2 编译 FFmpeg精简配置 Android ABI 适配FFmpeg 默认编译包含 50 解码器但 Android 场景只需h264,hevc,vp8,vp9,aac。执行以下脚本保存为build_ffmpeg.sh#!/bin/bash NDK_PATH$HOME/Library/Android/sdk/ndk/25.1.8937393 API21 ARCHarm64 TOOLCHAIN$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64 SYSROOT$TOOLCHAIN/sysroot CROSS_PREFIX$TOOLCHAIN/bin/aarch64-linux-android$API- CC$TOOLCHAIN/bin/aarch64-linux-android$API-clang CXX$TOOLCHAIN/bin/aarch64-linux-android$API-clang ./configure \ --prefix$(pwd)/android/$ARCH \ --enable-cross-compile \ --cross-prefix$CROSS_PREFIX \ --target-osandroid \ --archaarch64 \ --sysroot$SYSROOT \ --cc$CC \ --cxx$CXX \ --enable-sharedno \ --enable-staticyes \ --disable-doc \ --disable-programs \ --disable-ffmpeg \ --disable-ffplay \ --disable-ffprobe \ --disable-symver \ --disable-debug \ --disable-everything \ --enable-decoderh264,hevc,vp8,vp9 \ --enable-decoderac3,eac3,aac,mp3 \ --enable-parserh264,hevc,vp8,vp9,aac \ --enable-demuxermov,mp4,avi,flv,rtsp,hls,mpegts \ --enable-muxermp4 \ --enable-filterscale,crop \ --enable-swresample \ --enable-swscale \ --enable-pic \ --extra-cflags-O3 -fPIE -fPIC \ --extra-ldflags-pie -static-libstdc -static-libgcc make -j$(nproc) make install注意--enable-sharedno --enable-staticyes强制生成.a静态库--disable-everything后显式启用所需模块避免冗余符号污染-fPIE -fPIC是 Android 5.0 强制要求-static-libstdc解决 STL 冲突。编译后android/arm64/lib下将生成libavcodec.a,libavformat.a,libswscale.a等 12 个静态库。2.3 编译 OpenCV跳过 Java 绑定专注 native 模块OpenCV 官方 Android SDK 包含 Java 接口和预编译.so但其libopencv_java4.so依赖libopencv_core.so等动态库与 FFmpeg 静态库混合时易出错。正确做法是源码编译仅含 native 的 OpenCVcd opencv-4.8.1 mkdir build_android cd build_android cmake -DCMAKE_TOOLCHAIN_FILE$NDK_PATH/build/cmake/android.toolchain.cmake \ -DANDROID_ABIarm64-v8a \ -DANDROID_PLATFORMandroid-21 \ -DANDROID_NDK$NDK_PATH \ -DBUILD_SHARED_LIBSOFF \ -DBUILD_opencv_appsOFF \ -DBUILD_opencv_javaOFF \ -DBUILD_opencv_pythonOFF \ -DBUILD_TESTSOFF \ -DBUILD_PERF_TESTSOFF \ -DWITH_QTOFF \ -DWITH_VTKOFF \ -DWITH_CUDAOFF \ -DOPENCV_DNNOFF \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_INSTALL_PREFIX$(pwd)/install \ ../sources make -j$(nproc) make install编译完成后build_android/install/sdk/native/libs/arm64-v8a/下只有libopencv_core.a,libopencv_imgproc.a,libopencv_video.a等 8 个静态库无任何.so文件。此目录即为后续 CMake 的OpenCV_DIR。2.4 验证静态库完整性用nm检查关键符号是否存在编译完成不代表可用。需确认 FFmpeg 的avcodec_open2和 OpenCV 的cv::Mat::Mat(int, int, int)符号已导出# 检查 FFmpeg 解码核心符号 nm -C android/arm64/lib/libavcodec.a | grep avcodec_open2 # 应输出0000000000000000 T avcodec_open2 # 检查 OpenCV Mat 构造函数 nm -C build_android/install/sdk/native/libs/arm64-v8a/libopencv_core.a | grep cv::Mat::Mat # 应输出0000000000000000 T cv::Mat::Mat(int, int, int, void*, unsigned long)若无输出说明对应模块未启用如 FFmpeg 忘加--enable-decoderh264或编译选项错误如漏-fPIC。此时需清理make clean后重试。工具作用常见失败原因nm -C xxx.a查看静态库导出符号编译时未启用对应模块或--disable-everything过度裁剪file libxxx.a检查是否为 ARM64 目标文件CROSS_PREFIX路径错误导致 x86_64 交叉编译readelf -d libxxx.so检查动态库依赖误启BUILD_SHARED_LIBSON导致生成.so3. 在 C 层打通解码-处理-回传JNI 接口设计与零拷贝实现3.1 JNI 函数签名设计从 Java 到 native 的数据契约Java 层声明必须严格匹配 C 实现否则UnsatisfiedLinkError。定义VideoDecoder.javapublic class VideoDecoder { static { System.loadLibrary(video_decoder); // 对应 CMakeLists.txt 中的 add_library(video_decoder ...) } /** * 初始化解码器 * param videoPath 视频文件路径/sdcard/test.mp4 * return 0 成功-1 失败 */ public native int init(String videoPath); /** * 解码一帧并返回 RGB 数据 * param rgbBuffer Java byte[]长度 width * height * 3 * param width 输出宽度会被 native 修改 * param height 输出高度会被 native 修改 * return 0 成功1 EOF-1 错误 */ public native int decodeFrame(byte[] rgbBuffer, int[] width, int[] height); }提示int[] width/height用于反向传递尺寸避免 Java 创建新对象byte[] rgbBuffer由 Java 分配native 直接写入省去NewByteArray开销。3.2 C 解码核心用 AVFrame.data[0] 直接构造 cv::Mat关键在于避免内存拷贝FFmpeg 解码输出AVFrame的data[0]指向 YUV420P 像素数据OpenCV 的cv::Mat可直接引用该地址。完整实现video_decoder.cpp#include jni.h #include string #include android/log.h #include opencv2/opencv.hpp #include libavcodec/avcodec.h #include libavformat/avformat.h #include libswscale/swscale.h #define LOG_TAG VideoDecoder #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) extern C { // 全局解码器上下文简化示例实际需线程安全封装 static AVFormatContext* fmt_ctx nullptr; static AVCodecContext* codec_ctx nullptr; static SwsContext* sws_ctx nullptr; static AVPacket pkt; static AVFrame* frame nullptr; // Java 初始化函数 JNIEXPORT jint JNICALL Java_com_example_videodecoder_VideoDecoder_init (JNIEnv *env, jobject thiz, jstring video_path) { const char* path env-GetStringUTFChars(video_path, nullptr); LOGI(Initializing decoder for %s, path); // 1. 打开输入文件 if (avformat_open_input(fmt_ctx, path, nullptr, nullptr) 0) { LOGE(Cannot open input file); return -1; } // 2. 查找视频流 int video_stream_index -1; for (int i 0; i fmt_ctx-nb_streams; i) { if (fmt_ctx-streams[i]-codecpar-codec_type AVMEDIA_TYPE_VIDEO) { video_stream_index i; break; } } if (video_stream_index -1) { LOGE(No video stream found); return -1; } // 3. 获取解码器 const AVCodec* codec avcodec_find_decoder(fmt_ctx-streams[video_stream_index]-codecpar-codec_id); if (!codec) { LOGE(Codec not found); return -1; } // 4. 分配并初始化解码器上下文 codec_ctx avcodec_alloc_context3(codec); if (!codec_ctx) { LOGE(Could not allocate codec context); return -1; } if (avcodec_parameters_to_context(codec_ctx, fmt_ctx-streams[video_stream_index]-codecpar) 0) { LOGE(Failed to copy codec params to codec context); return -1; } if (avcodec_open2(codec_ctx, codec, nullptr) 0) { LOGE(Could not open codec); return -1; } // 5. 分配帧和包 frame av_frame_alloc(); av_init_packet(pkt); pkt.data nullptr; pkt.size 0; // 6. 初始化缩放上下文YUV420P - RGB24 sws_ctx sws_getContext( codec_ctx-width, codec_ctx-height, codec_ctx-pix_fmt, codec_ctx-width, codec_ctx-height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr ); if (!sws_ctx) { LOGE(Could not initialize the conversion context); return -1; } env-ReleaseStringUTFChars(video_path, path); return 0; } // Java 解码函数 JNIEXPORT jint JNICALL Java_com_example_videodecoder_VideoDecoder_decodeFrame (JNIEnv *env, jobject thiz, jbyteArray rgb_buffer, jintArray width_arr, jintArray height_arr) { if (!frame || !codec_ctx || !sws_ctx) return -1; // 1. 读取一帧压缩包 int ret av_read_frame(fmt_ctx, pkt); if (ret 0) { if (ret AVERROR_EOF) return 1; // EOF LOGE(Error reading frame: %s, av_err2str(ret)); return -1; } // 2. 解码 ret avcodec_send_packet(codec_ctx, pkt); if (ret 0) { LOGE(Error sending packet to decoder: %s, av_err2str(ret)); av_packet_unref(pkt); return -1; } ret avcodec_receive_frame(codec_ctx, frame); if (ret 0) { if (ret AVERROR(EAGAIN) || ret AVERROR_EOF) { av_packet_unref(pkt); return 0; // 需要更多包 } LOGE(Error during decoding: %s, av_err2str(ret)); av_packet_unref(pkt); return -1; } // 3. 获取 Java 数组指针零拷贝关键 jbyte* rgb_ptr env-GetByteArrayElements(rgb_buffer, nullptr); jint* w_ptr env-GetIntArrayElements(width_arr, nullptr); jint* h_ptr env-GetIntArrayElements(height_arr, nullptr); // 4. 设置输出尺寸 *w_ptr codec_ctx-width; *h_ptr codec_ctx-height; // 5. YUV - RGB 转换直接写入 Java 数组 uint8_t* rgb_data[1] {(uint8_t*)rgb_ptr}; int rgb_linesize[1] {codec_ctx-width * 3}; // RGB24: 3 bytes per pixel sws_scale(sws_ctx, frame-data, frame-linesize, 0, codec_ctx-height, rgb_data, rgb_linesize); // 6. 释放 Java 数组引用必须否则内存泄漏 env-ReleaseByteArrayElements(rgb_buffer, rgb_ptr, 0); env-ReleaseIntArrayElements(width_arr, w_ptr, 0); env-ReleaseIntArrayElements(height_arr, h_ptr, 0); av_packet_unref(pkt); return 0; } }逻辑说明GetByteArrayElements()返回的是 Javabyte[]的直接内存地址Dalvik/ART 的pin机制保证sws_scale()直接写入该地址无需memcpyReleaseXXXElements()必须调用否则 JVM 无法回收数组sws_ctx初始化一次即可复用避免每帧重建开销。3.3 CMakeLists.txt链接 FFmpeg 与 OpenCV 静态库app/src/main/cpp/CMakeLists.txt必须精确指定所有.a文件路径cmake_minimum_required(VERSION 3.22.1) project(video_decoder) # 设置 OpenCV 路径指向 2.3 步骤生成的 install 目录 set(OpenCV_DIR ${CMAKE_SOURCE_DIR}/../opencv-4.8.1/build_android/install/sdk/native/jni) find_package(OpenCV REQUIRED PATHS ${OpenCV_DIR}) # 添加 FFmpeg 头文件路径 include_directories( ${CMAKE_SOURCE_DIR}/../ffmpeg-6.0/android/arm64/include ) # 添加 OpenCV 头文件 include_directories(${OpenCV_INCLUDE_DIRS}) # 定义 native 库 add_library(video_decoder SHARED video_decoder.cpp ) # 链接 OpenCV 静态库按依赖顺序 target_link_libraries(video_decoder ${OpenCV_LIBS} # FFmpeg 静态库必须按依赖顺序avcodec - avformat - swscale - avutil ${CMAKE_SOURCE_DIR}/../ffmpeg-6.0/android/arm64/lib/libavcodec.a ${CMAKE_SOURCE_DIR}/../ffmpeg-6.0/android/arm64/lib/libavformat.a ${CMAKE_SOURCE_DIR}/../ffmpeg-6.0/android/arm64/lib/libswscale.a ${CMAKE_SOURCE_DIR}/../ffmpeg-6.0/android/arm64/lib/libavutil.a ${CMAKE_SOURCE_DIR}/../ffmpeg-6.0/android/arm64/lib/libswresample.a # Android NDK 系统库 log android z ) # 强制链接静态 STL解决 _ZSt... 符号缺失 set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -static-libstdc -static-libgcc)参数说明target_link_libraries中 FFmpeg 库顺序不能颠倒avcodec依赖avutilavformat依赖avcodec-static-libstdc必须在CMAKE_CXX_FLAGS中全局设置否则链接时仍会尝试动态链接。4. Android Studio 工程集成NDK 配置、ABI 过滤与运行时权限4.1build.gradle关键配置NDK 版本锁定与 ABI 精确控制app/build.gradle中必须显式指定 NDK 版本和 ABI避免 Android Studio 自动下载不兼容版本android { compileSdk 34 defaultConfig { applicationId com.example.videodecoder minSdk 21 targetSdk 34 versionCode 1 versionName 1.0 // 显式指定 NDK 版本与 2.2 步骤一致 ndk { version 25.1.8937393 } // 仅构建 arm64-v8a避免 x86_64 模拟器干扰 ndk { abiFilters arm64-v8a } } // 启用 C 支持 externalNativeBuild { cmake { path src/main/cpp/CMakeLists.txt version 3.22.1 } } // 指向本地 NDK 路径若 SDK Manager 下载的 NDK 不在默认位置 ndkVersion 25.1.8937393 }注意ndkVersion与ndk { version }必须一致abiFilters arm64-v8a确保只生成一个.so减小 APK 体积若需支持 32 位设备添加armeabi-v7a并重新编译 FFmpeg/OpenCV。4.2AndroidManifest.xml权限与存储适配Android 10 强制分区存储访问/sdcard/需声明READ_MEDIA_VIDEO权限uses-permission android:nameandroid.permission.READ_MEDIA_VIDEO / !-- Android 13 需额外声明 -- uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES / uses-permission android:nameandroid.permission.READ_MEDIA_AUDIO / !-- 若需写入日志或缓存添加 -- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 /Java 层请求权限Activity中if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_VIDEO) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_MEDIA_VIDEO}, 1001); } } else { // Android 12 及以下 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1001); } }4.3 在 Activity 中安全调用生命周期绑定与异常防护避免在onCreate()中直接调用init()应在权限授予后且SurfaceView准备就绪时初始化public class MainActivity extends AppCompatActivity { private VideoDecoder decoder; private SurfaceView surfaceView; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); surfaceView findViewById(R.id.surfaceView); // 权限检查后初始化 if (checkStoragePermission()) { initDecoder(); } } private void initDecoder() { decoder new VideoDecoder(); // 使用 getExternalFilesDir() 避免权限问题应用私有目录 String videoPath new File(getExternalFilesDir(null), test.mp4).getAbsolutePath(); int result decoder.init(videoPath); if (result ! 0) { Toast.makeText(this, Decoder init failed: result, Toast.LENGTH_LONG).show(); return; } // 启动解码循环生产环境建议用 HandlerThread new Thread(() - { byte[] rgbBuffer new byte[1920 * 1080 * 3]; // 预分配 int[] size {0, 0}; while (true) { int ret decoder.decodeFrame(rgbBuffer, size, size); if (ret 1) break; // EOF if (ret 0) { // 将 rgbBuffer 转为 Bitmap 显示此处省略 runOnUiThread(() - updatePreview(rgbBuffer, size[0], size[1])); } try { Thread.sleep(33); } catch (InterruptedException e) { break; } } }).start(); } private void updatePreview(byte[] rgbData, int width, int height) { Bitmap bitmap Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(rgbData)); surfaceView.getHolder().getSurface().lockCanvas(); // 实际需 SurfaceHolder 回调 // ... 绘制 bitmap } }提示getExternalFilesDir(null)返回应用私有目录无需权限适合存放测试视频decodeFrame()调用需在子线程避免阻塞 UIrunOnUiThread()仅用于更新 UI像素处理逻辑应在 native 层完成。5. 性能调优与常见崩溃排查从帧率瓶颈到 JNI 引用泄漏5.1 提升解码帧率关键参数调优表参数默认值推荐值效果风险codec_ctx-thread_count0自动2利用双核并行解码可能增加延迟sws_ctx缩放算法SWS_BILINEARSWS_FAST_BILINEAR提速 15%图像质量轻微下降avcodec_open2()flags0AV_CODEC_FLAG_LOW_DELAY减少缓冲帧可能丢帧AVPacket复用每帧新建全局复用减少 malloc/free需手动av_packet_unref()修改init()中的avcodec_open2调用// 替换原调用 AVDictionary* opts nullptr; av_dict_set(opts, threads, 2, 0); av_dict_set(opts, low_delay, 1, 0); if (avcodec_open2(codec_ctx, codec, opts) 0) { /* ... */ } av_dict_free(opts);5.2 JNI 引用泄漏诊断用adb shell dumpsys meminfo定位若 App 运行数分钟后 OOM可能是GetByteArrayElements()未配对Release。验证方法# 启动 App 后执行 adb shell dumpsys meminfo com.example.videodecoder | grep Dalvik Heap # 观察 Objects 行中 byte[] 数量是否持续增长 # 正常应稳定在 1~2 个复用缓冲区若达数百则存在泄漏修复方案确保每个GetXXXElements()后必有ReleaseXXXElements()且Release在return之前。5.3 崩溃日志分析从logcat提取关键线索当出现A/libc: Fatal signal 11 (SIGSEGV)时logcat中搜索signal 11后的寄存器状态adb logcat | grep -A 10 signal 11 # 输出示例 # *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** # Build fingerprint: google/sdk_gphone64_arm64/gphone64_arm64:14/UP1A.231005.007/9379840:userdebug/dev-keys # Revision: 0 # ABI: arm64 # Timestamp: 2023-11-20 10:23:45.1234567890800 # pid: 12345, tid: 12346, name: Thread-2 com.example.videodecoder # uid: 10123 # signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 # x0 0000000000000000 x1 0000007b8a123456 x2 0000007b8a123456 x3 0000007b8a123456 # x4 0000007b8a123456 x5 0000007b8a123456 x6 0000007b8a123456 x7 0000007b8a123456 # x8 0000007b8a123456 x9 0000007b8a123456 x10 0000007b8a123456 x11 0000007b8a123456 # x12 0000007b8a123456 x13 0000007b8a123456 x14 0000007b8a123456 x15 0000007b8a123456 # x16 0000007b8a123456 x17 0000007b8a123456 x18 0000007b8a123456 x19 0000007b8a123456 # x20 0000007b8a123456 x21 0000007b8a123456 x22 0000007b8a123456 x23 0000007b8a123456 # x24 0000007b8a123456 x25 0000007b8a123456 x26 0000007b8a123456 x27 0000007b8a123456 # x28 0000007b8a123456 x29 0000007b8a123456 # sp 0000007b8a123456 lr 0000007b8a123456 pc 0000007b8a123456关键线索fault addr 0x0表示空指针解引用检查frame或sws_ctx是否为nullptrpc寄存器值程序计数器可反向定位到video_decoder.cpp的哪一行用addr2line工具解析aarch64-linux-android-addr2line -C -f -e app/build/intermediates/merged_native_libs/debug/out/lib/arm64-v8a/libvideo_decoder.so 0000007b8a1234565.4 一个具体技巧用AVFrame的buf字段管理内存生命周期FFmpeg 的AVFrame可能持有AVBufferRef若av_frame_unref(frame)被提前调用后续sws_scale()会访问已释放内存。安全做法是让AVFrame生命周期覆盖整个解码循环// 在 init() 中分配一次 frame av_frame_alloc(); // 在 decodeFrame() 中复用不调用 av_frame_unref() // av_frame_unref(frame) 仅在 deinit() 中调用同时在decodeFrame()开头添加保护if (!frame-buf[0]) { LOGE(AVFrame buffer is null - frame not properly allocated); return -1; }此检查能提前捕获av_frame_unref()误调用避免 SIGSEGV。本文还有配套的精品资源点击获取
返回列表