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/
相关推荐
dr李四维2 小时前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
️ 邪神2 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】自定义View
flutter·ios·鸿蒙·reactnative·anroid
比格丽巴格丽抱13 小时前
flutter项目苹果编译运行打包上线
flutter·ios
网络安全-老纪15 小时前
iOS应用网络安全之HTTPS
web安全·ios·https
1024小神17 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
lzhdim18 小时前
iPhone 17 Air看点汇总:薄至6mm 刷新苹果轻薄纪录
ios·iphone
安和昂18 小时前
【iOS】知乎日报第四周总结
ios
麦田里的守望者江21 小时前
KMP 中的 expect 和 actual 声明
android·ios·kotlin
_黎明1 天前
【Swift】字符串和字符
开发语言·ios·swift
ZVAyIVqt0UFji1 天前
iOS屏幕共享技术实践
macos·ios·objective-c·cocoa