WebRTC 入門教程(四)| iOS 端如何使用 WebRTC

做者:李超,資深音視頻工程師,有多年的音視頻相關開發經驗。ios

文章首發於 RTC 開發者社區,如遇到開發問題,請點擊這裏給做者留言。git

前言

以前,我已經寫過 Android 端如何使用 WebRTC 的文章。在那篇文章中,我向你們介紹了在 Android 端是如何使用 WebRTC 進行音視頻通話的。今天,咱們再來看看 iOS 端1對1音視頻實時通話的具體實現。github

iOS 端的實現邏輯與 Android 端基本相同,最大的區別多是語言方面的差別啦!因此,下面我基本上仍是按照介紹 Android 端同樣的過程來介紹 iOS 端的實現。具體步驟以下:swift

  • 權限申請
  • 引入 WebRTC 庫
  • 採集並顯示本地視頻
  • 信令驅動
  • 建立音視頻數據通道
  • 媒體協商
  • 渲染遠端視頻

經過上面幾個小節,全面介紹如何在iOS端如何使用 WebRTC。數組

申請權限

首先,咱們來看一下 iOS 端是如何獲取訪問音視頻設備權限的。相比 Android 端而言,iOS端獲取相關權限要容易不少。其步驟以下:瀏覽器

  • 打開項目,點擊左側目錄中的項目。
  • 在左側目錄找到 info.plist,並將其打開。
  • 點擊 右側 看到 「+」 號的地方。
  • 添加 Camera 和 Microphone 訪問權限。

下面這張圖更清晰的展示了申請權限的步驟:bash

經過以上步驟,咱們就將訪問音視頻設備的權限申請好了。申請完權限後,下面咱們來看一下iOS端如何引入 WebRTC 庫。

引入WebRTC庫

在iOS端引入 WebRTC 庫有兩種方式:服務器

  • 第一種,是經過 WebRTC 源碼編譯出 WebRTC 庫,而後在項目中手動引入它;
  • 第二種方式,是 WebRTC 官方會按期發佈編譯好的 WebRTC 庫,咱們可使用 Pod 方式進行安裝。

在本項目中,咱們使用第二種方式。socket

使用第二種方式引入 WebRTC 庫很是簡單,咱們只須要寫個 Podfile 文件就能夠了。在 Podfile 中能夠指定下載 WebRTC 庫的地址,以及咱們要安裝的庫的名子。async

Podfile 文件的具體格式以下:

source 'https://github.com/CocoaPods/Specs.git'
  
platform :ios,'11.0'

target 'WebRTC4iOS2' do

pod 'GoogleWebRTC'

end

複製代碼
  • source,指定了庫文件從哪裏下載
  • platform,指定了使用的平臺及平臺版本
  • target,指定項目的名子
  • pod,指定要安裝的庫

有了 Podfile 以後,在當前目錄下執行 pod install 命令,這樣 Pod 工具就能夠將 WebRTC 庫從源上來載下來。

在執行 pod install 以後,它除了下載庫文件以外,會爲咱們產生一個新的工做空間文件,即**{project}.xcworkspace**。在該文件裏,會同時加載項目文件及剛纔安裝好的 Pod 依賴庫,並使二者創建好關聯。

這樣,WebRTC庫就算引入成功了。下面就能夠開始寫咱們本身的代碼了。

獲取本地視頻

WebRTC 庫引入成功以後,咱們就能夠開始真正的 WebRTC 之旅了。下面,咱們來看一下如何獲取本地視頻並將其展現出來。

在獲取視頻以前,咱們首先要選擇使用哪一個視頻設備採集數據。在WebRTC中,咱們能夠經過RTCCameraVideoCapture 類獲取全部的視頻設備。以下所示:

NSArray<AVCaptureDevice*>* devices = [RTCCameraVideoCapture captureDevices];
AVCaptureDevice* device = devices[0];
複製代碼

經過上面兩行代碼,咱們就拿到了視頻設備中的第一個設備。簡單吧!

固然,光有設備還不行。咱們還要清楚從設備中採集的數據放到哪裏了,這樣咱們才能將其展現出來。

WebRTC 爲咱們提供了一個專門的類,即 RTCVideoSource。它有兩層含義:

  • 一是代表它是一個視頻。當咱們要展現視頻的時候,就從這裏獲取數據;
  • 另外一方面,它也是一個終點。即,當咱們從視頻設備採集到視頻數據時,要交給它暫存起來。

除此以外,爲了能更方便的控制視頻設備,WebRTC 提供了一個專門用於操做設備的類,即 RTCCameraVideoCapture。經過它,咱們就能夠自如的控制視頻設備了。

經過上面介紹的兩個類,以及前面介紹的 AVCaptureDevice,咱們就能夠輕鬆的將視頻數據採集出來了。下面咱們就來具體看一下代碼吧!

在該代碼中,首先將 RTCVideoSourceRTCCameraVideoCapture 進行綁定,而後再開啓設備,這樣視頻數據就源源不斷的被採集到 RTCVideoSource 中了。

...

RTCVideoSource* videoSource = [factory videoSource];
capture = [[RTCCameraVideoCapturer alloc] initWithDelegate:videoSource];

...

[capture startCaptureWithDevice:device
                             format:format
                                fps:fps];
...
複製代碼

經過上面的幾行代碼就能夠從攝像頭捕獲視頻數據了。

這裏有一點須要特別強調一下,就是 factory 對象。在 WebRTC Native 層,factory 能夠說是 「萬物的根源」,像 RTCVideoSource、RTCVideoTrack、RTCPeerConnection 這些類型的對象,都須要經過 factory 來建立。 那麼,factory 對象又是如何建立出來的呢?

經過下面的代碼你就能夠一知究竟了:

...

[RTCPeerConnectionFactory initialize];
    
//若是點對點工廠爲空
if (!factory)
{
    RTCDefaultVideoDecoderFactory* decoderFactory = [[RTCDefaultVideoDecoderFactory alloc] init];
    RTCDefaultVideoEncoderFactory* encoderFactory = [[RTCDefaultVideoEncoderFactory alloc] init];
    NSArray* codecs = [encoderFactory supportedCodecs];
    [encoderFactory setPreferredCodec:codecs[2]];
    
    factory = [[RTCPeerConnectionFactory alloc] initWithEncoderFactory: encoderFactory
                                                        decoderFactory: decoderFactory];

}
...

複製代碼

在上面代碼中,

  • 首先要調用 RTCPeerConnectionFactory 類的 initialize 方法進行初始化;
  • 而後建立 factory 對象。須要注意的是,在建立 factory 對象時,傳入了兩個參數:一個是默認的編碼器;一個是默認的解碼器。咱們能夠經過修改這兩個參數來達到使用不一樣編解碼器的目的。

有了 factory 對象後,咱們就能夠開始建立其它對象了。那麼,緊接下來的問題就是如何將採集到的視頻展現出來了。

在iOS端展現本地視頻與Android端仍是有很大區別的,這主要是因爲不一樣系統底層實現方式不同。爲了更高效的展現本地視頻,它們採用了不一樣的方式。

在iOS端展現本地視頻其實很是的簡單,只須要在調用 capture 的 startCaptureWithDevice 方法以前執行下面的語句就行了:

self.localVideoView.captureSession = capture.captureSession;
複製代碼

固然,在iOS頁面初始化的時候,必定要記得定義 localVideoView 喲,其類型爲 RTCCameraPreviewView

經過上面的步驟,咱們就能夠看到視頻設備採集到的視頻圖像了。

信令驅動

上面咱們介紹了iOS端權限的申請,WebRTC庫的引入,以及本地視頻的採集與展現,這些功能實現起來都很簡單。但接下來咱們要介紹的信令就要複雜一些了。

在任何系統中,均可以說信令是系統的靈魂。例如,由誰來發起呼叫;媒體協商時,什麼時間發哪一種 SDP 都是由信令控制的。

對於本項目來講,它的信令相對仍是比較簡單,它包括下面幾種信令:

客戶端命令

  • join,用戶加入房間
  • leave,用戶離開房間
  • message,端到端命令(offer、answer、candidate)

服務端命令

  • joined,用戶已加入
  • leaved,用戶已離開
  • other_joined,其它用戶已加入
  • bye,其它用戶已離開
  • full,房間已滿

這些信令之間是怎樣一種關係?在什麼狀況下該發送怎樣的信令呢?要回答這個問題咱們就要看一下信令狀態機了。

信令狀態機

在 iOS 端的信令與咱們以前介紹的 js端 和 Android 端同樣,會經過一個信令狀態機來管理。在不一樣的狀態下,須要發不一樣的信令。一樣的,當收到服務端,或對端的信令後,狀態會隨之發生改變。下面咱們來看一下這個狀態的變化圖吧:

在初始時,客戶端處於 init/leaved 狀態。

  • 在 init/leaved 狀態下,用戶只能發送 join 消息。服務端收到 join 消息後,會返回 joined 消息。此時,客戶端會更新爲 joined 狀態。
  • joined 狀態下,客戶端有多種選擇,收到不一樣的消息會切到不一樣的狀態:
    • 若是用戶離開房間,那客戶端又回到了初始狀態,即 init/leaved 狀態。
    • 若是客戶端收到 second user join 消息,則切換到 join_conn 狀態。在這種狀態下,兩個用戶就能夠進行通話了。
    • 若是客戶端收到 second user leave 消息,則切換到 join_unbind 狀態。其實 join_unbind 狀態與 joined 狀態基本是一致的。
  • 若是客戶端處於 join_conn 狀態,當它收到 second user leave 消息時,也會轉成 joined_unbind 狀態。
  • 若是客戶端是 joined_unbind 狀態,當它收到 second user join 消息時,會切到 join_conn 狀態。

經過上面的狀態圖,咱們就很是清楚的知道了在什麼狀態下應該發什麼信令;或者說,發什麼樣的信令,狀態會發生怎樣的變化了。

引入 socket.io 庫

看過我以前文章的同窗應該都清楚,不管是在 js端,仍是在 Android 端的實時通話中,我一直使用 socket.io庫做爲信令的基礎庫。之因此選擇 socket.io,

  • 一方面是因爲它支持跨平臺,這樣在各個平臺上咱們均可以保持相同的邏輯;
  • 另外一方面,socket.io 使用簡單,功能又很是強大;

不過,在 iOS 端的 socket.io 是用 swift 語言實現的,而咱們的1對1系統則是用 Object-C 實現的。那麼,就帶來一個問題,在 OC (Object-C) 裏是否能夠直接使用 swift 編寫的庫呢?

答案是確定的。咱們只須要在 Podfile 中 增長 use_frameworks! 指令便可。 因此,咱們的 Podfile 如今應該變成這個樣子:

source 'https://github.com/CocoaPods/Specs.git'
  
platform :ios,'11.0'

use_frameworks!

target 'WebRTC4iOS2' do

pod 'Socket.IO-Client-Swift', '~> 13.3.0'
pod 'GoogleWebRTC'

end
複製代碼

上面 Podfile 中,每行的含義你們應該都很清楚了,我這裏就不作過多講解了。

信令的使用

socket.io 庫引入成功後,下面咱們來看一下何使用 socket.io。在 iOS 下,使用 socket.io 分爲三步:

  • 經過 url 獲取 socket。有了 socket 以後咱們就可創建與服務器的鏈接了。
  • 註冊偵聽的消息,併爲每一個偵聽的消息綁定一個處理函數。當收到服務器的消息後,隨之會觸發綁定的函數。
  • 經過 socket 創建鏈接。
  • 發送消息。

下咱們咱們就逐一的看它們是如何實現的吧!

獲取 socket

在 iOS 中獲取 socket 其實很簡單,咱們來看一下代碼:

NSURL* url = [[NSURL alloc] initWithString:addr];
manager = [[SocketManager alloc] initWithSocketURL:url
                                            config:@{
                                            	@"log": @YES,
    											@"forcePolling":@YES,
                                                @"forceWebsockets":@YES
                                                }];
socket = manager.defaultSocket;
複製代碼

沒錯,經過這三行代碼就能夠了。至於爲何這麼寫我就不解釋了,你們記下來就行了。這是 socket.io的固定格式。

註冊偵聽消息

使用 socket.io 註冊一個偵聽消息也很是容易,以下所示:

[socket on:@"joined" callback:^(NSArray * data, SocketAckEmitter * ack) {
    NSString* room = [data objectAtIndex:0];
    
    NSLog(@"joined room(%@)", room);
    
    [self.delegate joined:room];
    
}];
複製代碼

上面就是註冊一個 joined 消息,並給它綁定一個匿名的處理函數。若是帶來的消息還有參數的話,咱們能夠從 data 這個數組中獲取到。

一樣的道理,若是咱們想註冊一個新的偵聽消息,能夠按着上面的格式,只需將 joined 替換一下就能夠了。

創建鏈接 這個就更簡單了,下接上代碼了:

[socket connect];
複製代碼

沒錯,只這一句鏈接就建好了哈!

發送消息 接下來,讓咱們看一下如何使用 socket.io 發送消息。

...
if(socket.status == SocketIOStatusConnected){
    [socket emit:@"join" with:@[room]];
}
...
複製代碼

socket.io 使用 emit 方法發送消息。它能夠帶一些參數,這些參數都被放在一個數據裏。在上面的代碼中,首先要判斷socket是否已經處理鏈接狀態,只有處於鏈接狀態時,消息才能被真正發送出去。

以上就是 socket.io 的使用,是否是很是的簡單?

建立 RTCPeerConnection

信令系統創建好後,後面的邏輯都是圍繞着信令系統創建起來的。RTCPeerConnection 對象的創建也不例外。

在客戶端,用戶要想與遠端通話,首先要發送 join 消息,也就是要先進入房間。此時,若是服務器斷定用戶是合法的,則會給客戶端回 joined 消息。

客戶端收到 joined 消息後,就要建立 RTCPeerConnection 了,也就是要創建一條與遠端通話的音視頻數據傳輸通道。

下面,咱們就來看一下 RTCPeerConnection 是如何創建的:

...

if (!ICEServers) {
    ICEServers = [NSMutableArray array];
    [ICEServers addObject:[self defaultSTUNServer]];
}

RTCConfiguration* configuration = [[RTCConfiguration alloc] init];
[configuration setIceServers:ICEServers];
RTCPeerConnection* conn = [factory
                                 peerConnectionWithConfiguration:configuration
                                                     constraints:[self defaultPeerConnContraints]
                                                        delegate:self];

...

複製代碼

對於 iOS 的 RTCPeerConnection 對象有三個參數:

  • 第一個,是 RTCConfiguration 類型的對象,該對象中最重要的一個字段是 iceservers。它裏邊存放了 stun/turn 服務器地址。其主要做用是用於NAT穿越。對於 NAT 穿越的知識你們能夠自行學習。
  • 第二個參數,是 RTCMediaConstraints 類型對象,也就是對 RTCPeerConnection 的限制。如,是否接收視頻數據?是否接收音頻數據?若是要與瀏覽器互通還要開啓 DtlsSrtpKeyAgreement 選項。
  • 第三個參數,是委拖類型。至關於給 RTCPeerConnection 設置一個觀察者。這樣RTCPeerConnection 能夠將一個狀態/信息經過它通知給觀察者。但它並不屬於觀察者模式,這一點你們必定要清楚。

RTCPeerConnection 對象建立好後,接下來咱們介紹的是整個實時通話過程當中,最重要的一部分知識,那就是 媒體協商

媒體協商

首先,咱們要知道媒體協商內容使用是 SDP 協議,不瞭解這部分知識的同窗能夠自行學習。其次,咱們要清楚總體媒體協商的過程。

iOS 端的媒體協商過程與 Android/JS 端是如出一轍的。仍是下面這個經典的圖:

A 與 B 進行通話,通話的發起方,首先要建立 Offer 類型的 SDP 內容。以後調用 RTCPeerConnection 對象的 setLocalDescription 方法,將 Offer 保存到本地。

緊接着,將 Offer 發送給服務器。而後,經過信令服務器中轉到被呼叫方。被呼叫方收到 Offer 後,調用它的 RTCPeerConnection 對象的 setRemoteDescription 方法,將遠端的 Offer 保存起來。

以後,被呼到方建立 Answer 類型的 SDP 內容,並調用 RTCPeerConnection 對象的 setLocalDescription 方法將它存儲到本地。

一樣的,它也要將 Answer 發送給服務器。服務器收到該消息後,不作任何處理,直接中轉給呼叫方。呼叫方收到 Answer 後,調用 setRemoteDescription 將其保存起來。

經過上面的步驟,整個媒體協商部分就完成了。

下面咱們就具體看看,在 iOS 端是如何實現這個邏輯的:

...

[peerConnection offerForConstraints:[self defaultPeerConnContraints]
                  completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
                      if(error){
                          NSLog(@"Failed to create offer SDP, err=%@", error);
                      } else {
                          __weak RTCPeerConnection* weakPeerConnction = self->peerConnection;
                          [self setLocalOffer: weakPeerConnction withSdp: sdp];
                      }
                  }];
...
複製代碼

在iOS端使用 RTCPeerConnection 對象的 offerForConstraints 方法建立 Offer SDP。它有兩個參數:

  • 一個是 RTCMediaConstraints 類型的參數,該參數咱們在前面建立 RTCPeerConnection 對象時介紹過,這裏不在贅述。
  • 另外一個參數是一個匿名回調函數。能夠經過對 error 是否爲空來斷定 offerForConstraints 方法有沒有執行成功。若是執行成功了,參數 sdp 就是建立好的 SDP 內容。

若是成功得到了 sdp,按照以前的處理流程描述,咱們首先要將它只存到本地;而後再將它發送給他務器,服務器中轉給另外一端。

咱們的代碼也是嚴格按照這個過程來的。在上面代碼中 setLocalOffer 方法就是作這件事兒。具體代碼以下:

...
[pc setLocalDescription:sdp completionHandler:^(NSError * _Nullable error) {
        if (!error) {
            NSLog(@"Successed to set local offer sdp!");
        }else{
            NSLog(@"Failed to set local offer sdp, err=%@", error);
        }
    }];
    
__weak NSString* weakMyRoom = myRoom;
dispatch_async(dispatch_get_main_queue(), ^{
    
    NSDictionary* dict = [[NSDictionary alloc] initWithObjects:@[@"offer", sdp.sdp]
                                                       forKeys: @[@"type", @"sdp"]];
    
    [[SignalClient getInstance] sendMessage: weakMyRoom
                                    withMsg: dict];
});
...
複製代碼

從上面的代碼能夠清楚的看出,它作了兩件事兒。一是調用 setLocalDescription 方法將 sdp 保存到本地;另外一件事兒就是發送消息;

因此,經過上面的描述你們也就知道後面的全部邏輯了。這裏咱們就不一一展開來說了。

當整個協商完成以後,緊接着,在WebRTC底層就會進行音視頻數據的傳輸。若是遠端的視頻數據到達本地後,咱們就須要將它展現到界面上。這又是如何作到的呢?

渲染遠端視頻

你們是否還記得,在咱們建立 RTCPeerConnection 對象時,同時給RTCPeerConnection設置了一個委拖,在咱們的項目中就是 CallViewController 對象。在該對象中咱們實現了全部 RTCPeerConnection 對象的代理方法。其中比較關鍵的有下面幾個:

  • (void)peerConnection:(RTCPeerConnection *)peerConnection didGenerateIceCandidate:(RTCIceCandidate *)candidate;該方法用於收集可用的 Candidate。

  • (void)peerConnection:(RTCPeerConnection *)peerConnection didChangeIceConnectionState:(RTCIceConnectionState)newState;當 ICE 鏈接狀態發生變化時會觸發該方法

  • (void)peerConnection:(RTCPeerConnection *)peerConnection didAddReceiver:(RTCRtpReceiver *)rtpReceiver streams:(NSArray<RTCMediaStream *> *)mediaStreams;該方法在偵聽到遠端 track 時會觸發。

那麼,何時開始渲染遠端視頻呢?當有遠端視頻流過來的時候,就會觸發 (void)peerConnection:(RTCPeerConnection *)peerConnection didAddReceiver:(RTCRtpReceiver *)rtpReceiver streams:(NSArray<RTCMediaStream *> *)mediaStreams 方法。因此咱們只須要在該方法中寫一些邏輯便可。

當上面的函數被調用後,咱們能夠經過 rtpReceiver 參數獲取到 track。這個track有多是音頻trak,也有多是視頻trak。因此,咱們首先要對 track 作個判斷,看其是視頻仍是音頻。

若是是視頻的話,就將remoteVideoView加入到trak中,至關於給track添加了一個觀察者,這樣remoteVideoView就能夠從track獲取到視頻數據了。在 remoteVideoView 實現了渲染方法,一量收到數據就會直接進行渲染。最終,咱們就能夠看到遠端的視頻了。

具體代碼以下:

...
RTCMediaStreamTrack* track = rtpReceiver.track;
if([track.kind isEqualToString:kRTCMediaStreamTrackKindVideo]){
   
    if(!self.remoteVideoView){
        NSLog(@"error:remoteVideoView have not been created!");
        return;
    }
    
    remoteVideoTrack = (RTCVideoTrack*)track;
  	
  	 [remoteVideoTrack addRenderer: self.remoteVideoView];
}
   
...

複製代碼

經過上面的代碼,咱們就能夠將遠端傳來的視頻展現出來了。

小結

以上我就將 iOS 端實現1對1實時通話的總體邏輯講解完了。總體來看,其過程與 js/Android 端基本上是如出一轍的。

在本文中,我經過對下面幾個主題的介紹,向你們完整的講解了 iOS 端該如何實現一個實時音視頻通話程序:

  • 權限申請
  • 引入 WebRTC 庫
  • 採集並顯示本地視頻
  • 信令驅動
  • 建立音視頻數據通道
  • 媒體協商
  • 渲染遠端視頻

對於一個熟悉 iOS 的開發者來講,經過本文的講解,應該能夠很快寫出這樣一個實時通話的程序。

謝謝!

相關文章
相關標籤/搜索