iOS P2P通信实现流程

一、背景

  • 背景:记录下iOS P2P通信的实现的流程,以及实现过程中遇到的问题,方便后续bug调试,为代码整洁规范化提供设计思路
  • 目标群体:iOS及客户端开发人员
  • 技术应用场景:iPhone、iPad端音视频会议通话
  • 整体思路:1、使用WebSocket连接信令服务器,执行登录操作(userId登录,没有则用游客身份登录),2、WebRTC实现P2P连接
    • 先入会者:1、监听新人加入-->2、创建与新人的PeerConnection连接对象,发送_start信令-->3、收到_startResp后创建offer,设置setLocalDescription为offer,发送_offer信令-->4、收到_answer信令,setRemoteDescription为answer-->5、互相发送可用的ice_candidate,WebRTC会筛选出最合适的ICE通道连接-->6、双方建立P2P连接,开始通信;
    • 后入会者:1、查询会议成员列表,创建与会中所有人的PeerConnection对象-->2、收到_start信令后回复_startResp-->3、收到_offer信令,setRemoteDescription为offer-->4、创建answer,setLocalDescription为answer,发送_answer给对方-->后续步骤同先入会者步骤5、6

流程图:

暂时无法在文档外展示此内容

二、操作步骤

2.1 开发前的准备工作

准备工作一

  • Xcode安装好动态库管理器cocoaPods,引入WebRTC、WebSocket两个库,代码如下

pod 'GoogleWebRTC'

pod 'SocketRocket'

准备工作二

创建EMServerBaseManager信令基类,实现EMServerManagerProtocol,公共的连接、断开的连接、发送数据等方法声明放此协议中,方便扩展新的信令协议(WebSocket、MQTT等,目前服务端用的是WebSocket协议);

2.2 进入开发阶段

1、连接信令服务器

建立webSocket长连接,连接失败就执行重连操作,重连时间间隔以2的指数倍增长,网络断开的情况下就开启一个定时器去检查网络情况,有网络了就更新重连时间,马上进行重连;

- (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;
}

//连接成功回调
- (void)webSocketDidOpen:(SRWebSocket *)webSocket { 
  self.reConnectTime = 0; // 重连间隔时长
  [self sendDataToServer:@{EMSocketMsg:EMSocketMsg_login}]; // 登录
  [self initHeartBeat]; //开启心跳
  self.connectState = EMSocketConnectStateConnected; 
} 

//连接失败回调
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
  self.connectState = EMSocketConnectStateDisConnected;
  //用户主动断开连接,就不去进行重连
  if(self.isActivelyClose) {
    return;
  }

  [self destoryHeartBeat]; //断开连接时销毁心跳

  /// 刚断开连接,网络状态可能还没有改变,导致网络检测定时器没有打开
  __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];//开启网络检测定时器
    }
  });
  //判断网络环境
  if (![self checkNetWork]) //没有网络
  {
    [self noNetWorkStartTestingTimer];//开启网络检测定时器
  }
  else //有网络
  {
    [self reConnectServer];//连接失败就重连
  }
}

//连接关闭,注意连接关闭不是连接断开,关闭是 [socket close] 客户端主动关闭,断开可能是断网了,被动断开的。
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
  // 在这里判断 webSocket 的状态 是否为 open
  self.connectState = EMSocketConnectStateDisConnected;
  if(self.webSocket.readyState == SR_OPEN || self.isActivelyClose) {
    return;
  }

  DLog(@"被关闭连接,code:%ld,reason:%@,wasClean:%d",code,reason,wasClean);

  [self destoryHeartBeat]; //断开连接时销毁心跳

  //判断网络环境
  if (![self checkNetWork]) //没有网络
  {
    [self noNetWorkStartTestingTimer];//开启网络检测
  }
  else //有网络
  {
    [self reConnectServer];//连接失败就重连
  }
}

2、WebRtc P2P连接过程

1、创建peerConnectionFactory对象,设置默认的视频编解码工厂类,此处可以自定义视频编解码类;

- (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、创建新的PeerConnection对象,需要设置配置RTCConfiguration、约束RTCMediaConstraints;

// 创建新的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超时
    _rtcConfig = configuration;
  }
  return _rtcConfig;
}

// 此处填写透传服务器的地址,username,password等
- (RTCIceServer *)defaultSTUNServer {
  RTCIceServer *defaultServer = [[RTCIceServer alloc] initWithURLStrings:@[@""] username:@"" credential:@""];
  return defaultServer;
}

- (RTCMediaConstraints *)defaultPeerConnContraints {
  // 配置信息的基础单元,以键值对的方式
   NSMutableDictionary *mandatory = @{kRTCMediaConstraintsOfferToReceiveAudio:kRTCMediaConstraintsValueTrue}.mutableCopy;
   [mandatory setValue:kRTCMediaConstraintsValueTrue forKey:kRTCMediaConstraintsOfferToReceiveVideo];
  RTCMediaConstraints *media = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatory optionalConstraints:nil];
  return media;
}

3、创建本地媒体流信息,添加本地音视频轨道到媒体流中,其中初始化数据源中,adaptOutputFormatToWidth可以指定视频的宽高比,帧率等等,当前先用默认的;

// 创建本地音视频轨道
- (RTCMediaStream *)localStream {
  if (!_localStream) { 
    // 初始化媒体流,kEM_STREAM和kEM_AUDIO_0、kEM_VIDEO_0为专业标识符。
    _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]; 
    //获取数据源
    RTCVideoSource *_source = [self.peerConnectionFactory videoSource];
//    [_source adaptOutputFormatToWidth:720 height:1280 fps:20];
    //拿到capture对象
    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、先入会者发送_start、创建offer,设置本地描述,成功就发送_offer信令给对方,等待对方发送的answer存到本地远程描述;setStartState为保存start状态,方便后续断线重连阶段判断是有谁发起start

/// 请求开始进行 P2P 传输准备
- (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回复
  if ([msg isEqualToString:EMSocketMsg_start_resp]) {
    NSString *fromId = dict[EMSocketFromId];
    [self setStartState:true isSender:true fromId:fromId];
    RTCPeerConnection *peerConnection = [wSelf.connectionDic objectForKey:fromId];
    /// 发送offer
    [wSelf createOffer:peerConnection];
  }
}

/**
 * 创建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、后入会者查询群内成员列表,建立与他们的peerConnection,等待_start信令,发送_start_resp响应,等待_offer信令到来,把offer设置到远程描述,创建answer设置到本地,发送给对方;

#pragma mark --收到 offer
- (void)offerWith:(NSDictionary *)dic {
  NSDictionary *dataDic = dic[@"data"];
  NSString *fromId = dic[EMSocketFromId];
  //拿到SDP
  NSDictionary *sdp = dataDic[@"sdp"];
  NSString *sdpStr = sdp[@"sdp"];
  //根据类型和SDP 生成SDP描述对象
  RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:RTCSdpTypeOffer sdp:sdpStr];
  //拿到当前对应的点对点连接
  RTCPeerConnection *peerConnection = [self.connectionDic objectForKey:fromId];
  // 需要判断当前peerConnection是否有answer或者offer,有就重置
  if (peerConnection.remoteDescription || peerConnection.localDescription) {
    // 创建一个新的连接
    peerConnection = [self createPeerConnection:fromId];
    //并且设置到Dic中去
    [self.connectionDic setObject:peerConnection forKey:fromId];
  }
  //设置给这个点对点连接
  __weak RTCPeerConnection *weakPeerConnection = peerConnection;
  __weak typeof(self) wSelf = self;
  [peerConnection setRemoteDescription:remoteSdp completionHandler:^(NSError * _Nullable error) {
    if (!error) {
      //创建一个answer,会把自己的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、双方互相设置好offer、answer,建立P2P连接,WebRtc会把收集到的可用的candidate回调给本地,本地在通过信令服务器发送给对方就可以,对方收到远端的ice_candidate,给本地peerConnection addIceCandidate就可以了,IceCandidate可能会转发多次,WebRtc会找到最合适的通道建立连接;

// 收到远端的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"];
  //生成远端网络地址对象
  RTCIceCandidate *candidate = [[RTCIceCandidate alloc] initWithSdp:sdp sdpMLineIndex:sdpMLineIndex sdpMid:sdpMid];
  //添加到点对点连接中
  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、将对方视频渲染到本地

/* *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]; 
      // 远端视频轨道一定要保存在本地,否则会渲染不出来
      if ([self->_delegate respondsToSelector:@selector(webRtcManager:addRemoteStream:userId:)]) {
        [self->_delegate webRtcManager:self addRemoteStream:stream userId:userId];
      }
    }
  });
}

2.3 注意事项

1、iOS视频编码格式

iOS并不支持VP8的视频编码格式,但是WebRtc生成的offer、answer描述又都是把VP8放前,默认是VP8编码格式,所以此处要对WebRTC生成的描述进行处理,交互VP8跟H264编码的顺序(iOS对H264的支持度很高),然后再生成新的描述对象,存到本地;

RTCSessionDescription *newSdp = [self getNewSdpWithType:(RTCSdpTypeOffer) sdp:sdp];
[elf setLocalOffer:peerConnection withSdp:newSdp];

2.4 参考资料

iOS WebSocket长链接

iOS WebRTC的使用

iOS webRTC SDP介绍及设置

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容