# **Chapter 16: Building a SIP Client in SartajPHP Mobile App**
## Building a SIP Client in SartajPHP Mobile App
In this chapter, we will develop a **fully working SIP VoIP client** inside a SartajPHP Mobile Application using:
* PJSIP (pjsua2 Android binding)
* SartajPHP KotlinGate Engine
* WebView UI (Onsen UI + Bootstrap + FontAwesome)
* Event-driven communication between Web UI and Kotlin
By the end of this chapter, you will be able to:
✅ Register SIP account
✅ Make outgoing calls
✅ Receive incoming calls
✅ Answer / Reject calls
✅ Disconnect active calls
✅ Connect SIP engine with Web UI
---
# 📌 16.1 SIP Architecture in SartajPHP
Unlike traditional mobile SIP apps, SartajPHP separates logic into:
```
Frontend (HTML / JS / Onsen UI)
↓
SartajPHP Router
↓
KotlinGate Controller
↓
SipEngine (Native PJSIP Core)
```
---
# 📌 16.2 Required Components
We will create:
| File | Purpose |
| ------------- | ---------------------------- |
| Index.kt | Home controller |
| SipApp.kt | SIP call controller |
| SipEngine.kt | Core SIP handler |
| SipAccount.kt | Registration + Incoming call |
| SipCall.kt | Call media + audio routing |
| sip.front | Dial pad UI |
| sip.js | UI event handling |
---
# 📌 16.3 Installing PJSIP Library
Download and compile PJSIP library:
Copy Compiled library into:
```
app/libs
```
Load library in engine:
```kotlin
System.loadLibrary("pjsua2")
```
```kotlin
// File apps/kotlin_apps/IndexGate.kt
package com.sartajphp.gate
import android.util.Log
import com.sartajphp.webviewlib.KotlinGate
import com.sartajphp.webviewlib.JSServerC
import org.json.JSONObject
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import kotlin.concurrent.thread
import org.pjsip.pjsua2.*
import com.sartajphp.apps.engine.SipEngine
class Index: KotlinGate(){
/** on start Life Cycle Event of KotlinGate
Set All Permissions of MobileApp and then create SipEngine Object */
override suspend fun onstart(){
if (!getAuthenticate("ADMIN")) {
JSServer.addJSONHTMLBlock("spnstatus", "Permission required")
JSServer.flush()
//KotlinApi.println("Permission Denied");
//KotlinApi.setMsg("index","Permission Denied")
forward("signin","","",data)
return
}
JSServer.addJSONHTMLBlock("spnstatus", "Permission Passed")
JSServer.flush()
// if already registered
if(KotlinApi.getProp("sip_register") == true){
JSServer.addJSONJSBlock("onsen.loadPage(\"sippage.html\");")
JSServer.flush()
exitMe()
}else{
// make sure that property available
KotlinApi.addProp("sip_register",false)
}
}
override suspend fun onrun(){
// make sure create Sip Engine Object before further processing
var eng1 = getSipEngine()
eng1.setSphpApi(KotlinApi)
JSServer.addJSONHTMLBlock("spnstatus", "SIP Engine Ready")
JSServer.flush()
KotlinApi.println("Permission Passed3")
}
/** When Form Submit with user,pass then regsiter Sip Server and open Page sip dialer */
suspend fun page_event_register(evtp:String){
// stop change user if already regsitered
if(KotlinApi.getProp("sip_register") == false){
val user = data["user"].toString()
val pass = data["pass"].toString()
val domain = data["domain"].toString()
val engine = getSipEngine()
engine.start(user,pass,domain)
// save status,User is regsitered
KotlinApi.addProp("sip_register",true);
}
JSServer.addJSONHTMLBlock("spnstatus", "SIP Engine Registered")
JSServer.flush()
// load sip dialer page
//JSServer.addJSONJSBlock("onsen.loadPage(\"sippage.html\");")
//JSServer.flush()
}
private suspend fun getSipEngine(): SipEngine {
if (!KotlinApi.isProp("sip_engine")) {
val eng = SipEngine()
KotlinApi.addProp("sip_engine", eng)
}
return KotlinApi.getProp("sip_engine") as SipEngine
}
}
```
```kotlin
// File apps/kotlin_apps/SipGate.kt
package com.sartajphp.gate
import com.sartajphp.webviewlib.KotlinGate
import org.pjsip.pjsua2.*
import com.sartajphp.gate.engine.SipEngine
class SipGate : KotlinGate() {
override suspend fun onstart(){
if(KotlinApi.getAuthenticateType() != "ADMIN"){
// stop further processing
forward("index","","",data)
}
}
suspend fun isRegistered(): Boolean{
if(KotlinApi.getProp("sip_register") == false){
// load Home page
JSServer.addJSONJSBlock("onsen.loadPage(\"home.html\");")
return false;
}
return true;
}
suspend fun page_event_call(evtp:String){
val engine = KotlinApi.getProp("sip_engine") as SipEngine
engine.makeCall(data["number"].toString())
JSServer.addJSONHTMLBlock("spnsip", "Call Start")
JSServer.flush()
}
suspend fun page_event_disconnect(evtp:String){
val engine = KotlinApi.getProp("sip_engine") as SipEngine
engine.disconnectCall()
JSServer.addJSONHTMLBlock("spnsip", "Call Ended")
JSServer.flush()
}
suspend fun page_event_answer(evtp:String){
val engine = KotlinApi.getProp("sip_engine") as SipEngine
engine.answerCall()
JSServer.addJSONHTMLBlock("spnsip", "Call Start")
JSServer.flush()
}
suspend fun page_event_rejectCall(evtp:String){
val engine = KotlinApi.getProp("sip_engine") as SipEngine
engine.rejectCall()
JSServer.addJSONHTMLBlock("spnsip", "Call Ended")
JSServer.flush()
}
suspend fun page_event_hold(evtp:String){
val engine = KotlinApi.getProp("sip_engine") as SipEngine
engine.holdCall()
JSServer.addJSONHTMLBlock("spnsip", "Call Hold")
JSServer.flush()
}
suspend fun page_event_resume(evtp:String){
val engine = KotlinApi.getProp("sip_engine") as SipEngine
engine.resumeCall()
JSServer.addJSONHTMLBlock("spnsip", "Call Resume")
JSServer.flush()
}
suspend fun page_event_mute(evtp:String){
val engine = KotlinApi.getProp("sip_engine") as SipEngine
engine.muteCall()
JSServer.addJSONHTMLBlock("spnsip", "Call Mute")
JSServer.flush()
}
suspend fun page_event_unmute(evtp:String){
val engine = KotlinApi.getProp("sip_engine") as SipEngine
engine.unmuteCall()
JSServer.addJSONHTMLBlock("spnsip", "Call Unmute")
JSServer.flush()
}
suspend fun page_event_transfer(evtp:String){
val engine = KotlinApi.getProp("sip_engine") as SipEngine
engine.transferCall(data["number"].toString())
JSServer.addJSONHTMLBlock("spnsip", "Call Transfered")
JSServer.flush()
}
}
```
---
# 📌 16.4 Creating SIP Engine Core
Create:
```kotlin
// File apps/kotlin_apps/engine/SipEngine.kt
package com.sartajphp.gate.engine
import android.util.Log
import com.sartajphp.webviewlib.SphpKotlinApi
import org.pjsip.pjsua2.*
class SipEngine {
var KotlinApi: SphpKotlinApi? = null
var endpoint: Endpoint? = null
var account: SipAccount? = null
var currentCall: SipCall? = null
var domain:String = "sip2sip.info"
var isMuted:Boolean = false
var isHold:Boolean = false
fun setSphpApi(sapi:SphpKotlinApi){
KotlinApi = sapi
}
// ----------------------------
// Initialize SIP Stack
// ----------------------------
fun start(username: String, password: String, sipdomain:String) {
try {
Log.d("SIP_ENGINE", "Start Registration")
domain = sipdomain
System.loadLibrary("pjsua2")
endpoint = Endpoint()
endpoint!!.libCreate()
val epConfig = EpConfig()
epConfig.logConfig.level = 6
epConfig.logConfig.consoleLevel = 6
endpoint!!.libInit(epConfig)
val sipTpConfig = TransportConfig()
sipTpConfig.port = 5060
endpoint!!.transportCreate(
pjsip_transport_type_e.PJSIP_TRANSPORT_UDP,
sipTpConfig
)
endpoint!!.libStart()
Log.d("SIP_ENGINE", "Endpoint started")
registerAccount(username, password)
} catch (e: Exception) {
Log.e("SIP_ENGINE", "Start failed: ${e.message}")
}
}
// ----------------------------
// Registration
// ----------------------------
private fun registerAccount(username: String, password: String) {
val accCfg = AccountConfig()
accCfg.videoConfig.autoTransmitOutgoing = false
accCfg.videoConfig.autoShowIncoming = false
accCfg.idUri = "sip:$username@$domain"
accCfg.regConfig.registrarUri = "sip:$domain"
val cred = AuthCredInfo(
"digest",
"*",
username,
0,
password
)
accCfg.sipConfig.authCreds.add(cred)
accCfg.natConfig.iceEnabled = true
accCfg.natConfig.iceAggressiveNomination = true
accCfg.natConfig.sipOutboundUse = 1
accCfg.natConfig.contactRewriteUse = 1
account = SipAccount(this)
account!!.create(accCfg)
Log.d("SIP_ENGINE", "Registering account")
}
// ----------------------------
// Make Call
// ----------------------------
fun makeCall(dest: String) {
if (account == null) return
if (currentCall != null) {
Log.d("SIP_ENGINE", "Call already active")
return
}
currentCall = SipCall(this,account!!)
val prm = CallOpParam(true)
prm.opt.audioCount = 1
prm.opt.videoCount = 0
val uri = "sip:$dest@$domain"
Log.d("SIP_ENGINE", "Dialing -> $uri")
currentCall!!.makeCall(uri, prm)
}
// ----------------------------
// Disconnect Call
// ----------------------------
fun disconnectCall() {
try {
if (currentCall == null) return
val prm = CallOpParam()
prm.statusCode = pjsip_status_code.PJSIP_SC_DECLINE
//prm.statusCode = pjsip_status_code.PJSIP_SC_OK
currentCall!!.hangup(prm)
Log.d("SIP_ENGINE", "Call disconnected")
} catch (e: Exception) {
Log.e("SIP_ENGINE", "Disconnect failed: ${e.message}")
}
}
fun onIncomingCall(call: SipCall) {
Log.d("SIP_ENGINE", "Incoming call...")
KotlinApi!!.JSServer.addJSONHTMLBlock("spnsip", "Call RX")
KotlinApi!!.JSServer.callJsFunction("ringcall", "Number")
KotlinApi!!.JSServer.flush()
// Here you can:
// 1. Ring UI
// 2. Notify WebView
// 3. Auto answer (optional)
}
fun answerCall() {
currentCall?.let {
val prm = CallOpParam()
prm.statusCode = pjsip_status_code.PJSIP_SC_OK
it.answer(prm)
Log.d("SIP_ENGINE", "Call answered")
}
}
fun rejectCall() {
currentCall?.let {
val prm = CallOpParam()
prm.statusCode = pjsip_status_code.PJSIP_SC_DECLINE
it.hangup(prm)
Log.d("SIP_ENGINE", "Call rejected")
}
}
fun holdCall() {
try {
currentCall?.let {
val prm = CallOpParam()
it.setHold(prm)
isHold = true
Log.d("SIP_ENGINE", "Call on HOLD")
}
} catch (e: Exception) {
Log.e("SIP_ENGINE", "Hold failed: ${e.message}")
}
}
fun resumeCall() {
try {
currentCall?.let {
val prm = CallOpParam(true)
prm.opt.audioCount = 1
prm.opt.videoCount = 0
it.reinvite(prm)
isHold = false
Log.d("SIP_ENGINE", "Call RESUMED")
}
} catch (e: Exception) {
Log.e("SIP_ENGINE", "Resume failed: ${e.message}")
}
}
fun muteCall() {
try {
currentCall?.let {
val audDevManager = Endpoint.instance().audDevManager()
val callInfo = it.info
for (i in 0 until callInfo.media.size) {
val media = it.getMedia(i.toLong()) ?: continue
if (callInfo.media[i].type == pjmedia_type.PJMEDIA_TYPE_AUDIO) {
val audioMedia = AudioMedia.typecastFromMedia(media)
audDevManager.captureDevMedia.stopTransmit(audioMedia)
isMuted = true
Log.d("SIP_ENGINE", "Mic MUTED")
}
}
}
} catch (e: Exception) {
Log.e("SIP_ENGINE", "Mute failed: ${e.message}")
}
}
fun unmuteCall() {
try {
currentCall?.let {
val audDevManager = Endpoint.instance().audDevManager()
val callInfo = it.info
for (i in 0 until callInfo.media.size) {
val media = it.getMedia(i.toLong()) ?: continue
if (callInfo.media[i].type == pjmedia_type.PJMEDIA_TYPE_AUDIO) {
val audioMedia = AudioMedia.typecastFromMedia(media)
audioMedia.startTransmit(audDevManager.captureDevMedia)
isMuted = false
Log.d("SIP_ENGINE", "Mic UNMUTED")
}
}
}
} catch (e: Exception) {
Log.e("SIP_ENGINE", "Unmute failed: ${e.message}")
}
}
fun transferCall(dest: String) {
try {
if (currentCall == null) return
val uri = "sip:$dest@$domain"
val prm = CallOpParam(true)
prm.opt.audioCount = 1
prm.opt.videoCount = 0
currentCall!!.xfer(uri,prm)
Log.d("SIP_ENGINE", "Call transferred to $uri")
} catch (e: Exception) {
Log.e("SIP_ENGINE", "Transfer failed: ${e.message}")
}
}
fun attendedTransfer(otherCall: SipCall) {
try {
val prm = CallOpParam(true)
prm.opt.audioCount = 1
prm.opt.videoCount = 0
currentCall?.xferReplaces(otherCall,prm)
Log.d("SIP_ENGINE", "Attended transfer done")
} catch (e: Exception) {
Log.e("SIP_ENGINE", "Attended transfer failed: ${e.message}")
}
}
// ----------------------------
// Stop Engine
// ----------------------------
fun stop() {
endpoint?.libDestroy()
endpoint = null
}
}
```
```kotlin
// File apps/kotlin_apps/engine/SipAccount.kt
package com.sartajphp.gate.engine
import android.util.Log
import org.pjsip.pjsua2.*
class SipAccount(val engine: SipEngine) : Account() {
override fun onRegState(prm: OnRegStateParam?) {
val info = this.info
Log.d(
"SIP_ACCOUNT",
"Registration: ${info.regIsActive} code=${info.regStatus}"
)
}
override fun onIncomingCall(prm: OnIncomingCallParam?) {
Log.d("SIP_ACCOUNT", "Incoming call")
val call = SipCall(engine,this, prm!!.callId)
engine.currentCall = call
engine.onIncomingCall(call)
}
}
```
```kotlin
// File apps/kotlin_apps/engine/SipCall.kt
package com.sartajphp.gate.engine
import android.util.Log
import org.pjsip.pjsua2.*
class SipCall(
private val engine: SipEngine,
acc: Account,
callId: Int = -1
) : Call(acc, callId) {
override fun onCallState(prm: OnCallStateParam?) {
val info = this.info
Log.d("SIP_CALL", "State: ${info.stateText}")
if (info.state == pjsip_inv_state.PJSIP_INV_STATE_DISCONNECTED) {
engine.isMuted = false
engine.isHold = false
engine.currentCall = null
delete()
}
}
override fun onCallMediaState(prm: OnCallMediaStateParam?) {
val callInfo = info
val ep = Endpoint.instance()
val audDevManager = ep.audDevManager()
for (i in 0 until callInfo.media.size) {
val media = getMedia(i.toLong()) ?: continue
if (callInfo.media[i].type == pjmedia_type.PJMEDIA_TYPE_AUDIO &&
callInfo.media[i].status == pjsua_call_media_status.PJSUA_CALL_MEDIA_ACTIVE
) {
val audioMedia = AudioMedia.typecastFromMedia(media)
audioMedia.startTransmit(audDevManager.captureDevMedia)
audDevManager.playbackDevMedia.startTransmit(audioMedia)
Log.d("SIP_CALL", "Audio connected")
}
}
}
}
```
These classes manages:
* Endpoint initialization
* Account registration
* Call handling
* NAT configuration
---
## ⭐ SIP Engine Initialization
```kotlin
fun start(username: String, password: String, sipdomain:String)
```
This function:
1. Creates SIP endpoint
2. Configures STUN
3. Starts SIP transport
4. Registers SIP account
---
## ⭐ STUN Configuration
```kotlin
epConfig.uaConfig.stunServer.add("stun.l.google.com:19302")
```
This enables NAT traversal.
---
## ⭐ Transport Creation
```kotlin
endpoint!!.transportCreate(
pjsip_transport_type_e.PJSIP_TRANSPORT_UDP,
sipTpConfig
)
```
UDP transport is commonly used in SIP VoIP.
---
# 📌 16.5 SIP Account Registration
Registration is handled using:
```kotlin
AccountConfig()
```
---
## ⭐ Registration Setup
```kotlin
accCfg.idUri = "sip:$username@$domain"
accCfg.regConfig.registrarUri = "sip:$domain"
```
---
## ⭐ Authentication Credentials
```kotlin
val cred = AuthCredInfo(
"digest",
"*",
username,
0,
password
)
```
---
## ⭐ NAT Optimization
```kotlin
accCfg.natConfig.iceEnabled = true
accCfg.natConfig.contactRewriteUse = 1
```
These improve connectivity across networks.
---
# 📌 16.6 Creating SIP Account Listener
Create:
```
SipAccount.kt
```
This listens to:
* Registration events
* Incoming call events
---
## ⭐ Incoming Call Listener
```kotlin
override fun onIncomingCall(prm: OnIncomingCallParam?)
```
This method:
1. Creates new SipCall object
2. Stores active call
3. Notifies UI
---
# 📌 16.7 Creating Call Handler
Create:
```
SipCall.kt
```
This class manages:
* Call states
* Media connection
* Audio routing
---
## ⭐ Audio Connection
```kotlin
audioMedia.startTransmit(audDevManager.captureDevMedia)
audDevManager.playbackDevMedia.startTransmit(audioMedia)
```
This connects:
* Microphone → Remote
* Remote → Speaker
---
# 📌 16.8 Making Outgoing Calls
Inside SipEngine:
```kotlin
fun makeCall(dest: String)
```
---
## ⭐ Call Example
```kotlin
val uri = "sip:$dest@$domain"
currentCall!!.makeCall(uri, prm)
```
---
# 📌 16.9 Disconnecting Calls
```kotlin
fun disconnectCall()
```
```kotlin
currentCall!!.hangup(prm)
```
---
# 📌 16.10 Answering Incoming Calls
```kotlin
fun answerCall()
```
```kotlin
prm.statusCode = PJSIP_SC_OK
```
---
# 📌 16.11 Rejecting Calls
```kotlin
fun rejectCall()
```
```kotlin
prm.statusCode = PJSIP_SC_DECLINE
```
---
# 📌 16.12 Creating SIP Controller (SipApp.kt)
SipGate acts as WebView bridge.
---
## ⭐ Outgoing Call Event
```kotlin
suspend fun page_event_call(evtp:String)
```
---
## ⭐ Disconnect Event
```kotlin
suspend fun page_event_disconnect(evtp:String)
```
---
## ⭐ Answer Event
```kotlin
suspend fun page_event_answer(evtp:String)
```
---
## ⭐ Reject Event
```kotlin
suspend fun page_event_rejectCall(evtp:String)
```
---
# 📌 16.13 Designing Dial Pad UI
Create:
```
sip.front
```
---
## ⭐ Dial Pad Layout
```html
<input id="number"/>
<button id="btnrx">Call</button>
<button id="btncancel">Disconnect</button>
```
---
# 📌 16.14 Handling UI Events
Create:
```
sip.js
```
---
## ⭐ Initialize SIP Page
```javascript
function comp_sippage_init(eventer){
callKotlinApp("sipapp",{});
}
```
---
## ⭐ Call Button Logic
```javascript
callKotlinAppEvent('sipapp','call','',{number:$('#number').val()})
```
---
## ⭐ Disconnect Logic
```javascript
callKotlinAppEvent('sipapp','disconnect')
```
---
## ⭐ Incoming Call Ring UI
```javascript
function ringcall(number1){
alert("Call Rx from " + number1);
}
```
---
# 📌 16.15 Connecting Kotlin and Web UI
SipEngine notifies UI using:
```kotlin
KotlinApi!!.JSServer.callJsFunction("ringcall", "Number")
```
---
# 📌 16.16 Application Flow
---
## ⭐ Registration Flow
```
User Login
↓
Index.kt
↓
SipEngine.start()
↓
Account Registered
```
---
## ⭐ Outgoing Call Flow
```
Dial Pad → JS
↓
SipApp.kt
↓
SipEngine.makeCall()
```
---
## ⭐ Incoming Call Flow
```
SIP INVITE Received
↓
SipAccount.onIncomingCall()
↓
SipEngine.onIncomingCall()
↓
JS Ring UI
```
---
# 📌 16.17 NAT Handling Tips
VoIP often fails across networks. Use:
* STUN servers
* ICE support
* Contact rewrite
* SIP outbound
---
# 📌 16.18 Debugging SIP Calls
Increase logging:
```kotlin
epConfig.logConfig.level = 6
```
Monitor:
```
Registration Status
Call State
RTP Media Connection
```
---
# 📌 16.19 Testing Setup
Recommended testing:
| Device | Tool |
| ------- | ------------------- |
| PC | X-Lite |
| Android | SartajPHP Gate |
| Server | Asterisk / ViciDial |
---
# 📌 16.20 Chapter Summary
In this chapter you learned:
✔ How to integrate PJSIP with SartajPHP
✔ How to build SIP Engine
✔ How to connect Web UI with Native SIP
✔ How to handle incoming and outgoing calls
✔ How to route audio streams
✔ How to manage NAT VoIP problems
---