第23章:媒体探测与转码:ffprobe 与 ffmpeg 封装
本章分析 internal/pkg/media 包:ffprobe 探测器(时长与分辨率)、ffmpeg 转码器(ASR 标准音频、精确裁剪与关键帧快速裁剪双模式),以及参数构建函数与命令执行的工程化处理。ffmpeg 是本系统唯一的外部本地进程依赖,封装质量决定 ASR 音频质量与切片精度。
流程
media 包提供两类能力,分别服务两条流水线:
- ASR 链路(ffprobe + ConvertToASRMP3):源视频 → ffprobe 探出 duration/width/height → ffmpeg 抽音轨并转成「16kHz 单声道 64kbps MP3」→ 上传对象存储给豆包远端识别。去视频轨与降采样把几 GB 的视频压成几十 MB 音频,是远程 ASR 可行的前提。
- 草稿链路 (CutVideoSegment / CutVideoSegmentFast):源视频 → 按毫秒区间裁出 clip_XXX.mp4。双模式选择规则在 prepare 包(第 39 章):总时长 ≤ 10 分钟走精确重编码 (帧级精度),超过走关键帧快速拷贝(-c copy 流拷贝,秒级完成但起点对齐关键帧)。
命令执行统一走 exec.CommandContext:ctx 取消即杀进程(优雅退出时不会留孤儿 ffmpeg)、CombinedOutput 捕获 stderr 进错误信息(ffmpeg 的报错全在 stderr,直接进日志可查)、-threads 6 限制单进程线程数(多任务并发时不打满 CPU,常量注释明示)。
实现
ffmpeg.go 的几个精细设计:
- 常量即调参依据 :
DefaultASRSampleRate=16000(注释:识别准确率与体积平衡)、DefaultASRChannels=1(注释:显著减小体积)、DefaultASRMP3Bitrate="64k"------每个数值都有理由写在旁边,未来调优有据可依。 buildASRMP3Args独立成纯函数 :注释「便于单元测试校验」------ffmpeg 参数正确性无法用集成测试保证(没有二进制的环境跑不了),纯函数让ffmpeg_test.go可以逐参数断言,这是测试外部命令的范式。- 对齐滤镜链(ASRAlignOptions) :
ConvertToASRMP3Aligned支持三参数对齐音轨与视频时间轴------TrimStartSec(裁掉音轨前缀)、LeadPadMs(adelay片头补静音,注释「使内容落在视频时间轴上」)、TargetDurSec(apad=whole_dur封口到目标时长)。配套注释还记录了一个深坑:「始终 asetpts 归零,避免源片 audio.start_time>0 时 apad/whole_dur 按错误时间戳提前结束」------ffmpeg 时间戳语义的踩坑结晶。 - 滤镜链拼接 :
buildASRAlignAudioFilter按条件拼接 atrim→asetpts→adelay→aformat→apad,顺序即语义(先裁再归零再补再格式化最后封口),每个环节有注释。 - channelLayoutName:1→mono、2→stereo、其他直接数字,aformat 参数的合法值适配。
ffprobe.go(NewFFprobeProber(""))解析 JSON 输出取 duration/width/height,探测结果回写素材的 Duration/Width/Height 字段(第 10 章 service 注释「分辨率由 ASR 预处理 ffprobe 回写」)。
裁剪双模式的差异:精确模式用 -ss ... -to ... + 重编码(-c:v libx264 -c:a aac 一族),快速模式 -ss ... -to ... -c copy。CutVideoSegmentFast 的起点会落到 ss 之前最近的关键帧(或之后,取决于输入位置),适合「成片总时长优先」的大批量场景。
📌 设计决策
- exec.CommandContext 而非常驻 ffmpeg 服务:进程隔离最干净(一个任务崩不影响别的)、参数天然并发安全;代价是每次启动开销(百毫秒级),对分钟级转码可忽略。
- 线程上限 6:与 Docker compose 的
cpus: '6'配对(第 50 章),软件限流与容器配额双保险。 - 对齐参数化而非写死:m3u8/直播流的时间戳偏移千奇百怪,对齐需求只在特定场景出现(直播流裁片时音画时间轴有偏移),零值即关闭。
代码示例
参数构建纯函数------测试的锚点:
go
// internal/pkg/media/ffmpeg.go(节选)
func buildASRMP3Args(sampleRate, channels int, bitrate,
inputPath, outputPath string, align ASRAlignOptions) []string {
args := []string{
"-y",
"-threads", strconv.Itoa(DefaultFFmpegThreads), // 单进程线程上限
"-i", inputPath,
"-vn", // 去视频轨
"-af", buildASRAlignAudioFilter(sampleRate, channels, align),
"-c:a", "libmp3lame",
"-b:a", bitrate,
}
if align.TargetDurSec > 0 {
args = append(args, "-t", formatFFmpegSeconds(align.TargetDurSec))
}
args = append(args, outputPath)
return args
}
执行与错误现场保留:
go
// internal/pkg/media/ffmpeg.go(节选)
func (c *FFmpegConverter) ConvertToASRMP3Aligned(ctx context.Context,
inputPath, outputPath string, align ASRAlignOptions) error {
binary := c.BinaryPath
if strings.TrimSpace(binary) == "" {
binary = DefaultFFmpegBinary // PATH 查找,兼容本地与容器
}
args := buildASRMP3Args(/* ... */)
cmd := exec.CommandContext(ctx, binary, args...)
output, err := cmd.CombinedOutput()
if err != nil {
// stderr 全文进错误:ffmpeg 报错排障的第一现场
return fmt.Errorf("ffmpeg 转 MP3 失败: %w, output: %s",
err, strings.TrimSpace(string(output)))
}
return nil
}
小结
- media 包 = ffprobe 探测 + ffmpeg 双模式转码,ASR 与草稿两条流水线共用。
- 参数构建纯函数化 + CombinedOutput + CommandContext 是命令封装三件套。
- 常量注释记录调参依据,滤镜链注释记录踩坑,代码即运维手册。
思考题
- 关键帧快速裁剪的精度损失(起点漂移到关键帧)对成片有什么可感知影响?什么业务必须用精确模式?
- 若并发 10 个任务同时转码,-threads 6 与容器 cpus 6 的组合下实际会发生什么?如何验证?
项目信息
- GitHub仓库:github.com/Chyona/live-mixer
- 项目案例:gogoshine.com