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 文件的具体格式如下:
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 来创建。
[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 上。
RTCAudioSource* audioSource = [factory audioSource]; RTCAudioTrack* audioTrack = [factory audioTrackWithSource:audioSource trackId:@"ARDAMSa0"] RTCVideoSource* videoSource = [factory videoSource]; RTCVideoTrack* videoTrack = [factory videoTrackWithSource:videoSource trackId:@"ARDAMSv0"]
视频采集
在获取视频之前,我们首先要选择使用哪个视频设备采集数据。在WebRTC中,我们可以通过RTCCameraVideoCapture类操作设备:
创建对象:
capture = [[RTCCameraVideoCapturer alloc] initWithDelegate:videoSource];
获取所有视频设备:
NSArray<AVCaptureDevice*>* devices = [RTCCameraVideoCapture captureDevices]; AVCaptureDevice* device = devices[0];
开启摄像头:
[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() 在应用程序启动后被调用,属于应用程序生命周期的开始阶段。
@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:
self.localVideoView.captureSession = capture.captureSession;
传递 captureSession 后,localVideoView 就可以从 RTCCameraVideoCapturer 上获取数据并渲染了。
建立信令系统
在 iOS 端我们仍然使用 socket.io 与信令服务器连接。
Podfile:
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:
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 消息为例:
[socket on:@"joined" callback:^(NSArray* data,SocketAckEmitter* ack) { NSString* room =[data objectAtIndex:0]; NSLog(@"joined room(%@)", room); [self.delegate joined:room]; }];
连接信令服务器:
[socket connect];
使用 emit 方法发送信令:
if(socket.status == SocketIOStatusConnected) { [socket emit:@"join" with:@[room]]; }
创建 RCTPeerConnection
当信令系统建立好后,后面的逻辑都是围绕着信令系统建立起来的。
客户端用户想要与远端通话,首先要发送join消息,也就是要先进入房间。此时,如果服务器判断用户是合法的,则会给客户端会joined消息。
客户端收到joined消息后,就要创建RTCPeerConnection了,也就是要建立一条与远端通话的音视频数据传输通道。
创建 RCTPeerConnection:
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消息:
[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,首先存到本地:
[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]; });
当整个协商完成后,紧接着会交换 Candidate,在WebRTC底层开始建立物理连接。网络连接完成后,双方就会进行音视频数据的传输。
远端视频渲染
将 RTCEAGLVideoView 与远端视频的 Track 关联:
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 中获取视频数据了。
参考
- https://webrtc.org.cn/20190517_tutorial4_webrtc_ios/