權(quán)限申請(qǐng):
Info.plist中添加 Camera和Microphone訪問權(quán)限
引入WebRTC庫(kù):
1、通過WebRTC源碼編譯出WebRTC庫(kù),然后再項(xiàng)目中手動(dòng)引入它
2、WebRTC官方會(huì)定期發(fā)布編譯好的WebRTC庫(kù),可以使用Pod方式進(jìn)行安裝(GoogleWebRTC)
獲取本地視頻:
WebRTC庫(kù)引入成功后,就可以開始真正的WebRTC之旅了。
????在獲取視頻之前,首先要選擇使用哪個(gè)視頻設(shè)備采集數(shù)據(jù)。在WebRTC中,我們可以通過RTCCameraVideoCapture類獲取所有的視頻設(shè)備。
????NSArray *devices =[RTCCameraVideoCapture captureDevices];
????AVCaptureDevice *device = devices[0];
光有設(shè)備不行,還要清楚從設(shè)備中采集的數(shù)據(jù)放到哪里,這樣才能將其展示出來(lái)。
????WebRTC提供了一個(gè)專門的類,即RTCVideoSource,有兩層含義:
????1、表明它是視頻源。當(dāng)要展示視頻的時(shí)候,就從這里獲取數(shù)據(jù)
????2、它也是一個(gè)終點(diǎn),即:當(dāng)從視頻設(shè)備采集到視頻數(shù)據(jù)時(shí),要交給它暫存起來(lái)
????RTCCameraVideoCapture,專門用于操作設(shè)備的類,通過它,可以自如的控制視頻設(shè)備了。
信令驅(qū)動(dòng):
在任何系統(tǒng)中,都可以說(shuō)是信令是系統(tǒng)的靈魂。例如,由誰(shuí)來(lái)發(fā)起呼叫;媒體協(xié)商時(shí),什么時(shí)間發(fā)哪種SDP都是有信令控制的。
????客戶端命令:
? ? ? 1、join,用戶加入房間
? ? ? 2、leave,用戶離開房間
? ? ? 3、message,端到端命令(offer,answer,candidate)
????服務(wù)端命令:
? ? ? 1、joined,用戶已加入
? ? ? 2、leaved,用戶已離開
? ? ? 3、other_joined,其他用戶已加入
? ? ? 4、bye,其他用戶已離開
? ? ? 5、full,房間已滿
????信令狀態(tài)機(jī)
????通過信令狀態(tài)機(jī)來(lái)管理信令,不同的狀態(tài)下,需要發(fā)不同的信令。同樣的,當(dāng)收到服務(wù)端或?qū)Χ说男帕詈?,狀態(tài)會(huì)隨之發(fā)生改變。
????在初始時(shí),客戶端處于init/leaved狀態(tài)。
????在 init/leaved 狀態(tài)下,用戶只能發(fā)送join消息。服務(wù)端收到join消息后,會(huì)返回joined消息。此時(shí),客戶端會(huì)更新為joined狀態(tài)。
????在joined狀態(tài)下,客戶端有多種選擇,收到不同的消息會(huì)切到不同的狀態(tài):
????如果用戶離開房間,那客戶端又回到了初始狀態(tài),即 init/leaved 狀態(tài)。
????如果客戶端收到second user join消息,則切換到join_conn狀態(tài)。在這種狀態(tài)下,兩個(gè)用戶就可以進(jìn)行通話了。
????如果客戶端收到second user leave消息,則切換到join_unbind狀態(tài)。其實(shí)join_unbind狀態(tài)與joined狀態(tài)基本是一致的。
????如果客戶端處于join_conn狀態(tài),當(dāng)它收到second user leave消息時(shí),也會(huì)轉(zhuǎn)成joined_unbind狀態(tài)。
????如果客戶端是joined_unbind狀態(tài),當(dāng)它收到second user join消息時(shí),會(huì)切到join_conn狀態(tài)。
socket.io? (信令的基礎(chǔ)庫(kù))
信令的使用:
????1、通過url獲取socket。有了socket之后就可建立與服務(wù)器的連接了
????2、注冊(cè)偵聽的消息,并為每個(gè)偵聽的消息綁定一個(gè)處理函數(shù)。當(dāng)收到服務(wù)器的消息后,隨之會(huì)觸發(fā)綁定的函數(shù)
????3、通過socket建立連接
????4、發(fā)送消息
????獲取socket
????NSURL* url =[[NSURL alloc]initWithString:addr];
????manager =[[SocketManager alloc]initWithSocketURL:url
????????? config:@{
????????????????????@"log": @YES,
????????????????????@"forcePolling":@YES,
? ? ? ? ? ? ? ? ? ? @"forceWebsockets":@YES
???? }];
????socket = manager.defaultSocket;
注冊(cè)偵聽消息:
????[socket on:@"joined" callback:^(NSArray * data,SocketAckEmitter * ack){
????????NSString* room =[data objectAtIndex:0];
????????NSLog(@"joined room(%@)",room);
????????[self.delegate joined:room];
????}];
建立連接:
????[socket connect];
發(fā)送消息:
????if(socket.status == SocketIOStatusConnected){
????????[socket emit:@"join" with:@[room]];
????}
創(chuàng)建RCTPeerConnection
????當(dāng)信令系統(tǒng)建立好后,后面的邏輯都是圍繞著信令系統(tǒng)建立起來(lái)的
????客戶端用戶想要與遠(yuǎn)端通話,首先要發(fā)送join消息,也就是要先進(jìn)入房間。此時(shí),如果服務(wù)器判斷用戶是合法的,則會(huì)給客戶端會(huì)joined消息
????客戶端收到j(luò)oined消息后,就要?jiǎng)?chuàng)建RTCPeerConnection了,也就是要建立一條與遠(yuǎn)端通話的音視頻數(shù)據(jù)傳輸通道
????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];
????RTCPeerConnection對(duì)象有三個(gè)參數(shù):
????????1、RTCConfiguration類型的對(duì)象,該對(duì)象中最重要的一個(gè)字段是iceservers。它里面存放了stun/turn服務(wù)器地址。其主要作用是用于NAT穿越。
????????2、RTCMediaConstraints類型對(duì)象,也就是對(duì)RTCPeerConnection的限制。
????????????如:是否接受視頻數(shù)據(jù)?是否接受音頻數(shù)據(jù)?如果要與瀏覽器互通還要開啟DtlsSrtpKeyAgreement選項(xiàng)
????????3、委托類型。相當(dāng)于給RTCPeerConnection設(shè)置一個(gè)觀察者。這樣RTCPeerConnection可以將一個(gè)狀態(tài)/信息通過它通知給觀察者。
????RTCPeerConnection建立好之后,接下來(lái)就是整個(gè)實(shí)時(shí)通話過程中,最重要的部分,媒體協(xié)商
媒體協(xié)商
????媒體協(xié)商內(nèi)容使用的是SDP協(xié)議
????Amy與Bob進(jìn)行通話,通話的發(fā)起方(Amy),首先要?jiǎng)?chuàng)建Offer類型的SDP消息,之后調(diào)用RTCPeerConnection對(duì)象的setLocalDescription方法,將Offer保存到本地
????緊接著,將Offer發(fā)送給服務(wù)器。然后通過信令服務(wù)器中轉(zhuǎn)到被呼叫方(Bob)。被呼叫方收到Offer后,調(diào)用它的RTCPeerConnection對(duì)象的setRemoteDescription方法,將遠(yuǎn)端的Offer保存起來(lái)
????之后,被呼叫方創(chuàng)建Answer類型的SDP內(nèi)容,并調(diào)用RTCPeerConnection對(duì)象的setLocalDescription方法將它存儲(chǔ)到恩地
????同樣的,它也要將Answer發(fā)送給服務(wù)器。服務(wù)器收到該消息后,不做任何處理,直接中轉(zhuǎn)給呼叫方。呼叫方收到Answer后,調(diào)用setRemoteDescription將其保存起來(lái)

????通過上面的步驟,整個(gè)媒體協(xié)商部分就完成了
????[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對(duì)象的offerForConstraints方法創(chuàng)建Offer SDP。它有兩個(gè)參數(shù)
????????1、RTCMediaConstraints類型的參數(shù)
????????2、匿名回調(diào)函數(shù)??梢酝ㄟ^對(duì)error是否為空來(lái)判定offerForConstraints方法有沒有執(zhí)行成功。如果執(zhí)行成功啦,參數(shù)sdp就是創(chuàng)建好的SDP內(nèi)容
????如果成功獲得了SDP,首先存到本地,然后再將它發(fā)送給服務(wù)端,服務(wù)器中轉(zhuǎn)給另一端
????[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];
????});
????其實(shí)就是做了兩件事。一是調(diào)用setLocalDescription方法將SDP保存到本地,另一件事就是發(fā)消息。
????當(dāng)整個(gè)協(xié)商完成后,緊接著,在WebRTC底層就會(huì)進(jìn)行音視頻數(shù)據(jù)的傳輸。
渲染遠(yuǎn)端視頻:
????在創(chuàng)建RTCPeerConnection對(duì)象時(shí),同時(shí)給RTCPeerConnection設(shè)置了一個(gè)委托
????該委托對(duì)象中,實(shí)現(xiàn)了所有RTCPeerConnection對(duì)象的代理方法,其中比較關(guān)鍵的有下面幾個(gè):
? ? 1、- (void)peerConnection:(RTCPeerConnection *)peerConnection didGenerateIceCandidate:(RTCIceCandidate *)candidate;//該方法用于收集可用的Candidate。
? ? 2、-(void)peerConnection:(RTCPeerConnection *)peerConnection
didChangeIceConnectionState:(RTCIceConnectionState)newState;//當(dāng)ICE連接狀態(tài)發(fā)生變化時(shí)會(huì)觸發(fā)該方法
? ? 3、-(void)peerConnection:(RTCPeerConnection *)peerConnection
didAddReceiver:(RTCRtpReceiver *)rtpReceiver streams:(NSArray *)mediaStreams;//該方法在偵聽到遠(yuǎn)端track時(shí)會(huì)觸發(fā)。
????那么,什么時(shí)候開始渲染遠(yuǎn)端視頻呢?當(dāng)有遠(yuǎn)端視頻流過來(lái)的時(shí)候,就會(huì)觸發(fā)?
? ? ? -(void)peerConnection:(RTCPeerConnection *)peerConnection didAddReceiver:(RTCRtpReceiver *)rtpReceiver streams: (NSArray *)mediaStreams方法。所以我們只需要在該方法中寫一些邏輯即可。
????當(dāng)上面的函數(shù)被調(diào)用后,我們可以通過rtpReceiver參數(shù)獲取到track。這個(gè)track有可能是音頻trak,也有可能是視頻trak。所以,我們首先要對(duì) track 做個(gè)判斷,看其是視頻還是音頻。
????如果是視頻的話,就將remoteVideoView加入到trak中,相當(dāng)于給track添加了一個(gè)觀察者,這樣remoteVideoView就可以從track獲取到視頻數(shù)據(jù)了。在remoteVideoView實(shí)現(xiàn)了渲染方法,一量收到數(shù)據(jù)就會(huì)直接進(jìn)行渲染。最終,我們就可以看到遠(yuǎn)端的視頻了。
????具體代碼如下:
????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];
????}
通過上面的代碼,我們就可以將遠(yuǎn)端傳來(lái)的視頻展示出來(lái)了。
**********************************************************************************************************************
總結(jié):(七步驟)
權(quán)限申請(qǐng)
引入WebRTC庫(kù)
獲取本地視頻
信令驅(qū)動(dòng)
創(chuàng)建音視頻數(shù)據(jù)通道
媒體協(xié)商
渲染遠(yuǎn)端視頻