
本文详解如何在 Go 中正确捕获 ffmpeg 的实时输出流(如 rawvideo),避免因忽略 Read 返回值导致的数据截断问题,并提供可扩展的字节处理框架,适用于后续 UDP 流输入/输出及中间数据篡改场景。
本文详解如何在 go 中正确捕获 ffmpeg 的实时输出流(如 rawvideo),避免因忽略 `read` 返回值导致的数据截断问题,并提供可扩展的字节处理框架,适用于后续 udp 流输入/输出及中间数据篡改场景。
在 Go 中调用 ffmpeg 进行实时音视频流处理(如从文件读取 → 编码/转封装 → 输出原始字节流)是构建流媒体中间件的常见起点。但初学者常陷入一个关键误区:直接对 io.Read 的缓冲区全量写入,而忽略其实际读取字节数(n)——这正是你遇到“输出文件比输入小 1MB”问题的根本原因。
os/exec.Cmd.StdoutPipe() 返回的是一个 io.ReadCloser,其底层行为遵循标准 I/O 协议:每次 Read(p []byte) 调用最多填充 len(p) 字节,但实际读取数 n 可能远小于缓冲区长度(尤其在网络流、管道或高吞吐场景下)。你的原代码中:
_, err5 := stdout.Read(bytes) // ❌ 忽略返回的 n
if len(bytes) > 0 { // ❌ 错误判断:bytes 长度恒为 1024,非有效数据长度
_, err6 := f.Write(bytes) // ❌ 将未读取的“脏内存”也写入文件!
}这会导致大量零填充(zero-filled)垃圾字节被写入文件,破坏原始 bitstream 结构,VLC 无法解析。
✅ 正确做法是始终检查 Read 的返回值 n,并仅操作 buf[:n] 这一有效切片:
package main
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
)
func must(err error) {
if err != nil {
panic(err)
}
}
func main() {
dir, _ := os.Getwd()
inputFile := filepath.Join(dir, "teste-4k.mp4")
outputFile := filepath.Join(dir, "out.raw")
// 启动 ffmpeg:以低延迟方式输出原始 H.264 NAL 单元流(注意:-f h264 更合理,非 rawvideo)
cmd := exec.Command("ffmpeg",
"-hide_banner", "-re",
"-i", inputFile,
"-c:v", "copy", // 透传视频流,不重新编码
"-an", // 丢弃音频(简化测试)
"-f", "h264", // 输出标准 H.264 Annex B 格式(含起始码 00000001)
"-",
)
stdout, err := cmd.StdoutPipe()
must(err)
// ⚠️ 关键:必须启动命令后才能获取 pipe
must(cmd.Start())
// 创建/清空输出文件(O_CREATE | O_TRUNC 更安全,避免追加污染)
f, err := os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
must(err)
defer f.Close()
// 使用合理大小缓冲区(40KB 平衡性能与内存)
buf := make([]byte, 40*1024)
for {
n, err := stdout.Read(buf)
if n > 0 {
// ✅ 仅写入实际读取的有效字节
written, werr := f.Write(buf[:n])
must(werr)
if written != n {
panic(fmt.Sprintf("short write: expected %d, got %d", n, written))
}
}
if err == io.EOF {
break // 流结束
}
if err != nil {
panic(fmt.Sprintf("read error: %v", err))
}
}
// ✅ 必须等待 ffmpeg 进程退出,确保所有数据已刷出
must(cmd.Wait())
fmt.Println("✅ Streaming completed. Output saved to:", outputFile)
}? 关键注意事项:
-
格式选择:
-f rawvideo输出的是未压缩的 YUV 帧(极大,无编码头),而-f h264输出标准 Annex B 格式 H.264 码流(含00000001起始码),更符合“字节级处理 + 再输入 ffmpeg”的需求; -
文件打开模式:避免
O_APPEND(易导致并发写错位),使用O_TRUNC确保干净覆盖; -
进程同步:
cmd.Wait()不可省略,否则可能在 ffmpeg 仍在写入时就关闭文件句柄; -
错误处理:
must()替代verificaErro(),语义更清晰;同时校验Write返回值,防止部分写入; -
后续扩展提示:当转向 UDP 流时,只需将
f.Write(...)替换为udpConn.WriteTo(...),并在 goroutine 中处理收发,即可构建双向流处理管道。
此模式奠定了高可控流处理的基础:你可在 buf[:n] 上任意解析 NALU、注入 SEI、修改时间戳或加密 payload,再无缝喂给下一个 ffmpeg 实例(如 ffmpeg -i udp://... -f flv rtmp://...)。掌握 Read/Write 的原子性,是 Go 流媒体开发的第一道必修课。

















