Bước 2: Giải nén file .zip vừa mới tải về, copy file AndroidSample/SIPSample_AndroidStudio/SIPSample/libs/portsipvoipsdk.jar vào thư mục app/libs của project Android
Bước 3: Compile Libs : Chuột phải vào file portsipvoipsdk.jar chọn Add as Library...
Bước 4: Khai báo các permission cần thiết trong file AndroidManifest.xml
<manifestxmlns:android="http://schemas.android.com/apk/res/android"package="com.example.app"> <uses-permissionandroid:name="android.permission.INTERNET" /> <uses-permissionandroid:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permissionandroid:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permissionandroid:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permissionandroid:name="android.permission.CHANGE_NETWORK_STATE" /><!-- Các permission liên quan đến internet. --> <uses-permissionandroid:name="android.permission.WAKE_LOCK" /><!-- Cho phép ứng dụng có thể unlock điện thoại khi có cuộc gọi đến. --> <uses-permissionandroid:name="android.permission.USE_FULL_SCREEN_INTENT" /><!-- Cho phép ứng dụng có thể mở giao diện fullscreen khi nhận thông báo cuộc gọi đến lúc điện thoại đang ở trạng thái không active (khoá màn hình, kill app, không ở màn hình ứng dụng) --> <uses-permissionandroid:name="android.permission.DISABLE_KEYGUARD" /><!-- Cho phép người dùng có thể thao tác khi máy đã ở trạng thái lock (bấm chấp nhận/từ chối cuộc gọi). --> <uses-permissionandroid:name="android.permission.RECORD_AUDIO" /><!-- Cho phép sử dụng microphone. --> <uses-permissionandroid:name="android.permission.CAMERA" /><!-- Cho phép sử dụng camera. --> <uses-permissionandroid:name="android.permission.VIBRATE" /><!-- Cho phép ứng dụng điều khiển điện thoại rung khi có cuộc gọi đến. --></manifest>
Những ví dụ từ mục II trở đi áp dụng cho 2 file: MainActivity.kt và PortsipService.kt
II. Khởi tạo SDK
Ở file PortsipService.kt
Khai báo biến global val portsipSDK: PortSipSdk = PortSipSdk() . Dùng để gọi các API của PortsipSDK.
Class PortsipService phải implement interface: **OnPortSIPEvent .**
fun initialSDK() {portsipSDK.setOnPortSIPEvent(this)portsipSDK.CreateCallManager(application)portsipSDK.initialize(PortSipEnumDefine.ENUM_TRANSPORT_TCP,"0.0.0.0",10002,PortSipEnumDefine.ENUM_LOG_LEVEL_NONE,"",8,"PortSIP SDK for Android",0,0,"","",false,"")// những cấu hình liên quan đến AudioportsipSDK.addAudioCodec(PortSipEnumDefine.ENUM_AUDIOCODEC_OPUS)portsipSDK.addAudioCodec(PortSipEnumDefine.ENUM_AUDIOCODEC_G729)portsipSDK.addAudioCodec(PortSipEnumDefine.ENUM_AUDIOCODEC_PCMA)portsipSDK.addAudioCodec(PortSipEnumDefine.ENUM_AUDIOCODEC_PCMU)// những cấu hình liên quan đến VideoportsipSDK.addVideoCodec(PortSipEnumDefine.ENUM_VIDEOCODEC_H264)portsipSDK.addVideoCodec(PortSipEnumDefine.ENUM_VIDEOCODEC_VP8)portsipSDK.addAudioCodec(PortSipEnumDefine.ENUM_VIDEOCODEC_VP9)portsipSDK.setVideoBitrate(-1,512)portsipSDK.setVideoFrameRate(-1,20)portsipSDK.setVideoResolution(480,640)portsipSDK.setVideoNackStatus(true)// có thể đặt InstanceId là 1 constant bất kì.portsipSDK.setInstanceId("PORTSIP_INSTANCE_ID")portsipSDK.setLicenseKey("PORTSIP_UC_LICENSE")}
Hàm này nên được gọi chung lúc với đăng nhập máy nhánh.
III. Đăng nhập máy nhánh (extension)
Đầu tiên, ở file MainActivity.kt, ta phải khởi chạy PortsipService trước
// cần lưu các data về extension (số máy nhánh, password, domain tenant, ...) vào storage của máy.// sử dụng SharedPreferences// Vì khi kill app, các data sẽ bị mất nên phải lưu xuống storage những thông tin này.// Giải thích:// số máy nhánh: (ví dụ: 1234, 5678, ...)// password máy nhánh: (ví dụ: abcDefg123, xzyqerTT11, ...)// domain tenant: (ví dụ: voip.example.com, ...)val sharedPref =getSharedPreferences("CallManager",MODE_PRIVATE) ?:return0with(sharedPref.edit()) {putString(getString(R.string.call_manager_ps_extension), số máy nhánh)putString(getString(R.string.call_manager_ps_password), password)putString(getString(R.string.call_manager_ps_domain), domain tenant)apply() }Intent(this, PortsipService::class.java).also { intent ->startService(intent)}
Ở file PortsipService.kt, sau khi startService như bước 1 thì onStartCommand của PortsipService được chạy, đăng nhập máy nhánh có thể được gọi lúc này, được hiện thực như ví dụ sau:
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {registerPortsip()returnsuper.onStartCommand(intent, flags, startId)}fun registerPortsip() { val sharedPref =getSharedPreferences("CallManager",MODE_PRIVATE) val psExtension =sharedPref.getString(getString(R.string.call_manager_ps_extension),"") val psPassword =sharedPref.getString(getString(R.string.call_manager_ps_password),"") val psDomain =sharedPref.getString(getString(R.string.call_manager_ps_domain),"") val pushMessage =sharedPref.getString(getString(R.string.call_manager_push_message),"") ?:""// addSipMessageHeader dùng cho việc nhận cuộc gọi khi ứng dụng đang không active// ví dụ: tắt màn hình, kill app, ...// sẽ trình bày chi tiết ở mục VI.if (pushMessage.isNotEmpty()) {portsipSDK.clearAddedSipMessageHeaders()portsipSDK.addSipMessageHeader(-1,"REGISTER",1,"x-p-push", pushMessage) }portsipSDK.removeUser()var code =portsipSDK.setUser( psExtension, psExtension, psExtension, psPassword, psDomain,"sip.etelecom.vn",5063,"",0,"",0)if (code !=PortSipErrorcode.ECoreErrorNone) {return }portsipSDK.unRegisterServer() code =portsipSDK.registerServer(90,0)}
Nên có 1 biến portsipRegistered dùng để lưu trạng thái đã register với portsip thành công hay chưa. Nếu đã register thành công thì chỉ cần gọi API refreshRegistration(), không cần qua 2 bước initialSDK() và registerPortsip() bên trên.
portsipSDK.refreshRegistration(0)
Nếu muốn register lại từ đầu thì phải Khởi tạo SDK lại như mục II.
Có 2 event sẽ xảy ra khi đăng nhập máy nhánh:
// Đăng nhập máy nhánh thành côngfun onRegisterSuccess(reason: String?, code: Int, sipMessage: String?) {}// Đăng nhập máy nhánh thất bạifun onRegisterFailure(reason: String?, code: Int, sipMessage: String?) {}
_sessionID =portsipSDK.call(callee: String, sendSdp: Boolean, videoCall: Boolean)// _sessionID là 1 biến global, khi một cuộc gọi bất kì được tạo ra thì sẽ có 1 session, biến _sessionID này sẽ lưu lại id của session đó.// callee là số điện thoại user muốn gọi đến.// sendSdp: có truyền lên Session Description Protocol hay không. Thường sẽ để bằng true// videoCall: có thực hiện video call hay không.
Khi người nhận cuộc gọi từ chối cuộc gọi/cuộc gọi không được bắt máy/hay vì 1 lý do nào đó mà cuộc gọi không thành công:
sự kiện onInviteFailure() xảy ra.
fun onInviteFailure(sessionId: Long, reason: String?, code: Int, sipMessage: String?) {}
Chủ động kết thúc cuộc gọi:
portsipSDK.hangUp(sessionID: Long)
VI. Nhận cuộc gọi (incoming call)
Cần áp dụng Mobile Push (push notification voip) để ứng dụng có thể nhận cuộc gọi trong các trường hợp không active:
Người dùng không ở màn hình ứng dụng
Khoá màn hình
Kill app
6.1. Thiết lập mobile push
Để sử dụng mobile push, cần khai báo ứng dụng Android với hệ thống portsip thông qua các bước:
Lấy 2 thông tin: 1 Server Key và 1 Sender ID trên Firebase của Project.
applicationId nằm trong file build.gradle (Module: android.app) ví dụ: vn.etelecom.app
Gửi cho eTelecom 3 thông tin này để tạo thêm Mobile Push.
6.2. Hiện thực
Khi có một cuộc gọi đến máy nhánh:
Sự kiện onInviteIncoming() xảy ra.
onInviteIncoming( sessionId: Long, callerDisplayName: String, caller: String, calleeDisplayName: String, callee: String, audioCodecs: String, videoCodecs: String, existsAudio: Boolean, existsVideo: Boolean, sipMessage: String) {// do something...// Ví dụ:// start 1 Activity hiển thị thông tin cuộc gọi đến}
Tạo 1 service là FirebaseService, và khai báo nó ở AndroidManifest.xml
FirebaseService.kt
classFirebaseService : FirebaseMessagingService() { companion object {constval ACTION_REFRESH_PUSH_TOKEN ="REFRESH_PUSH_TOKEN"constval ACTION_PROCESS_PUSH_PBX ="PROCESS_PUSH_PBX"constval PUSH_TOKEN ="PUSH_TOKEN" }override fun onCreate() {super.onCreate() }// Khi eTelecom bắn 1 notification về việc có cuộc gọi đến máy nhánh của bạn.override fun onMessageReceived(remoteMessage:RemoteMessage) {// trong biến data này sẽ gồm thông tin về caller, callee, có video call hay không, ... val data: Map<String, String>=remoteMessage.dataif***app đang ở trạng thái unactive (kill app, screen off,...)*** {Intent(this, PortsipService::class.java).also { intent ->intent.action =ACTION_PROCESS_PUSH_PBXstartService(intent) } } }// Khi token của Firebase được resetoverride fun onNewToken(newToken:String) {Intent(this, PortsipService::class.java).also { intent ->intent.action =ACTION_REFRESH_PUSH_TOKENintent.putExtra(PUSH_TOKEN, newToken)startService(intent) } }}
Tạo notification channel: để dùng trong việc thông báo cuộc gọi đến. Ta nên gọi hàm này cùng lúc với đăng nhập máy nhánh.
fun initNotification() { notiManager =getSystemService(Context.NOTIFICATION_SERVICE) asNotificationManager val callChannel =NotificationChannel(BACKGROUND_INCOMING_CALL_CHANNEL_ID,"Cuộc gọi đến",NotificationManager.IMPORTANCE_HIGH)callChannel.lockscreenVisibility =Notification.VISIBILITY_PUBLICnotiManager?.createNotificationChannel(callChannel) }
Khi onStartCommand của PortsipService được chạy, đã trình bày chi tiết ở mục III.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {if (intent !=null) {// có thêm 2 tình huống cần xử lý bên dưới đây (bên cạnh tình huống đã được trình bày ở mục III.)// 1. Khi token của Firebase được refresh// 2. Khi kill app, hoặc tắt màn hình điện thoại, cuộc gọi đến sẽ được bắn qua Firebase. }returnsuper.onStartCommand(intent, flags, startId)}
Khi token của Firebase được refresh:
if (intent.action ==FirebaseService.ACTION_REFRESH_PUSH_TOKEN) { firebasePushToken =intent.getStringExtra(FirebaseService.PUSH_TOKEN) ?:""refreshPushToken(true)portsipSDK.refreshRegistration(0)}
Khi kill app, hoặc tắt màn hình điện thoại, cuộc gọi đến sẽ được bắn qua Firebase và sẽ xử lý tại đây:
if (intent.action ==FirebaseService.ACTION_PROCESS_PUSH_PBX) {// Đăng nhập máy nhánh lại, vì khi kill app thì trạng thái đăng nhập sẽ mất đi.registerPortsip()// Sau đó, hiển thị popup notification thông báo cuộc gọi đến.showPendingCallNotification()}
Khi khởi động ứng dụng, lần đầu tiên start của PortsipService, ta phải lắng nghe token của Firebase rồi đăng nhập máy nhánh sau.
if (intent?.action ==null||intent.action!!.isEmpty()) {FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->if (!task.isSuccessful) {Log.w("FIREBASE","Fetching FCM registration token failed",task.exception)return@OnCompleteListener }// Get new FCM registration token firebasePushToken =task.result ?:""refreshPushToken(true)// Trường hợp Firebase init chậm, xảy ra sau khi đăng nhập máy nhánh thành công, ta sẽ cần refreshRegistration lại. portsipSDK.refreshRegistration(0) })registerPortsip()}
Hàm refreshPushToken có thể hiện thực như sau:
fun refreshPushToken(willPush: Boolean) {if (firebasePushToken.isNotEmpty()) {portsipSDK.clearAddedSipMessageHeaders() val pushMessage ="device-os=android;device-uid=$firebasePushToken;allow-call-push=$willPush;allow-message-push=$willPush;app-id=$packageName"portsipSDK.addSipMessageHeader(-1,"REGISTER",1,"x-p-push", pushMessage)// lưu pushMessage vào storage của máy val sharedPref =getSharedPreferences("CallManager",MODE_PRIVATE)with(sharedPref.edit()) {putString(getString(R.string.call_manager_push_message), pushMessage)apply() } }}
Hàm showPendingNotification có thể hiện thực như sau:
fun showPendingCallNotification() {// Cần phải có 1 Activity để hiển thị view Cuộc gọi đến // khi user bấm vào notification hoặc khi màn hình tắt. val fullScreenIntent =Intent(this, IncomingActivity::class.java)fullScreenIntent.flags =Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK val fullScreenPendingIntent =PendingIntent.getActivity(this,0, fullScreenIntent,PendingIntent.FLAG_UPDATE_CURRENT) val notification =Notification.Builder(this,BACKGROUND_INCOMING_CALL_CHANNEL_ID).setSmallIcon(R.drawable.ic_notification).setColor(Color.argb(1,47,204,112)).setContentTitle("Cuộc gọi đến từ").setContentText(Html.fromHtml("<strong>${CallManager.callerParsed}</strong>",Html.FROM_HTML_MODE_LEGACY)).setOngoing(true).setShowWhen(true).setContentIntent(fullScreenPendingIntent).setFullScreenIntent(fullScreenPendingIntent,true).build()notiManager?.notify(BACKGROUND_INCOMING_CALL_NOTIFICATION, notification)}
Một số thao tác user có thể thực hiện khi có cuộc gọi: Chấp nhận (bắt máy), Từ chối, Ngắt máy (sau khi đã chấp nhận), v.v....
//1. Chấp nhận cuộc gọiportsipSDK.answerCall(sessionId: Long, videoCall: Boolean) {}//2. Từ chối cuộc gọi// code nên có giá trị là 486portsipSDK.rejectCall(sessionId: Long, code: Int) {}//3. Ngắt máy sau khi cuộc gọi đã được chấp nhận và đang diễn raportsipSDK.hangUp(sessionId: Long) {}
VII. Cuộc gọi video
Cần có 1 Activity thể hiện cuộc gọi Video, ví dụ như sau:
classVideoCallActivity: Activity(), View.OnClickListener {// video của user var localVideo// video của người đang đàm thoại với user var remoteVideo// khi cuộc gọi video được bắt đầu (1 trong caller hoặc callee chấp nhận cuộc gọi từ người còn lại)override fun onCreate(savedInstanceState:Bundle?) {super.onCreate(savedInstanceState)// phần giao diện của Activity này, được hiện thực trong file video_call_view.xmlsetContentView(R.layout.video_call_view) localVideo =findViewById(R.id.local_video_view) remoteVideo =findViewById(R.id.remote_video_view)portsipSDK.setRemoteVideoWindow(sessionId: Long, remoteVideo)portsipSDK.displayLocalVideo(true,true, localVideo)// nếu user tắt cam thì tham số send bằng false, ngược lại thì bằng trueportsipSDK.sendVideo(sessionId: Long, send: Boolean) }// khi cuộc gọi kết thúc, Activity bị dismiss thì cần release các video này ra.override fun onDestroy() {super.onDestroy()portsipSDK.displayLocalVideo(false,false,null)localVideo.release()portsipSDK.setRemoteVideoWindow(sessionId: Long,null)remoteVideo.release() }}
Nên dùng biến portsipSDK ở file PortsipService.kt để không xảy ra hiện tượng nhiều instance của 1 biến được tạo ra, sẽ phát sinh nhiều bug không mong muốn.
Giao diện của Activity bên trên, có thể hiện thực ở file video_call_view.xml như ví dụ sau:
<FrameLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:orientation="vertical"android:background="#EEEEEE"android:layout_width="match_parent"android:layout_height="match_parent"><!-- Remote Video: video của người đang đàm thoại với user --> <LinearLayoutandroid:id="@+id/remote_video_view_wrapper"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:background="#333"android:orientation="vertical"> <com.portsip.PortSIPVideoRendererandroid:id="@+id/remote_video_view"android:layout_width="wrap_content"android:layout_height="wrap_content"/> </LinearLayout><!-- Local Video: video của user --> <LinearLayoutandroid:id="@+id/local_video_view_wrapper"android:layout_width="120dp"android:layout_height="166dp"android:layout_alignParentTop="true"android:layout_marginTop="70dp"android:layout_alignParentEnd="true"android:layout_marginEnd="22dp"android:clipChildren="true"android:background="@drawable/transparent_rounded_corner"> <com.portsip.PortSIPVideoRendererandroid:id="@+id/local_video_view"android:layout_width="wrap_content"android:layout_height="wrap_content"/> </LinearLayout></FrameLayout>
Gọi ra và nhận cuộc gọi đến:
// 1. Gọi ra, với videoCall bằng trueportsipSDK.call(callee: String,true, videoCall: Boolean)// 2. Nhận cuộc gọi đếnonInviteIncoming( sessionId: Long, callerDisplayName: String, caller: String, calleeDisplayName: String, callee: String, audioCodecs: String, videoCodecs: String, existsAudio: Boolean, existsVideo: Boolean, sipMessage: String) {// existsVideo để cho biết cuộc gọi hiện tại có phải là videoCall hay không.}