一、背景
- 背景:記錄下iOS P2P通信的實(shí)現(xiàn)的流程废膘,以及實(shí)現(xiàn)過程中遇到的問題旬薯,方便后續(xù)bug調(diào)試,為代碼整潔規(guī)范化提供設(shè)計(jì)思路
- 目標(biāo)群體:iOS及客戶端開發(fā)人員
- 技術(shù)應(yīng)用場景:iPhone敛劝、iPad端音視頻會(huì)議通話
- 整體思路:1余爆、使用WebSocket連接信令服務(wù)器,執(zhí)行登錄操作(userId登錄夸盟,沒有則用游客身份登錄)蛾方,2、WebRTC實(shí)現(xiàn)P2P連接
- 先入會(huì)者:1上陕、監(jiān)聽新人加入-->2桩砰、創(chuàng)建與新人的PeerConnection連接對(duì)象,發(fā)送_start信令-->3释簿、收到_startResp后創(chuàng)建offer亚隅,設(shè)置setLocalDescription為offer,發(fā)送_offer信令-->4庶溶、收到_answer信令煮纵,setRemoteDescription為answer-->5、互相發(fā)送可用的ice_candidate偏螺,WebRTC會(huì)篩選出最合適的ICE通道連接-->6行疏、雙方建立P2P連接,開始通信砖茸;
- 后入會(huì)者:1隘擎、查詢會(huì)議成員列表,創(chuàng)建與會(huì)中所有人的PeerConnection對(duì)象-->2凉夯、收到_start信令后回復(fù)_startResp-->3货葬、收到_offer信令,setRemoteDescription為offer-->4劲够、創(chuàng)建answer震桶,setLocalDescription為answer,發(fā)送_answer給對(duì)方-->后續(xù)步驟同先入會(huì)者步驟5征绎、6
流程圖:
暫時(shí)無法在文檔外展示此內(nèi)容
二蹲姐、操作步驟
2.1 開發(fā)前的準(zhǔn)備工作
準(zhǔn)備工作一
Xcode安裝好動(dòng)態(tài)庫管理器cocoaPods磨取,引入WebRTC、WebSocket兩個(gè)庫柴墩,代碼如下
pod 'GoogleWebRTC'
pod 'SocketRocket'
準(zhǔn)備工作二
創(chuàng)建EMServerBaseManager信令基類忙厌,實(shí)現(xiàn)EMServerManagerProtocol,公共的連接江咳、斷開的連接逢净、發(fā)送數(shù)據(jù)等方法聲明放此協(xié)議中,方便擴(kuò)展新的信令協(xié)議(WebSocket歼指、MQTT等爹土,目前服務(wù)端用的是WebSocket協(xié)議);
2.2 進(jìn)入開發(fā)階段
1踩身、連接信令服務(wù)器
建立webSocket長連接胀茵,連接失敗就執(zhí)行重連操作,重連時(shí)間間隔以2的指數(shù)倍增長挟阻,網(wǎng)絡(luò)斷開的情況下就開啟一個(gè)定時(shí)器去檢查網(wǎng)絡(luò)情況琼娘,有網(wǎng)絡(luò)了就更新重連時(shí)間,馬上進(jìn)行重連赁濒;
- (void)connectServer {
if (self.connectState == EMSocketConnectStateConnected || self.connectState == EMSocketConnectStateConnecting) return;
self.isActivelyClose = NO;
[self webSocketClose];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:EMSocketBaseUrl]];
self.webSocket = [[SRWebSocket alloc] initWithURLRequest:request];
self.webSocket.delegate = self;
[self.webSocket open];
self.connectState = EMSocketConnectStateConnecting;
}
//連接成功回調(diào)
- (void)webSocketDidOpen:(SRWebSocket *)webSocket {
self.reConnectTime = 0; // 重連間隔時(shí)長
[self sendDataToServer:@{EMSocketMsg:EMSocketMsg_login}]; // 登錄
[self initHeartBeat]; //開啟心跳
self.connectState = EMSocketConnectStateConnected;
}
//連接失敗回調(diào)
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
self.connectState = EMSocketConnectStateDisConnected;
//用戶主動(dòng)斷開連接轨奄,就不去進(jìn)行重連
if(self.isActivelyClose) {
return;
}
[self destoryHeartBeat]; //斷開連接時(shí)銷毀心跳
/// 剛斷開連接,網(wǎng)絡(luò)狀態(tài)可能還沒有改變拒炎,導(dǎo)致網(wǎng)絡(luò)檢測定時(shí)器沒有打開
__weak typeof(self) wSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if(AFNetworkReachabilityManager.sharedManager.networkReachabilityStatus == AFNetworkReachabilityStatusNotReachable && !wSelf.netWorkTestingTimer) {
[wSelf noNetWorkStartTestingTimer];//開啟網(wǎng)絡(luò)檢測定時(shí)器
}
});
//判斷網(wǎng)絡(luò)環(huán)境
if (![self checkNetWork]) //沒有網(wǎng)絡(luò)
{
[self noNetWorkStartTestingTimer];//開啟網(wǎng)絡(luò)檢測定時(shí)器
}
else //有網(wǎng)絡(luò)
{
[self reConnectServer];//連接失敗就重連
}
}
//連接關(guān)閉,注意連接關(guān)閉不是連接斷開挪拟,關(guān)閉是 [socket close] 客戶端主動(dòng)關(guān)閉,斷開可能是斷網(wǎng)了击你,被動(dòng)斷開的玉组。
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
// 在這里判斷 webSocket 的狀態(tài) 是否為 open
self.connectState = EMSocketConnectStateDisConnected;
if(self.webSocket.readyState == SR_OPEN || self.isActivelyClose) {
return;
}
DLog(@"被關(guān)閉連接,code:%ld,reason:%@,wasClean:%d",code,reason,wasClean);
[self destoryHeartBeat]; //斷開連接時(shí)銷毀心跳
//判斷網(wǎng)絡(luò)環(huán)境
if (![self checkNetWork]) //沒有網(wǎng)絡(luò)
{
[self noNetWorkStartTestingTimer];//開啟網(wǎng)絡(luò)檢測
}
else //有網(wǎng)絡(luò)
{
[self reConnectServer];//連接失敗就重連
}
}
2丁侄、WebRtc P2P連接過程
1惯雳、創(chuàng)建peerConnectionFactory對(duì)象,設(shè)置默認(rèn)的視頻編解碼工廠類鸿摇,此處可以自定義視頻編解碼類石景;
- (RTCPeerConnectionFactory *)peerConnectionFactory {
if (!_peerConnectionFactory) {
[RTCPeerConnectionFactory initialize];
RTCDefaultVideoDecoderFactory *decoderFactory = [[RTCDefaultVideoDecoderFactory alloc] init];
RTCDefaultVideoEncoderFactory *encoderFactory = [[RTCDefaultVideoEncoderFactory alloc] init];
NSArray *codecs = [encoderFactory supportedCodecs];
[encoderFactory setPreferredCodec:codecs[2]];
_peerConnectionFactory = [[RTCPeerConnectionFactory alloc] initWithEncoderFactory:encoderFactory decoderFactory:decoderFactory];
}
return _peerConnectionFactory;
}
2、創(chuàng)建新的PeerConnection對(duì)象拙吉,需要設(shè)置配置RTCConfiguration潮孽、約束RTCMediaConstraints;
// 創(chuàng)建新的PeerConnection
- (RTCPeerConnection *)createPeerConnection:(NSString *)connectionId {
RTCPeerConnection *peerConnection = [self.peerConnectionFactory peerConnectionWithConfiguration:self.rtcConfig constraints:[self defaultPeerConnContraints] delegate:self];
//把本地流加到連接中去
[peerConnection addStream:self.localStream];
return peerConnection;
}
- (RTCConfiguration *)rtcConfig {
if (!_rtcConfig) {
NSArray *ICEServers = [NSArray arrayWithObject:[self defaultSTUNServer]];
RTCConfiguration *configuration = [[RTCConfiguration alloc] init];
[configuration setIceServers:ICEServers];
configuration.iceConnectionReceivingTimeout = 90000;// 90s超時(shí)
_rtcConfig = configuration;
}
return _rtcConfig;
}
// 此處填寫透傳服務(wù)器的地址筷黔,username往史,password等
- (RTCIceServer *)defaultSTUNServer {
RTCIceServer *defaultServer = [[RTCIceServer alloc] initWithURLStrings:@[@""] username:@"" credential:@""];
return defaultServer;
}
- (RTCMediaConstraints *)defaultPeerConnContraints {
// 配置信息的基礎(chǔ)單元,以鍵值對(duì)的方式
NSMutableDictionary *mandatory = @{kRTCMediaConstraintsOfferToReceiveAudio:kRTCMediaConstraintsValueTrue}.mutableCopy;
[mandatory setValue:kRTCMediaConstraintsValueTrue forKey:kRTCMediaConstraintsOfferToReceiveVideo];
RTCMediaConstraints *media = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory optionalConstraints:nil];
return media;
}
3佛舱、創(chuàng)建本地媒體流信息椎例,添加本地音視頻軌道到媒體流中挨决,其中初始化數(shù)據(jù)源中,adaptOutputFormatToWidth可以指定視頻的寬高比订歪,幀率等等脖祈,當(dāng)前先用默認(rèn)的;
// 創(chuàng)建本地音視頻軌道
- (RTCMediaStream *)localStream {
if (!_localStream) {
// 初始化媒體流陌粹,kEM_STREAM和kEM_AUDIO_0撒犀、kEM_VIDEO_0為專業(yè)標(biāo)識(shí)符。
_localStream = [self.peerConnectionFactory mediaStreamWithStreamId:kEM_STREAM];
// 添加音頻軌道
self.localAudioTrack = [self.peerConnectionFactory audioTrackWithTrackId:kEM_AUDIO_0];
[_localStream addAudioTrack:self.localAudioTrack];
// 添加視頻軌道
[_localStream addVideoTrack:self.localVideoTrack];
}
return _localStream;
}
- (RTCVideoTrack *)localVideoTrack {
if (!_localVideoTrack) {
// 獲取攝像頭
AVCaptureDevice * device = [self defaultDevice];
//獲取數(shù)據(jù)源
RTCVideoSource *_source = [self.peerConnectionFactory videoSource];
// [_source adaptOutputFormatToWidth:720 height:1280 fps:20];
//拿到capture對(duì)象
RTCCameraVideoCapturer *capture = [[RTCCameraVideoCapturer alloc] initWithDelegate:_source];
AVCaptureDeviceFormat *format = [[RTCCameraVideoCapturer supportedFormatsForDevice:device] lastObject];
// CGFloat fps = [[format videoSupportedFrameRateRanges] firstObject].maxFrameRate;
if (self.cameraState == EMMediaDeviceStatePlay) {
[capture startCaptureWithDevice:device format:format fps:20 completionHandler:^(NSError * error) {
}];
}
self.capture = capture;
_localVideoTrack = [self.peerConnectionFactory videoTrackWithSource:_source trackId:kEM_VIDEO_0];
}
return _localVideoTrack;
}
4掏秩、先入會(huì)者發(fā)送_start、創(chuàng)建offer荆姆,設(shè)置本地描述蒙幻,成功就發(fā)送_offer信令給對(duì)方,等待對(duì)方發(fā)送的answer存到本地遠(yuǎn)程描述胆筒;setStartState為保存start狀態(tài)邮破,方便后續(xù)斷線重連階段判斷是有誰發(fā)起start
/// 請求開始進(jìn)行 P2P 傳輸準(zhǔn)備
- (void)sendStartMsgToMemberId:(NSString *)memberId {
NSDictionary *data = @{EMSocketMsg:EMSocketMsg_start,
EMSocketToId: memberId};
[self sendDataToServer:data completionHandler:nil];
[self setStartState:false isSender:true fromId:memberId];
}
- (void)getSocketMsg:(NSNotification *)x {
NSDictionary *dict = x.userInfo;
NSString *msg = dict[EMSocketMsg];
// 收到start回復(fù)
if ([msg isEqualToString:EMSocketMsg_start_resp]) {
NSString *fromId = dict[EMSocketFromId];
[self setStartState:true isSender:true fromId:fromId];
RTCPeerConnection *peerConnection = [wSelf.connectionDic objectForKey:fromId];
/// 發(fā)送offer
[wSelf createOffer:peerConnection];
}
}
/**
* 創(chuàng)建offer
*/
- (void)createOffer:(RTCPeerConnection *)peerConnection{
__weak typeof(self) wSelf = self;
[peerConnection offerForConstraints:[self defaultPeerConnContraints] completionHandler:^(RTCSessionDescription *_Nullable sdp, NSError *_Nullable error) {
if(error){
DLog(@"Failed to create offer SDP, err=%@", error);
} else {
RTCSessionDescription *newSdp = [wSelf getNewSdpWithType:(RTCSdpTypeOffer) sdp:sdp];
[wSelf setLocalOffer:peerConnection withSdp:newSdp];
}
}];
}
- (void)setLocalOffer:(RTCPeerConnection *)pc withSdp:(RTCSessionDescription *)sdp {
__weak RTCPeerConnection *weakPeerConnection = pc;
__weak typeof(self) wSelf = self;
[pc setLocalDescription:sdp completionHandler:^(NSError *_Nullable error) {
if (!error) {
DLog(@"Successed to set local offer sdp!");
[wSelf sendOffer:weakPeerConnection withSdp:sdp];
}else {
DLog(@"Failed to set local offer sdp, err=%@", error);
}
}];
}
- (void)sendOffer:(RTCPeerConnection *)pc withSdp:(RTCSessionDescription *)sdp {
NSString *currentId = [self getKeyFromConnectionDic:pc];
NSDictionary *dict = [[NSDictionary alloc] initWithObjects:@[@"offer", sdp.sdp] forKeys: @[@"type", @"sdp"]];
NSDictionary *data = @{EMSocketMsg:EMSocketMsg_offer,
@"data":@{@"sdp":dict},
EMSocketToId:currentId};
[self sendDataToServer:data completionHandler:nil];
}
#pragma mark --收到 answer
- (void)answerWith:(NSDictionary *)dic{
NSDictionary *dataDic = dic[@"data"];
NSDictionary *sdp = dataDic[@"sdp"];
NSString *sdpStr = sdp[@"sdp"];
NSString *fromId = dic[EMSocketFromId];
RTCPeerConnection *peerConnection = [self.connectionDic objectForKey:fromId];
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:RTCSdpTypeAnswer sdp:sdpStr];
[peerConnection setRemoteDescription:remoteSdp completionHandler:^(NSError * _Nullable error) {
if (error){
DLog(@"Failed to setRemoteDescription, err=%@", error);
}
}];
}
5、后入會(huì)者查詢?nèi)簝?nèi)成員列表仆救,建立與他們的peerConnection抒和,等待_start信令,發(fā)送_start_resp響應(yīng)彤蔽,等待_offer信令到來摧莽,把offer設(shè)置到遠(yuǎn)程描述,創(chuàng)建answer設(shè)置到本地顿痪,發(fā)送給對(duì)方镊辕;
#pragma mark --收到 offer
- (void)offerWith:(NSDictionary *)dic {
NSDictionary *dataDic = dic[@"data"];
NSString *fromId = dic[EMSocketFromId];
//拿到SDP
NSDictionary *sdp = dataDic[@"sdp"];
NSString *sdpStr = sdp[@"sdp"];
//根據(jù)類型和SDP 生成SDP描述對(duì)象
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:RTCSdpTypeOffer sdp:sdpStr];
//拿到當(dāng)前對(duì)應(yīng)的點(diǎn)對(duì)點(diǎn)連接
RTCPeerConnection *peerConnection = [self.connectionDic objectForKey:fromId];
// 需要判斷當(dāng)前peerConnection是否有answer或者offer,有就重置
if (peerConnection.remoteDescription || peerConnection.localDescription) {
// 創(chuàng)建一個(gè)新的連接
peerConnection = [self createPeerConnection:fromId];
//并且設(shè)置到Dic中去
[self.connectionDic setObject:peerConnection forKey:fromId];
}
//設(shè)置給這個(gè)點(diǎn)對(duì)點(diǎn)連接
__weak RTCPeerConnection *weakPeerConnection = peerConnection;
__weak typeof(self) wSelf = self;
[peerConnection setRemoteDescription:remoteSdp completionHandler:^(NSError * _Nullable error) {
if (!error) {
//創(chuàng)建一個(gè)answer,會(huì)把自己的SDP信息返回出去
[weakPeerConnection answerForConstraints:[wSelf defaultPeerConnContraints] completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) {
if (!error) {
RTCSessionDescription *newSdp = sdp;// [wSelf getNewSdpWithType:(RTCSdpTypeAnswer) sdp:sdp];
[weakPeerConnection setLocalDescription:newSdp completionHandler:^(NSError * _Nullable error) {
NSDictionary *dic = @{EMSocketMsg: EMSocketMsg_answer,
@"data": @{
@"sdp": @{@"sdp":newSdp.sdp,@"type":@"answer",}
},
EMSocketToId:fromId};
[wSelf sendDataToServer:dic completionHandler:nil];
}];
} else {
DLog(@"Failed to create answer an sdp, err=%@", error);
}
}];
} else {
DLog(@"Failed to setRemoteDescription, err=%@", error);
}
}];
}
6蚁袭、雙方互相設(shè)置好offer征懈、answer,建立P2P連接揩悄,WebRtc會(huì)把收集到的可用的candidate回調(diào)給本地卖哎,本地在通過信令服務(wù)器發(fā)送給對(duì)方就可以,對(duì)方收到遠(yuǎn)端的ice_candidate删性,給本地peerConnection addIceCandidate就可以了亏娜,IceCandidate可能會(huì)轉(zhuǎn)發(fā)多次,WebRtc會(huì)找到最合適的通道建立連接镇匀;
// 收到遠(yuǎn)端的ice_candidate
- (void)_ice_candidateWith:(NSDictionary *)dic {
NSDictionary *dataDic = dic[@"data"];
NSDictionary *candidateDic =dataDic[@"candidate"];
NSString *sdpMid = candidateDic[@"sdpMid"];
int sdpMLineIndex = [candidateDic[@"sdpMLineIndex"] intValue];
NSString *sdp = candidateDic[@"candidate"];
//生成遠(yuǎn)端網(wǎng)絡(luò)地址對(duì)象
RTCIceCandidate *candidate = [[RTCIceCandidate alloc] initWithSdp:sdp sdpMLineIndex:sdpMLineIndex sdpMid:sdpMid];
//添加到點(diǎn)對(duì)點(diǎn)連接中
NSString *fromId = dic[EMSocketFromId];
RTCPeerConnection *peerConnection = [_connectionDic objectForKey:fromId];
[peerConnection addIceCandidate:candidate];
}
#pragma mark - RTCPeerConnectionDelegate
// 該方法用于收集可用的candidate
- (void)peerConnection:(RTCPeerConnection *)peerConnection didGenerateIceCandidate:(RTCIceCandidate *)candidate {
NSString *userId = [self getKeyFromConnectionDic:peerConnection];
NSDictionary *dic = @{
EMSocketMsg: EMSocketMsg_ice_candidate,
@"data":@{
@"candidate":@{
@"sdpMid":candidate.sdpMid ,
@"sdpMLineIndex":[NSNumber numberWithInteger:candidate.sdpMLineIndex],
@"candidate": candidate.sdp
}
} ,
EMSocketToId:userId
};
[self sendDataToServer:dic completionHandler:nil];
}
7照藻、將對(duì)方視頻渲染到本地
/* *Called when media is received on a new stream from remote peer. */
- (void)peerConnection:(RTCPeerConnection *)peerConnection didAddStream:(RTCMediaStream *)stream {
LOG_ME
dispatch_async(dispatch_get_main_queue(), ^{
if (stream.videoTracks.count) {
NSString *userId = [self getKeyFromConnectionDic:peerConnection];
// RTCVideoTrack *remoteVideoTrack = stream.videoTracks[0];
// 遠(yuǎn)端視頻軌道一定要保存在本地,否則會(huì)渲染不出來
if ([self->_delegate respondsToSelector:@selector(webRtcManager:addRemoteStream:userId:)]) {
[self->_delegate webRtcManager:self addRemoteStream:stream userId:userId];
}
}
});
}
2.3 注意事項(xiàng)
1汗侵、iOS視頻編碼格式
iOS并不支持VP8的視頻編碼格式幸缕,但是WebRtc生成的offer群发、answer描述又都是把VP8放前,默認(rèn)是VP8編碼格式发乔,所以此處要對(duì)WebRTC生成的描述進(jìn)行處理熟妓,交互VP8跟H264編碼的順序(iOS對(duì)H264的支持度很高),然后再生成新的描述對(duì)象栏尚,存到本地起愈;
RTCSessionDescription *newSdp = [self getNewSdpWithType:(RTCSdpTypeOffer) sdp:sdp];
[elf setLocalOffer:peerConnection withSdp:newSdp];