语音通话涉及到的概念介绍:
VoIP(Voice over Internet Protocol)即首先数字化语音信号并压缩成帧,转换为IP数据包在网络上传输,以此完成语音通话的业务,是一种利用IP协议传输语音数据的通信技术,VoIP通话中媒体流是走的UDP,一旦网络质量不好,语音的质量就会有延时或者断续,但是速度快。
Linphone是一款基于标准SIP的开源VoIP电话工具,是一款遵循GPL的开源的网络电话系统。它能够让你通过internet来查询朋友的IP,并通过IP给他打电话的软件,功能非常强大,既支持桌面系统,也支持移动终端,还能支持WEB浏览器。使用linphone,我们可以在互联网上随意的通信,通过语音、视频、即时文本消息。
Linphone基于ilbc的编解码;ilbc的编解码压缩比率还是比较大的,大概在1/10至1/9之间。也就是说假如每秒20kb的语音数据,编码后就2kb/s,非常小,非常利于网络传输。它使用了一个C库speex, 来实现回声消除模块。Linphone的最大优势在于全平台支持,android,ios,winphone,windows,linux,mac osx,web 全都支持
语音通话开发流程:
1)Cocoapods集成
Cocoapods 需要引入的开源三方库及版本:'linphone-sdk', ‘4.2’ 'CocoaAsyncSocket', '7.6.5'
linphone-sdk:用于实际的语音通话功能
CocoaAsyncSocket:用于与后台建立链接,分配网络座席和通话状态改变监听(开通多少个座席要注意,这会涉及到不菲的价格…)
2)后台的简单介绍
因为不是我做的后台,我只能做简要的介绍,我们后台使用的语言是C++,如果不想付出太高的成本,又有比较高的效率, C++无疑是很好的选择。在语音通话过程中,客户端与后台会进行三大类型的交互:
1.向主管账号信息的服务器发送请求交互,获取与第二个服务器进行第二类和第三类交互要使用的数据模型
2.根据第一类与服务器交互获取的数据模型,LinPhoneSDK与服务器进行第二类交互,建立UDP链接,用于语音通话
3.根据第一类与后台交互获取的数据模型,使用客户端基于TCP/IP协议的socket网络库GCDAsyncSocket与后台进行第三类交互,建立链接,保持长链接,用于获取网络坐席和更改通话状态
3)客户端语音通话的各状态搭建分析
1.登录
I. 向主管账号信息的服务器发送请求, 传入对应的URL,账号和密码,使用AES加密(key和偏移量与后台协商一致),获取接下来要使用的数据模型
//向主管账号信息的服务器发送请求,建立链接,获取接下来要用到的数据模型
YGCallManager *manager = [YGCallManager instance];
[manager initSdk:model success:^(NSDictionary * _Nullable responseObject) {
NSLog(@"initSdk:%@", responseObject);
//与socket服务器和LinPhone服务器建立链接
[[YGCallManager instance] login];
} failure:^(NSError * _Nullable error) {
NSLog(@"initSdk:%@", error);
}];
II. LinPhoneSDK与服务器建立第二类交互,建立UDP连接,为语音通话服务
ESSipManager *sipManager = [ESSipManager instance];
[sipManager login:@"你的LoginNum" password:@"你的pwd" displayName:@"" domain:@"你的sipIP:sipPort" port:@"你的sipPort" withTransport:@"UDP"];
III. 当Linphone登录成功,会调用成功的回调,这时socket与服务器建立第三类链接,为开通网络坐席和切换通话状态服务
sipManager.linphoneBlock = ^(NSInteger registrationState) {
if (self.linphoneRegistrationState != registrationState) {
self.linphoneRegistrationState = registrationState;
if (registrationState == 2) {
//2.socket连接
self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
NSError *socketError = nil;
if (![self.clientSocket connectToHost:self.tcpModel.TransferIP onPort:self.tcpModel.TransferPort withTimeout:-1 error:&socketError]) {
if (socketError) {
NSLog(@"连接服务器失败:%@", socketError.localizedDescription);
return;
}
}
这里说的切换通话状态服务,也就是说接通和挂断等需要让客户端知道,以做到相应的处理,客户端接受状态的变化主要是通过socket的代理来实现
主要功能代码:
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
//接收登录服务消息
if ([lastString containsString:@"LOGIN_SUCCEED"]) {
//向app端推送EventLogin成功消息
if (self.eventBlock) {
NSDictionary *dic = @{@"msg":@"登录成功"};
self.eventBlock(EVENTLogin, YES, dic);
}
//通知服务器当前账号的通道已经被占用
NSData *data4 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:[NSString stringWithFormat:@"与后台协商好的信令服务"]];
[self.clientSocket writeData:data4 withTimeout:-1 tag:4];
}
}
IV. 做登录功能遇到的问题: 在一开始实现登录的时候发现多次登录会经常报用户正忙,无法登录。后来与后台联调,发现单点登录功能(也就是这次登录会把之前的登录踢下去)还不完善,针对socket接口进行了优化升级.流程修改为: 发送准备状态给语音通话的服务器 => 发送初始化消息给分配座席服务器 => 发送清理通道消息给服务器 =》 发送登录消息给服务器
实现功能的部分代码(主要是发送与后台协商好的socket):
//登录
- (void)ygTcpLogin {
//发送准备状态给服务器
NSData *data = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"与后台约定好的信令服务"];
[self.clientSocket writeData:data withTimeout:-1 tag:0];
//发送初始化消息给转发服务器
NSData *data1 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"与后台约定好的信令服务"];
[self.clientSocket writeData:data1 withTimeout:-1 tag:1];
//发送清理通道消息给服务器
NSData *data2 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"与后台约定好的信令服务"];
[self.clientSocket writeData:data2 withTimeout:-1 tag:2];
//发送登录消息给服务器
NSData *data3 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"与后台约定好的信令服务"];
[self.clientSocket writeData:data3 withTimeout:-1 tag:3];
}
V. 做登陆功能遇到的问题2: iOS16以上的版本Linphone与语音通话后台无法建立链接,原因是端口存在被占用的可能。解决方式是每次链接时,给LinPhone分配未被占用的随机端口
实现的功能的部分代码:
[LinphoneManager.instance resetLinphoneCore];
LinphoneProxyConfig *config = linphone_core_create_proxy_config(LC);
LinphoneAddress *addr = linphone_address_new([NSString stringWithFormat:@"sip:%@@%@",username, domain].UTF8String);
Linp





