大话ion系列(一)
點(diǎn)擊上方“LiveVideoStack”關(guān)注我們
作者 | 王朋闖
本文為王朋闖老師創(chuàng)作的系列ion文章,LiveVideoStack已獲得授權(quán)發(fā)布,未來將持續(xù)更新。
一、為什么用ion-sfu
1.簡介
ion-sfu作為ion分布式架構(gòu)里的核心模塊,SFU是選擇轉(zhuǎn)發(fā)單元的簡稱,可以分發(fā)WebRTC的媒體流。ion-sfu從pion/ion拆分出來,經(jīng)過社區(qū)打磨,是目前GO方案中最成熟且使用最廣的SFU。
https://github.com/pion/ion
已經(jīng)有多家開始商用了,這點(diǎn)國外公司比較快,比如:100ms、Screenleap和Tandem等。
100ms:https://www.100ms.live/
Screenleap:https://www.screenleap.com/
Tandem:https://tandem.chat/
2.ion-sfu優(yōu)點(diǎn)
?
純GO,開發(fā)效率高,且能幫你繞過很多坑
單進(jìn)程多協(xié)程模型:
- 可以利用多核
-?大大降低級聯(lián)/單端口復(fù)雜度(其他SFU,可能存在本機(jī)不同worker間relay的問題;監(jiān)聽單端口時,存在worker間搶包的問題)
高并發(fā),曾在谷歌云4核壓測到單房間50方會議 (大概2500路流-0.5Mbps)
功能全面:
- 雙PeerConnection+多Track設(shè)計,有良好的瀏覽器兼容性,節(jié)省系統(tǒng)資源
-?支持多對多音視頻通信
-?支持大小流Simulcast
-?支持屏幕分享Screenshare
-?支持發(fā)言方自動檢測Audio-Level-Detect
-?支持定制DataChannel
-?支持節(jié)點(diǎn)間Relay
-?支持單端口,大大降低部署難度
-?完善的抗弱網(wǎng)機(jī)制,抗丟包40%左右,支持TWCC/REMB+PLI/FIR+Nack+SR/RR等
配套SDK完善,JS/Flutter/GO等
3.使用方式
ion-sfu使用方式有兩種:
作為服務(wù)使用,比如編譯帶grpc或jsonrpc信令的ion-sfu,然后再做一個自己的信令服務(wù)(推薦ion分布式套裝),遠(yuǎn)程調(diào)用即可。
作為包使用,import導(dǎo)入,然后做二次開發(fā)。此時拋棄了cmd下邊的信令層,只需導(dǎo)入pkg/sfu下邊的包即可,然后自行定制信令層,可以在sfu、session、peer層面,通過繼承接口定制自己的業(yè)務(wù),比較復(fù)雜。
二、架構(gòu)與模塊
上面給一個簡單架構(gòu)圖,很多細(xì)節(jié)表示不出來,需要看代碼。
1.簡介
得益于GO,ion-sfu整體代碼精簡,擁有極高的開發(fā)效率。結(jié)合現(xiàn)有SDK使用,可以避免很多坑:ion-sdk-js等。
ion-sfu基于pion/webrtc,所以代碼風(fēng)格偏標(biāo)準(zhǔn)webrtc,比如:PeerConnection。因為是使用了標(biāo)準(zhǔn)API,熟悉了之后很容易看懂其他工程,比如:ion-sdk-go/js/flutter。
這樣從前到后,整體門檻都降低了。
2.工程組織
這里給出主要模塊列表:
├── Makefile //用來編譯二進(jìn)制和grpc文件 ├── bin //編譯好的二進(jìn)制目錄 ├── cmd │ └── signal //包含三個主文件 grpc、jsonrpc、allrpc ├── config.toml //配置文件 ├── examples //網(wǎng)頁示例目錄 ├── pkg├── buffer //buffer包,用于緩存包├── logger //日志├── middlewares //中間件,主要是支持自定義datachannel├── relay //中繼├── sfu //sfu主模塊,包含router、session、peer等├── stats //狀態(tài)統(tǒng)計└── twcc //transport-cc3.信令層
信令代碼和主程序在一起,在cmd/signal/下邊。
支持jsonrpc,主要處理邏輯在:
支持grpc,主要處理邏輯在:
而allrpc,是jsonrpc和grpc的合體封裝,運(yùn)行時會進(jìn)入上面兩個函數(shù)。
信令很簡單:
join:加入一個session。
description:發(fā)起offer或回復(fù)answer,用于協(xié)商和重協(xié)商。
trickle:發(fā)送trickle candidate。
另外,出于簡單考慮,一些信令和事件,直接走datachannel了,比如:大小流切換、聲音檢測、自定義信令等。
4.媒體層
媒體層的主要模塊:
├── audioobserver.go //聲音檢測 ├── datachannel.go //dc中間件的封裝 ├── downtrack.go //下行track ├── helpers.go //工具函數(shù)集 ├── mediaengine.go //SDP相關(guān)codec、rtp參數(shù)設(shè)置 ├── peer.go //peer封裝,一個peer包含一個publisher和一個subscriber,雙pc設(shè)計 ├── publisher.go //publisher,封裝上行pc ├── receiver.go //subscriber,封裝下行pc ├── router.go //router,包含pc、session、一組receivers ├── sequencer.go //記錄包的信息:序列號sn、偏移、時間戳ts等 ├── session.go //會話,包含多個peer、dc ├── sfu.go //分發(fā)單元,包含多個session、dc ├── simulcast.go //大小流配置 ├── subscriber.go //subscriber,封裝下行pc、DownTrack、dc └── turn.go //內(nèi)置turn server相比以前版本,增加了一些interface,主要是為了作為包使用時,封裝自己的類。
三、主函數(shù)與信令流程
1.主函數(shù)
這里拿jsonrpc來分析,其他rpc流程上是一樣的。
func main() {if !parse() {showHelp()os.Exit(-1)}//創(chuàng)建SFU和DC,這里的DC用于Simulcast和AudioLevels := sfu.NewSFU(conf)dc := s.NewDatachannel(sfu.APIChannelLabel)dc.Use(datachannel.SubscriberAPI)//接下來是標(biāo)準(zhǔn)websocket服務(wù)器啟動的流程upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {return true},ReadBufferSize: 1024,WriteBufferSize: 1024,}//這里jsonrpc基于websocket,websocket從標(biāo)準(zhǔn)http upgrade過來的http.Handle("/ws", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {c, err := upgrader.Upgrade(w, r, nil)if err != nil {panic(err)}defer c.Close()//這里創(chuàng)建了JSONSignal,每次真實(shí)請求到來時會新建一個Peer,進(jìn)入Handle函數(shù)處理p := server.NewJSONSignal(sfu.NewPeer(s), logger)defer p.Close()jc := jsonrpc2.NewConn(r.Context(), websocketjsonrpc2.NewObjectStream(c), p)<-< span="">jc.DisconnectNotify()}))go startMetrics(metricsAddr)var err errorif key != "" && cert != "" {logger.Info("Started listening", "addr", "https://"+addr)err = http.ListenAndServeTLS(addr, cert, key, nil)} else {logger.Info("Started listening", "addr", "http://"+addr)err = http.ListenAndServe(addr, nil)}if err != nil {panic(err)} }2.協(xié)商&重協(xié)商
協(xié)商(negotiate):
WebRTC對外的類是PeerConnection,簡稱PC,通過信令服務(wù)交換SDP給PC進(jìn)行操作。協(xié)商就是指雙方通過信令交換SDP,通過PC的一些接口,達(dá)到協(xié)商雙方的媒體格式、傳輸?shù)刂范丝诘刃畔?#xff0c;從而實(shí)現(xiàn)推流和播放的目的。
一次協(xié)商完整流程:
本端CreateOffer-》本端SetLocalDescription(offer)-》本端發(fā)送offer-》對端SetRemoteDescription(offer)-》對端CreateAnswer-》SetLocalDescription(answer)-》對端對端返回answer-》本端SetRemoteDescription(answer)
重協(xié)商(renegotiate):
就是指再次協(xié)商。
為什么要重協(xié)商?
因為客戶端和服務(wù)器的track都是變化的,重協(xié)商是通知對端的必要手段,比如:客戶端發(fā)起屏幕分享,服務(wù)器有人進(jìn)出房間等。
重協(xié)商的原則:
誰變化誰發(fā)起(offer)。
3.信令流程
首先,客戶端ws連接成功:
服務(wù)端會建立一個Peer,可以參考上邊代碼。
然后,客戶端發(fā)起第一次協(xié)商:
客戶端pc.CreateOffer(一個只包含dc的offer)-》pc.SetLocalDescription(offer),然后把offer放入Join信令,發(fā)送給服務(wù)端,然后服務(wù)器協(xié)商【pc.SetRemoteDescription(offer)-》pc.CreateAnswer-》pc.SetLocalDescription(answer)】,返回answer給客戶端,至此完成數(shù)據(jù)通道(datachannel)建立。首先打通dc,是為了方便audio-level/simulcast通道的建立,此時也可以創(chuàng)建自定dc做定制業(yè)務(wù)。
接下來,服務(wù)端發(fā)起第二次協(xié)商:
服務(wù)端pc.CreateOffer,SetLocalDescription,發(fā)送offer,此時offer會攜帶上此房間內(nèi)的所有track信息,客戶端收到后會CreateAnswer,SetLocalDescription,把a(bǔ)nswer返回來,然后服務(wù)端pc.SetRemoteDescription(answer),此時客戶端可以收到服務(wù)器此房間內(nèi)的所有流了。
最后,客戶端publish發(fā)流時會發(fā)起第三次協(xié)商:
同第一次流程一樣,不同的是同時攜帶了音視頻的track,本次協(xié)商完成后,服務(wù)器可以收到客戶端的流了,收到之后會對同房間內(nèi)的其他客戶端發(fā)起重協(xié)商。
往后只要客戶端或服務(wù)器track有變化,都會再次發(fā)起重協(xié)商。
4.代碼分析
JsonRPC所有的信令都會進(jìn)入Handle函數(shù)。為了簡化流程,可以暫時不看Trickle和OnIceCandidate函數(shù),這個是開啟trickle-ICE時才會有。
// Handle incoming RPC call events like join, answer, offer and trickle // JSONSignal是繼承了LocalPeer,所以會繼承一些屬性和回調(diào):OnOffer等。 // 可以在瀏覽器端ws網(wǎng)絡(luò)工具里查看具體信令內(nèi)容 func (p *JSONSignal) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {replyError := func(err error) {_ = conn.ReplyWithError(ctx, req.ID, &jsonrpc2.Error{Code: 500,Message: fmt.Sprintf("%s", err),})}switch req.Method {case "join":// 首先客戶端會發(fā)join信令過來var join Joinerr := json.Unmarshal(*req.Params, &join)if err != nil {p.Logger.Error(err, "connect: error parsing offer")replyError(err)break}//設(shè)置OnOffer,即SFU發(fā)起offer時(重協(xié)商),會使用這個回調(diào),比如重協(xié)商時,因為有很多客戶端peer同時連到SFU,每個Peer的Track增刪時,SFU需要向其他Peer重協(xié)商來告訴Track的變更p.OnOffer = func(offer *webrtc.SessionDescription) {if err := conn.Notify(ctx, "offer", offer); err != nil {p.Logger.Error(err, "error sending offer")}}//設(shè)置OnIceCandidate,即SFU在ICE流程獲取到新候選時,會回調(diào)這個函數(shù),告訴客戶端新增了啥候選p.OnIceCandidate = func(candidate *webrtc.ICECandidateInit, target int) {if err := conn.Notify(ctx, "trickle", Trickle{Candidate: *candidate,Target: target,}); err != nil {p.Logger.Error(err, "error sending ice candidate")}}//加入某個會話(房間)err = p.Join(join.SID, join.UID, join.Config)if err != nil {replyError(err)break}//根據(jù)offer回復(fù)answeranswer, err := p.Answer(join.Offer)if err != nil {replyError(err)break}_ = conn.Reply(ctx, req.ID, answer)//如果是客戶端發(fā)offer,回復(fù)answer,此時為客戶端發(fā)起重協(xié)商case "offer":var negotiation Negotiationerr := json.Unmarshal(*req.Params, &negotiation)if err != nil {p.Logger.Error(err, "connect: error parsing offer")replyError(err)break}answer, err := p.Answer(negotiation.Desc)if err != nil {replyError(err)break}_ = conn.Reply(ctx, req.ID, answer)//如果是客戶端發(fā)answer,設(shè)置SetRemoteDescription即可case "answer":var negotiation Negotiationerr := json.Unmarshal(*req.Params, &negotiation)if err != nil {p.Logger.Error(err, "connect: error parsing offer")replyError(err)break}err = p.SetRemoteDescription(negotiation.Desc)if err != nil {replyError(err)}//如果是客戶端發(fā)送Trickle-ICE的候選過來,設(shè)置即可case "trickle":var trickle Trickleerr := json.Unmarshal(*req.Params, &trickle)if err != nil {p.Logger.Error(err, "connect: error parsing candidate")replyError(err)break}err = p.Trickle(trickle.Candidate, trickle.Target)if err != nil {replyError(err)}} }注意OnOffer是服務(wù)器重協(xié)商的回調(diào),即房間內(nèi)某客戶端track有變化,服務(wù)器會回調(diào)此函數(shù)通知其他客戶端。
總結(jié)一句話,客戶端《---》SFU的核心邏輯就是不斷重協(xié)商,誰變化誰發(fā)起offer。
作者簡介:
王朋闖:前百度RTN資深工程師,前金山云RTC技術(shù)專家,前VIPKID流媒體架構(gòu)師,ION開源項目發(fā)起人。
特別說明:
本文發(fā)布于知乎,已獲得作者授權(quán)轉(zhuǎn)載。
掃描圖中二維碼或點(diǎn)擊閱讀原文
了解大會更多信息
喜歡我們的內(nèi)容就點(diǎn)個“在看”吧!
超強(qiáng)干貨來襲 云風(fēng)專訪:近40年碼齡,通宵達(dá)旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的大话ion系列(一)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 三星电子推出X-net架构用于语音通话
- 下一篇: Easy Tech:什么是I帧、P帧和B