基于Go和Pion的WebRTC data channel应用示例
参考文档:
https://github.com/bigwhite/experiments/tree/master/webrtc-data-channel
https://tonybai.com/2023/09/23/p2p-rtc-implementation-with-go-and-webrtc-data-channel/
WebRTC技术栈十分复杂,日常WebRTC应用开发时,我们一般会基于开源的实现进行开发。Go语言在WebRTC开发领域也有比较成熟的开源项目,如Pion。Pion提供了纯Go实现的WebRTC API实现以及WebRTC相关组件实现,使用Pion可以帮助我们快速高效开发WebRTC服务器和客户端应用。
pion: 纯Go的WebRTC实现
根据pion之父的说法,pion的诞生源于用WebRTC构建东西的挫败感,这种挫败感来源于Google开源的首个webrtc实现libwebrtc,因为将libwebrtc构建和运行起来似乎十分困难。
pion就是根据libwebrtc的教训而设计的,pion给开发者的第一印象就是它十分容易构建和运行起来。这一定程度要归功于pion是用Go编写的,更模块化,也更透明,并且pion之父最初便考虑了将其用在Chromium之外的应用中。
pion是一个纯粹的WebRTC软件的Go集合, 涵盖了WebRTC项目中需要的所有主要元素:
同时,pion项目还为WebRTC开发者贡献了一本非常好的WebRTC资料《WebRTC For The Curious》,很值得一读。另外,pion项目的examples也十分丰富,非常利于初学者快速掌握WebRTC以及如何使用pion开发WebRTC应用。
下面我们就基于pion的webrtc实现项目开发一个基于data channel的端到端实时通信示例。
根据之前对WebRTC建立过程的说明,我们首先需要设计一下这个示例的信令服务器以及信令协议。
信令服务与协议设计
信令服务器在WebRTC通信中扮演协调者的角色。它传递客户端的媒体参数和连接候选信息。
我们的业务模型是,信令服务器维护一个被动连接的peer集合,这个集合中的peer是在这些peer在启动时通过register信令注册到信令服务器中的,每个peer有一个唯一的ID,我称这个集合为answer peer集合吧。主动连接方(这里称为offer peer)则通过ID去连接answer peer。一旦建立与某个peer的连接后,它们便可以通过建立的data channel全双工的实时通信了。下面是信令服务与offer peer和answer peer的信令交互图:
参照前面提到的WebRTC建连过程,你可以很容易的看懂这个协议设计。
这里我设计了一个Message抽象来表示信令服务可以收发的消息:
//webrtc-data-channel/signaling/proto/proto.go
type Message struct {
Cmd int `json:"command"`
Payload []byte `json:"payload"` // carry all kinds of request and response
}
其中的Cmd字段标识Message类型,可选值如下:
//webrtc-data-channel/signaling/proto/proto.go
const (
// originated from answer peer
CmdInit = iota + 1
CmdAnswer
// originated from answer peer
CmdOffer
// from both peer
CmdCandidate
)
const (
CmdInitResp = iota + 101 // CmdInit + 100
CmdAnswerResp
CmdOfferResp
CmdCandidateResp
)
Message既可以承载Request,亦可以承载Response。Message的Payload字段中存放的是Request或Response序列化后的结果。Request和Response结构如下:
//webrtc-data-channel/signaling/proto/proto.go
// Request is one kind of payload for Message
type Request struct {
SourceID string `json:"source"`
TargetID string `json:"target"`
Body []byte `json:"body"` // carry register, offer, answer, candidate
}
// Request is another payload for Message
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
Request类型的Body中存放的是WebRTC Offer/Answer的SDP以及ICE Candidate序列化后的结果。
此外,在这个示例中,我们使用WebSocket来作为信令协议的载体,便于信令服务器与offer peer/answer peer
进行双向通信。
信令服务器的实现
按照上述设计,我们的信令服务器就是一个websocket的server:
//webrtc-data-channel/signaling/main.go
func main() {
flag.Parse()
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
http.HandleFunc("/register", register) // for peerAnswer
http.HandleFunc("/offer", offer) // for peerOffer
log.Fatal(http.ListenAndServe(*addr, nil))
}
在这个server中我们提供了两个endpoint,一个是/register,供answer peer建立连接使用;另外一个是/offer,供offer peer与信令服务器建连并通信的。
两个endpoint对应的Handler的处理模式也相对一致,都是进入一个event loop中。
//webrtc-data-channel/signaling/main.go
func register(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil) // *websocket.Conn
if err != nil {
log.Print("signaling: websocket upgrade error:", err)
return
}
defer c.Close()
err = answerPeerEventLoop(c, w)
if err != nil {
log.Println("signaling: answerPeerEventLoop error:", err)
return
}
log.Println("signaling: answerPeerEventLoop exit")
}
func offer(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil) // *websocket.Conn
if err != nil {
log.Print("signaling: websocket upgrade error:", err)
return
}
defer c.Close()
err = offerPeerEventLoop(c, w)
if err != nil {
log.Println("signaling: offerPeerEventLoop error:", err)
return
}
log.Println("signaling: offerPeerEventLoop exit")
}
注:offer和register这两个Handler都会在单独的goroutine中执行。
offerPeerEventLoop和answerPeerEventLoop的代码都比较长,这里就不贴出来了。这两个函数的代码也都比较模式化,基本处理流程就是读取一个Message,判断Message的Cmd类型,然后根据Cmd类型分别处理,处理的逻辑参见上面信令服务器的信令处理流程:基本上就是转发、转发、转发。
answer peer的实现
answer peer启动后会建立RTCPeerConnection类型实例,并设置RTCPeerConnection实例的事件处理函数:
- OnICECandidate 本地收集到ICE候选者信息,处理动作是将这些ICE候选者信息通过信令服务转发到对端。
- OnConnectionStateChange 当与对端的连接状态发生变化时触发,比如连接建立、连接断开时。处理动作仅为输出相应的日志。
- OnDataChannel 当与对端的Data Channel创建成功时,处理逻辑是注册DataChannel.OnOpen和DataChannel.OnMessage两个事件处理函数。
完成这些后,answer peer会向上面设计的那样,与信令服务器建立连接,并发送请求到信令服务的/register端点,然后进入event loop。在event loop中负责处理信令服务器转发过来的Offer、Candidate等信息,以及各种信令服务器返回的Response。
当收到Offer时,answer peer会创建Answer并发给信令服务器;当收到Candidate时,会调用AddICECandidate将Candidate信息添加到peerConnection中,供后续配对使用。后续WebRTC连接自动建立后,便可以通过data channel收发数据了。
answer peer的代码较长,大家可以自行到https://github.com/bigwhite/experiments/tree/master/webrtc-data-channel/answer阅读。
注:answer peer的代码改编自pion/webrtc项目的pion-to-pion/answer示例。
offer peer的实现
offer peer的实现与answer相似。
offer peer启动后会建立RTCPeerConnection类型实例,并设置RTCPeerConnection实例的事件处理函数:
- OnICECandidate
- OnConnectionStateChange
- DataChannel的OnOpen
- DataChannel的OnMessage
offer peer会主动创建DataChannel,然后与信令服务器建立连接,并发送请求到信令服务的/offer端点并主动向信令服务器发送Offer,最后进入event loop。在event loop中负责处理信令服务器转发过来的Answer、Candidate等信息,以及各种信令服务器返回的Response。
当收到Answer时,offer peer会将Answer中携带的SDP传给SetRemoteDescription,同时调用SetLocalDescription开启ICE候选者的收集过程;当收到Candidate时,会调用AddICECandidate将Candidate信息添加到peerConnection中,供后续配对使用。后续WebRTC连接自动建立后,便可以通过data channel收发数据了。
offer peer的代码较长,大家可以自行到https://github.com/bigwhite/experiments/tree/master/webrtc-data-channel/offer阅读。
注:offer peer的代码改编自pion/webrtc项目的pion-to-pion/offer示例。
运行示例
来运行一下这个示例。
先来启动信令服务器:
$cd webrtc-data-channel/signaling
$go run main.go
启动answer peer:
$cd webrtc-data-channel/answer
$go run main.go
2023/09/23 21:24:45.201213 answer: NewPeerConnection ok
2023/09/23 21:24:45.201256 answer: connecting to ws://localhost:18080/register
2023/09/23 21:24:45.203993 answer: recv resp[101]: proto.Response{Code:0, Msg:"ok"}
这时我们会从信令服务器的输出日志中看到:
2023/09/23 21:24:45.203702 signaling: add answer peer: answer-peer-1
我们看到,answer peer成功注册到信令服务器中了,其ID为answer-peer-1。
下面我们来启动offer peer,其要连接的target为answer-peer-1:
$cd webrtc-data-channel/offer
$go run main.go -target answer-peer-1
2023/09/23 21:25:26.462845 offer: new peerConnection ok
2023/09/23 21:25:26.462880 offer: create new channel
2023/09/23 21:25:26.462890 offer: connecting to ws://localhost:18080/offer
2023/09/23 21:25:26.464863 offer: create offer
2023/09/23 21:25:26.465131 offer: recv resp[103]: proto.Response{Code:0, Msg:"ok"}
2023/09/23 21:25:26.465957 offer: recv answer(sdp) message from answer-peer-1
2023/09/23 21:25:26.466064 offer: set local desc
2023/09/23 21:25:26.466099 offer: set remote desc
2023/09/23 21:25:26.466201 offer: Peer Connection State has changed: connecting
2023/09/23 21:25:26.466297 offer: recv candidate message from answer-peer-1
2023/09/23 21:25:26.466344 offer: invoke peerConnection.OnICECandidate: webrtc.ICECandidate{statsID:"candidate:KsXlIk2JNeiDqK3l+znsoB3sDwuh1/2x", Foundation:"4104056053", Priority:0x7effffff, Address:"192.168.1.105", Protocol:1, Port:0xc2b1, Typ:1, Component:0x1, RelatedAddress:"", RelatedPort:0x0, TCPType:""}
2023/09/23 21:25:26.466506 offer: recv resp[104]: proto.Response{Code:0, Msg:"ok"}
2023/09/23 21:25:26.468342 offer: Peer Connection State has changed: connected
2023/09/23 21:25:26.469105 offer: Data channel 'data'-'824634439080' open. Random messages will now be sent to any connected DataChannels every 5 seconds
2023/09/23 21:25:26.859774 offer: recv candidate message from answer-peer-1
2023/09/23 21:25:31.469811 offer: Sending 'offer-1013426535'
2023/09/23 21:25:31.470846 offer: Message from DataChannel 'data': 'answer-695102175'
2023/09/23 21:25:36.469653 offer: Sending 'offer-2065047193'
2023/09/23 21:25:36.470495 offer: Message from DataChannel 'data': 'answer-750781464'
2023/09/23 21:25:41.469603 offer: Sending 'offer-153497802'
2023/09/23 21:25:41.469938 offer: Message from DataChannel 'data': 'answer-2102723687'
2023/09/23 21:25:46.469504 offer: Sending 'offer-1287609150'
2023/09/23 21:25:46.470097 offer: Message from DataChannel 'data': 'answer-645051512'
2023/09/23 21:25:51.470078 offer: Sending 'offer-1486812657'
2023/09/23 21:25:51.470572 offer: Message from DataChannel 'data': 'answer-1325372035'
offer peer的启动引发了“连锁反应”,在信令服务器的帮助下,offer peer与answer peer成功建立了连接,并在打开的Data Channel进行着“定时”的双工实时通信。
信令服务器的输出日志如下:
2023/09/23 21:25:26.465049 signaling: recv request[3] from offer peer
2023/09/23 21:25:26.465070 signaling: send offer resp ok
2023/09/23 21:25:26.465073 signaling: add offer peer: offer-peer-1
2023/09/23 21:25:26.465085 signaling: forward request[3] to answer peer ok
2023/09/23 21:25:26.465247 signaling: recv offer response from answer peer
2023/09/23 21:25:26.465868 signaling: recv request[2] from answer peer
2023/09/23 21:25:26.465896 signaling: forward request[2] to offer peer[offer-peer-1] ok
2023/09/23 21:25:26.466003 signaling: recv answer response from offer peer
2023/09/23 21:25:26.466218 signaling: recv request[4] from answer peer
2023/09/23 21:25:26.466245 signaling: forward request[4] to offer peer[offer-peer-1] ok
2023/09/23 21:25:26.466363 signaling: recv candidate response from offer peer
2023/09/23 21:25:26.466415 signaling: recv request[4] from offer peer
2023/09/23 21:25:26.466429 signaling: send offer resp ok
2023/09/23 21:25:26.466435 signaling: add offer peer: offer-peer-1
2023/09/23 21:25:26.466445 signaling: forward request[4] to answer peer ok
2023/09/23 21:25:26.466526 signaling: recv candidate response from answer peer
2023/09/23 21:25:26.859520 signaling: recv request[4] from answer peer
2023/09/23 21:25:26.859609 signaling: forward request[4] to offer peer[offer-peer-1] ok
2023/09/23 21:25:26.859951 signaling: recv candidate response from offer peer
answer peer的输出日志如下:
2023/09/23 21:25:26.465182 answer: recv offer message from offer-peer-1
2023/09/23 21:25:26.465823 answer: send sdp answer
2023/09/23 21:25:26.465834 answer: Peer Connection State has changed: connecting
2023/09/23 21:25:26.465925 answer: set local desc
2023/09/23 21:25:26.465928 answer: recv resp[102]: proto.Response{Code:0, Msg:"ok"}
2023/09/23 21:25:26.466108 answer: invoke peerConnection.OnICECandidate: 192.168.1.105
2023/09/23 21:25:26.466285 answer: recv resp[104]: proto.Response{Code:0, Msg:"ok"}
2023/09/23 21:25:26.466481 answer: recv candidate message from offer-peer-1
2023/09/23 21:25:26.468475 answer: Peer Connection State has changed: connected
2023/09/23 21:25:26.469002 answer: New DataChannel data 824634440046
2023/09/23 21:25:26.469049 answer: Data channel 'data'-'824634440046' open. Random messages will now be sent to any connected DataChannels every 5 seconds
2023/09/23 21:25:26.859199 answer: invoke peerConnection.OnICECandidate: 175.160.224.151
2023/09/23 21:25:26.859770 answer: recv resp[104]: proto.Response{Code:0, Msg:"ok"}
2023/09/23 21:25:31.470331 answer: Sending 'answer-695102175'
2023/09/23 21:25:31.470366 answer: message from DataChannel 'data': 'offer-1013426535'
2023/09/23 21:25:36.470028 answer: Sending 'answer-750781464'
2023/09/23 21:25:36.470123 answer: message from DataChannel 'data': 'offer-2065047193'
2023/09/23 21:25:41.469624 answer: Sending 'answer-2102723687'
2023/09/23 21:25:41.469978 answer: message from DataChannel 'data': 'offer-153497802'
2023/09/23 21:25:46.469606 answer: Sending 'answer-645051512'
2023/09/23 21:25:46.469883 answer: message from DataChannel 'data': 'offer-1287609150'
2023/09/23 21:25:51.470303 answer: Sending 'answer-1325372035'
2023/09/23 21:25:51.470421 answer: message from DataChannel 'data': 'offer-1486812657'
这次运行是在本地同一主机下运行的。你也可以将信令服务器搭建在公网主机上,然后将answer peer和offer peer分别放到不同的公有云虚机上,你看看是否依然可以连通!我在阿里云上的测试结果是ok的(信令服务器放在美国)。
注:示例中使用的stun server:74.125.137.127:19302实际上就是stun.l.google.com:19302。
ali 云测试(失败)
上示例再ali云运行: signaling
./bin/signaling
2023/12/14 16:21:37.559206 signaling: add answer peer: answer-peer-1
2023/12/14 16:22:35.461059 signaling: recv request[3] from offer peer
2023/12/14 16:22:35.461098 signaling: send offer resp ok
2023/12/14 16:22:35.461102 signaling: add offer peer: offer-peer-1
2023/12/14 16:22:35.461114 signaling: forward request[3] to answer peer ok
2023/12/14 16:22:35.469240 signaling: recv offer response from answer peer
2023/12/14 16:22:35.472114 signaling: recv request[2] from answer peer
2023/12/14 16:22:35.472145 signaling: forward request[2] to offer peer[offer-peer-1] ok
2023/12/14 16:22:35.473134 signaling: recv request[4] from answer peer
2023/12/14 16:22:35.473155 signaling: forward request[4] to offer peer[offer-peer-1] ok
2023/12/14 16:22:35.473221 signaling: recv request[4] from answer peer
2023/12/14 16:22:35.473235 signaling: forward request[4] to offer peer[offer-peer-1] ok
2023/12/14 16:22:35.473559 signaling: recv request[4] from answer peer
2023/12/14 16:22:35.473576 signaling: forward request[4] to offer peer[offer-peer-1] ok
2023/12/14 16:22:35.483580 signaling: recv answer response from offer peer
2023/12/14 16:22:35.484375 signaling: recv candidate response from offer peer
2023/12/14 16:22:35.484618 signaling: recv candidate response from offer peer
2023/12/14 16:22:35.485373 signaling: recv candidate response from offer peer
2023/12/14 16:22:35.485756 signaling: recv request[4] from offer peer
2023/12/14 16:22:35.485882 signaling: send offer resp ok
2023/12/14 16:22:35.485924 signaling: add offer peer: offer-peer-1
2023/12/14 16:22:35.485978 signaling: forward request[4] to answer peer ok
2023/12/14 16:22:35.486062 signaling: recv request[4] from offer peer
2023/12/14 16:22:35.486118 signaling: send offer resp ok
2023/12/14 16:22:35.486156 signaling: add offer peer: offer-peer-1
2023/12/14 16:22:35.486205 signaling: forward request[4] to answer peer ok
2023/12/14 16:22:35.486367 signaling: recv request[4] from offer peer
2023/12/14 16:22:35.486414 signaling: send offer resp ok
2023/12/14 16:22:35.486457 signaling: add offer peer: offer-peer-1
2023/12/14 16:22:35.486490 signaling: forward request[4] to answer peer ok
2023/12/14 16:22:35.487258 signaling: recv request[4] from offer peer
2023/12/14 16:22:35.487308 signaling: send offer resp ok
2023/12/14 16:22:35.487349 signaling: add offer peer: offer-peer-1
2023/12/14 16:22:35.487384 signaling: forward request[4] to answer peer ok
2023/12/14 16:22:35.493476 signaling: recv candidate response from answer peer
2023/12/14 16:22:35.493646 signaling: recv candidate response from answer peer
2023/12/14 16:22:35.494009 signaling: recv candidate response from answer peer
2023/12/14 16:22:35.494877 signaling: recv candidate response from answer peer
2023/12/14 16:22:35.662658 signaling: recv request[4] from answer peer
2023/12/14 16:22:35.662837 signaling: forward request[4] to offer peer[offer-peer-1] ok
2023/12/14 16:22:35.674296 signaling: recv candidate response from offer peer
2023/12/14 16:22:35.687722 signaling: recv request[4] from offer peer
2023/12/14 16:22:35.687829 signaling: send offer resp ok
2023/12/14 16:22:35.687878 signaling: add offer peer: offer-peer-1
2023/12/14 16:22:35.687938 signaling: forward request[4] to answer peer ok
2023/12/14 16:22:35.695504 signaling: recv candidate response from answer peer
2023/12/14 16:23:05.558629 signaling: read message from offer peer error: websocket: close 1006 (abnormal closure): unexpected EOF
2023/12/14 16:23:05.558655 signaling: offerPeerEventLoop error: websocket: close 1006 (abnormal closure): unexpected EOF
2023/12/14 16:23:05.594757 signaling: read message from answer peer error: websocket: close 1006 (abnormal closure): unexpected EOF
2023/12/14 16:23:05.594770 signaling: answerPeerEventLoop error: websocket: close 1006 (abnormal closure): unexpected EOF
在内网1运行answer:
./bin/answer -signaling-address www.wl119.club:18080
2023/12/14 16:21:37.524731 answer: NewPeerConnection ok
2023/12/14 16:21:37.524766 answer: connecting to ws://www.wl119.club:18080/register
2023/12/14 16:21:37.558433 answer: recv resp[101]: proto.Response{Code:0, Msg:"ok"}
2023/12/14 16:22:35.460627 answer: recv offer message from offer-peer-1
2023/12/14 16:22:35.463568 answer: Peer Connection State has changed: connecting
2023/12/14 16:22:35.463612 answer: send sdp answer
2023/12/14 16:22:35.464012 answer: set local desc
2023/12/14 16:22:35.464434 answer: invoke peerConnection.OnICECandidate: 10.130.48.10
2023/12/14 16:22:35.464698 answer: invoke peerConnection.OnICECandidate: 192.168.0.201
2023/12/14 16:22:35.464985 answer: invoke peerConnection.OnICECandidate: 172.17.0.1
2023/12/14 16:22:35.471063 answer: recv resp[102]: proto.Response{Code:0, Msg:"ok"}
2023/12/14 16:22:35.472088 answer: recv resp[104]: proto.Response{Code:0, Msg:"ok"}
2023/12/14 16:22:35.472129 answer: recv resp[104]: proto.Response{Code:0, Msg:"ok"}
2023/12/14 16:22:35.472464 answer: recv resp[104]: proto.Response{Code:0, Msg:"ok"}
2023/12/14 16:22:35.484934 answer: recv candidate message from offer-peer-1
2023/12/14 16:22:35.485142 answer: recv candidate message from offer-peer-1
2023/12/14 16:22:35.485521 answer: recv candidate message from offer-peer-1
2023/12/14 16:22:35.486343 answer: recv candidate message from offer-peer-1
2023/12/14 16:22:35.654000 answer: invoke peerConnection.OnICECandidate: 156.59.84.78
2023/12/14 16:22:35.661716 answer: recv resp[104]: proto.Response{Code:0, Msg:"ok"}
2023/12/14 16:22:35.686974 answer: recv candidate message from offer-peer-1
2023/12/14 16:23:05.584535 answer: Peer Connection State has changed: failed
2023/12/14 16:23:05.584593 answer: Peer Connection has gone to failed exiting
在内网1运行offer:
./bin/offer -target=answer-peer-1 -signaling-address www.wl119.club:18080
2023/12/14 16:22:35.417444 offer: new peerConnection ok
2023/12/14 16:22:35.417689 offer: create new channel
2023/12/14 16:22:35.417876 offer: connecting to ws://www.wl119.club:18080/offer
2023/12/14 16:22:35.452730 offer: create offer
2023/12/14 16:22:35.464761 offer: recv resp[103]: proto.Response{Code:0, Msg:"ok"}
2023/12/14 16:22:35.475777 offer: recv answer(sdp) message from answer-peer-1
2023/12/14 16:22:35.476167 offer: set local desc
2023/12/14 16:22:35.476345 offer: set remote desc
2023/12/14 16:22:35.476729 offer: recv candidate message from answer-peer-1
2023/12/14 16:22:35.476867 offer: recv candidate message from answer-peer-1
2023/12/14 16:22:35.476582 offer: Peer Connection State has changed: connecting
2023/12/14 16:22:35.477459 offer: recv candidate message from answer-peer-1
2023/12/14 16:22:35.477735 offer: invoke peerConnection.OnICECandidate: webrtc.ICECandidate{statsID:"candidate:G0Wm0aJ82wHlzVYfSDzyHZDHxmGvT9lq", Foundation:"4239050546", Priority:0x7effffff, Address:"192.168.1.4", Protocol:1, Port:0xa423, Typ:1, Component:0x1, RelatedAddress:"", RelatedPort:0x0, TCPType:""}
2023/12/14 16:22:35.478033 offer: invoke peerConnection.OnICECandidate: webrtc.ICECandidate{statsID:"candidate:usFfnYv9NAYk9chQnI4Fk4Iwc5kmtNvD", Foundation:"3370325036", Priority:0x7effffff, Address:"2409:8a00:7890:88b0:f5a5:57f3:18ed:e897", Protocol:1, Port:0xed37, Typ:1, Component:0x1, RelatedAddress:"", RelatedPort:0x0, TCPType:""}
2023/12/14 16:22:35.478461 offer: invoke peerConnection.OnICECandidate: webrtc.ICECandidate{statsID:"candidate:EpS9sHUYM8SyFzp8PtMT+8S300yiqyZ1", Foundation:"1643992863", Priority:0x7effffff, Address:"2409:8a00:7890:88b0:f7e7:ef03:418d:9aca", Protocol:1, Port:0xd935, Typ:1, Component:0x1, RelatedAddress:"", RelatedPort:0x0, TCPType:""}
2023/12/14 16:22:35.478753 offer: invoke peerConnection.OnICECandidate: webrtc.ICECandidate{statsID:"candidate:FzPVfOVTb9PoDh2+ONFBQVK7hAMSX4iu", Foundation:"233762139", Priority:0x7effffff, Address:"172.17.0.1", Protocol:1, Port:0x95ff, Typ:1, Component:0x1, RelatedAddress:"", RelatedPort:0x0, TCPType:""}
2023/12/14 16:22:35.489468 offer: recv resp[104]: proto.Response{Code:0, Msg:"ok"}
2023/12/14 16:22:35.489547 offer: recv resp[104]: proto.Response{Code:0, Msg:"ok"}
2023/12/14 16:22:35.490731 offer: recv resp[104]: proto.Response{Code:0, Msg:"ok"}
2023/12/14 16:22:35.490808 offer: recv resp[104]: proto.Response{Code:0, Msg:"ok"}
2023/12/14 16:22:35.666470 offer: recv candidate message from answer-peer-1
2023/12/14 16:22:35.679505 offer: invoke peerConnection.OnICECandidate: webrtc.ICECandidate{statsID:"candidate:axiSYkTylbr3YeY5U+GDIsOnU3ug633j", Foundation:"4056666621", Priority:0x64ffffff, Address:"120.244.232.65", Protocol:1, Port:0x8fa9, Typ:2, Component:0x1, RelatedAddress:"0.0.0.0", RelatedPort:0xc85b, TCPType:""}
2023/12/14 16:22:35.691229 offer: recv resp[104]: proto.Response{Code:0, Msg:"ok"}
2023/12/14 16:23:05.549120 offer: Peer Connection State has changed: failed
2023/12/14 16:23:05.549222 offer: Peer Connection has gone to failed exiting