前言
android-ngn-stack是android2.x(或更高版本)NGN(下一代网络)软件栈。ngn基于doubango框架。doubango是目前世界上最先进的开源3GPP IMS/RCS嵌入式和桌面系统架构。其主要目的就是提供一个开放源码为开发者构建自己的VoIP应用。这一框架提供了一组独特的特征,从音频/视频呼叫,内容共享,消息,会议增强通信社交存在。所有这些功能都是按照GMSA RCS,3GPP IMS或VoLTE标准实施。
介绍
该文档由(Doubango电信)提供,以帮助开发人员快速构建新的android多媒体应用。如果你是一名开发人员,正在寻找开发NGN(网络电话,消息,视频会议,…)或丰富的android应用程序那么你选对地方了。如果你想获得帮助,或者想给我们一些反馈,那么请访问我们的网站http://code.google.com/p/imsdroid/
Doubango解决方案
android-ngn-stack是Doubango的解决方案,其中包括许多组件等部分;
客户端:
1、 Boghe:IMS/RCS Windows客户端
2、 IMSDroid:Android版IMS/RCS客户端采用android-ngn-stack
3、iDoubs:IOS(iPhone,iPad和iPod Touch)版IMS/RCS客户端
服务端:
4、OpenVCS:OpenVCS代表开源视频会议服务器和用于管理多点控制单元(MCU)。每个MCU(又称桥接器)最多可处理64个参与者
5、Flash2IMS:Adobe? 闪存SIP/IMS网管
亮点(支持特征):
6、SIP(RFC 3261,3GPP TS 24.229 Rel-9)
7、TCP和UDP IPv4或IPv6
8、信号压缩,SigComp(RFC3320,3485,4077,4464,4465,4896,5049,5112和1951)
9、加强版通讯录(XCAP存储,授权,presence)
10、发布第三版GSMA富通信套件
11、部分支持声音档案V1.0.0(GSMA VoLTE)
12、部分支持MMTEL UNI(GSMA RCS和GSMA VoLTE)
13、基本的IMS-AKA的注册(包括AKA-V1和AKA-V2),MD5
14、3GPP IMS早期安全性(3GPP TS33.978)
15、Proxy-CSCF使用DNS NAPTR+SRV
16、3GPP对SIP私有头(Headers)扩展
18、服务路由(Service Route discovery)
19、订阅注册事件包(履行网络发起(re/de/un)注册(registration)事件)
20、3GPP SMS IP(3GPP TS23.038,24.040,24.011,24.431和24.451)
21、语音呼叫(G729AB1, AMR-NB, iLBC, GSM, PCMA, PCMU, Speex-NB)
22、视频呼叫((H264, MP4V-ES, Theora, H.263, H.263-1998, H.261)
23、DTMF (RFC 4733)
24、使用前提QoS协商(RFC 3312, 4032 和5027)
25、SIP会话计时器(RFC4028)
26、临时响应确认( Provisional Response Acknowledgments)(PRACK)
27、保持通信(3GPP TS 24.610)
28、消息等待提示(3GPP TS 24.606)
29、使用ENUM协议调用E.164号码(RFC3761)
30、NAT穿越使用STUN2(RFC5389)与可能性自动发现服务器
通过使用DNS SRV(TURN已经实现,ICE正在测试)
31、一对一和群聊
32、文件传输和内容共享
建立NGN项目
这部分内容解释如果使用Eclipse建立一个NGN项目
检出(Checking)源代码
要检出NGN源代码,您首先需要一个SVN客户端。使用此命令检出项目源代码:
svn checkout http://imsdroid.googlecode.com/svn imsdroid
该库的源代码在:
imsdroid/branches/2.0/android-ngn-stack
导入NGN项目到Eclipse
NGN项目是下一代网络库
1、打开eclipse
2、选择文件(File)—>导入(Import)—>常规(General)—>现有项目到工作区(Existing Project into workspace)
3、选择android-ngn-stack文件夹,然后单击完成(Finish)
使用Eclipse创建你的第一个NGN应用
1、打开Eclipse,然后选择文件(File)—>新建(New)—>Android Project(android项目)
2、从下一个窗口(”新建Android项目(New Android Project)”)填充如下文本字段:
项目名称(Project name):myFirstApp
路径(Location):<设置任意路径>
构建目标版本(Build Targe):至少Android 2.0
应用程序名称(Application name):myFirstApp
包名称(Package name):org.doubango.test
选中 ”create activity”并命名为”Main”
单击Finish(完成)创建项目
译者注:可能eclipse是操作页面有区别,但基本一致。
3、从Eclipse package explorer中,右键单击myFirstApp,选择”(属性)Properties”然后选择Android从属性窗口,选择“(添加)add”按钮,然后从列表中选择android-ngn-stack库
4、选择“Java编译器(Java Compiler)”从左边更改1.5版本到1.6
5、从左侧选择“Java构建路径(Java Build Path)”,然后选择”库(Libraries)”选项卡。单击“添加JAR文件…(Add JARS…)”然后选择文件”android-ngn-stack/libs/simple-xml-2.3.4.jar”然后“确认(OK)”关闭窗口
6、单击”确认(OK)”关闭窗口
设置Android权限
为了使用该框架你必须在AndroidManifest.xml清单文件添加一些用户权限。
打开myFirstApp/AndroidManifest.xml ,然后填充如下这些权限配置:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.WRITE_SETTINGS" /> <uses-permission android:name="android.permission.DISABLE_KEYGUARD" /> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_CONTACTS" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" /> <uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
声明你的应用程序作为NGN
如果你的编程水平高建议你把应用程序声明为NGN
1、从”Eclipse package explorer”打开”AndroidManifest.xm“选择“Application”标签下点击浏览(上右Name)然后从列表选择”NgnApplication”
架构(Architecture )
该协议栈提供了三个层次的编程:低,中,高。
构建和运行项目之前,您一个看一看讲解如何设置一个NGN项目
底层
此级别允许您直接通过JNI来访问doubango功能。这个水平是最灵活的一个,但是很难控制(but is out of scoop)因为它太难管理。在这个级别所有的功能都在一个单一包:org.doubango.tinyWRAP例如,下面的代码演示了如果注册到SIP/IMS服务器:
final String realm = "sip:doubango.org"; final String privateIdentity = "001"; final String publicIdentity = "sip:[email protected]"; final String password = "my secret"; final String proxyHost = "192.168.0.1"; RegistrationSession registrationSession; // Sip Callback final SipCallback callback = new SipCallback() { @Override public int OnDialogEvent(DialogEvent e) { final SipSession sipSession = e.getBaseSession(); final long sipSessionId = sipSession.getId(); final short code = e.getCode(); switch (code) { case tinyWRAPConstants.tsip_event_code_dialog_connecting: if (registrationSession != null && registrationSession.getId() == sipSessionId) { // Registration in progress } break; case tinyWRAPConstants.tsip_event_code_dialog_connected: if (registrationSession != null && registrationSession.getId() == sipSessionId) { // You are registered } break; case tinyWRAPConstants.tsip_event_code_dialog_terminating: if (registrationSession != null && registrationSession.getId() == sipSessionId) { // You are unregistering } break; case tinyWRAPConstants.tsip_event_code_dialog_terminated: if (registrationSession != null && registrationSession.getId() == sipSessionId) { // You are unregistered } break; } return 0; } @Override public int OnRegistrationEvent(RegistrationEvent e) { // low level events return 0; } }; // Create the SipStack SipStack sipStack = new SipStack(callback, realm, privateIdentity, publicIdentity); // Set Proxy Host and port sipStack.setProxyCSCF(proxyHost, 5060, "UDP", "IPv4"); // Set password sipStack.setPassword(password); if (sipStack.isValid()) { if (sipStack.start()) { registrationSession = new RegistrationSession(sipStack); registrationSession.setFromUri(publicIdentity); // Send SIP register request registrationSession.register_(); } }
中级
这个水平是建立在低水平的。这个水平的主要特点是,它的灵活性,而不过于复杂,所有低级别的功能都封装在全面的API里。例如,如果你想实现一个多层次(multi-stack)(多账户(multi-account))的应用程序,这是不错的选择。
高层次
这个水平是建立在底层之上且以后容易得多。高级别由一组服务有单一的NGN引擎实例管理。每个服务负责特定任务。例如,你有一个服务SIP,一个用于联系人管理,一个网络等等
NGN引擎
该引擎是一个包含所有服务的一个黑盒子。你必须始终通过引擎检索服务。
你还必须通过NGN引擎启动/停止服务。
下面的代码演示了然后实例化一个引擎
//获取一个引擎实例。这个函数总是服务相同的实例 //这意味着,只要你想你可以从你的代码任何地方调用它 final NgnEngine mEngine = NgnEngine.getInstance();
下面的代码演示如何从引擎得到一些服务:
//获取配置服务 INgnConfigurationService mConfigurationService = mEngine.getConfigurationService(); //获取SIP/IMS服务 INgnSipService mSipService = mEngine.getSipService(); //等等等等
下面的代码演示如何启动/停止引擎。
//启动引擎 mEngine.start(); //停止引擎 mEngine.stop(); //启动/停止所有基础服务引擎 /** 你应该在你的Android扩展类定义应用程序全局对象 */
基础服务(Base Service)
所有的NGN服务都继承这个类
联系服务(Contact Service)
联系服务用于检索来自本地地址薄中的联系人
HTTP/HTTPS服务(HTTP/HTTPS Service)
HTTP/HTTPS服务用于检索发送和接收来自远程服务HTTP/HTTPS协议的数据
网络服务(Network Service)
网络服务用于管理WiFi和3G/4G网络连接
声音服务(Sound Service)
该服务是用来播放(铃声,回铃音,警报,…)。使用它之前你必须启动NGN引擎服务。
//获取和实例化NGN引擎 NgnEngine mEngine = NgnEngine.getInstance(); //播放回铃音 mEngine.getSoundService().startRingBackTone(); //停止回铃音 mEngine.getSoundService().stopRingBackTone();
存储服务(Storage Service)
该服务用于管理存储功能
配置服务(Configuration Service)
配置服务用于存储用户偏好。保存所有喜好该服务是持久的,这意味着你可以在应用程序/设备重启找回它。你不应该自己创建或启动此服务。
这项服务的一个实例可以这样检索的:
final INgnConfigurationService mConfigurationService =NgnEngine.getInstance().getConfigurationService();
历史服务(History Service)
该服务用于存储/检索历史事件(音频/视频,消息,…)。你不应该自己创建或启动此服务。
这项服务的一个实例是这样检索的:
final INgnHistoryService mHistoryService = NgnEngine.getInstance().getHistoryService();
SIP/IMS服务(SIP/IMS Service)
该服务用于管理SIP/IMS栈。你不应该自己创建或启动此服务。
这项服务的一个实例可以这样检索:
final INgnSipService mSipService = NgnEngine.getInstance().getSipService();
音频/视频通话(Audio/Video calls)
音频呼叫(Making audio call)
得到音频/视频通话有关通知状态请看这里
final String remoteUri = "+33600000000"; final String validUri = NgnUriUtils.makeValidSipUri(remoteUri);
// sip:+33600000000"@doubango.org NgnAVSession avSession = NgnAVSession.createOutgoingSession( mSipService.getSipStack(), NgnMediaType.Audio); if (avSession.makeCall(validUri)) {
Log.d(TAG, "all is ok"); } else { Log.e(TAG, "Failed to place the call"); }
视频通话(Making video call)
得到音频/视频通话有关通知状态请看这里
final String remoteUri = "+33600000000"; final String validUri = NgnUriUtils.makeValidSipUri(remoteUri); // sip:+33600000000"@doubango.org NgnAVSession avSession = NgnAVSession.createOutgoingSession( mSipService.getSipStack(), NgnMediaType.AudioVideo); if (avSession.makeCall(validUri)) { Log.d(TAG, "all is ok"); } else { Log.e(TAG, "Failed to place the call"); }
短信与聊天(SMS and Chat)
3GPP Binary SMS
// 短信中心 final String SMSC = "sip:[email protected]"; // 对方 final String remotePartyUri = "sip:[email protected]"; final String textToSend = "hello world!"; final NgnMessagingSession imSession = NgnMessagingSession.createOutgoingSession(mSipService.getSipStack(),remotePartyUri); if(!imSession.SendBinaryMessage(textToSend,SMSC)){ Log.e(TAG,"Failed to send"); }else{ Log.d(TAG,"Message sent"); } //发布会话
NgnMessagingSession.releaseSession(imSession);
寻呼机模式IM(Pager Mode IM)
final String textToSend = "hello world!"; // 对方
final String remotePartyUri = "sip:[email protected]"; final NgnMessagingSession imSession = NgnMessagingSession.createOutgoingSession(mSipService.getSipStack(),remotePartyUri); if(!imSession.sendTextMessage(textToSend)){ Log.e(TAG,"Failed to send"); }else{ Log.d(TAG,"Message sent"); } // 发布会话
NgnMessagingSession.releaseSession(imSession);
监听事件(Listening to events)
SIP/IMS服务负责所有任务相关的SIP协议(注册,音频/视频通话,寻呼机模式IM),你可以订阅事件改变当注册状态变化,新的SIP消息被接收,新得到的通知,输入的音频/视频通话,…所有通知通过不止一次异步查询的方式发送给您。
监听状态注册状态改变(Listening for registration state change)
你可以监听得到当你登录/退出时注册状态变化通知
final TextView mTvInfo = (TextView) findViewById(R.id.textViewInfo); final BroadcastReceiver mSipBroadCastRecv = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); // Registration Event if (NgnRegistrationEventArgs.ACTION_REGISTRATION_EVENT.equals(action)) { NgnRegistrationEventArgs args = intent.getParcelableExtra(NgnEventArgs.EXTRA_EMBEDDED); if (args == null) { Log.e(TAG, "Invalid event args"); return; } switch (args.getEventType()) { case REGISTRATION_NOK: mTvInfo.setText("Failed to register :("); break; case UNREGISTRATION_OK: mTvInfo.setText("You are now unregistered :)"); break; case REGISTRATION_OK: mTvInfo.setText("You are now registered :)"); break; case REGISTRATION_INPROGRESS: mTvInfo.setText("Trying to register..."); break; case UNREGISTRATION_INPROGRESS: mTvInfo.setText("Trying to unregister..."); break; case UNREGISTRATION_NOK: mTvInfo.setText("Failed to unregister :("); break; } } } }; final IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(NgnRegistrationEventArgs.ACTION_REGISTRATION_EVENT); registerReceiver(mSipBroadCastRecv, intentFilter);
监听音频/视频通话状态变化(Listening for audio/video call state change)
你可以监听获得音频/视频呼叫状态变更通知,呼叫状态变更(呼入(incoming),正在呼叫(incall),外呼(outgoing),终止(terminated))
final BroadcastReceiver mSipBroadCastRecv = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { InviteState state; final String action = intent.getAction(); if (NgnInviteEventArgs.ACTION_INVITE_EVENT.equals(action)) { NgnInviteEventArgs args = intent.getParcelableExtra(NgnEventArgs.EXTRA_EMBEDDED); if (args == null) { Log.e(TAG, "Invalid event args"); return; } Log.d(TAG,"This is anevent for session number "+ args.getSessionId()); // Retrieve the sessionfrom the store NgnAVSession avSession = NgnAVSession.getSession(args.getSessionId()); if (avSession == null) { Log.e(TAG, "Cannot find session"); return; } switch ((state = avSession.getState())) { case NONE: default: break; case INCOMING: Log.i(TAG, "Incoming call"); break; case INPROGRESS: Log.i(TAG, "Call in progress"); break; case REMOTE_RINGING: Log.i(TAG, "Remote party is ringing"); break; case EARLY_MEDIA: Log.i(TAG, "Early media started"); break; case INCALL: Log.i(TAG, "Call connected"); break; case TERMINATING: Log.i(TAG, "Call terminating"); break; case TERMINATED: Log.i(TAG, "Call terminated"); break; } } } };
配置(Configuration)
试图注册到SIP/IMS服务你必须配置你的凭证。配置服务负责这些任务。使用喜好配置服务都是持久的,这意味着应用程序/设备重新启动。配置信息,你必须得到一个实例这样一个引擎服务:
final INgnConfigurationService mConfigurationService
域(Realm)
域是验证域的名称。它应该是一个有效的SIP URI(例如:sip:open-ims.test or sip:10.0.0.1 )。该域是强制性的,栈开始之前应该设置,一旦栈开始时你不能改变它的值。如果Proxy-CSCF的地址找不到,那么栈会自动使用DNS NAPTR+SRV/或用于动态搜索的DHCP机制。域的值将用作于DNS NAPTR查询的域名中。欲了解更多有关于如何设置CSCF IP地址和端口的信息,请参见第22.1.8
final String myRealm = "sip:doubango.org"; final boolean bSaveNow = true; mConfigurationService(ConfigurationEntry.NETWORK_REALM, myRealm, bSaveNow);
IMS 私有标识(IMPI)(IMS Private Identity (IMPI))
IMS私有标识(又名IMPI)是分配给一个家庭网络用户(或UE)的唯一标识符。它可以是一个SIP URI(例如:sip:[email protected]),一个电话URI(例如,电话:+33100000)或者如何字母数字字符串(例如:[email protected] 或 bob)它被用来验证UE(SIP授权/Proxy-Authorization用户名字段)。在现实世界中,应该存储在UICC(通用集成电路卡)。对于那些使用该IMS协议栈作为基本(IETF)的SIP协议栈,该IMPU验证名称应与他们一致。该IMPI是强制性的,栈开始之前,应该设置。你不应该在栈被启动去改变IMPI。
IMPI被栈启动
final String myIMPI = "33446677887"; final boolean bSaveNow = true; mConfigurationService(ConfigurationEntry.IDENTITY_IMPI, myIMPI, bSaveNow);
IMS公共标识(IMPU)(IMS Public Identity (IMPU))
正如它的名字一样,它是你的公共可见的标识符,你愿意接听电话或者任何需求。一个IMPU可以是一个SIP或者URI(比如电话:tel:+33100000 或 sip:[email protected])。在IMS中,用户可具有相关联的独特的IMPI个数的IMPU。对于那些使用该IMS栈基本的SIP协议栈,该IMPU应该与他们的SIP URI一直。该IMPU是强制性的,栈开始前,一个进行设置。你不应该在栈被启动去改变IMPU。
如果你想在栈开始改变IMPU(而不是,改变P-Preferred-Identity默认的公共标识符)
final boolean bSaveNow = true; final String myIMPU = "sip:[email protected]"; mConfigurationService(ConfigurationEntry.IDENTITY_IMPU, myIMPU, bSaveNow);
首选身份(Preferred Identity)
作为一个用户有多个IMPU,它可以为呼出每个请求,定义IMPU被使用设置首选身份。用户检查该IPMU没有禁止。一个是IMPU如果它没有出现在返回200和相关联的URL被禁止。缺省情况下,首选的身份是在相关联的标识列表中第一个URL。如果IMPU用于注册的用户被禁止,那么栈将使用URI返回默认SCSCF。
你不应该手动设置此SIP(P-Preferred-Identity)它是由栈
Proxy-CSCF 主机地址(Proxy-CSCF Host address)
Proxy-CSCF 主机是SIP的IP地址(192.168.0.1)或FQDN(doubango.org)注册商。你应该在必要时设置代理CSCF地址和IP。动态机制(DNS NAPTR/或DHCPv4/v6)应该被使用。
下面的代码演示了如何设置ProxyCSCF IP地址和端口。如果端口找不到,那么它的默认值是5060
// Sets IP address final String proxyHost = "192.168.0.1"; mConfigurationService(ConfigurationEntry.NETWORK_PCSCF_HOST, proxyHost); // Sets port final int proxyPort = 5060; mConfigurationService.putInt(ConfigurationEntry.NETWORK_PCSCF_PORT, proxyPort); Save changes mConfigurationService.commit();