使用Go实现webrtc播放音频的流程步骤

 更新时间:2025年07月22日 09:49:28   作者:bug菌¹  
WebRTC是一项实时通信技术,允许网络应用或站点,在不需要中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流、音频流或普通数据的传输,本文给大家介绍了使用Go实现webrtc播放音频的流程步骤,需要的朋友可以参考下

问题描述

怎么通过go语言实现webrtc播放服务器音频,代码没有报错,但是就是运行不了。

package main
 
import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "os"
    "time"
 
    "github.com/gen2brain/malgo"
    "github.com/gorilla/websocket"
    "github.com/pion/webrtc/v3"
    "github.com/zaf/g711"
)
 
type WSMessage struct {
    Type string `json:"type"`
    Call string `json:"call,omitempty"`
}
 
func mustMarshalJSON(v interface{}) string {
    data, err := json.Marshal(v)
    if err != nil {
        log.Fatalf("Failed to Marshal JSON: %v", err)
    }
    return string(data)
}
 
func connectToWebSocket(url string) (*websocket.Conn, error) {
    dialer := websocket.DefaultDialer
 
    // Attempt to reconnect infinitely
    for {
        conn, _, err := dialer.Dial(url, nil)
        if err != nil {
            log.Printf("Failed to connect to WebSocket server: %v. Retrying...", err)
            time.Sleep(5 * time.Second) // Wait before retrying
            continue
        }
        return conn, nil
    }
}
 
func main() {
    wsURL := "wss://chat.ruzhila.cn/rtc/radio"
    conn, err := connectToWebSocket(wsURL)
    if err != nil {
        log.Fatalf("WebSocket connection failed: %v", err)
    }
    defer conn.Close()
 
    peerConnection, err := configurePeerConnection()
    if err != nil {
        log.Fatalf("Peer connection configuration failed: %v", err)
    }
    defer peerConnection.Close()
 
    offer, err := peerConnection.CreateOffer(nil)
    if err != nil {
        log.Fatalf("Failed to create offer: %v", err)
    }
 
    err = peerConnection.SetLocalDescription(offer)
    if err != nil {
        log.Fatalf("Failed to set local description: %v", err)
    }
 
    <-webrtc.GatheringCompletePromise(peerConnection)
 
    callMessage := WSMessage{
        Type: "Call",
        Call: mustMarshalJSON(*peerConnection.LocalDescription()),
    }
 
    err = conn.WriteJSON(callMessage)
    if err != nil {
        log.Fatalf("Failed to send call message: %v", err)
    }
 
    go pingServer(conn)
    handleWebSocketMessages(conn, peerConnection)
}
 
func configurePeerConnection() (*webrtc.PeerConnection, error) {
    config := webrtc.Configuration{}
    peerConnection, err := webrtc.NewPeerConnection(config)
    if err != nil {
        return nil, fmt.Errorf("Failed to create peer connection: %w", err)
    }
 
    peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
        log.Printf("Track added: %s", track.Kind().String())
        go handleAudioTrack(track)
    })
 
    audioTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{
        MimeType:  webrtc.MimeTypePCMU,
        ClockRate: 8000,
        Channels:  1,
    }, "audio", "pion")
 
    if err != nil {
        return nil, fmt.Errorf("Failed to create audio track: %w", err)
    }
 
    _, err = peerConnection.AddTrack(audioTrack)
    if err != nil {
        return nil, fmt.Errorf("Failed to add audio track: %w", err)
    }
 
    peerConnection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
        log.Printf("ICE Connection State has changed: %s", state.String())
    })
 
    return peerConnection, nil
}
 
func handleWebSocketMessages(conn *websocket.Conn, peerConnection *webrtc.PeerConnection) {
    for {
        var message WSMessage
        err := conn.ReadJSON(&message)
        if err != nil {
            log.Printf("Socket closed or error reading: %v. Attempting to reconnect...", err)
            conn, _ = reconnectToWebSocket()
            continue
        }
 
        if message.Type == "answer" {
            var answer webrtc.SessionDescription
            err := json.Unmarshal([]byte(message.Call), &answer)
            if err != nil {
                log.Printf("Failed to unmarshal answer: %v", err)
                continue
            }
 
            err = peerConnection.SetRemoteDescription(answer)
            if err != nil {
                log.Printf("Failed to set remote description: %v", err)
                continue
            }
 
            log.Printf("Answer set successfully")
        }
    }
}
 
func reconnectToWebSocket() (*websocket.Conn, error) {
    // You can repeat the connect logic with logging and error handling if required
    return connectToWebSocket("wss://chat.ruzhila.cn/rtc/radio")
}
 
func decodePCMU(payload []byte) []byte {
    return g711.DecodeUlaw(payload)
}
 
var audioBuffer bytes.Buffer
 
func handleAudioTrack(track *webrtc.TrackRemote) {
    log.Println("Audio track started")
 
    for {
        rtpPacket, _, err := track.ReadRTP()
        if err != nil {
            log.Printf("Failed to read RTP packet: %v", err)
            return
        }
 
        pcmData := decodePCMU(rtpPacket.Payload)
        if len(pcmData) > 0 {
            audioBuffer.Write(pcmData)
        }
    }
}
 
func pingServer(conn *websocket.Conn) {
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                log.Printf("Failed to send ping message: %v", err)
                return
            }
        }
    }
}
 
func playWavFile() error {
    pcmData := audioBuffer.Bytes()
    if len(pcmData) == 0 {
        return fmt.Errorf("No PCM data to write")
    }
    err := os.WriteFile("output.wav", pcmData, 0644)
    if err != nil {
        return fmt.Errorf("Failed to write WAV file: %v", err)
    }
 
    file, err := os.Open("output.wav")
    if err != nil {
        return fmt.Errorf("Failed to open WAV file: %v", err)
    }
    defer file.Close()
 
    ctx, err := malgo.InitContext(nil, malgo.ContextConfig{}, func(message string) {
        log.Println(message)
    })
 
    if err != nil {
        return fmt.Errorf("Failed to initialize malgo: %v", err)
    }
    defer ctx.Uninit()
    defer ctx.Free()
 
    deviceConfig := malgo.DefaultDeviceConfig(malgo.Playback)
    deviceConfig.Playback.Channels = 1
    deviceConfig.Playback.Format = malgo.FormatS16
    deviceConfig.SampleRate = 8000
    deviceConfig.Alsa.NoMMap = 1
 
    onSample:=func(pOutPut,pInPut []byte,frameCount uint32){
        io.ReadFull(reader,pOutPut)
    }
 
    deviceCallbacks := malgo.DeviceCallbacks{
        Data: onSample,
    }
    device, err := malgo.InitDevice(ctx.Context, deviceConfig, deviceCallbacks)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer device.Uninit()
 
    err = device.Start()
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
 
    fmt.Println("Press Enter to quit...")
    fmt.Scanln()
 
    return nil
}

请知悉:如下方案不保证一定适配你的问题!

如下是针对上述问题进行专业角度剖析答疑,不喜勿喷,仅供参考:

问题理解

你希望通过 Go 语言 实现 WebRTC 播放音频,已使用 github.com/pion/webrtc 库来配置 WebRTC 连接,并通过 WebSocket 进行信令传递。你已经能够连接 WebRTC 并接收到音频流,但是音频没有正确播放。现阶段的问题是:虽然代码没有报错,但无法播放音频流。

我们将详细分析你当前的实现,并提供切实可行的解决方案。这个方案包括音频流的接收、解码、缓冲管理和音频播放设备的配置,最终确保音频能通过设备正确播放。

问题分析

你提供的代码基本框架是正确的,问题可能出现在以下几个方面:

音频流的正确接收与解码

  • 在 OnTrack 回调函数中,你已经处理了 WebRTC 音频流,并通过 g711.DecodeUlaw 进行了音频数据的解码,但尚未完全确保这些音频数据能够正确传递到播放设备。

音频设备配置

  • 你使用了 malgo 库来播放音频,但代码中并没有明确地将解码后的音频数据传递给播放设备,可能导致音频没有播放。

音频缓冲区的管理

  • 你使用了 audioBuffer 来缓存音频数据,但没有确保解码后的音频数据能够及时传输到设备,导致音频播放过程中出现延迟或中断。

WebRTC 和信令问题

  • 音频流的接收和解码与信令的正确配置(如 offeranswer)及 ICE 连接的建立密切相关。如果信令过程中的某个环节出错,也可能导致音频无法播放。

改进方案

为了确保音频能够正确接收、解码并播放,我们需要对现有代码进行一些改进,具体步骤如下:

步骤 1: 配置 WebRTC PeerConnection 和音频流接收

首先,确保 WebRTC 信令的配置和音频流的接收没有问题。在 OnTrack 回调中,确保音频数据可以通过 g711 解码并缓存在 audioBuffer 中。

1.1 音频流接收与解码

// 接收音频流并解码
func handleAudioTrack(track *webrtc.TrackRemote) {
    log.Println("Audio track started")

    for {
        rtpPacket, _, err := track.ReadRTP()
        if err != nil {
            log.Printf("Failed to read RTP packet: %v", err)
            return
        }

        // 解码 PCM 数据
        pcmData := decodePCMU(rtpPacket.Payload)
        if len(pcmData) > 0 {
            audioBuffer.Write(pcmData) // 将解码后的音频数据写入缓冲区
        }
    }
}

此部分代码确保了音频数据通过 decodePCMU 解码后被写入缓冲区 audioBuffer

步骤 2: 配置音频播放设备

我们使用 malgo 库来播放解码后的 PCM 数据。关键是正确配置音频设备并将缓冲区中的 PCM 数据传递给设备进行播放。

2.1 音频设备的初始化与配置

我们将在 handleAudioTrack 函数中初始化音频设备,并使用 malgo 库播放音频。具体的步骤如下:

func handleAudioTrack(track *webrtc.TrackRemote) {
    log.Println("Audio track started")

    // 初始化 malgo 上下文
    ctx, err := malgo.InitContext(nil, malgo.ContextConfig{}, func(message string) {
        log.Println(message)
    })
    if err != nil {
        log.Printf("Failed to initialize malgo context: %v", err)
        return
    }
    defer ctx.Uninit()
    defer ctx.Free()

    // 配置音频播放设备
    deviceConfig := malgo.DefaultDeviceConfig(malgo.Playback)
    deviceConfig.Playback.Channels = 1 // 设置单声道
    deviceConfig.Playback.Format = malgo.FormatS16
    deviceConfig.SampleRate = 8000  // 设置采样率为 8000Hz
    deviceConfig.Alsa.NoMMap = 1    // 配置 ALSA 参数

    // 音频播放回调
    onSample := func(pOutPut, pInPut []byte, frameCount uint32) {
        pcmData := audioBuffer.Bytes() // 从缓存中取出解码后的音频数据
        if len(pcmData) > 0 {
            copy(pOutPut, pcmData) // 将 PCM 数据传递给输出缓冲
            audioBuffer.Reset()     // 清空缓冲区
        }
    }

    // 初始化设备并开始播放
    deviceCallbacks := malgo.DeviceCallbacks{
        Data: onSample,
    }
    device, err := malgo.InitDevice(ctx.Context, deviceConfig, deviceCallbacks)
    if err != nil {
        log.Printf("Failed to initialize audio device: %v", err)
        return
    }
    defer device.Uninit()

    err = device.Start()
    if err != nil {
        log.Printf("Failed to start audio device: %v", err)
        return
    }

    // 循环读取 RTP 包并解码音频数据
    for {
        rtpPacket, _, err := track.ReadRTP()
        if err != nil {
            log.Printf("Failed to read RTP packet: %v", err)
            return
        }

        pcmData := decodePCMU(rtpPacket.Payload)
        if len(pcmData) > 0 {
            audioBuffer.Write(pcmData) // 将解码后的 PCM 数据写入缓冲区
        }
    }
}

2.2 音频数据的传输与播放

在 onSample 回调函数中,我们将解码后的 PCM 数据传输给音频设备的输出缓冲区。每次播放时,设备会从缓冲区读取 PCM 数据并进行播放。

步骤 3: 音频缓冲管理

确保 audioBuffer 能够及时地从缓冲区取出数据并传递给设备进行播放。要做到这一点,audioBuffer 必须确保缓存中有足够的 PCM 数据进行播放,否则可能会出现无音频输出的情况。

  • 音频缓冲区的大小和数据处理:你可以设置一个较大的缓冲区,并定期将数据写入音频设备的播放缓冲区。确保缓冲区不会过早被清空,避免音频播放中断。
  • 数据流的连续性:确保解码后的音频数据连续地传输到设备,避免因数据不足导致播放中断或卡顿。

步骤 4: 调试与日志输出

为了调试音频播放的过程,可以在各个步骤中加入详细的日志输出,以确保数据流的每一部分都能正常工作:

log.Printf("Decoded %d bytes of PCM data", len(pcmData))
log.Printf("Writing %d bytes to playback buffer", len(pOutPut))

通过这些日志,你可以更清晰地看到每次音频数据的解码、缓存和传输过程。

小结

通过以下几个步骤,我们可以确保 WebRTC 音频流的正确接收、解码和播放

音频流接收与解码

  • 使用 OnTrack 回调接收音频流,并通过 g711.DecodeUlaw 解码 PCM 数据。

音频设备的配置与播放

  • 使用 malgo 库初始化音频设备,配置播放参数(如通道数、采样率等),并通过回调函数将解码后的音频数据传递给播放设备。

音频缓冲管理

  • 使用 audioBuffer 缓存解码后的音频数据,并确保数据能够及时传输给设备进行播放。

调试与日志

  • 加入详细的日志输出,帮助你调试音频数据的接收、解码和播放过程。

这样,你的 Go WebRTC 音频播放 方案应该能够成功接收、解码并播放音频流,解决当前运行时无法播放音频的问题。

希望如上措施及解决方案能够帮到有需要的你。

以上就是使用Go实现webrtc播放音频的流程步骤的详细内容,更多关于Go webrtc播放音频的资料请关注脚本之家其它相关文章!

相关文章

  • 手把手教你如何在Goland中创建和运行项目

    手把手教你如何在Goland中创建和运行项目

    欢迎来到本指南!我们将手把手地教您在Goland中如何创建、配置并运行项目,通过简单的步骤,您将迅速上手这款强大的集成开发环境(IDE),轻松实现您的编程梦想,让我们一起开启这段精彩的旅程吧!
    2024-02-02
  • Go语言中缓冲bufio的原理解读与应用实战

    Go语言中缓冲bufio的原理解读与应用实战

    Go语言标准库中的bufio包提供了带缓冲的I/O操作,它通过封装io.Reader和io.Writer接口,减少频繁的I/O操作,提高读写效率,本文就来详细的介绍一下,感兴趣的可以学习
    2024-10-10
  • golang基于errgroup实现并发调用的方法

    golang基于errgroup实现并发调用的方法

    这篇文章主要介绍了golang基于errgroup实现并发调用,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-09-09
  • golang接口IP限流,IP黑名单,IP白名单的实例

    golang接口IP限流,IP黑名单,IP白名单的实例

    这篇文章主要介绍了golang接口IP限流,IP黑名单,IP白名单的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • Go语言自定义包构建自己的编程工具库

    Go语言自定义包构建自己的编程工具库

    Go 语言的强大不仅体现在其内置功能上,还在于其支持自定义包,这为开发者提供了极大的灵活性和可扩展性,本文将深入介绍如何创建、使用和管理自定义包,探索 Go 语言包的奥秘,打造属于你的编程工具库
    2023-11-11
  • gRPC中拦截器的使用详解

    gRPC中拦截器的使用详解

    这篇文章主要介绍了gRPC中拦截器的使用详解,本次主要介绍在gRPC中使用拦截器,包括一元拦截器和流式拦截器,在拦截器中添加JWT认证,客户端登录之后会获得token,请求特定的API时候需要带上token才能访问,需要的朋友可以参考下
    2023-10-10
  • Go语言切片常考的面试真题解析

    Go语言切片常考的面试真题解析

    了解最新的Go语言面试题型,让面试不再是难事,下面这篇文章主要给大家介绍了关于Go语言切片面试常考的一些问题,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-02-02
  • golang通过递归遍历生成树状结构的操作

    golang通过递归遍历生成树状结构的操作

    这篇文章主要介绍了golang通过递归遍历生成树状结构的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Go素数筛选分析详解

    Go素数筛选分析详解

    学习Go语言的过程中,遇到素数筛选的问题。这是一个经典的并发编程问题,是某大佬的代码,短短几行代码就实现了素数筛选,这篇文章主要介绍了Go素数筛选分析,需要的朋友可以参考下
    2022-10-10
  • Go微服务项目配置文件的定义和读取示例详解

    Go微服务项目配置文件的定义和读取示例详解

    这篇文章主要为大家介绍了Go微服务项目配置文件的定义和读取示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06

最新评论