iOS WebRTC的使用

WebRTC是Google公司的一款跨平臺的音視頻通話技術(shù)晨逝,它為我們提供了音視頻通信的核心技術(shù),包括音視頻的采集懦铺、編解碼捉貌、網(wǎng)絡(luò)傳輸、視頻顯示等功能冬念。
借助這款A(yù)PI趁窃,我們可以更容易地實(shí)現(xiàn)音視頻聊天功能。

準(zhǔn)備工作:
1急前、兩個客戶端棚菊。
2、一臺socket長連接服務(wù)器叔汁,用于交換客戶端兩端的通信信息(ip地址统求,端口等),以及管理通信狀態(tài)等据块。
3码邻、一臺STUN/TURN/ICE Server,用于獲取公網(wǎng)IP另假。

通信步驟:
1像屋、客戶端告知WebRTC獲取公網(wǎng)地址的服務(wù)器的IP,整個過程中WebRTC會不斷去獲取公網(wǎng)地址边篮,獲取公網(wǎng)地址后己莺,通過socket服務(wù)器發(fā)送給對方奏甫。
2、客戶端拿到對方發(fā)送的公網(wǎng)地址凌受,保存起來阵子,WebRTC會在通信的時候選擇最優(yōu)的公網(wǎng)地址。
3胜蛉、呼叫方生成一個類型為offer的會話描述挠进,并設(shè)置為本地回話描述,然后通過socket服務(wù)器發(fā)送給接聽方誊册。
4领突、接聽方收到offer的會話描述后,把offer設(shè)置為了遠(yuǎn)程回話描述案怯。并生成一個類型為answer的回話描述君旦,發(fā)送給呼叫方。
5嘲碱、呼叫方收到接收方的answer金砍,把a(bǔ)nswer設(shè)置為了遠(yuǎn)程回話描述。
6悍汛、至此捞魁,雙方P2P連接建立,開始音視頻通信离咐。

具體實(shí)現(xiàn):

@property(nonatomic, strong) RTCPeerConnection* peerConnection; 
@property(nonatomic, strong) RTCPeerConnectionFactory* peerConnectionFactory;   
@property(nonatomic, strong) RTCSessionDescription* localSdp;
@property(nonatomic, strong) RTCSessionDescription* remoteSdp;
@property(nonatomic, strong) RTCMediaConstraints* sdpMediaConstraints;
@property(nonatomic, strong) RTCAudioTrack* audioTrack;
@property(nonatomic, strong) RTCMediaStream* localMediaStream;
@property(nonatomic, strong) NSObject* candidateMutex;
@property(nonatomic, strong) NSMutableArray* queuedRemoteCandidates;
@property(nonatomic, strong) NSObject* sdpMutex;
@property(nonatomic, strong) NSMutableArray* queuedRemoteSdp;
@property(nonatomic, strong) RTCConfiguration *rtcConfig;
// video
@property(nonatomic, strong) RTCEAGLVideoView* localVideoView;
@property(nonatomic, strong) RTCEAGLVideoView* remoteVideoView;
@property(nonatomic, strong) RTCVideoTrack* localVideoTrack;
@property(nonatomic, strong) RTCVideoTrack* remoteVideoTrack;

  • RTCPeerConnection:連接管理類谱俭。
  • RTCPeerConnectionFactory:RTC連接工廠類,負(fù)責(zé)一些全局配置宵蛀,也負(fù)責(zé)RTC對象的- 實(shí)例化昆著。
  • RTCSessionDescription:會話描述,呼叫方類型為offer术陶;接聽方為answer凑懂。
  • RTCMediaConstraints:媒體信息約束,可以理解為對媒體信息進(jìn)行配置的類梧宫。
  • RTCMediaStream: 媒體流接谨。
  • RTCAudioTrack:音頻軌道,用于添加進(jìn)RTCMediaStream里塘匣,才會有聲音傳輸脓豪。
  • RTCVideoTrack:視頻軌道,用于添加進(jìn)RTCMediaStream里忌卤,才會有視頻傳輸扫夜。
  • RTCEAGLVideoView:RTCVideoTrack渲染之后,可顯示視頻畫面。
  • RTCConfiguration:配置連接信息笤闯,配置連接時候的超時信息堕阔,ICE服務(wù)器的地址等。

接下來進(jìn)行整體的初始化:
創(chuàng)建ConnectionFactory颗味;

    #如果想要更加安全就開啟SSL超陆。
    [RTCPeerConnectionFactory initializeSSL];
    self.peerConnectionFactory = [[RTCPeerConnectionFactory alloc] init];

創(chuàng)建媒體流,并且添加音頻軌道和視頻軌道

    if (self.peerConnectionFactory) {
       # 初始化媒體流脱衙,ARDAMSa0和ARDAMS為專業(yè)標(biāo)識符侥猬。
        self.localMediaStream = [self.peerConnectionFactory mediaStreamWithLabel:@"ARDAMS"];
        #添加音頻軌道
        self.audioTrack = [self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"];
        [self.localMediaStream addAudioTrack:self.audioTrack];
        
        if (self.supportVideo) {
          #添加視頻軌道
            self.localVideoTrack = [self createVideoTrack:self.useFrontFacingCamera];
            [self.localMediaStream addVideoTrack:self.localVideoTrack];
           #把本地視頻軌道渲染到localVideoView
         [self.localVideoTrack  addRenderer:self.localVideoView];
        }
    }

創(chuàng)建媒體會話描述SDP:

   #配置信息的基礎(chǔ)單元例驹,以鍵值對的方式
   RTCPair* audio = [[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"];
    NSArray* mandatory;
    if (self.supportVideo) {
        RTCPair* video = [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"];
        mandatory = @[ audio, video ];
    } else {
        mandatory = @[ audio ];
    }
    self.sdpMediaConstraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory
                                                                     optionalConstraints:nil];
     #此處將ICEServers的ip地址和登錄信息傳遞給RTC
    NSMutableArray *iceServers = [[NSMutableArray alloc] init];
        for(NSDictionary * dic in list)
        {
            NSString *strUsername = [dic objectForKey:@"userName"];
            NSString *strPassword = [dic objectForKey:@"password"];
            NSURL * strUri= [NSURL URLWithString:[dic objectForKey:@"uri"]];
            
            RTCICEServer * iceServer = [[RTCICEServer alloc] initWithURI:strUri username:strUsername password:strPassword];
            [iceServers addObject:iceServer];//jay for test
        }
      #config設(shè)置
    self.rtcConfig = [[RTCConfiguration alloc] init];
    self.rtcConfig.iceServers = iceServers;
    self.rtcConfig.iceConnectionReceivingTimeout = 90000; // 90s
    
    #沒有使用DtlsSrtp加密捐韩。
    RTCPair *dtlssrtp = [[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:@"false"];   //lianwei add for test
    RTCMediaConstraints *constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory optionalConstraints:@[dtlssrtp]];
     #創(chuàng)建peerConnection
    if (self.peerConnectionFactory) {
        self.peerConnection = [self.peerConnectionFactory peerConnectionWithConfiguration:self.rtcConfig
                                                                              constraints:constraints
                                                                                 delegate:self];
    }
      #把媒體流添加進(jìn)peerConnection
    if (self.peerConnection) {
        [self.peerConnection addStream:self.localMediaStream];
    }

當(dāng)我們將ICEServers告知RTC,且設(shè)置好peerConnection的delegate之后鹃锈,如果獲取到公網(wǎng)地址信息(ICECandidate)荤胁,則會觸發(fā)- (void)peerConnection:(RTCPeerConnection*)peerConnection gotICECandidate:(RTCICECandidate*)candidate方法:

- (void)peerConnection:(RTCPeerConnection*)peerConnection gotICECandidate:(RTCICECandidate*)candidate
{
ZUSLOG(@"gotICECandidate,peerConnection=%@,canidate=%@",peerConnection,candidate);
        NSDictionary *dicCandidate = @{
                                       @"type" : @"candidate",
                                       @"label" : @(candidate.sdpMLineIndex),
                                       @"id" : candidate.sdpMid,
                                       @"candidate" : candidate.sdp
                                       };
        NSString *message = [NSString stringWithDictionary:dicCandidate];
        # 將獲取到ICECandidate通過socket服務(wù)器發(fā)送給其他客戶端。
        [self sendRTCMessage:message withOfferType:NO];
}

而當(dāng)其他端收到發(fā)送過來的ICECandidate屎债,需要存入peerConnection備用:

    NSString *type = dict[@"type"];
    if ([type isEqualToString:@"candidate"]) {
        NSString* mid = MessageDict[@"id"];
        NSNumber* sdpLineIndex = MessageDict[@"label"];
        NSString* sdp = MessageDict[@"candidate"];
        
        //added by wafer for connection information,2017.06.15
        _sdpCandidate = [NSString stringWithFormat:@"%@%@\r\n",_sdpCandidate,sdp];
        
        RTCICECandidate* candidate = [[RTCICECandidate alloc] initWithMid:mid index:sdpLineIndex.intValue sdp:sdp];
         #添加到peerConnection里
          if (self.peerConnection) {
              [self.peerConnection addICECandidate:candidate];
                 ZUSLOG(@"receiveRTCMessage,addICECandidate");
            }
    }

到這里仅政,通信之前的準(zhǔn)備工作就差不多了,接下來要開始撥打電話了:
呼叫方首先生成一個offer類型的SDP(會話描述):

        #判斷是否是呼叫方
      if (self.initiator) {   
                 #呼叫方生成一個offer類型的SDP
        [self.peerConnection createOfferWithDelegate:self constraints:self.sdpMediaConstraints];
      }

這里又出現(xiàn)了delegate盆驹,于是我們需要在回調(diào)方法里處理事件圆丹。
這里的protocol一共有兩個方法:
1、生成SDP的回調(diào):
- (void)peerConnection:(RTCPeerConnection *)peerConnection didCreateSessionDescription:(RTCSessionDescription *)sdp error:(NSError *)error;
2躯喇、這個是設(shè)置本地或者遠(yuǎn)程sdp的時候會調(diào)用:
- (void)peerConnection:(RTCPeerConnection *)peerConnection didSetSessionDescriptionWithError:(NSError *)error;
我們先處理第一個方法:
首先我們在成功生成SDP后辫封,

- (void)peerConnection:(RTCPeerConnection*)peerConnection didCreateSessionDescription:(RTCSessionDescription*)origSdp error:(NSError*)error
{
    ZUSLOG(@"didCreateSessionDescription,peerConnection=%@,origSdp=%@,error=%@",peerConnection,origSdp,error);
        if (error) {  #生成失敗
            return;
        }
        #(不管是呼叫方還是接收方,處理都是一樣的)
        self.localSdp = [[RTCSessionDescription alloc] initWithType:origSdp.type sdp:[NSString preferVoiceCodec:origSdp.description voiceCodecType:self.voiceCodecType]];
        if (self.peerConnection) {
           # 生成成功就把他設(shè)為本地SDP
            [self.peerConnection setLocalDescriptionWithDelegate:self sessionDescription:self.localSdp];
        
        }
        # 然后再把生成的SDP發(fā)出去
        NSDictionary *dicSdp = @{@"type":self.localSdp.type, @"sdp":self.localSdp.description};
        NSString *message = [NSString stringWithDictionary:dicSdp];
        [self sendRTCMessage:message withOfferType:YES];
    });
}

然后就是接收方的處理(呼叫的信息是通過socket服務(wù)器告知的):
接收方在收到呼叫方的offer類型的SDP后廉丽,需要設(shè)置RemoteDescription為對方的SDP:

 if ([type isEqualToString:@"offer"] ) {
        NSString *sdpString = MessageDict[@"sdp"];
        self.remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:[NSString preferVoiceCodec:sdpString voiceCodecType:self.voiceCodecType]];
            設(shè)置RemoteDescription為對方的SDP
            if (self.peerConnection) {
                [self.peerConnection setRemoteDescriptionWithDelegate:self
                                                   sessionDescription:self.remoteSdp];
        }

設(shè)置成功之后會觸發(fā)代理方法:
- (void)peerConnection:(RTCPeerConnection *)peerConnection didSetSessionDescriptionWithError:(NSError *)error;
我們處理接收方設(shè)置完成之后的回調(diào)倦微,接收方需要生成類型為answer的SDP返回給呼叫方,表示同意接受通信正压。

- (void)peerConnection:(RTCPeerConnection*)peerConnection didSetSessionDescriptionWithError:(NSError*)error
{
        if (error) {
            return;
        }
         #判斷是接收方
        if (!self.initiator) { 
            if (self.peerConnection && self.peerConnection.remoteDescription && !self.peerConnection.localDescription) {
          #
                [self.peerConnection createAnswerWithDelegate:self constraints:self.sdpMediaConstraints];
            } 
        } 
}

然后又觸發(fā)delegate方法
- (void)peerConnection:(RTCPeerConnection *)peerConnection didCreateSessionDescription:(RTCSessionDescription *)sdp error:(NSError *)error;
還是一模一樣的欣福,把生成的answer的SDP設(shè)為接收方自己的SDP,再把這個SDP再返回給呼叫方焦履。

最后再回到接收方
接收方在收到回調(diào)之后拓劝,再把RemoteDescription設(shè)置好,至此嘉裤,連接正式建立

if ([type isEqualToString:@"answer"]) {
        NSString *sdpString = MessageDict[@"sdp"];
        self.remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:[NSString preferVoiceCodec:sdpString voiceCodecType:self.voiceCodecType]];
            if (self.peerConnection) {
                [self.peerConnection setRemoteDescriptionWithDelegate:self
                                                   sessionDescription:self.remoteSdp];
            } 
    }

從代碼的角度看郑临,呼叫方和接收方的邏輯公用還有回調(diào)之間的跳來跳去是有點(diǎn)暈。

但是總結(jié)一下:其實(shí)就是呼叫方生成offer的SDP价脾,接收方生成answer的SDP牧抵,雙方交換,然后設(shè)置好兩端的localSDP和RemoteSDP。

最后犀变,視頻流傳輸?shù)倪^程中會觸發(fā)回調(diào)方法妹孙,記得渲染到遠(yuǎn)端視頻界面remoteVideoView:

- (void)peerConnection:(RTCPeerConnection*)peerConnection addedStream:(RTCMediaStream*)stream
{
    dispatch_async(dispatch_get_main_queue(), ^{
        ZUSLOG(@"PCO onAddStream.");
        
        if (self.supportVideo && stream.videoTracks.count) {
            self.remoteVideoTrack = stream.videoTracks[0];
            [self.remoteVideoTrack addRenderer:self.remoteVideoView];   //渲染
        }
    });
}

完 ~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市获枝,隨后出現(xiàn)的幾起案子蠢正,更是在濱河造成了極大的恐慌,老刑警劉巖省店,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嚣崭,死亡現(xiàn)場離奇詭異,居然都是意外死亡懦傍,警方通過查閱死者的電腦和手機(jī)雹舀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粗俱,“玉大人说榆,你說我怎么就攤上這事〈缛希” “怎么了签财?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長偏塞。 經(jīng)常有香客問我唱蒸,道長,這世上最難降的妖魔是什么灸叼? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任神汹,我火速辦了婚禮,結(jié)果婚禮上怜姿,老公的妹妹穿的比我還像新娘慎冤。我一直安慰自己,他們只是感情好沧卢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布蚁堤。 她就那樣靜靜地躺著,像睡著了一般但狭。 火紅的嫁衣襯著肌膚如雪披诗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天立磁,我揣著相機(jī)與錄音呈队,去河邊找鬼。 笑死唱歧,一個胖子當(dāng)著我的面吹牛宪摧,可吹牛的內(nèi)容都是我干的粒竖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼几于,長吁一口氣:“原來是場噩夢啊……” “哼蕊苗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起沿彭,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤朽砰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后喉刘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞧柔,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年睦裳,在試婚紗的時候發(fā)現(xiàn)自己被綠了造锅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡推沸,死狀恐怖备绽,靈堂內(nèi)的尸體忽然破棺而出券坞,到底是詐尸還是另有隱情鬓催,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布恨锚,位于F島的核電站宇驾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏猴伶。R本人自食惡果不足惜课舍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望他挎。 院中可真熱鬧筝尾,春花似錦、人聲如沸办桨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呢撞。三九已至损姜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間殊霞,已是汗流浹背摧阅。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绷蹲,地道東北人棒卷。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓顾孽,卻偏偏與公主長得像,于是被迫代替她去往敵國和親比规。 傳聞我的和親對象是個殘疾皇子岩齿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容