HarmonyOS FFmpeg 工具库 —— 在鸿蒙中调用 FFmpeg 命令行工具(fftools),最终驱动 FFmpeg.so 执行音视频处理任务。同时提供硬件加速能力,在保证稳定性的前提下显著提升处理效率。
ohpm install @prq/ffmpeg-tools本库的核心能力是将 FFmpeg 命令行工具封装为 ArkTS 可调用的 Native 接口,支持:
- 提供统一、可编程的音视频处理能力
- 支持转码、提取、下载等常见场景
- 内置完善的任务管理与进度控制机制
- 提供硬件解码与编码加速能力,在保证稳定性的前提下显著提升处理效率,适合对性能与能耗敏感的音视频场景。
- 最低运行版本 "compatibleSdkVersion": "5.0.0(12)"
- 体积小巧:仅 11.46MB,在同类工具中体量最小,可有效控制包体积增长。
- 能力灵活且完备:既支持通过
FFmpegCommandBuilder链式构建自定义 FFmpeg 命令,也在FFmpegFactory中内置了多种常用命令,开箱即用。 - 性能表现优秀:支持硬件加速,2 分钟视频加水印仅需 15.68s。
- 实现透明、可扩展:整体原理与核心代码均已开源,并提供完整的实现笔记与文档参考,便于二次开发与问题排查。
已测试从网络 MP4 下载并转换为 mkv、avi、mp4 等格式,输出结果正常可用。
示例执行命令:
ffmpeg -i https://example.com/video.mp4 -c:v copy -c:a copy -f avi -y /data/storage/el2/base/haps/entry/files/output.avi
性能统计:
- 原视频大小:4981937 字节(约 4.75MB),时长 2 分钟
- MP4 → MP4(copy):耗时 0.42s,输出 4981937 字节
- MP4 → MKV(copy):耗时 0.35s,输出 4831.78 KB
示例结果:
视频加水印
示例执行命令:
ffmpeg -i https://sns-video-al.xhscdn.com/stream/110/405/01e583cb6e0fed5a010370038c8ad962fb_405.mp4
-i /data/storage/el2/base/haps/entry/files/watermark_selected.png -filter_complex [0:v][1:v]overlay=main_w-overlay_w-10:main_h-overlay_h-10[outv] -map [outv] -map 0:a -c:v h264_ohosavcodec -c:a copy -y /data/storage/el2/base/haps/entry/files/watermark_output.mp4
- 这里使用的是h264_ohosavcodec进行硬解码和硬编码相关处理
性能统计:
- 2分钟时长视频,硬解加水印耗时约15.68S
- [1:41:13 PM]:输出文件大小:90712.12 KB
示例结果:
视频裁剪
测试素材配置:
- 编码:H.264
- 时长:约 1 分 34 秒
- 原始大小:4.75 MB
示例执行命令:
ffmpeg -i /data/storage/el2/base/haps/entry/files/selected_video.mp4
-vf crop=640:360:0:0
-c:v h264_ohosavcodec
-c:a copy
-vsync cfr -fps_mode cfr
-max_muxing_queue_size 1024
-y /data/storage/el2/base/haps/entry/files/crop_output.mp4
执行流程:
- 输入视频:H.264
- 硬解码(GPU)→ 原始 YUV 帧
- crop 滤镜(CPU)→ 裁剪后的 YUV
- 硬编码(GPU)→ 输出 H.264 视频
性能统计:
**示例结果:**对比32s位置,方便大家进行对比观测效果
import { FFmpegManager, FFmpegFactory, ContainerFormat, TaskCallback } from '@prq/ffmpeg-tools';
// 获取管理器实例
const manager = FFmpegManager.getInstance();
// 执行视频格式转换(零拷贝)
const taskId = manager.execute(
FFmpegFactory.remux(inputPath, outputPath, ContainerFormat.FLV),
120000, // 超时时间(毫秒)
{
onStart: () => console.log('任务开始'),
onProgress: (progress: number) => console.log(`进度: ${(progress * 100).toFixed(1)}%`),
onSuccess: () => console.log('转换成功'),
onFailure: () => console.log('转换失败')
} as TaskCallback
);
// 取消任务
manager.cancel(taskId);import { FFMpegUtils } from '@prq/ffmpeg-tools';
// 开启 FFmpeg native 层日志输出
FFMpegUtils.showLog(true);import { FFmpegFactory, ContainerFormat } from '@prq/ffmpeg-tools';
// 封装格式转换
FFmpegFactory.remux(input, output, ContainerFormat.MP4); // 默认 MP4
FFmpegFactory.remux(input, output, ContainerFormat.FLV); // MP4 → FLV
FFmpegFactory.remux(input, output, ContainerFormat.AVI); // MP4 → AVI
FFmpegFactory.remux(input, output, ContainerFormat.MKV); // MP4 → MKV
FFmpegFactory.remux(input, output, ContainerFormat.TS); // MP4 → TS
// 视频裁剪
FFmpegFactory.cut(input, output, '00:00:10', '30'); // 从10秒开始裁剪30秒
// 音频提取
FFmpegFactory.extractAudio(input, output); // 提取 AAC 音频import { FFmpegFactory } from '@prq/ffmpeg-tools';
// 视频缩放
FFmpegFactory.scale(input, output, 1280, 720); // 缩放到 720p
// 视频转码
FFmpegFactory.transcode(input, output); // 默认转码
FFmpegFactory.transcode(input, output, '2M'); // 指定码率 2Mbps
// 添加水印(右下角)
FFmpegFactory.watermark(input, watermarkImg, output);
// 视频拼接
FFmpegFactory.concat([video1, video2, video3], output);import { FFmpegFactory } from '@prq/ffmpeg-tools';
// RTSP 流录制
FFmpegFactory.downloadRtsp(rtspUrl, output); // 持续录制
FFmpegFactory.downloadRtsp(rtspUrl, output, 60); // 录制60秒
// HLS 流下载
FFmpegFactory.downloadHls(hlsUrl, output);import { FFmpegCommandBuilder } from '@prq/ffmpeg-tools';
// 链式构建自定义命令
const cmd = new FFmpegCommandBuilder()
.input(inputPath)
.hwaccel() // 启用硬解硬编
.scale(1280, 720) // 缩放
.fps(30) // 帧率
.videoBitrate('2M') // 视频码率
.audioCodec('aac') // 音频编码
.audioBitrate('128k') // 音频码率
.output(outputPath)
.build();
// 执行命令
manager.execute(cmd, 180000, callback);将 FFmpeg 命令行工具(fftools)封装为 ArkTS 可调用的 Native 库,ArkTS 以 API 形式驱动常见转码/下载任务。
采用 AKI 框架 实现 ArkTS(ETS) ⇄ C++ 的双向调用:
- ETS → Native:通过
JSBIND_PFUNCTION宏将执行接口注册到 ArkTS;请求进入 Native 后由框架投递到线程池执行 - Native → ETS:通过带
UUID的回调机制上报任务进度与最终结果,回调可以把异步状态传回 ETS 层
- 原 FFmpeg 在严重错误时会调用
exit()导致进程退出 - 为避免影响宿主进程,已将
exit_program()改为基于setjmp/longjmp的非局部跳转方案,使出错时能优雅返回错误码并由上层处理(而非终止进程) - 状态隔离:Native 层使用
thread_local来尽可能隔离每个任务的局部状态,避免不同任务互相污染
实测发现:FFmpeg 内部仍大量依赖全局变量与共享状态——因此不适合在同一进程内多线程并发执行多个 FFTools 实例;并发运行会导致互相干扰、崩溃或数据错乱。
当前策略:任务调度层通过限制工作线程数量为 1,保证串行执行。
| 方法 | 说明 |
|---|---|
remux(input, output, format?) |
封装格式转换(零拷贝) |
cut(input, output, startTime, duration) |
视频裁剪(零拷贝) |
extractAudio(input, output) |
提取音频(AAC) |
scale(input, output, width, height) |
视频缩放(硬解硬编) |
watermark(input, watermarkImg, output) |
添加水印(硬编码) |
transcode(input, output, bitrate?) |
视频转码(硬解硬编) |
concat(inputFiles, output) |
视频拼接(硬解硬编) |
downloadRtsp(rtspUrl, output, duration?) |
RTSP 流录制 |
downloadHls(hlsUrl, output) |
HLS 流下载 |
| videoCrop( input, output, width, height, x, y ) | 视频裁剪(crop + 硬解硬编) |
| videoCropCenter( input, output, width, height) | 视频居中裁剪(crop + 硬解硬编) |
| 方法 | 说明 |
|---|---|
videoToGif(input, output, fps?, width?) |
视频转 GIF(默认 10fps, 320px 宽) |
videoSnapshot(input, output, time?) |
视频截图(默认第1秒) |
videoToImages(input, outputPattern, fps?) |
视频批量截图(默认每秒1张) |
imagesToVideo(inputPattern, output, fps?) |
图片序列合成视频(默认 25fps) |
imageScale(input, output, width, height?) |
图片缩放(height=-1 保持宽高比) |
imageConvert(input, output, quality?) |
图片格式转换(quality: 1-31,默认2) |
imageWatermark(input, watermark, output, position?) |
图片添加水印(支持5个位置) |
imageHStack(inputs, output) |
图片横向拼接 |
imageVStack(inputs, output) |
图片纵向拼接 |
imageRotate(input, output, angle) |
图片旋转(90/180/270度) |
imageCrop(input, output, width, height, x?, y?) |
图片裁剪 |
imageAddText(input, output, text, fontSize?, color?, x?, y?) |
图片添加文字 |
| 方法 | 说明 |
|---|---|
input(path) |
添加输入文件 |
output(path) |
设置输出文件 |
hwaccel() |
启用硬件加速(硬解+硬编) |
hwDecode() |
仅启用硬件解码 |
hwEncode() |
仅启用硬件编码 |
filter(expr) |
添加视频滤镜 |
scale(width, height) |
视频缩放 |
fps(value) |
设置帧率 |
videoCodec(codec) |
设置视频编码器 |
audioCodec(codec) |
设置音频编码器 |
videoBitrate(bitrate) |
设置视频码率 |
audioBitrate(bitrate) |
设置音频码率 |
preset(value) |
设置 x264 预设 |
crf(value) |
设置 CRF 质量 |
format(fmt) |
设置输出格式 |
startTime(time) |
设置开始时间 |
duration(time) |
设置持续时长 |
arg(key, value?) |
添加额外参数 |
build() |
构建命令数组 |
buildString() |
构建命令字符串(调试用) |
| 方法 | 说明 |
|---|---|
getInstance() |
获取单例实例 |
execute(commands, duration, callback) |
执行任务 |
executeWithPriority(commands, duration, priority, callback) |
带优先级执行 |
cancel(taskId) |
取消任务 |
cancelAll() |
取消所有任务 |
getPendingTaskCount() |
获取等待任务数 |
getActiveTaskCount() |
获取活动任务数 |
| 方法 | 说明 |
|---|---|
executeFFmpegCommand(options) |
执行 FFmpeg 命令(底层接口) |
showLog(show) |
开启/关闭 Native 层日志 |
| 回调 | 说明 |
|---|---|
onStart() |
任务开始 |
onProgress(progress) |
进度更新 (0-1) |
onSuccess() |
任务成功 |
onFailure() |
任务失败 |
onCancelled?() |
任务取消 |
onTimeout?() |
任务超时 |
onError?(error) |
错误信息 |
| 格式 | 说明 |
|---|---|
MP4 |
MP4 格式 |
FLV |
FLV 格式 |
MKV |
MKV 格式 |
AVI |
AVI 格式 |
TS |
MPEG-TS 格式 |
| 优先级 | 说明 |
|---|---|
HIGH |
高优先级 |
NORMAL |
普通优先级(默认) |
LOW |
低优先级 |
-
网络权限:访问网络 URL 需要在
module.json5中添加权限:"requestPermissions": [ { "name": "ohos.permission.INTERNET" } ]
-
包体积优化:当前
libffmpegutils.so约 70MB(依赖完整 FFmpeg 库)- 建议在
module.json5中开启压缩:"compressNativeLibs": true - 可参考华为官方方案进行拆分与裁剪:华为开发者博客
- 建议在
-
架构支持:仅支持 arm64-v8a 架构
-
系统要求:HarmonyOS 5.0+ (API 12+)
-
并发限制:FFmpeg 内部使用全局变量,不支持多线程并发执行,任务会串行处理
-
当前优化:
-
优化包体积管理,aki通过依赖引入,其自带了多个架构的so文件,nativeLib 配置来过滤无用的架构
"buildOption": { "napiLibFilterOption": { "excludes": [ "**/armeabi-v7a/**", "**/x86_64/**" ] } },
-
- 使用过程中如发现问题,欢迎通过 Issue 提交反馈;
- 也非常欢迎感兴趣的开发者提交 PR,共同完善项目;
- 若遇到较复杂的问题,建议开启 native 层日志,并携带相关日志信息反馈,我会尽快协助排查和处理。
- 目前项目中使用暂未遇到问题,暂无更新计划。如果大家使用的时候有遇到什么问题,可以提issue反馈,我会及时更新处理。
提供硬件解码与编码加速能力,在保证稳定性的前提下显著提升处理效率,适合对性能与能耗敏感的音视频场景。
1.修复视频缩放场景下音频流未正确写入的问题
2.解决 #issue1,新增图片处理相关能力
1.默认开启 native 日志
2.降低 API 版本要求:将 最低SDK版本从 17 降低到 12,让库可以在更多设备上运行
1.native层可以正确的返回错误
1.解决 #issue2,修复 HAR 包缺少 libaki_jsbind.so 导致的 "Cannot read property JSBind of undefined" 错误
1.解决 #issue3,新增视频裁剪相关能力
感谢 zpswz、FXY970610、magicalapp 提出的相关 issue,帮助我更好地完善和验证了本项目。
MIT





