← Back to list

How I Built a Real-Time SIP Calling App Using JsSIP and Asterisk

Browsers cannot speak SIP directly. JsSIP solves that by speaking SIP over WebSocket and using WebRTC for media. Asterisk with chan_sip can…

Mirza Muhammad Arslan Ali · 2025-10-18 16:41 · 0 claps · 3.4 min read
#voip #react #asterisk #jssip #javascript
Open on Medium ↗
Wiki topics: 🌐 · Web Development 🔒 · Cybersecurity

How I Built a Real-Time SIP Calling App Using JsSIP and Asterisk

Browsers cannot speak SIP directly. JsSIP solves that by speaking SIP over WebSocket and using WebRTC for media. Asterisk with chan_sip can accept SIP over secure WebSocket and bridge media to any other SIP endpoint. That is enough to place and receive calls from a web page.

What you will set up

  1. Asterisk with chan_sip configured for WebRTC, WSS, and DTLS-SRTP
  2. A built-in Asterisk HTTP server that serves the WebSocket endpoint on port 8089
  3. Two SIP identities for a quick test, one browser user and one softphone or desk phone
  4. A simple React app that registers with JsSIP and places a call

Asterisk configuration

All file paths are the standard Debian or CentOS locations. Adjust as needed.

1. Generate certificates for DTLS and HTTPS

You can use the Asterisk TLS script or your own certs. Place them here.

/etc/asterisk/keys/asterisk.pem
/etc/asterisk/keys/asterisk.key

2. Enable the HTTP and WebSocket servers

/etc/asterisk/http.conf

[general]
enabled=yes
bindaddr=0.0.0.0
bindport=8088
tlsenable=yes
tlsbindaddr=0.0.0.0:8089
tlscertfile=/etc/asterisk/keys/asterisk.pem
tlsprivatekey=/etc/asterisk/keys/asterisk.key

Asterisk will now expose these endpoints http on port 8088 wss on port 8089 at path /ws

Make sure modules res_http_websocket and chan_sip are loaded.

/etc/asterisk/modules.conf

[modules]
load => chan_sip.so
load => res_rtp_asterisk.so
load => res_http_websocket.so

3. SIP and WebRTC settings for chan_sip

/etc/asterisk/sip.conf

[general]
context=outgoing
allowoverlap=yes
tcpenable=no
bindport=5060
bindaddr=0.0.0.0
srvlookup=yes
qualify=yes
dtmfmode=rfc2833
canreinvite=no
nat=force_rport,comedia
relaxdtmf=yes
rfc2833compensate=yes
register_retry_403=yes
registertimeout=60
maxexpiry=3600
minexpiry=3600
udpbindaddr=0.0.0.0
transport=udp
;externip=your.public.ip
;localnet=your.localnet.ip

; Example of a SIP trunk registration (replace with your provider)
;register => user:password@sip.provider.com

[provider]
type=peer
username=your_username
secret=your_password
qualify=yes
insecure=port,invite
outboundproxy=sip.provider.com:5060
host=sip.provider.com
fromdomain=sip.provider.com
context=from-provider
canreinvite=no
disallow=all
allow=ulaw,alaw

[test]
username=test
type=friend
context=internal
secret=StandardPassword
host=dynamic

[1101]
username=your_username
secret=your_password
type=friend
context=internal
callerid="Agent 1101" <1101>
host=dynamic
nat=force_rport,comedia
encryption=yes
avpf=yes
icesupport=yes
directmedia=no
transport=udp,ws,wss
force_avp=yes
dtlsenable=yes
dtlsverify=fingerprint
dtlscertfile=/etc/asterisk/keys/asterisk.pem
dtlsprivatekey=/etc/asterisk/keys/privkey.pem
dtlssetup=actpass
rtcp_mux=yes
allow=!all
allow=ulaw
allow=alaw
allow=gsm
media_encryption=dtls
qualify=yes

This creates one WebRTC SIP user (1101) that can register through WebSocket Secure (WSS). It uses DTLS-SRTP for encrypted media and supports both UDP and WebSocket transport.

4. Dialplan Configuration

File: /etc/asterisk/extensions.conf

[internal]
exten => 1101,1,NoOp(Call 1101)
same => n,Dial(SIP/1101,30)
same => n,Hangup()

This allows the user to test registration and loopback calling for verification. You can later add external trunks or routes as needed.

Reload Asterisk.

asterisk -rvvvvv
module reload chan_sip.so
sip reload
dialplan reload

Confirm WebSocket is listening.

http show status

You should see TLS bind on 0.0.0.0:8089 and WebSocket active.

Browser requirements

Browsers require a secure context for microphone access and for WSS. Serve your React app over HTTPS and connect JsSIP to wss://yourdomain:8089/ws. If you are testing locally, use a valid certificate or configure your browser to trust your CA.

Install JsSIP:

npm install jssip

Create a file WebPhone.jsx:

import React, { useEffect, useRef, useState } from "react"
import JsSIP from "jssip"
export default function WebPhone() {
  const [status, setStatus] = useState("Disconnected")
  const [target, setTarget] = useState("sip:1101@yourdomain.com")
  const remoteAudio = useRef(null)
  const uaRef = useRef(null)
  const sessionRef = useRef(null)
  useEffect(() => {
    const socket = new JsSIP.WebSocketInterface("wss://yourdomain.com:8089/ws")
    const configuration = {
      sockets: [socket],
      uri: "sip:1101@yourdomain.com",
      password: "1101"
    }
    const ua = new JsSIP.UA(configuration)
    uaRef.current = ua
    ua.on("registered", () => setStatus("Registered"))
    ua.on("unregistered", () => setStatus("Unregistered"))
    ua.on("registrationFailed", () => setStatus("Registration failed"))
    ua.on("newRTCSession", (data) => {
      const session = data.session
      sessionRef.current = session
      session.on("peerconnection", (e) => {
        const pc = e.peerconnection
        pc.addEventListener("track", (ev) => {
          const stream = ev.streams[0]
          if (remoteAudio.current) {
            remoteAudio.current.srcObject = stream
            remoteAudio.current.play().catch(() => {})
          }
        })
      })
      if (data.originator === "remote") {
        setStatus("Incoming call")
        session.answer({ mediaConstraints: { audio: true } })
      }
      session.on("accepted", () => setStatus("In call"))
      session.on("ended", () => setStatus("Call ended"))
    })
    ua.start()
  }, [])
  const makeCall = () => {
    const options = { mediaConstraints: { audio: true } }
    sessionRef.current = uaRef.current.call(target, options)
    setStatus("Calling...")
  }
  const hangUp = () => {
    if (sessionRef.current) {
      sessionRef.current.terminate()
      setStatus("Call ended")
    }
  }
  return (
    <div style={{ padding: 20, fontFamily: "Inter, sans-serif", maxWidth: 400, margin: "auto" }}>
      <h2>Web SIP Phone</h2>
      <p>Status {status}</p>
      <input
        style={{ width: "100%", padding: 8 }}
        value={target}
        onChange={(e) => setTarget(e.target.value)}
        placeholder="sip:1101@yourdomain.com"
      />
      <div style={{ marginTop: 12, display: "flex", gap: 8 }}>
        <button onClick={makeCall}>Call</button>
        <button onClick={hangUp}>Hang Up</button>
      </div>
      <audio ref={remoteAudio} autoPlay />
    </div>
  )
}

Serve this React app over HTTPS. When you open it, it will automatically register the SIP user 1101 with your Asterisk server.

If you connect a softphone (like Linphone) using the same user, you can test calling between them or verify media flow.

Common Question: Why Does WebRTC Require WSS and DTLS?

Browsers require secure communication for audio access and WebRTC sessions. WebSocket Secure (WSS) is needed for SIP signaling, while DTLS-SRTP encrypts the actual audio stream. Without these secure layers, browsers block microphone access and media transmission for security reasons.

Conclusion

Creating a single-user WebRTC phone using JsSIP and Asterisk chan_sip is simpler than it looks. With only a few configuration lines and a lightweight React component, you can turn a browser into a working SIP endpoint.

This setup forms the foundation for building larger web communication platforms from call centers to browser-based VoIP clients all powered by open technology.


메타데이터
post_id
c1b94a060f34
slug
how-i-built-a-real-time-sip-calling-app-using-jssip-and-asterisk-c1b94a060f34
url
https://medium.com/@arslan.ali1396/how-i-built-a-real-time-sip-calling-app-using-jssip-and-asterisk-c1b94a060f34
canonical_url
https://medium.com/@arslan.ali1396/how-i-built-a-real-time-sip-calling-app-using-jssip-and-asterisk-c1b94a060f34
author_url
https://medium.com/@arslan.ali1396
status
ok
fetched_at
2026-07-16 13:47:04