WebRTC在iOS端的使用
权限申请:
Info.plist中添加 Camera和Microphone访问权限
引入WebRTC库:
1、通过WebRTC源码编译出WebRTC库,然后再项目中手动引入它
2、WebRTC官方会定期发布编译好的WebRTC库,可以使用Pod方式进行安装(GoogleWebRTC)
获取本地视频:
WebRTC库引入成功后,就可以开始真正的WebRTC之旅了。
在获取视频之前,首先要选择使用哪个视频设备采集数据。在WebRTC中,我们可以通过RTCCameraVideoCapture类获取所有的视频设备。
NSArray *devices =[RTCCameraVideoCapture captureDevices];
AVCaptureDevice *device = devices[0];
光有设备不行,还要清楚从设备中采集的数据放到哪里,这样才能将其展示出来。
WebRTC提供了一个专门的类,即RTCVideoSource,有两层含义:
1、表明它是视频源。当要展示视频的时候,就从这里获取数据
2、它也是一个终点,即:当从视频设备采集到视频数据时,要交给它暂存起来
RTCCameraVideoCapture,专门用于操作设备的类,通过它,可以自如的控制视频设备了。
信令驱动:
在任何系统中,都可以说是信令是系统的灵魂。例如,由谁来发起呼叫;媒体协商时,什么时间发哪种SDP都是有信令控制的。
客户端命令:
1、join,用户加入房间
2、leave,用户离开房间
3、message,端到端命令(offer,answer,candidate)
服务端命令:
1、joined,用户已加入
2、leaved,用户已离开
3、other_joined,其他用户已加入
4、bye,其他用户已离开
5、full,房间已满
信令状态机
通过信令状态机来管理信令,不同的状态下,需要发不同的信令。同样的,当收到服务端或对端的信令后,状态会随之发生改变。
在初始时,客户端处于init/leaved状态。
在 init/leaved 状态下,用户只能发送join消息。服务端收到join消息后,会返回joined消息。此时,客户端会更新为joined状态。
在joined状态下,客户端有多种选择,收到不同的消息会切到不同的状态:
如果用户离开房间,那客户端又回到了初始状态,即 init/leaved 状态。
如果客户端收到second user join消息,则切换到join_conn状态。在这种状态下,两个用户就可以进行通话了。
如果客户端收到second user leave消息,则切换到join_unbind状态。其实join_unbind状态与joined状态基本是一致的。
如果客户端处于join_conn状态,当它收到second user leave消息时,也会转成joined_unbind状态。
如果客户端是joined_unbind状态,当它收到second user join消息时,会切到join_conn状态。
socket.io (信令的基础库)
信令的使用:
1、通过url获取socket。有了socket之后就可建立与服务器的连接了
2、注册侦听的消息,并为每个侦听的消息绑定一个处理函数。当收到服务器的消息后,随之会触发绑定的函数
3、通过socket建立连接
4、发送消息
获取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];
发送消息:
if(socket.status == SocketIOStatusConnected){
[socket emit:@"join" with:@[room]];
}
创建RCTPeerConnection
当信令系统建立好后,后面的逻辑都是围绕着信令系统建立起来的
客户端用户想要与远端通话,首先要发送join消息,也就是要先进入房间。此时,如果服务器判断用户是合法的,则会给客户端会joined消息
客户端收到joined消息后,就要创建RTCPeerConnection了,也就是要建立一条与远端通话的音视频数据传输通道
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对象有三个参数:
1、RTCConfiguration类型的对象,该对象中最重要的一个字段是iceservers。它里面存放了stun/turn服务器地址。其主要作用是用于NAT穿越。
2、RTCMediaConstraints类型对象,也就是对RTCPeerConnection的限制。
如:是否接受视频数据?是否接受音频数据?如果要与浏览器互通还要开启DtlsSrtpKeyAgreement选项
3、委托类型。相当于给RTCPeerConnection设置一个观察者。这样RTCPeerConnection可以将一个状态/信息通过它通知给观察者。
RTCPeerConnection建立好之后,接下来就是整个实时通话过程中,最重要的部分,媒体协商
媒体协商
媒体协商内容使用的是SDP协议
Amy与Bob进行通话,通话的发起方(Amy),首先要创建Offer类型的SDP消息,之后调用RTCPeerConnection对象的setLocalDescription方法,将Offer保存到本地
紧接着,将Offer发送给服务器。然后通过信令服务器中转到被呼叫方(Bob)。被呼叫方收到Offer后,调用它的RTCPeerConnection对象的setRemoteDescription方法,将远端的Offer保存起来
之后,被呼叫方创建Answer类型的SDP内容,并调用RTCPeerConnection对象的setLocalDescription方法将它存储到恩地
同样的,它也要将Answer发送给服务器。服务器收到该消息后,不做任何处理,直接中转给呼叫方。呼叫方收到Answer后,调用setRemoteDescription将其保存起来
手绘图通过上面的步骤,整个媒体协商部分就完成了
[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方法创建Offer SDP。它有两个参数
1、RTCMediaConstraints类型的参数
2、匿名回调函数。可以通过对error是否为空来判定offerForConstraints方法有没有执行成功。如果执行成功啦,参数sdp就是创建好的SDP内容
如果成功获得了SDP,首先存到本地,然后再将它发送给服务端,服务器中转给另一端
[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];
});
其实就是做了两件事。一是调用setLocalDescription方法将SDP保存到本地,另一件事就是发消息。
当整个协商完成后,紧接着,在WebRTC底层就会进行音视频数据的传输。
渲染远端视频:
在创建RTCPeerConnection对象时,同时给RTCPeerConnection设置了一个委托
该委托对象中,实现了所有RTCPeerConnection对象的代理方法,其中比较关键的有下面几个:
1、- (void)peerConnection:(RTCPeerConnection *)peerConnection didGenerateIceCandidate:(RTCIceCandidate *)candidate;//该方法用于收集可用的Candidate。
2、-(void)peerConnection:(RTCPeerConnection *)peerConnection
didChangeIceConnectionState:(RTCIceConnectionState)newState;//当ICE连接状态发生变化时会触发该方法
3、-(void)peerConnection:(RTCPeerConnection *)peerConnection
didAddReceiver:(RTCRtpReceiver *)rtpReceiver streams:(NSArray *)mediaStreams;//该方法在侦听到远端track时会触发。
那么,什么时候开始渲染远端视频呢?当有远端视频流过来的时候,就会触发
-(void)peerConnection:(RTCPeerConnection *)peerConnection didAddReceiver:(RTCRtpReceiver *)rtpReceiver streams: (NSArray *)mediaStreams方法。所以我们只需要在该方法中写一些逻辑即可。
当上面的函数被调用后,我们可以通过rtpReceiver参数获取到track。这个track有可能是音频trak,也有可能是视频trak。所以,我们首先要对 track 做个判断,看其是视频还是音频。
如果是视频的话,就将remoteVideoView加入到trak中,相当于给track添加了一个观察者,这样remoteVideoView就可以从track获取到视频数据了。在remoteVideoView实现了渲染方法,一量收到数据就会直接进行渲染。最终,我们就可以看到远端的视频了。
具体代码如下:
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];
}
通过上面的代码,我们就可以将远端传来的视频展示出来了。
**********************************************************************************************************************
总结:(七步骤)
权限申请
引入WebRTC库
获取本地视频
信令驱动
创建音视频数据通道
媒体协商
渲染远端视频