iOS 基于WebRTC的音视频通信 总结篇(2019最新)

2020-04-13 11:39:36 蜻蜓队长

公司要用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连接的流程说明

其主要流程如上图所示, 具体流程说明如下:

  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通道的基础。

  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, 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

以上内容来自于网络,如有侵权联系即删除
相关文章

上一篇: 最小生成树(Prim算法和Kruskal算法算法详解)

下一篇: Spring Security 认证流程梳理

客服紫薇:15852074331
在线咨询
客户经理