WebRTC 在 iOS 端实现一对一通信

WebRTC 在 iOS 端实现一对一通信

WebRTC 在 iOS 端实现一对一通信

在 iOS 端,我们将按以下几个步骤实现 WebRTC 一对一通信:

  1. 申请权限
  2. 引入 WebRTC 库
  3. 构造 PeerConnectionFactory
  4. 创建音视频源
  5. 视频采集
  6. 本地视频预览
  7. 建立信令系统
  8. 创建 RTCPeerConnection
  9. 远端视频渲染

申请权限

为了让您的应用能够使用麦克风和摄像头,您需要在应用的Info.plist文件中添加相应的权限配置。以下是设置应用权限的步骤:

  1. 在Xcode中打开您的项目,点击项目导航器中的项目名称。
  2. 找到Info.plist文件,并展开它。
  3. 在Info.plist文件中,右键点击空白处,选择"Add Row"选项。
  4. 在弹出的窗口中,选择"Privacy - Microphone Usage Description"选项。
  5. 在右侧的值字段中,输入一条描述您应用使用麦克风的信息,例如"我们需要使用麦克风进行音频通话"。
  6. 再次右键点击空白处,选择"Add Row"选项。
  7. 在弹出的窗口中,选择"Privacy - Camera Usage Description"选项。
  8. 在右侧的值字段中,输入一条描述您应用使用摄像头的信息,例如"我们需要使用摄像头进行视频通话"。
  9. 保存并关闭Info.plist文件。

引入 WebRTC 库

接下来,您需要导入WebRTC框架和库到您的iOS项目中。通过WebRTC源码编译出WebRTC库,然后再项目中手动引入它。

以下是导入WebRTC的步骤:

  1. 在Xcode中打开您的项目,点击项目导航器中的项目名称。
  2. 在项目设置中,选择"General"选项卡。
  3. 在"Embedded Binaries"部分点击"+"按钮。
  4. 在弹出的窗口中,点击"Add Other..."按钮,并选择WebRTC.framework文件。
  5. 确保在"Add to targets"选项中勾选您的项目。
  6. 在弹出的窗口中,选择"Copy items if needed"选项,并点击"Finish"按钮。
  7. 等待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,它才是我们说的视频流。

视频采集的流程:

  1. RTCCameraVideoCapturer 将采集的视频数据交给RTCVideoSource
  2. 通过RTCVideoSource 创建 RTCVideoTrack
  3. RTCMediaStream 添加视频轨 videoTrack。

本地视频预览

在 iOS 端,WebRTC 准备了两种 View:

  1. RTCCameraPreviewView:专门用于预览本地视频。不再从 RTCVideoTrack 获得数据,而是直接从 RTCCameraVideoCapturer 获取,效率更高。
  2. 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

信令的使用:

  1. 通过url获取socket。有了socket之后就可建立与服务器的连接了。
  2. 注册侦听的消息,并为每个侦听的消息绑定一个处理函数。当收到服务器的消息后,随之会触发绑定的函数。
  3. 通过socket建立连接。
  4. 发送信令。

通过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 对象有三个参数:

  1. RTCConfiguration类型的对象,该对象中最重要的一个字段是iceServers。它里面存放了stun/turn服务器地址。其主要作用是用于NAT穿越。
  2. RTCMediaConstraints类型对象,也就是对RTCPeerConnection的限制。
    如:是否接受视频数据?是否接受音频数据?如果要与浏览器互通还要开启DtlsSrtpKeyAgreement选项
  3. 委托类型。相当于给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。它有两个参数:

  1. RTCMediaConstraints类型的参数。
  2. 匿名回调函数。可以通过对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 中获取视频数据了。

参考

  1. https://webrtc.org.cn/20190517_tutorial4_webrtc_ios/
相关推荐
他们都不看好你,偏偏你最不争气3 小时前
iOS —— 天气预报仿写总结
ios
白玉cfc9 小时前
【iOS】网易云仿写
ui·ios·objective-c
归辞...12 小时前
「iOS」——内存五大分区
macos·ios·cocoa
HX43612 小时前
MP - List (not just list)
android·ios·全栈
肥or胖14 小时前
【音视频协议篇】WebRTC 快速入门
ffmpeg·音视频·webrtc
忆江南16 小时前
NSProxy是啥,用来干嘛的
ios
忆江南16 小时前
dyld
ios
守城小轩1 天前
WebRTC指纹识别——未来展望(下篇)
chrome·webrtc·chrome devtools·指纹浏览器·浏览器开发
守城小轩1 天前
WebRTC指纹——技术背景(上篇)
webrtc·chrome devtools·指纹浏览器·浏览器开发·超级浏览器
归辞...1 天前
「iOS」——GCD其他方法详解
macos·ios·cocoa