iOS 基于LinPhone的语音通话

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

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);
    LinphoneAddress *tmpAddr = linphone_address_new([NSString stringWithFormat:@"sip:%@",domain].UTF8String);
    linphone_address_set_username(addr, username.UTF8String);
    linphone_address_set_port(addr, linphone_address_get_port(tmpAddr));
    linphone_address_set_domain(addr, linphone_address_get_domain(tmpAddr));
    if (displayName && ![displayName isEqualToString:@""]) {
        linphone_address_set_display_name(addr, displayName.UTF8String);
    }
    linphone_proxy_config_set_identity_address(config, addr);
    if (transport) {
        linphone_proxy_config_set_route(
                                        config,
                                        [NSString stringWithFormat:@"%s;transport=%s", domain.UTF8String, transport.lowercaseString.UTF8String]
                                        .UTF8String);
        linphone_proxy_config_set_server_addr(
                                              config,
                                              [NSString stringWithFormat:@"%s;transport=%s", domain.UTF8String, transport.lowercaseString.UTF8String]
                                              .UTF8String);
    }
    
    linphone_proxy_config_enable_publish(config, FALSE);
    linphone_proxy_config_enable_register(config, TRUE);
    
    LinphoneAuthInfo *info =
    linphone_auth_info_new(linphone_address_get_username(addr), // username
                           NULL,                                // user id
                           password.UTF8String,                        // passwd
                           NULL,                                // ha1
                           linphone_address_get_domain(addr),   // realm - assumed to be domain
                           linphone_address_get_domain(addr)    // domain
                           );
    linphone_core_add_auth_info(LC, info);
    linphone_address_unref(addr);
    linphone_address_unref(tmpAddr);
    //分配随机端口
    LCSipTransports transportValue = {-1,-1,-1,-1};
    
    if (linphone_core_set_sip_transports(LC, &transportValue)) {
        NSLog(@"cannot set transport");
    }

2.外呼

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

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

//当前账号置为繁忙
NSData *data8 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"繁忙信令"];
[self.clientSocket writeData:data8 withTimeout:-1 tag:8];

//分配座席,并建立语音通话链接
NSData *data9 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"座席建立链接信令"];
[self.clientSocket writeData:data9 withTimeout:-1 tag:9];

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

实现功能的部分代码:

//监听通话状态变化
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onCallUpdate:) name:kLinphoneCallUpdate object:nil];

- (void) onCallUpdate: (NSNotification*) notification {
    NSDictionary* userInfo = [notification userInfo];
    NSValue* c = [userInfo valueForKey:@"call"];
    //    int state = (int)[userInfo valueForKey:@"state"];
    LinphoneCallState state = [[userInfo objectForKey:@"state"] intValue];
    NSString* message = [userInfo valueForKey:@"message"];
    NSLog(@"========== state: %d, message: %@", state, message);
    LinphoneCall* call = c.pointerValue;
    
    NSDictionary *dict = @{@"call" : [NSValue valueWithPointer:call],
                           @"state" : [NSNumber numberWithInt:state],
                           @"message" : message};
    
    switch (state) {
        //接听
        case LinphoneCallIncomingReceived: {
            [NSNotificationCenter.defaultCenter postNotificationName:ES_ON_CALL_COMMING object: self userInfo:dict];
            if (self.callBlock) {
                self.callBlock((NSInteger)state, dict);
            }
            break;
        }
            case LinphoneCallOutgoingInit:
            case LinphoneCallConnected:
            case LinphoneCallStreamsRunning: {
                // check video
                if (![self isVideoEnabled:call]) {
                    const LinphoneCallParams *param = linphone_call_get_current_params(call);
                    const LinphoneCallAppData *callAppData =
                    (__bridge const LinphoneCallAppData *)(linphone_call_get_user_data(call));
                    if (state == LinphoneCallStreamsRunning && callAppData->videoRequested &&
                        linphone_call_params_low_bandwidth_enabled(param)) {
                        // too bad video was not enabled because low bandwidth
                        
                        NSLog(@"带宽太低,无法开启视频通话");
                        
                        callAppData->videoRequested = FALSE; /*reset field*/
                    }
                }
                [NSNotificationCenter.defaultCenter postNotificationName:ES_ON_CALL_STREAM_UPDATE object:self userInfo:dict];
                break;
            }
            case LinphoneCallUpdatedByRemote: {
                const LinphoneCallParams *current = linphone_call_get_current_params(call);
                const LinphoneCallParams *remote = linphone_call_get_remote_params(call);
                
                /* remote wants to add video */
                if ((linphone_core_video_display_enabled([LinphoneManager getLc]) && !linphone_call_params_video_enabled(current) &&
                     linphone_call_params_video_enabled(remote)) &&
                    (!linphone_core_get_video_policy([LinphoneManager getLc])->automatically_accept ||
                     (([UIApplication sharedApplication].applicationState != UIApplicationStateActive) &&
                      floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max))) {
                         linphone_core_defer_call_update([LinphoneManager getLc], call);
                         
                         
                         [NSNotificationCenter.defaultCenter postNotificationName:ES_ON_REMOTE_OPEN_CEMERA object: self userInfo:dict];
                         
                         //                     [self allowToOpenCameraByRemote:call];
                         
                     } else if (linphone_call_params_video_enabled(current) && !linphone_call_params_video_enabled(remote)) {
                         
                     }
                break;
            }
            case LinphoneCallUpdating:
            break;
            case LinphoneCallPausing:
            case LinphoneCallPaused:
            break;
            case LinphoneCallPausedByRemote:
            break;
        //挂断
        case LinphoneCallEnd: {//LinphoneCallEnd
            [NSNotificationCenter.defaultCenter postNotificationName:ES_ON_CALL_END object: self userInfo:NULL];
            if (self.callBlock) {
                self.callBlock((NSInteger)state, dict);
            }
            break;
        }
        case LinphoneCallReleased: {
            if (self.callBlock) {
                self.callBlock((NSInteger)state, dict);
            }
            break;
        }
        case LinphoneCallError:
        default:
            break;
    }
}

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

- (void)acceptCall:(LinphoneCall *)call evenWithVideo:(BOOL)video {
	LinphoneCallParams *lcallParams = linphone_core_create_call_params(theLinphoneCore, call);
	if (!lcallParams) {
		LOGW(@"Could not create call parameters for %p, call has probably already ended.", call);
		return;
	}

	if ([self lpConfigBoolForKey:@"edge_opt_preference"]) {
		bool low_bandwidth = self.network == network_2g;
		if (low_bandwidth) {
			LOGI(@"Low bandwidth mode");
		}
		linphone_call_params_enable_low_bandwidth(lcallParams, low_bandwidth);
	}
	linphone_call_params_enable_video(lcallParams, video);

	linphone_call_accept_with_params(call, lcallParams);
    linphone_call_params_unref(lcallParams);
}

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

3.挂断

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

I. 己方挂断

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

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

 NSData *data20 = [self sendMsgWithName:self.tcpModel.PhoneNo type:@"" targetType:@"" msg:[NSString stringWithFormat:@"开始结束通话信令服务"];

  LinphoneCore* lc = [LinphoneManager getLc];
    LinphoneCall* currentcall = linphone_core_get_current_call(lc);
    if (linphone_core_is_in_conference(lc) || // In conference
        (linphone_core_get_conference_size(lc) > 0) // Only one conf
        ) {
        linphone_core_terminate_conference(lc);
    } else if(currentcall != NULL) { // In a call
//        linphone_core_terminate_call(lc, currentcall);
        linphone_call_terminate(currentcall);
    } else {
        const MSList* calls = linphone_core_get_calls(lc);
        if (ms_list_size(calls) == 1) { // Only one call
//            linphone_core_terminate_call(lc,(LinphoneCall*)(calls->data));
            linphone_call_terminate((LinphoneCall *)(calls->data));
        }
    }

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

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

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

case LinphoneCallReleased: {
            if (self.callBlock) {
                self.callBlock((NSInteger)state, dict);
            }
            break;
 }


sipManager.callBlock = ^(NSInteger callState, NSDictionary *dict) {
        if (self->_ISDIALOUT == 0) {
            return;
        }
        if (callState == 1) {
            NSLog(@"接听--callState == 1");
            LinphoneCall *call = [dict[@"call"] pointerValue];
            [weakManager acceptCall:(ESCall *)call];
        } else if (callState == 18) {
            NSLog(@"挂断--callState == 18");
            //向app端推送EventHangup
//            if (self.eventBlock) {
//                NSDictionary *dic = @{@"CallId":@"",
//                                      @"CallTime":@"",
//                                      @"CodeCause":@"",
//                                      @"ConnectTime":@"",
//                                      @"EndTime":@"",
//                                      @"Rebark":self.Rebark ? self.Rebark : @"",
//                                      @"SignalIng":@"",
//                                      @"WavFile":self.WavFile ? self.WavFile : @""
//                };
//                self.eventBlock(EVENTHangup, YES, dic);
//            }
            //清空本地数据
            self->_callModel = nil;
            self->_CallId = nil;
            self->_ConnectTime = nil;
            self->_Rebark = nil;
            self->_WavFile = nil;
            self->_ISDIALOUT = 10;
   }

II. 对方挂断 

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

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

4.来电

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

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


//繁忙信令
NSData *data13 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:[NSString stringWithFormat:@"当前账号置为繁忙的信令"]]; //与data10一样;
        [self.clientSocket writeData:data13 withTimeout:-1 tag:13];

//分配座席信令        
NSData *data14 = [self sendMsgWithName:@"" type:@"" targetType:@"FlyCn" msg:[NSString stringWithFormat:@"转发分配座席的信令"]; //与data10一样
        [self.clientSocket writeData:data14 withTimeout:-1 tag:14];

4.接听

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

LinphoneCallParams *lcallParams = linphone_core_create_call_params(theLinphoneCore, call);
	if (!lcallParams) {
		LOGW(@"Could not create call parameters for %p, call has probably already ended.", call);
		return;
	}

	if ([self lpConfigBoolForKey:@"edge_opt_preference"]) {
		bool low_bandwidth = self.network == network_2g;
		if (low_bandwidth) {
			LOGI(@"Low bandwidth mode");
		}
		linphone_call_params_enable_low_bandwidth(lcallParams, low_bandwidth);
	}
	linphone_call_params_enable_video(lcallParams, video);

	linphone_call_accept_with_params(call, lcallParams);
    linphone_call_params_unref(lcallParams);

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

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

//听筒/扬声器切换
- (void)speakerToggle {
    //true:开启扬声器; false:关闭扬声器
    [LinphoneManager.instance setSpeakerEnabled:self->speaker];
    self->speaker = !self->speaker;
}

//静音切换
- (void)muteToggle {
    //true:开启静音; false:关闭静音
    linphone_core_enable_mic(LC, self->mute);
    self->mute = !self->mute;
}

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

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

//保持
- (void)holdCall {
    NSData *data30 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:[NSString stringWithFormat:@"保持通话信令"];
    [self.clientSocket writeData:data30 withTimeout:-1 tag:30];
    
    NSData *data31 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:[NSString stringWithFormat:@"保持繁忙状态信令"];
    [self.clientSocket writeData:data31 withTimeout:-1 tag:31];
    
    //向app端推送状态事件EventAgentState
    if (self.eventBlock) {

    }
    
    self.flag = ([self.flag isEqualToString:@"HOLDCALL"]) ? @"REHOLDCALL" : @"HOLDCALL";
}

语音通话后台保活的实现

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

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

manager.eventBlock = ^(EVENT event, BOOL result, NSDictionary * _Nonnull resultMsg) {
        switch (event) {
            case EVENTAgentState: {
                //状态变化事件,下一行注释均为回调具体参数
                [[NSNotificationCenter defaultCenter] postNotificationName:kEVENTAgentState object:nil userInfo:resultMsg];
            }
                break;
            case EVENTLogin: {
                //登录成功事件
                if (result) {
                    ViewController *vc = [[ViewController alloc] init];
                    [self.navigationController pushViewController:vc animated:YES];
                } else {
                    sender.enabled = YES;
                    [self alertWithMessage:@"登录失败,请重新登录"];
                }
            }
                break;
            case EVENTQuitLogin: {
                //退出登录或工号异处登录事件
                [[NSNotificationCenter defaultCenter] postNotificationName:kEVENTQuitLogin object:nil userInfo:resultMsg];
            }
                break;
            case EVENTMakeCall: {
                //外呼事件
            }
                break;
            case EVENTComeCall: {
                //来电事件
                [[NSNotificationCenter defaultCenter] postNotificationName:kComeCallUpdate object:nil userInfo:resultMsg];
                AVAudioSession *audioSession = [AVAudioSession sharedInstance];
                // 设置多声道播放
                NSError *error = nil;
                [audioSession setCategory:AVAudioSessionCategoryMultiRoute withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
                [[ZSAVPlayerInstance sharedInstance] start];
            }
                break;
            case EVENTCalling: {
                //通话中事件
                [[ZSAVPlayerInstance sharedInstance] stop];
                AVAudioSession *audioSession = [AVAudioSession sharedInstance];
                // 设置后台播放
                NSError *error = nil;
                [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
            }
                break;
            case EVENTHangup: {
                //挂机事件
                [[NSNotificationCenter defaultCenter] postNotificationName:kEVENTHangup object:nil userInfo:resultMsg];
                [[ZSAVPlayerInstance sharedInstance] stop];
                AVAudioSession *audioSession = [AVAudioSession sharedInstance];
                // 设置后台播放
                NSError *error = nil;
                [audioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
            }
                break;
            default:
                break;
        }
        [resultMsg enumerateKeysAndObjectsUsingBlock:^(id key,id obj, BOOL *stop) {
             NSLog(@"event:%d key:%@ value:%@", (int)event, key, obj);
          }];
    };

II.问题:

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

播放器部分代码:

static ZSAVPlayerInstance *instance;
static dispatch_once_t onceToken;
+ (instancetype)sharedInstance {
    dispatch_once(&onceToken, ^{
        instance = [ZSAVPlayerInstance new];
    });
    return instance;
}


- (void)setup {
    [self setupAudioSession];
    [self setupAudioPlayer];
}

- (void)setupAudioSession {
    // 新建AudioSession会话
    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    // 设置多声道混合播放
    NSError *error = nil;
    [audioSession setCategory:AVAudioSessionCategoryMultiRoute withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
    if (error) {
        NSLog(@"Error setCategory AVAudioSession: %@", error);
    }
    NSLog(@"%d", audioSession.isOtherAudioPlaying);
    NSError *activeSetError = nil;
    // 启动AudioSession,如果一个前台app正在播放音频则可能会启动失败
    [audioSession setActive:YES error:&activeSetError];
    if (activeSetError) {
        NSLog(@"Error activating AVAudioSession: %@", activeSetError);
    }
}

- (void)setupAudioPlayer {
    //铃声文件
    self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"notes_of_the_optimistic" withExtension:@"caf"] error:nil];
    //音量
    self.audioPlayer.volume = 1.0;
    //播放多次
    self.audioPlayer.numberOfLoops = -1;
    [self.audioPlayer prepareToPlay];
}

#pragma mark - public method

- (void)start {
    NSLog(@"--ringUrl:%@", self.ringUrl.absoluteString);
    [self setupAudioSession];
    [self setupAudioPlayer];
    if (!self.audioPlayer.isPlaying) {
        [self.audioPlayer play];
    }
}

- (void)stop {
    [self.audioPlayer stop];
}

结语

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
用户点击 用户点击 linphone linphone linphone linphone 的图标后就开始了 的图标后就开始了 的图标后就开始了 的图标后就开始了 的图标后就开始了 linphone linphone linphone linphone 软件,这时 软件,这时 软件,这时 软件,这时 软件,这时 linphoneActivity linphoneActivity linphoneActivity linphoneActivity linphoneActivity linphoneActivity linphoneActivity开始运行,它 开始运行,它 开始运行,它 开始运行,它 使 linphoneService linphoneService linphoneService linphoneService linphoneServicelinphoneServicelinphoneService 开始,并做一些 开始,并做一些 开始,并做一些 开始,并做一些 linphone linphone linphone linphone 帐号密码的登录操作同时引导用户进行环境变 帐号密码的登录操作同时引导用户进行环境变 帐号密码的登录操作同时引导用户进行环境变 帐号密码的登录操作同时引导用户进行环境变 帐号密码的登录操作同时引导用户进行环境变 帐号密码的登录操作同时引导用户进行环境变 帐号密码的登录操作同时引导用户进行环境变 帐号密码的登录操作同时引导用户进行环境变 帐号密码的登录操作同时引导用户进行环境变 帐号密码的登录操作同时引导用户进行环境变 量的设置( 量的设置( LinphonePreferencesActivity LinphonePreferencesActivity LinphonePreferencesActivityLinphonePreferencesActivity LinphonePreferencesActivityLinphonePreferencesActivityLinphonePreferencesActivityLinphonePreferencesActivity LinphonePreferencesActivity LinphonePreferencesActivity LinphonePreferencesActivity LinphonePreferencesActivity)。 环境变量都储存在 环境变量都储存在 环境变量都储存在 环境变量都储存在 sharedPreferencessharedPreferences sharedPreferencessharedPreferences sharedPreferencessharedPreferencessharedPreferencessharedPreferences sharedPreferences 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 中,它是整个工程共享的一变量池。这些环境有 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器自动启回校正网络 音频和视编码设置选择,帐号密服务器
iOS中引入linphone-sdk可以通过以下步骤: 1. 下载linphone-sdk:可以从linphone官方网站下载最新的linphone-sdk的压缩包。 2. 解压压缩包:将下载的linphone-sdk压缩包解压到目标文件夹中。 3. 创建新的Xcode工程:使用Xcode创建一个新的iOS工程。 4. 导入linphone-sdk到工程中:在Xcode中的工程导航栏中右键点击“Frameworks”文件夹,选择“Add Files to 'Your project name'”选项,然后导航到刚才解压的linphone-sdk文件夹中,选择liblinphone.xcodeproj文件,点击“Add”按钮。 5. 添加依赖库:点击Xcode中的工程导航栏,选择你的项目的target,在General选项卡中,找到“Linked Frameworks and Libraries”部分,点击“+”按钮,选择添加以下依赖库: - libiconv.tbd - libz.tbd - libsqlite3.0.tbd - AudioToolbox.framework - AVFoundation.framework - CoreAudio.framework - CoreVideo.framework - CoreGraphics.framework - CoreMedia.framework - VideoToolbox.framework - UIKit.framework - Foundation.framework - CFNetwork.framework - Security.framework - SystemConfiguration.framework 6. 配置Build Settings:点击Xcode中的工程导航栏,选择你的项目的target,在Build Settings选项卡中,找到“Header Search Paths”部分,添加linphone-sdk的头文件路径。 7. 配置Build Phases:点击Xcode中的工程导航栏,选择你的项目的target,在Build Phases选项卡中,展开“Target Dependencies”部分,点击“+”按钮,选择添加liblinphone iOS库。 8. 添加代码:在需要使用linphone-sdk的地方,引入头文件并编写相应的代码,如初始化linphone对象,注册账号等。 以上就是在iOS中引入linphone-sdk的一般步骤。根据具体情况可能会有一些特殊步骤或配置。了解linphone-sdk的文档和示例代码将有助于更深入地了解如何使用该SDK。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值