1. 程式人生 > IOS開發 >iOS 基於WebRTC的音視訊通訊 總結篇(2019最新)

iOS 基於WebRTC的音視訊通訊 總結篇(2019最新)

公司要用webrtc進行音視訊通訊,參考了國內外眾多部落格和demo,總結一下經驗: webrtc官網 webrtc對iOS使用的說明

WEBRTC結構

完整的WebRTC框架,分為 Server端、Client端兩大部分。

  • Server端: Stun伺服器 : 伺服器用於獲取裝置的外部網路地址 Turn伺服器 : 伺服器是在點對點失敗後用於通訊中繼 信令伺服器 : 負責端到端的連線。兩端在連線之初,需要交換信令,如sdp、candidate等,都是通過信令伺服器 進行轉發交換的。
  • Client有四大應用端: Android iOS PC Broswer

介紹下WebRTC三個主要API,以及實現點對點連線的流程。

  1. MediaStream:通過MediaStream的API能夠通過裝置的攝像頭及話筒獲得視訊、音訊的同步流
  2. RTCPeerConnection:RTCPeerConnection是WebRTC用於構建點對點之間穩定、高效的流傳輸的元件
  3. RTCDataChannel:RTCDataChannel使得瀏覽器之間(點對點)建立一個高吞吐量、低延時的通道,用於傳輸任意資料。 其中RTCPeerConnection是我們WebRTC的核心元件。

WEBRTC的建立連線流程圖

webrtc流程圖.png

整個webrtc連線的流程說明

其主要流程如上圖所示,具體流程說明如下:

  1. 客戶端通過socket,和伺服器建立起TCP長連結,這部分WebRTC並沒有提供相應的API,所以這裡可以藉助第三方框架,OC程式碼建議使用CocoaAsyncSocket

    第三方框架進行socket連線github.com/robbiehanso… swift程式碼的話國外工程師最喜歡用Starscream github.com/daltoniam/S…

  2. 客戶端通過信令伺服器,進行offer SDP 握手

SDP(Session Description Protocol):描述建立音視訊連線的一些屬性,如音訊的編碼格式、視訊的編碼格式、是否接收/傳送音視訊等等 SDP 是通過webrtc框架裡面的PeerConnection所建立,詳細建立請參考我的demo.

3.客戶端通過信令伺服器,進行Candidate 握手

Candidate:主要包含了相關方的IP資訊,包括自身區域網的ip、公網ip、turn伺服器ip、stun伺服器ip等 Candidate

是通過webrtc框架裡面的PeerConnection所建立,詳細建立請參考我的demo.

  1. 客戶端在SDP 和Candidate握手成功後,就建立起一個P2P端對端的連結,視訊流就能直接傳輸,不需要經過伺服器啦.

SDP握手流程和Candidate握手流程類似,但有點繁瑣,下面就SDP握手流程簡要說明:

下圖為WebRTC通過信令建立一個SDP握手的過程。只有通過SDP握手,雙方才知道對方的資訊,這是建立p2p通道的基礎。

SDP.jpg

  1. anchor端通過 createOffer 生成 SDP 描述
  2. anchor通過 setLocalDescription,設定本地的描述資訊
  3. anchor將 offer SDP 傳送給使用者
  4. audience通過 setRemoteDescription,設定遠端的描述資訊
  5. audience通過 createAnswer 創建出自己的 SDP 描述
  6. audience通過 setLocalDescription,設定本地的描述資訊
  7. audience將 anwser SDP 傳送給主播
  8. anchor通過 setRemoteDescription,設定遠端的描述資訊。
  9. 通過SDP握手後,瀏覽器之間就會建立起一個端對端的直接通訊通道。

由於我們所處的網路環境錯綜複雜,使用者可能處在私有內網內,使用p2p傳輸時,將會遇到NAT以及防火牆等阻礙。這個時候我們就需要在SDP握手時,通過STUN/TURN/ICE相關NAT穿透技術來保障p2p連結的建立。

下面用一個demo演示能很好的幫助大家對整套webrtc音視訊通訊的梳理:

研究發現國內很多WebRTC部落格文章附帶的程式碼和demo都很老舊過時,很多執行不起來,在綜合了各自的優點後整理了一個demo,能順利實現手機兩端音視訊視訊通訊,現給大家分享出來,大家有問題可以QQ我: 506299396

與伺服器端建立長連線,選用了socket連線,這裡用的第三方框架是CocoaAsyncSocket,其實也可以使用WebSocket,看你們團隊的方案選型吧.

  • 以下是socket建立連線以及WebRTC建立連線的邏輯程式碼. socket連線其實程式碼量極少,socket連線參考一下github的CocoaAsyncSocket說明就好,不必花太多時間在這塊,重點還是在WebRTC建立連線,在與服務端進行資料傳輸的時候,注意你們可能會有資料分包策略.
  • 網上絕大部分程式碼用的是OC,而且很多已經過且零散的,OC版本相對簡單,以下分享的是swift版,閱讀以下程式碼請一定一定要先看看以上提到的兩個邏輯時序圖.
// MARK: - socket狀態代理
protocol SocketClientDelegate: class {
    
    func signalClientDidConnect(_ signalClient: SocketClient)
    func signalClientDidDisconnect(_ signalClient: SocketClient)
    func signalClient(_ signalClient: SocketClient,didReceiveRemoteSdp sdp: RTCSessionDescription)
    func signalClient(_ signalClient: SocketClient,didReceiveCandidate candidate: RTCIceCandidate)
}

final class SocketClient: NSObject {
    
    //socket
    var socket: GCDAsyncSocket = {
       return GCDAsyncSocket.init()
    }()
    
    private var host: String? //服務端IP
    private var port: UInt16? //埠
    weak var delegate: SocketClientDelegate?//代理
    
    var receiveHeartBeatDuation = 0 //心跳計時計數
    let heartBeatOverTime = 10 //心跳超時
    var sendHeartbeatTimer:Timer? //傳送心跳timer
    var receiveHeartbearTimer:Timer? //接收心跳timer

    //接收資料快取
    var dataBuffer:Data = Data.init()
    
    //登入獲取的peer_id
    var peer_id = 0
    //登入獲取的遠端裝置peer_id
    var remote_peer_id = 0

    // MARK:- 初始化
    init(hostStr: String,port: UInt16) {
        super.init()
        
        self.socket.delegate = self
        self.socket.delegateQueue = DispatchQueue.main
        self.host = hostStr
        self.port = port
        //socket開始連線
        connect()
    }

    // MARK:- 開始連線
    func connect() {
        
        do {
            try  self.socket.connect(toHost: self.host ?? "",onPort: self.port ?? 6868,withTimeout: -1)
            
        }catch {
            print(error)
        }
    }
    
    // MARK:- 傳送訊息
    func sendMessage(_ data: Data){
        self.socket.write(data,withTimeout: -1,tag: 0)
    }

    // MARK:- 傳送sdp offer/answer
    func send(sdp rtcSdp: RTCSessionDescription) {
        
        //轉成我們的sdp
        let type = rtcSdp.type
        var typeStr = ""
        switch type {
        case .answer:
            typeStr = "answer"
        case .offer:
            typeStr = "offer"
        default:
            print("sdpType錯誤")
        }
        let newSDP:SDPSocket = SDPSocket.init(sdp: rtcSdp.sdp,type: typeStr)
        let jsonInfo = newSDP.toJSON()
        let dic = ["sdp" : jsonInfo]
        let info:SocketInfo = SocketInfo.init(type: .sdp,source: self.peer_id,destination: self.remote_peer_id,params: dic as Dictionary<String,Any>)
        let data = self.packData(info: info)
        //print(data)
        self.sendMessage(data)
        print("傳送SDP")
    }

    // MARK:- 傳送iceCandidate
    func send(candidate rtcIceCandidate: RTCIceCandidate) {
        
        let iceCandidateMessage = IceCandidate_Socket(from: rtcIceCandidate)
        let jsonInfo = iceCandidateMessage.toJSON()
        let dic = ["icecandidate" : jsonInfo]
        let info:SocketInfo = SocketInfo.init(type: .icecandidate,Any>)
        let data = self.packData(info: info)
        //print(data)
        self.sendMessage(data)
         print("傳送ICE")
    }
}

extension SocketClient: GCDAsyncSocketDelegate {
    
    // MARK:- socket連線成功
    func socket(_ sock: GCDAsyncSocket,didConnectToHost host: String,port: UInt16) {
        
        debugPrint("socket連線成功")
        self.delegate?.signalClientDidConnect(self)
        
        //登入獲取身份id peer_id
        login()
        //傳送心跳
        startHeartbeatTimer()
        //開啟接收心跳計時
        startReceiveHeartbeatTimer()
        
        //繼續接收資料
        socket.readData(withTimeout: -1,tag: 0)
    }
    
    // MARK:- 接收資料  socket接收到一個數據包
    func socket(_ sock: GCDAsyncSocket,didRead data: Data,withTag tag: Int) {
        
        //debugPrint("socket接收到一個數據包")
        let _:SocketInfo? = self.unpackData(data)
        //let type:SigType = SigType(rawValue: socketInfo?.type ?? "")!
        //print(socketInfo ?? "")
        //print(type)

        //繼續接收資料
        socket.readData(withTimeout: -1,tag: 0)
    }
    
    // MARK:- 斷開連線
    func socketDidDisconnect(_ sock: GCDAsyncSocket,withError err: Error?) {
        
        debugPrint("socket斷開連線")
        print(err ?? "")
        
        self.disconnectSocket()
        
        // try to reconnect every two seconds
        DispatchQueue.global().asyncAfter(deadline: .now() + 5) {
            debugPrint("Trying to reconnect to signaling server...")
            self.connect()
        }
    }

}
複製程式碼

持續更新中.....

大家有問題可以QQ我: 506299396