WebRTC 在 iOS 端实现一对一通信
- [WebRTC 在 iOS 端实现一对一通信](#WebRTC 在 iOS 端实现一对一通信)
WebRTC 在 iOS 端实现一对一通信
在 iOS 端,我们将按以下几个步骤实现 WebRTC 一对一通信:
- 申请权限
- 引入 WebRTC 库
- 构造 PeerConnectionFactory
- 创建音视频源
- 视频采集
- 本地视频预览
- 建立信令系统
- 创建 RTCPeerConnection
- 远端视频渲染
申请权限
为了让您的应用能够使用麦克风和摄像头,您需要在应用的Info.plist文件中添加相应的权限配置。以下是设置应用权限的步骤:
- 在Xcode中打开您的项目,点击项目导航器中的项目名称。
- 找到Info.plist文件,并展开它。
- 在Info.plist文件中,右键点击空白处,选择"Add Row"选项。
- 在弹出的窗口中,选择"Privacy - Microphone Usage Description"选项。
- 在右侧的值字段中,输入一条描述您应用使用麦克风的信息,例如"我们需要使用麦克风进行音频通话"。
- 再次右键点击空白处,选择"Add Row"选项。
- 在弹出的窗口中,选择"Privacy - Camera Usage Description"选项。
- 在右侧的值字段中,输入一条描述您应用使用摄像头的信息,例如"我们需要使用摄像头进行视频通话"。
- 保存并关闭Info.plist文件。
引入 WebRTC 库
接下来,您需要导入WebRTC框架和库到您的iOS项目中。通过WebRTC源码编译出WebRTC库,然后再项目中手动引入它。
以下是导入WebRTC的步骤:
- 在Xcode中打开您的项目,点击项目导航器中的项目名称。
- 在项目设置中,选择"General"选项卡。
- 在"Embedded Binaries"部分点击"+"按钮。
- 在弹出的窗口中,点击"Add Other..."按钮,并选择WebRTC.framework文件。
- 确保在"Add to targets"选项中勾选您的项目。
- 在弹出的窗口中,选择"Copy items if needed"选项,并点击"Finish"按钮。
- 等待Xcode将WebRTC.framework文件导入到项目中。
WebRTC官方会定期发布编译好的WebRTC库,也可以使用Pod方式进行安装(GoogleWebRTC)。我们只需要写个 Podfile 文件就可以了。在 Podfile 中可以指定下载 WebRTC 库的地址,以及我们要安装的库的名字。
Podfile 文件的具体格式如下:
podfile
source 'https://github.com/CocoaPods/Specs.git'
platform :ios,'11.0'
target 'WebRTC4iOS2' do
pod 'GoogleWebRTC'
end
有了 Podfile 之后,在当前目录下执行 pod install 命令,这样 Pod 工具就可以将 WebRTC 库从源上来载下来。
在执行 pod install 之后,它除了下载库文件之外,会为我们产生一个新的工作空间文件,即 {project}.xcworkspace。在该文件里,会同时加载项目文件及刚才安装好的 Pod 依赖库,并使两者建立好关联。
这样,WebRTC库就算引入成功了。下面就可以开始写我们自己的代码了。
构造 RTCPeerConnectionFactory
iOS 端的工厂与 Android 端一样,只是命名上要加上 RTC 前缀。
在 WebRTC Native 层,factory 可以说是 "万物的根源",像 RTCVideoSource、RTCVideoTrack、RTCPeerConnection这些类型的对象,都需要通过 factory 来创建。
objectivec
[RTCPeerConnectionFactory initialize];
//如果点对点工厂为空
if (!factory)
{
RTCDefaultVideoDecoderFactory* decoderFactory =
[[RTCDefaultVideoDecoderFactory alloc] init];
RTCDefaultVideoEncoderFactory* encoderFactory =
[[RTCDefaultVideoEncoderFactory alloc] init];
NSArray* codecs = [encoderFactory supportedCodecs];
[encoderFactory setPreferredCodec:codecs[2]];
factory = [[RTCPeerConnectionFactory alloc]
initWithEncoderFactory: encoderFactory
decoderFactory: decoderFactory];
}
首先要调用 RTCPeerConnectionFactory 类的 initialize 方法进行初始化。然后创建 factory 对象。需要注意的是,在创建 factory 对象时,传入了两个参数:一个是默认的编码器;一个是默认的解码器。我们可以通过修改这两个参数来达到使用不同编解码器的目的。
创建音视频源
分别创建音视频数据源对象(Source),分别创建音视频 Track,分别将音视频源绑定到对应的 Track 上。
objectivec
RTCAudioSource* audioSource = [factory audioSource];
RTCAudioTrack* audioTrack =
[factory audioTrackWithSource:audioSource trackId:@"ARDAMSa0"]
RTCVideoSource* videoSource = [factory videoSource];
RTCVideoTrack* videoTrack =
[factory videoTrackWithSource:videoSource trackId:@"ARDAMSv0"]
视频采集
在获取视频之前,我们首先要选择使用哪个视频设备采集数据。在WebRTC中,我们可以通过RTCCameraVideoCapture类操作设备:
创建对象:
objectivec
capture = [[RTCCameraVideoCapturer alloc] initWithDelegate:videoSource];
获取所有视频设备:
objectivec
NSArray<AVCaptureDevice*>* devices = [RTCCameraVideoCapture captureDevices];
AVCaptureDevice* device = devices[0];
开启摄像头:
objectivec
[capture startCaptureWithDevice:device
format:format
fps:fps];
现在已经可以通过RTCCameraVideoCapturer类控制视频设备来采集视频了, 那如何获取采集的视频流呢?上面的代码我们已经将视频采集到视频源RTCVideoSource了,那RTCVideoSource就是我们的视频流吗?显然不是。这里要提到的是WebRTC三大对象中的其中一个对象RTCMediaStream,它才是我们说的视频流。
视频采集的流程:
- RTCCameraVideoCapturer 将采集的视频数据交给RTCVideoSource
- 通过RTCVideoSource 创建 RTCVideoTrack
- RTCMediaStream 添加视频轨 videoTrack。
本地视频预览
在 iOS 端,WebRTC 准备了两种 View:
- RTCCameraPreviewView:专门用于预览本地视频。不再从 RTCVideoTrack 获得数据,而是直接从 RTCCameraVideoCapturer 获取,效率更高。
- RTCEAGLVideoView:显示远端视频。
viewDidLoad() 在应用程序启动后被调用,属于应用程序生命周期的开始阶段。
objectivec
@property (strong, nonatomic) RTCCameraPreviewView *localVideoView;
- (void)viewDidLoad {
CGRect bounds = self.view.bounds;
self.localVideoView = [[RTCCameraPreviewView alloc]
initWithFrame:CGRectZero];
[self.view addSubview:self.localVideoView];
CGRect localVideoFrame =
CGRectMake(0, 0, bounds.size.width, bounds.size.height);
[self.localVideoView setFrame:localVideoFrame];
}
在 viewDidLoad() 函数里我们创建并初始化了一个 RTCCameraPreviewView,将 localVideoView 对象添加到应用程序的 Main View 中,最后设置了大小和显示位置。
关联 localVideoView 和 RTCCameraVideoCapturer:
objectivec
self.localVideoView.captureSession = capture.captureSession;
传递 captureSession 后,localVideoView 就可以从 RTCCameraVideoCapturer 上获取数据并渲染了。
建立信令系统
在 iOS 端我们仍然使用 socket.io 与信令服务器连接。
Podfile:
swift
source 'https://github.com.CocoaPods.Specs.git'
use_frameworks!
platform : ios, '9.0'
target 'YourProjectName' do
pod 'Socket.IO-Client-Swift', '~> 1.0'
end
信令的使用:
- 通过url获取socket。有了socket之后就可建立与服务器的连接了。
- 注册侦听的消息,并为每个侦听的消息绑定一个处理函数。当收到服务器的消息后,随之会触发绑定的函数。
- 通过socket建立连接。
- 发送信令。
通过url获取socket:
objectivec
SocketIOClient* socket;
NSURL* url =[[NSURL alloc]initWithString:addr];
manager = [[SocketManager alloc] initWithSocketURL:url
config:@{
@"log": @YES,
@"forcePolling":@YES,
@"forceWebsockets":@YES
}];
socket = manager.defaultSocket;
为socket注册侦听消息,以 joined 消息为例:
objectivec
[socket on:@"joined" callback:^(NSArray* data,SocketAckEmitter* ack) {
NSString* room =[data objectAtIndex:0];
NSLog(@"joined room(%@)", room);
[self.delegate joined:room];
}];
连接信令服务器:
objectivec
[socket connect];
使用 emit 方法发送信令:
objectivec
if(socket.status == SocketIOStatusConnected) {
[socket emit:@"join" with:@[room]];
}
创建 RCTPeerConnection
当信令系统建立好后,后面的逻辑都是围绕着信令系统建立起来的。
客户端用户想要与远端通话,首先要发送join消息,也就是要先进入房间。此时,如果服务器判断用户是合法的,则会给客户端会joined消息。
客户端收到joined消息后,就要创建RTCPeerConnection了,也就是要建立一条与远端通话的音视频数据传输通道。
创建 RCTPeerConnection:
objectivec
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 对象有三个参数:
- RTCConfiguration类型的对象,该对象中最重要的一个字段是iceServers。它里面存放了stun/turn服务器地址。其主要作用是用于NAT穿越。
- RTCMediaConstraints类型对象,也就是对RTCPeerConnection的限制。
如:是否接受视频数据?是否接受音频数据?如果要与浏览器互通还要开启DtlsSrtpKeyAgreement选项 - 委托类型。相当于给RTCPeerConnection设置一个观察者。这样RTCPeerConnection可以将一个状态/信息通过它通知给观察者。
RTCPeerConnection 建立好之后,在建立物理连接之前,还需要进行媒体协商。
创建Offer类型的SDP消息:
objectivec
[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。它有两个参数:
- RTCMediaConstraints类型的参数。
- 匿名回调函数。可以通过对error是否为空来判定offerForConstraints方法有没有执行成功。如果执行成功,参数sdp就是创建好的SDP内容。
如果成功获得了SDP,首先存到本地:
objectivec
[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);
}
}
然后再将它发送给服务端,服务器中转给另一端:
objectivec
__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];
});
当整个协商完成后,紧接着会交换 Candidate,在WebRTC底层开始建立物理连接。网络连接完成后,双方就会进行音视频数据的传输。
远端视频渲染
将 RTCEAGLVideoView 与远端视频的 Track 关联:
objectivec
RTCEAGLVideoView* remoteVideoView;
(void)peerConnection:
didAddReceiver:(RTCRtpReceiver *)rtpReceiver
streams:(NSArray *)mediaStreams {
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];
}
peerConnection:didAddReceiver:streams 函数与 JS 的 ontrack 类似,当有远端的流传来时,就会触发该函数。从 rtpReceiver 中获取远端的 track 后,把它添加到 remoteVideoTrack 中,这样 remoteVideoView 就可以从 track 中获取视频数据了。