做者:李超,資深音視頻工程師,有多年的音視頻相關開發經驗。ios
文章首發於 RTC 開發者社區,如遇到開發問題,請點擊這裏給做者留言。git
以前,我已經寫過 Android 端如何使用 WebRTC 的文章。在那篇文章中,我向你們介紹了在 Android 端是如何使用 WebRTC 進行音視頻通話的。今天,咱們再來看看 iOS 端1對1音視頻實時通話的具體實現。github
iOS 端的實現邏輯與 Android 端基本相同,最大的區別多是語言方面的差別啦!因此,下面我基本上仍是按照介紹 Android 端同樣的過程來介紹 iOS 端的實現。具體步驟以下:swift
經過上面幾個小節,全面介紹如何在iOS端如何使用 WebRTC。數組
首先,咱們來看一下 iOS 端是如何獲取訪問音視頻設備權限的。相比 Android 端而言,iOS端獲取相關權限要容易不少。其步驟以下:瀏覽器
下面這張圖更清晰的展示了申請權限的步驟:bash
在iOS端引入 WebRTC 庫有兩種方式:服務器
在本項目中,咱們使用第二種方式。socket
使用第二種方式引入 WebRTC 庫很是簡單,咱們只須要寫個 Podfile 文件就能夠了。在 Podfile 中能夠指定下載 WebRTC 庫的地址,以及咱們要安裝的庫的名子。async
Podfile 文件的具體格式以下:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios,'11.0'
target 'WebRTC4iOS2' do
pod 'GoogleWebRTC'
end
複製代碼
有了 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,咱們就能夠輕鬆的將視頻數據採集出來了。下面咱們就來具體看一下代碼吧!
在該代碼中,首先將 RTCVideoSource 與 RTCCameraVideoCapture 進行綁定,而後再開啓設備,這樣視頻數據就源源不斷的被採集到 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];
}
...
複製代碼
在上面代碼中,
有了 factory 對象後,咱們就能夠開始建立其它對象了。那麼,緊接下來的問題就是如何將採集到的視頻展現出來了。
在iOS端展現本地視頻與Android端仍是有很大區別的,這主要是因爲不一樣系統底層實現方式不同。爲了更高效的展現本地視頻,它們採用了不一樣的方式。
在iOS端展現本地視頻其實很是的簡單,只須要在調用 capture 的 startCaptureWithDevice 方法以前執行下面的語句就行了:
self.localVideoView.captureSession = capture.captureSession;
複製代碼
固然,在iOS頁面初始化的時候,必定要記得定義 localVideoView 喲,其類型爲 RTCCameraPreviewView!
經過上面的步驟,咱們就能夠看到視頻設備採集到的視頻圖像了。
上面咱們介紹了iOS端權限的申請,WebRTC庫的引入,以及本地視頻的採集與展現,這些功能實現起來都很簡單。但接下來咱們要介紹的信令就要複雜一些了。
在任何系統中,均可以說信令是系統的靈魂。例如,由誰來發起呼叫;媒體協商時,什麼時間發哪一種 SDP 都是由信令控制的。
對於本項目來講,它的信令相對仍是比較簡單,它包括下面幾種信令:
客戶端命令
服務端命令
這些信令之間是怎樣一種關係?在什麼狀況下該發送怎樣的信令呢?要回答這個問題咱們就要看一下信令狀態機了。
在 iOS 端的信令與咱們以前介紹的 js端 和 Android 端同樣,會經過一個信令狀態機來管理。在不一樣的狀態下,須要發不一樣的信令。一樣的,當收到服務端,或對端的信令後,狀態會隨之發生改變。下面咱們來看一下這個狀態的變化圖吧:
經過上面的狀態圖,咱們就很是清楚的知道了在什麼狀態下應該發什麼信令;或者說,發什麼樣的信令,狀態會發生怎樣的變化了。
看過我以前文章的同窗應該都清楚,不管是在 js端,仍是在 Android 端的實時通話中,我一直使用 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 分爲三步:
下咱們咱們就逐一的看它們是如何實現的吧!
獲取 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 對象的創建也不例外。
在客戶端,用戶要想與遠端通話,首先要發送 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 對象有三個參數:
RTCPeerConnection 對象建立好後,接下來咱們介紹的是整個實時通話過程當中,最重要的一部分知識,那就是 媒體協商。
首先,咱們要知道媒體協商內容使用是 SDP 協議,不瞭解這部分知識的同窗能夠自行學習。其次,咱們要清楚總體媒體協商的過程。
iOS 端的媒體協商過程與 Android/JS 端是如出一轍的。仍是下面這個經典的圖:
緊接着,將 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。它有兩個參數:
若是成功得到了 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 端該如何實現一個實時音視頻通話程序:
對於一個熟悉 iOS 的開發者來講,經過本文的講解,應該能夠很快寫出這樣一個實時通話的程序。
謝謝!