iOS下音視頻通信-基于WebRTC

前言:

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ù)器:

  1. 瀏覽器之間交換建立通信的元數(shù)據(jù)(信令)必須通過服務(wù)器。
  2. 為了穿越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通信連接。

當然信令能做的事還有很多,這里大概列了一下:
  1. 用來控制通信開啟或者關(guān)閉的連接控制消息
  2. 發(fā)生錯誤時用來彼此告知的消息
  3. 媒體流元數(shù)據(jù),比如像解碼器、解碼器的配置、帶寬、媒體類型等等
  4. 用來建立安全連接的關(guān)鍵數(shù)據(jù)
  5. 外界所看到的的網(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è)備的攝像頭及話筒獲得視頻、音頻的同步流
  • RTCPeerConnectionRTCPeerConnection是WebRTC用于構(gòu)建點對點之間穩(wěn)定、高效的流傳輸?shù)慕M件
  • RTCDataChannelRTCDataChannel使得瀏覽器之間(點對點)建立一個高吞吐量、低延時的信道,用于傳輸任意數(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連接的過程:
  1. A和B連接上服務(wù)端,建立一個TCP長連接(任意協(xié)議都可以,WebSocket/MQTT/Socket原生/XMPP),我們這里為了省事,直接采用WebSocket,這樣一個信令通道就有了。
  2. A從ice server(STUN Server)獲取ice candidate并發(fā)送給Socket服務(wù)端,并生成包含session description(SDP)的offer,發(fā)送給Socket服務(wù)端。
  3. Socket服務(wù)端把A的offer和ice candidate轉(zhuǎn)發(fā)給B,B會保存下A這些信息。
  4. 然后B發(fā)送包含自己session descriptionanswer(因為它收到的是offer,所以返回的是answer,但是內(nèi)容都是SDP)和ice candidate給Socket服務(wù)端。
  5. Socket服務(wù)端把B的answerice candidate給A,A保存下B的這些信息。

至此A與B建立起了一個P2P連接。

這里理解整個P2P連接的流程是非常重要的,否則后面代碼實現(xiàn)部分便難以理解。

四.iOS客戶端的詳細實現(xiàn),以及服務(wù)端信令通道的搭建。
聊天室中的信令

上面是兩個用戶之間的信令交換流程,但我們需要建立一個多用戶在線視頻聊天的聊天室。所以需要進行一些擴展,來達到這個要求

用戶操作

首先需要確定一個用戶在聊天室中的操作大致流程:

  1. 打開頁面連接到服務(wù)器上
  2. 進入聊天室
  3. 與其他所有已在聊天室的用戶建立點對點的連接,并輸出在頁面上
  4. 若有聊天室內(nèi)的其他用戶離開,應(yīng)得到通知,關(guān)閉與其的連接并移除其在頁面中的輸出
  5. 若又有其他用戶加入,應(yīng)得到通知,建立于新加入用戶的連接,并輸出在頁面上
  6. 離開頁面,關(guān)閉所有連接
從上面可以看出來,除了點對點連接的建立,還需要服務(wù)器至少做如下幾件事:
  1. 新用戶加入房間時,發(fā)送新用戶的信息給房間內(nèi)的其他用戶
  2. 新用戶加入房間時,發(fā)送房間內(nèi)的其他用戶信息給新加入房間的用戶
  3. 用戶離開房間時,發(fā)送離開用戶的信息給房間內(nèi)的其他用戶
實現(xiàn)思路

以使用WebSocket為例,上面用戶操作的流程可以進行以下修改:

  1. 客戶端與服務(wù)器建立WebSocket連接
  2. 發(fā)送一個加入聊天室的信令(join),信令中需要包含用戶所進入的聊天室名稱
  3. 服務(wù)器根據(jù)用戶所加入的房間,發(fā)送一個其他用戶信令(peers),信令中包含聊天室中其他用戶的信息,客戶端根據(jù)信息來逐個構(gòu)建與其他用戶的點對點連接
  4. 若有用戶離開,服務(wù)器發(fā)送一個用戶離開信令(remove_peer),信令中包含離開的用戶的信息,客戶端根據(jù)信息關(guān)閉與離開用戶的信息,并作相應(yīng)的清除操作
  5. 若有新用戶加入,服務(wù)器發(fā)送一個用戶加入信令(new_peer),信令中包含新加入的用戶的信息,客戶端根據(jù)信息來建立與這個新用戶的點對點連接
  6. 用戶離開頁面,關(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個可選的方法,分別為:

  1. 本地設(shè)置流的回調(diào),可以用來顯示本地的視頻圖像。
  2. 遠程流到達的回調(diào),可以用來顯示對方的視頻圖像。
  3. 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步:

  1. 創(chuàng)建一個RTCEAGLVideoView類型的實例。
  2. 從代理回調(diào)中拿到RTCMediaStream類型的stream,從stream中拿到RTCVideoTrack實例:
_localVideoTrack = [stream.videoTracks lastObject];
  1. 用這個_localVideoTrackRTCEAGLVideoView實例設(shè)置渲染:
[_localVideoTrack addRenderer:localVideoView];

這樣一個視頻圖像就呈現(xiàn)在RTCEAGLVideoView實例上了,我們只需要把它添加到view上顯示即可。

這里切記需要注意的是RTCVideoTrack實例我們必須持有它(這里我們本機設(shè)置為屬性了,而遠程的添加到數(shù)組中,都是為了這么個目的)。否則有可能會導(dǎo)致視頻圖像無法顯示。

就這樣,一個簡單的WebRTC客戶端就搭建完了,接下來我們先忽略掉Socket服務(wù)端(先當作已實現(xiàn)),和WebRTCHelper的實現(xiàn),我們運行運行demo看看效果:

Paste_Image.png

這是我用手機截的圖,因為模擬器無法調(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
createOfferWithDelegateRTCPeerConnection的實例方法,創(chuàng)建一個offer,并且設(shè)置設(shè)置代理為自己RTCSessionDescriptionDelegate代理為自己。

看到這我們發(fā)現(xiàn)除了SRWebSocket的代理外,又多了兩個代理,一個是創(chuàng)建點對點連接的RTCPeerConnectionDelegate,一個是創(chuàng)建offerRTCSessionDescriptionDelegate。

相信大家看到這會覺得有點凌亂,我們收到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é)束了。

接著我們還是回到SRWebSocketDelegatedidReceiveMessage方法中來。

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。

而兩端的事件,是當SDPICE 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.htmlSkyRTC-client.js兩個文件實現(xiàn)了瀏覽器端的WebRTC通信,這樣就可以移動端和移動端、移動端和瀏覽器、瀏覽器與瀏覽器之間在同一個聊天室進行視頻通話了。

至于源碼我就不講了,大家可以到demo中去查看,這個瀏覽器端的代碼是我從下面文章的作者github中找來的:
WebRTC的RTCDataChannel
使用WebRTC搭建前端視頻聊天室——信令篇
使用WebRTC搭建前端視頻聊天室——入門篇

提倡大家去看看,他很詳細的講了WebRTCWeb端的實現(xiàn),和iOS端實現(xiàn)的基本原理、流程是一樣的,只是API略有不同。

本文demo地址:WebRTC_iOS
大家在運行demo的時候需要注意以下幾點:
  1. 運行WebSocket服務(wù)端,直接用命令行CD到server.js所在目錄下:
    Paste_Image.png

直接命令行中執(zhí)行(需要安裝nodejs環(huán)境)

node server.js

這樣Socket服務(wù)端就運行起來了,此時你可以打開瀏覽器輸入

localhost:3000#100

此3000為端口號,100為聊天室房間號,如果出現(xiàn)以下圖像,說明Socket服務(wù)端和Web客戶端已完成。

  1. 接著我們要去運行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é)的提醒,原因如下:
Paste_Image.png
Paste_Image.png

修改的地方大致如上圖所述,主要是發(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試試,也可以自行搭建。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容