iOS 基于WebRTC的音视频通信 总结篇(2019最新)
公司要用webrtc进行音视频通信, 参考了国内外众多博客和demo, 总结一下经验:
webrtc官网
webrtc对iOS使用的说明
WEBRTC结构
完整的WebRTC框架,分为 Server端、Client端两大部分。
- Server端:
Stun服务器
: 服务器用于获取设备的外部网络地址
Turn服务器
: 服务器是在点对点失败后用于通信中继
信令服务器
: 负责端到端的连接。两端在连接之初,需要交换信令,如sdp、candidate等,都是通过信令服务器 进行转发交换的。 - Client有四大应用端:
AndroidiOS
PC Broswer
介绍下WebRTC三个主要API,以及实现点对点连接的流程。
-
MediaStream
:通过MediaStream的API能够通过设备的摄像头及话筒获得视频、音频的同步流 -
RTCPeerConnection
:RTCPeerConnection是WebRTC用于构建点对点之间稳定、高效的流传输的组件 -
RTCDataChannel
:RTCDataChannel使得浏览器之间(点对点)建立一个高吞吐量、低延时的信道,用于传输任意数据。
其中RTCPeerConnection
是我们WebRTC的核心组件。
WEBRTC的建立连接流程图
整个webrtc连接的流程说明
其主要流程如上图所示, 具体流程说明如下:
客户端通过socket, 和服务器建立起TCP长链接, 这部分WebRTC并没有提供相应的API, 所以这里可以借助第三方框架, OC代码建议使用
CocoaAsyncSocket
第三方框架进行socket连接https://github.com/robbiehanson/CocoaAsyncSocket
swift代码的话国外工程师最喜欢用Starscream
(WebSocket)
https://github.com/daltoniam/Starscream客户端通过信令服务器, 进行offer SDP 握手
SDP
(Session Description Protocol):描述建立音视频连接的一些属性,如音频的编码格式、视频的编码格式、是否接收/发送音视频等等
SDP
是通过webrtc框架里面的PeerConnection
所创建, 详细创建请参考我的demo
.
3.客户端通过信令服务器, 进行Candidate 握手
Candidate
:主要包含了相关方的IP信息,包括自身局域网的ip、公网ip、turn服务器ip、stun服务器ip等
Candidate
是通过webrtc框架里面的PeerConnection
所创建, 详细创建请参考我的demo
.
- 客户端在SDP 和Candidate握手成功后, 就建立起一个P2P端对端的链接, 视频流就能直接传输, 不需要经过服务器啦.
SDP握手流程和Candidate握手流程类似, 但有点繁琐, 下面就SDP握手流程简要说明:
下图为WebRTC通过信令建立一个SDP握手的过程。只有通过SDP握手,双方才知道对方的信息,这是建立p2p通道的基础。
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