☰
ffmpeg_cli鸿蒙化适配:交叉编译与Flutter插件桥接全解析
2026/10/8 8:59:13 网站建设 项目流程

1. 项目背景与适配前的核心需求拆解

1.1 先搞清楚 ffmpeg_cli 到底是个什么东西

如果你在 Flutter 生态里做过音视频相关的功能,大概率听说过flutter_ffmpeg或者它的继任者ffmpeg_kit_flutter。而ffmpeg_cli本质上是一条更轻的路线:它把命令行的 ffmpeg 能力直接包装成 Flutter 插件,让你在 Dart 代码里写类似FFmpegKit.execute("-i input.mp4 -vf scale=1280:720 output.mp4")这样的调用,来完成视频转码、视频截帧、音频提取、画质压缩这些重活。相比逐个封装 C API,命令行方案的好处是不需要为每个 ffmpeg 滤镜写 Dart 封装,ffmpeg 那几百个滤镜全部保留,等于把整个多媒体工具箱都搬过来了。

但这里有个关键前提:flutter_ffmpeg / ffmpeg_kit 官方一直以 Android、iOS、macOS 为主战场,鸿蒙原生(HarmonyOS NEXT)出来后,原来基于 Android AAR 的集成方式直接失效了。鸿蒙不再兼容 Android APK,你没法把 java 层的包装硬塞进去,只能重新给鸿蒙编译出可用的 native 动态库,再走 Flutter 的通道把命令透传过去。

所以这个项目标题里说的“鸿蒙化适配”,不是简单地把 Android 的 so 拷过去,而是要完成四件事:

  1. 用 OpenHarmony 工具链重新交叉编译 ffmpeg 的完整动态库;
  2. 在鸿蒙侧实现一个能够执行命令行命令的桥接层,接管原来 Android 的 ShellExecutor / CommandExecutor 逻辑;
  3. 在 Dart 侧通过dart:ffi或 Platform Channel 把“执行 ffmpeg 命令”这条路打通;
  4. 处理好 ABI 差异、动态库打包、线程回调和文件路径这几个最容易翻车的点。

这篇文章就是把这四件事完整走一遍。适合谁看?你正在做 Flutter 应用向鸿蒙迁移,或者你手头有个音视频 App 想在鸿蒙元服务里塞一个“视频压缩/格式转换”能力,再或者你只是好奇 OpenHarmony 的交叉编译环境到底怎么搭——这篇文章都能给你省下少说两周的踩坑时间。

1.2 鸿蒙化适配真正要解决的问题

做鸿蒙适配最容易犯的错误,是一上来就搜“ffmpeg 鸿蒙编译”,拿到别人贴的 config 脚本就复制,编译出一堆 so 后发现 Flutter 侧还是调不通。说到底,鸿蒙化适配有三层问题,缺一层都会卡壳。

第一层是编译层。HarmonyOS NEXT 的 native 库虽然也是 ELF 格式的 .so,但它用的是 OpenHarmony SDK 自带的一套 sysroot,和 Android NDK 的 sysroot 并不完全一致。你如果用 Android 的 NDK clang 去编,编译可能会通过,但运行时会因为某些 libc 符号解析失败直接崩溃。更隐蔽的是,OpenHarmony 的 libc 是 musl 而不是 bionic(这是关键差异),所以 configure 的时候--target-os和--cc必须指向 OHOS 的工具链。这部分网上资料很少,踩坑只能靠实战。

第二层是桥接层。ffmpeg_cli 内部依赖的命令行执行能力,在 Android 上是通过Runtime.exec()或者 ffmpeg_kit 的 CommandExecutor 实现的。鸿蒙上没有Runtime.exec()这个说法(至少 HarmonyOS NEXT 的 API 上没有直接对应物),你得自己用ohos.process.ChildProcess,或者干脆绕开 shell,直接解析参数后传给 ffmpeg 的avformat_open_input那一套 C API。我个人建议优先走后者,理由后面会讲。

第三层是分发层。Flutter 插件在鸿蒙上不是走 pub.dev 的自动注册,也不是看GeneratedPluginRegistrant,而是要在鸿蒙工程的EntryAbility里手动把插件绑定到 FlutterEngine 上。这一层不处理好,插件代码写再多也不会被调用,而且不会报编译错,只会静默失败——这是最气人的。

1.3 选型对比:直接移植、Platform Channel 桥接、还是做成完整插件

动手之前,先想清楚你要做到哪一档。我整理一下三种路线:

路线工作量运行效率维护成本适合场景
路线 A:直接移植 Android 插件源码,仅替换 ffmpeg so小差高临时验证,不建议上线
路线 B:Platform Channel 桥接,Dart 发命令,鸿蒙侧执行中中中单功能场景,例如“视频压缩”
路线 C:完整插件化 + dart:ffi 直调 C API 封装大高中需要复用滤镜能力和多场景调用

如果你只做一个“把视频压缩到 10MB 以内再上传”的功能,路线 B 足够,代码量大概就 500 行。但如果你想保住 ffmpeg_cli 的灵活性,让产品经理随手就能提“加一个倍速播放”“加一个人声分离”,那我建议直接上路线 C,因为命令行的所有参数都在 Dart 侧拼好字符串就可以了,不需要为每一种新功能重新编译插件,这才是 ffmpeg_cli 存在的意义。

2. 环境准备与技术难点分析

2.1 工具清单:比想象中多一个 OHOS SDK

先列一下我实际用到的环境,实测下来这一套是最稳的:

  • 开发机:Ubuntu 22.04(x86_64),内存 16G 以上,磁盘至少预留 20G
  • 鸿蒙开发工具:DevEco Studio 5.0 以上(带 HarmonyOS SDK 5.0.0(12))
  • OpenHarmony SDK:从 DevEco 的Sdk/openharmony目录取,重点用到native/llvm/bin下的 clang 和native/sysroot
  • Flutter 版本:Flutter 3.22+,并且开启鸿蒙分支(或者用 OpenHarmony 官方维护的 flutter_flutter 仓库)
  • 真机:HarmonyOS NEXT 开发者预览版设备,开 USB 调试

这里我要特别强调:很多人以为装了 DevEco Studio 就够了,实际上真正编译 ffmpeg 需要的是它里面自带的 native SDK。你在 DevEco 的安装目录下找类似Sdk/openharmony/的结构,里面有native/llvm/bin/clang这样一条路。最好把它加进 PATH,之后所有编译命令都靠它。

环境变量建议这样配:

export OHOS_SDK=/path/to/DevEcoStudio/Sdk/openharmony export OHOS_NATIVE_TOOLCHAIN=$OHOS_SDK/native/llvm/bin export PATH=$OHOS_NATIVE_TOOLCHAIN:$PATH export CC=$OHOS_NATIVE_TOOLCHAIN/clang export CXX=$OHOS_NATIVE_TOOLCHAIN/clang++

注意这里别用 Android NDK 的 clang 顶替,后文会说为什么。

2.2 为什么不能用 Android NDK 的编译器

这是我在项目里踩过的最深的一个坑,值得单开一节讲清楚。

Android 的 native 库跑在bionic libc上,而 OpenHarmony 的 native 库跑在musl libc上。两者虽然都提供 pthread、malloc 这类标准接口,但具体实现的符号版本、链接行为有差异。ffmpeg 这种重度依赖系统调用的库,在 configure 阶段就会探测getaddrinfo、pthread_setname_np这类函数的存在与否。如果你用 Android NDK 的 clang 编译,链接器会把一些 bionic 才有的符号标记进来,最终产物在鸿蒙设备上加载时,很典型的报错是:

dlopen failed: cannot locate symbol "XXX" referenced by "libavcodec.so"

而且这类问题不一定在启动时爆发,有时是跑到某个解码器分支才触发,排查难度极高。所以我给你的第一条忠告:先确认 clang 版本来自 OHOS SDK,再谈编译。检查方法很简单:

clang --version

如果你在里面看到OpenHarmony或者HarmonyOS字样,就对了;如果看到的是 Android NDK 那串描述,哪怕其他环境都一样,也先停下来去换工具链。

2.3 configure 参数详解:照着这份抄作业

ffmpeg 的 configure 是整个移植里最核心的环节。参数不能照搬 Linux 桌面版的,也不能照搬 Android 版的,需要针对 OHOS 做调整。我整理了一份实测可用的配置,目标是编译出裁剪后的、只保留常用编解码器的动态库:

./configure \ --target-os=linux \ --arch=aarch64 \ --enable-cross-compile \ --cc=$OHOS_NATIVE_TOOLCHAIN/clang \ --cxx=$OHOS_NATIVE_TOOLCHAIN/clang++ \ --nm=$OHOS_NATIVE_TOOLCHAIN/llvm-nm \ --ar=$OHOS_NATIVE_TOOLCHAIN/llvm-ar \ --strip=$OHOS_NATIVE_TOOLCHAIN/llvm-strip \ --sysroot=$OHOS_SDK/native/sysroot \ --enable-shared \ --disable-static \ --disable-programs \ --disable-doc \ --disable-avdevice \ --disable-postproc \ --enable-small \ --enable-gpl \ --enable-libx264 \ --enable-libmp3lame \ --enable-libopus \ --enable-libvpx \ --enable-encoder=aac,libx264,mp3,libopus,libvpx_vp8,libvpx_vp9 \ --enable-decoder=aac,h264,hevc,mp3,vp8,vp9,opus,pcm_s16le \ --enable-parser=aac,h264,hevc,mpegaudio,opus,vp8,vp9 \ --enable-muxer=mp4,mov,matroska,mp3,ogg,opus \ --enable-demuxer=mov,matroska,mp3,ogg,opus \ --enable-protocol=file,pipe \ --enable-filter=scale,crop,trim,concat,aresample,volume,overlay

几个参数的解释和取舍逻辑:

  • --target-os=linux:在 ffmpeg 眼里,OpenHarmony 接近 Linux 内核,但又不能用--target-os=android(那个会启用 bionic 相关逻辑)。实测linux是最稳的。
  • --disable-avdevice:你如果在 Flutter App 里做视频处理,基本不会去操作摄像头采集设备,这个东西体积不小,去掉能省不少空间。
  • --disable-programs:不生成 ffmpeg 可执行文件,我们只要 so。
  • 编码器、解码器、封装器不要贪多,每多加一个滤镜或者解码器,都会让 so 体积肉眼可见地膨胀。我的建议是先用最小集通一遍流程,跑通之后再按需加回来。

configure 完之后就是 make:

make -j$(nproc)

编译过程大概 5 到 15 分钟,取决于机器性能。最后检查产物:

ls -lh libavcodec/libavcodec.so libavformat/libavformat.so \ libavutil/libavutil.so libswscale/libswscale.so \ libswresample/libswresample.so libavfilter/libavfilter.so

如果 6 个库里有一个没生成,八成是 configure 阶段某个 enable 选项对应的依赖没找到,回头看输出日志里的警告。另外,鸿蒙手机是 aarch64,别只编这一个架构就满足了,后面调试的时候如果跑在模拟器(x86_64)上会哑火,所以最佳做法是两个架构各编一遍,把产物分别放到libs/arm64-v8a和libs/x86_64目录。

3. 核心实操:交叉编译与插件桥接

3.1 第一步:把编译产物整理进鸿蒙工程

交叉编译完成只是万里长征第一步。接下来要做的,是把 ffmpeg 的 so 文件送进鸿蒙 native 工程里。鸿蒙工程里动态库的默认搜索位置是libs/arm64-v8a这种结构,你可以在 DevEco Studio 的模块目录下手动建一个:

entry/src/main/cpp/libs/arm64-v8a/ libavcodec.so libavformat.so libavutil.so libswscale.so libswresample.so libavfilter.so

然后在entry/oh-package.json5或者 CMakeLists.txt 里声明链接位置。比如 CMake 可以这样加:

set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/libs/${OHOS_ARCH}) add_library(ffmpeg_wrapper SHARED ffmpeg_wrapper.cpp) target_link_libraries(ffmpeg_wrapper PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/libs/${OHOS_ARCH}/libavcodec.so ${CMAKE_CURRENT_SOURCE_DIR}/libs/${OHOS_ARCH}/libavformat.so ${CMAKE_CURRENT_SOURCE_DIR}/libs/${OHOS_ARCH}/libavutil.so ${CMAKE_CURRENT_SOURCE_DIR}/libs/${OHOS_ARCH}/libswscale.so ${CMAKE_CURRENT_SOURCE_DIR}/libs/${OHOS_ARCH}/libswresample.so ${CMAKE_CURRENT_SOURCE_DIR}/libs/${OHOS_ARCH}/libavfilter.so )

注意:CMake 里链接的是带完整路径的 so,而不是用-l:libavcodec.so简写,避免链接器去系统路径里找同名库,把鸿蒙自带的一些多媒体库给误链了,这个问题我后面在问题清单里还会再提。

3.2 第二步:在鸿蒙侧封装 CommandExecutor

ffmpeg_cli 最初的实现依赖“执行命令行”这个能力。Android 那边用Runtime.exec(command)把参数丢给 shell,鸿蒙上没有这个现成接口,但可以通过ohos.process.ChildProcess来做。我先给你看一个最直白的鸿蒙侧封装:

import { process } from '@kit.BasicServicesKit'; export class CommandExecutor { static execute(command: string): Promise<string> { return new Promise((resolve, reject) => { const child = process.ChildProcess.exec(`${command} 2>&1`); child.on('message', (data: string) => { // 处理实时日志 }); child.on('exit', (code: number) => { if (code === 0) { resolve('ok'); } else { reject(new Error(`exit code: ${code}`)); } }); }); } }

这段代码在简单场景下可以跑,但你很快会碰到一个问题:ffmpeg 的输出日志量非常大,如果你用命令行的方式跑,大量的 stdout/stderr 回传会把 Channel 挤爆,Dart 侧 UI 卡顿是小事,回调乱序甚至崩溃都有可能。

所以我的实际建议是:命令行的方案只用来做调试验证,上线版本应该直接跳过 shell 这层,在鸿蒙侧解析参数后调用 ffmpeg 的 C API。ffmpeg 本身提供了avformat_open_input、avcodec_send_packet、avcodec_receive_frame这套解码流程,你把它包成一个FFmpegNativeBridge,对外只暴露三个方法:executeWithArgs(List<String> args)、cancel()、getLogs()。这样既不依赖 shell 的解析差异,又能精确控制进度回调。

3.3 第三步:Dart 侧 dynamic library 加载与 ffi 封装

Dart 侧需要先解决动态库加载的问题。在 Flutter 里通常用dart:ffi的DynamicLibrary.open(),但在鸿蒙上路径要写对:

import 'dart:ffi'; import 'dart:io'; final DynamicLibrary ffmpegBridge = Platform.isAndroid ? DynamicLibrary.open('libffmpeg_wrapper.so') : DynamicLibrary.open('libffmpeg_wrapper.so'); // 鸿蒙上同样使用 .so 后缀

注意一个细节:鸿蒙和 Android 的动态库加载逻辑不同。Android 上 Flutter 会从 APK 的 lib/ 目录自动找,而鸿蒙上 Flutter 的 dynamic library 默认搜索路径不一定覆盖到你放在entry/src/main/cpp/libs下的库。如果你发现DynamicLibrary.open报library not found,先检查一下 so 是否被正确打进了 HAP 包,解包看一眼libs/arm64-v8a下有没有对应文件。开发阶段可以临时把 so 放到entry/src/main/resources/rawfile,通过路径去打开,但上线前一定要归位到 libs 目录。

接下来定义一个简单的 FFI 接口:

typedef ExecuteNative = Int32 Function(Pointer<Utf8> args); typedef ExecuteDart = int Function(Pointer<Utf8> args); final ExecuteDart _execute = ffmpegBridge .lookup<NativeFunction<ExecuteNative>>('ffmpeg_wrapper_execute') .asFunction();

这里如果你熟悉package:ffi的套路,就知道字符串要转成Pointer<Utf8>。但有个坑:ffmpeg 参数里有空格、引号、中文路径,直接拼成字符串传过去,鸿蒙侧的ChildProcess.exec很可能因为转义不对而失败。更稳的做法是封装成“参数数组”:

final result = _executeArguments( ['-i', inputPath, '-vf', 'scale=1280:720', outputPath], );

底层 C 函数直接接收char** argv和int argc,从根上避开命令行转义问题。这个思路也正好规避了之前提到的 shell 解析差异。

3.4 第四步:插件注册与 MethodChannel 对接

现在到了 Flutter 插件机制的关键部分。鸿蒙上的插件注册方式跟 Android 差异很大,你没法依赖flutter pub get之后生成的GeneratedPluginRegistrant。正确做法是在鸿蒙工程里找到主 Ability 的.ets文件,手动挂插件:

// EntryAbility.ets import { FlutterAbility } from '@ohos/flutter_ability'; import { FlutterEngine } from '@ohos/flutter_engine'; import { FfmpegCliPlugin } from './FfmpegCliPlugin'; export default class EntryAbility extends FlutterAbility { onWindowStageCreate(windowStage: window.WindowStage): void { super.onWindowStageCreate(windowStage); const engine: FlutterEngine = this.flutterEngine; engine.getPluginRegistry().register(new FfmpegCliPlugin()); } }

FfmpegCliPlugin自己实现 MethodChannel 的 handler:

import { MethodChannel } from '@ohos/flutter_plugin'; export class FfmpegCliPlugin { private channel: MethodChannel; constructor() { this.channel = new MethodChannel('com.example.ffmpeg_cli/method'); this.channel.setMethodCallHandler((call, result) => { switch (call.method) { case 'execute': this.execute(call.arguments as Array<string>, result); break; case 'cancel': this.cancel(); result.success(true); break; default: result.notImplemented(); } }); } }

注意一个细节:MethodChannel 的 handler 回调线程不是 UI 线程,所以在鸿蒙侧执行耗时 ffmpeg 任务时,不要在回调里直接更新 UI 状态。Dart 侧收到 result 之后,再用 Provider 或者 ValueNotifier 通知界面刷新进度,别跨层拿 context。

如果你对 Flutter 组件通信和状态管理比较熟,可以在 Dart 侧这样设计:插件只负责“执行命令、回传结果”,用一个MediaTaskController extends ChangeNotifier维护任务状态,然后借助provider包在页面里监听。鸿蒙侧的 Channel 只管透传字符串和二进制数据,不要把 UI 逻辑写进插件里,否则后面想复用这个能力给鸿蒙元服务时就麻烦了。

4. 常见问题与排查技巧实录

4.1 问题速查表

我把这个项目里遇到的高频问题整理成一张表,方便你直接定位:

现象可能原因解决方案
编译时找不到sysroot里的头文件OHOS_SDK 路径配错检查$OHOS_SDK/native/sysroot/usr/include是否存在
so 加载失败,报 missing symbol用了 Android NDK 或者 bionic libc 工具链换成 OHOS SDK 自带的 clang 重新编译
HAP 包解出来没有 so 文件CMake 输出目录不对在 CMake 中显式设置LIBRARY_OUTPUT_DIRECTORY指定到 libs
ffmpeg 执行后日志乱码中文编码问题统一用 UTF-8,Dart 侧把字符串转成Pointer<Utf8>
进程执行后卡死没有任何回调子进程没有正确关闭 stdin调用child.closeStdin(),或者用参数数组模式规避
DynamicLibrary.open抛异常so 没打进 HAP查看libs/arm64-v8a,确认产物在正确目录
鸿蒙设备上 Hello World 插件能跑,ffmpeg 命令不生效插件注册代码没执行在EntryAbility的 onWindowStageCreate 里手动注册

4.2 逐个复盘:从库找不到聊到线程回调

逐个说一下最典型的几个场景。

“库找不到”是我遇到最多的反馈。很多人把 so 放进entry/src/main/cpp/libs了,但不知道 DevEco 的构建系统默认只打包entry/libs或者entry/src/main/cpp/libs下与 ABI 匹配的目录。如果你同时开了arm64-v8a和x86_64,但构建产物只出了 x86_64,那真机(arm64)上必然加载失败。解决方法是检查build-profile.json5里abiFilters是否配置了arm64-v8a。这个不是 Kotlin/Gradle 那套了,别惯性思维去找 build.gradle。

“回调不触发”是另一个重灾区。ffmpeg 执行时间长,如果鸿蒙侧的 CommandExecutor 是 async 的,但 Dart 侧的 MethodChannel 调用设置了超时,或者 Flutter 里用了await但事件一直没回,最常见的原因是你的 native 函数在 UI 线程上同步阻塞了。鸿蒙的 flutter 插件运行环境里,MethodChannel 的 handler 默认跑在平台的 UI 线程上。你把 ffmpeg 的转码循环直接放在 handler 里,等于把 UI 线程卡住,此时任何 UI 刷新和 Channel 回传都会被挂起。我说得难听一点,这是新手最容易犯的错。正确做法是:鸿蒙侧的 handler 里 new 一个 TaskPool 或者 Worker,把 execute 丢进去,完成后通过taskpool.Task回到主线程再调result.success()。

“非华为电脑连接鸿蒙手机调试”。这个话题很多人问。其实不必非得是华为电脑,只要你的开发机装了 DevEco Studio 并启用了 adb/haradb 工具,鸿蒙手机开启开发者模式后连 USB 就能被识别。我用的是一台普通的 Windows 笔记本,在cmd里执行haradb devices能看到设备就行。关键是别用 Flutter 默认的flutter devices去检测鸿蒙设备,要配合 OpenHarmony 的 Flutter SDK 来用。

“flutter run 在鸿蒙设备上跑不起来”。如果你是从 Flutter 3.x 的标准分支拉的代码,直接对鸿蒙设备flutter run大概率报错。因为需要先切换到支持鸿蒙的分支或使用 OpenHarmony 仓维护的 Flutter SDK,比如flutter_flutter的 harmony 分支。这个分支对flutter_aar这类懒加载组件支持更全。另一个办法是只把 Flutter 作为 Android/iOS 跨端代码,鸿蒙侧用 DevEco 的原生页面承载,非要复用 Flutter UI 的话再考虑完整分支。

4.3 性能与体积:能阉割就阉割

多媒体库最容易膨胀。ffmpeg 全量编译后 so 体积超过 100MB 是家常便饭,这在移动端是不可接受的。我给你的建议是从“最小可用集”开始加需求,而不是从全集开始减。

以视频压缩为例,你真正需要的解码器可能只有h264、hevc,编码器可能只需要libx264,封装/解封装只需要mp4。那么 configure 里就只保留这几个。哪怕后面要加音频,也是加aac和mp3,而不是把所有编解码器全勾上。

另一个体积杀手是--enable-debug。我见过有人为了排查问题编译了 debug 版 ffmpeg,so 直接翻三四倍,代码里还没有条件编译开关,连上线也没有切回 release。反面教材,引以为戒。建议 configure 时保留--disable-debug,需要日志时通过av_log_set_level在运行时控制级别。

性能方面,软编软解在鸿蒙上默认是吃 CPU 的。一味堆参数解决不了问题,后续可以接鸿蒙的 MediaCodec 硬解,让 ffmpeg 只做滤镜拼接和容器封装,这样能省不少电。进阶的做法是给 ffmpeg 加mediacodec的 hwaccel 后端,不过那部分工作量大很多,不在本文范围,但方向是对的。

5. 后续扩展:鸿蒙元服务与多场景玩法

ffmpeg_cli 在鸿蒙上跑通之后,能玩的花样比想象中多。

第一个方向是把能力嵌到鸿蒙元服务。鸿蒙的元服务天生轻量化,不太可能直接塞一个 50MB 的 ffmpeg,但如果你只编译libavcodec+libavutil这两个库,裁剪后可以压到几 MB 级别,实现一个“一键压缩视频”的小卡片服务,在微信里收到大视频后直接拉起处理,体验会有代差感。这也是鸿蒙生态里比较新的机会点,早入场的人有优势。

第二个方向是做音频层面的精细化处理。比如你在音频通话场景里做降噪、回声消除、音量均衡,ffmpeg 的aresample和volume滤镜足够用;再往上还可以接 G.722、Opus 等编码器。之前有开发者聊到鸿蒙通话蓝牙噪声的问题,这类系统级现象通常需要结合硬件策略,但 ffmpeg 侧的音频前处理能缓解一部分。

第三个方向是把 flutter 的 impeller 渲染引擎跟 ffmpeg 的画面处理结合。比如从视频里抠出一帧做图片超分、人脸检测后再送进 flutter 侧渲染。ffmpeg 负责视频解码和帧提取,flutter 侧负责视觉效果,两边各干各擅长的,架构上清晰也不容易崩。

最后再聊一点逆向工程的杂谈:flutter逆向工具箱里处理 Flutter 插件的逻辑,很大一部分依赖对 MethodChannel 名字和 init 入口的识别。你在鸿蒙上做适配时,把 channel 名字定义得规范一些、不要一股脑全塞进一个“万能 method”里,将来对自己调试、对第三方工具识别都会友好很多。这不是为了规避什么,纯粹是工程洁癖带来的一系列好处。

6. 实操经验补充:我是怎么把这个项目真正收尾的

如果只把 so 编译出来、插件能跑,这个项目只能算完成 60%。真正的收尾工作其实是把使用体验打磨到跟 Android/iOS 版本一致。

我做了三件事,你可以直接抄:

第一,统一日志输出。ffmpeg 的日志通过av_log_set_callback回传,Android 上很多人直接丢给 Logcat,鸿蒙上则需要转发到hilog。我在鸿蒙侧封装了一个logCallback,把 AV_LOG_LEVEL 映射到 hilog 的级别,比如AV_LOG_ERROR对应HiLogError,AV_LOG_DEBUG对应HiLogDebug。这样真机调试时hilog | grep Ffmpeg就能看到完整链路,不用再猜哪一步断了。

第二,做一个任务取消机制。ffmpeg 的长任务如果无法取消,用户界面会很被动。ffmpeg 提供了av_interrupt_callback,你可以在鸿蒙侧维护一个全局 AtomicBoolean,当 Dart 侧调用cancel()时置为 true,ffmpeg 在下一帧处理前检查到这个标记就会中断退出。这个机制加不加,体验差别非常大。没有它,用户点了个“取消”,界面却要等转码跑完,那感觉就像让用户等着看进度条走完再认输。

第三,把进度回调做成流式事件。ffmpeg 的转码进度可以从编码器的AV_TIME_BASE换算出来,但我更推荐直接读av_read_frame的解码位置百分比。在鸿蒙侧的 native 层每隔一段时间抛出一个 progress 事件,通过 EventChannel 持续推送到 Dart 侧。这里要说的是,Dart 侧收到进度后不要每次都 setState 刷新整个页面,用 ValueNotifier 或者 StreamBuilder 精准更新进度条,避免 Flutter 重绘风暴,这个细节在小内存设备上尤其重要。

我个人的体会是,鸿蒙化适配这种工作,本质上是把一个成熟生态里的成熟能力,迁移到一个还在快速演进的平台上。ffmpeg 本身不复杂,复杂的是它周围那一圈链路:工具链、ABI、插件注册、线程模型。你把这些链路逐一打通之后,会发现鸿蒙开发并没有想象中那么陌生,它只是换了一套编译器和一套系统接口,底层逻辑还是那些底层逻辑。下一次你再去适配其他 C/C++ 库时,把本文里这套“确认工具链 -> 交叉编译 -> 整理产物 -> 桥接通道 -> 流式回调 -> 任务控制”的流程复制过去,速度会快非常多。

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

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

立即咨询