cancel
Showing results for 
Search instead for 
Did you mean: 
cancel
1343
Views
6
Helpful
11
Replies

WebRTC to CUBE(SBC) Integration

jvcumpa11
Level 1
Level 1

Hello everyone, has anyone successfully deployed WebRTC integration with SBC/CUBE. If so, could you please share a sample configuration and any relevant Cisco documentation links? Thank you.

1 Accepted Solution

Accepted Solutions

collinks2
Level 5
Level 5

cucm_asterisk_sip_trunk_status.PNGInstallation of Asterisk

Step 1: Install the dependencies
sudo apt-get update
sudo apt-get install -y build-essential libxml2-dev libncurses5-dev libssl-dev libjansson-dev libsqlite3-dev uuid-dev

 

Step 2: Download the source code
Navigate to /usr/src and download the latest version of Asterisk 22:
sudo wget http://downloads.asterisk.org/pub/telephony/asterisk/asterisk-22-current.tar.gz

cd /usr/src
tar -xzvf asterisk-22-current.tar.gz

Step 3: Compile Asterisk
sudo tar -xvzf asterisk-21-current.tar.gz
cd asterisk-22*/
sudo contrib/scripts/install_prereq install
sudo ./configure
sudo make menuselect
sudo make && sudo make install
sudo make samples
sudo make config


Step 4:
sudo systemctl start asterisk
systemctl status asterisk
sudo asterisk -rvvv


Step 5: Test if Asterisk is running
Start Asterisk in the foreground to verify that it’s working:

sudo asterisk -vvvvvvvvgc

Step 6: Stop Asterisk
From the Asterisk CLI, stop the process:

cli core stop now

Step 7: Start Asterisk in the background
Run Asterisk as a background process:

sudo asterisk

Step 8: Connect to the Asterisk Console
To connect to the running Asterisk console:
sudo asterisk -r
You can press ctrl z to exit

 

Step 9 ***Creating certificates**

sudo openssl req -nodes -newkey rsa:2048 -keyout /etc/asterisk/keys//asterisk.key -out /etc/asterisk/keys/asterisk.csr
enter your common name and other details when prompted

Step 10:
grant permission
sudo chown asterisk:asterisk /etc/asterisk/keys/asterisk.crt
sudo chown asterisk:asterisk /etc/asterisk/keys/asterisk.pem
sudo chmod 644 /etc/asterisk/keys/asterisk.crt
sudo chmod 600 /etc/asterisk/keys/asterisk.pem

Step 11:optional (ref:
https://vitalpbx.com/blog/asterisk-webrtc-from-scratch/?srsltid=AfmBOoqsJzR4lmLpmihFfwLDBXxPqwiAcTjdMNz11rcrckxodCfPl4Em)

Creat a user asterisk and add to the group
groupadd asterisk
useradd -r -d /var/lib/asterisk -g asterisk asterisk
usermod -aG audio,dialout asterisk
chown asterisk. -R /etc/asterisk
chown asterisk. -R /var/{lib,log,spool}/asterisk
chown -R asterisk.asterisk /usr/lib64/asterisk

Step 12: modify the http.conf
sudo nano /etc/asterisk/http.conf
add this
[general]
enabled=yes
bindaddr=0.0.0.0
bindport=8088
tlsenable=yes
tlsbindaddr=0.0.0.0:8089
tlscertfile=/etc/asterisk/keys/asterisk.cer (replace with your cert)
tlsprivatekey=/etc/asterisk/keys/asterisk.key (replace with your cert)

Step 13: modify the pjsip.conf for webrtc and cucm-trunk
you can replace the cucm-trunk with the cube ip
**defining websocket secure transport**
[transport-wss]
type=transport
protocol=wss
bind=0.0.0.0:8089
cert_file=/etc/asterisk/keys/asterisk.cer
priv_key_file=/etc/asterisk/keys/asterisk.key

** webrtc section**
[webcall]
type=endpoint
context=default
disallow=all
direct_media=no
allow=ulaw,alaw,gsm,h264,VP8
webrtc=yes
aors=webcall
auth=webcall_auth

; Make Asterisk trust CallerID from the client
trust_id_inbound=yes
trust_id_outbound=yes
send_pai=yes
send_rpid=yes

dtls_auto_generate_cert=yes
rtp_engine=asterisk
use_avpf=yes
ice_support=yes
force_rport=yes
rewrite_contact=yes

[webcall_auth]
type=auth
auth_type=userpass
password=your password
username=webcall

[webcall]
type=aor
max_contacts=100


[CUCM-Trunk]
type=endpoint
transport=transport-udp ; or transport-tcp if CUCM requires TCP
context=from-external
disallow=all
allow=ulaw,alaw,gsm,h264,VP8
aors=CUCM-Trunk
outbound_auth=CUCM-Auth
direct_media=no

[CUCM-Trunk]
type=aor
contact=sip:IP:5060 ; CUCM or CUBE IP and SIP Port

[CUCM-Auth]
type=auth
auth_type=userpass
username=
password= ; Must match CUCM OR CUBE SIP trunk configuration

Step 14: modify extensions.conf
[default]
exten => 7100,1,NoOp(Forwarding Call to CUCM IVR)
same => n,Dial(PJSIP/7100@CUCM-Trunk)
same => n,Hangup()
step 15:
Connect to the Atserik console
sudo asterisk -r
pjsip reload
dialplan reload
module reload http
step 16 :verify if Asterik is running on 8089
sudo ss -tulnp | grep -E '8088|8089|5060'


step 17. setting up webrtc client using javascript
modify with your real parameters
<!DOCTYPE html>
<html>
<head>
<title>Collnetwork Care</title>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="{{ url_for('static', filename='sip.min.js') }}"></script>
<style>
body {
background-color: #f0f2f5;
font-family: sans-serif;
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 20px;
}

.keypad {
display: grid;
grid-template-columns: repeat(3, 80px);
gap: 15px;
justify-content: center;
margin-top: 20px;
}

.keypad button {
width: 80px;
height: 80px;
font-size: 24px;
font-weight: bold;
border-radius: 50%;
border: none;
background-color: #007bff;
color: white;
cursor: pointer;
transition: background-color 0.3s;
}

.keypad button:hover {
background-color: #0056b3;
}

/* Overlay keypad on video */
#dtmfKeypad {
position: absolute;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.9);
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 20;
}


.header {
margin-bottom: 20px;
}

.logo {
height: 50px;
}

.call-container {
position: relative;
width: 100%;
max-width: 900px;
background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 20px;
}

.main-video-wrapper {
width: 100%;
height: 500px;
background-color: #000;
border-radius: 8px;
overflow: hidden;
}

#remoteVideo {
width: 100%;
height: 100%;
object-fit: contain;
}

.local-video-wrapper {
position: absolute;
top: 30px;
right: 30px;
width: 200px;
height: 150px;
border: 2px solid #fff;
border-radius: 8px;
overflow: hidden;
z-index: 10;
}

#localVideo {
width: 100%;
height: 100%;
object-fit: cover;
}

.controls-container {
display: flex;
justify-content: center;
margin-top: 20px;
gap: 15px;
}

.controls-container button {
background-color: #555;
color: #fff;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
cursor: pointer;
font-size: 20px;
transition: background-color 0.3s;
}

.controls-container button:hover {
background-color: #777;
}

.call-btn {
background-color: #28a745 !important;
}

.call-btn:hover {
background-color: #218838 !important;
}

.hangup-btn {
background-color: #dc3545 !important;
}

.hangup-btn:hover {
background-color: #c82333 !important;
}

.welcome-message {
height: 500px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
background-color: #f8f9fa;
color: #495057;
border-radius: 8px;
padding: 20px;
}
</style>
</head>
<body>
<div class="header">
<img src="{{ url_for('static', filename='logo.png') }}" alt="Collnetwork Logo" class="logo">
</div>

<div class="call-container">
<div id="welcomeMessage" class="welcome-message">
<h2>Welcome to Collnetwork Care!</h2>
<p>Please enter your details to start a video call with our representative.</p>

<form id="callForm" style="margin-top: 20px;">
<div style="margin-bottom: 10px;">
<label for="callerName">Your Name:</label>
<input type="text" id="callerName" name="callerName" placeholder="e.g., Jane Doe" required>
</div>
<div style="margin-bottom: 20px;">
<label for="callerPhone">Phone Number:</label>
<input type="tel" id="callerPhone" name="callerPhone" placeholder="e.g., +15551234567" required>
</div>
</form>

<button id="callButton" class="call-btn">
<i class="fas fa-phone"></i> Start Call
</button>
</div>

<div class="main-video-wrapper" style="display:none;">
<video id="remoteVideo" autoplay playsinline></video>
</div>
<div class="local-video-wrapper" style="display:none;">
<video id="localVideo" autoplay muted playsinline></video>
</div>

<div class="controls-container">
<button id="hangupButton" class="hangup-btn" style="display:none;">
<i class="fas fa-phone-slash"></i>
</button>
<button id="muteButton" title="Mute/Unmute" style="display:none;">
<i class="fas fa-microphone"></i>
</button>
<button id="videoButton" title="Video On/Off" style="display:none;">
<i class="fas fa-video"></i>
</button>
<button id="screenShareButton" title="Share Screen" style="display:none;">
<i class="fas fa-desktop"></i>
</button>
<button id="dtmfButton" title="Keypad" style="display:none;">
<i class="fas fa-th"></i>
</button>
</div>

<div id="dtmfKeypad" style="display:none;">
<div class="keypad">
<button class="dtmf" data-digit="1">1</button>
<button class="dtmf" data-digit="2">2</button>
<button class="dtmf" data-digit="3">3</button>

<button class="dtmf" data-digit="4">4</button>
<button class="dtmf" data-digit="5">5</button>
<button class="dtmf" data-digit="6">6</button>

<button class="dtmf" data-digit="7">7</button>
<button class="dtmf" data-digit="8">8</button>
<button class="dtmf" data-digit="9">9</button>

<button class="dtmf" data-digit="*">*</button>
<button class="dtmf" data-digit="0">0</button>
<button class="dtmf" data-digit="#">#</button>
</div>
</div>

</div>
</body>
<script>
let userAgent;
let registerer;
let session;
let localStream;
let uaReady = false;
let screenSharing = false;

// Toggle Call
window.toggleCall = async function toggleCall() {
if (session) {
session.dispose();
session = null;
resetUI();
return;
}


const callerName = document.getElementById('callerName').value || 'WebRTC Client';
const callerPhone = document.getElementById('callerPhone').value || 'anonymous';
const combinedDisplayName = `${callerName} (${callerPhone})`;

if (!userAgent || userAgent.isTerminated()) {
userAgent = new SIP.UserAgent({
uri: SIP.UserAgent.makeURI(`sip:webcall@172.16.48.3`),
transportOptions: { server: 'wss://172.16.48.3:8089/ws', traceSip: true },
displayName: combinedDisplayName,
authorizationUsername: 'webcall',
authorizationPassword: 'your password as you defined in pjsip'
});

userAgent.delegate = {
onTransportError: () => {
console.warn('WebSocket closed unexpectedly');
alert('WebSocket connection closed. Please refresh the page.');
},
onInvite: (invitation) => {
handleSession(invitation);
invitation.accept();
}
};

try {
await userAgent.start();
registerer = new SIP.Registerer(userAgent);
await registerer.register();
console.log('SIP UA registered successfully');
uaReady = true;
} catch (err) {
console.error('Failed to connect/register', err);
alert('Failed to connect/register to SIP server');
return;
}
}

const target = SIP.UserAgent.makeURI('sip:0000@cucm ip');
if (!target) {
alert('Bad SIP URI for target');
return;
}

const inviter = new SIP.Inviter(userAgent, target, {
sessionDescriptionHandlerOptions: {
constraints: { audio: true, video: true }
}
});

try {
await inviter.invite();
handleSession(inviter);

// UI update
document.getElementById('welcomeMessage').style.display = 'none';
document.querySelector('.main-video-wrapper').style.display = 'block';
document.querySelector('.local-video-wrapper').style.display = 'block';
document.getElementById('callButton').style.display = 'none';
document.getElementById('hangupButton').style.display = 'inline-block';
document.getElementById('muteButton').style.display = 'inline-block';
document.getElementById('videoButton').style.display = 'inline-block';
document.getElementById('screenShareButton').style.display = 'inline-block';
document.getElementById('dtmfButton').style.display = 'inline-block';
} catch (e) {
console.error('Failed to make call', e);
alert('Failed to make call');
}
};

document.addEventListener('DOMContentLoaded', async () => {
await startLocalVideo();

document.getElementById('callButton').addEventListener('click', (e) => {
e.preventDefault();
const form = document.getElementById('callForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
window.toggleCall();
});

document.getElementById('hangupButton').addEventListener('click', () => window.toggleCall());
document.getElementById('screenShareButton').addEventListener('click', toggleScreenShare);
document.getElementById('muteButton').addEventListener('click', toggleMute);
document.getElementById('videoButton').addEventListener('click', toggleVideo);
document.getElementById('dtmfButton').addEventListener('click', toggleDTMFKeypad);

document.querySelectorAll('.dtmf').forEach(button => {

button.addEventListener('click', function () {
sendDTMF(this.getAttribute('data-digit'));
});
});

window.addEventListener('beforeunload', () => {
if (userAgent) userAgent.stop().catch(()=>{});
});
});

async function startLocalVideo() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
localStream = stream;
const localVideo = document.getElementById('localVideo');
if (localVideo) localVideo.srcObject = stream;
} catch (error) {
console.error("Error accessing media devices:", error);
alert("Unable to access your camera and microphone.");
}
}

function handleSession(newSession) {
session = newSession;

session.stateChange.addListener((st) => {
if (st === SIP.SessionState.Terminated) {
session = null;
resetUI();
}
});

session.sessionDescriptionHandler.peerConnection.ontrack = (ev) => {
const remoteVideo = document.getElementById('remoteVideo');
if (remoteVideo.srcObject !== ev.streams[0]) {
remoteVideo.srcObject = ev.streams[0];
}
};

localStream.getTracks().forEach((t) =>
session.sessionDescriptionHandler.peerConnection.addTrack(t, localStream)
);
}

function resetUI() {
// Reset UI to welcome state
document.getElementById('welcomeMessage').style.display = 'flex';
document.querySelector('.main-video-wrapper').style.display = 'none';
document.querySelector('.local-video-wrapper').style.display = 'none';
document.getElementById('callButton').style.display = 'inline-block';
document.getElementById('hangupButton').style.display = 'none';
document.getElementById('muteButton').style.display = 'none';
document.getElementById('videoButton').style.display = 'none';
document.getElementById('screenShareButton').style.display = 'none';
document.getElementById('dtmfButton').style.display = 'none';
document.getElementById('dtmfKeypad').style.display = 'none';

document.getElementById('remoteVideo').srcObject = null;
document.getElementById('callButton').textContent = 'Call';

// Re-enable form inputs
document.getElementById('callerName').disabled = false;
document.getElementById('callerPhone').disabled = false;
}


function toggleMute() {
if (!session) return;
const sender = session.sessionDescriptionHandler.peerConnection.getSenders().find(s => s.track && s.track.kind === 'audio');
if (!sender) return;
sender.track.enabled = !sender.track.enabled;
const icon = document.querySelector('#muteButton i');
if (sender.track.enabled) {
icon.classList.remove('fa-microphone-slash');
icon.classList.add('fa-microphone');
} else {
icon.classList.remove('fa-microphone');
icon.classList.add('fa-microphone-slash');
}
}

function toggleVideo() {
if (!session) return;
const sender = session.sessionDescriptionHandler.peerConnection.getSenders().find(s => s.track && s.track.kind === 'video');
if (!sender) return;
sender.track.enabled = !sender.track.enabled;
const icon = document.querySelector('#videoButton i');
const localEl = document.getElementById('localVideo');
if (sender.track.enabled) {
icon.classList.remove('fa-video-slash');
icon.classList.add('fa-video');
localEl.style.display = 'block';
} else {
icon.classList.remove('fa-video');
icon.classList.add('fa-video-slash');
localEl.style.display = 'none';
}
}

async function toggleScreenShare() {
if (!session) return;
if (screenSharing) {
const camTrack = localStream.getVideoTracks()[0];
const sender = session.sessionDescriptionHandler.peerConnection.getSenders().find(s => s.track && s.track.kind === 'video');
if (sender && camTrack) await sender.replaceTrack(camTrack);
screenSharing = false;
document.querySelector('#screenShareButton i').classList.remove('fa-compress');
document.querySelector('#screenShareButton i').classList.add('fa-desktop');
return;
}
try {
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
const screenTrack = screenStream.getVideoTracks()[0];
const sender = session.sessionDescriptionHandler.peerConnection.getSenders().find(s => s.track && s.track.kind === 'video');
if (sender && screenTrack) await sender.replaceTrack(screenTrack);
screenSharing = true;
document.querySelector('#screenShareButton i').classList.remove('fa-desktop');
document.querySelector('#screenShareButton i').classList.add('fa-compress');
} catch (e) {
console.error('Screen share failed', e);
alert('Failed to start screen sharing.');
}
}

function toggleDTMFKeypad() {
const keypad = document.getElementById('dtmfKeypad');
if (keypad.style.display === 'none' || !keypad.style.display) {
keypad.style.display = 'block';
} else {
keypad.style.display = 'none';
}
}

function sendDTMF(digit) {
if (session) {
// Find the RTCRtpSender for the audio track
const audioSender = session.sessionDescriptionHandler.peerConnection
.getSenders()
.find(sender => sender.track && sender.track.kind === 'audio');

// Check if the sender exists and has a DTMF sender object
if (audioSender && audioSender.dtmf) {
try {
// The .dtmf property IS the RTCDTMFSender object
audioSender.dtmf.insertDTMF(digit, 100, 50);
console.log(`Sent DTMF tone: ${digit}`);
} catch (error) {
console.error('Error sending DTMF tone:', error);
}
} else {
console.warn("RTCDTMFSender is not available for the audio track.");
}
} else {
console.warn("No active session to send DTMF.");
}
}
</script>
</html>
step 18:
https://asteriskip:8089/ws to accept the seilf signed ca (you wont do this if you have public ca)
open your html and try to make call

 

 

 

 

View solution in original post

11 Replies 11

collinks2
Level 5
Level 5

Hello, were you able to integrate Webrtc with CUBE? If yes, can you share the steps. 

What is the use case ?



Response Signature


I want to deploy Webrtc so that users can make use of a browser and make calls to uccx agents. I have been trying to set it up using opensips as the Webrtc gateway but media is not traversing.

As a result,I started to think of CUBE to see if it can handle Webrtc calls and forward them to cucm. So the user is to make webrtc calls to cucm via CUBE

Finally ,I was able to integrate webrtc with CUCM via Asterisk SIP Gateway.

wow! congrats. would you be so kind sharing how it is configured in CUBE and docs references on how it is setup.

I don't know if CUBE handles wss but I am sure CUBE handles sip calls.
Asterisk and some other sip servers handle wss calls
So you can install Asterisk on Ubuntu and configure sip trunk to cube using
dial plan.
Once Asterisk receives webrtc calls destined for the end point at cube, it
will route those calls to that end point .let me know if you want to
install Asterisk as a sip gateway so I can share with steps I used .

Would you mind sharing more details of your setup and the configurations. 



Response Signature


From what I know Cube does not have support for WebRTC. All references found has a WebRTC to SIP gateway in the mix and that is integrated with the SBC with a SIP trunk to then connect with Communication Manager.



Response Signature


yes, and i have made use of Asterisk as the sip gateway. There are other sip gateways such as opensips and kamailio.

Ok. I will compile them and revert to you.
Meanwhile you need to install Ubuntu. I installed Ubuntu version 25

collinks2
Level 5
Level 5

cucm_asterisk_sip_trunk_status.PNGInstallation of Asterisk

Step 1: Install the dependencies
sudo apt-get update
sudo apt-get install -y build-essential libxml2-dev libncurses5-dev libssl-dev libjansson-dev libsqlite3-dev uuid-dev

 

Step 2: Download the source code
Navigate to /usr/src and download the latest version of Asterisk 22:
sudo wget http://downloads.asterisk.org/pub/telephony/asterisk/asterisk-22-current.tar.gz

cd /usr/src
tar -xzvf asterisk-22-current.tar.gz

Step 3: Compile Asterisk
sudo tar -xvzf asterisk-21-current.tar.gz
cd asterisk-22*/
sudo contrib/scripts/install_prereq install
sudo ./configure
sudo make menuselect
sudo make && sudo make install
sudo make samples
sudo make config


Step 4:
sudo systemctl start asterisk
systemctl status asterisk
sudo asterisk -rvvv


Step 5: Test if Asterisk is running
Start Asterisk in the foreground to verify that it’s working:

sudo asterisk -vvvvvvvvgc

Step 6: Stop Asterisk
From the Asterisk CLI, stop the process:

cli core stop now

Step 7: Start Asterisk in the background
Run Asterisk as a background process:

sudo asterisk

Step 8: Connect to the Asterisk Console
To connect to the running Asterisk console:
sudo asterisk -r
You can press ctrl z to exit

 

Step 9 ***Creating certificates**

sudo openssl req -nodes -newkey rsa:2048 -keyout /etc/asterisk/keys//asterisk.key -out /etc/asterisk/keys/asterisk.csr
enter your common name and other details when prompted

Step 10:
grant permission
sudo chown asterisk:asterisk /etc/asterisk/keys/asterisk.crt
sudo chown asterisk:asterisk /etc/asterisk/keys/asterisk.pem
sudo chmod 644 /etc/asterisk/keys/asterisk.crt
sudo chmod 600 /etc/asterisk/keys/asterisk.pem

Step 11:optional (ref:
https://vitalpbx.com/blog/asterisk-webrtc-from-scratch/?srsltid=AfmBOoqsJzR4lmLpmihFfwLDBXxPqwiAcTjdMNz11rcrckxodCfPl4Em)

Creat a user asterisk and add to the group
groupadd asterisk
useradd -r -d /var/lib/asterisk -g asterisk asterisk
usermod -aG audio,dialout asterisk
chown asterisk. -R /etc/asterisk
chown asterisk. -R /var/{lib,log,spool}/asterisk
chown -R asterisk.asterisk /usr/lib64/asterisk

Step 12: modify the http.conf
sudo nano /etc/asterisk/http.conf
add this
[general]
enabled=yes
bindaddr=0.0.0.0
bindport=8088
tlsenable=yes
tlsbindaddr=0.0.0.0:8089
tlscertfile=/etc/asterisk/keys/asterisk.cer (replace with your cert)
tlsprivatekey=/etc/asterisk/keys/asterisk.key (replace with your cert)

Step 13: modify the pjsip.conf for webrtc and cucm-trunk
you can replace the cucm-trunk with the cube ip
**defining websocket secure transport**
[transport-wss]
type=transport
protocol=wss
bind=0.0.0.0:8089
cert_file=/etc/asterisk/keys/asterisk.cer
priv_key_file=/etc/asterisk/keys/asterisk.key

** webrtc section**
[webcall]
type=endpoint
context=default
disallow=all
direct_media=no
allow=ulaw,alaw,gsm,h264,VP8
webrtc=yes
aors=webcall
auth=webcall_auth

; Make Asterisk trust CallerID from the client
trust_id_inbound=yes
trust_id_outbound=yes
send_pai=yes
send_rpid=yes

dtls_auto_generate_cert=yes
rtp_engine=asterisk
use_avpf=yes
ice_support=yes
force_rport=yes
rewrite_contact=yes

[webcall_auth]
type=auth
auth_type=userpass
password=your password
username=webcall

[webcall]
type=aor
max_contacts=100


[CUCM-Trunk]
type=endpoint
transport=transport-udp ; or transport-tcp if CUCM requires TCP
context=from-external
disallow=all
allow=ulaw,alaw,gsm,h264,VP8
aors=CUCM-Trunk
outbound_auth=CUCM-Auth
direct_media=no

[CUCM-Trunk]
type=aor
contact=sip:IP:5060 ; CUCM or CUBE IP and SIP Port

[CUCM-Auth]
type=auth
auth_type=userpass
username=
password= ; Must match CUCM OR CUBE SIP trunk configuration

Step 14: modify extensions.conf
[default]
exten => 7100,1,NoOp(Forwarding Call to CUCM IVR)
same => n,Dial(PJSIP/7100@CUCM-Trunk)
same => n,Hangup()
step 15:
Connect to the Atserik console
sudo asterisk -r
pjsip reload
dialplan reload
module reload http
step 16 :verify if Asterik is running on 8089
sudo ss -tulnp | grep -E '8088|8089|5060'


step 17. setting up webrtc client using javascript
modify with your real parameters
<!DOCTYPE html>
<html>
<head>
<title>Collnetwork Care</title>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="{{ url_for('static', filename='sip.min.js') }}"></script>
<style>
body {
background-color: #f0f2f5;
font-family: sans-serif;
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 20px;
}

.keypad {
display: grid;
grid-template-columns: repeat(3, 80px);
gap: 15px;
justify-content: center;
margin-top: 20px;
}

.keypad button {
width: 80px;
height: 80px;
font-size: 24px;
font-weight: bold;
border-radius: 50%;
border: none;
background-color: #007bff;
color: white;
cursor: pointer;
transition: background-color 0.3s;
}

.keypad button:hover {
background-color: #0056b3;
}

/* Overlay keypad on video */
#dtmfKeypad {
position: absolute;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.9);
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 20;
}


.header {
margin-bottom: 20px;
}

.logo {
height: 50px;
}

.call-container {
position: relative;
width: 100%;
max-width: 900px;
background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 20px;
}

.main-video-wrapper {
width: 100%;
height: 500px;
background-color: #000;
border-radius: 8px;
overflow: hidden;
}

#remoteVideo {
width: 100%;
height: 100%;
object-fit: contain;
}

.local-video-wrapper {
position: absolute;
top: 30px;
right: 30px;
width: 200px;
height: 150px;
border: 2px solid #fff;
border-radius: 8px;
overflow: hidden;
z-index: 10;
}

#localVideo {
width: 100%;
height: 100%;
object-fit: cover;
}

.controls-container {
display: flex;
justify-content: center;
margin-top: 20px;
gap: 15px;
}

.controls-container button {
background-color: #555;
color: #fff;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
cursor: pointer;
font-size: 20px;
transition: background-color 0.3s;
}

.controls-container button:hover {
background-color: #777;
}

.call-btn {
background-color: #28a745 !important;
}

.call-btn:hover {
background-color: #218838 !important;
}

.hangup-btn {
background-color: #dc3545 !important;
}

.hangup-btn:hover {
background-color: #c82333 !important;
}

.welcome-message {
height: 500px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
background-color: #f8f9fa;
color: #495057;
border-radius: 8px;
padding: 20px;
}
</style>
</head>
<body>
<div class="header">
<img src="{{ url_for('static', filename='logo.png') }}" alt="Collnetwork Logo" class="logo">
</div>

<div class="call-container">
<div id="welcomeMessage" class="welcome-message">
<h2>Welcome to Collnetwork Care!</h2>
<p>Please enter your details to start a video call with our representative.</p>

<form id="callForm" style="margin-top: 20px;">
<div style="margin-bottom: 10px;">
<label for="callerName">Your Name:</label>
<input type="text" id="callerName" name="callerName" placeholder="e.g., Jane Doe" required>
</div>
<div style="margin-bottom: 20px;">
<label for="callerPhone">Phone Number:</label>
<input type="tel" id="callerPhone" name="callerPhone" placeholder="e.g., +15551234567" required>
</div>
</form>

<button id="callButton" class="call-btn">
<i class="fas fa-phone"></i> Start Call
</button>
</div>

<div class="main-video-wrapper" style="display:none;">
<video id="remoteVideo" autoplay playsinline></video>
</div>
<div class="local-video-wrapper" style="display:none;">
<video id="localVideo" autoplay muted playsinline></video>
</div>

<div class="controls-container">
<button id="hangupButton" class="hangup-btn" style="display:none;">
<i class="fas fa-phone-slash"></i>
</button>
<button id="muteButton" title="Mute/Unmute" style="display:none;">
<i class="fas fa-microphone"></i>
</button>
<button id="videoButton" title="Video On/Off" style="display:none;">
<i class="fas fa-video"></i>
</button>
<button id="screenShareButton" title="Share Screen" style="display:none;">
<i class="fas fa-desktop"></i>
</button>
<button id="dtmfButton" title="Keypad" style="display:none;">
<i class="fas fa-th"></i>
</button>
</div>

<div id="dtmfKeypad" style="display:none;">
<div class="keypad">
<button class="dtmf" data-digit="1">1</button>
<button class="dtmf" data-digit="2">2</button>
<button class="dtmf" data-digit="3">3</button>

<button class="dtmf" data-digit="4">4</button>
<button class="dtmf" data-digit="5">5</button>
<button class="dtmf" data-digit="6">6</button>

<button class="dtmf" data-digit="7">7</button>
<button class="dtmf" data-digit="8">8</button>
<button class="dtmf" data-digit="9">9</button>

<button class="dtmf" data-digit="*">*</button>
<button class="dtmf" data-digit="0">0</button>
<button class="dtmf" data-digit="#">#</button>
</div>
</div>

</div>
</body>
<script>
let userAgent;
let registerer;
let session;
let localStream;
let uaReady = false;
let screenSharing = false;

// Toggle Call
window.toggleCall = async function toggleCall() {
if (session) {
session.dispose();
session = null;
resetUI();
return;
}


const callerName = document.getElementById('callerName').value || 'WebRTC Client';
const callerPhone = document.getElementById('callerPhone').value || 'anonymous';
const combinedDisplayName = `${callerName} (${callerPhone})`;

if (!userAgent || userAgent.isTerminated()) {
userAgent = new SIP.UserAgent({
uri: SIP.UserAgent.makeURI(`sip:webcall@172.16.48.3`),
transportOptions: { server: 'wss://172.16.48.3:8089/ws', traceSip: true },
displayName: combinedDisplayName,
authorizationUsername: 'webcall',
authorizationPassword: 'your password as you defined in pjsip'
});

userAgent.delegate = {
onTransportError: () => {
console.warn('WebSocket closed unexpectedly');
alert('WebSocket connection closed. Please refresh the page.');
},
onInvite: (invitation) => {
handleSession(invitation);
invitation.accept();
}
};

try {
await userAgent.start();
registerer = new SIP.Registerer(userAgent);
await registerer.register();
console.log('SIP UA registered successfully');
uaReady = true;
} catch (err) {
console.error('Failed to connect/register', err);
alert('Failed to connect/register to SIP server');
return;
}
}

const target = SIP.UserAgent.makeURI('sip:0000@cucm ip');
if (!target) {
alert('Bad SIP URI for target');
return;
}

const inviter = new SIP.Inviter(userAgent, target, {
sessionDescriptionHandlerOptions: {
constraints: { audio: true, video: true }
}
});

try {
await inviter.invite();
handleSession(inviter);

// UI update
document.getElementById('welcomeMessage').style.display = 'none';
document.querySelector('.main-video-wrapper').style.display = 'block';
document.querySelector('.local-video-wrapper').style.display = 'block';
document.getElementById('callButton').style.display = 'none';
document.getElementById('hangupButton').style.display = 'inline-block';
document.getElementById('muteButton').style.display = 'inline-block';
document.getElementById('videoButton').style.display = 'inline-block';
document.getElementById('screenShareButton').style.display = 'inline-block';
document.getElementById('dtmfButton').style.display = 'inline-block';
} catch (e) {
console.error('Failed to make call', e);
alert('Failed to make call');
}
};

document.addEventListener('DOMContentLoaded', async () => {
await startLocalVideo();

document.getElementById('callButton').addEventListener('click', (e) => {
e.preventDefault();
const form = document.getElementById('callForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
window.toggleCall();
});

document.getElementById('hangupButton').addEventListener('click', () => window.toggleCall());
document.getElementById('screenShareButton').addEventListener('click', toggleScreenShare);
document.getElementById('muteButton').addEventListener('click', toggleMute);
document.getElementById('videoButton').addEventListener('click', toggleVideo);
document.getElementById('dtmfButton').addEventListener('click', toggleDTMFKeypad);

document.querySelectorAll('.dtmf').forEach(button => {

button.addEventListener('click', function () {
sendDTMF(this.getAttribute('data-digit'));
});
});

window.addEventListener('beforeunload', () => {
if (userAgent) userAgent.stop().catch(()=>{});
});
});

async function startLocalVideo() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
localStream = stream;
const localVideo = document.getElementById('localVideo');
if (localVideo) localVideo.srcObject = stream;
} catch (error) {
console.error("Error accessing media devices:", error);
alert("Unable to access your camera and microphone.");
}
}

function handleSession(newSession) {
session = newSession;

session.stateChange.addListener((st) => {
if (st === SIP.SessionState.Terminated) {
session = null;
resetUI();
}
});

session.sessionDescriptionHandler.peerConnection.ontrack = (ev) => {
const remoteVideo = document.getElementById('remoteVideo');
if (remoteVideo.srcObject !== ev.streams[0]) {
remoteVideo.srcObject = ev.streams[0];
}
};

localStream.getTracks().forEach((t) =>
session.sessionDescriptionHandler.peerConnection.addTrack(t, localStream)
);
}

function resetUI() {
// Reset UI to welcome state
document.getElementById('welcomeMessage').style.display = 'flex';
document.querySelector('.main-video-wrapper').style.display = 'none';
document.querySelector('.local-video-wrapper').style.display = 'none';
document.getElementById('callButton').style.display = 'inline-block';
document.getElementById('hangupButton').style.display = 'none';
document.getElementById('muteButton').style.display = 'none';
document.getElementById('videoButton').style.display = 'none';
document.getElementById('screenShareButton').style.display = 'none';
document.getElementById('dtmfButton').style.display = 'none';
document.getElementById('dtmfKeypad').style.display = 'none';

document.getElementById('remoteVideo').srcObject = null;
document.getElementById('callButton').textContent = 'Call';

// Re-enable form inputs
document.getElementById('callerName').disabled = false;
document.getElementById('callerPhone').disabled = false;
}


function toggleMute() {
if (!session) return;
const sender = session.sessionDescriptionHandler.peerConnection.getSenders().find(s => s.track && s.track.kind === 'audio');
if (!sender) return;
sender.track.enabled = !sender.track.enabled;
const icon = document.querySelector('#muteButton i');
if (sender.track.enabled) {
icon.classList.remove('fa-microphone-slash');
icon.classList.add('fa-microphone');
} else {
icon.classList.remove('fa-microphone');
icon.classList.add('fa-microphone-slash');
}
}

function toggleVideo() {
if (!session) return;
const sender = session.sessionDescriptionHandler.peerConnection.getSenders().find(s => s.track && s.track.kind === 'video');
if (!sender) return;
sender.track.enabled = !sender.track.enabled;
const icon = document.querySelector('#videoButton i');
const localEl = document.getElementById('localVideo');
if (sender.track.enabled) {
icon.classList.remove('fa-video-slash');
icon.classList.add('fa-video');
localEl.style.display = 'block';
} else {
icon.classList.remove('fa-video');
icon.classList.add('fa-video-slash');
localEl.style.display = 'none';
}
}

async function toggleScreenShare() {
if (!session) return;
if (screenSharing) {
const camTrack = localStream.getVideoTracks()[0];
const sender = session.sessionDescriptionHandler.peerConnection.getSenders().find(s => s.track && s.track.kind === 'video');
if (sender && camTrack) await sender.replaceTrack(camTrack);
screenSharing = false;
document.querySelector('#screenShareButton i').classList.remove('fa-compress');
document.querySelector('#screenShareButton i').classList.add('fa-desktop');
return;
}
try {
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
const screenTrack = screenStream.getVideoTracks()[0];
const sender = session.sessionDescriptionHandler.peerConnection.getSenders().find(s => s.track && s.track.kind === 'video');
if (sender && screenTrack) await sender.replaceTrack(screenTrack);
screenSharing = true;
document.querySelector('#screenShareButton i').classList.remove('fa-desktop');
document.querySelector('#screenShareButton i').classList.add('fa-compress');
} catch (e) {
console.error('Screen share failed', e);
alert('Failed to start screen sharing.');
}
}

function toggleDTMFKeypad() {
const keypad = document.getElementById('dtmfKeypad');
if (keypad.style.display === 'none' || !keypad.style.display) {
keypad.style.display = 'block';
} else {
keypad.style.display = 'none';
}
}

function sendDTMF(digit) {
if (session) {
// Find the RTCRtpSender for the audio track
const audioSender = session.sessionDescriptionHandler.peerConnection
.getSenders()
.find(sender => sender.track && sender.track.kind === 'audio');

// Check if the sender exists and has a DTMF sender object
if (audioSender && audioSender.dtmf) {
try {
// The .dtmf property IS the RTCDTMFSender object
audioSender.dtmf.insertDTMF(digit, 100, 50);
console.log(`Sent DTMF tone: ${digit}`);
} catch (error) {
console.error('Error sending DTMF tone:', error);
}
} else {
console.warn("RTCDTMFSender is not available for the audio track.");
}
} else {
console.warn("No active session to send DTMF.");
}
}
</script>
</html>
step 18:
https://asteriskip:8089/ws to accept the seilf signed ca (you wont do this if you have public ca)
open your html and try to make call