Linphone是一款开源基于SIP协议的语音视频电话软件,可移植到移动端Android、IOS、WindowsPhone8,桌面系统包括GNU/Linux、Windows、Mac,以及Web浏览器。
文档直连
1、linehone官网 : http://www.linphone.org/technical-corner/liblinphone
2、官网-android文档: https://wiki.linphone.org/xwiki/wiki/public/view/Lib/Getting%20started/Android/
3、官网-android案例: https://github.com/BelledonneCommunications/linphone-android
4、可以直接从maven库下载android的aar文件
https://linphone.org/releases/maven_repository/org/linphone/linphone-sdk-android/
5、Android基于网络的VoIP电话的实现linphone https://blog.csdn.net/weixin_39947864/article/details/81910485
6、Linphone探索:1 . Linphone官方源码探究 https://blog.csdn.net/u012812482/article/details/51491226
以下内容分为两个部分,第一部分运行官方demo,简单了解linphone的语音通话功能;第二部分就是在自己的项目中集成linphone库,并自定义linphone的工具类库便于直接使用。【注意点:Android6.0之后的项目要动态授权--语音权限】
第一部分:运行官方简单的demo
首先下载liphone-android(下载)编译项目,simple文件就是linphone语音通话最简单的演示,界面如下:

image.png

image.png
官方的项目中还包含详细的工具库,可参考。
Linphone官方源码探究
以下内容来源:Linphone探索 https://blog.csdn.net/u012812482/article/details/51491226
BluetoothManager:蓝牙管理器。
BootReceiver:继承自BroadcastReceiver的类,用于在设备启动时自动启动LinphoneService。
CallActivity:通话界面。
CallAudioFragment:通话音频界面。
CallIncomingActivity:来电界面。电话的接听,挂断。 当前没有活动电话的情况下:可以通过按键挂断和接听来电。
通过LinphoneCoreListenerBase类,复写callState(电话方法)监听liphone内核电话状态,如果电话已经被挂断(分两种情况,对方挂断,本方挂断)则挂断电话。如果linphone内核已经检测到音频流(这里是铃声的流)则使linphone内核打开手机的喇叭,此时来电铃声就会播放。
CallManager:通话管理。
inviteAddress:向某个地址发起invite 请求
reinviteWithVideo:向当前的音频通路发起视频的invite请求,若当前带宽太窄或当前通路已经有视频流,这不发起invite请求。
reinvite:根据当前的profile向当前通路发起invite请求。
updateCall:改变当前视频通话的视频尺寸,调用该方法将会在重新建立流媒体通道和重新设置电话参数时重新打开摄像头。
CallOutgoingActivity:拨出电话界面。
CallVideoFragment:视频通话界面。 在当前的Activity中放置了一个继承自viewsurface的控件,用来绘制视频界面。
LinphoneManager:Linphone管理器。
启动管理:linphone内核(core),
文件管理:来电铃声、信息铃声、暂停铃声、配置文件
在线状态:在线,离线。
网络状态:网络状态发生变化。
通话管理:拨出,挂断,DTMF,接听。
短消息:状态
设备管理:摄像头,
编码器管理:音、视频编码器的检测
通道管理(Tunnel):
音量管理
音频通道:喇叭或者蓝牙
铃声管理:启动或者停止
第二部分,在自己项目中集成linphone库,语音通话界面如下:

image.png

image.png
自己项目具体实现
(1)添加Linphone的依赖库
如何在自己的项目中,引入linphone的库,具体步骤如下:
1、在项目的build.gradle文件中,添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | buildscript { repositories { jcenter() google() } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' } } allprojects { repositories { jcenter() google() //linphone maven { url "https://linphone.org/releases/maven_repository/" } } } |
2、在model项目中的build.gradle文件,添加:
1 2 3 4 5 6 7 8 | dependencies { //linphone debugImplementation "org.linphone:linphone-sdk-android-debug:4.2+" releaseImplementation "org.linphone:linphone-sdk-android:4.2+" //权限管理 implementation 'com.yanzhenjie:permission:2.0.3' } |
(2)代码实现
LinphoneService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | public class LinphoneService extends Service { private static LinphoneService sInstance; private static PhoneServiceCallback sPhoneServiceCallback; private Core mCore; public static void addCallback(PhoneServiceCallback phoneServiceCallback) { sPhoneServiceCallback = phoneServiceCallback; } public static boolean isReady() { return sInstance != null; } public static Core getCore() { return sInstance.mCore; } //监听 private CoreListenerStub mCoreListnerStub = new CoreListenerStub() { /** * 通话状态 * @param lc * @param call * @param cstate * @param message */ @Override public void onCallStateChanged(Core lc, Call call, Call.State cstate, String message) { Log.i("zss", "---- 通话状态 [ 状态:" + cstate + " ;消息: " + message + " ]"); if (cstate == Call.State.IncomingReceived) { //来电 // Log.i("zss","----- getRemoteAddress().getUsername: " + call.getRemoteAddress().getUsername() + " getRemoteAddress().getDomain: " + call.getRemoteAddress().getDomain() + " getRemoteAddress().getDisplayName:" + call.getRemoteAddress().getDisplayName() + " getRemoteAddress().getPort:" + call.getRemoteAddress().getPort() + " getUsername: " + call.getRemoteAddress().getPassword() ); // Log.i("zss", "----getTlsCert: " + authInfo[i].getTlsCert() + " getTlsCertPath:" + authInfo[i].getTlsCertPath()); Intent intent = new Intent(LinphoneService.this, CustomReceiveActivity.class); CustomReceiveActivity.getReceivedCallFromService(call); ReceiveDataModel receiveDataModel = new ReceiveDataModel(); receiveDataModel.setActiveCall(false); receiveDataModel.setNum(call.getRemoteAddress().getUsername()); intent.putExtra("ReceiveDataModel", receiveDataModel); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); // if (null != sPhoneServiceCallback) { // Log.i("zss", "---- sPhoneServiceCallback "); // sPhoneServiceCallback.incomingCall(call); // } } else if (cstate == Call.State.OutgoingProgress) { //正在呼叫 } else if (cstate == Call.State.Connected) { //接通或者拒绝 if (null != sPhoneServiceCallback) { sPhoneServiceCallback.callConnected(); } } else if (cstate == Call.State.End || (cstate == Call.State.Released)) { //挂断,未接 if (null != sPhoneServiceCallback) { sPhoneServiceCallback.callReleased(); } } } /** * 注册状态 * @param lc * @param cfg * @param cstate * @param message */ @Override public void onRegistrationStateChanged(Core lc, ProxyConfig cfg, RegistrationState cstate, String message) { if (null != sPhoneServiceCallback) { sPhoneServiceCallback.onRegistrationStateChanged(lc, cfg, cstate, message); } } }; @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); Log.i("zss", "---- Service_onCreate "); LinphoneManager.createAndStart(LinphoneService.this, mCoreListnerStub); } @Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); Log.i("zss", "---- Service_onStartCommand "); // If our Service is already running, no need to continue if (sInstance != null) { return START_STICKY; } sInstance = this; return START_STICKY; } @Override public void onDestroy() { Log.i("zss", "---- Service_onDestroy "); sInstance = null; LinphoneManager.destroy(); super.onDestroy(); } @Override public void onTaskRemoved(Intent rootIntent) { Log.i("zss", "---- Service_onTaskRemoved "); sInstance = null; LinphoneManager.destroy(); // For this sample we will kill the Service at the same time we kill the app stopSelf(); super.onTaskRemoved(rootIntent); } } |
LinphoneManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | /** * 初始化 linphone */ public class LinphoneManager { private Context mServiceContext; private static LinphoneManager instance; private static boolean sExited; private String mLinphoneFactoryConfigFile = null; public String mLinphoneConfigFile = null; private String mLPConfigXsd = null; private String mLinphoneRootCaFile = null; private String mRingSoundFile = null; private String mRingBackSoundFile = null; private String mPauseSoundFile = null; private String mChatDatabaseFile = null; private String mUserCerts = null; private Resources mResources; private Core mCore; private CoreListener mCoreListener; private Timer mTimer; private Handler mHandler; public LinphoneManager(Context serviceContext) { mServiceContext = serviceContext; sExited = false; String basePath = mServiceContext.getFilesDir().getAbsolutePath(); mLPConfigXsd = basePath + "/lpconfig.xsd"; mLinphoneFactoryConfigFile = basePath + "/linphonerc"; mLinphoneConfigFile = basePath + "/.linphonerc"; mLinphoneRootCaFile = basePath + "/rootca.pem"; mRingSoundFile = basePath + "/dont_wait_too_long.mkv"; //dont_wait_too_long.mkv oldphone_mono.wav mRingBackSoundFile = basePath + "/ringback.wav"; mPauseSoundFile = basePath + "/toy_mono.wav"; mChatDatabaseFile = basePath + "/linphone-history.db"; mUserCerts = basePath + "/user-certs"; // mErrorToneFile = basePath + "/error.wav"; mResources = serviceContext.getResources(); Factory.instance().setLogCollectionPath(basePath); Factory.instance().enableLogCollection(LogCollectionState.Enabled); //日志开关 Factory.instance().setDebugMode(true, "Linphone"); mHandler = new Handler(); } public synchronized static final LinphoneManager createAndStart(Context context, CoreListener coreListener) { if (instance != null) { throw new RuntimeException("Linphone Manager is already initialized"); } instance = new LinphoneManager(context); instance.startLibLinphone(context, coreListener); return instance; } private synchronized void startLibLinphone(Context context, CoreListener coreListener) { try { mCoreListener = coreListener; copyAssetsFromPackage(); // Create the Core and add our listener mCore = Factory.instance().createCore(mLinphoneConfigFile, mLinphoneFactoryConfigFile, context); mCore.addListener(coreListener); initLibLinphone(); // Core must be started after being created and configured mCore.start(); // We also MUST call the iterate() method of the Core on a regular basis TimerTask lTask = new TimerTask() { @Override public void run() { mHandler.post(new Runnable() { @Override public void run() { if (mCore != null) { mCore.iterate(); } } }); } }; mTimer = new Timer("Linphone scheduler"); mTimer.schedule(lTask, 0, 20); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } private void copyAssetsFromPackage() throws IOException { // LinphoneUtils.copyIfNotExist(mServiceContext, R.raw.dont_wait_too_long, mRingSoundFile); // LinphoneUtils.copyIfNotExist(mServiceContext, R.raw.ringback, mRingBackSoundFile); // LinphoneUtils.copyIfNotExist(mServiceContext, R.raw.toy_mono, mPauseSoundFile); LinphoneUtils.copyIfNotExist(mServiceContext, R.raw.linphonerc_default, mLinphoneConfigFile); LinphoneUtils.copyIfNotExist(mServiceContext, R.raw.linphonerc_factory, new File(mLinphoneFactoryConfigFile).getName()); // LinphoneUtils.copyIfNotExist(mServiceContext, R.raw.lpconfig, mLPConfigXsd); // LinphoneUtils.copyIfNotExist(mServiceContext, R.raw.rootca, mLinphoneRootCaFile); } private void initLibLinphone() { File f = new File(mUserCerts); if (!f.exists()) { if (!f.mkdir()) { Log.e(mUserCerts + " can't be created."); } } mCore.setUserCertificatesPath(mUserCerts); } public static synchronized Core getCoreIfManagerNotDestroyOrNull() { if (sExited || instance == null) { Log.e("Trying to get linphone core while LinphoneManager already destroyed or not created"); return null; } return getCore(); } public static synchronized final Core getCore() { return getInstance().mCore; } public static final boolean isInstanceiated() { return instance != null; } public static synchronized final LinphoneManager getInstance() { if (instance != null) { return instance; } if (sExited) { throw new RuntimeException("Linphone Manager was already destroyed. " + "Better use getLcIfManagerNotDestroyed and check returned value"); } throw new RuntimeException("Linphone Manager should be created before accessed"); } public static synchronized void destroy() { if (instance == null) { return; } sExited = true; instance.doDestroy(); } private void doDestroy() { try { mCore.removeListener(mCoreListener); mTimer.cancel(); mCore.stop(); } catch (RuntimeException e) { e.printStackTrace(); } finally { mCore = null; instance = null; } } } |
PhoneServiceCallback
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public abstract class PhoneServiceCallback { /** * 注册状态 */ public void onRegistrationStateChanged(Core lc, ProxyConfig cfg, RegistrationState cstate, String message) { } /** * 来电状态 * * @param linphoneCall */ public void incomingCall(Call linphoneCall) { } /** * 电话接通 */ public void callConnected() { } /** * 电话被挂断 */ public void callReleased() { } /** * 正在呼叫 */ public void OutgoingProgress(){ } } |
LinphoneUtils
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class LinphoneUtils { public static void copyIfNotExist(Context context, int ressourceId, String target) throws IOException { File lFileToCopy = new File(target); if (!lFileToCopy.exists()) { copyFromPackage(context, ressourceId, lFileToCopy.getName()); } } public static void copyFromPackage(Context context, int ressourceId, String target) throws IOException { FileOutputStream lOutputStream = context.openFileOutput(target, 0); InputStream lInputStream = context.getResources().openRawResource(ressourceId); int readByte; byte[] buff = new byte[8048]; while ((readByte = lInputStream.read(buff)) != -1) { lOutputStream.write(buff, 0, readByte); } lOutputStream.flush(); lOutputStream.close(); lInputStream.close(); } } |
PhoneVoiceUtils
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | public class PhoneVoiceUtils { private static volatile PhoneVoiceUtils sPhoneVoiceUtils; private Core mLinphoneCore = null; public static PhoneVoiceUtils getInstance() { if (sPhoneVoiceUtils == null) { synchronized (PhoneVoiceUtils.class) { if (sPhoneVoiceUtils == null) { sPhoneVoiceUtils = new PhoneVoiceUtils(); } } } return sPhoneVoiceUtils; } private PhoneVoiceUtils() { mLinphoneCore = LinphoneManager.getCore(); // mLinphoneCore.enableEchoCancellation(true); // mLinphoneCore.enableEchoLimiter(true); } /** * 注册到服务器 * * @param name 账号名 * @param password 密码 * @param host IP地址:端口号 */ public void registerUserAuth(String name, String password, String host) { registerUserAuth(name, password, host, TransportType.Udp); } /** * 注册到服务器 * * @param name 账号名 * @param password 密码 * @param host IP地址:端口号 * @param type TransportType.Udp TransportType.Tcp TransportType.Tls */ public void registerUserAuth(String name, String password, String host, TransportType type) { // String identify = "sip:" + name + "@" + host; AccountCreator mAccountCreator = mLinphoneCore.createAccountCreator(null); mAccountCreator.setUsername(name); mAccountCreator.setDomain(host); mAccountCreator.setPassword(password); mAccountCreator.setTransport(type); ProxyConfig cfg = mAccountCreator.createProxyConfig(); // Make sure the newly created one is the default mLinphoneCore.setDefaultProxyConfig(cfg); } //取消注册 public void unRegisterUserAuth() { mLinphoneCore.clearAllAuthInfo(); } /** * 是否已经注册了 * * @return */ public boolean isRegistered() { AuthInfo[] authInfos = mLinphoneCore.getAuthInfoList(); if (authInfos.length > 0) { return true; } else { return false; } } /** * 拨打电话 * * @param phone 手机号 * @return */ public Call startSingleCallingTo(String phone) { Call call = null; try { Address addressToCall = mLinphoneCore.interpretUrl(phone); CallParams params = mLinphoneCore.createCallParams(null); params.enableVideo(false); //不可视频 if (addressToCall != null) { call = mLinphoneCore.inviteAddressWithParams(addressToCall, params); } } catch (Exception e) { e.printStackTrace(); } return call; } /** * 挂断电话 */ public void hangUp() { if (mLinphoneCore == null) { mLinphoneCore = LinphoneManager.getCore(); } Call currentCall = mLinphoneCore.getCurrentCall(); if (currentCall != null) { mLinphoneCore.terminateCall(currentCall); } else if (mLinphoneCore.isInConference()) { mLinphoneCore.terminateConference(); } else { mLinphoneCore.terminateAllCalls(); } } /** * 是否静音 * * @param isMicMuted */ public void toggleMicro(boolean isMicMuted) { if (mLinphoneCore == null) { mLinphoneCore = LinphoneManager.getCore(); } mLinphoneCore.enableMic(isMicMuted); } /** * 接听来电 * * @param */ public void receiveCall(Call call) { if (mLinphoneCore == null) { mLinphoneCore = LinphoneManager.getCore(); } CallParams params = mLinphoneCore.createCallParams(call); params.enableVideo(false); if (null != call) { call.acceptWithParams(params); } } } |
项目代码链接