Android视频解码实战:NDK+JNI集成FFmpeg与OpenCV
2026/9/11 13:27:26 网站建设 项目流程

简介:本资源是一套面向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人学习下载,资源提供从环境搭建、交叉编译配置到OpenCV+FFmpeg联合调用的全链路工程实践,包含AIDL接口定义(OpenCVEngineInterface.aidl)、Gradle NDK构建模板及可直接运行的Native层解码示例,是掌握移动端CV底层集成不可多得的参考范例。

1. 在 Android 设备上跑通视频解码链路:为什么必须用 NDK+JNI 拉通 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 提供图像算法原子能力,而 NDK+JNI 是唯一能同时调度二者、且满足实时性(30fps+)与内存可控性的桥梁。本文面向已配置好 Android Studio 的开发者,不讲“NDK 是什么”,只聚焦如何把libavcodec.solibopencv_core.so和你的 JNI C++ 代码编译进一个.so,并在 Activity 中安全调用——所有命令、路径、参数均经 Android Studio Giraffe(2023.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编译的 FFmpeg+OpenCV 组合,在 Android 8.0~14 的 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 API=21 ARCH=arm64 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-os=android \ --arch=aarch64 \ --sysroot=$SYSROOT \ --cc=$CC \ --cxx=$CXX \ --enable-shared=no \ --enable-static=yes \ --disable-doc \ --disable-programs \ --disable-ffmpeg \ --disable-ffplay \ --disable-ffprobe \ --disable-symver \ --disable-debug \ --disable-everything \ --enable-decoder=h264,hevc,vp8,vp9 \ --enable-decoder=ac3,eac3,aac,mp3 \ --enable-parser=h264,hevc,vp8,vp9,aac \ --enable-demuxer=mov,mp4,avi,flv,rtsp,hls,mpegts \ --enable-muxer=mp4 \ --enable-filter=scale,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-shared=no --enable-static=yes强制生成.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 的 OpenCV:

cd opencv-4.8.1 mkdir build_android && cd build_android cmake -DCMAKE_TOOLCHAIN_FILE=$NDK_PATH/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a \ -DANDROID_PLATFORM=android-21 \ -DANDROID_NDK=$NDK_PATH \ -DBUILD_SHARED_LIBS=OFF \ -DBUILD_opencv_apps=OFF \ -DBUILD_opencv_java=OFF \ -DBUILD_opencv_python=OFF \ -DBUILD_TESTS=OFF \ -DBUILD_PERF_TESTS=OFF \ -DWITH_QT=OFF \ -DWITH_VTK=OFF \ -DWITH_CUDA=OFF \ -DOPENCV_DNN=OFF \ -DCMAKE_BUILD_TYPE=Release \ -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-decoder=h264)或编译选项错误(如漏-fPIC)。此时需清理make clean后重试。

工具作用常见失败原因
nm -C xxx.a查看静态库导出符号编译时未启用对应模块,或--disable-everything过度裁剪
file libxxx.a检查是否为 ARM64 目标文件CROSS_PREFIX路径错误导致 x86_64 交叉编译
readelf -d libxxx.so检查动态库依赖误启BUILD_SHARED_LIBS=ON导致生成.so

3. 在 C++ 层打通解码-处理-回传:JNI 接口设计与零拷贝实现

3.1 JNI 函数签名设计:从 Java 到 native 的数据契约

Java 层声明必须严格匹配 C++ 实现,否则UnsatisfiedLinkError。定义VideoDecoder.java

public 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 解码输出AVFramedata[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" }

注意ndkVersionndk { version }必须一致;abiFilters 'arm64-v8a'确保只生成一个.so,减小 APK 体积;若需支持 32 位设备,添加'armeabi-v7a'并重新编译 FFmpeg/OpenCV。

4.2AndroidManifest.xml权限与存储适配

Android 10+ 强制分区存储,访问/sdcard/需声明READ_MEDIA_VIDEO权限:

<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <!-- Android 13+ 需额外声明 --> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <!-- 若需写入日志或缓存,添加 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />

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()调用需在子线程,避免阻塞 UI;runOnUiThread()仅用于更新 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(),且Releasereturn之前。

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.123456789+0800 # 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表示空指针解引用,检查framesws_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 0000007b8a123456

5.4 一个具体技巧:用AVFramebuf字段管理内存生命周期

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。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询