iOS下WebRTC音视频通话(二)-局域网内音视频通话
这里是iOS 下WebRTC音视频通话开发的第二篇,在这一篇会利用一个局域网内音视频通话的例子介绍WebRTC中常用的API。
如果你下载并编译完成之后,会看到一个iOS 版的WebRTC Demo。但是那个demo涉及到外网的通讯需要翻墙,而且还有对信令消息的封装理解起来非常的困难。
但是,我将要写的这个demo去掉了STUN服务器、TURN服务器配置,以及信令的包装,基本上是用WebRTC进行音视频通话的最精简主干了,非常容易理解。
准备
因为这个Demo用到了我之前写的另外两个工程:
一个XMPP聊天的Demo
音视频通话的UI效果视图
如果你对在本地搭建OpenFire服务以及开发一个基于XMPP的聊天小程序感兴趣
教程在这里:
XMPP系列(一):OpenFire环境搭建
XMPP系列(二)—-用户注册和用户登录功能
XMPP系列(三)—获取好友列表、添加好友
XMPP系列(四)—发送和接收文字消息,获取历史消息功能
XMPP系列(五)—文件传输
所以只需要下载上面两个工程,然后把一些控件合并下,然后配置好你的XMPP服务器的IP和端口号,就可以继续做音视频功能的开发了。
开始着手开发
首先我在聊天介绍导航栏上加了两个按钮【视频】【语音】(主要是太懒,不想在输入框做更多功能)。如下图:
然后为视频按钮添加点击事件,在这个点击事件里需要做几件事:
1、弹出一个拨打的界面。
2、播放拨打视频通话的声音。
3、做WebRTC的配置。
- (void)videoAction
{
NSLog(@"%s",__func__);
[self startCommunication:YES];
}
- (void)startCommunication:(BOOL)isVideo
{
WebRTCClient *client = [WebRTCClient sharedInstance];
[client startEngine];
client.myJID = [HLIMCenter sharedInstance].xmppStream.myJID.full;
client.remoteJID = self.chatJID.full;
[client showRTCViewByRemoteName:self.chatJID.full isVideo:isVideo isCaller:YES];
}
所有与WebRTC相关的操作都先封装在WebRTCClient内,然后再根据功能做拆分,后面你可以拆分到不同的类里。
下面开始介绍WebRTC的相关配置:
- (void)startEngine
{
//如果你需要安全一点,用到SSL验证,那就加上这句话。
[RTCPeerConnectionFactory initializeSSL];
//set RTCPeerConnection's constraints
self.peerConnectionFactory = [[RTCPeerConnectionFactory alloc] init];
NSArray *mandatoryConstraints = @[[[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"],
[[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"]
];
NSArray *optionalConstraints = @[[[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:@"false"]];
self.pcConstraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints optionalConstraints:optionalConstraints];
//set SDP's Constraints in order to (offer/answer)
NSArray *sdpMandatoryConstraints = @[[[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"],
[[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"]
];
self.sdpConstraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:sdpMandatoryConstraints optionalConstraints:nil];
//set RTCVideoSource's(localVideoSource) constraints
self.videoConstraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil optionalConstraints:nil];
}
上面的三个约束,只有pcConstraints是必须的,其他的都不是必须的。这些约束主要是控制音视频的采集,以及PeerConnection的设置。
其他RTC相关的配置是在显示拨打界面后做的操作:
- (void)showRTCViewByRemoteName:(NSString *)remoteName isVideo:(BOOL)isVideo isCaller:(BOOL)isCaller
{
// 1.显示视图
self.rtcView = [[RTCView alloc] initWithIsVideo:isVideo isCallee:!isCaller];
self.rtcView.nickName = remoteName;
self.rtcView.connectText = @"等待对方接听";
self.rtcView.netTipText = @"网络状况良好";
[self.rtcView show];
// 2.播放声音
NSURL *audioURL;
if (isCaller) {
audioURL = [[NSBundle mainBundle] URLForResource:@"AVChat_waitingForAnswer.mp3" withExtension:nil];
} else {
audioURL = [[NSBundle mainBundle] URLForResource:@"AVChat_incoming.mp3" withExtension:nil];
}
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:audioURL error:nil];
_audioPlayer.numberOfLoops = -1;
[_audioPlayer prepareToPlay];
[_audioPlayer play];
// 3.拨打时,禁止黑屏
[UIApplication sharedApplication].idleTimerDisabled = YES;
// 4.监听系统电话
[self listenSystemCall];
// 5.做RTC必要设置
if (isCaller) {
[self initRTCSetting];
// 如果是发起者,创建一个offer信令
[self.peerConnection createOfferWithDelegate:self constraints:self.sdpConstraints];
} else {
// 如果是接收者,就要处理信令信息,创建一个answer,但是设置和创建answer应该在点击接听后才开始
NSLog(@"如果是接收者,就要处理信令信息");
self.rtcView.connectText = isVideo ? @"视频通话":@"语音通话";
}
}
上面的注释已经很明白了。主要内容在[initRTCSetting]中。
* 1.已ICE服务器地址、pc约束、代理作为参数创建RTCPeerConnection对象。
self.peerConnection = [self.peerConnectionFactory peerConnectionWithICEServers:_ICEServers constraints:self.pcConstraints delegate:self];
- 2.创建本地多媒体流
RTCMediaStream *mediaStream = [self.peerConnectionFactory mediaStreamWithLabel:@"ARDAMS"];
- 3.为多媒体流添加音频轨迹
RTCAudioTrack *localAudioTrack = [self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"];
[mediaStream addAudioTrack:localAudioTrack];
音频的采集,已经封装在peerConnectionFactory工厂内。
* 4.为多媒体流添加视频轨迹
RTCAVFoundationVideoSource *source = [[RTCAVFoundationVideoSource alloc] initWithFactory:self.peerConnectionFactory constraints:self.videoConstraints];
RTCVideoTrack *localVideoTrack = [[RTCVideoTrack alloc] initWithFactory:self.peerConnectionFactory source:source trackId:@"AVAMSv0"];
[mediaStream addVideoTrack:localVideoTrack];
随着WebRTC的更新,API也被替换了很多,现在视频的采集多了一个新的类RTCAVFoundationVideoSource
* 5.为视频流添加渲染视图
RTCEAGLVideoView *localVideoView = [[RTCEAGLVideoView alloc] initWithFrame:self.rtcView.ownImageView.bounds];
localVideoView.transform = CGAffineTransformMakeScale(-1, 1);
localVideoView.delegate = self;
[self.rtcView.ownImageView addSubview:localVideoView];
self.localVideoView = localVideoView;
// 添加渲染视图
[self.localVideoTrack addRenderer:self.localVideoView];
RTCEAGLVideoView
也是新增的一个视图类,我们可以直接将这个视图添加到某个视图上。
* 6.将多媒体流绑定到peerConnection上
[self.peerConnection addStream:mediaStream];
至此发起方的RTC 设置完毕,只用在创建一个Offer,然后将Offer发送给对方。
* 7.创建Offer
[self.peerConnection createOfferWithDelegate:self constraints:self.sdpConstraints];
- 8.在createSession的回调里,为peerConnection设置localDescription,并发送信令给对方—(其实在setSession的代理方法中发送信令更合适,但是那样就得保存sdp,所以这里偷了个懒)。
- (void)peerConnection:(RTCPeerConnection *)peerConnection
didCreateSessionDescription:(RTCSessionDescription *)sdp
error:(NSError *)error
{
if (error) {
NSLog(@"创建SessionDescription 失败");
#warning 这里创建 创建SessionDescription 失败
} else {
NSLog(@"创建SessionDescription 成功");
// 这里将SessionDescription转换成H264的sdp,因为默认的是V8格式的视频。
RTCSessionDescription *sdpH264 = [self descriptionWithDescription:sdp videoFormat:@"H264"];
[self.peerConnection setLocalDescriptionWithDelegate:self sessionDescription:sdpH264];
NSDictionary *jsonDict = @{ @"type" : sdp.type, @"sdp" : sdp.description };
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:0 error:nil];
NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
// 将offer信令消息通过XMPP发送给对方
[[HLIMClient shareClient] sendSignalingMessage:jsonStr toUser:self.remoteJID];
}
}
9.等待对方返回Answer信令消息,当接听后,发送Answer信令消息回来后,将其设置为peerConnection的RemoteDescription即可。然后RTC在处理完成后就开始像对方发送多媒体流啦。
补充:
RTCPeerConnection有很多个回调,他们分别是在不同的时机触发
在为peerConnection添加RTCMediaStream之后就会触发下面这个代理方法:
- (void)peerConnectionOnRenegotiationNeeded:(RTCPeerConnection *)peerConnection
设置完LocalDescription之后,ICE框架才会开始去进行流数据传输,才会触发下面这几个方法
- (void)peerConnection:(RTCPeerConnection *)peerConnection
signalingStateChanged:(RTCSignalingState)stateChanged
- (void)peerConnection:(RTCPeerConnection *)peerConnection
iceGatheringChanged:(RTCICEGatheringState)newState
// 而局域网内,因为没有涉及到穿墙,这个代理方法并没有回调。
- (void)peerConnection:(RTCPeerConnection *)peerConnection
iceConnectionChanged:(RTCICEConnectionState)newState
其中各种不同的状态的枚举值含义,在这篇文中里有英文解释:中间部分有各种枚举值的解释
而搜索到ICECandidate之后,会回调:
- (void)peerConnection:(RTCPeerConnection *)peerConnection
gotICECandidate:(RTCICECandidate *)candidate
我们需要在上面这个回调中,将候选信息发送给对方,然后对方讲接收到的候选添加到peerConnection中。
完整代码:
- (void)peerConnection:(RTCPeerConnection *)peerConnection
gotICECandidate:(RTCICECandidate *)candidate
{
if (self.HaveSentCandidate) {
return;
}
NSLog(@"新的 Ice candidate 被发现.");
NSDictionary *jsonDict = @{@"type":@"candidate",
@"label":[NSNumber numberWithInteger:candidate.sdpMLineIndex],
@"id":candidate.sdpMid,
@"sdp":candidate.sdp
};
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:0 error:nil];
if (jsonData.length > 0) {
NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
[[HLIMClient shareClient] sendSignalingMessage:jsonStr toUser:self.remoteJID];
self.HaveSentCandidate = YES;
}
}
接收方
接收方在收到发起方通过XMPP发送过来的信令(可能会有Offer信令,Candidate信令,bye信令)后,先将其保存到数组中,同时展示音视频通话界面,并播放声音。
这里需要注意:要将收到的Offer信令消息插入到第一个,Offer信令消息必须先处理。
当点击接听按钮时,初始化RTC的设置,即上面的[initRTCSetting]方法。然后处理之前保存的信令消息。
- (void)acceptAction
{
[self.audioPlayer stop];
[self initRTCSetting];
for (NSDictionary *dict in self.messages) {
[self processMessageDict:dict];
}
[self.messages removeAllObjects];
}
如何处理之前的信令消息呢?
处理Offer信令消息:
将收到的Offer信令设置为peerConnection的RemoteDescription,并创建一个Answer信令发送给对方。
处理Candidate信令消息
将收到的信令消息包装成RTCICECandidate对象,然后添加到peerConnection上。
具体代码:
- (void)processMessageDict:(NSDictionary *)dict
{
NSString *type = dict[@"type"];
if ([type isEqualToString:@"offer"]) {
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:dict[@"sdp"]];
[self.peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:remoteSdp];
[self.peerConnection createAnswerWithDelegate:self constraints:self.sdpConstraints];
} else if ([type isEqualToString:@"answer"]) {
RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:dict[@"sdp"]];
[self.peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:remoteSdp];
} else if ([type isEqualToString:@"candidate"]) {
NSString *mid = [dict objectForKey:@"id"];
NSNumber *sdpLineIndex = [dict objectForKey:@"label"];
NSString *sdp = [dict objectForKey:@"sdp"];
RTCICECandidate *candidate = [[RTCICECandidate alloc] initWithMid:mid index:sdpLineIndex.intValue sdp:sdp];
[self.peerConnection addICECandidate:candidate];
} else if ([type isEqualToString:@"bye"]) {
if (self.rtcView) {
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
if (jsonStr.length > 0) {
[[HLIMClient shareClient] sendSignalingMessage:jsonStr toUser:self.remoteJID];
}
[self.rtcView dismiss];
[self cleanCache];
}
}
}
需要注意的是因为没有用到ICE穿墙,所以必须在同一个路由器下,否则可能无法进行点对点传输多媒体流。至此,局域网内音视频通话的小程序就完成了。
完整的Demo地址:局域网内WebRTC音视频通话
Demo中用到的WebRTC静态库已放到:百度网盘
iOS下WebRTC音视频通话(二)-局域网内音视频通话的更多相关文章
- iOS下WebRTC音视频通话(一)
在iOS下做IM功能时,难免都会涉及到音频通话和视频通话.QQ中的QQ电话和视频通话效果就非常好,但是如果你没有非常深厚的技术,也没有那么大的团队,很难做到QQ那么快速和稳定的通话效果. 但是利用We ...
- iOS下WebRTC音视频通话(三)-音视频通话
前两篇文章记录了音视频通话的一些概念和一些流程,以及一个局域网内音视频通话的示例. 今天以一个伪真实网络间的音视频通话示例,来分析WebRTC音视频通话的过程. 上一篇因为是在相同路由内,所以不需要穿 ...
- 将vue项目部署在Linux的Nginx下,并设置为局域网内访问
1. 下载 wget http://nginx.org/download/nginx-1.12.2.tar.gz 2. 解压缩 tar -zxvf linux-nginx-1.12.2.tar.gz ...
- iOS下单例模式实现(二)利用宏定义快速实现
在上一节里提到了用利用gcd快速实现单例模式. 一个项目里面可能有好几个类都需要实现单例模式.为了更高效的编码,可以利用c语言中宏定义来实现. 新建一个Singleton.h的头文件. // @int ...
- Windows下查看局域网内某台计算机的MAC地址
我们知道在局域网中,在Windows下,查看局域网内其他主机的ip和对应mac地址的命令是: arp -a 这样可以看到ip和mac地址的对应关系,还是比较方便的 但是有些时候使用arp命令并不能列出 ...
- spa(单页应用)中,使用history模式时,微信长按识别二维码在ios下失效的问题
spa(单页应用,vue)中,使用history模式时,微信长按识别二维码在ios下失效的问题. 触发条件: spa单页应用: 路由模式 history 从其他页面跳转到带有微信二维码识别的页面(不是 ...
- Linux 下查看局域网内所有主机IP和MAC
linux环境下,执行namp对局域网扫描一遍,然后查看arp缓存表就可以知道局域内ip对应的mac.namp比较强大也可以直接扫描mac地址和端口,执行扫描之后就可以在/proc/net/arp查看 ...
- 内嵌iframe页面在IOS下会受内部元素影响自动撑开的问题
IOS下的webview页面,内嵌iframe元素,将其样式指定为宽高100%: .iframe { width: %; height: %; } 在安卓下运行均无问题,但是在IOS下会出现异常. 具 ...
- Win10系统下怎么让局域网内其他电脑通过IP访问网站
最近,有位win10系统用户在电脑上制作好网站后,希望能让局域网内的其他电脑通过IP直接访问自己电脑的网站,以便得到更好地测试效果.可是,该用户操作了很久都没成功.那么,我们如何配置win10电脑的I ...
随机推荐
- [BZOJ]1095 Hide捉迷藏(ZJOI2007)
一道神题,两种神做法. Description 捉迷藏 Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子.某天,Jiajia.Wind和孩子们决定在家里玩捉迷藏游戏.他们的家很大且构造很奇特 ...
- java的数据结构
常见的数据结构 线性表(list) 1.有序列表,就像小朋友排队(一队)放学出校门,插入的顺序作为遍历的顺序,位置不变(长度固定) 2.顺序存储:从起始位置开始依次向后存储,查询方便,但是插入(排队 ...
- mysql 使用问题?
linux中安装了mysql客户端和服务器端,为什么无法使用,总是报错呢 解决办法:使用dpkg -r mysql命令删除掉mysql-client和mysql-server了,还是不行,而且查看软件 ...
- 垃圾回收机制(GC)
垃圾收集器(GC)与内存分配策略 GC需要完成的三件事: 判断哪些内存需要回收 什么时候回收 如何回收 在java内存运行时区域的各个部分中,程序计数器.虚拟机栈.本地方法栈3个区域随线程而生,随线程 ...
- SQL语句删除字段,改变字段长度
1.改变字段长度 ALTER TABLE T_MSG_SEND_R_ACC MODIFY reply_content VARCHAR(512); 2.删除字段ALTER TABLE MSG_TX_BA ...
- TCP/UDP的区别
TCP与UDP区别 TCP提供的是面向连接的.可靠的数据流传输: UDP提供的是非面向连接的.不可靠的数据流传输. TCP提供可靠的服务,通过TCP连接传送的数据,无差错.不丢失,不重复,按序到达:U ...
- 分布式锁的几种使用方式(redis、zookeeper、数据库)
Q:一个业务服务器,一个数据库,操作:查询用户当前余额,扣除当前余额的3%作为手续费 synchronized lock db lock Q:两个业务服务器,一个数据库,操作:查询用户当前余额,扣除当 ...
- Spring boot 整合 Mybatis + Thymeleaf开发web(二)
上一章我把整个后台的搭建和逻辑给写出来了,也贴的相应的代码,这章节就来看看怎么使用Thymeleaf模板引擎吧,Spring Boot默认推荐Thymeleaf模板,之前是用jsp来作为视图层的渲染, ...
- 服务器&阵列卡&组raid 5
清除raid信息后,机器将会读不到系统, 后面若进一步操作处理, raid信息有可能会被初始化掉,那么硬盘数据就有可能会被清空, 导致数据丢失, 否则如果只是清除raid信息,重做raid是可以还原系 ...
- 排序算法的C语言实现(上 比较类排序:插入排序、快速排序与归并排序)
总述:排序是指将元素集合按规定的顺序排列.通常有两种排序方法:升序排列和降序排列.例如,如整数集{6,8,9,5}进行升序排列,结果为{5,6,8,9},对其进行降序排列结果为{9,8,6,5}.虽然 ...