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的建立连接流程图

iOS 基于WebRTC的音视频通信 总结篇(2019最新)_第1张图片
webrtc流程图.png

整个webrtc连接的流程说明

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

  1. 客户端通过socket, 和服务器建立起TCP长链接, 这部分WebRTC并没有提供相应的API, 所以这里可以借助第三方框架, OC代码建议使用CocoaAsyncSocket第三方框架进行socket连接https://github.com/robbiehanson/CocoaAsyncSocket
    swift代码的话国外工程师最喜欢用Starscream (WebSocket)
    https://github.com/daltoniam/Starscream

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


iOS 基于WebRTC的音视频通信 总结篇(2019最新)_第2张图片
SDP.jpg

1.anchor端通过 createOffer 生成 SDP 描述
2.anchor通过 setLocalDescription,设置本地的描述信息
3.anchor将 offer SDP 发送给用户
4.用户通过 setRemoteDescription,设置远端的描述信息
5.用户通过 createAnswer 创建出自己的 SDP 描述
6.用户通过 setLocalDescription,设置本地的描述信息
7.用户将 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)
        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)
        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

你可能感兴趣的:(iOS 基于WebRTC的音视频通信 总结篇(2019最新))