Android
I. Thiết lập Android Studio project
Bước 1: Tải portsipSDK tại đây.
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>
Tài liệu tham khảo: https://www.portsip.com/docs/sdk/manual/portsip-voip-sdk-user-manual-android.pdf
II. Khởi tạo SDK
Ở file PortsipService.kt
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?) {}
IV. Đăng xuất máy nhánh (extension)
Ở file PortsipService.kt
portsipSDK.removeUser()
portsipSDK.unRegisterServer()
V. Thực hiện cuộc gọi (outgoing call)
Ở file PortsipService.kt
Để thực hiện cuộc gọi:
_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 chấp nhận cuộc gọi:
Sự kiện onInviteAnswered()
xảy ra.
fun onInviteAnswered(
sessionId: Long,
callerDisplayName: String,
caller: String,
calleeDisplayName: String,
callee: String,
audioCodecNames: String,
videoCodecNames: String,
existsAudio: Boolean,
existsVideo: Boolean,
sipMessage: String
) {}
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 filebuild.gradle (Module: android.app)
ví dụ: vn.etelecom.appGử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
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)
}
}
}
<service android:name=".FirebaseService" android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
build.gradle(Project: android)
, thêm classpath sau đây:
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.3.8'
}
}
build.gradle(Module: android.app)
, thêm dependencies như sau:
apply plugin: 'com.google.gms.google-services'
dependencies {
implementation platform('com.google.firebase:firebase-bom:28.1.0')
implementation 'com.google.firebase:firebase-messaging'
}
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 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ọ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.
}
Last updated