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

相关文章
相关标签/搜索