You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于Go+Google Speech API的未知长度RTMP视频流转写问题排查

解决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_indextimestamp,转写服务维护一个有序队列,按顺序拼接音频,避免乱序导致的转写混乱。

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

火山引擎 最新活动