Go语言 如何将生成的ogg数据写入webrc音轨?

yxyvkwin  于 2023-09-28  发布在  Go
关注(0)|答案(2)|浏览(155)

我正在使用Azure语音SDK做tts并生成OGG数据,我想将流式输出发送到WebRTC音轨中,我编写了这个函数,可以将数据发送到对等端,但我无法在客户端听到音频。
这段代码有什么问题,我怎么才能让它工作?

func sendAudioData(data io.Reader, audioTrack *webrtc.TrackLocalStaticSample) error {
    ogg, _, oggErr := oggreader.NewWith(data)
    if oggErr != nil {
        return oggErr
    }
    var lastGranule uint64
    for {
        pageData, pageHeader, oggErr := ogg.ParseNextPage()
        if errors.Is(oggErr, io.EOF) {
            fmt.Printf("All audio pages parsed and sent")
            return nil
        }

        if oggErr != nil {
            return oggErr
        }
        slog.Info("ParseNextPage", "header", pageHeader)
        // The amount of samples is the difference between the last and current timestamp
        sampleCount := float64(pageHeader.GranulePosition - lastGranule)
        lastGranule = pageHeader.GranulePosition
        sampleDuration := time.Duration((sampleCount/48000)*1000) * time.Millisecond

        slog.Info("Write Sample", "duration", sampleDuration, "length", len(pageData))
        if oggErr = audioTrack.WriteSample(media.Sample{Data: pageData, Duration: sampleDuration}); oggErr != nil {
            return oggErr
        }
    }
}

这是生成流数据的代码。

package speech

import (
    "errors"
    "fmt"
    "os"
    "time"

    "github.com/Microsoft/cognitive-services-speech-sdk-go/audio"
    "github.com/Microsoft/cognitive-services-speech-sdk-go/common"
    "github.com/Microsoft/cognitive-services-speech-sdk-go/speech"
)

func azureTTSStream(text, lang string) (*speech.AudioDataStream, error) {
    // stream, err := audio.CreatePullAudioOutputStream()
    // if err != nil {
    //  return nil, err
    // }
    // audioConfig, err := audio.NewAudioConfigFromStreamOutput(stream)
    // if err != nil {
    //  panic(err)
    // }
    speechKey := os.Getenv("AZURE_SPEECH_KEY")
    speechRegion := os.Getenv("AZURE_SPEECH_REGION")
    speechConfig, err := speech.NewSpeechConfigFromSubscription(speechKey, speechRegion)
    if err != nil {
        panic(err)
    }
    speechConfig.SetSpeechSynthesisOutputFormat(common.Ogg48Khz16BitMonoOpus)
    synthesizer, err := speech.NewSpeechSynthesizerFromConfig(speechConfig, nil)
    if err != nil {
        panic(err)
    }

    voice := azureVoices[lang]
    input := fmt.Sprintf(azureSAMLInput, lang, voice, text)

    task := synthesizer.SpeakSsmlAsync(input)
    var outcome speech.SpeechSynthesisOutcome
    select {
    case outcome = <-task:
    case <-time.After(60 * time.Second):
        fmt.Println("timeout")
        return nil, errors.New("timeout")
    }
    defer outcome.Close()
    if outcome.Error != nil {
        fmt.Println("outcome error: ", outcome.Error)
    }
    
    return speech.NewAudioDataStreamFromSpeechSynthesisResult(outcome.Result)

    // if outcome.Result.Reason == common.SynthesizingAudioCompleted {
    //  fmt.Println("outcome completed")
    // }

    // cancellation, _ := speech.NewCancellationDetailsFromSpeechSynthesisResult(outcome.Result)
    // msg := fmt.Sprintf("CANCELED: Reason=%d.\n", cancellation.Reason)
    // if cancellation.Reason == common.Error {
    //  fmt.Sprintf("%s ErrorCode=%d, ErrorDetails=[%s]\n", msg, cancellation.ErrorCode, cancellation.ErrorDetails)
    // }
}
zwghvu4y

zwghvu4y1#

我认为主要问题可能与最大OPUS帧尺寸有关。我做了很多实验,发现Azure文本到语音API生成的OGG文件的页面大小通常约为。520ms的音频数据。
代码使用ogg.ParseNextPage()获取整个页面数据,然后使用audioTrack.WriteSample将其作为样本发送:

azureSample.ogg
---------------
sendAudioData start
2023/09/01 23:12:00 INFO ParseNextPage header="&{GranulePosition:0 sig:[79 103 103 83] version:0 headerType:0 serial:1006886592 index:1 segmentsCount:1}"
2023/09/01 23:12:00 INFO Write Sample duration=0s length=47
2023/09/01 23:12:00 INFO ParseNextPage header="&{GranulePosition:24000 sig:[79 103 103 83] version:0 headerType:0 serial:1006886592 index:2 segmentsCount:25}"
2023/09/01 23:12:00 INFO Write Sample duration=500ms length=4000
2023/09/01 23:12:00 INFO ParseNextPage header="&{GranulePosition:48960 sig:[79 103 103 83] version:0 headerType:0 serial:1006886592 index:3 segmentsCount:26}"
2023/09/01 23:12:00 INFO Write Sample duration=520ms length=4160
2023/09/01 23:12:00 INFO ParseNextPage header="&{GranulePosition:73920 sig:[79 103 103 83] version:0 headerType:0 serial:1006886592 index:4 segmentsCount:26}"
2023/09/01 23:12:00 INFO Write Sample duration=520ms length=4160
2023/09/01 23:12:00 INFO ParseNextPage header="&{GranulePosition:98880 sig:[79 103 103 83] version:0 headerType:0 serial:1006886592 index:5 segmentsCount:26}"
2023/09/01 23:12:00 INFO Write Sample duration=520ms length=4160
2023/09/01 23:12:00 INFO ParseNextPage header="&{GranulePosition:123840 sig:[79 103 103 83] version:0 headerType:0 serial:1006886592 index:6 segmentsCount:26}"
2023/09/01 23:12:00 INFO Write Sample duration=520ms length=4160
2023/09/01 23:12:00 INFO ParseNextPage header="&{GranulePosition:132312 sig:[79 103 103 83] version:0 headerType:4 serial:1006886592 index:7 segmentsCount:9}"
2023/09/01 23:12:00 INFO Write Sample duration=176ms length=1440
All audio pages parsed and sent

根据RFC 7578和RFC 6716,数据包的最大长度为120 ms:

  1. RFC 7578. RTP Payload Format for the Opus Speech and Audio Codec
    4.2. Opus编码器可以输出表示2.5、5、10、20、40或60ms语音或音频数据的编码帧。此外,可以将任意数量的帧组合成分组,直到表示120ms的语音或音频数据的最大分组持续时间。
  2. RFC 6716. Definition of the Opus Audio Codec
    2.1.4.帧持续时间Opus可以编码2.5、5、10、20、40或60 ms的帧。它还可以将多个帧组合成长达120 ms的数据包。
    3.2.5.代码3:数据包M中的信号帧数不得为零,数据包中包含的音频持续时间不得超过120毫秒
    [R5]代码3数据包包含至少一个帧,但不超过120毫秒的音频总量。
    RFC 6716也有关于帧大小的建议:
    对于实时应用,每秒发送较少的数据包可以降低比特率,因为它减少了IP、UDP和RTP报头的开销。然而,它增加了延迟和对分组丢失的敏感性,因为丢失一个分组构成丢失更大的音频块。增加帧持续时间也略微提高了编码效率,但是对于20ms以上的帧大小,增益变小。因此,对于大多数应用,20 ms帧是一个不错的选择。
    最简单的解决方法是将帧大小转换为20ms。我使用了ffmpeg(不幸的是,我没有在纯Go中找到任何用于此目的的包):
ffmpeg -i azureSample.ogg -c:a libopus -page_duration 20000 -vn azureSample20ms.ogg

我觉得你的代码也有问题。建议使用time.ticker来迭代音频文件页面。举例来说:

ticker := time.NewTicker(oggPageDuration)
for ; true; <-ticker.C {
    pageData, pageHeader, oggErr := ogg.ParseNextPage()
...

另外,请确保您的浏览器未阻止自动播放您的网站。
我也尝试过将页面分割成块,但这需要深入的Opus file formatRTP protocol知识,目前我甚至不确定这是否可行。pion/webrtc不支持
另外值得一提的是,你可以尝试使用oggwriter来转换帧大小,而不是ffmpeg,但是我还没有找到如何改变页面持续时间。看起来它只处理段的大小和段的数量(每个段should be 255 bytes length(除了页面中的最后一个和最大值)。段255的数量也)
另外,我可以推荐使用Chrome进行调试。它将为您提供有关WebRTC连接的大量信息。打开这个URL:chrome://webrtc-internals/
如果想了解更多关于WebRTC的信息,有一些额外的信息可以帮助您进一步研究:

  1. WebRTC For The Curious
  2. WebRTC servers explained
  3. WebRTC API
  4. Get started with WebRTC
hgqdbh6s

hgqdbh6s2#

你不应该使用webrtc.TrackLocalStaticRTP方法来处理语音吗?我注意到在您的代码中使用了webrtc.TrackLocalStaticSample,但它通常用于静态音频,而不是语音。
RTP(实时传输协议)是在实时应用中使用的网络协议,这将是对于这种情况的更合适的解决方案。
下面是一个使用SFU和WebSocket进行实时传输的例子:https://github.com/pion/example-webrtc-applications/blob/master/sfu-ws/main.go
这里是另一个使用示例,演示了如何处理具有多个联播rtp流的输入轨道,并将它们发送回https://github.com/pion/webrtc/tree/1390b16097d38597308a23a4473e8eb20b9f8efd/examples/simulcast
在您的代码中,您可以将webrtc.TrackLocalStaticSample替换为webrtc.TrackLocalStaticRTP,并包含以下代码段来处理格式:

// Convert pageData to RTP packet format
    rtpPacket := rtp.Packet{
        PayloadType:  audioTrack.PayloadType(),
        SequenceNumber: audioTrack.GetNextSequenceNumber(),
        Timestamp: uint32(pageHeader.GranulePosition),
        Payload: pageData,
    }

    // Write RTP packet to the audioTrack
    if rtpErr := audioTrack.WriteRTP(&rtpPacket); rtpErr != nil {
        return rtpErr
    }

有关WebRTC架构的更多示例,请查看:https://medium.com/secure-meeting/webrtc-architecture-basics-p2p-sfu-mcu-and-hybrid-approaches-e2aea14c80f9
我希望我在这件事上能帮上点忙。

相关问题