linphone是一款老牌的全平台的多人语音视频通话业务的软件(始自2001年),不仅支持视频和语音通话,还支持即时消息。
当然,重要的是:linphone是开源的,毕竟linphone的sip服务和媒体数据处理也大量使用开源框架;更重要的是,linphone官网提供免费的sip服务,这也意味着你不需要自己手动搭建sip服务即可享受voip业务,这对于有voip需求的前端来说无疑是最佳的选择。但是虽然开源,linphone的工程比较庞大,依赖项较多,编译的过程中容易出现问题。
那么下面,我将介绍自己linphone-iphone的安装历程。
获取Linphone源代码的途径
1.在CSDN上获取:(需要CSDN的2积分,只能在真机上运行)http://download.csdn.net/detail/showhilllee/8688073
2.在Github上获取:
https://github.com/BelledonneCommunications/linphone-iphone
之后,我把代码都上传到自己的Github上,亲测能跑,欢迎下载。
https://github.com/chencanfeng/CHLinphoneForCSDN
https://github.com/chencanfeng/CHLinphoneForGitHub
在GitHub下载linphone-iphone项目,并进行编译
1.linphone-iphone的下载:
这里要用git clone命令下载,主要是比直接download下载的多了一个git管理的仓库,命令行输入:
git clone https://github.com/BelledonneCommunications/linphone-iphone.git
2.linphone-iphone的编译

按照上面的步骤:
(1)为了支撑脚本顺利运行,我们需要安装 HomeBrew

①根据官网提示在终端输入安装命令:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
②装上wget
brew install wget
完成。
(2)安装好环境之后进入linphone目录内:cd linphone-iphone/
运行下面的命令来安装linphone-iphone的依赖项:
./prepare.py
如无例外,会报如下错误
ERROR:root:Could not find prepare module: 'module' object has no attribute 'Target', probably missing submodules/cmake-builder? Try running:
git submodule sync && git submodule update --init --recursive
原因是执行python脚本缺少模块,安照他给的提示执行
git submodule sync && git submodule update --init --recursive
注:git submodule sync && git submodule update --init --recursive 这句要卡很久,大概花了一天一夜的时间,醒来就已经执行完了。
接着再执行
./prepare.py
(3)配置环境路径
执行命令 export PATH=/usr/local/bin:$PATH
(4)Build SDK
执行命令 ./prepare.py -c && ./prepare.py && make
这里又会报错:

提示我们要安装java JDK,那就去装一下好了
①安装Java JDK
到官网下载MAC版的java jdk然后安装 :
http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
②根据提示运行组件,执行如下命令
brew install doxygen nasm intltool yasm automake coreutils optipng autoconf pkg-config
完成这些准备工作后,可以开始重头戏编译了,执行如下的make命令
./prepare.py -c && ./prepare.py && make
然后进入等待中。。。大概20分钟左右就可以编译好。
make完之后就可以看到子目录下多出了一个liblinphone-sdk文件夹,里面是编译好的liblinphone在各个平台下的静态库。

如果缺少文件就要重新执行一次make命令。
完成,打开linphone.xcodepro,工程文件夹中自带了企业推送证书,可以实现推送,连接真机后,按⌘R就可以享受了。
主要了解通话业务相关的代码
1.注册与使用

选择CREAT ACCOUNT进行帐号的注册,注册完成即可享受免费的sip服务,进入注册页面后,建议用户名最好填写电话号码或者纯数字帐号,因为sip地址是根据用户名来定义的,而且拨号盘只能输入数字。注册后在邮箱收到邮件并点击链接验证后帐号注册完成,可以向另一台注册了帐号的设备发送voip通话了。
2.与工程相关的类全部放在Classes中,LinphoneUI中存放自定义的各种控件,Utils中存放用到的工具类和第三方视图类,XMLRPC是远程过程调用的一个ios框架,工程中几乎所有的UIViewController都继承自TPMultiLayoutViewController,是用来解决同时适用横向和竖向的问题。
3.跟通话相关的类型主要有:
- CallIncomingView:电话打入的视图;
- CallOutgoingView:电话打出的视图;
- CallView:电话接通后的视图、包括语音聊天、视频聊天、会议电话的视图;
- PhoneMainView: 电话视图的容器,单例模式;监听大部分的通话状态变化,负责管理不同通话视图间的跳转和主要业务逻辑;
- LinphoneManager:对liblinphone中各种状态的封装以及功能的补充(音频播放、音频设备设置,音频设备占用处理,低电量,新消息处理,前后台处理),中间件。
4.代码分析
UI:
与通话相关的页面一共有3个:CallIncomingView、CallOutgoingView、CallView,分别为打入电话、呼出电话与通话期间的视图。没有特别难懂的地方,用通知来进行UI更新。
而"PhoneMainView"则比较特殊,该单例不仅起到业务层的作用,而且是上面3个页面的容器,通过它来进行通话页面之间的切换和管理。
业务,位于linphoneManager中:
-
基本业务:
1.拨号
判断网络状况
检查是否在GSM通话中
检测提供的addr是否合法
2g网络下是否使用low_bandwidth模式
linphone_core_invite_address_with_params
2.接听
2g网络是否是同low_bandwidth模式
linphone_call_params_enable_video 是否允许视频流
linphone_core_accept_call_with_params 接电话 -
状态监听:
voip通话状态改变的监听是通过通知kLinphoneCallUpdate下发的,注册该通知后,就可以从userInfo中的keycall和state中分别拿到当前通话的实例以及通话状态码。
那么,该通知又是怎么来的呢,看了voipManager.m便知,在linphonecore.h文件中有一个void (*LinphoneCoreCallStateChangedCb)(LinphoneCore *lc, LinphoneCall *call, LinphoneCallState cstate, const char *message)类型的blockcall_state_changed,该回调会在通话状态改变时触发,所以将方法static void linphone_iphone_call_state(LinphoneCore *lc, LinphoneCall *call, LinphoneCallState state, const char *message)赋值给了该blcok,在该方法中会对各种state进行处理,最后将state、call、message封装到dict后发送通知。
- 音频设备相关
需要处理两个问题:
1.音频设备占用,这里又分为GSM电话和普通音频应用的占用;
2.音频设备监测,主要针对蓝牙设备的接入。 - 前台与后台
为了保证应用进入后台也能长时间通话,做了如下处理:
1.触发applicationDidEnterBackground时:enterBackgroundMode
(1)refreshRegisters
(2)设置超时处理[[UIApplication sharedApplication] setKeepAliveTimeout: 600 handler:^{ linphone_core_iterate }
(3)有通话则延长后台存活时间startCallPausedLongRunningTask
(4)[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler
(5)停止视频预览 linphone_core_stop_dtmf_stream
(6)如果不进入后台模式,destroy voip socket if any
2.触发applicationDidBecomeActive时:调用becomeActive
(1)refreshRegisters
(2)停止后台任务pausedCallBgTask和incallBgTask
(3)linphone_core_start_dtmf_stream
其他部分就不一一列举了,说的不够清楚,建议如果感兴趣的话自己实际编译调试一遍,从而对linphone的ios端接口有更深的了解。
如何将Linphone移植到自己的项目
在网上关于Linphone移植的资料并不多,当时从GitHub下载的项目怎么也编译不成功,花了两三天的时间,最终选择CSDN上的案例进行移植,小编是看了下面的两篇文章慢慢摸索,最终移植成功了。
好了,接下来开始编写demo
1.新建工程,拷贝CSDN案例中的liblinphone-sdk到新项目,导入lib文件夹中所有的依赖库,如图所示:

2.这时编译会报很多错,导入如下图的依赖库,一个不漏喔 ~

3.Build Settings 设置 Header Search Paths 、 Library Search Paths、Other Linker Flags



4.在lnfo.plist文件,设置http请求
从iOS9开始,苹果建议所有的网络请求都要使用https,如果还想保留原有的http请求方式,开发者需要修改配置文件。
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
5.在info.plist 文件,设置可以在后台运行
一般的iOS程序进入后台后会被系统挂起,就会停止执行,不能执行任何操作
从iOS4开始,苹果增加了特性,很好的支持了语音通话功能:
苹果支持应用可以在后台播放和录制声音;
苹果支持网络托管,保证应用在后台时,还能保持网络连接,能接收到来电;
应用可以设置一个超时处理,程序在后台运行时,周期性地唤醒应用,保证客户端和服务器有长连接,使网络不断开。
SDK封装了这些特性,保证了在iOS平台上,有很好的语音通话体验。
开发者需要修改配置文件,这样iOS工程才能支持这些特性。
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>voip</string>
</array>
6、编译
如果出现了形如“file not found”的错误提示,检查头文件、静态库是否关联成功
目前为止,这个demo已经配置好了Linphone的开发环境。这里使用了CSDN案例中的几个类文件,包括:
LinphoneManager.[h,m],FastAddressBook.[h,m],Utils.[h.m]
注:非 ARC 模式的代码文件需要加入 -fno-objc-arc 标签。
事不宜迟,直接上代码,~
1.在AppDelegate.m初始化Linphone
[[LinphoneManager instance] startLibLinphone];
2.使用sip账号登陆Linphone,配置账号信息
云会议项目获取sip账号的接口:
/bjtel/RegisterVoipUser.mt
参数:voipusername = [UIDevice currentDevice].imei
- (BOOL)addProxyConfig:(NSString*)username password:(NSString*)password displayName:(NSString *)displayName domain:(NSString*)domain port:(NSString *)port withTransport:(NSString*)transport {
LinphoneCore* lc = [LinphoneManager getLc];
if (lc == nil) {
[[LinphoneManager instance] startLibLinphone];
lc = [LinphoneManager getLc];
}
LinphoneProxyConfig* proxyCfg = linphone_core_create_proxy_config(lc);
NSString* server_address = domain;
char normalizedUserName[256];
linphone_proxy_config_normalize_number(proxyCfg, [username cStringUsingEncoding:[NSString defaultCStringEncoding]], normalizedUserName, sizeof(normalizedUserName));
const char *identity = [[NSString stringWithFormat:@"sip:%@@%@", username, domain] cStringUsingEncoding:NSUTF8StringEncoding];
LinphoneAddress* linphoneAddress = linphone_address_new(identity);
linphone_address_set_username(linphoneAddress, normalizedUserName);
if (displayName && displayName.length != 0) {
linphone_address_set_display_name(linphoneAddress, (displayName.length ? displayName.UTF8String : NULL));
}
if( domain && [domain length] != 0) {
if( transport != nil ){
server_address = [NSString stringWithFormat:@"%@:%@;transport=%@", server_address, port, [transport lowercaseString]];
}
// when the domain is specified (for external login), take it as the server address
linphone_proxy_config_set_server_addr(proxyCfg, [server_address UTF8String]);
linphone_address_set_domain(linphoneAddress, [domain UTF8String]);
}
// 添加了昵称后的identity
identity = linphone_address_as_string(linphoneAddress);
linphone_address_destroy(linphoneAddress);
LinphoneAuthInfo* info = linphone_auth_info_new([username UTF8String]
, NULL, [password UTF8String]
, NULL
, linphone_proxy_config_get_realm(proxyCfg)
,linphone_proxy_config_get_domain(proxyCfg));
[self setDefaultSettings:proxyCfg];
[self clearProxyConfig];
linphone_proxy_config_set_identity(proxyCfg, identity);
linphone_proxy_config_set_expires(proxyCfg, 2000);
linphone_proxy_config_enable_register(proxyCfg, true);
linphone_core_add_auth_info(lc, info);
linphone_core_add_proxy_config(lc, proxyCfg);
linphone_core_set_default_proxy_config(lc, proxyCfg);
ms_free(identity);
return TRUE;
}
- (void)clearProxyConfig {
linphone_core_clear_proxy_config([LinphoneManager getLc]);
linphone_core_clear_all_auth_info([LinphoneManager getLc]);
}
- (void)setDefaultSettings:(LinphoneProxyConfig*)proxyCfg {
LinphoneManager* lm = [LinphoneManager instance];
[lm configurePushTokenForProxyConfig:proxyCfg];
}
3.监听名为 kLinphoneRegistrationUpdate 的通知,通过 linphone_proxy_config_get_state方法可获取到当前的状态,就可以知道有没有登陆成功。
#pragma mark -发送通知
- (void)onRegister:(LinphoneCore *)lc cfg:(LinphoneProxyConfig*) cfg state:(LinphoneRegistrationState) state message:(const char*) message;
#pragma mark -监听到通知,获取到当前的状态
- (void)registrationUpdate: (NSNotification*) notif {
LinphoneProxyConfig* config = NULL;
linphone_core_get_default_proxy([LinphoneManager getLc], &config);
LinphoneRegistrationState state = LinphoneRegistrationNone;
if (config == NULL) {
state = LinphoneRegistrationNone;
} else {
state = linphone_proxy_config_get_state(config);
}
...
}
4.监听名为 kLinphoneCallUpdate 的通知,可知道当前电话的状态,通过 state 的状态进行各种事件处理:
#pragma mark -发送通知
- (void)onCall:(LinphoneCall*)call StateChanged:(LinphoneCallState)state withMessage:(const char *)message;
#pragma mark -这里是监听通话结束时挂断电话
- (void)callUpdateEvent:(NSNotification *)notif {
LinphoneCallState state = [[notif.userInfo objectForKey: @"state"] intValue];
if (state == LinphoneCallError || state == LinphoneCallEnd) {
[self processHangup];
[self.navigationController popViewControllerAnimated:YES];
}
}
5.开始拨打电话
(1)拨打电话/挂断电话
sip的电话需要拼接一串前缀,
云会议项目拨打的前缀是 8319*12829565* + 国际区号 + 手机号码
#pragma mark -点击按钮拨打电话
- (void)processMakeCall {
NSString *address = [self.callNumber copy];
if(![address containsString:@"8319*12829565"]) {
address = [NSString stringWithFormat:@"8319*12829565*%@",self.callNumber];
}
if( [address length] > 0){
[[LinphoneManager instance] call:address recordPath:_contact.path transfer:FALSE];
}
}
#pragma mark -点击按钮挂断电话
- (void)processHangup {
LinphoneCore* lc = [LinphoneManager getLc];
LinphoneCall* currentcall = linphone_core_get_current_call(lc);
if(currentcall != NULL) { // In a call
linphone_core_terminate_call(lc, currentcall);
}
}
#pragma mark -监听通讯状态,当通话结束时挂断电话
- (void)callUpdateEvent:(NSNotification *)notif {
LinphoneCallState state = [[notif.userInfo objectForKey: @"state"] intValue];
if (state == LinphoneCallError || state == LinphoneCallEnd) {
[self processHangup];
[self.navigationController popViewControllerAnimated:YES];
}
}
(2)打开/关闭扩音器 (扬声器)
#pragma mark -打开扬声器
[[LinphoneManager instance] setSpeakerEnabled:TRUE];
#pragma mark -关闭扬声器
[[LinphoneManager instance] setSpeakerEnabled:FALSE];
(3)打开/关闭麦克风(关闭时,对方听不到我方的声音)
#pragma mark -打开麦克风
linphone_core_mute_mic([LinphoneManager getLc], true);
#pragma mark -关闭麦克风
linphone_core_mute_mic([LinphoneManager getLc], false);
(4)设置录音保存的位置以及格式
linphone_call_params_set_record_file
这个函数时linPhone中对通话时的音频进行录音,并保存到本地的一个重要函数。其作用就是设置录音保存的位置以及格式等。注意在linPhoneCall还没有分配资源时就应该调用这个函数。
也就是说,在点击拨打号码创建LinphoneCallParams时,就应该调用
- (void)call:(NSString *)address recordPath:(NSString*)path transfer:(BOOL)transfer {
...
LinphoneProxyConfig* proxyCfg;
//get default proxy
linphone_core_get_default_proxy(theLinphoneCore,&proxyCfg);
LinphoneCallParams* lcallParams = linphone_core_create_default_call_parameters(theLinphoneCore);
//linPhone对通话时的音频进行录音,并保存到本地
[self initSetRcordWithParams:lcallParams recordPath:path];
...
}
- (void)initSetRcordWithParams:(LinphoneCallParams *)params recordPath:(NSString *)strPath{
//NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
//NSString *strPath = [NSString stringWithFormat:@"%@/voip_recordfile_%@.wav", documentsDirectory,fullTime];
//设置音频保存路径
const char *path = [strPath UTF8String];
//如果文件存在则删除
NSFileManager *fileManager = [NSFileManager defaultManager];
if([fileManager fileExistsAtPath:strPath]) {
[fileManager removeItemAtPath:strPath error:nil];
}
linphone_call_params_set_record_file(params,path);
}
(5)开始/结束录音
#pragma mark -打开录音
LinphoneCall* call = linphone_core_get_current_call([LinphoneManager getLc]);
linphone_call_start_recording(call);
#pragma mark -结束录音
LinphoneCall* call = linphone_core_get_current_call([LinphoneManager getLc]);
linphone_call_stop_recording(call);
录音是由第一次按开始和最后一次按结束来确定录音范围,同一个通话只生成一段录音,会自动衔接各段录音。完成录音之后,就可以在你保存录音的位置获取到录音文件,我是保存在真机NSDocumentDirectory文件下。
注意:录音为双向录音,被叫和主叫均被录音。结束录音前必须保证已经开始了,否则会crash。
到此为止,我们已经成功地将Linphone移植到项目中去,可以开心的使用了。
参考文档
Linphone的官网:http://www.linphone.org/
Linphone的说明文档:https://www.linphone.org/docs/liblinphone/modules.html
whiteking的文章:http://www.jianshu.com/p/ec5cfb72c9e7
江湖的文章:http://www.jianshu.com/p/2ba2d4aa168e
偶尔鸣叫的丹丹龙的文章:http://www.jianshu.com/p/845cd812fcd7
linphone-iphone的安装与调试: http://www.jianshu.com/p/a4a00264a8e8
网友评论