百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

WebRTC 入门篇:iOS 端如何使用 WebRTC

zhezhongyun 2025-03-30 23:09 24 浏览

之前,我已经写过 Android 端如何使用 WebRTC 的文章。在那篇文章中,我向大家介绍了在 Android 端是如何使用 WebRTC 进行音视频通话的。今天,我们再来看看 iOS 端1对1音视频实时通话的具体实现。

iOS 端的实现逻辑与 Android 端基本相同,最大的区别可能是语言方面的差异啦!所以,下面我基本上还是按照介绍 Android 端一样的过程来介绍 iOS 端的实现。具体步骤如下:

权限申请

引入 WebRTC 库

采集并显示本地视频

信令驱动

创建音视频数据通道

媒体协商

渲染远端视频

通过上面几个小节,全面介绍如何在iOS端如何使用 WebRTC。

申请权限

首先,我们来看一下 iOS 端是如何获取访问音视频设备权限的。相比 Android 端而言,iOS端获取相关权限要容易很多。其步骤如下:

打开项目,点击左侧目录中的项目。

在左侧目录找到 info.plist,并将其打开。

点击 右侧 看到 “+” 好的地方。

添加 Camera 和 Microphone 访问权限。

下面这张图更清晰地展现了申请权限的步骤:

通过以上步骤,我们就将访问音视频设备的权限申请好了。申请完权限后,下面我们来看一下iOS端如何引入 WebRTC 库。

引入WebRTC库

在iOS端引入 WebRTC 库有两种方式:

第一种,是通过 WebRTC 源码编译出 WebRTC 库,然后在项目中手动引入它;

第二种方式,是 WebRTC 官方会定期发布编译好的 WebRTC 库,我们可以使用 Pod 方式进行安装。

在本项目中,我们使用第二种方式。

使用第二种方式引入 WebRTC 库非常简单,我们只需要写个 Podfile 文件就可以了。在 Podfile 中可以指定下载 WebRTC 库的地址,以及我们要安装的库的名子。

Podfile 文件的具体格式如下:

source 'https://github.com/CocoaPods/Specs.git'
  
platform :ios,'11.0'
target 'WebRTC4iOS2' do
pod 'GoogleWebRTC'
end

source,指定了库文件从哪里下载

platform,指定了使用的平台及平台版本

target,指定项目的名子

pod,指定要安装的库

有了 Podfile 之后,在当前目录下执行 pod install 命令,这样 Pod 工具就可以将 WebRTC 库从源上来载下来。

在执行 pod install 之后,它除了下载库文件之外,会为我们产生一个新的工作空间文件,即**{project}.xcworkspace**。在该文件里,会同时加载项目文件及刚才安装好的 Pod 依赖库,并使两者建立好关联。

这样,WebRTC库就算引入成功了。下面就可以开始写我们自己的代码了。

获取本地视频

WebRTC 库引入成功之后,我们就可以开始真正的 WebRTC 之旅了。下面,我们来看一下如何获取本地视频并将其展示出来。

在获取视频之前,我们首先要选择使用哪个视频设备采集数据。在WebRTC中,我们可以通过RTCCameraVideoCapture 类获取所有的视频设备。如下所示:

NSArray* devices = [RTCCameraVideoCapture captureDevices];
AVCaptureDevice* device = devices[0];

通过上面两行代码,我们就拿到了视频设备中的第一个设备。简单吧!

当然,光有设备还不行。我们还要清楚从设备中采集的数据放到哪里了,这样我们才能将其展示出来。

WebRTC 为我们提供了一个专门的类,即 RTCVideoSource。它有两层含义:

一是表明它是一个视频源。当我们要展示视频的时候,就从这里获取数据;

另一方面,它也是一个终点。即,当我们从视频设备采集到视频数据时,要交给它暂存 起来。

除此之外,为了能更方便的控制视频设备,WebRTC 提供了一个专门用于操作设备的类,即 RTCCameraVideoCapture。通过它,我们就可以自如的控制视频设备了。

通过上面介绍的两个类,以及前面介绍的 AVCaptureDevice,我们就可以轻松地将视频数据采集出来了。下面我们就来具体看一下代码吧!

在该代码中,首先将 RTCVideoSource 与 RTCCameraVideoCapture 进行绑定,然后再开启设备,这样视频数据就源源不断地被采集到 RTCVideoSource 中了。

...
RTCVideoSource* videoSource = [factory videoSource];
capture = [[RTCCameraVideoCapturer alloc] initWithDelegate:videoSource];
...
[capture startCaptureWithDevice:device
                             format:format
                                fps:fps];
...

通过上面的几行代码就可以从摄像头捕获视频数据了。

这里有一点需要特别强调一下,就是 factory 对象。在 WebRTC Native 层,factory 可以说是 “万物的根源”,像 RTCVideoSource、RTCVideoTrack、RTCPeerConnection 这些类型的对象,都需要通过 factory 来创建。 那么,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 对象时,传入了两个参数:一个是默认的编码器;一个是默认的解码器。我们可以通过修改这两个参数来达到使用不同编解码器的目的。

有了 factory 对象后,我们就可以开始创建其它对象了。那么,紧接下来的问题就是如何将采集到的视频展示出来了。

在iOS端展示本地视频与Android端还是有很大区别的,这主要是由于不同系统底层实现方式不一样。为了更高效地展示本地视频,它们采用了不同的方式。

在iOS端展示本地视频其实非常的简单,只需要在调用 capture 的 startCaptureWithDevice 方法之前执行下面的语句就好了:

self.localVideoView.captureSession = capture.captureSession;

当然,在iOS页面初始化的时候,一定要记得定义 localVideoView 哟,其类型为 RTCCameraPreviewView!

通过上面的步骤,我们就可以看到视频设备采集到的视频图像了。

信令驱动

上面我们介绍了iOS端权限的申请,WebRTC库的引入,以及本地视频的采集与展示,这些功能实现起来都很简单。但接下来我们要介绍的信令就要复杂一些了。

在任何系统中,都可以说信令是系统的灵魂。例如,由谁来发起呼叫;媒体协商时,什么时间发哪种 SDP 都是由信令控制的。

对于本项目来说,它的信令相对还是比较简单,它包括下面几种信令:

客户端命令

join,用户加入房间

leave,用户离开房间

message,端到端命令(offer、answer、candidate)

服务端命令

joined,用户已加入

leaved,用户已离开

other_joined,其它用户已加入

bye,其它用户已离开

full,房间已满

这些信令之间是怎样一种关系?在什么情况下该发送怎样的信令呢?要回答这个问题我们就要看一下信令状态机了。

信令状态机

在 iOS 端的信令与我们之前介绍的 js端 和 Android 端一样,会通过一个信令状态机来管理。在不同的状态下,需要发不同的信令。同样的,当收到服务端,或对端的信令后,状态会随之发生改变。下面我们来看一下这个状态的变化图吧:

在初始时,客户端处于 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 库

看过我之前文章的同学应该都清楚,无论是在 js端,还是在 Android 端的实时通话中,我一直使用 socket.io库作为信令的基础库。之所以选择 socket.io,

一方面是由于它支持跨平台,这样在各个平台上我们都可以保持相同的逻辑;

另一方面,socket.io 使用简单,功能又非常强大;

不过,在 iOS 端的 socket.io 使用 swift 语言实现的,而我们的1对1系统则是用 Object-C 实现的。那么,就带来一个问题,在 OC (Object-C) 里是否可以直接使用 swift 编写的库呢?

答案是肯定的。我们只需要在 Podfile 中 增加 use_frameworks! 指令即可。 所以,我们的 Podfile 现在应该变成这个样子:

source 'https://github.com/CocoaPods/Specs.git'
  
platform :ios,'11.0'
use_frameworks!
target 'WebRTC4iOS2' do
pod 'Socket.IO-Client-Swift', '~> 13.3.0'
pod 'GoogleWebRTC'
end

上面 Podfile 中,每行的含义大家应该都很清楚了,我这里就不做过多讲解了。

信令的使用

socket.io 库引入成功后,下面我们来看一下何使用 socket.io。在 iOS 下,使用 socket.io 分为三步:

通过 url 获取 socket。有了 socket 之后我们就可建立与服务器的连接了。

注册侦听的消息,并为每个侦听的消息绑定一个处理函数。当收到服务器的消息后,随之会触发绑定的函数。

通过 socket 建立连接。

发送消息。

下我们我们就逐一地看它们是如何实现的吧!

获取 socket

在 iOS 中获取 socket 其实很简单,我们来看一下代码:

NSURL* url = [[NSURL alloc] initWithString:addr];
manager = [[SocketManager alloc] initWithSocketURL:url
                                            config:@{
                                            	@"log": @YES,
    											@"forcePolling":@YES,
                                                @"forceWebsockets":@YES
                                                }];
socket = manager.defaultSocket;

没错,通过这三行代码就可以了。至于为什么这么写我就不解释了,大家记下来就好了。这是 socket.io的固定格式。

注册侦听消息

使用 socket.io 注册一个侦听消息也非常容易,如下所示:

[socket on:@"joined" callback:^(NSArray * data, SocketAckEmitter * ack) {
    NSString* room = [data objectAtIndex:0];
    
    NSLog(@"joined room(%@)", room);
    
    [self.delegate joined:room];
    
}];

上面就是注册一个 joined 消息,并给它绑定一个匿名的处理函数。如果带来的消息还有参数的话,我们可以从 data 这个数组中获取到。

同样的道理,如果我们想注册一个新的侦听消息,可以按照上面的格式,只需将 joined 替换一下就可以了。

建立连接

这个就更简单了,下接上代码了:

[socket connect];

没错,只这一句连接就建好了哈!

发送消息

接下来,让我们看一下如何使用 socket.io 发送消息。

...
if(socket.status == SocketIOStatusConnected){
    [socket emit:@"join" with:@[room]];
}
...

socket.io 使用 emit 方法发送消息。它可以带一些参数,这些参数都被放在一个数据里。在上面的代码中,首先要判断socket是否已经处理连接状态,只有处于连接状态时,消息才能被真正发送出去。

以上就是 socket.io 的使用,是不是非常的简单?

创建 RTCPeerConnection

信令系统建立好后,后面的逻辑都是围绕着信令系统建立起来的。RTCPeerConnection 对象的建立也不例外。

在客户端,用户要想与远端通话,首先要发送 join 消息,也就是要先进入房间。此时,如果服务器判定用户是合法的,则会给客户端回 joined 消息。

客户端收到 joined 消息后,就要创建 RTCPeerConnection 了,也就是要建立一条与远端通话的音视频数据传输通道。

下面,我们就来看一下 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];
...

对于 iOS 的 RTCPeerConnection 对象有三个参数:

第一个,是 RTCConfiguration 类型的对象,该对象中最重要的一个字段是 iceservers。它里边存放了 stun/turn 服务器地址。其主要作用是用于NAT穿越。对于 NAT 穿越的知识大家可以自行学习。

第二个参数,是 RTCMediaConstraints 类型对象,也就是对 RTCPeerConnection 的限制。如,是否接收视频数据?是否接收音频数据?如果要与浏览器互通还要开启 DtlsSrtpKeyAgreement 选项。

第三个参数,是委托类型。相当于给 RTCPeerConnection 设置一个观察者。这样RTCPeerConnection 可以将一个状态/信息通过它通知给观察者。但它并不属于观察者模式,这一点大家一定要清楚。

RTCPeerConnection 对象创建好后,接下来我们介绍的是整个实时通话过程中,最重要的一部分知识,那就是 媒体协商。

媒体协商

首先,我们要知道媒体协商内容使用是 SDP 协议,不了解这部分知识的同学可以自行学习。其次,我们要清楚整体媒体协商的过程。

iOS 端的媒体协商过程与 Android/JS 端是一模一样的。还是下面这个经典的图:

A 与 B 进行通话,通话的发起方,首先要创建 Offer 类型的 SDP 内容。之后调用 RTCPeerConnection 对象的 setLocalDescription 方法,将 Offer 保存到本地。

紧接着,将 Offer 发送给服务器。然后,通过信令服务器中转到被呼叫方。被呼叫方收到 Offer 后,调用它的 RTCPeerConnection 对象的 setRemoteDescription 方法,将远端的 Offer 保存起来。

之后,被呼到方创建 Answer 类型的 SDP 内容,并调用 RTCPeerConnection 对象的 setLocalDescription 方法将它存储到本地。

同样的,它也要将 Answer 发送给服务器。服务器收到该消息后,不做任何处理,直接中转给呼叫方。呼叫方收到 Answer 后,调用 setRemoteDescription 将其保存起来。

通过上面的步骤,整个媒体协商部分就完成了。

下面我们就具体看看,在 iOS 端是如何实现这个逻辑的:

...
[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 类型的参数,该参数我们在前面创建 RTCPeerConnection 对象时介绍过,这里不再赘述。

另一个参数是一个匿名回调函数。可以通过对 error 是否为空来判定 offerForConstraints 方法有没有执行成功。如果执行成功了,参数 sdp 就是创建好的 SDP 内容。

如果成功获得了 sdp,按照之前的处理流程描述,我们首先要将它只存到本地;然后再将它发送给服务器,服务器中转给另一端。

我们的代码也是严格按照这个过程来的。在上面代码中 setLocalOffer 方法就是做这件事儿。具体代码如下:

...
[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设置了一个委托,在我们的项目中就是 CallViewController 对象。在该对象中我们实现了所有 RTCPeerConnection对象的代理方法。其中比较关键的有下面几个:

(void)peerConnection:(RTCPeerConnection *)peerConnection

didGenerateIceCandidate:(RTCIceCandidate *)candidate;该方法用于收集可用的 Candidate。

(void)peerConnection:(RTCPeerConnection *)peerConnection


didChangeIceConnectionState:(RTCIceConnectionState)newState;当 ICE 连接状态发生变化时会触发该方法

(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];
}
   
...

通过上面的代码,我们就可以将远端传来的视频展示出来了。

小结

以上我就将 iOS 端实现1对1实时通话的整体逻辑讲解完了。整体来看,其过程与 js/Android 端基本上是一模一样的。

在本文中,我通过对下面几个主题的介绍,向大家完整的讲解了 iOS 端该如何实现一个实时音视频通话程序:

权限申请

引入 WebRTC 库

采集并显示本地视频

信令驱动

创建音视频数据通道

媒体协商

渲染远端视频

对于一个熟悉 iOS 的开发者来说,通过本文的讲解,应该可以很快写出这样一个实时通话的程序。

相关推荐

激光手术矫正视力对眼睛到底有没有伤害?

因为大家询问到很多关于“基质不能完全愈合”的问题,有必要在这里再详细解释一下。谢谢@珍惜年少时光提出的疑问:因为手头刚好在看组织学,其中提到:”角膜基质约占角膜的全厚度的90%,主要成分是胶原板层,...

OneCode核心概念解析——View(视图)

什么是视图?在前面的章节中介绍过,Page相关的概念,Page是用户交互的入口,具有Url唯一性。但Page还只是一个抽象的容器,而View则是一个具备了具体业务能力的特殊的Page,它可以是一个...

精品博文图文详解Xilinx ISE14.7 安装教程

在软件安装之前,得准备好软件安装包,可从Xilinx官网上下载:http://china.xilinx.com/support/download/index.html/content/xilinx/z...

卡片项目管理(Web)(卡片设计的流程)

简洁的HTML文档卡片管理,简单框架个人本地离线使用。将个人工具类的文档整理使用。优化方向:添加图片、瀑布式布局、颜色修改、毛玻璃效果等。<!DOCTYPEhtml><html...

GolangWeb框架Iris项目实战-JWT和中间件(Middleware)的使用EP07

前文再续,上一回我们完成了用户的登录逻辑,将之前用户管理模块中添加的用户账号进行账号和密码的校验,过程中使用图形验证码强制进行人机交互,防止账号的密码被暴力破解。本回我们需要为登录成功的用户生成Tok...

sitemap 网站地图是什么格式?有什么好处?

sitemap网站地图方便搜索引擎发现和爬取网页站点地图是一种xml文件,或者是txt,是将网站的所有网址列在这个文件中,为了方便搜索引擎发现并收录的。sitemap网站地图分两种:用于用户导...

如何在HarmonyOS NEXT中处理页面间的数据传递?

大家好,前两天的Mate70的发布,让人热血沸腾啊,不想错过,自学的小伙伴一起啊,今天分享的学习笔记是关于页面间数据伟递的问题,在HarmonyOSNEXT5.0中,页面间的数据传递可以有很多种...

从 Element UI 源码的构建流程来看前端 UI 库设计

作者:前端森林转发链接:https://mp.weixin.qq.com/s/ziDMLDJcvx07aM6xoEyWHQ引言由于业务需要,近期团队要搞一套自己的UI组件库,框架方面还是Vue。而业界...

jq+ajax+bootstrap改了一个动态分页的表格

最近在维护一个很古老的项目,里面是用jq的dataTable方法实现一个分页的表格,不过这些表格的分页是本地分页。现在想要的是点击分页去请求数据。经过多次的修改,以失败告终。分页的不准确,还会有这个错...

学习ES6- 入门Vue(大量源代码及笔记,带你起飞)

ES6学习网站:https://es6.ruanyifeng.com/箭头函数普通函数//普通函数this指向调用时所在的对象(可变)letfn=functionfn(a,b){...

青锋微服务架构之-Ant Design Pro 基本配置

青锋(msxy)-Gitee.com1、更换AntDesignPro的logo和名称需要修改文件所在位置:/config/defaultSetting.jsconstproSett...

大数据调度服务监控平台(大数据调度服务监控平台官网)

简介SmartKettle是针对上述企业的痛点,对kettle的使用做了一些包装、优化,使其在web端也能具备基础的kettle作业、转换的配置、调度、监控,能在很大一定程度上协助企业完成不同...

Flask博客实战 - 实现博客首页视图及样式

本套教程是一个Flask实战类教程,html/css/javascript等相关技术栈不会过多的去详细解释,那么就需要各位初学者尽可能的先去掌握这些基础知识,当然本套教程不需要你对其非常精通,但最起码...

Web自动化测试:模拟鼠标操作(ActionChains)

在日常的测试中,经常会遇到需要鼠标去操作的一些事情,比如说悬浮菜单、拖动验证码等,这一节我们来学习如何使用webdriver模拟鼠标的操作首页模拟鼠标的操作要首先引入ActionChains的包fro...

DCS F-16C 中文指南 16.9ILS仪表降落系统教程

10–ILS教程我们的ILS(仪表着陆进近)将到达Batumi巴统机场。ILS频率:110.30跑道航向:120磁航向/126真航向无线电塔频率:131.0001.设置雷达高度表开关打开(前)并...