公司要用webrtc进行音视频通讯, 参考了国内外众多博客和demo, 总结一下经验: webrtc官网 webrtc对iOS使用的说明ios
Stun服务器
: 服务器用于获取设备的外部网络地址 Turn服务器
: 服务器是在点对点失败后用于通讯中继 信令服务器
: 负责端到端的链接。两端在链接之初,须要交换信令,如sdp、candidate等,都是经过信令服务器 进行转发交换的。iOS
PC BroswerMediaStream
:经过MediaStream的API可以经过设备的摄像头及话筒得到视频、音频的同步流RTCPeerConnection
:RTCPeerConnection是WebRTC用于构建点对点之间稳定、高效的流传输的组件RTCDataChannel
:RTCDataChannel使得浏览器之间(点对点)创建一个高吞吐量、低延时的信道,用于传输任意数据。 其中RTCPeerConnection
是咱们WebRTC的核心组件。其主要流程如上图所示, 具体流程说明以下:git
客户端经过socket, 和服务器创建起TCP长连接, 这部分WebRTC并无提供相应的API, 因此这里能够借助第三方框架, OC代码建议使用CocoaAsyncSocket
第三方框架进行socket链接github.com/robbiehanso… swift代码的话国外工程师最喜欢用Starscream
github.com/daltoniam/S…github
客户端经过信令服务器, 进行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
.浏览器
下图为WebRTC经过信令创建一个SDP握手的过程。只有经过SDP握手,双方才知道对方的信息,这是创建p2p通道的基础。 缓存
![]()
- anchor端经过 createOffer 生成 SDP 描述
- anchor经过 setLocalDescription,设置本地的描述信息
- anchor将 offer SDP 发送给用户
- audience经过 setRemoteDescription,设置远端的描述信息
- audience经过 createAnswer 建立出本身的 SDP 描述
- audience经过 setLocalDescription,设置本地的描述信息
- audience将 anwser SDP 发送给主播
- anchor经过 setRemoteDescription,设置远端的描述信息。
- 经过SDP握手后,浏览器之间就会创建起一个端对端的直接通信通道。
因为咱们所处的网络环境错综复杂,用户可能处在私有内网内,使用p2p传输时,将会遇到NAT以及防火墙等阻碍。这个时候咱们就须要在SDP握手时,经过STUN/TURN/ICE相关NAT穿透技术来保障p2p连接的创建。bash
研究发现国内不少WebRTC博客文章附带的代码和demo都很老旧过期, 不少运行不起来, 在综合了各自的优势后整理了一个demo, 能顺利实现手机两端音视频视频通讯, 现给你们分享出来, 你们有问题能够QQ我: 506299396服务器
- 如下是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