公司要用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