本文永久链接 – https://tonybai.com/2024/12/26/exploring-the-connection-establish-process-of-webrtc-app-built-with-pion

在《WebRTC第一课:从信令、ICE到NAT穿透的连接建立全流程》一文中,我们从理论层面全面细致地了解了WebRTC连接建立的完整流程。这个流程大致可以分为以下几个阶段:

  • 与信令服务器的交互
  • ICE候选项的采集、交换与排序
  • 形成ICE候选检查表、进行连通性检查,并最终确定最优候选路径

这个过程的复杂性不言而喻。即便多次阅读全文,读者可能仍难以形成深入的理解。因此,如果能够配上一个真实的示例,相信会更有助于读者全面把握这一过程的细节和原理。

在这篇文章中,我就为大家呈现一个真实的示例,我将使用Go语言开源WebRTC项目pion/webrtc来实现一个基于datachannel的WebRTC演示版程序,通过将pion/webrtc的日志级别设置为TRACE级,输出更多pion/webrtc实现层面的日志,以帮助大家理解WebRTC建连过程。同时,我还会实现一个简易版的基于“Room抽象模型”的信令服务器,供WebRTC通信两端交换信息使用。希望该示例能帮助大家更好的理解WebRTC端到端的建连流程。

按照WebRTC建连的流程,我们先来实现一个简易版的信令服务器。

注:提醒各位读者,本文中所有例子均以演示和帮助大家理解为目的,不建议在生产中使用示例中的代码。

1. 信令服务器(signaling-server)

下面是一个基于WebSocket的WebRTC信令服务器的简化实现,使用WebSocket进行WebRTC信令交换可以提供更快速、更高效和更灵活的通信体验,同时WebSocket生态丰富,可复用的代码库有很多,实现起来也比较简单。

这个信令服务器是基于Room抽象模型的,因此其主要结构是一个Room结构体,代表一个聊天室。我们具体看一下该信令服务器的实现代码:

// webrtc-first-lesson/part2/signaling-server/main.go

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "sync"

    "github.com/gorilla/websocket"
)

type Room struct {
    Clients map[*websocket.Conn]bool
    mu      sync.Mutex
}

var (
    upgrader = websocket.Upgrader{
        CheckOrigin: func(r *http.Request) bool {
            return true
        },
    }
    rooms  = make(map[string]*Room)
    roomMu sync.Mutex
)

func main() {
    http.HandleFunc("/ws", handleWebSocket)
    log.Println("Signaling server starting on :28080")
    log.Fatal(http.ListenAndServe(":28080", nil))
}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println("Error upgrading to WebSocket:", err)
        return
    }
    defer conn.Close()

    remoteAddr := conn.RemoteAddr().String()
    log.Println("New WebSocket connection from:", remoteAddr)

    roomID := r.URL.Query().Get("room")
    if roomID == "" {
        roomID = fmt.Sprintf("room_%d", len(rooms)+1)
        log.Printf("Created new room: %s\n", roomID)
    }

    roomMu.Lock()
    room, exists := rooms[roomID]
    if !exists {
        room = &Room{Clients: make(map[*websocket.Conn]bool)}
        rooms[roomID] = room
    }
    roomMu.Unlock()

    room.mu.Lock()
    room.Clients[conn] = true
    room.mu.Unlock()

    log.Printf("Client[%v] joined room %s\n", remoteAddr, roomID)

    for {
        messageType, message, err := conn.ReadMessage()
        if err != nil {
            log.Println("Error reading message:", err)
            break
        }

        var msg map[string]interface{}
        if err := json.Unmarshal(message, &msg); err != nil {
            log.Println("Error unmarshaling message:", err)
            continue
        }

        msg["roomId"] = roomID
        updatedMessage, _ := json.Marshal(msg)

        room.mu.Lock()
        for client := range room.Clients {
            if client != conn {
                clientAddr := client.RemoteAddr().String()
                if err := client.WriteMessage(messageType, updatedMessage); err != nil {
                    log.Println("Error writing message:", err)
                } else {
                    log.Printf("writing message to client[%v] ok\n", clientAddr)
                }
            }
        }
        room.mu.Unlock()
    }

    room.mu.Lock()
    delete(room.Clients, conn)
    room.mu.Unlock()
    log.Printf("Client[%v] left room %s\n", remoteAddr, roomID)
}

我们看到:Room结构体包含一个WebSocket连接的map和一个互斥锁。演示程序使用全局变量rooms(房间map)和相应的互斥锁管理房间和加入房间的连接,并在房间内进行消息广播,以保证消息能转发到参与通信的所有端(Peer)。当然,如果仅有两端在一个房间中,那么这就变成了一对一的实时通信。

这个信令服务器程序启动后,默认监听28080端口,当客户端连接时,会根据URL参数来将客户端连接加入到某个房间,如果房间号参数为空,则代表该客户端期望创建一个房间。先创建房间并加入的客户端作为answer端,等待offer端的连接。当从某个客户端连接收到消息后,会广播给房间内的其他客户端。当客户端断开连接时,便将其从房间中移除。

当然这仅是一个演示版程序,并未对历史建立的房间进行回收,同时也没有进行身份认证等安全方面的控制。

接下来,我们再来看看借助信令服务器进行端到端实时通信的端侧WebRTC应用的实现。

2. 端侧WebRTC应用(webrtc-peer)

WebRTC应用的代码通常都很“样板化”。在开发WebRTC应用程序时,信令连接、设置本地和远程描述、收集ICE候选以及转发信令消息等步骤都是一些常见且重复性较高的任务。这些步骤在不同的WebRTC应用程序中通常都大同小异。以下是这些重复性任务的一些具体步骤示例:

1) 信令连接处理
– 创建信令通道(如WebSocket连接)
– 监听连接建立、断开等事件
– 通过信令通道交换offer/answer等信令消息

2) 本地和远程描述设置
– 创建c实例
– 设置本地描述(createOffer/createAnswer)
– 设置远程描述(setRemoteDescription)

3) ICE 候选收集与交换
– 监听ICE候选事件,收集本地ICE候选
– 通过信令通道交换ICE候选信息
– 将远程ICE候选添加到RTCPeerConnection实例

4) 信令消息转发
– 接收来自远程的信令消息
– 根据消息类型,转发给本地RTCPeerConnection实例

这些基本步骤在大多数WebRTC应用程序中都是必需的。我们的示例代码也不例外,下面就是webrtc-peer程序源码,有些长,也很繁琐:

// webrtc-first-lesson/part2/webrtc-peer/main.go

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "log"
    "time"

    "github.com/gorilla/websocket"
    "github.com/pion/logging"
    "github.com/pion/webrtc/v3"
)

type signalMsg struct {
    Type string `json:"type"`
    Data string `json:"data"`
}

var (
    signalingServer string
    roomID          string
)

func init() {
    flag.StringVar(&signalingServer, "server", "ws://localhost:28080/ws", "Signaling server WebSocket URL")
    flag.StringVar(&roomID, "room", "", "Room ID (leave empty to create a new room)")
    flag.Parse()
}

func main() {
    // Connect to signaling server
    signalingURL := fmt.Sprintf("%s?room=%s", signalingServer, roomID)
    conn, _, err := websocket.DefaultDialer.Dial(signalingURL, nil)
    if err != nil {
        log.Fatal("Error connecting to signaling server:", err)
    }
    defer conn.Close()
    log.Println("connect to signaling server ok")

    // Create a new RTCPeerConnection
    config := webrtc.Configuration{
        ICEServers: []webrtc.ICEServer{
            {
                URLs: []string{"stun:stun.l.google.com:19302"},
            },
        },
    }

    // 创建一个自定义的日志工厂
    loggerFactory := logging.NewDefaultLoggerFactory()
    loggerFactory.DefaultLogLevel = logging.LogLevelTrace
    //loggerFactory.DefaultLogLevel = logging.LogLevelInfo
    //loggerFactory.DefaultLogLevel = logging.LogLevelDebug

    // Enable detailed logging
    s := webrtc.SettingEngine{}
    s.LoggerFactory = loggerFactory
    s.SetICETimeouts(5*time.Second, 5*time.Second, 5*time.Second)

    api := webrtc.NewAPI(webrtc.WithSettingEngine(s))
    peerConnection, err := api.NewPeerConnection(config)
    if err != nil {
        log.Fatal(err)
    }

    // Create a datachannel
    dataChannel, err := peerConnection.CreateDataChannel("test", nil)
    if err != nil {
        log.Fatal(err)
    }

    dataChannel.OnOpen(func() {
        log.Println("Data channel is open")
        go func() {
            for {
                err := dataChannel.SendText("Hello from " + roomID)
                if err != nil {
                    log.Println(err)
                }
                time.Sleep(5 * time.Second)
            }
        }()
    })

    dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
        log.Printf("Received message: %s\n", string(msg.Data))
    })

    // Set the handler for ICE connection state
    peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
        log.Printf("ICE Connection State has changed: %s\n", connectionState.String())
    })

    // Set the handler for Peer connection state
    peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
        log.Printf("Peer Connection State has changed: %s\n", s.String())
    })

    // Set the handler for Signaling state
    peerConnection.OnSignalingStateChange(func(s webrtc.SignalingState) {
        log.Printf("Signaling State has changed: %s\n", s.String())
    })

    // Register data channel creation handling
    peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
        log.Printf("New DataChannel %s %d\n", d.Label(), d.ID())

        d.OnOpen(func() {
            log.Printf("Data channel '%s'-'%d' open.\n", d.Label(), d.ID())
        })

        d.OnMessage(func(msg webrtc.DataChannelMessage) {
            log.Printf("Message from DataChannel '%s': '%s'\n", d.Label(), string(msg.Data))
        })
    })

    // Set the handler for ICE candidate generation
    peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) {
        if i == nil {
            return
        }

        candidateString, err := json.Marshal(i.ToJSON())
        if err != nil {
            log.Println(err)
            return
        }

        if writeErr := conn.WriteJSON(&signalMsg{
            Type: "candidate",
            Data: string(candidateString),
        }); writeErr != nil {
            log.Println(writeErr)
        }
    })

    // Handle incoming messages from signaling server
    go func() {
        for {
            _, rawMsg, err := conn.ReadMessage()
            if err != nil {
                log.Println("Error reading message:", err)
                return
            }
            log.Println("recv msg from signaling server")

            var msg signalMsg
            if err := json.Unmarshal(rawMsg, &msg); err != nil {
                log.Println("Error parsing message:", err)
                continue
            }
            log.Println("recv msg is", msg)

            switch msg.Type {
            case "offer":
                log.Println("recv a offer msg")
                offer := webrtc.SessionDescription{}
                if err := json.Unmarshal([]byte(msg.Data), &offer); err != nil {
                    log.Println("Error parsing offer:", err)
                    continue
                }

                if err := peerConnection.SetRemoteDescription(offer); err != nil {
                    log.Println("Error setting remote description:", err)
                    continue
                }

                answer, err := peerConnection.CreateAnswer(nil)
                if err != nil {
                    log.Println("Error creating answer:", err)
                    continue
                }

                if err := peerConnection.SetLocalDescription(answer); err != nil {
                    log.Println("Error setting local description:", err)
                    continue
                }

                answerString, err := json.Marshal(answer)
                if err != nil {
                    log.Println("Error encoding answer:", err)
                    continue
                }

                if err := conn.WriteJSON(&signalMsg{
                    Type: "answer",
                    Data: string(answerString),
                }); err != nil {
                    log.Println("Error sending answer:", err)
                }
                log.Println("send answer ok")

            case "answer":
                log.Println("recv a answer msg")
                answer := webrtc.SessionDescription{}
                if err := json.Unmarshal([]byte(msg.Data), &answer); err != nil {
                    log.Println("Error parsing answer:", err)
                    continue
                }

                if err := peerConnection.SetRemoteDescription(answer); err != nil {
                    log.Println("Error setting remote description:", err)
                }
                log.Println("set remote desc for answer ok")

            case "candidate":
                candidate := webrtc.ICECandidateInit{}
                if err := json.Unmarshal([]byte(msg.Data), &candidate); err != nil {
                    log.Println("Error parsing candidate:", err)
                    continue
                }

                if err := peerConnection.AddICECandidate(candidate); err != nil {
                    log.Println("Error adding ICE candidate:", err)
                }
                log.Println("adding ICE candidate:", candidate)
            }
        }
    }()

    // Create an offer if we are the peer to join the room
    if roomID != "" {
        offer, err := peerConnection.CreateOffer(nil)
        if err != nil {
            log.Fatal(err)
        }

        if err := peerConnection.SetLocalDescription(offer); err != nil {
            log.Fatal(err)
        }

        offerString, err := json.Marshal(offer)
        if err != nil {
            log.Fatal(err)
        }

        if err := conn.WriteJSON(&signalMsg{
            Type: "offer",
            Data: string(offerString),
        }); err != nil {
            log.Fatal(err)
        }
        log.Printf("send offer to signaling server ok\n")
    }

    // Wait forever
    select {}
}

通过代码,我们看到:这个使用Go实现的WebRTC对等连接示例程序通过WebSocket与信令服务器通信,创建和管理RTCPeerConnection,处理ICE候选、offer和answer,并实现了数据通道功能。程序支持创建新房间或加入现有房间,展示了完整的WebRTC连接建立流程,包括信令交换和ICE处理。它通过对pion/webrtc的日志级别设置让其具有详细的日志记录能力,这为我们后续通过日志分别WebRTC建连各个阶段奠定了基础。

3. 建立连接流程的日志分析

下面是实验环境的拓扑图:

webrtc-peer分别位于两台服务器上,其中Host A是一台位于NAT后面的内网主机,而HOST B则是一台位于美国的公网主机,信令服务器搭建在HOST B上,stun服务器使用的是Google提供的公网免费stun server。

下面是信令服务器和两端peer服务器的编译和启动步骤:

我们先启动信令服务器:

//在Host B上signaling-server目录下
$make
$./signaling-server
2024/08/20 21:45:50 Signaling server starting on :28080

接下来,启动Host A上的webrtc-peer程序:

//在Host A上webrtc-peer目录下
$make
$./webrtc-peer -server ws://206.189.166.16:28080/ws

这时信令服务器就会发现有新的websocket连入,并创建了room_6(这只是多次运行中的某一次的room id罢了):

2024/08/20 21:48:52 New WebSocket connection from: 47.93.3.95:17355
2024/08/20 21:48:52 Created new room: room_6
2024/08/20 21:48:52 Client[47.93.3.95:17355] joined room room_6

然后我们启动Host B上的webrtc-peer程序,将这一端加入到上面创建的room_6中:

//在Host B上webrtc-peer目录下
$make
$./webrtc-peer -room room_6 -server ws://206.189.166.16:28080/ws

这之后,信令服务器也会发现Host B上的webrtc-peer的连接。之后便开始从信令交互开始逐步实现端到端的建连。以下是对各个阶段产生的详细日志的分析:

3.1 信令服务连接(房间加入)和SDP 交互

  1. 信令连接
"2024/08/20 21:45:48 connect to signaling server ok"

以上日志表示成功连接到信令服务器。如果房间号为空,则该peer(answer)先启动并在信令服务器建立房间,然后另一个peer(offer)加入该房间,通过信令服务器交换信息。

  1. SDP交互

下面日志则是表示接收到另一个peer的offer SDP:

"2024/08/20 21:45:55 recv msg is {offer {"type":"offer","sdp":"v=0\r\no=- 2149168073199454578 1724143555 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=msid-semantic:WMS*\r\na=fingerprint:sha-256 A6:D6:AE:F3:30:0D:D8:07:D2:23:C9:A5:69:27:F2:CC:B1:8C:A4:DB:30:79:E7:62:9B:09:87:B7:68:1F:55:A7\r\na=extmap-allow-mixed\r\na=group:BUNDLE 0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 0.0.0.0\r\na=setup:actpass\r\na=mid:0\r\na=sendrecv\r\na=sctp-port:5000\r\na=ice-ufrag:TYfjBFmqpgGEtKbh\r\na=ice-pwd:NGdAyXsOgVwFfzXnlLmNrcWrBgJWFceB\r\n"}}

其中”recv a offer msg”表示程序识别到收到了offer消息。而”offer := webrtc.SessionDescription{}”及后续代码则是处理offer,创建answer并发送回给另一个peer。

在WebRTC中,信令服务器用于交换SDP(Session Description Protocol)信息,SDP描述了连接的媒体信息,如编解码器、IP 地址、端口等。先启动的peer创建房间,等待offer,后加入的peer发送offer后,等待answer的回复,双方通过信令服务器交换这些信息以建立连接。

接下来,便是两端的ICE流程。

3.2 ice Candidate Gathering、Candidate Priorization以及candidate的排序列表

下面一行日志表示开始收集ICE 候选者,这里是一个host类型的候选者:

"2024/08/20 21:45:55 adding ICE candidate: {candidate:3384150427 1 udp 2130706431 206.189.166.16 52256 typ host 0xc000210230 0xc0002121fe <nil>}"

后续有多个类似的日志,分别添加不同类型的候选者,如 host、srflx(Server Reflexive)等:

2024/08/20 21:45:55 adding ICE candidate: {candidate:604015337 1 udp 2130706431 10.46.0.5 38367 typ host 0xc000210260 0xc000212250 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:3019421960 1 udp 2130706431 2604:a880:2:d0::2094:3001 48394 typ host 0xc000210290 0xc000212298 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:2090009598 1 udp 2130706431 10.0.0.1 58895 typ host 0xc0002102d0 0xc0002122e0 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:233762139 1 udp 2130706431 172.17.0.1 58343 typ host 0xc000210300 0xc000212328 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:2943811937 1 udp 1694498815 2604:a880:2:d0::2094:3001 40480 typ srflx raddr :: rport 40480 0xc00038c070 0xc00038e050 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:2614874796 1 udp 1694498815 206.189.166.16 38534 typ srflx raddr 0.0.0.0 rport 38534 0xc000210760 0xc000212b98 <nil>}

不过,在输出的日志中,我们看到并没有明确输出我们期待的经过 Candidate Priorization(候选者优先级排序)后的候选者排序列表。

注:重温一下ICE(Interactive Connectivity Establishment),这是一种用于在两个peer之间建立连接的协议,通过收集各种类型的候选者(如 host 表示本机地址、srflx 表示通过 NAT 反射得到的地址等),增加连接成功的可能性。

3.3 ice connectivity check的各个子阶段

  1. 子阶段1:输出每一端的角色

在ICE连接中,会确定一个controlling方和一个controlled方,用于决定连接的发起和响应顺序。 下面这行输出日志表示本端不是controlling方:

"ice DEBUG: 21:45:55.401065 agent.go:395: Started agent: isControlling? false, remoteUfrag: "TYfjBFmqpgGEtKbh", remotePwd: "NGdAyXsOgVwFfzXnlLmNrcWrBgJWFceB"
  1. 子阶段2:每端输出形成的检查列表(Forming checklist)

这个阶段日志中没有明确输出检查列表,但日志中有大量的“Ping STUN from… to…”表示正在进行连接检查,这些日志汇总在一起可以看成是形成的检查列表。例如:

ice TRACE: 21:45:55.401676 agent.go:999: Ping STUN from udp4 host 172.17.0.1:7115 to udp4 host 206.189.166.16:52256。

每一端都会通过发送STUN请求来检查不同候选者之间的连接性。

  1. 子阶段3: 输出针对每个列表项的连接检查结果

日志中有很多类似的日志表示收到了来自特定候选者的成功响应:

"ice TRACE: 21:45:55.563530 selection.go:229: Inbound STUN (SuccessResponse) from udp4 host 206.189.166.16:52256 to udp4 host 172.17.0.1:7115"

根据连接检查的结果,如果发现Peer Reflexive 候选,也会有相应的日志输出,比如:

ice DEBUG: 21:45:25.771665 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:61194
ice DEBUG: 21:45:25.772355 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:26408
ice DEBUG: 21:45:25.775320 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:40491
ice DEBUG: 21:45:25.776894 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:5767
ice DEBUG: 21:45:25.777018 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:61432
... ...

3.4 NAT穿透尝试并输出最终的最佳候选者对

日志中大量的”Ping STUN”和”Inbound STUN (SuccessResponse)”表示正在进行 NAT 穿透尝试。例如:

ice TRACE: 21:45:55.401676 agent.go:999: Ping STUN from udp4 host 172.17.0.1:7115 to udp4 host 206.189.166.16:52256

ice TRACE: 21:45:55.563530 selection.go:229: Inbound STUN (SuccessResponse) from udp4 host 206.189.166.16:52256 to udp4 host 172.17.0.1:7115

通过STUN请求和响应来确定是否能够穿透NAT,如果穿透失败,则将其标记为failed:

ice TRACE: 21:45:56.274839 agent.go:550: Maximum requests reached for pair prio 9151314440652587007 (local, prio 2130706431) udp4 host 172.18.0.1:59520 <-> udp4 host 10.0.0.1:58895 (remote, prio 2130706431), state: in-progress, nominated: false, nominateOnBindingSuccess: false, marking it as failed

如果能够成功穿透,则可以建立连接。下面的日志表示选出了最终的最佳候选者对:

ice TRACE: 21:45:56.656900 agent.go:524: Set selected candidate pair: prio 9151314440652587007 (local, prio 2130706431) udp4 host 192.168.10.1:60662 <-> udp4 host 206.189.166.16:52256 (remote, prio 2130706431), state: succeeded, nominated: true, nominateOnBindingSuccess: false
ice TRACE: 21:45:56.823017 selection.go:239: Found valid candidate pair: prio 9151314440652587007 (local, prio 2130706431) udp4 host 192.168.10.1:60662 <-> udp4 host 206.189.166.16:52256 (remote, prio 2130706431), state: succeeded, nominated: true, nominateOnBindingSuccess: false

一旦确定了最佳候选者对,连接就算建立成功了!

接下来,就是打开datachannel通道并进行数据传输了!

3.5 data channel打开以及定时(5秒一次)的数据传输

下面日志表示数据通道已打开:

"Data channel is open"

下面日志表示创建了一个名为“test”的数据通道:

"New DataChannel test 824638605290"

下面日志表示数据通道打开成功:

"Data channel 'test'-'824638605290' open"

示例代码中,启动一个goroutine用于定时向data channel发送数据,当出现下面日志时,表示接收到来自另一个 peer 的数据:

"Message from DataChannel 'test': 'Hello from room_6'"

4. 小结

在这篇文章中,我通过使用Go语言开源项目pion/webrtc实现的webrtc端侧应用,为大家详细展示了WebRTC应用的建连过程。

首先,我实现了一个基于WebSocket的简易信令服务器。这个信令服务器基于Room抽象模型,使用全局变量来管理房间和连接,并进行消息广播。

接下来,我介绍了端侧WebRTC应用的实现。这个应用通过与信令服务器通信,创建RTCPeerConnection,处理ICE候选、offer和answer,以及实现数据通道功能。我还通过设置TRACE日志级别,展示了详细的建连流程。

之后,我在实验环境的实际执行了上述程序,并通过对日志的分析展示了建连过程。这些分析涵盖了信令服务连接和SDP交互、ICE候选收集与优先级排序、ICE 连通性检查各子阶段、NAT穿透尝试及最佳候选者对确定,以及数据通道打开和数据传输。希望这样的分析可以帮助大家更深刻的理解和体会建连过程。

WebRTC网络结构和建连就先讲到这里,后面的系列文章中,我们会开始聚焦WebRTC技术栈的另外一个主要方面:音视频质量,包括编码器以及媒体流处理等。

本文涉及的Go源码在这里可以下载到 – https://github.com/bigwhite/experiments/blob/master/webrtc-first-lesson/part2


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

© 2024, bigwhite. 版权所有.

Related posts:

  1. WebRTC第一课:从信令、ICE到NAT穿透的连接建立全流程
  2. WebRTC第一课:网络架构与NAT工作原理
  3. 使用Go和WebRTC data channel实现端到端实时通信
  4. 探索Docker默认网络NAT映射的分配与过滤行为
  5. 如何在Go语言中使用Websockets:最佳工具与行动指南