基于Go+Google Speech API的未知长度RTMP视频流转写问题排查
我之前做过类似的实时音视频转写项目,碰到过几乎一样的问题,结合我的实践经验给你拆解问题并给出可行方案:
一、先搞定数据丢失:从切片和PubSub可靠性入手
1. 别直接分割字节流,用FFmpeg做时间对齐的音频切片
你提到PubSub有消息大小限制,直接把转码后的Linear16字节流硬分割很容易导致音频帧不完整,甚至丢帧。正确的做法是用FFmpeg按时间切片,生成大小可控的完整音频片段:
# 每200ms生成一个单声道16kHz的Linear16 WAV片段,远低于PubSub的大小限制 ffmpeg -i rtmp://your-stream-url -vn -acodec pcm_s16le -ar 16000 -ac 1 -f segment -segment_time 0.2 -segment_format wav -strftime 1 /tmp/audio_%Y%m%d%H%M%S_%3N.wav
然后在Go转码服务里监听临时目录的文件变化,读取完整的WAV文件后再发送到PubSub——这样能保证每个消息都是完整的音频帧,不会出现截断丢失。
2. 配置PubSub的可靠性投递
默认的PubSub配置可能会因为网络波动丢消息,你需要在Go客户端里开启重试和至少一次投递:
ctx := context.Background() client, err := pubsub.NewClient(ctx, "your-project-id") if err != nil { log.Fatalf("创建PubSub客户端失败: %v", err) } topic := client.Topic("audio-transcribe-topic") // 调整并发数和重试策略,确保消息能投递成功 topic.PublishSettings = pubsub.PublishSettings{ NumGoroutines: 10, RetrySettings: pubsub.RetrySettings{ MaxAttempts: 5, MaxBackoff: time.Second * 30, }, }
同时,转写服务处理消息时一定要正确ACK——只有确认处理完成再ACK,避免消息被提前丢弃。
3. 转码时避免FFmpeg丢帧
FFmpeg默认的音频同步策略可能会丢帧,加上-async 1强制同步:
ffmpeg -i rtmp://your-stream-url -vn -acodec pcm_s16le -ar 16000 -ac 1 -async 1 -f wav pipe:1
如果用管道输出到Go服务,别用固定大小的Read,改用io.Copy循环读取,防止阻塞导致的字节流丢失。
二、提升转写准确率:从音频质量和转写逻辑优化
1. 保证音频片段的连续性与完整性
转写API对音频的完整性要求很高,哪怕丢几帧都可能导致识别错误:
- 检查每个PubSub消息的音频字节长度:Linear16单声道16kHz是32KB/秒,每帧2字节,所以字节长度必须是2的倍数。如果不是,就把不完整的字节缓存起来,和下一个片段合并后再发送给转写API。
- 给每个消息加时序标记:在PubSub消息的Attributes里添加
segment_index和timestamp,转写服务维护一个有序队列,按顺序拼接音频,避免乱序导致的转写混乱。
2. 用流式转写API替代批量转写
如果你现在是把单个片段发给转写API做批量识别,准确率肯定高不了——实时转写需要用支持流式识别的API。转写服务可以把接收到的片段拼接成连续的音频流,再推送给流式API:
// 初始化流式识别客户端 stream, err := speechClient.StreamingRecognize(ctx) if err != nil { log.Fatalf("创建流式识别客户端失败: %v", err) } defer stream.CloseSend() // 缓存不完整的音频帧 var cachedBytes []byte // 处理PubSub消息 go func() { for msg := range msgChan { audioBytes := msg.Data // 检查是否是完整的帧 if len(audioBytes)%2 != 0 { cachedBytes = append(cachedBytes, audioBytes...) continue } // 先发送缓存的完整帧 if len(cachedBytes) > 0 { if err := stream.Send(&speech.StreamingRecognizeRequest{ AudioContent: cachedBytes, }); err != nil { log.Printf("发送缓存音频失败: %v", err) } cachedBytes = nil } // 发送当前片段 if err := stream.Send(&speech.StreamingRecognizeRequest{ AudioContent: audioBytes, }); err != nil { log.Printf("发送音频片段失败: %v", err) } // 确认消息处理完成 msg.Ack() } }() // 接收实时转写结果 for { resp, err := stream.Recv() if err == io.EOF { break } if err != nil { log.Printf("接收转写结果失败: %v", err) continue } for _, result := range resp.Results { fmt.Printf("实时转写结果: %s\n", result.Alternatives[0].Transcript) } }
3. 先降噪再转写
视频通话的背景噪音是准确率杀手,用FFmpeg的降噪滤镜预处理:
ffmpeg -i rtmp://your-stream-url -vn -acodec pcm_s16le -ar 16000 -ac 1 -af afftdn -async 1 -f wav pipe:1
afftdn是频域降噪滤镜,能有效减少键盘声、背景音等干扰,大幅提升转写准确率。
三、进阶优化:减少中间环节的延迟与丢失
如果实时性要求很高,其实可以跳过PubSub,让转码服务直接通过gRPC把音频流推送给转写服务——这样能减少中间环节的延迟和消息丢失风险。另外,记得在两个服务里加详细日志,记录每个音频片段的大小、时间戳、转写结果,方便排查问题。
内容的提问来源于stack exchange,提问作者Josh




