權限申請:
Info.plist中添加 Camera和Microphone訪問權限
引入WebRTC庫:
1暖哨、通過WebRTC源碼編譯出WebRTC庫,然后再項目中手動引入它
2、WebRTC官方會定期發(fā)布編譯好的WebRTC庫,可以使用Pod方式進行安裝(GoogleWebRTC)
獲取本地視頻:
WebRTC庫引入成功后,就可以開始真正的WebRTC之旅了脐雪。
????在獲取視頻之前,首先要選擇使用哪個視頻設備采集數(shù)據(jù)恢共。在WebRTC中战秋,我們可以通過RTCCameraVideoCapture類獲取所有的視頻設備。
????NSArray *devices =[RTCCameraVideoCapture captureDevices];
????AVCaptureDevice *device = devices[0];
光有設備不行讨韭,還要清楚從設備中采集的數(shù)據(jù)放到哪里获询,這樣才能將其展示出來。
????WebRTC提供了一個專門的類拐袜,即RTCVideoSource,有兩層含義:
????1梢薪、表明它是視頻源蹬铺。當要展示視頻的時候,就從這里獲取數(shù)據(jù)
????2秉撇、它也是一個終點甜攀,即:當從視頻設備采集到視頻數(shù)據(jù)時,要交給它暫存起來
????RTCCameraVideoCapture琐馆,專門用于操作設備的類规阀,通過它,可以自如的控制視頻設備了瘦麸。
信令驅(qū)動:
在任何系統(tǒng)中谁撼,都可以說是信令是系統(tǒng)的靈魂。例如滋饲,由誰來發(fā)起呼叫厉碟;媒體協(xié)商時,什么時間發(fā)哪種SDP都是有信令控制的屠缭。
????客戶端命令:
? ? ? 1箍鼓、join,用戶加入房間
? ? ? 2呵曹、leave款咖,用戶離開房間
? ? ? 3何暮、message,端到端命令(offer铐殃,answer海洼,candidate)
????服務端命令:
? ? ? 1、joined背稼,用戶已加入
? ? ? 2贰军、leaved,用戶已離開
? ? ? 3蟹肘、other_joined词疼,其他用戶已加入
? ? ? 4、bye帘腹,其他用戶已離開
? ? ? 5贰盗、full,房間已滿
????信令狀態(tài)機
????通過信令狀態(tài)機來管理信令阳欲,不同的狀態(tài)下舵盈,需要發(fā)不同的信令。同樣的球化,當收到服務端或?qū)Χ说男帕詈蠡嗤恚瑺顟B(tài)會隨之發(fā)生改變。
????在初始時筒愚,客戶端處于init/leaved狀態(tài)赴蝇。
????在 init/leaved 狀態(tài)下,用戶只能發(fā)送join消息巢掺。服務端收到join消息后句伶,會返回joined消息。此時陆淀,客戶端會更新為joined狀態(tài)考余。
????在joined狀態(tài)下,客戶端有多種選擇轧苫,收到不同的消息會切到不同的狀態(tài):
????如果用戶離開房間楚堤,那客戶端又回到了初始狀態(tài),即 init/leaved 狀態(tài)含懊。
????如果客戶端收到second user join消息钾军,則切換到join_conn狀態(tài)。在這種狀態(tài)下绢要,兩個用戶就可以進行通話了吏恭。
????如果客戶端收到second user leave消息,則切換到join_unbind狀態(tài)重罪。其實join_unbind狀態(tài)與joined狀態(tài)基本是一致的樱哼。
????如果客戶端處于join_conn狀態(tài)哀九,當它收到second user leave消息時,也會轉(zhuǎn)成joined_unbind狀態(tài)搅幅。
????如果客戶端是joined_unbind狀態(tài)阅束,當它收到second user join消息時,會切到join_conn狀態(tài)茄唐。
socket.io? (信令的基礎庫)
信令的使用:
????1息裸、通過url獲取socket。有了socket之后就可建立與服務器的連接了
????2沪编、注冊偵聽的消息呼盆,并為每個偵聽的消息綁定一個處理函數(shù)。當收到服務器的消息后蚁廓,隨之會觸發(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;
注冊偵聽消息:
????[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
????當信令系統(tǒng)建立好后相嵌,后面的邏輯都是圍繞著信令系統(tǒng)建立起來的
????客戶端用戶想要與遠端通話腿时,首先要發(fā)送join消息,也就是要先進入房間饭宾。此時批糟,如果服務器判斷用戶是合法的,則會給客戶端會joined消息
????客戶端收到joined消息后看铆,就要創(chuàng)建RTCPeerConnection了跃赚,也就是要建立一條與遠端通話的音視頻數(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對象有三個參數(shù):
????????1、RTCConfiguration類型的對象性湿,該對象中最重要的一個字段是iceservers。它里面存放了stun/turn服務器地址满败。其主要作用是用于NAT穿越肤频。
????????2、RTCMediaConstraints類型對象算墨,也就是對RTCPeerConnection的限制宵荒。
????????????如:是否接受視頻數(shù)據(jù)?是否接受音頻數(shù)據(jù)净嘀?如果要與瀏覽器互通還要開啟DtlsSrtpKeyAgreement選項
????????3报咳、委托類型。相當于給RTCPeerConnection設置一個觀察者挖藏。這樣RTCPeerConnection可以將一個狀態(tài)/信息通過它通知給觀察者暑刃。
????RTCPeerConnection建立好之后,接下來就是整個實時通話過程中,最重要的部分,媒體協(xié)商
媒體協(xié)商
????媒體協(xié)商內(nèi)容使用的是SDP協(xié)議
????Amy與Bob進行通話绸狐,通話的發(fā)起方(Amy)摇邦,首先要創(chuàng)建Offer類型的SDP消息簇爆,之后調(diào)用RTCPeerConnection對象的setLocalDescription方法夫啊,將Offer保存到本地
????緊接著瓜客,將Offer發(fā)送給服務器君纫。然后通過信令服務器中轉(zhuǎn)到被呼叫方(Bob)谷扣。被呼叫方收到Offer后土全,調(diào)用它的RTCPeerConnection對象的setRemoteDescription方法,將遠端的Offer保存起來
????之后会涎,被呼叫方創(chuàng)建Answer類型的SDP內(nèi)容裹匙,并調(diào)用RTCPeerConnection對象的setLocalDescription方法將它存儲到恩地
????同樣的,它也要將Answer發(fā)送給服務器在塔。服務器收到該消息后幻件,不做任何處理,直接中轉(zhuǎn)給呼叫方蛔溃。呼叫方收到Answer后绰沥,調(diào)用setRemoteDescription將其保存起來
????通過上面的步驟,整個媒體協(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對象的offerForConstraints方法創(chuàng)建Offer SDP贺待。它有兩個參數(shù)
????????1徽曲、RTCMediaConstraints類型的參數(shù)
????????2、匿名回調(diào)函數(shù)麸塞⊥撼迹可以通過對error是否為空來判定offerForConstraints方法有沒有執(zhí)行成功。如果執(zhí)行成功啦哪工,參數(shù)sdp就是創(chuàng)建好的SDP內(nèi)容
????如果成功獲得了SDP奥此,首先存到本地,然后再將它發(fā)送給服務端雁比,服務器中轉(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];
????});
????其實就是做了兩件事稚虎。一是調(diào)用setLocalDescription方法將SDP保存到本地,另一件事就是發(fā)消息偎捎。
????當整個協(xié)商完成后蠢终,緊接著,在WebRTC底層就會進行音視頻數(shù)據(jù)的傳輸茴她。
渲染遠端視頻:
????在創(chuàng)建RTCPeerConnection對象時寻拂,同時給RTCPeerConnection設置了一個委托
????該委托對象中,實現(xiàn)了所有RTCPeerConnection對象的代理方法丈牢,其中比較關鍵的有下面幾個:
? ? 1祭钉、- (void)peerConnection:(RTCPeerConnection *)peerConnection didGenerateIceCandidate:(RTCIceCandidate *)candidate;//該方法用于收集可用的Candidate己沛。
? ? 2朴皆、-(void)peerConnection:(RTCPeerConnection *)peerConnection
didChangeIceConnectionState:(RTCIceConnectionState)newState帕识;//當ICE連接狀態(tài)發(fā)生變化時會觸發(fā)該方法
? ? 3、-(void)peerConnection:(RTCPeerConnection *)peerConnection
didAddReceiver:(RTCRtpReceiver *)rtpReceiver streams:(NSArray *)mediaStreams遂铡;//該方法在偵聽到遠端track時會觸發(fā)肮疗。
????那么,什么時候開始渲染遠端視頻呢扒接?當有遠端視頻流過來的時候伪货,就會觸發(fā)?
? ? ? -(void)peerConnection:(RTCPeerConnection *)peerConnection didAddReceiver:(RTCRtpReceiver *)rtpReceiver streams: (NSArray *)mediaStreams方法。所以我們只需要在該方法中寫一些邏輯即可钾怔。
????當上面的函數(shù)被調(diào)用后碱呼,我們可以通過rtpReceiver參數(shù)獲取到track。這個track有可能是音頻trak宗侦,也有可能是視頻trak愚臀。所以,我們首先要對 track 做個判斷矾利,看其是視頻還是音頻姑裂。
????如果是視頻的話,就將remoteVideoView加入到trak中男旗,相當于給track添加了一個觀察者舶斧,這樣remoteVideoView就可以從track獲取到視頻數(shù)據(jù)了。在remoteVideoView實現(xiàn)了渲染方法察皇,一量收到數(shù)據(jù)就會直接進行渲染茴厉。最終,我們就可以看到遠端的視頻了什荣。
????具體代碼如下:
????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];
????}
通過上面的代碼矾缓,我們就可以將遠端傳來的視頻展示出來了。
**********************************************************************************************************************
總結(jié):(七步驟)
權限申請
引入WebRTC庫
獲取本地視頻
信令驅(qū)動
創(chuàng)建音視頻數(shù)據(jù)通道
媒體協(xié)商
渲染遠端視頻