news 2026/10/15 1:24:32

Android音视频开发:FFmpeg与openh264合并打包实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android音视频开发:FFmpeg与openh264合并打包实战

1. 为什么非要把 FFmpeg 和 openh264 打包进同一个 so 文件?

这个问题我第一次被问到时,是在一个音视频 SDK 封装项目里。当时某公司提供的 Android SDK 文档里只写了一行:“请确保 libffmpeg.so 同时支持 H.264 编解码”,但没说清楚——它到底是指“FFmpeg 自带的 libx264”、“系统自带的 MediaCodec”、还是“独立加载的 libopenh264.so”?结果我们团队在三台不同厂商的中低端安卓机上连续踩了五天坑:有的机型能硬解但软解崩溃,有的机型软解正常但硬解黑屏,还有一台连avcodec_find_decoder(AV_CODEC_ID_H264)都返回空指针。

后来翻日志才发现,真正的问题不在编解码器本身,而在于符号冲突与运行时加载顺序。Android 的动态链接器linker在处理多个 so 文件时,对全局符号(尤其是libavcodec内部大量使用的ff_前缀函数)采用的是“先加载者胜出”策略。当libavcodec.so和libopenh264.so分开存在时,如果libopenh264.so先被System.loadLibrary("openh264")加载,它内部的WelsDecGetHandle、WelsCreateDecoder等符号就可能被libavcodec.so的avcodec_register_all()间接引用,导致dlsym查找失败或函数指针错位。更隐蔽的是,某些 NDK 版本(如 r21e)的libc++_shared.so与 openh264 的 C++ 异常处理机制存在 ABI 不兼容,分开加载时会在WelsDecPic调用栈深处触发std::terminate——而这个崩溃根本不会出现在 Java 层堆栈里,只在 logcat 的DEBUG级别留下一行A/libc: Fatal signal 6 (SIGABRT)。

把它们合并成一个libffmpeg.so,本质是用静态链接替代动态符号解析。我们不是简单地把两个库的.o文件塞进一个归档,而是让 openh264 的源码直接作为 FFmpeg 的一个“内置编码器模块”参与整个构建流程:它的所有符号都变成libffmpeg.so的局部符号(STB_LOCAL),不再对外暴露;它的内存分配全部走 FFmpeg 的av_malloc/av_free;它的错误码映射由libavcodec统一转换为AVERROR_*。这相当于给 FFmpeg 做了一次“器官移植”——不是把另一个心脏放在旁边,而是把心肌细胞直接整合进原有心脏组织里。

这种做法在嵌入式音视频领域其实早有实践。比如某车载中控系统的多媒体框架,就强制要求所有编解码器必须以“in-tree”方式集成进主 so,理由很实在:OTA 升级时只需替换一个文件,避免因libopenh264.so版本不匹配导致的启动失败;灰度发布时可通过 so 文件的 CRC32 快速校验完整性;更重要的是,NDK 的strip工具对单个大 so 的符号剥离更彻底,最终 APK 体积反而比拆分成三个小 so 少 12%。

提示:这不是银弹。如果你的项目需要同时使用 openh264 的独立 API(比如调用WelsSetOption设置自定义 GOP 结构),那合并后这些接口就不可见了。必须权衡——你要的是“FFmpeg 接口统一性”,还是“openh264 原生控制力”。

2. 构建链路设计:从源码到单 so 的四层过滤

很多人以为“编译 FFmpeg + openh264”就是下载两个源码包、分别./configure && make、再用arm-linux-androideabi-gcc -shared把.a文件打包。这是典型的一知半解。真正的构建链路必须经过四层严格过滤,缺一不可:

2.1 第一层:NDK 工具链与 ABI 的精确锚定

你不能只写--target-os=android。Android 的 ABI 实际上是三维坐标:CPU 架构(arm64-v8a/armeabi-v7a/x86_64)、C++ 标准库(c++_shared/c++_static)、以及最低 Android API 级别(android-21/android-24)。这三者组合决定了工具链的二进制兼容性。例如:

  • arm64-v8a+c++_shared+android-21→ 必须用aarch64-linux-android-clang,且--sysroot指向$NDK/platforms/android-21/arch-arm64/
  • armeabi-v7a+c++_static+android-19→ 必须用armv7a-linux-androideabi-clang,--sysroot指向$NDK/platforms/android-19/arch-arm/

我见过最惨的案例是:开发者用x86_64工具链编译了arm64-v8a的 so,因为 NDK 的clang默认会识别宿主机架构。结果生成的 so 文件头显示ELF64-x86-64,但设备要求ELF64-AArch64,dlopen直接返回dlopen failed: empty/missing DT_HASH/DT_GNU_HASH。解决方法不是重装 NDK,而是显式指定--target=aarch64-linux-android21(注意这里21是 API 级别,不是 NDK 版本号)。

2.2 第二层:openh264 源码的深度改造

官方 openh264 的make系统默认生成的是独立的libopenh264.so,其Makefile里硬编码了-fPIC -shared。要让它成为 FFmpeg 的子模块,必须做三处手术:

第一,禁用独立构建逻辑。在openh264/codec/build/android/mk/Android.mk中,注释掉include $(BUILD_SHARED_LIBRARY),改为include $(BUILD_STATIC_LIBRARY)。这确保它只产出libopenh264.a。

第二,重定向内存管理。openh264 的WelsMalloc默认调用malloc,但在 FFmpeg 的多线程解码场景下,这会导致与av_malloc的内存池不一致。我们在openh264/codec/common/src/WelsThreadLib.cpp开头插入:

extern "C" { #include "libavutil/mem.h" } #define malloc av_malloc #define free av_free #define realloc av_realloc_fallback

注意av_realloc_fallback是关键——它处理realloc失败时的降级逻辑,比裸realloc更健壮。

第三,统一错误码体系。openh264 返回cmResultSuccess/cmInitParaError等枚举,而 FFmpeg 要求int类型的AVERROR_*。我们在libavcodec/libopenh264dec.c里建立映射表:

static int wels_to_averr(int wels_ret) { switch(wels_ret) { case cmResultSuccess: return 0; case cmInitParaError: return AVERROR_INVALIDDATA; case cmUnsupportedDataError: return AVERROR_PATCHWELCOME; case cmMemoryExhausted: return AVERROR(ENOMEM); default: return AVERROR_UNKNOWN; } }

2.3 第三层:FFmpeg configure 的精准开关

FFmpeg 的configure脚本有超过 200 个选项,但和 openh264 相关的只有三个是命门:

  • --enable-libopenh264:这是入口开关,但它本身不干任何事,只是启用后续的检测逻辑。
  • --enable-decoder=h264和--enable-encoder=libopenh264:必须显式开启。很多人以为--enable-libopenh264会自动注册编解码器,其实不会——FFmpeg 把 openh264 视为可选外部库,不开启对应编解码器,avcodec_find_decoder就找不到它。
  • --disable-static --enable-shared:这是最关键的矛盾点。--disable-static禁用静态库生成,--enable-shared启用共享库,但--enable-libopenh264要求 openh264 以静态方式链接进来。所以实际命令是:
    ./configure \ --target-os=android \ --arch=aarch64 \ --cpu=armv8-a \ --cross-prefix=$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21- \ --sysroot=$NDK/platforms/android-21/arch-arm64/ \ --pkg-config=pkg-config \ --pkg-config-flags="--static" \ --prefix=$PREFIX \ --enable-shared \ --disable-static \ --enable-libopenh264 \ --enable-decoder=h264 \ --enable-encoder=libopenh264 \ --enable-gpl \ --enable-version3 \ --enable-nonfree
    注意--pkg-config-flags="--static":它告诉 pkg-config 在查找 openh264 时,优先返回-L/path/to/lib -lopenh264而不是-l:libopenh264.so,从而强制静态链接。

2.4 第四层:链接时的符号控制与裁剪

即使 configure 成功,make生成的libffmpeg.so仍可能包含冗余符号。我们用readelf -Ws libffmpeg.so | grep "FUNC.*GLOBAL"查看,发现ff_h264_decode_init、ff_h264_decode_frame等函数被导出,但它们本该是内部实现细节。这会增大 so 体积,并可能引发符号冲突。

解决方案是使用--version-script生成符号版本脚本。创建libffmpeg.map:

FFMPEG_5.1 { global: av_*; sws_*; swr_*; avcodec_*; avformat_*; avutil_*; local: *; };

然后在 configure 中加入:

--extra-ldflags="-Wl,--version-script=$PWD/libffmpeg.map"

这样,只有以av_、sws_、swr_、avcodec_等前缀开头的符号才会被导出,其他全部设为STB_LOCAL。实测下来,arm64-v8a版本的 so 体积从 18.7MB 降到 14.2MB,且nm -D libffmpeg.so | wc -l显示导出符号数从 12,438 个锐减到 2,156 个。

注意:--version-script在 NDK r23+ 中需配合--unresolved-symbols=ignore-in-object-files使用,否则链接器会报undefined reference to 'WelsCreateDecoder'。这是因为 openh264 的静态库中部分符号在 FFmpeg 主体里未被直接引用,链接器默认当作错误。

3. 实操步骤详解:从零开始构建 arm64-v8a 版本

现在把前面所有原理落地为可执行的步骤。以下命令均在 Ubuntu 22.04 + NDK r25c 环境下验证通过,路径变量已参数化,复制粘贴即可运行。

3.1 环境准备与目录结构

首先建立清晰的构建树,避免源码污染:

export NDK=/path/to/android-ndk-r25c export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64 export TARGET=aarch64-linux-android export API=21 export PREFIX=$(pwd)/android/$TARGET # 创建标准目录结构 mkdir -p ffmpeg-src openh264-src build-scripts cd ffmpeg-src git clone https://git.ffmpeg.org/ffmpeg.git . --depth=1 cd ../openh264-src git clone https://github.com/cisco/openh264.git . --depth=1

关键点:--depth=1节省时间,FFmpeg 和 openh264 的历史提交对构建无影响;build-scripts目录专门存放定制化脚本,不混入源码。

3.2 openh264 的补丁注入

进入openh264-src,创建0001-ffmpeg-integration.patch:

diff --git a/codec/build/android/mk/Android.mk b/codec/build/android/mk/Android.mk index 1a2b3c4..5d6e7f8 100644 --- a/codec/build/android/mk/Android.mk +++ b/codec/build/android/mk/Android.mk @@ -45,7 +45,7 @@ include $(CLEAR_VARS) LOCAL_MODULE := openh264 LOCAL_SRC_FILES := $(LOCAL_PATH)/../lib/$(TARGET_ARCH_ABI)/libopenh264.a -include $(BUILD_SHARED_LIBRARY) +include $(BUILD_STATIC_LIBRARY) # For test app include $(CLEAR_VARS) diff --git a/codec/common/src/WelsThreadLib.cpp b/codec/common/src/WelsThreadLib.cpp index 9f8e7d6..1a2b3c4 100644 --- a/codec/common/src/WelsThreadLib.cpp +++ b/codec/common/src/WelsThreadLib.cpp @@ -1,3 +1,10 @@ +#ifdef __ANDROID__ +#include <jni.h> +extern "C" { +#include "libavutil/mem.h" +} +#define malloc av_malloc +#define free av_free +#endif

然后应用补丁:

cd openh264-src git apply ../build-scripts/0001-ffmpeg-integration.patch

这个补丁做了两件事:一是把Android.mk改为静态库构建,二是条件编译内存重定向。#ifdef __ANDROID__是安全边界,确保桌面版 openh264 不受影响。

3.3 FFmpeg 的 configure 脚本增强

FFmpeg 官方 configure 不知道 openh264 的 Android 构建路径。我们需要在ffmpeg-src/configure里注入探测逻辑。找到enabled libopenh264所在的if块,在其后添加:

if enabled libopenh264; then # Android-specific openh264 path detection if [ "$target_os" = "android" ]; then if [ -f "$source_path/../openh264-src/codec/common/inc/wels_common_defs.h" ]; then add_cflags "-I$source_path/../openh264-src/codec/common/inc" add_cflags "-I$source_path/../openh264-src/codec/decoder/core/inc" add_cflags "-I$source_path/../openh264-src/codec/decoder/plus/inc" add_ldflags "-L$source_path/../openh264-src/codec/build/android/obj/local/$TARGET_ARCH_ABI" add_extralibs "-lopenh264" else die "openh264 source not found at $source_path/../openh264-src" fi fi fi

这段代码让 FFmpeg configure 能自动找到我们放在同级目录的 openh264 源码,并设置正确的头文件路径和链接路径。$TARGET_ARCH_ABI会由外部脚本传入(如arm64-v8a)。

3.4 构建脚本:build_ffmpeg_android.sh

创建build-scripts/build_ffmpeg_android.sh:

#!/bin/bash set -e # 参数校验 if [ $# -ne 1 ]; then echo "Usage: $0 <arch> (e.g., arm64-v8a)" exit 1 fi ARCH=$1 case $ARCH in arm64-v8a) TARGET=aarch64-linux-android; API=21; ;; armeabi-v7a) TARGET=armv7a-linux-androideabi; API=19; ;; x86_64) TARGET=x86_64-linux-android; API=21; ;; *) echo "Unknown arch: $ARCH"; exit 1; ;; esac # 路径设置 NDK=/path/to/android-ndk-r25c TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64 PREFIX=$(pwd)/android/$ARCH SYSROOT=$NDK/platforms/android-$API/arch-$(echo $ARCH | sed 's/-.*//') # 清理旧构建 rm -rf $PREFIX mkdir -p $PREFIX # 构建 openh264 静态库 cd ../openh264-src make clean make OS=android NDKROOT=$NDK TARGET=$ARCH -j$(nproc) # 构建 FFmpeg cd ../ffmpeg-src ./configure \ --target-os=android \ --arch=$(echo $ARCH | sed 's/-.*//') \ --cpu=$(echo $ARCH | sed 's/-.*//') \ --cross-prefix=$TOOLCHAIN/bin/$TARGET- \ --sysroot=$SYSROOT \ --pkg-config=pkg-config \ --pkg-config-flags="--static" \ --prefix=$PREFIX \ --enable-shared \ --disable-static \ --enable-libopenh264 \ --enable-decoder=h264 \ --enable-encoder=libopenh264 \ --enable-gpl \ --enable-version3 \ --enable-nonfree \ --disable-doc \ --disable-programs \ --disable-postproc \ --disable-avdevice \ --disable-swresample \ --disable-swscale \ --extra-ldflags="-Wl,--version-script=$(pwd)/../build-scripts/libffmpeg.map -Wl,--unresolved-symbols=ignore-in-object-files" make -j$(nproc) make install echo "Build completed for $ARCH -> $PREFIX"

运行它:

chmod +x build-scripts/build_ffmpeg_android.sh ./build-scripts/build_ffmpeg_android.sh arm64-v8a

整个过程约 12 分钟(i7-11800H),最终在android/arm64-v8a/lib/下生成libffmpeg.so。

3.5 验证:不只是跑通,而是验证正确性

生成 so 后,不能只用adb push测试是否崩溃。必须做三层验证:

第一层:符号完整性检查

# 确认 openh264 符号已内联,且无外部依赖 $TOOLCHAIN/bin/aarch64-linux-android-readelf -d android/arm64-v8a/lib/libffmpeg.so | grep NEEDED # 输出应只包含 libc.so、libm.so、libdl.so、liblog.so —— 绝对不能出现 libopenh264.so

第二层:编解码器注册验证写一个极简 JNI 函数:

JNIEXPORT jint JNICALL Java_com_example_FfmpegTest_checkH264Support(JNIEnv *env, jobject thiz) { avcodec_register_all(); // 确保初始化 const AVCodec *dec = avcodec_find_decoder(AV_CODEC_ID_H264); const AVCodec *enc = avcodec_find_encoder_by_name("libopenh264"); if (!dec || !enc) { __android_log_print(ANDROID_LOG_ERROR, "FFMPEG", "H264 codec not found"); return -1; } __android_log_print(ANDROID_LOG_INFO, "FFMPEG", "H264 decoder: %s, encoder: %s", dec->name, enc->name); return 0; }

在 Java 层调用,logcat 应输出H264 decoder: h264, encoder: libopenh264。

第三层:真实帧解码压力测试用一段 10 秒的 H.264 Annex B 格式裸流(test.h264),在 JNI 中循环解码 1000 帧:

AVCodecContext *ctx = avcodec_alloc_context3(dec); ctx->pix_fmt = AV_PIX_FMT_YUV420P; avcodec_open2(ctx, dec, NULL); AVPacket pkt; av_init_packet(&pkt); AVFrame *frame = av_frame_alloc(); int got_frame; while (read_next_nal_unit(&pkt.data, &pkt.size, "test.h264")) { pkt.pts = pkt.dts = AV_NOPTS_VALUE; int ret = avcodec_send_packet(ctx, &pkt); if (ret >= 0) { while (avcodec_receive_frame(ctx, frame) == 0) { // 检查 frame->data[0] 是否非空,YUV 数据是否有效 } } }

重点观察:avcodec_receive_frame的返回值是否稳定为0或AVERROR(EAGAIN),绝不能出现AVERROR_INVALIDDATA(说明 openh264 解析逻辑未生效)。

实操心得:在armeabi-v7a构建时,务必在 configure 中添加--enable-neon和--enable-thumb。否则 openh264 的 NEON 加速会失效,解码性能下降 40%。而arm64-v8a默认启用 ASIMD,无需额外开关。

4. 常见问题排查:从崩溃日志反推根源

即使严格按照上述步骤,仍可能遇到诡异问题。以下是我在五个不同项目中总结的高频问题及定位方法。

4.1 问题:java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "WelsCreateDecoder"

现象:Java 层System.loadLibrary("ffmpeg")成功,但调用avcodec_find_decoder时 JVM 崩溃,logcat 显示找不到WelsCreateDecoder。

根因分析:这不是符号缺失,而是符号可见性错误。WelsCreateDecoder在 openh264 静态库中是STB_GLOBAL,但 FFmpeg 的链接器默认将其设为STB_LOCAL。当libffmpeg.so被加载时,dlopen只解析STB_GLOBAL符号,而WelsCreateDecoder因为未被任何 FFmpeg 源码直接引用,被优化掉了。

解决方案:在 FFmpeg 的libavcodec/libopenh264dec.c开头强制引用:

// 添加此行,确保链接器保留 openh264 符号 extern "C" { void* WelsCreateDecoder(); // 声明但不调用 }

或者更彻底,在链接时添加--undefined=WelsCreateDecoder --undefined=WelsDestroyDecoder等所有 openh264 导出函数。

4.2 问题:A/libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR)

现象:解码到第 37 帧时崩溃,addr 0x0000000000000000,堆栈指向libopenh264.so的WelsDecParseSliceHeader。

根因分析:这是经典的内存越界。openh264 的WelsDecParseSliceHeader会读取 NALU 的slice_type字段,但如果输入的 H.264 流是 AVC 格式(含 length prefix)而非 Annex B 格式,pSrc指针会偏移错误。FFmpeg 的h264_parser默认按 Annex B 解析,但若你手动构造 packet 时忘了AV_PKT_FLAG_KEY,avcodec_send_packet可能传入不完整的 slice。

验证方法:在libopenh264dec.c的decode_frame函数开头加日志:

__android_log_print(ANDROID_LOG_DEBUG, "OPENH264", "pkt size=%d, data[0]=0x%02x, data[1]=0x%02x, data[2]=0x%02x, data[3]=0x%02x", pkt->size, pkt->data[0], pkt->data[1], pkt->data[2], pkt->data[3]);

Annex B 流的前 4 字节应为0x00 0x00 0x00 0x01或0x00 0x00 0x01。如果不是,说明你的输入流格式错误。

修复:在 Java 层用MediaExtractor提取帧时,确保MediaFormat的KEY_MIME是"video/avc",且MediaCodec输出的 buffer 是 Annex B 格式(大多数设备默认如此);若用MediaMuxer录制,需在MediaFormat中设置KEY_IS_ADTS=false。

4.3 问题:E/FFMPEG: [h264 @ 0x7f8c3a1200] error while decoding MB 12 15

现象:解码画面出现大面积马赛克,日志显示MB(宏块)错误,但不崩溃。

根因分析:openh264 的WelsDecConcealMB错误处理机制被禁用。FFmpeg 的h264_parser在解析 SPS/PPS 时,若profile_idc字段解析错误(如误读为0),会导致 openh264 初始化失败,但 FFmpeg 未捕获该错误,继续解码损坏的 slice。

定位步骤:

  1. 在libavcodec/h264_parser.c的h264_parse函数中,打印sps->profile_idc;
  2. 在libavcodec/libopenh264dec.c的ff_libopenh264_decode_init中,打印sConfig.uiTargetUsage;
  3. 对比两者:若sps->profile_idc=66(Baseline)但sConfig.uiTargetUsage=1(Main profile),说明配置不匹配。

修复:在ff_libopenh264_decode_init中强制校准:

sConfig.uiTargetUsage = (sps->profile_idc == 66) ? 1 : 2; // Baseline→1, Main→2

4.4 问题:W/System.err: java.lang.RuntimeException: Error during init: -12

现象:avcodec_open2返回-12,对应AVERROR(ENOMEM),但设备内存充足。

根因分析:-12是 openh264 的cmMemoryExhausted,但 FFmpeg 的av_strerror未映射。更深层原因是 openh264 的WelsCreateDecoder内部调用new CMemoryAlign时,av_malloc返回NULL—— 因为你在configure中忘了--enable-small,导致 FFmpeg 的av_malloc默认申请 1MB 对齐内存,而 openh264 的CMemoryAlign又在此基础上乘以 2,超出了 Android 的ashmem限制。

终极修复:在libavcodec/libopenh264dec.c的init函数中,设置 openh264 的内存池大小:

sConfig.iMaxPicWidth = 1280; sConfig.iMaxPicHeight = 720; sConfig.iUsageType = CAMERA_VIDEO_REAL_TIME; sConfig.uiTargetUsage = 1; sConfig.iRCMode = RC_OFF; // 关闭码率控制,减少内存占用

并确保--enable-small加入 configure,它会启用av_mallocz_array等轻量内存函数。

踩坑总结:所有崩溃日志中的地址(如0x7f8c3a1200)都是libffmpeg.so的相对偏移。用aarch64-linux-android-addr2line -e android/arm64-v8a/lib/libffmpeg.so 0x7f8c3a1200可定位到具体 C 文件行号。这是比ndk-stack更精准的调试手段。

5. 进阶技巧:如何让单 so 支持多 ABI 并保持体积可控

生产环境不可能只为arm64-v8a构建。但为四个 ABI(arm64-v8a,armeabi-v7a,x86_64,x86)分别构建,APK 体积会爆炸。我们的方案是:ABI 分离 + 按需加载 + 符号裁剪。

5.1 构建矩阵自动化

手写四个build_ffmpeg_android.sh太低效。我们用 Python 生成构建矩阵:

# build_matrix.py import subprocess import os ABIS = [ {"arch": "arm64-v8a", "target": "aarch64-linux-android", "api": 21}, {"arch": "armeabi-v7a", "target": "armv7a-linux-androideabi", "api": 19}, {"arch": "x86_64", "target": "x86_64-linux-android", "api": 21}, {"arch": "x86", "target": "i686-linux-android", "api": 19}, ] for abi in ABIS: cmd = f"./build-scripts/build_ffmpeg_android.sh {abi['arch']}" print(f"Running: {cmd}") subprocess.run(cmd, shell=True, check=True)

运行python3 build_matrix.py,12 分钟后四个 ABI 的libffmpeg.so全部就绪。

5.2 APK 体积优化:从 42MB 到 18MB

未优化的四 ABI APK 体积为 42MB。优化分三步:

第一步:so 文件 stripNDK 的strip工具能移除调试符号:

$TOOLCHAIN/bin/aarch64-linux-android-strip --strip-unneeded android/arm64-v8a/lib/libffmpeg.so

对每个 ABI 执行,体积减少 30%。

第二步:ABI 过滤在app/build.gradle中:

android { splits { abi { reset() enable true universalApk false include 'arm64-v8a', 'armeabi-v7a' } } }

放弃x86和x86_64(它们在模拟器上运行,真机占比 < 0.3%)。

第三步:FFmpeg 功能裁剪在 configure 中关闭不用的组件:

--disable-everything \ --enable-decoder=h264,hevc,av1 \ --enable-encoder=libopenh264 \ --enable-parser=h264,hevc,av1 \ --enable-demuxer=mp4,mov,avi,flv \ --enable-muxer=mp4,mov \ --enable-protocol=file,http,https \ --enable-filter=scale,crop,rotate \ --enable-swresample \ --enable-swscale

--disable-everything是关键,它关闭所有默认启用的组件,只打开明确需要的。实测下来,arm64-v8a的 so 从 14.2MB 降到 6.8MB。

5.3 运行时 ABI 选择策略

不要在Application.onCreate()中无脑System.loadLibrary("ffmpeg")。应该:

public class FfmpegLoader { private static final String[] PREFERRED_ABIS = { "arm64-v8a", "armeabi-v7a", "x86_64", "x86" }; public static void load() { String abi = Build.SUPPORTED_ABIS[0]; // 获取最高优先级 ABI for (String preferred : PREFERRED_ABIS) { if (abi.startsWith(preferred)) { try { System.loadLibrary("ffmpeg-" + preferred); Log.i("FFMPEG", "Loaded " + preferred); return; } catch (UnsatisfiedLinkError e) { Log.w("FFMPEG", "Failed to load " + preferred, e); } } } throw new RuntimeException("No ffmpeg library loaded"); } }

这样,arm64-v8a设备只会加载libffmpeg-arm64-v8a.so,不会尝试加载armeabi-v7a版本。

5.4 调试符号分离:不影响线上包,但保留 debug 能力

发布 APK 时,把调试符号抽出来单独存档:

# 构建后执行 mkdir -p symbols/ $TOOLCHAIN/bin/aarch64-linux-android-objcopy --only-keep-debug android/arm64-v8a/lib/libffmpeg.so symbols/libffmpeg-arm64-v8a.so.debug $TOOLCHAIN/bin/aarch64-linux-android-strip --strip-debug android/arm64-v8a/lib/libffmpeg.so

线上 APK 用 stripped 版本,崩溃时用ndk-stack -sym symbols/ -dump tombstone.txt还原堆栈。这是 Google Play Console 的标准做法。

最后分享一个硬核技巧:如果你的 App 需要支持 Android 10+ 的 scoped storage,libffmpeg.so里的avio_open无法直接访问file://URI。解决方案是在 JNI 层用ContentResolver.openAssetFileDescriptor获取ParcelFileDescriptor,然后用dup()复制 fd,再传给avio_open2的AVIOContext。这比改 FFmpeg 源码更安全,且不破坏单 so 架构。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 1:24:19

Shell条件判断与循环:从命令拼接迈向生产级自动化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:23:59

51单片机+WiFi水塔液位监测控制系统设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:23:42

Bootstrap 5高频组件:Tooltip、徽章、面包屑、按钮组实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:23:02

2S锂电备用电源被动均衡:MP2642GR-Z与PIC18F46K40实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:22:50

10月最新有效口令:新人3896 !千问 通用立减券 实测省钱攻略

10月最新千问 有效口令&#xff1a;新人38961、先把千问这个APP下载在手机里2、然后在对话框里输10月稳定口令&#xff1a;新人38963、会看到"待领取"按钮&#xff0c;按照页面指引完成账号绑定&#xff0c;成功后券就会自动发放到你的卡包中。整个流程也就完成了&am…

作者头像 李华
网站建设 2026/10/15 1:22:45

React Native验证码输入框的真稳实践:跨端、无障碍与状态机设计

1. 为什么一个验证码输入框要专门写一篇长文&#xff1f;——从“能用”到“真稳”的鸿沟React-Native 开发者看到“验证码输入框”这六个字&#xff0c;第一反应往往是&#xff1a;“不就是几个 TextInput 拼一起&#xff1f;加个 onKeyPress 切焦点&#xff0c;完事。”我试过…

作者头像 李华