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
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<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" />
<!-- Các permission liên quan đến internet. -->
<uses-permission android: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-permission android: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-permission android: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-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Cho phép sử dụng microphone. -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Cho phép sử dụng camera. -->
<uses-permission android: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 Audio
portsipSDK.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 Video
portsipSDK.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) ?: return 0
with(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()
return super.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ông
fun onRegisterSuccess(reason: String?, code: Int, sipMessage: String?) {}
// Đăng nhập máy nhánh thất bại
fun 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
class FirebaseService : FirebaseMessagingService() {
companion object {
const val ACTION_REFRESH_PUSH_TOKEN = "REFRESH_PUSH_TOKEN"
const val ACTION_PROCESS_PUSH_PBX = "PROCESS_PUSH_PBX"
const val 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.data
if ***app đang ở trạng thái unactive (kill app, screen off, ...)*** {
Intent(this, PortsipService::class.java).also { intent ->
intent.action = ACTION_PROCESS_PUSH_PBX
startService(intent)
}
}
}
// Khi token của Firebase được reset
override fun onNewToken(newToken: String) {
Intent(this, PortsipService::class.java).also { intent ->
intent.action = ACTION_REFRESH_PUSH_TOKEN
intent.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) as NotificationManager
val callChannel = NotificationChannel(BACKGROUND_INCOMING_CALL_CHANNEL_ID, "Cuộc gọi đến", NotificationManager.IMPORTANCE_HIGH)
callChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
notiManager?.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.
}
return super.onStartCommand(intent, flags, startId)
}
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ọi
portsipSDK.answerCall(sessionId: Long, videoCall: Boolean) {}
//2. Từ chối cuộc gọi
// code nên có giá trị là 486
portsipSDK.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 ra
portsipSDK.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:
class VideoCallActivity: 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.xml
setContentView(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 true
portsipSDK.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:
<FrameLayout xmlns: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 -->
<LinearLayout
android: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.PortSIPVideoRenderer
android:id="@+id/remote_video_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<!-- Local Video: video của user -->
<LinearLayout
android: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.PortSIPVideoRenderer
android: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 true
portsipSDK.call(callee: String, true, videoCall: Boolean)
// 2. Nhận cuộc gọi đến
onInviteIncoming(
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.
}