HarmonyOS APP开发---“铃声工坊“音频剪辑App,需要用到这个库
·
HarmonyOS APP开发—“铃声工坊"音频剪辑App,需要用到这个库> 用户选一段视频,拖波形图选出 00:15~00:45,转成 M4R 铃声。这三步背后是探测、裁剪、重编码——自己写解码器编码器工作量按人月算。@ohos/ffmpeg-kit 让你用 FFmpeg 命令搞定,-ss -t -vn 三个参数就是全部。> 📦 仓库地址 | ohpm install @ohos/ffmpeg-kit—## 场景"铃声工坊"的用户流程:1. 选一个视频或音频文件2. 拖动波形图选起止时间3. 选输出格式(MP3 / M4R / AAC)4. 点"生成铃声”,等几秒,保存四步全是音视频硬活:画波形要先知道总时长(FFprobe)、裁剪要精确到帧(-ss -t)、转格式要重编码(-c:a)。FFmpeg 一条命令干完,@ohos/ffmpeg-kit 让你在 ArkTS 里调这条命令。—## 为什么是 FFmpegKit| 需求 | 自己实现 | @ohos/ffmpeg-kit ||------|---------|------------------|| 探测媒体信息 | 手写 demuxer | ✅ 一行 FFprobe || 时间裁剪 | 手写帧定位 | ✅ -ss -t || 格式转换 | 手写编码器 | ✅ -c:a || 提取音频 | 手写分离逻辑 | ✅ -vn || 格式兼容 | 逐个适配 | ✅ FFmpeg 全格式 || 开发周期 | 数月 | ✅ 数天 |—## 关键实现### 探测源文件(画波形的前提)typescriptimport { FFprobeKit, FFmpegKit, ReturnCode } from '@ohos/ffmpeg-kit'interface MediaInfo { duration: number // 秒 audioCodec: string sampleRate: string channels: number}async function probeMedia(path: string): Promise<MediaInfo | null> { const session = await FFprobeKit.getMediaInformation(path) const info = session.getMediaInformation() if (!info) { console.error('探测失败,原始输出: ' + session.getOutput()) // 排查时看原始文本 return null } const audio = info.getStreams().find((s) => s.getType() === 'audio') if (!audio) { promptAction.showToast({ message: '该文件没有音频轨' }) return null } return { duration: parseFloat(info.getDuration()) / 1000, // ms → s audioCodec: audio.getCodec(), sampleRate: audio.getSampleRate(), channels: audio.getChannels(), }}用 getMediaInformation() 拿结构化对象,getOutput() 只在排查问题时用。### 裁剪 + 转码typescripttype OutputFormat = 'mp3' | 'm4r' | 'aac'interface RingtoneConfig { input: string output: string startSec: number durationSec: number format: OutputFormat}function buildCommand(cfg: RingtoneConfig): string { const codecMap: Record<OutputFormat, string> = { mp3: '-c:a libmp3lame -b:a 192k', m4r: '-c:a aac -b:a 192k -f ipod', // M4R = AAC 编码 + ipod 容器 aac: '-c:a aac -b:a 192k', } return [ `-ss ${cfg.startSec}`, // 放 -i 之前 = 快速定位模式,不解码前面内容 `-t ${cfg.durationSec}`, // 持续时长(不是结束时间) `-i "${cfg.input}"`, '-vn', // 去掉视频轨 codecMap[cfg.format], '-ar 44100 -ac 2', // 铃声标准:44.1kHz 双声道 '-y', // 覆盖已存在文件,否则会卡住等输入 `"${cfg.output}"`, ].join(' ')}async function generate(cfg: RingtoneConfig): Promise<boolean> { const session = FFmpegKit.execute(buildCommand(cfg)) session.setLogCallback((log) => { const msg = log.getMessage() // 从日志解析 time= 字段算进度 const m = msg.match(/time=(\d+):(\d+):(\d+\.\d+)/) if (m) { const sec = parseInt(m[1]) * 3600 + parseInt(m[2]) * 60 + parseFloat(m[3]) updateProgress(Math.min(100, Math.ceil(sec / cfg.durationSec * 100))) } }) // ✅ 等 complete 之后再查输出 const stats = await session.getCompleteFuture() if (ReturnCode.isSuccess(stats.getReturnCode())) { console.info('成功: ' + session.getOutput()) return true } if (ReturnCode.isCancel(stats.getReturnCode())) { cleanupPartialFile(cfg.output) // 取消后清理残留文件 return false } console.error('失败: ' + stats.getFailStackTrace()) promptAction.showToast({ message: '生成失败,请换个文件试试' }) return false}四个参数的坑:| 参数 | 说明 ||------|------|| -ss 位置 | 放 -i 之前是快速定位(不解码),放之后是精确但慢 || -t | 是持续时长,不是结束时间。选 15~45 秒要写 -ss 15 -t 30 || -f ipod | M4R 格式必须指定 ipod 容器 || -y | 不加会卡在"是否覆盖"的交互提示上 |### 支持取消typescriptlet currentSessionId = -1async function generateWithCancel(cfg: RingtoneConfig) { const session = FFmpegKit.execute(buildCommand(cfg)) currentSessionId = session.getSessionId() const stats = await session.getCompleteFuture() currentSessionId = -1 return ReturnCode.isSuccess(stats.getReturnCode())}function onCancel() { if (currentSessionId > 0) FFmpegKit.cancel(currentSessionId) }aboutToDisappear() { FFmpegKit.cancel() } // ✅ 页面退出取消全部,别让它后台空跑—## 两个容易踩的点1. 查询输出必须等 completetypescript// ❌ 命令还在跑就查,拿到空的const session = FFmpegKit.execute(cmd)console.info(session.getOutput())// ✅ await 之后再查const session = FFmpegKit.execute(cmd)await session.getCompleteFuture()console.info(session.getOutput())2. 批量处理要串行FFmpeg 吃 CPU,并行跑多个只会让每个都变慢:typescriptfor (const cfg of configs) { const ok = await generate(cfg) if (ok) successCount++ await new Promise((r) => setTimeout(r, 200)) // 批间喘息,避免 CPU 持续满载}—## 格式预设(产品化小技巧)typescriptconst PRESETS = { phone: { format: 'm4r', maxDuration: 30 }, // 手机铃声 alarm: { format: 'mp3', maxDuration: 60 }, // 闹钟 notification: { format: 'aac', maxDuration: 10 }, // 通知音}// 生成前校验时长if (cfg.durationSec > PRESETS[type].maxDuration) { promptAction.showToast({ message: `${type} 最长 ${PRESETS[type].maxDuration} 秒` }) return}—## 小结"铃声工坊"这个场景里,@ohos/ffmpeg-kit 解决了三件事:FFprobe 拿时长画波形、三个参数完成裁剪转码、异步执行配合日志解析出进度条。音频处理是个深坑,但 FFmpeg 已经填平了。装上这个库,你的工作量从"实现编解码器"变成"拼一条命令行"。
更多推荐

所有评论(0)