一、背景
- 背景:记录下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];