iOS 基於WebRTC的音視訊通訊 總結篇(2019最新)
阿新 • • 發佈:2019-12-25
公司要用webrtc進行音視訊通訊,參考了國內外眾多部落格和demo,總結一下經驗: webrtc官網 webrtc對iOS使用的說明
WEBRTC結構
完整的WebRTC框架,分為 Server端、Client端兩大部分。
- Server端:
Stun伺服器
: 伺服器用於獲取裝置的外部網路地址Turn伺服器
: 伺服器是在點對點失敗後用於通訊中繼信令伺服器
: 負責端到端的連線。兩端在連線之初,需要交換信令,如sdp、candidate等,都是通過信令伺服器 進行轉發交換的。 - Client有四大應用端:
Android
iOS
PC Broswer
介紹下WebRTC三個主要API,以及實現點對點連線的流程。
-
MediaStream
:通過MediaStream的API能夠通過裝置的攝像頭及話筒獲得視訊、音訊的同步流 -
RTCPeerConnection
:RTCPeerConnection是WebRTC用於構建點對點之間穩定、高效的流傳輸的元件 -
RTCDataChannel
:RTCDataChannel使得瀏覽器之間(點對點)建立一個高吞吐量、低延時的通道,用於傳輸任意資料。 其中RTCPeerConnection
是我們WebRTC的核心元件。
WEBRTC的建立連線流程圖
整個webrtc連線的流程說明
其主要流程如上圖所示,具體流程說明如下:
-
客戶端通過socket,和伺服器建立起TCP長連結,這部分WebRTC並沒有提供相應的API,所以這裡可以藉助第三方框架,OC程式碼建議使用
CocoaAsyncSocket
Starscream
github.com/daltoniam/S… -
客戶端通過信令伺服器,進行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通道的基礎。
- 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連結的建立。
下面用一個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,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