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

公司要用webrtc進行音視頻通訊, 參考了國內外衆多博客和demo, 總結一下經驗: webrtc官網 webrtc對iOS使用的說明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鏈接的流程說明

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

  1. 客戶端經過socket, 和服務器創建起TCP長連接, 這部分WebRTC並無提供相應的API, 因此這裏能夠藉助第三方框架, OC代碼建議使用CocoaAsyncSocket第三方框架進行socket鏈接github.com/robbiehanso… swift代碼的話國外工程師最喜歡用Starscream github.com/daltoniam/S…github

  2. 客戶端經過信令服務器, 進行offer SDP 握手web

SDP(Session Description Protocol):描述創建音視頻鏈接的一些屬性,如音頻的編碼格式、視頻的編碼格式、是否接收/發送音視頻等等 SDP 是經過webrtc框架裏面的PeerConnection所建立, 詳細建立請參考個人demo.json

3.客戶端經過信令服務器, 進行Candidate 握手swift

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連接的創建。bash

下面用一個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, 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("發送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

相關文章
相關標籤/搜索