iOS 端实现1对1音视频实时通话

作者 : 开心源码 本文共10044个字,预计阅读时间需要26分钟 发布时间: 2022-05-12 共126人阅读

前言

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

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

  • 权限申请
  • 引入 WebRTC 库
  • 采集并显示本地视频
  • 信令驱动
  • 创立音视频数据通道
  • 媒体协商
  • 渲染远端视频

通过上面几个小节,全面详情如何在iOS端实现1对1实时通话。

申请权限

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

  • 打开项目,点击左侧目录中的项目。
  • 在左侧目录找到 info.plist,并将其打开。
  • 点击 右侧 看到 “+” 号的地方。
  • 增加 Camera 和 Microphone 访问权限。

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

iOS申请权限

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

引入WebRTC库

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

  • 第一种,是通过 WebRTC 源码编译出 WebRTC 库,而后在项目中手动引入它;
  • 第二种方式,是 WebRTC 官方会定期发布编译好的 WebRTC 库,我们可以使用 Pod 方式进行安装。

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

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

Podfile 文件的具体格式如下:

source ' CocoaPods/Specs.git'  platform :ios,'11.0'target 'WebRTC4iOS2' dopod 'GoogleWebRTC'end
  • source,指定了库文件从哪里下载
  • platform,指定了使用的平台及平台版本
  • target,指定项目的名子
  • pod,指定要安装的库

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

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

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

获取本地视频

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

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

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

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

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

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

  • 一是表明它是一个视频。当我们要展现视频的时候,就从这里获取数据;
  • 另一方面,它也是一个终点。即,当我们从视频设施采集到视频数据时,要交给它暂存起来。

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

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

在该代码中,首先将 RTCVideoSourceRTCCameraVideoCapture 进行绑定,而后再开启设施,这样视频数据就源源不断的被采集到 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 ' CocoaPods/Specs.git'  platform :ios,'11.0'use_frameworks!target 'WebRTC4iOS2' dopod '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 穿越的知识大家可以看 《WebRTC实时互动直播技术入门与实战》 ,这门课里对其原理做了说细阐述。
  • 第二个参数,是 RTCMediaConstraints 类型对象,也就是对 RTCPeerConnection 的限制。如,能否接收视频数据?能否接收音频数据?假如要与浏览器互通还要开启 DtlsSrtpKeyAgreement 选项。
  • 第三个参数,是委拖类型。相当于给 RTCPeerConnection 设置一个观察者。这样RTCPeerConnection 可以将一个状态/信息通过它通知给观察者。但它并不属于观察者模式,这一点大家肯定要清楚。

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

媒体协商

首先,我们要知道媒体协商内容使用是 SDP 协议,不理解这部分知识的同学可以看 《WebRTC实时互动直播技术入门与实战》 这门课,在门课里对其做了详细讲解。

其次,我们要清楚整体媒体协商的过程。

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<RTCMediaStream *> *)mediaStreams;该方法在侦听到远端 track 时会触发。

那么,什么时候开始渲染远端视频呢?当有远端视频流过来的时候,就会触发 (void)peerConnection:(RTCPeerConnection *)peerConnection
didAddReceiver:(RTCRtpReceiver *)rtpReceiver
streams:(NSArray<RTCMediaStream *> *)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 的开发者来说,通过本文的讲解,应该可以很快写出这样一个实时通话的程序。

谢谢!

参考资料

《WebRTC实时互动直播技术入门与实战》

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » iOS 端实现1对1音视频实时通话

发表回复