当前位置:   article > 正文

iOS 基于LinPhone的语音通话_ios. 与服务器语音通话

ios. 与服务器语音通话

语音通话涉及到的概念介绍:

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和偏移量与后台协商一致),获取接下来要使用的数据模型

  1. //向主管账号信息的服务器发送请求,建立链接,获取接下来要用到的数据模型
  2. YGCallManager *manager = [YGCallManager instance];
  3. [manager initSdk:model success:^(NSDictionary * _Nullable responseObject) {
  4. NSLog(@"initSdk:%@", responseObject);
  5. //与socket服务器和LinPhone服务器建立链接
  6. [[YGCallManager instance] login];
  7. } failure:^(NSError * _Nullable error) {
  8. NSLog(@"initSdk:%@", error);
  9. }];

II. LinPhoneSDK与服务器建立第二类交互,建立UDP连接,为语音通话服务

  1. ESSipManager *sipManager = [ESSipManager instance];
  2. [sipManager login:@"你的LoginNum" password:@"你的pwd" displayName:@"" domain:@"你的sipIP:sipPort" port:@"你的sipPort" withTransport:@"UDP"];

III. 当Linphone登录成功,会调用成功的回调,这时socket与服务器建立第三类链接,为开通网络坐席和切换通话状态服务

  1. sipManager.linphoneBlock = ^(NSInteger registrationState) {
  2. if (self.linphoneRegistrationState != registrationState) {
  3. self.linphoneRegistrationState = registrationState;
  4. if (registrationState == 2) {
  5. //2.socket连接
  6. self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
  7. NSError *socketError = nil;
  8. if (![self.clientSocket connectToHost:self.tcpModel.TransferIP onPort:self.tcpModel.TransferPort withTimeout:-1 error:&socketError]) {
  9. if (socketError) {
  10. NSLog(@"连接服务器失败:%@", socketError.localizedDescription);
  11. return;
  12. }
  13. }

这里说的切换通话状态服务,也就是说接通和挂断等需要让客户端知道,以做到相应的处理,客户端接受状态的变化主要是通过socket的代理来实现

主要功能代码:

  1. - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
  2. //接收登录服务消息
  3. if ([lastString containsString:@"LOGIN_SUCCEED"]) {
  4. //向app端推送EventLogin成功消息
  5. if (self.eventBlock) {
  6. NSDictionary *dic = @{@"msg":@"登录成功"};
  7. self.eventBlock(EVENTLogin, YES, dic);
  8. }
  9. //通知服务器当前账号的通道已经被占用
  10. NSData *data4 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:[NSString stringWithFormat:@"与后台协商好的信令服务"]];
  11. [self.clientSocket writeData:data4 withTimeout:-1 tag:4];
  12. }
  13. }

IV. 做登录功能遇到的问题: 在一开始实现登录的时候发现多次登录会经常报用户正忙,无法登录。后来与后台联调,发现单点登录功能(也就是这次登录会把之前的登录踢下去)还不完善,针对socket接口进行了优化升级.流程修改为: 发送准备状态给语音通话的服务器 => 发送初始化消息给分配座席服务器 => 发送清理通道消息给服务器 =》 发送登录消息给服务器

实现功能的部分代码(主要是发送与后台协商好的socket):

  1. //登录
  2. - (void)ygTcpLogin {
  3. //发送准备状态给服务器
  4. NSData *data = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"与后台约定好的信令服务"];
  5. [self.clientSocket writeData:data withTimeout:-1 tag:0];
  6. //发送初始化消息给转发服务器
  7. NSData *data1 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"与后台约定好的信令服务"];
  8. [self.clientSocket writeData:data1 withTimeout:-1 tag:1];
  9. //发送清理通道消息给服务器
  10. NSData *data2 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"与后台约定好的信令服务"];
  11. [self.clientSocket writeData:data2 withTimeout:-1 tag:2];
  12. //发送登录消息给服务器
  13. NSData *data3 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"与后台约定好的信令服务"];
  14. [self.clientSocket writeData:data3 withTimeout:-1 tag:3];
  15. }

V.  做登陆功能遇到的问题2: iOS16以上的版本Linphone与语音通话后台无法建立链接,原因是端口存在被占用的可能。解决方式是每次链接时,给LinPhone分配未被占用的随机端口

实现的功能的部分代码:

  1. [LinphoneManager.instance resetLinphoneCore];
  2. LinphoneProxyConfig *config = linphone_core_create_proxy_config(LC);
  3. LinphoneAddress *addr = linphone_address_new([NSString stringWithFormat:@"sip:%@@%@",username, domain].UTF8String);
  4. LinphoneAddress *tmpAddr = linphone_address_new([NSString stringWithFormat:@"sip:%@",domain].UTF8String);
  5. linphone_address_set_username(addr, username.UTF8String);
  6. linphone_address_set_port(addr, linphone_address_get_port(tmpAddr));
  7. linphone_address_set_domain(addr, linphone_address_get_domain(tmpAddr));
  8. if (displayName && ![displayName isEqualToString:@""]) {
  9. linphone_address_set_display_name(addr, displayName.UTF8String);
  10. }
  11. linphone_proxy_config_set_identity_address(config, addr);
  12. if (transport) {
  13. linphone_proxy_config_set_route(
  14. config,
  15. [NSString stringWithFormat:@"%s;transport=%s", domain.UTF8String, transport.lowercaseString.UTF8String]
  16. .UTF8String);
  17. linphone_proxy_config_set_server_addr(
  18. config,
  19. [NSString stringWithFormat:@"%s;transport=%s", domain.UTF8String, transport.lowercaseString.UTF8String]
  20. .UTF8String);
  21. }
  22. linphone_proxy_config_enable_publish(config, FALSE);
  23. linphone_proxy_config_enable_register(config, TRUE);
  24. LinphoneAuthInfo *info =
  25. linphone_auth_info_new(linphone_address_get_username(addr), // username
  26. NULL, // user id
  27. password.UTF8String, // passwd
  28. NULL, // ha1
  29. linphone_address_get_domain(addr), // realm - assumed to be domain
  30. linphone_address_get_domain(addr) // domain
  31. );
  32. linphone_core_add_auth_info(LC, info);
  33. linphone_address_unref(addr);
  34. linphone_address_unref(tmpAddr);
  35. //分配随机端口
  36. LCSipTransports transportValue = {-1,-1,-1,-1};
  37. if (linphone_core_set_sip_transports(LC, &transportValue)) {
  38. NSLog(@"cannot set transport");
  39. }

2.外呼

I. 外呼功能流程:客户端拨打 => 通知服务器将当前账号置于繁忙状态并分配座席 => 客户端收到转座席是否成功的socket回调并做相应的处理, 同时服务器(语音后台)发送虚拟号码和被呼叫的号码等相关信息给Lin Phone,通知Linphone拨打网络电话 

实现外呼功能的部分代码(客户端主要是把自己和被呼叫者的信息发给后台,LinPhone拨打电话实际是由后台触发的):

  1. //当前账号置为繁忙
  2. NSData *data8 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"繁忙信令"];
  3. [self.clientSocket writeData:data8 withTimeout:-1 tag:8];
  4. //分配座席,并建立语音通话链接
  5. NSData *data9 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"座席建立链接信令"];
  6. [self.clientSocket writeData:data9 withTimeout:-1 tag:9];

II.外呼动作完成后,就需要等待被呼叫者,看其是否接听,这就需要监听通话状态的改变,这里是由LinPhone提供的状态变化的宏来实现的,我们需要对这个后果进行通知监听,一旦监听到状态变化,调用LinPhone相关的API来进行接听或挂断。

实现功能的部分代码:

  1. //监听通话状态变化
  2. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onCallUpdate:) name:kLinphoneCallUpdate object:nil];
  3. - (void) onCallUpdate: (NSNotification*) notification {
  4. NSDictionary* userInfo = [notification userInfo];
  5. NSValue* c = [userInfo valueForKey:@"call"];
  6. // int state = (int)[userInfo valueForKey:@"state"];
  7. LinphoneCallState state = [[userInfo objectForKey:@"state"] intValue];
  8. NSString* message = [userInfo valueForKey:@"message"];
  9. NSLog(@"========== state: %d, message: %@", state, message);
  10. LinphoneCall* call = c.pointerValue;
  11. NSDictionary *dict = @{@"call" : [NSValue valueWithPointer:call],
  12. @"state" : [NSNumber numberWithInt:state],
  13. @"message" : message};
  14. switch (state) {
  15. //接听
  16. case LinphoneCallIncomingReceived: {
  17. [NSNotificationCenter.defaultCenter postNotificationName:ES_ON_CALL_COMMING object: self userInfo:dict];
  18. if (self.callBlock) {
  19. self.callBlock((NSInteger)state, dict);
  20. }
  21. break;
  22. }
  23. case LinphoneCallOutgoingInit:
  24. case LinphoneCallConnected:
  25. case LinphoneCallStreamsRunning: {
  26. // check video
  27. if (![self isVideoEnabled:call]) {
  28. const LinphoneCallParams *param = linphone_call_get_current_params(call);
  29. const LinphoneCallAppData *callAppData =
  30. (__bridge const LinphoneCallAppData *)(linphone_call_get_user_data(call));
  31. if (state == LinphoneCallStreamsRunning && callAppData->videoRequested &&
  32. linphone_call_params_low_bandwidth_enabled(param)) {
  33. // too bad video was not enabled because low bandwidth
  34. NSLog(@"带宽太低,无法开启视频通话");
  35. callAppData->videoRequested = FALSE; /*reset field*/
  36. }
  37. }
  38. [NSNotificationCenter.defaultCenter postNotificationName:ES_ON_CALL_STREAM_UPDATE object:self userInfo:dict];
  39. break;
  40. }
  41. case LinphoneCallUpdatedByRemote: {
  42. const LinphoneCallParams *current = linphone_call_get_current_params(call);
  43. const LinphoneCallParams *remote = linphone_call_get_remote_params(call);
  44. /* remote wants to add video */
  45. if ((linphone_core_video_display_enabled([LinphoneManager getLc]) && !linphone_call_params_video_enabled(current) &&
  46. linphone_call_params_video_enabled(remote)) &&
  47. (!linphone_core_get_video_policy([LinphoneManager getLc])->automatically_accept ||
  48. (([UIApplication sharedApplication].applicationState != UIApplicationStateActive) &&
  49. floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max))) {
  50. linphone_core_defer_call_update([LinphoneManager getLc], call);
  51. [NSNotificationCenter.defaultCenter postNotificationName:ES_ON_REMOTE_OPEN_CEMERA object: self userInfo:dict];
  52. // [self allowToOpenCameraByRemote:call];
  53. } else if (linphone_call_params_video_enabled(current) && !linphone_call_params_video_enabled(remote)) {
  54. }
  55. break;
  56. }
  57. case LinphoneCallUpdating:
  58. break;
  59. case LinphoneCallPausing:
  60. case LinphoneCallPaused:
  61. break;
  62. case LinphoneCallPausedByRemote:
  63. break;
  64. //挂断
  65. case LinphoneCallEnd: {//LinphoneCallEnd
  66. [NSNotificationCenter.defaultCenter postNotificationName:ES_ON_CALL_END object: self userInfo:NULL];
  67. if (self.callBlock) {
  68. self.callBlock((NSInteger)state, dict);
  69. }
  70. break;
  71. }
  72. case LinphoneCallReleased: {
  73. if (self.callBlock) {
  74. self.callBlock((NSInteger)state, dict);
  75. }
  76. break;
  77. }
  78. case LinphoneCallError:
  79. default:
  80. break;
  81. }
  82. }

当触发callBlock后,就会调用LinPhone相应的接听的方法:

  1. - (void)acceptCall:(LinphoneCall *)call evenWithVideo:(BOOL)video {
  2. LinphoneCallParams *lcallParams = linphone_core_create_call_params(theLinphoneCore, call);
  3. if (!lcallParams) {
  4. LOGW(@"Could not create call parameters for %p, call has probably already ended.", call);
  5. return;
  6. }
  7. if ([self lpConfigBoolForKey:@"edge_opt_preference"]) {
  8. bool low_bandwidth = self.network == network_2g;
  9. if (low_bandwidth) {
  10. LOGI(@"Low bandwidth mode");
  11. }
  12. linphone_call_params_enable_low_bandwidth(lcallParams, low_bandwidth);
  13. }
  14. linphone_call_params_enable_video(lcallParams, video);
  15. linphone_call_accept_with_params(call, lcallParams);
  16. linphone_call_params_unref(lcallParams);
  17. }

此时,一个呼叫-接听的流程就已经建立,接下来就要说到挂断了,因为有接听,必然就有挂断

3.挂断

挂断分为两类,一类是己方主动挂断,另一类是对方挂断,这是两种不同的处理方式

I. 己方挂断

流程: 己方点击挂断 => 通知后台己方挂断,即将执行挂断流程 => 通知LinPhone执行挂断 => LinPhone执行挂断后通知语音服务器执行挂断处理 => 语音服务器通知与socket进行长链接的服务器进行挂断处理 => 与socket进行长链接的服务器会给客户端回调,设置完全通话结束socket信令给socket服务器,并设置己方处于空闲状态 => 语音服务器会给Linphone发送通话完全结束,Linphone清理本次通话数据并通知客户端,进行本地通话数据清理

开始通知后台执行己方挂断,并且通知Linphone结束通话的部分代码:

  1. NSData *data20 = [self sendMsgWithName:self.tcpModel.PhoneNo type:@"" targetType:@"" msg:[NSString stringWithFormat:@"开始结束通话信令服务"];
  2. LinphoneCore* lc = [LinphoneManager getLc];
  3. LinphoneCall* currentcall = linphone_core_get_current_call(lc);
  4. if (linphone_core_is_in_conference(lc) || // In conference
  5. (linphone_core_get_conference_size(lc) > 0) // Only one conf
  6. ) {
  7. linphone_core_terminate_conference(lc);
  8. } else if(currentcall != NULL) { // In a call
  9. // linphone_core_terminate_call(lc, currentcall);
  10. linphone_call_terminate(currentcall);
  11. } else {
  12. const MSList* calls = linphone_core_get_calls(lc);
  13. if (ms_list_size(calls) == 1) { // Only one call
  14. // linphone_core_terminate_call(lc,(LinphoneCall*)(calls->data));
  15. linphone_call_terminate((LinphoneCall *)(calls->data));
  16. }
  17. }

后台收到结束通话信令,并且通知客户端传送结束的socket信息,客户端向后台发送己方置为空闲的部分代码:

  1. //设置话后
  2. NSData *data11 = [self sendMsgWithName:@"" type:@"" targetType:@"FlyCcs" msg:[NSString stringWithFormat:@"话后信令"]];
  3. [self.clientSocket writeData:data11 withTimeout:-1 tag:11];
  4. //设置话后提交
  5. NSData *data12 = [self sendMsgWithName:@"" type:@"" targetType:@"FlyCcs" msg:[NSString stringWithFormat:@"话后提交信令"]];
  6. [self.clientSocket writeData:data12 withTimeout:-1 tag:12];
  7. //空闲
  8. NSData *data19 = [self sendMsgWithName:self.tcpModel.PhoneNo type:@"FlyCcs" targetType:@"FlyCcs" msg:[NSString stringWithFormat:@"空闲信令"]];
  9. [self.clientSocket writeData:data19 withTimeout:-1 tag:19];

LinPhone发送彻底消除通话的通知,客户端执行本地通话的数据清理的部分代码:

  1. case LinphoneCallReleased: {
  2. if (self.callBlock) {
  3. self.callBlock((NSInteger)state, dict);
  4. }
  5. break;
  6. }
  7. sipManager.callBlock = ^(NSInteger callState, NSDictionary *dict) {
  8. if (self->_ISDIALOUT == 0) {
  9. return;
  10. }
  11. if (callState == 1) {
  12. NSLog(@"接听--callState == 1");
  13. LinphoneCall *call = [dict[@"call"] pointerValue];
  14. [weakManager acceptCall:(ESCall *)call];
  15. } else if (callState == 18) {
  16. NSLog(@"挂断--callState == 18");
  17. //向app端推送EventHangup
  18. // if (self.eventBlock) {
  19. // NSDictionary *dic = @{@"CallId":@"",
  20. // @"CallTime":@"",
  21. // @"CodeCause":@"",
  22. // @"ConnectTime":@"",
  23. // @"EndTime":@"",
  24. // @"Rebark":self.Rebark ? self.Rebark : @"",
  25. // @"SignalIng":@"",
  26. // @"WavFile":self.WavFile ? self.WavFile : @""
  27. // };
  28. // self.eventBlock(EVENTHangup, YES, dic);
  29. // }
  30. //清空本地数据
  31. self->_callModel = nil;
  32. self->_CallId = nil;
  33. self->_ConnectTime = nil;
  34. self->_Rebark = nil;
  35. self->_WavFile = nil;
  36. self->_ISDIALOUT = 10;
  37. }

II. 对方挂断 

流程;对方挂断 => LinPhone通知客户端挂断 => LinPhone通知服务器通话挂断 => 服务器通知客户端发动结束通话,置为空闲的信令 => 服务器通知LinPhone消除本次通话数据 => LinPhone通知客户端消除本次通话数据

实现功能的关键代码与己方挂断代码是相同的,所以就不列举了

4.来电

流程: LinPhone收到来电通知 => 通知服务器和客户端回调收到来电 => 服务器通过socket代理通知客户端来电和相关来电消息 => 客户端发送繁忙信令和转发分配座席信令 =>通话信道建立完毕

实现功能的部分代码(与上面重复的部分省略):

  1. //繁忙信令
  2. NSData *data13 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:[NSString stringWithFormat:@"当前账号置为繁忙的信令"]]; //与data10一样;
  3. [self.clientSocket writeData:data13 withTimeout:-1 tag:13];
  4. //分配座席信令
  5. NSData *data14 = [self sendMsgWithName:@"" type:@"" targetType:@"FlyCn" msg:[NSString stringWithFormat:@"转发分配座席的信令"]; //与data10一样
  6. [self.clientSocket writeData:data14 withTimeout:-1 tag:14];

4.接听

 实现主要功能的部分代码:

  1. LinphoneCallParams *lcallParams = linphone_core_create_call_params(theLinphoneCore, call);
  2. if (!lcallParams) {
  3. LOGW(@"Could not create call parameters for %p, call has probably already ended.", call);
  4. return;
  5. }
  6. if ([self lpConfigBoolForKey:@"edge_opt_preference"]) {
  7. bool low_bandwidth = self.network == network_2g;
  8. if (low_bandwidth) {
  9. LOGI(@"Low bandwidth mode");
  10. }
  11. linphone_call_params_enable_low_bandwidth(lcallParams, low_bandwidth);
  12. }
  13. linphone_call_params_enable_video(lcallParams, video);
  14. linphone_call_accept_with_params(call, lcallParams);
  15. linphone_call_params_unref(lcallParams);

听筒/扬声器切换 静音切换 保持通话

还有就是一些比较细小的功能,比如听筒/扬声器切换,静音切换

  1. //听筒/扬声器切换
  2. - (void)speakerToggle {
  3. //true:开启扬声器; false:关闭扬声器
  4. [LinphoneManager.instance setSpeakerEnabled:self->speaker];
  5. self->speaker = !self->speaker;
  6. }
  7. //静音切换
  8. - (void)muteToggle {
  9. //true:开启静音; false:关闭静音
  10. linphone_core_enable_mic(LC, self->mute);
  11. self->mute = !self->mute;
  12. }

还有就是先忙其他事,保持当前通话,对方听不到声音:

流程:客户端向服务器发送保持当前通话信令 => 客户端向服务器发送保持繁忙状态信令 => 语音服务区通知LinPhone进入保持当前通话状态

  1. //保持
  2. - (void)holdCall {
  3. NSData *data30 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:[NSString stringWithFormat:@"保持通话信令"];
  4. [self.clientSocket writeData:data30 withTimeout:-1 tag:30];
  5. NSData *data31 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:[NSString stringWithFormat:@"保持繁忙状态信令"];
  6. [self.clientSocket writeData:data31 withTimeout:-1 tag:31];
  7. //向app端推送状态事件EventAgentState
  8. if (self.eventBlock) {
  9. }
  10. self.flag = ([self.flag isEqualToString:@"HOLDCALL"]) ? @"REHOLDCALL" : @"HOLDCALL";
  11. }

语音通话后台保活的实现

 通话过程中的保活已经由LinPhone底层实现,但是没有通话还处于后台模式,怎么保活来保证收到来电和播放响铃呢,这里的响铃不是微信里的通知短暂响铃,而是长时间的响铃,我这边的实现方式主要是基于播放无声音乐

I. LinPhone给客户端状态回调,通知来电,在来电的回调里调用播放长时间响铃 ,挂断电话时,在回调里通知挂断,然后结束播放铃声

  1. manager.eventBlock = ^(EVENT event, BOOL result, NSDictionary * _Nonnull resultMsg) {
  2. switch (event) {
  3. case EVENTAgentState: {
  4. //状态变化事件,下一行注释均为回调具体参数
  5. [[NSNotificationCenter defaultCenter] postNotificationName:kEVENTAgentState object:nil userInfo:resultMsg];
  6. }
  7. break;
  8. case EVENTLogin: {
  9. //登录成功事件
  10. if (result) {
  11. ViewController *vc = [[ViewController alloc] init];
  12. [self.navigationController pushViewController:vc animated:YES];
  13. } else {
  14. sender.enabled = YES;
  15. [self alertWithMessage:@"登录失败,请重新登录"];
  16. }
  17. }
  18. break;
  19. case EVENTQuitLogin: {
  20. //退出登录或工号异处登录事件
  21. [[NSNotificationCenter defaultCenter] postNotificationName:kEVENTQuitLogin object:nil userInfo:resultMsg];
  22. }
  23. break;
  24. case EVENTMakeCall: {
  25. //外呼事件
  26. }
  27. break;
  28. case EVENTComeCall: {
  29. //来电事件
  30. [[NSNotificationCenter defaultCenter] postNotificationName:kComeCallUpdate object:nil userInfo:resultMsg];
  31. AVAudioSession *audioSession = [AVAudioSession sharedInstance];
  32. // 设置多声道播放
  33. NSError *error = nil;
  34. [audioSession setCategory:AVAudioSessionCategoryMultiRoute withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
  35. [[ZSAVPlayerInstance sharedInstance] start];
  36. }
  37. break;
  38. case EVENTCalling: {
  39. //通话中事件
  40. [[ZSAVPlayerInstance sharedInstance] stop];
  41. AVAudioSession *audioSession = [AVAudioSession sharedInstance];
  42. // 设置后台播放
  43. NSError *error = nil;
  44. [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
  45. }
  46. break;
  47. case EVENTHangup: {
  48. //挂机事件
  49. [[NSNotificationCenter defaultCenter] postNotificationName:kEVENTHangup object:nil userInfo:resultMsg];
  50. [[ZSAVPlayerInstance sharedInstance] stop];
  51. AVAudioSession *audioSession = [AVAudioSession sharedInstance];
  52. // 设置后台播放
  53. NSError *error = nil;
  54. [audioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
  55. }
  56. break;
  57. default:
  58. break;
  59. }
  60. [resultMsg enumerateKeysAndObjectsUsingBlock:^(id key,id obj, BOOL *stop) {
  61. NSLog(@"event:%d key:%@ value:%@", (int)event, key, obj);
  62. }];
  63. };

II.问题:

在这个过程中,遇到的问题是后台保活音乐和铃声播放存在冲突,后台保活音乐要设置AVAudioSession为AVAudioSessionCategoryPlayback后台播放模式,铃声在这种模式下无法播放,播放铃声要设置为AVAudioSessionCategoryMultiRoute多声道模式, 等到接听铃声播放结束再设置为AVAudioSessionCategoryPlayback后台播放模式,

播放器部分代码:

  1. static ZSAVPlayerInstance *instance;
  2. static dispatch_once_t onceToken;
  3. + (instancetype)sharedInstance {
  4. dispatch_once(&onceToken, ^{
  5. instance = [ZSAVPlayerInstance new];
  6. });
  7. return instance;
  8. }
  9. - (void)setup {
  10. [self setupAudioSession];
  11. [self setupAudioPlayer];
  12. }
  13. - (void)setupAudioSession {
  14. // 新建AudioSession会话
  15. AVAudioSession *audioSession = [AVAudioSession sharedInstance];
  16. // 设置多声道混合播放
  17. NSError *error = nil;
  18. [audioSession setCategory:AVAudioSessionCategoryMultiRoute withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
  19. if (error) {
  20. NSLog(@"Error setCategory AVAudioSession: %@", error);
  21. }
  22. NSLog(@"%d", audioSession.isOtherAudioPlaying);
  23. NSError *activeSetError = nil;
  24. // 启动AudioSession,如果一个前台app正在播放音频则可能会启动失败
  25. [audioSession setActive:YES error:&activeSetError];
  26. if (activeSetError) {
  27. NSLog(@"Error activating AVAudioSession: %@", activeSetError);
  28. }
  29. }
  30. - (void)setupAudioPlayer {
  31. //铃声文件
  32. self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"notes_of_the_optimistic" withExtension:@"caf"] error:nil];
  33. //音量
  34. self.audioPlayer.volume = 1.0;
  35. //播放多次
  36. self.audioPlayer.numberOfLoops = -1;
  37. [self.audioPlayer prepareToPlay];
  38. }
  39. #pragma mark - public method
  40. - (void)start {
  41. NSLog(@"--ringUrl:%@", self.ringUrl.absoluteString);
  42. [self setupAudioSession];
  43. [self setupAudioPlayer];
  44. if (!self.audioPlayer.isPlaying) {
  45. [self.audioPlayer play];
  46. }
  47. }
  48. - (void)stop {
  49. [self.audioPlayer stop];
  50. }

结语

语音通话的主要知识点就是这些,还有一些其他的就需要结合具体业务流程就不方便说了,视频通话也可以使用LinPhone,有时间再写吧,感觉有帮助的话给个star吧!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/128734
推荐阅读
相关标签
  

闽ICP备14008679号