前言:
WebRTC,名稱源自網(wǎng)頁實時通信(Web Real-Time Communication)的縮寫奔坟,簡而言之它是一個支持網(wǎng)頁瀏覽器進行實時語音對話或視頻對話的技術(shù)壳坪。
它為我們提供了視頻會議的核心技術(shù),包括音視頻的采集尝抖、編解碼毡们、網(wǎng)絡(luò)傳輸、顯示等功能昧辽,并且還支持跨平臺:windows漏隐,linux,mac奴迅,android青责,iOS。
它在2011年5月開放了工程的源代碼取具,在行業(yè)內(nèi)得到了廣泛的支持和應(yīng)用脖隶,成為下一代視頻通話的標準。
本文將站在巨人的肩膀上暇检,基于WebRTC去實現(xiàn)不同客戶端之間的音視頻通話产阱。這個不同的客戶端,不局限于移動端和移動端块仆,還包括移動端和Web瀏覽器之間构蹬。
目錄:
- 一.WebRTC的實現(xiàn)原理。
- 二.iOS下WebRTC環(huán)境的搭建悔据。
- 三.介紹下WebRTC的API庄敛,以及實現(xiàn)點對點連接的流程。
- 四.iOS客戶端的詳細實現(xiàn)科汗,以及服務(wù)端信令通道的搭建藻烤。
正文:
一.WebRTC的實現(xiàn)原理。
WebRTC的音視頻通信是基于P2P头滔,那么什么是P2P呢怖亭?
它是點對點連接的英文縮寫。
1.我們從P2P連接模式來講起:
一般我們傳統(tǒng)的連接方式坤检,都是以服務(wù)器為中介的模式:
- 類似
http
協(xié)議:客戶端?服務(wù)端(當然這里服務(wù)端返回的箭頭僅僅代表返回請求數(shù)據(jù))兴猩。 - 我們在進行即時通訊時,進行文字早歇、圖片倾芝、錄音等傳輸?shù)臅r候:客戶端A?服務(wù)器?客戶端B。
而點對點的連接恰恰數(shù)據(jù)通道一旦形成缺前,中間是不經(jīng)過服務(wù)端的蛀醉,數(shù)據(jù)直接從一個客戶端流向另一個客戶端:
客戶端A?客戶端B ... 客戶端A?客戶端C ...(可以無數(shù)個客戶端之間互聯(lián))
這里可以想想音視頻通話的應(yīng)用場景,我們服務(wù)端確實是沒必要去獲取兩者通信的數(shù)據(jù)衅码,而且這樣做有一個最大的一個優(yōu)點就是拯刁,大大的減輕了服務(wù)端的壓力。
而WebRTC
就是這樣一個基于P2P的音視頻通信技術(shù)逝段。
2.WebRTC的服務(wù)器與信令垛玻。
講到這里割捅,可能大家覺得WebRTC
就不需要服務(wù)端了么?這是顯然是錯誤的認識帚桩,嚴格來說它僅僅是不需要服務(wù)端來進行數(shù)據(jù)中轉(zhuǎn)而已亿驾。
WebRTC提供了瀏覽器到瀏覽器(點對點)之間的通信,但并不意味著WebRTC不需要服務(wù)器账嚎。暫且不說基于服務(wù)器的一些擴展業(yè)務(wù)莫瞬,WebRTC至少有兩件事必須要用到服務(wù)器:
- 瀏覽器之間交換建立通信的元數(shù)據(jù)(信令)必須通過服務(wù)器。
- 為了穿越NAT和防火墻郭蕉。
第1條很好理解疼邀,我們在A和B需要建立P2P連接的時候,至少要服務(wù)器來協(xié)調(diào)召锈,來控制連接開始建立旁振。而連接斷開的時候,也需要服務(wù)器來告知另一端P2P連接已斷開涨岁。這些我們用來控制連接的狀態(tài)的數(shù)據(jù)稱之為信令拐袜,而這個與服務(wù)端連接的通道,對于WebRTC
而言就是信令通道梢薪。
圖中signalling就是往服務(wù)端發(fā)送信令蹬铺,然后底層調(diào)用WebRTC
,WebRTC
通過服務(wù)端得到的信令沮尿,得知通信對方的基本信息丛塌,從而實現(xiàn)虛線部分Media
通信連接。
當然信令能做的事還有很多畜疾,這里大概列了一下:
- 用來控制通信開啟或者關(guān)閉的連接控制消息
- 發(fā)生錯誤時用來彼此告知的消息
- 媒體流元數(shù)據(jù),比如像解碼器印衔、解碼器的配置啡捶、帶寬、媒體類型等等
- 用來建立安全連接的關(guān)鍵數(shù)據(jù)
- 外界所看到的的網(wǎng)絡(luò)上的數(shù)據(jù)奸焙,比如IP地址瞎暑、端口等
在建立連接之前,客戶端之間顯然沒有辦法傳遞數(shù)據(jù)与帆。所以我們需要通過服務(wù)器的中轉(zhuǎn)了赌,在客戶端之間傳遞這些數(shù)據(jù),然后建立客戶端之間的點對點連接玄糟。但是WebRTC API中并沒有實現(xiàn)這些勿她,這些就需要我們來實現(xiàn)了。
而第2條中的NAT這個概念阵翎,我們之前在iOS即時通訊逢并,從入門到“放棄”之剧?
,中也提到過砍聊,不過那個時候我們是為了應(yīng)對NAT超時背稼,所造成的TCP連接中斷。在這里我們就不展開去講了玻蝌,感興趣的可以看看:NAT百科
這里我簡要說明一下蟹肘,NAT技術(shù)的出現(xiàn),其實就是為了解決IPV4下的IP地址匱乏俯树。舉例來說帘腹,就是通常我們處在一個路由器之下,而路由器分配給我們的地址通常為192.168.0.1 聘萨、192.168.0.2如果有n個設(shè)備竹椒,可能分配到192.168.0.n,而這個IP地址顯然只是一個內(nèi)網(wǎng)的IP地址米辐,這樣一個路由器的公網(wǎng)地址對應(yīng)了n個內(nèi)網(wǎng)的地址胸完,通過這種使用少量的公有IP 地址代表較多的私有IP 地址的方式,將有助于減緩可用的IP地址空間的枯竭翘贮。
但是這也帶來了一系列的問題赊窥,例如這里點對點連接下,會導(dǎo)致這樣一個問題:
如果客戶端A想給客戶端B發(fā)送數(shù)據(jù)狸页,則數(shù)據(jù)來到客戶端B所在的路由器下锨能,會被NAT阻攔,這樣B就無法收到A的數(shù)據(jù)了芍耘。
但是A的NAT此時已經(jīng)知道了B這個地址址遇,所以當B給A發(fā)送數(shù)據(jù)的時候,NAT不會阻攔斋竞,這樣A就可以收到B的數(shù)據(jù)了倔约。這就是我們進行NAT穿越的核心思路。
于是我們就有了以下思路:
我們借助一個公網(wǎng)IP服務(wù)器,a,b都往公網(wǎng)IP/PORT發(fā)包,公網(wǎng)服務(wù)器就可以獲知a,b的IP/PORT坝初,又由于a,b主動給公網(wǎng)IP服務(wù)器發(fā)包浸剩,所以公網(wǎng)服務(wù)器可以穿透NAT A,NAT B送包給a,b。
所以只要公網(wǎng)IP將b的IP/PORT發(fā)給a,a的IP/PORT發(fā)給b鳄袍。這樣下次a和b互相消息绢要,就不會被NAT阻攔了。
而WebRTC的NAT/防火墻穿越技術(shù)拗小,就是基于上述的一個思路來實現(xiàn)的:
建立點對點信道的一個常見問題重罪,就是NAT穿越技術(shù)。在處于使用了NAT設(shè)備的私有TCP/IP網(wǎng)絡(luò)中的主機之間需要建立連接時需要使用NAT穿越技術(shù)。以往在VoIP領(lǐng)域經(jīng)常會遇到這個問題蛆封。目前已經(jīng)有很多NAT穿越技術(shù)唇礁,但沒有一項是完美的,因為NAT的行為是非標準化的惨篱。這些技術(shù)中大多使用了一個公共服務(wù)器盏筐,這個服務(wù)使用了一個從全球任何地方都能訪問得到的IP地址。在RTCPeeConnection中砸讳,使用ICE框架來保證RTCPeerConnection能實現(xiàn)NAT穿越
這里提到了ICE協(xié)議框架琢融,它大約是由以下幾個技術(shù)和協(xié)議組成的:STUN、NAT簿寂、TURN漾抬、SDP,這些協(xié)議技術(shù)常遂,幫助ICE共同實現(xiàn)了NAT/防火墻穿越纳令。
小伙伴們可能又一臉懵逼了,一下子又出來這么多名詞克胳,沒關(guān)系平绩,這里我們暫且不去管它們,等我們后面實現(xiàn)的時候漠另,還會提到他們捏雌,這里提前感興趣的可以看看這篇文章:WebRTC protocols
二.iOS下WebRTC環(huán)境的搭建:
首先,我們需要明白的一點是:WebRTC已經(jīng)在我們的瀏覽器中了笆搓。如果我們用瀏覽器性湿,則可以直接使用js調(diào)用對應(yīng)的WebRTC
的API,實現(xiàn)音視頻通信满败。
然而我們是在iOS平臺肤频,所以我們需要去官網(wǎng)下載指定版本的源碼,并且對其進行編譯算墨,大概一下着裹,其中源碼大小10個多G,編譯過程會遇到一系列坑米同,而我們編譯完成最終形成的webrtc
的.a
庫大概有300多m。
這里我們不寫編譯過程了摔竿,感興趣的可以看看這篇文章:
WebRTC(iOS)下載編譯
最終我們編譯成功的文件如下WebRTC
:
其中包括一個
.a
文件面粮,和include
文件夾下的一些頭文件。(大家測試的時候可以直接使用這里編譯好的文件继低,但是如果以后需要WebRTC最新版熬苍,就只能自己動手去編譯了)
接著我們把整個WebRTC
文件夾添加到工程中,并且添加以下系統(tǒng)依賴庫:
至此,一個iOS
下的WebRTC
環(huán)境就搭建完畢了
三.介紹下WebRTC的API柴底,以及實現(xiàn)點對點連接的流程婿脸。
1.WebRTC主要實現(xiàn)了三個API,分別是:
-
MediaStream
:通過MediaStream
的API能夠通過設(shè)備的攝像頭及話筒獲得視頻柄驻、音頻的同步流 -
RTCPeerConnection
:RTCPeerConnection
是WebRTC用于構(gòu)建點對點之間穩(wěn)定狐树、高效的流傳輸?shù)慕M件 -
RTCDataChannel
:RTCDataChannel
使得瀏覽器之間(點對點)建立一個高吞吐量、低延時的信道鸿脓,用于傳輸任意數(shù)據(jù)抑钟。
其中RTCPeerConnection
是我們WebRTC
的核心組件。
2.WebRTC建立點對點連接的流程:
我們在使用WebRTC
來實現(xiàn)音視頻通信前野哭,我們必須去了解它的連接流程在塔,否則面對它的API
將無從下手。
我們之前講到過WebRTC用ICE
協(xié)議來保證NAT穿越拨黔,所以它有這么一個流程:我們需要從STUN Server
中得到一個ice candidate
蛔溃,這個東西實際上就是公網(wǎng)地址,這樣我們就有了客戶端自己的公網(wǎng)地址篱蝇。而這個STUN Server
所做的事就是之前所說的贺待,把保存起來的公網(wǎng)地址,互相發(fā)送數(shù)據(jù)包态兴,防止后續(xù)的NAT
阻攔狠持。
而我們之前講過,還需要一個自己的服務(wù)端瞻润,來建立信令通道喘垂,控制A和B什么時候建立連接,建立連接的時候告知互相的ice candidate
(公網(wǎng)地址)是什么绍撞、SDP
是什么正勒。還包括什么時候斷開連接等等一系列信令。
對了傻铣,這里補充一下SDP
這個概念章贞,它是會話描述協(xié)議Session Description Protocol (SDP) 是一個描述多媒體連接內(nèi)容的協(xié)議,例如分辨率非洲,格式鸭限,編碼,加密算法等两踏。所以在數(shù)據(jù)傳輸時兩端都能夠理解彼此的數(shù)據(jù)败京。本質(zhì)上,這些描述內(nèi)容的元數(shù)據(jù)并不是媒體流本身梦染。
講到這我們來捋一捋建立P2P連接的過程:
- A和B連接上服務(wù)端赡麦,建立一個TCP長連接(任意協(xié)議都可以朴皆,WebSocket/MQTT/Socket原生/XMPP),我們這里為了省事泛粹,直接采用
WebSocket
遂铡,這樣一個信令通道就有了。 - A從
ice server
(STUN Server)獲取ice candidate
并發(fā)送給Socket服務(wù)端晶姊,并生成包含session description
(SDP)的offer扒接,發(fā)送給Socket服務(wù)端。 - Socket服務(wù)端把A的offer和
ice candidate
轉(zhuǎn)發(fā)給B帽借,B會保存下A這些信息珠增。 - 然后B發(fā)送包含自己
session description
的answer
(因為它收到的是offer,所以返回的是answer
砍艾,但是內(nèi)容都是SDP)和ice candidate
給Socket服務(wù)端蒂教。 - Socket服務(wù)端把B的
answer
和ice candidate
給A,A保存下B的這些信息脆荷。
至此A與B建立起了一個P2P連接凝垛。
這里理解整個P2P連接的流程是非常重要的,否則后面代碼實現(xiàn)部分便難以理解蜓谋。
四.iOS客戶端的詳細實現(xiàn)梦皮,以及服務(wù)端信令通道的搭建。
聊天室中的信令
上面是兩個用戶之間的信令交換流程桃焕,但我們需要建立一個多用戶在線視頻聊天的聊天室剑肯。所以需要進行一些擴展,來達到這個要求
用戶操作
首先需要確定一個用戶在聊天室中的操作大致流程:
- 打開頁面連接到服務(wù)器上
- 進入聊天室
- 與其他所有已在聊天室的用戶建立點對點的連接,并輸出在頁面上
- 若有聊天室內(nèi)的其他用戶離開,應(yīng)得到通知拴清,關(guān)閉與其的連接并移除其在頁面中的輸出
- 若又有其他用戶加入,應(yīng)得到通知溃睹,建立于新加入用戶的連接,并輸出在頁面上
- 離開頁面胰坟,關(guān)閉所有連接
從上面可以看出來因篇,除了點對點連接的建立,還需要服務(wù)器至少做如下幾件事:
- 新用戶加入房間時笔横,發(fā)送新用戶的信息給房間內(nèi)的其他用戶
- 新用戶加入房間時竞滓,發(fā)送房間內(nèi)的其他用戶信息給新加入房間的用戶
- 用戶離開房間時,發(fā)送離開用戶的信息給房間內(nèi)的其他用戶
實現(xiàn)思路
以使用WebSocket為例吹缔,上面用戶操作的流程可以進行以下修改:
- 客戶端與服務(wù)器建立WebSocket連接
- 發(fā)送一個加入聊天室的信令(join)虽界,信令中需要包含用戶所進入的聊天室名稱
- 服務(wù)器根據(jù)用戶所加入的房間,發(fā)送一個其他用戶信令(peers)涛菠,信令中包含聊天室中其他用戶的信息,客戶端根據(jù)信息來逐個構(gòu)建與其他用戶的點對點連接
- 若有用戶離開,服務(wù)器發(fā)送一個用戶離開信令(remove_peer)俗冻,信令中包含離開的用戶的信息礁叔,客戶端根據(jù)信息關(guān)閉與離開用戶的信息,并作相應(yīng)的清除操作
- 若有新用戶加入迄薄,服務(wù)器發(fā)送一個用戶加入信令(new_peer)琅关,信令中包含新加入的用戶的信息,客戶端根據(jù)信息來建立與這個新用戶的點對點連接
- 用戶離開頁面讥蔽,關(guān)閉WebSocket連接
這樣有了基本思路涣易,我們來實現(xiàn)一個基于WebRTC
的視頻聊天室。
我們首先來實現(xiàn)客戶端實現(xiàn)冶伞,先看看WebRTCHelper.h
:
@protocol WebRTCHelperDelegate;
@interface WebRTCHelper : NSObject<SRWebSocketDelegate>
+ (instancetype)sharedInstance;
@property (nonatomic, weak)id<WebRTCHelperDelegate> delegate;
/**
* 與服務(wù)器建立連接
*
* @param server 服務(wù)器地址
* @param room 房間號
*/
- (void)connectServer:(NSString *)server port:(NSString *)port room:(NSString *)room;
/**
* 退出房間
*/
- (void)exitRoom;
@end
@protocol WebRTCHelperDelegate <NSObject>
@optional
- (void)webRTCHelper:(WebRTCHelper *)webRTChelper setLocalStream:(RTCMediaStream *)stream userId:(NSString *)userId;
- (void)webRTCHelper:(WebRTCHelper *)webRTChelper addRemoteStream:(RTCMediaStream *)stream userId:(NSString *)userId;
- (void)webRTCHelper:(WebRTCHelper *)webRTChelper closeWithUserId:(NSString *)userId;
@end
這里我們對外的接口很簡單新症,就是一個生成單例的方法,一個代理响禽,還有一個與服務(wù)器連接的方法徒爹,這個方法需要傳3個參數(shù)過去,分別是server的地址芋类、端口號隆嗅、以及房間號。還有一個退出房間的方法侯繁。
說說代理部分吧胖喳,代理有3個可選的方法,分別為:
- 本地設(shè)置流的回調(diào)贮竟,可以用來顯示本地的視頻圖像丽焊。
- 遠程流到達的回調(diào),可以用來顯示對方的視頻圖像坝锰。
-
WebRTC
連接關(guān)閉的回調(diào)粹懒,注意這里關(guān)閉僅僅與當前userId
的連接關(guān)閉,而如果你除此之外還與聊天室其他的人建立連接顷级,是不會有影響的凫乖。
接著我們先不去看如何實現(xiàn)的,先運行起來看看效果吧:
VideoChatViewController.m
:
[WebRTCHelper sharedInstance].delegate = self;
[[WebRTCHelper sharedInstance]connectServer:@"192.168.0.7" port:@"3000" room:@"100"];
僅僅需要設(shè)置代理為自己弓颈,然后連接上socket
服務(wù)器即可帽芽。
我們來看看我們對代理的處理:
- (void)webRTCHelper:(WebRTCHelper *)webRTChelper setLocalStream:(RTCMediaStream *)stream userId:(NSString *)userId
{
RTCEAGLVideoView *localVideoView = [[RTCEAGLVideoView alloc] initWithFrame:CGRectMake(0, 0, KVedioWidth, KVedioHeight)];
//標記本地的攝像頭
localVideoView.tag = 100;
_localVideoTrack = [stream.videoTracks lastObject];
[_localVideoTrack addRenderer:localVideoView];
[self.view addSubview:localVideoView];
NSLog(@"setLocalStream");
}
- (void)webRTCHelper:(WebRTCHelper *)webRTChelper addRemoteStream:(RTCMediaStream *)stream userId:(NSString *)userId
{
//緩存起來
[_remoteVideoTracks setObject:[stream.videoTracks lastObject] forKey:userId];
[self _refreshRemoteView];
NSLog(@"addRemoteStream");
}
- (void)webRTCHelper:(WebRTCHelper *)webRTChelper closeWithUserId:(NSString *)userId
{
//移除對方視頻追蹤
[_remoteVideoTracks removeObjectForKey:userId];
[self _refreshRemoteView];
NSLog(@"closeWithUserId");
}
- (void)_refreshRemoteView
{
for (RTCEAGLVideoView *videoView in self.view.subviews) {
//本地的視頻View和關(guān)閉按鈕不做處理
if (videoView.tag == 100 ||videoView.tag == 123) {
continue;
}
//其他的移除
[videoView removeFromSuperview];
}
__block int column = 1;
__block int row = 0;
//再去添加
[_remoteVideoTracks enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, RTCVideoTrack *remoteTrack, BOOL * _Nonnull stop) {
RTCEAGLVideoView *remoteVideoView = [[RTCEAGLVideoView alloc] initWithFrame:CGRectMake(column * KVedioWidth, 0, KVedioWidth, KVedioHeight)];
[remoteTrack addRenderer:remoteVideoView];
[self.view addSubview:remoteVideoView];
//列加1
column++;
//一行多余3個在起一行
if (column > 3) {
row++;
column = 0;
}
}];
}
代碼很簡單,基本核心的是調(diào)用了WebRTC
的API的那幾行:
這里我們得到本地流和遠程流的時候翔冀,就可以用這個流來設(shè)置視頻圖像了导街,而音頻是自動輸出的(遠程的音頻會輸出,自己本地的音頻則不會)纤子。
基本上顯示視頻圖像只需要下面3步:
- 創(chuàng)建一個
RTCEAGLVideoView
類型的實例搬瑰。 - 從代理回調(diào)中拿到
RTCMediaStream
類型的stream
款票,從stream
中拿到RTCVideoTrack
實例:
_localVideoTrack = [stream.videoTracks lastObject];
- 用這個
_localVideoTrack
為RTCEAGLVideoView
實例設(shè)置渲染:
[_localVideoTrack addRenderer:localVideoView];
這樣一個視頻圖像就呈現(xiàn)在RTCEAGLVideoView
實例上了,我們只需要把它添加到view
上顯示即可泽论。
這里切記需要注意的是RTCVideoTrack
實例我們必須持有它(這里我們本機設(shè)置為屬性了艾少,而遠程的添加到數(shù)組中,都是為了這么個目的)翼悴。否則有可能會導(dǎo)致視頻圖像無法顯示缚够。
就這樣,一個簡單的WebRTC
客戶端就搭建完了鹦赎,接下來我們先忽略掉Socket
服務(wù)端(先當作已實現(xiàn))谍椅,和WebRTCHelper
的實現(xiàn),我們運行運行demo看看效果:
這是我用手機截的圖古话,因為模擬器無法調(diào)用mac攝像頭雏吭,第一個是本地視頻圖像,而后面的則是遠端用戶傳過來的煞额,如果有n個遠程用戶思恐,則會一直往下排列。
等我們整個講完膊毁,大家可以運行下github
上的demo
胀莹,嘗試嘗試這個視頻聊天室。
接著我們來講講WebRTCHelper
的實現(xiàn):
首先前面順著應(yīng)用這個類的順序來婚温,我們首先調(diào)用了單例描焰,設(shè)置了代理:
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[[self class] alloc] init];
[instance initData];
});
return instance;
}
- (void)initData
{
_connectionDic = [NSMutableDictionary dictionary];
_connectionIdArray = [NSMutableArray array];
}
很簡單,就是初始化了實例栅螟,并且初始化了兩個屬性荆秦,其中是_connectionDic
用來裝RTCPeerConnection
實例的。_connectionIdArray
是用來裝已連接的用戶id的力图。
接著我們調(diào)用了connectServer
:
//初始化socket并且連接
- (void)connectServer:(NSString *)server port:(NSString *)port room:(NSString *)room
{
_server = server;
_room = room;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"ws://%@:%@",server,port]] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
_socket = [[SRWebSocket alloc] initWithURLRequest:request];
_socket.delegate = self;
[_socket open];
}
這個方法連接到了我們的socket
服務(wù)器步绸,這里我們使用的是webScoekt
,使用的框架是谷歌的SocketRocket
吃媒,至于它的用法我就不贅述了瓤介,不熟悉的可以看看樓主的iOS即時通訊,從入門到“放棄”赘那? 刑桑。
這里我們設(shè)置代理為自己,并且建立連接募舟,然后連接成功后祠斧,回調(diào)到成的代理:
- (void)webSocketDidOpen:(SRWebSocket *)webSocket
{
NSLog(@"websocket建立成功");
//加入房間
[self joinRoom:_room];
}
成功的連接后,我們調(diào)用了加入房間的方法拱礁,加入我們一開始設(shè)置的房間號:
- (void)joinRoom:(NSString *)room
{
//如果socket是打開狀態(tài)
if (_socket.readyState == SR_OPEN)
{
//初始化加入房間的類型參數(shù) room房間號
NSDictionary *dic = @{@"eventName": @"__join", @"data": @{@"room": room}};
//得到j(luò)son的data
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil];
//發(fā)送加入房間的數(shù)據(jù)
[_socket send:data];
}
}
加入房間琢锋,我們僅僅是把這個一個json
數(shù)據(jù)用socket
發(fā)給服務(wù)端辕漂,類型為__join
。
接著就是服務(wù)端的邏輯了吩蔑,服務(wù)端拿到這個類型的數(shù)據(jù)钮热,會給我們發(fā)送這么一條消息:
{
data = {
connections = (
);
you = "e297f0c0-fda5-4e67-b4dc-3745943d91bd";
};
eventName = "_peers";
}
這條消息類型是_peers
,意思為房間新用戶烛芬,并且把我們在這個房間的id返回給我們,拿到這條消息飒责,說明我們加入房間成功赘娄,我們就可以去做一系列的初始化了。而connections
這個字段為空宏蛉,說明當前房間沒有人遣臼,如果已經(jīng)有人的話,會返回這么一串:
{
data = {
connections = (
"85fc08a4-77cb-4f45-81f9-c0a0ef1b6949"
);
you = "4b73e126-e9c4-4307-bf8e-20a5a9b1f133";
};
eventName = "_peers";
}
其中connections
里面裝的是已在房間用戶的id拾并。
接著就是我們整個類運轉(zhuǎn)的核心代理方法揍堰,就是收到socket
消息后的處理:
#pragma mark--SRWebSocketDelegate
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message
{
NSLog(@"收到服務(wù)器消息:%@",message);
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:[message dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:nil];
NSString *eventName = dic[@"eventName"];
//1.發(fā)送加入房間后的反饋
if ([eventName isEqualToString:@"_peers"])
{
//得到data
NSDictionary *dataDic = dic[@"data"];
//得到所有的連接
NSArray *connections = dataDic[@"connections"];
//加到連接數(shù)組中去
[_connectionIdArray addObjectsFromArray:connections];
//拿到給自己分配的ID
_myId = dataDic[@"you"];
//如果為空,則創(chuàng)建點對點工廠
if (!_factory)
{
//設(shè)置SSL傳輸
[RTCPeerConnectionFactory initializeSSL];
_factory = [[RTCPeerConnectionFactory alloc] init];
}
//如果本地視頻流為空
if (!_localStream)
{
//創(chuàng)建本地流
[self createLocalStream];
}
//創(chuàng)建連接
[self createPeerConnections];
//添加
[self addStreams];
[self createOffers];
}
//接收到新加入的人發(fā)了ICE候選嗅义,(即經(jīng)過ICEServer而獲取到的地址)
else if ([eventName isEqualToString:@"_ice_candidate"])
{
NSDictionary *dataDic = dic[@"data"];
NSString *socketId = dataDic[@"socketId"];
NSInteger sdpMLineIndex = [dataDic[@"label"] integerValue];
NSString *sdp = dataDic[@"candidate"];
//生成遠端網(wǎng)絡(luò)地址對象
RTCICECandidate *candidate = [[RTCICECandidate alloc] initWithMid:nil index:sdpMLineIndex sdp:sdp];
//拿到當前對應(yīng)的點對點連接
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:socketId];
//添加到點對點連接中
[peerConnection addICECandidate:candidate];
}
//其他新人加入房間的信息
else if ([eventName isEqualToString:@"_new_peer"])
{
NSDictionary *dataDic = dic[@"data"];
//拿到新人的ID
NSString *socketId = dataDic[@"socketId"];
//再去創(chuàng)建一個連接
RTCPeerConnection *peerConnection = [self createPeerConnection:socketId];
if (!_localStream)
{
[self createLocalStream];
}
//把本地流加到連接中去
[peerConnection addStream:_localStream];
//連接ID新加一個
[_connectionIdArray addObject:socketId];
//并且設(shè)置到Dic中去
[_connectionDic setObject:peerConnection forKey:socketId];
}
//有人離開房間的事件
else if ([eventName isEqualToString:@"_remove_peer"])
{
//得到socketId屏歹,關(guān)閉這個peerConnection
NSDictionary *dataDic = dic[@"data"];
NSString *socketId = dataDic[@"socketId"];
[self closePeerConnection:socketId];
}
//這個新加入的人發(fā)了個offer
else if ([eventName isEqualToString:@"_offer"])
{
NSDictionary *dataDic = dic[@"data"];
NSDictionary *sdpDic = dataDic[@"sdp"];
//拿到SDP
NSString *sdp = sdpDic[@"sdp"];
NSString *type = sdpDic[@"type"];
NSString *socketId = dataDic[@"socketId"];
//拿到這個點對點的連接
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:socketId];
//根據(jù)類型和SDP 生成SDP描述對象
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:sdp];
//設(shè)置給這個點對點連接
[peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:remoteSdp];
//把當前的ID保存下來
_currentId = socketId;
//設(shè)置當前角色狀態(tài)為被呼叫,(被發(fā)offer)
_role = RoleCallee;
}
//收到別人的offer之碗,而回復(fù)answer
else if ([eventName isEqualToString:@"_answer"])
{
NSDictionary *dataDic = dic[@"data"];
NSDictionary *sdpDic = dataDic[@"sdp"];
NSString *sdp = sdpDic[@"sdp"];
NSString *type = sdpDic[@"type"];
NSString *socketId = dataDic[@"socketId"];
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:socketId];
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:sdp];
[peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:remoteSdp];
}
}
這里蝙眶,我們對6種事件進行了處理,這6種事件就是我們之前說了半天的信令事件褪那,不過這僅僅是其中的一部分而已幽纷。
簡單的談一下這里對6種信令事件的處理:
注意:這里6種事件的順序希望大家能自己運行demo
打斷點看看,由于各種事件導(dǎo)致收到消息的順序組合比較多博敬,展開講會很亂友浸,所以這里我們僅僅按照代碼的順序來講。
1.收到_peers
:
證明我們新加入房間偏窝,我們就需要對本地的一些東西初始化收恢,其中包括往_connectionIdArray
添加房間已有用戶ID。初始化點對點連接對象的工廠:
if (!_factory)
{
//設(shè)置SSL傳輸
[RTCPeerConnectionFactory initializeSSL];
_factory = [[RTCPeerConnectionFactory alloc] init];
}
創(chuàng)建本地視頻流:
//如果本地視頻流為空
if (!_localStream)
{
//創(chuàng)建本地流
[self createLocalStream];
}
- (void)createLocalStream
{
_localStream = [_factory mediaStreamWithLabel:@"ARDAMS"];
//音頻
RTCAudioTrack *audioTrack = [_factory audioTrackWithID:@"ARDAMSa0"];
[_localStream addAudioTrack:audioTrack];
//視頻
NSArray *deviceArray = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
AVCaptureDevice *device = [deviceArray lastObject];
//檢測攝像頭權(quán)限
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if(authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied)
{
NSLog(@"相機訪問受限");
if ([_delegate respondsToSelector:@selector(webRTCHelper:setLocalStream:userId:)])
{
[_delegate webRTCHelper:self setLocalStream:nil userId:_myId];
}
}
else
{
if (device)
{
RTCVideoCapturer *capturer = [RTCVideoCapturer capturerWithDeviceName:device.localizedName];
RTCVideoSource *videoSource = [_factory videoSourceWithCapturer:capturer constraints:[self localVideoConstraints]];
RTCVideoTrack *videoTrack = [_factory videoTrackWithID:@"ARDAMSv0" source:videoSource];
[_localStream addVideoTrack:videoTrack];
if ([_delegate respondsToSelector:@selector(webRTCHelper:setLocalStream:userId:)])
{
[_delegate webRTCHelper:self setLocalStream:_localStream userId:_myId];
}
}
else
{
NSLog(@"該設(shè)備不能打開攝像頭");
if ([_delegate respondsToSelector:@selector(webRTCHelper:setLocalStream:userId:)])
{
[_delegate webRTCHelper:self setLocalStream:nil userId:_myId];
}
}
}
}
這里利用了系統(tǒng)的AVCaptureDevice
囚枪、AVAuthorizationStatus
派诬,以及RTC的RTCVideoCapturer
、RTCVideoSource
链沼、RTCVideoTrack
等一系列類完成了_localStream
本地流的初始化默赂,至于具體用法,大家看看代碼吧括勺,還是比較簡單缆八,我就不講了曲掰。
我們接著創(chuàng)建了點對點連接核心對象:
[self createPeerConnections];
/**
* 創(chuàng)建所有連接
*/
- (void)createPeerConnections
{
//從我們的連接數(shù)組里快速遍歷
[_connectionIdArray enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL * _Nonnull stop) {
//根據(jù)連接ID去初始化 RTCPeerConnection 連接對象
RTCPeerConnection *connection = [self createPeerConnection:obj];
//設(shè)置這個ID對應(yīng)的 RTCPeerConnection對象
[_connectionDic setObject:connection forKey:obj];
}];
}
- (RTCPeerConnection *)createPeerConnection:(NSString *)connectionId
{
//如果點對點工廠為空
if (!_factory)
{
//先初始化工廠
[RTCPeerConnectionFactory initializeSSL];
_factory = [[RTCPeerConnectionFactory alloc] init];
}
//得到ICEServer
if (!ICEServers) {
ICEServers = [NSMutableArray array];
[ICEServers addObject:[self defaultSTUNServer]];
}
//用工廠來創(chuàng)建連接
RTCPeerConnection *connection = [_factory peerConnectionWithICEServers:ICEServers constraints:[self peerConnectionConstraints] delegate:self];
return connection;
}
大概就是用這兩個方法,創(chuàng)建了RTCPeerConnection
實例奈辰,并且設(shè)置了RTCPeerConnectionDelegate
代理為自己栏妖。最后把它保存在我們的_connectionDic
,對應(yīng)的key
為對方id
。
然后我們給所有RTCPeerConnection
實例添加了流:
[self addStreams];
/**
* 為所有連接添加流
*/
- (void)addStreams
{
//給每一個點對點連接奖恰,都加上本地流
[_connectionDic enumerateKeysAndObjectsUsingBlock:^(NSString *key, RTCPeerConnection *obj, BOOL * _Nonnull stop) {
if (!_localStream)
{
[self createLocalStream];
}
[obj addStream:_localStream];
}];
}
最后吊趾,因為是新加入房間的用戶,所以我們創(chuàng)建了offer:
[self createOffers];
- (void)createOffers
{
//給每一個點對點連接瑟啃,都去創(chuàng)建offer
[_connectionDic enumerateKeysAndObjectsUsingBlock:^(NSString *key, RTCPeerConnection *obj, BOOL * _Nonnull stop) {
_currentId = key;
_role = RoleCaller;
[obj createOfferWithDelegate:self constraints:[self offerOranswerConstraint]];
}];
}
我們?nèi)ケ闅v連接字典论泛,去給每一個連接都去創(chuàng)建一個offer,角色設(shè)置為發(fā)起者RoleCaller
蛹屿。
createOfferWithDelegate
是RTCPeerConnection
的實例方法屁奏,創(chuàng)建一個offer,并且設(shè)置設(shè)置代理為自己RTCSessionDescriptionDelegate
代理為自己错负。
看到這我們發(fā)現(xiàn)除了SRWebSocket
的代理外坟瓢,又多了兩個代理,一個是創(chuàng)建點對點連接的RTCPeerConnectionDelegate
犹撒,一個是創(chuàng)建offer
的RTCSessionDescriptionDelegate
折联。
相信大家看到這會覺得有點凌亂,我們收到socket
消息的代理還沒有講完油航,一下子又多出這么多代理崭庸,沒關(guān)系,我們一步步來看谊囚。
我們先來看看所有的代理方法:
一共如圖這么多怕享,一共隸屬于socket
,點對點連接對象镰踏,還有SDP
(offer或者answer)函筋。
相信前兩者需要代理,大家能明白為什么奠伪,因為是網(wǎng)絡(luò)回調(diào)跌帐,所以使用了代理,而SDP
為什么要使用代理呢绊率?帶著疑惑谨敛,我們先來看看RTCSessionDescriptionDelegate
的兩個代理方法:
//創(chuàng)建了一個SDP就會被調(diào)用,(只能創(chuàng)建本地的)
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didCreateSessionDescription:(RTCSessionDescription *)sdp
error:(NSError *)error
{
NSLog(@"%s",__func__);
//設(shè)置本地的SDP
[peerConnection setLocalDescriptionWithDelegate:self sessionDescription:sdp];
}
上面是第一個代理方法滤否,當我們創(chuàng)建了一個SDP就會被調(diào)用脸狸,因為我們也僅僅只能創(chuàng)建本機的SDP,我們之前調(diào)用createOfferWithDelegate
這個方法,創(chuàng)建成功后就會觸發(fā)這個代理炊甲,在這個代理中我們給這個連接設(shè)置了這個SDP泥彤。
然而調(diào)用setLocalDescriptionWithDelegate
設(shè)置本地SDP,則會觸發(fā)它的第二代理方法(與之相呼應(yīng)的還有一個setRemoteDescriptionWithDelegate
設(shè)置遠程的SDP):
//當一個遠程或者本地的SDP被設(shè)置就會調(diào)用
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didSetSessionDescriptionWithError:(NSError *)error
{
NSLog(@"%s",__func__);
//判斷卿啡,當前連接狀態(tài)為吟吝,收到了遠程點發(fā)來的offer,這個是進入房間的時候颈娜,尚且沒人剑逃,來人就調(diào)到這里
if (peerConnection.signalingState == RTCSignalingHaveRemoteOffer)
{
//創(chuàng)建一個answer,會把自己的SDP信息返回出去
[peerConnection createAnswerWithDelegate:self constraints:[self offerOranswerConstraint]];
}
//判斷連接狀態(tài)為本地發(fā)送offer
else if (peerConnection.signalingState == RTCSignalingHaveLocalOffer)
{
if (_role == RoleCallee)
{
NSDictionary *dic = @{@"eventName": @"__answer", @"data": @{@"sdp": @{@"type": @"answer", @"sdp": peerConnection.localDescription.description}, @"socketId": _currentId}};
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil];
[_socket send:data];
}
//發(fā)送者,發(fā)送自己的offer
else if(_role == RoleCaller)
{
NSDictionary *dic = @{@"eventName": @"__offer", @"data": @{@"sdp": @{@"type": @"offer", @"sdp": peerConnection.localDescription.description}, @"socketId": _currentId}};
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil];
[_socket send:data];
}
}
else if (peerConnection.signalingState == RTCSignalingStable)
{
if (_role == RoleCallee)
{
NSDictionary *dic = @{@"eventName": @"__answer", @"data": @{@"sdp": @{@"type": @"answer", @"sdp": peerConnection.localDescription.description}, @"socketId": _currentId}};
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil];
[_socket send:data];
}
}
}
這個方法無論是設(shè)置本地,還是遠程的SDP官辽,設(shè)置成功后都會調(diào)用炕贵,這里我們根據(jù)_role
的不同,來判斷是應(yīng)該生成offer
還是answer
類型的數(shù)據(jù)來包裹SDP野崇。最后用_socket
把數(shù)據(jù)發(fā)送給服務(wù)端,服務(wù)端在轉(zhuǎn)發(fā)給我們指定的socketId
的用戶亩钟。
注意:這個socketId
是在我們進入房間后乓梨,connections
里獲取到的,或者我們已經(jīng)在房間里清酥,收到別人的offer
拿到的扶镀。
這樣我們一個SDP
生成、綁定焰轻、發(fā)送的流程就結(jié)束了臭觉。
接著我們還是回到SRWebSocketDelegate
的didReceiveMessage
方法中來。
2.我們來講第2種信令事件:_ice_candidate
這個事件辱志,我們在原理中講過蝠筑,其實它的數(shù)據(jù)就是一個對方客戶端的一個公網(wǎng)IP,只不過這個公網(wǎng)IP是由STU Server
下發(fā)的揩懒,為了NAT
/防火墻穿越什乙。
我們收到這種事件,需要把對端的IP保存在點對點連接對象中已球。
我們接著來看看代碼:
//接收到新加入的人發(fā)了ICE候選臣镣,(即經(jīng)過ICEServer而獲取到的地址)
else if ([eventName isEqualToString:@"_ice_candidate"])
{
NSDictionary *dataDic = dic[@"data"];
NSString *socketId = dataDic[@"socketId"];
NSInteger sdpMLineIndex = [dataDic[@"label"] integerValue];
NSString *sdp = dataDic[@"candidate"];
//生成遠端網(wǎng)絡(luò)地址對象
RTCICECandidate *candidate = [[RTCICECandidate alloc] initWithMid:nil index:sdpMLineIndex sdp:sdp];
//拿到當前對應(yīng)的點對點連接
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:socketId];
//添加到點對點連接中
[peerConnection addICECandidate:candidate];
}
我們在這里創(chuàng)建了一個RTCICECandidate
實例candidate
,這個實例用來標識遠端地址智亮。并且把它添加到對應(yīng)ID的peerConnection
中去了忆某。
這里我們僅僅看到接受到遠端的_ice_candidate
,但是要知道這個地址同樣是我們客戶端發(fā)出的阔蛉,那么發(fā)送是在什么地方呢弃舒?
我們來看看RTCPeerConnectionDelegate
,有這么一個代理方法:
//創(chuàng)建peerConnection之后馍忽,從server得到響應(yīng)后調(diào)用棒坏,得到ICE 候選地址
- (void)peerConnection:(RTCPeerConnection *)peerConnection
gotICECandidate:(RTCICECandidate *)candidate
{
NSLog(@"%s",__func__);
NSDictionary *dic = @{@"eventName": @"__ice_candidate", @"data": @{@"label": [NSNumber numberWithInteger:candidate.sdpMLineIndex], @"candidate": candidate.sdp, @"socketId": _currentId}};
NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil];
[_socket send:data];
}
當我們創(chuàng)建peerConnection
的時候燕差,就會去我們一開始初始化的時候,添加的ICEServers
數(shù)組中坝冕,去ICE Server地址中去請求徒探,得到ICECandidate
就會調(diào)用這個代理方法,我們在這里用socket
把自己的網(wǎng)絡(luò)地址發(fā)送給了對端喂窟。
講到這個ICEServers
,我們這里提一下测暗,這里需要一個STUN
服務(wù)器,這里我們用的是谷歌的:
static NSString *const RTCSTUNServerURL = @"stun:stun.l.google.com:19302";
//初始化STUN Server (ICE Server)
- (RTCICEServer *)defaultSTUNServer {
NSURL *defaultSTUNServerURL = [NSURL URLWithString:RTCSTUNServerURL];
return [[RTCICEServer alloc] initWithURI:defaultSTUNServerURL
username:@""
password:@""];
}
有些STUN
服務(wù)器可能被墻磨澡,下面這些提供給大家備用碗啄,或者可以自行搭建:
stun.l.google.com:19302
stun1.l.google.com:19302
stun2.l.google.com:19302
stun3.l.google.com:19302
stun4.l.google.com:19302
stun01.sipphone.com
stun.ekiga.net
stun.fwdnet.net
stun.ideasip.com
stun.iptel.org
stun.rixtelecom.se
stun.schlund.de
stunserver.org
stun.softjoys.com
stun.voiparound.com
stun.voipbuster.com
stun.voipstunt.com
stun.voxgratia.org
stun.xten.com
3.我們回到didReceiveMessage
代理來講第3種信令事件:_new_peer
else if ([eventName isEqualToString:@"_new_peer"])
{
NSDictionary *dataDic = dic[@"data"];
//拿到新人的ID
NSString *socketId = dataDic[@"socketId"];
//再去創(chuàng)建一個連接
RTCPeerConnection *peerConnection = [self createPeerConnection:socketId];
if (!_localStream)
{
[self createLocalStream];
}
//把本地流加到連接中去
[peerConnection addStream:_localStream];
//連接ID新加一個
[_connectionIdArray addObject:socketId];
//并且設(shè)置到Dic中去
[_connectionDic setObject:peerConnection forKey:socketId];
}
這個_new_peer
表示你已經(jīng)在房間,這時候有新的用戶加入稳摄,這時候你需要為這個用戶再去創(chuàng)建一個點對點連接對象peerConnection
稚字。
并且把本地流加到這個新的對象中去,然后設(shè)置_connectionIdArray
和_connectionDic
厦酬。
4.第4種信令事件:_remove_peer
//有人離開房間的事件
else if ([eventName isEqualToString:@"_remove_peer"])
{
//得到socketId胆描,關(guān)閉這個peerConnection
NSDictionary *dataDic = dic[@"data"];
NSString *socketId = dataDic[@"socketId"];
[self closePeerConnection:socketId];
}
這個事件是有人離開了,我們則需要調(diào)用closePeerConnection
:
/**
* 關(guān)閉peerConnection
*
* @param connectionId <#connectionId description#>
*/
- (void)closePeerConnection:(NSString *)connectionId
{
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:connectionId];
if (peerConnection)
{
[peerConnection close];
}
[_connectionIdArray removeObject:connectionId];
[_connectionDic removeObjectForKey:connectionId];
dispatch_async(dispatch_get_main_queue(), ^{
if ([_delegate respondsToSelector:@selector(webRTCHelper:closeWithUserId:)])
{
[_delegate webRTCHelper:self closeWithUserId:connectionId];
}
});
}
關(guān)閉peerConnection
仗阅,并且從_connectionIdArray
昌讲、_connectionDic
中移除,然后對外調(diào)用關(guān)閉連接的代理减噪。
5.第5種信令事件:_offer
這個事件短绸,是別人新加入房間后,會發(fā)出的offer
筹裕,提出與我們建立點對點連接醋闭。
我們來看看處理:
//這個新加入的人發(fā)了個offer
else if ([eventName isEqualToString:@"_offer"])
{
NSDictionary *dataDic = dic[@"data"];
NSDictionary *sdpDic = dataDic[@"sdp"];
//拿到SDP
NSString *sdp = sdpDic[@"sdp"];
NSString *type = sdpDic[@"type"];
NSString *socketId = dataDic[@"socketId"];
//拿到這個點對點的連接
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:socketId];
//根據(jù)類型和SDP 生成SDP描述對象
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:sdp];
//設(shè)置給這個點對點連接
[peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:remoteSdp];
//把當前的ID保存下來
_currentId = socketId;
//設(shè)置當前角色狀態(tài)為被呼叫,(被發(fā)offer)
_role = RoleCallee;
}
這里我們從offer
中拿到SDP饶碘,并且調(diào)用我們之前提到的setRemoteDescriptionWithDelegate
設(shè)置遠端的SDP目尖,這個設(shè)置成功后,又調(diào)回到SDP的代理方法:didSetSessionDescriptionWithError
中去了扎运。
在這代理方法我們生成了一個answer
瑟曲,把本機的SDP包裹起來傳了過去。如此形成了一個閉環(huán)豪治。
6.第6種信令事件:_answer
這個事件是自己發(fā)出offer后洞拨,得到別人的awser
回答,這時候我們需要做的僅僅是保存起來遠端SDP
即可负拟,到這一步兩端互相有了對方的SDP
烦衣。
而兩端的事件,是當SDP
和ICE Candidate
,都交換完成后花吟,點對點連接才建立完成秸歧。
至此6種信令事件講完了,通過這些信令衅澈,我們完成了加入房間键菱,退出房間,建立連接等控制過程今布。
這個類基本上核心的東西就這些了经备,其他的一些零碎的小細節(jié),包括連接成功后部默,遠端的流過來調(diào)用RTCPeerConnectionDelegate
代理等等:
// Triggered when media is received on a new stream from remote peer.
- (void)peerConnection:(RTCPeerConnection *)peerConnection
addedStream:(RTCMediaStream *)stream
{
NSLog(@"%s",__func__);
dispatch_async(dispatch_get_main_queue(), ^{
if ([_delegate respondsToSelector:@selector(webRTCHelper:addRemoteStream:userId:)])
{
[_delegate webRTCHelper:self addRemoteStream:stream userId:_currentId];
}
});
}
在這里我們僅僅是把這個視頻流用主線程回調(diào)出去給外部代理處理侵蒙,而點對點連接關(guān)閉的時候也是這么處理的,這樣就和我們之前提到的對外代理方法銜接起來了傅蹂。
其他的大家可以自己去demo
中查看吧纷闺。
接著我們客戶端講完了,這里我們略微帶過一下我們的WebSocket
服務(wù)端份蝴,這里我們?nèi)匀挥玫?code>Node.js急但,為什么用用它呢?因為太多好用的簡單好用的框架了搞乏,簡直不用動腦子...
這里我們用了skyrtc
框架,具體代碼如下:
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var SkyRTC = require('skyrtc').listen(server);
var path = require("path");
var port = process.env.PORT || 3000;
server.listen(port);
app.use(express.static(path.join(__dirname, 'public')));
app.get('/', function(req, res) {
res.sendfile(__dirname + '/index.html');
});
SkyRTC.rtc.on('new_connect', function(socket) {
console.log('創(chuàng)建新連接');
});
SkyRTC.rtc.on('remove_peer', function(socketId) {
console.log(socketId + "用戶離開");
});
SkyRTC.rtc.on('new_peer', function(socket, room) {
console.log("新用戶" + socket.id + "加入房間" + room);
});
SkyRTC.rtc.on('socket_message', function(socket, msg) {
console.log("接收到來自" + socket.id + "的新消息:" + msg);
});
SkyRTC.rtc.on('ice_candidate', function(socket, ice_candidate) {
console.log("接收到來自" + socket.id + "的ICE Candidate");
});
SkyRTC.rtc.on('offer', function(socket, offer) {
console.log("接收到來自" + socket.id + "的Offer");
});
SkyRTC.rtc.on('answer', function(socket, answer) {
console.log("接收到來自" + socket.id + "的Answer");
});
SkyRTC.rtc.on('error', function(error) {
console.log("發(fā)生錯誤:" + error.message);
});
基本上戒努,用了這個框架请敦,我們除了打印之外,沒有做任何的處理储玫,所有的消息轉(zhuǎn)發(fā)侍筛,都是由框架內(nèi)部識別并且處理完成的。
這里需要提一下的是撒穷,由于作者沒有那么富帥匣椰,沒那么多手機,所以在這里用瀏覽器來充當一部分的客戶端端礼,所以你會看到禽笑,這里用了http
框架,監(jiān)聽了本機3000
端口蛤奥,如果誰調(diào)用網(wǎng)頁的則去渲染當前文件下的index.html
佳镜。
在這里,用index.html
和SkyRTC-client.js
兩個文件實現(xiàn)了瀏覽器端的WebRTC
通信凡桥,這樣就可以移動端和移動端蟀伸、移動端和瀏覽器、瀏覽器與瀏覽器之間在同一個聊天室進行視頻通話了。
至于源碼我就不講了啊掏,大家可以到demo
中去查看蠢络,這個瀏覽器端的代碼是我從下面文章的作者github
中找來的:
WebRTC的RTCDataChannel
使用WebRTC搭建前端視頻聊天室——信令篇
使用WebRTC搭建前端視頻聊天室——入門篇
提倡大家去看看,他很詳細的講了WebRTC
在Web
端的實現(xiàn)迟蜜,和iOS
端實現(xiàn)的基本原理刹孔、流程是一樣的,只是API
略有不同小泉。
本文demo地址:WebRTC_iOS
大家在運行demo
的時候需要注意以下幾點:
- 運行
WebSocket
服務(wù)端芦疏,直接用命令行CD到server.js
所在目錄下:
直接命令行中執(zhí)行(需要安裝nodejs環(huán)境)
node server.js
這樣Socket
服務(wù)端就運行起來了,此時你可以打開瀏覽器輸入
localhost:3000#100
此3000為端口號微姊,100為聊天室房間號酸茴,如果出現(xiàn)以下圖像,說明Socket
服務(wù)端和Web
客戶端已完成兢交。
- 接著我們要去運行iOS的客戶端了薪捍,首先我們需要去百度網(wǎng)盤下載
WebRTC
頭文件和靜態(tài)庫.a。
下載完成配喳,解壓縮酪穿,直接按照本文第二條中:iOS下WebRTC環(huán)境的搭建即可。
程序能運行起來后晴裹,接著我們需要替換VideoChatViewController
中的server
地址:
[[WebRTCHelper sharedInstance]connectServer:@"192.168.0.7" port:@"3000" room:@"100"];
這里的server地址被济,如果你是用和本機需要替換成localhost
,而如果你是用手機等涧团,則需要和電腦同處一個局域網(wǎng)(wifi下)只磷,并且IP地址一致才行。
在這里由于我的電腦IP地址是192.168.0.7
:
所以我在手機上運行泌绣,連接到這個server
钮追,也就是連接到電腦。
至此就可以看到iOS端的視頻聊天效果了阿迈,大家可以多開幾個Web
客戶端看看效果元媚。
寫在結(jié)尾:
引用這篇文章:從demo到實用,中間還差1萬個WebRTC里的一段話來結(jié)尾吧:
WebRTC開源之前苗沧,實時音視頻通信聽起來好高級:回聲消除刊棕、噪聲抑制……對于看到傅里葉變換都頭疼的工程師很難搞定這些專業(yè)領(lǐng)域的問題。
Google收購了GIPS待逞,開源了WebRTC項目之后鞠绰,開發(fā)者可以自己折騰出互聯(lián)網(wǎng)音視頻通信了。下載飒焦、編譯蜈膨、集成之后屿笼,第一次聽到通過互聯(lián)網(wǎng)傳過來的喂喂喂,工程師會非常興奮翁巍,demo到萬人直播現(xiàn)場只差一步了驴一。
但是,電信行業(yè)要求可用性4個9灶壶,而剛剛讓人興奮的“喂喂喂”肝断,1個9都到不了。某公司在展會上演示跨國音視頻驰凛,多次呼叫無法接通胸懈,自嘲說我們還沒有做網(wǎng)絡(luò)優(yōu)化嘛。這就等于互聯(lián)網(wǎng)全民創(chuàng)業(yè)時期的”就差個程序員了“恰响,本質(zhì)上是和demo與真正產(chǎn)品之間的差距趣钱,是外行與內(nèi)行之間的差距。
IM的路還有很長胚宦,一萬個WebRTC已經(jīng)走過了一個首有?
注:源代碼運行后有小伙伴反映移動端連接黑屏的問題,經(jīng)張速同學(xué)的提醒枢劝,原因如下:
修改的地方大致如上圖所述井联,主要是發(fā)送ICE的時候添加了一個id字段的數(shù)據(jù),這個字段的內(nèi)容為candidate.stpMid您旁。
官方對這個stpMid字段的解釋是:
// If present, this contains the identifier of the "media stream
// identification" as defined in [RFC 3388] for m-line this candidate is
// associated with.
意思是這個字段是用來標識流媒體的id烙常,這個字段需要和ICE綁定在一起。
至于瀏覽器端為什么不會有影響鹤盒,原因應(yīng)該是web
端和移動端的SDK
差異所導(dǎo)致的军掂。
所以除了客戶端需要添加這個字段外,在我們server端昨悼,找到SkyRTC.js,也需要添加這個id字段跃洛,把它轉(zhuǎn)發(fā)給另一個客戶端率触,添加上后,移動端之間視頻聊天應(yīng)該就不會有問題了汇竭。
github
上的代碼我已經(jīng)修改過了葱蝗,重新拉一下代碼即可。
除此之外细燎,如果不同網(wǎng)段之間两曼,出現(xiàn)視頻聊天黑屏的問題,那么很可能是STUN服務(wù)器導(dǎo)致的玻驻,建議多嘗試幾個STUN試試悼凑,也可以自行搭建偿枕。