Skip to content

Instantly share code, notes, and snippets.

@coderkhalide
Created March 17, 2025 11:29
Show Gist options
  • Save coderkhalide/26d64b403e2b12b625cf8df5393d3e47 to your computer and use it in GitHub Desktop.
Save coderkhalide/26d64b403e2b12b625cf8df5393d3e47 to your computer and use it in GitHub Desktop.
Working SIP phone node js
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const cors = require('cors');
const dgram = require('dgram');
const os = require('os');
const crypto = require('crypto');
// Create Express app and HTTP server
const app = express();
const server = http.createServer(app);
// Enable CORS and JSON parsing
app.use(cors());
app.use(express.json());
// SIP configuration
const SIP_USER = '120';
const SIP_PASS = 'Natur0$120';
const SIP_SERVER = '27.147.188.250';
const SIP_PORT = 5060;
const WS_PORT = 5002;
const RTP_PORT = 10000; // Port for RTP media
// Get local IP address
function getLocalIP() {
const interfaces = os.networkInterfaces();
for (let ifname in interfaces) {
const iface = interfaces[ifname];
for (let i = 0; i < iface.length; i++) {
const { address, family, internal } = iface[i];
if (family === 'IPv4' && !internal) {
return address;
}
}
}
return '127.0.0.1';
}
const LOCAL_IP = getLocalIP();
console.log(`Using local IP address: ${LOCAL_IP}`);
// Create UDP socket for direct SIP communication
const udpSocket = dgram.createSocket('udp4');
let localPort = 0;
// Create UDP socket for RTP media
const rtpSocket = dgram.createSocket('udp4');
let rtcpSocket = dgram.createSocket('udp4');
let rtpLocalPort = RTP_PORT;
let rtcpLocalPort = RTP_PORT + 1;
let remoteRtpPort = null;
let remoteRtpAddress = null;
// Track call state
let currentCallId = null;
let currentCallTag = null;
let remoteTag = null;
let authRealm = null;
let authNonce = null;
let registerSeq = 1;
let inviteSeq = 1;
let registered = false;
let callActive = false;
let audioBuffers = [];
// Bind UDP socket to random port
udpSocket.bind(() => {
localPort = udpSocket.address().port;
console.log(`UDP socket bound to port ${localPort}`);
});
// Bind RTP sockets
rtpSocket.bind(rtpLocalPort, () => {
console.log(`RTP socket bound to port ${rtpLocalPort}`);
});
rtcpSocket.bind(rtcpLocalPort, () => {
console.log(`RTCP socket bound to port ${rtcpLocalPort}`);
});
// Handle RTP incoming packets
rtpSocket.on('message', (msg, rinfo) => {
console.log(`Received RTP packet from ${rinfo.address}:${rinfo.port}, length: ${msg.length} bytes`);
// Store remote RTP info if not already set
if (!remoteRtpAddress || !remoteRtpPort) {
remoteRtpAddress = rinfo.address;
remoteRtpPort = rinfo.port;
console.log(`Set remote RTP endpoint to ${remoteRtpAddress}:${remoteRtpPort}`);
}
// Forward audio data to connected WebSocket clients
if (callActive) {
broadcast({
type: 'audio_data',
data: Array.from(msg)
});
}
});
// Handle RTCP incoming packets
rtcpSocket.on('message', (msg, rinfo) => {
console.log(`Received RTCP packet from ${rinfo.address}:${rinfo.port}, length: ${msg.length} bytes`);
});
// Handle incoming UDP messages for SIP
udpSocket.on('message', (msg, rinfo) => {
const message = msg.toString();
console.log(`Received SIP message from ${rinfo.address}:${rinfo.port}:`);
console.log(message.split('\r\n').slice(0, 10).join('\r\n') + '...');
// Broadcast to WebSocket clients
broadcast({
type: 'sip_message',
direction: 'incoming',
message: message
});
// Extract headers from SIP message
const headers = parseSipMessage(message);
// Handle SIP responses
if (message.startsWith('SIP/2.0')) {
// Extract response code
const statusLine = message.split('\r\n')[0];
const statusCode = parseInt(statusLine.split(' ')[1]);
const statusText = statusLine.split(' ').slice(2).join(' ');
// Handle authentication challenge
if (statusCode === 401 || statusCode === 407) {
console.log(`Received authentication challenge`);
// Extract auth params from WWW-Authenticate or Proxy-Authenticate header
const authHeader = message.match(/WWW-Authenticate:\s*([^\r\n]+)/i) ||
message.match(/Proxy-Authenticate:\s*([^\r\n]+)/i);
if (authHeader) {
const authStr = authHeader[1];
const realm = authStr.match(/realm="([^"]+)"/i);
const nonce = authStr.match(/nonce="([^"]+)"/i);
if (realm && nonce) {
authRealm = realm[1];
authNonce = nonce[1];
console.log(`Auth params: realm=${authRealm}, nonce=${authNonce}`);
// Check if this is for REGISTER
if (headers.cseq && headers.cseq.includes('REGISTER')) {
sendAuthenticatedRegister();
}
// Check if this is for INVITE
else if (headers.cseq && headers.cseq.includes('INVITE')) {
sendAuthenticatedInvite(headers['to-uri'] || headers.to);
}
}
}
}
// Handle successful registration
else if (statusCode === 200) {
// Extract headers we need
const callId = headers['call-id'];
const cseq = headers.cseq;
// Check if this is a response to REGISTER
if (cseq && cseq.includes('REGISTER')) {
console.log(`Registration successful`);
registered = true;
broadcast({
type: 'status',
message: 'Successfully registered',
status: 'Registered'
});
}
// Check if this is a response to INVITE
else if (cseq && cseq.includes('INVITE')) {
console.log(`Call connected`);
callActive = true;
// Extract remote media info from SDP if available
const sdpContent = extractSdpContent(message);
if (sdpContent) {
const remoteMediaInfo = parseSdpForMedia(sdpContent);
if (remoteMediaInfo.address && remoteMediaInfo.port) {
remoteRtpAddress = remoteMediaInfo.address;
remoteRtpPort = remoteMediaInfo.port;
console.log(`Set remote RTP endpoint to ${remoteRtpAddress}:${remoteRtpPort} from SDP`);
}
}
// Get tags from To and From headers for proper dialog
if (headers.to) {
const toTag = headers.to.match(/tag=([^;]+)/);
if (toTag) remoteTag = toTag[1];
}
// Send ACK
sendAck(headers);
broadcast({
type: 'status',
message: 'Call connected',
status: 'Connected'
});
}
// Check if this is a response to BYE
else if (cseq && cseq.includes('BYE')) {
console.log(`Call ended`);
currentCallId = null;
currentCallTag = null;
remoteTag = null;
callActive = false;
broadcast({
type: 'status',
message: 'Call ended',
status: 'Idle'
});
}
}
// Handle ringing
else if (statusCode === 180) {
console.log(`Phone is ringing`);
broadcast({
type: 'status',
message: 'Phone is ringing',
status: 'Ringing'
});
}
// Handle various error responses
else if (statusCode >= 400) {
console.log(`Error: ${statusCode} ${statusText}`);
broadcast({
type: 'status',
message: `Error: ${statusCode} ${statusText}`,
status: 'Error'
});
}
}
// Handle SIP requests
else {
const requestLine = message.split('\r\n')[0];
const method = requestLine.split(' ')[0];
// Handle BYE request
if (method === 'BYE') {
console.log('Call ended by remote party');
// Send 200 OK response to BYE
const okResponse = createOkResponse(headers);
sendSipMessage(okResponse);
currentCallId = null;
currentCallTag = null;
remoteTag = null;
callActive = false;
broadcast({
type: 'status',
message: 'Call ended by remote party',
status: 'Idle'
});
}
}
});
// Extract SDP content from SIP message
function extractSdpContent(message) {
const parts = message.split('\r\n\r\n');
if (parts.length > 1) {
return parts[1];
}
return null;
}
// Parse SDP to get media information
function parseSdpForMedia(sdp) {
const mediaInfo = {};
// Get connection address
const connectionMatch = sdp.match(/c=IN IP4 ([^\r\n]+)/);
if (connectionMatch) {
mediaInfo.address = connectionMatch[1];
}
// Get media port
const mediaMatch = sdp.match(/m=audio (\d+) RTP/);
if (mediaMatch) {
mediaInfo.port = parseInt(mediaMatch[1]);
}
return mediaInfo;
}
// Parse SIP message into headers
function parseSipMessage(message) {
const lines = message.split('\r\n');
const headers = {};
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (!line) break; // End of headers
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const name = line.substring(0, colonIndex).toLowerCase();
const value = line.substring(colonIndex + 1).trim();
headers[name] = value;
}
}
// Extract some common URIs
if (headers.to) {
const toUriMatch = headers.to.match(/<([^>]+)>/);
if (toUriMatch) headers['to-uri'] = toUriMatch[1];
}
if (headers.from) {
const fromUriMatch = headers.from.match(/<([^>]+)>/);
if (fromUriMatch) headers['from-uri'] = fromUriMatch[1];
}
return headers;
}
// Create 200 OK response
function createOkResponse(headers) {
return [
`SIP/2.0 200 OK`,
`Via: ${headers.via}`,
`From: ${headers.from}`,
`To: ${headers.to}`,
`Call-ID: ${headers['call-id']}`,
`CSeq: ${headers.cseq}`,
`User-Agent: SIP-WebClient/1.0`,
`Content-Length: 0`,
'',
''
].join('\r\n');
}
// Send ACK for successful INVITE
function sendAck(headers) {
const branch = `z9hG4bK${Math.floor(Math.random() * 1e10)}`;
const toUri = headers['to-uri'] || `sip:${SIP_SERVER}`;
const ack = [
`ACK ${toUri} SIP/2.0`,
`Via: SIP/2.0/UDP ${LOCAL_IP}:${localPort};branch=${branch}`,
`From: <sip:${SIP_USER}@${SIP_SERVER}>;tag=${currentCallTag || ''}`,
`To: ${headers.to}`,
`Call-ID: ${headers['call-id']}`,
`CSeq: ${inviteSeq} ACK`,
`Contact: <sip:${SIP_USER}@${LOCAL_IP}:${localPort};transport=udp>`,
`Max-Forwards: 70`,
`User-Agent: SIP-WebClient/1.0`,
`Content-Length: 0`,
'',
''
].join('\r\n');
console.log('Sending ACK:');
console.log(ack.split('\r\n').slice(0, 8).join('\r\n'));
sendSipMessage(ack);
}
// Send SIP message
function sendSipMessage(message) {
const buffer = Buffer.from(message);
udpSocket.send(buffer, 0, buffer.length, SIP_PORT, SIP_SERVER, (err) => {
if (err) {
console.error(`Error sending SIP message: ${err.message}`);
}
});
// Broadcast to clients
broadcast({
type: 'sip_message',
direction: 'outgoing',
message: message
});
}
// Generate MD5 digest for authentication
function generateAuthResponse(method, uri) {
// MD5(username:realm:password)
const ha1 = crypto.createHash('md5')
.update(`${SIP_USER}:${authRealm}:${SIP_PASS}`)
.digest('hex');
// MD5(method:uri)
const ha2 = crypto.createHash('md5')
.update(`${method}:${uri}`)
.digest('hex');
// MD5(ha1:nonce:ha2)
return crypto.createHash('md5')
.update(`${ha1}:${authNonce}:${ha2}`)
.digest('hex');
}
// Send authenticated REGISTER request
function sendAuthenticatedRegister() {
const callId = currentCallId || `${Math.floor(Math.random() * 1e10)}@${LOCAL_IP}`;
currentCallId = callId;
const branch = `z9hG4bK${Math.floor(Math.random() * 1e10)}`;
const tag = currentCallTag || `${Math.floor(Math.random() * 1e10)}`;
currentCallTag = tag;
registerSeq++;
const uri = `sip:${SIP_SERVER}`;
const authResponse = generateAuthResponse('REGISTER', uri);
const register = [
`REGISTER ${uri} SIP/2.0`,
`Via: SIP/2.0/UDP ${LOCAL_IP}:${localPort};branch=${branch}`,
`From: <sip:${SIP_USER}@${SIP_SERVER}>;tag=${tag}`,
`To: <sip:${SIP_USER}@${SIP_SERVER}>`,
`Call-ID: ${callId}`,
`CSeq: ${registerSeq} REGISTER`,
`Contact: <sip:${SIP_USER}@${LOCAL_IP}:${localPort};transport=udp>`,
`Authorization: Digest username="${SIP_USER}", realm="${authRealm}", nonce="${authNonce}", uri="${uri}", response="${authResponse}", algorithm=MD5`,
`Max-Forwards: 70`,
`User-Agent: SIP-WebClient/1.0`,
`Allow: INVITE, ACK, CANCEL, BYE, NOTIFY, REFER, OPTIONS`,
`Expires: 3600`,
`Content-Length: 0`,
'',
''
].join('\r\n');
console.log(`Sending authenticated REGISTER request`);
console.log(register.split('\r\n').slice(0, 10).join('\r\n') + '...');
sendSipMessage(register);
}
// Send authenticated INVITE
function sendAuthenticatedInvite(destination) {
const callId = currentCallId;
const branch = `z9hG4bK${Math.floor(Math.random() * 1e10)}`;
const tag = currentCallTag;
inviteSeq++;
const uri = `sip:${SIP_SERVER}`;
const authResponse = generateAuthResponse('INVITE', uri);
// Create SDP content
const sdp = createSdpContent();
const invite = [
`INVITE ${destination} SIP/2.0`,
`Via: SIP/2.0/UDP ${LOCAL_IP}:${localPort};branch=${branch}`,
`From: <sip:${SIP_USER}@${SIP_SERVER}>;tag=${tag}`,
`To: <${destination}>`,
`Call-ID: ${callId}`,
`CSeq: ${inviteSeq} INVITE`,
`Contact: <sip:${SIP_USER}@${LOCAL_IP}:${localPort};transport=udp>`,
`Authorization: Digest username="${SIP_USER}", realm="${authRealm}", nonce="${authNonce}", uri="${uri}", response="${authResponse}", algorithm=MD5`,
`Max-Forwards: 70`,
`User-Agent: SIP-WebClient/1.0`,
`Allow: INVITE, ACK, CANCEL, BYE, NOTIFY, REFER, OPTIONS`,
`Content-Type: application/sdp`,
`Content-Length: ${sdp.length}`,
'',
sdp
].join('\r\n');
console.log(`Sending authenticated INVITE request`);
console.log(invite.split('\r\n').slice(0, 10).join('\r\n') + '...');
sendSipMessage(invite);
}
// Send RTP packet
function sendRtpPacket(data) {
if (!remoteRtpAddress || !remoteRtpPort || !callActive) {
console.log('Cannot send RTP packet: no active call or remote endpoint unknown');
return;
}
rtpSocket.send(data, 0, data.length, remoteRtpPort, remoteRtpAddress, (err) => {
if (err) {
console.error(`Error sending RTP packet: ${err.message}`);
} else {
console.log(`Sent RTP packet to ${remoteRtpAddress}:${remoteRtpPort}, length: ${data.length} bytes`);
}
});
}
// Create WebSocket server
const wss = new WebSocket.Server({ server });
const clients = new Set();
// Handle WebSocket connections
wss.on('connection', (ws) => {
clients.add(ws);
ws.send(JSON.stringify({
type: 'status',
message: 'Connected to SIP server',
status: 'Connected'
}));
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
if (data.type === 'audio_data' && callActive) {
// Convert base64 audio data to buffer
const audioData = Buffer.from(data.data);
// Send as RTP packet
sendRtpPacket(audioData);
}
} catch (e) {
console.error('Error processing WebSocket message:', e);
}
});
ws.on('close', () => {
clients.delete(ws);
});
});
// Broadcast message to all connected clients
function broadcast(message) {
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
}
// Register endpoint
app.post('/register', (req, res) => {
// Reset registration state
registerSeq = 1;
authRealm = null;
authNonce = null;
// Create initial REGISTER message
const callId = `${Math.floor(Math.random() * 1e10)}@${LOCAL_IP}`;
currentCallId = callId;
const branch = `z9hG4bK${Math.floor(Math.random() * 1e10)}`;
const tag = `${Math.floor(Math.random() * 1e10)}`;
currentCallTag = tag;
const register = [
`REGISTER sip:${SIP_SERVER} SIP/2.0`,
`Via: SIP/2.0/UDP ${LOCAL_IP}:${localPort};branch=${branch}`,
`From: <sip:${SIP_USER}@${SIP_SERVER}>;tag=${tag}`,
`To: <sip:${SIP_USER}@${SIP_SERVER}>`,
`Call-ID: ${callId}`,
`CSeq: ${registerSeq} REGISTER`,
`Contact: <sip:${SIP_USER}@${LOCAL_IP}:${localPort};transport=udp>`,
`Max-Forwards: 70`,
`User-Agent: SIP-WebClient/1.0`,
`Allow: INVITE, ACK, CANCEL, BYE, NOTIFY, REFER, OPTIONS`,
`Expires: 3600`,
`Content-Length: 0`,
'',
''
].join('\r\n');
console.log(`Sending initial REGISTER request`);
console.log(register.split('\r\n').slice(0, 10).join('\r\n') + '...');
// Send REGISTER message
const message = Buffer.from(register);
udpSocket.send(message, 0, message.length, SIP_PORT, SIP_SERVER, (err) => {
if (err) {
console.error(`Error sending REGISTER: ${err.message}`);
return res.status(500).json({ success: false, message: `Error: ${err.message}` });
}
console.log('REGISTER sent successfully (expect 401 challenge)');
broadcast({
type: 'status',
message: 'Registration in progress...',
status: 'Registering'
});
res.json({ success: true, message: 'Registration request sent' });
});
});
// Make a call
app.post('/call', (req, res) => {
const { destination } = req.body;
if (!destination) {
return res.status(400).json({ error: 'Missing destination number' });
}
if (!registered) {
return res.status(400).json({ error: 'Not registered with SIP server' });
}
console.log(`Making call to ${destination}...`);
// Reset call state
inviteSeq = 1;
remoteTag = null;
remoteRtpAddress = null;
remoteRtpPort = null;
callActive = false;
// Create initial INVITE message
const callId = `${Math.floor(Math.random() * 1e10)}@${LOCAL_IP}`;
currentCallId = callId;
const branch = `z9hG4bK${Math.floor(Math.random() * 1e10)}`;
const tag = `${Math.floor(Math.random() * 1e10)}`;
currentCallTag = tag;
// Create SDP content
const sdp = createSdpContent();
const destinationUri = `sip:${destination}@${SIP_SERVER}`;
const invite = [
`INVITE ${destinationUri} SIP/2.0`,
`Via: SIP/2.0/UDP ${LOCAL_IP}:${localPort};branch=${branch}`,
`From: <sip:${SIP_USER}@${SIP_SERVER}>;tag=${tag}`,
`To: <${destinationUri}>`,
`Call-ID: ${callId}`,
`CSeq: ${inviteSeq} INVITE`,
`Contact: <sip:${SIP_USER}@${LOCAL_IP}:${localPort};transport=udp>`,
`Max-Forwards: 70`,
`User-Agent: SIP-WebClient/1.0`,
`Allow: INVITE, ACK, CANCEL, BYE, NOTIFY, REFER, OPTIONS`,
`Content-Type: application/sdp`,
`Content-Length: ${sdp.length}`,
'',
sdp
].join('\r\n');
console.log(`Sending initial INVITE request`);
console.log(invite.split('\r\n').slice(0, 10).join('\r\n') + '...');
// Send INVITE message
const message = Buffer.from(invite);
udpSocket.send(message, 0, message.length, SIP_PORT, SIP_SERVER, (err) => {
if (err) {
console.error(`Error sending INVITE: ${err.message}`);
return res.status(500).json({ success: false, message: `Error: ${err.message}` });
}
console.log('INVITE sent successfully (expect 401 challenge)');
broadcast({
type: 'status',
message: 'Call request sent...',
status: 'Calling'
});
res.json({ success: true, message: 'Call initiated', callId });
});
});
// Hang up call
app.post('/hangup', (req, res) => {
if (!currentCallId) {
return res.status(400).json({ error: 'No active call' });
}
// Create BYE message
const branch = `z9hG4bK${Math.floor(Math.random() * 1e10)}`;
const bye = [
`BYE sip:${SIP_SERVER} SIP/2.0`,
`Via: SIP/2.0/UDP ${LOCAL_IP}:${localPort};branch=${branch}`,
`From: <sip:${SIP_USER}@${SIP_SERVER}>;tag=${currentCallTag}`,
`To: <sip:${SIP_SERVER}>${remoteTag ? ';tag=' + remoteTag : ''}`,
`Call-ID: ${currentCallId}`,
`CSeq: ${inviteSeq + 1} BYE`,
`Max-Forwards: 70`,
`User-Agent: SIP-WebClient/1.0`,
`Content-Length: 0`,
'',
''
].join('\r\n');
console.log(`Sending BYE request`);
console.log(bye);
// Send BYE message
const message = Buffer.from(bye);
udpSocket.send(message, 0, message.length, SIP_PORT, SIP_SERVER, (err) => {
if (err) {
console.error(`Error sending BYE: ${err.message}`);
return res.status(500).json({ success: false, message: `Error: ${err.message}` });
}
console.log('BYE sent successfully');
callActive = false;
broadcast({
type: 'status',
message: 'Hangup request sent',
status: 'Hanging Up'
});
res.json({ success: true, message: 'Hangup request sent' });
});
});
// Create SDP content for the call
function createSdpContent() {
const sessionId = Math.floor(Date.now() / 1000);
return [
'v=0',
`o=${SIP_USER} ${sessionId} ${sessionId} IN IP4 ${LOCAL_IP}`,
's=SIP Call',
`c=IN IP4 ${LOCAL_IP}`,
't=0 0',
`m=audio ${rtpLocalPort} RTP/AVP 0 8 101`,
'a=rtpmap:0 PCMU/8000',
'a=rtpmap:8 PCMA/8000',
'a=rtpmap:101 telephone-event/8000',
'a=fmtp:101 0-16',
'a=ptime:20',
'a=sendrecv'
].join('\r\n');
}
// Serve the web client
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>SIP Web Client with Audio</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/opus-recorder/0.5.0/recorder.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.container {
display: flex;
flex-direction: column;
gap: 20px;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
background-color: #f9f9f9;
}
.status {
padding: 10px 15px;
border-radius: 5px;
background-color: #e0e0e0;
font-weight: bold;
margin-bottom: 15px;
}
.btn {
padding: 10px 15px;
border: none;
border-radius: 5px;
background-color: #4CAF50;
color: white;
cursor: pointer;
font-weight: bold;
}
.btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
flex-grow: 1;
}
.log {
height: 300px;
overflow-y: auto;
background-color: #f0f0f0;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-family: monospace;
font-size: 12px;
}
.log-entry {
margin-bottom: 5px;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 5px;
}
.info {
background-color: #e3f2fd;
padding: 10px 15px;
border-radius: 5px;
margin-top: 20px;
font-size: 14px;
}
.audio-controls {
margin-top: 15px;
}
#volumeMeter {
width: 100%;
height: 20px;
background-color: #ddd;
border-radius: 10px;
overflow: hidden;
}
#volumeFill {
height: 100%;
background-color: #4CAF50;
width: 0%;
transition: width 0.1s;
}
</style>
</head>
<body>
<div class="container">
<h1>SIP Client with Audio Support</h1>
<div class="card">
<div id="status" class="status">Status: Disconnected</div>
<div>
<button id="registerBtn" class="btn" onclick="register()">Register</button>
</div>
</div>
<div class="card">
<h2>Make a Call</h2>
<div class="input-group">
<input type="text" id="destination" placeholder="Enter phone number">
<button id="callBtn" class="btn" onclick="makeCall()" disabled>Call</button>
<button id="hangupBtn" class="btn" onclick="hangup()" disabled style="background-color: #f44336;">Hang Up</button>
</div>
<div class="audio-controls">
<h3>Audio Controls</h3>
<div>
<button id="micBtn" class="btn" onclick="toggleMicrophone()" disabled>Enable Microphone</button>
<button id="muteBtn" class="btn" onclick="toggleMute()" disabled>Mute</button>
</div>
<div style="margin-top: 10px;">
<label for="volumeSlider">Speaker Volume:</label>
<input type="range" id="volumeSlider" min="0" max="1" step="0.1" value="0.5" oninput="adjustVolume()">
</div>
<div style="margin-top: 10px;">
<p>Microphone Level:</p>
<div id="volumeMeter">
<div id="volumeFill"></div>
</div>
</div>
</div>
</div>
<div class="card">
<h2>Logs</h2>
<div id="log" class="log"></div>
</div>
<div class="info">
<strong>Note:</strong> This client handles SIP signaling with authentication using the local IP ${LOCAL_IP}
and includes real-time audio support. Make sure to grant microphone permissions when prompted.
Audio is transmitted using RTP protocol on port ${rtpLocalPort}.
</div>
</div>
<script>
let registered = false;
let inCall = false;
let microphoneEnabled = false;
let muted = false;
let micStream = null;
let audioContext = null;
let mediaRecorder = null;
let analyserNode = null;
let audioPlayer = null;
let audioQueue = [];
let rtpSequence = 0;
let rtpTimestamp = Math.floor(Math.random() * 1000000);
let rtpSsrc = Math.floor(Math.random() * 1000000);
// Audio output
let audioBufferSources = [];
let audioPlaying = false;
// Connect WebSocket
const ws = new WebSocket('ws://' + window.location.host);
ws.onopen = () => {
addLog('Connected to server');
initAudio();
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'log') {
addLog(data.message);
}
else if (data.type === 'sip_message') {
addLog((data.direction === 'incoming' ? '<<< Received' : '>>> Sent') + ' SIP Message:');
const shortMessage = message.split('\\r\\n').slice(0, 5).join('\\r\\n') + '...';
addLog(shortMessage);
}
else if (data.type === 'status') {
addLog(data.message);
document.getElementById('status').textContent = 'Status: ' + data.status;
if (data.status === 'Registered') {
registered = true;
document.getElementById('registerBtn').disabled = true;
document.getElementById('callBtn').disabled = false;
document.getElementById('micBtn').disabled = false;
}
else if (data.status === 'Connected' || data.status === 'Ringing') {
inCall = true;
document.getElementById('callBtn').disabled = true;
document.getElementById('hangupBtn').disabled = false;
if (microphoneEnabled) {
document.getElementById('muteBtn').disabled = false;
}
}
else if (data.status === 'Idle' || data.status === 'Error') {
inCall = false;
document.getElementById('callBtn').disabled = !registered;
document.getElementById('hangupBtn').disabled = true;
document.getElementById('muteBtn').disabled = true;
// Stop recording if needed
stopRecording();
}
}
else if (data.type === 'audio_data' && inCall) {
// Process incoming audio data
if (audioContext) {
try {
// Convert array back to buffer and play
const audioBuffer = new Uint8Array(data.data);
// Remove RTP header (first 12 bytes)
const audioData = audioBuffer.slice(12);
// Play audio
playAudioData(audioData);
} catch (e) {
console.error('Error processing audio data:', e);
}
}
}
};
ws.onclose = () => {
addLog('Disconnected from server');
document.getElementById('status').textContent = 'Status: Disconnected';
};
// Initialize audio context and player
function initAudio() {
try {
// Create audio context
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Create gain node for volume control
gainNode = audioContext.createGain();
gainNode.gain.value = document.getElementById('volumeSlider').value;
gainNode.connect(audioContext.destination);
addLog('Audio system initialized');
} catch (e) {
addLog('Error initializing audio: ' + e.message);
}
}
// Play audio data
function playAudioData(audioData) {
if (!audioContext) return;
try {
// For G.711 PCMU/PCMA codec, we need to decode the data
// This is a simplified implementation - in a real app, we'd use a proper codec
// Create a buffer with the decoded audio
const buffer = audioContext.createBuffer(1, audioData.length, 8000);
const channel = buffer.getChannelData(0);
// PCMU (µ-law) basic decoding - this is a very simplified version
// In a real implementation, you'd use a proper G.711 codec library
for (let i = 0; i < audioData.length; i++) {
// Convert 8-bit µ-law to float
channel[i] = ((audioData[i] - 128) / 128.0);
}
// Create a buffer source
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(gainNode);
// Start playing
source.start();
// Store for cleanup
audioBufferSources.push(source);
// Clean up old sources
if (audioBufferSources.length > 10) {
audioBufferSources.shift();
}
} catch (e) {
console.error('Error playing audio:', e);
}
}
// Toggle microphone access
function toggleMicrophone() {
if (microphoneEnabled) {
stopMicrophone();
document.getElementById('micBtn').textContent = "Enable Microphone";
microphoneEnabled = false;
} else {
startMicrophone();
}
}
// Start microphone access
function startMicrophone() {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({ audio: true, video: false })
.then(stream => {
micStream = stream;
microphoneEnabled = true;
document.getElementById('micBtn').textContent = "Disable Microphone";
addLog('Microphone access granted');
// Set up audio analyzer for volume meter
setupAudioAnalyzer(stream);
// If in a call, enable recording
if (inCall) {
startRecording();
document.getElementById('muteBtn').disabled = false;
}
})
.catch(err => {
addLog('Error accessing microphone: ' + err.message);
});
} else {
addLog('getUserMedia not supported in this browser');
}
}
// Stop microphone access
function stopMicrophone() {
if (micStream) {
stopRecording();
micStream.getTracks().forEach(track => {
track.stop();
});
micStream = null;
// Reset volume meter
document.getElementById('volumeFill').style.width = '0%';
addLog('Microphone disabled');
document.getElementById('muteBtn').disabled = true;
}
}
// Set up audio analyzer for volume meter
function setupAudioAnalyzer(stream) {
if (!audioContext) return;
const source = audioContext.createMediaStreamSource(stream);
analyserNode = audioContext.createAnalyser();
analyserNode.fftSize = 256;
source.connect(analyserNode);
// Start updating volume meter
updateVolumeMeter();
}
// Update volume meter display
function updateVolumeMeter() {
if (!analyserNode || !microphoneEnabled) return;
const dataArray = new Uint8Array(analyserNode.frequencyBinCount);
analyserNode.getByteFrequencyData(dataArray);
// Calculate average volume
let sum = 0;
for (let i = 0; i < dataArray.length; i++) {
sum += dataArray[i];
}
const average = sum / dataArray.length;
// Update volume meter display (0-100%)
const volumePercent = Math.min(100, Math.max(0, average * 100 / 255));
document.getElementById('volumeFill').style.width = volumePercent + '%';
// Schedule next update
requestAnimationFrame(updateVolumeMeter);
}
// Start recording audio from microphone
function startRecording() {
if (!micStream || !microphoneEnabled || !inCall) return;
try {
// Create simple DataChannel-based recorder
const recorder = new MediaRecorder(micStream, {
mimeType: 'audio/webm'
});
// Set up data handler
recorder.ondataavailable = (event) => {
if (event.data.size > 0 && !muted) {
// Convert to ArrayBuffer and send
event.data.arrayBuffer().then(buffer => {
const audioData = new Uint8Array(buffer);
// Create a simple RTP-like packet
// In real implementation, we'd use proper RTP encoding
// This is a simplified version
const rtpHeader = new Uint8Array(12);
rtpHeader[0] = 0x80; // RTP version 2
rtpHeader[1] = 0x00; // PT: PCMU
// Sequence number (2 bytes)
rtpHeader[2] = (rtpSequence >> 8) & 0xFF;
rtpHeader[3] = rtpSequence & 0xFF;
rtpSequence = (rtpSequence + 1) & 0xFFFF;
// Timestamp (4 bytes)
rtpHeader[4] = (rtpTimestamp >> 24) & 0xFF;
rtpHeader[5] = (rtpTimestamp >> 16) & 0xFF;
rtpHeader[6] = (rtpTimestamp >> 8) & 0xFF;
rtpHeader[7] = rtpTimestamp & 0xFF;
rtpTimestamp += 160; // 20ms at 8000Hz
// SSRC (4 bytes)
rtpHeader[8] = (rtpSsrc >> 24) & 0xFF;
rtpHeader[9] = (rtpSsrc >> 16) & 0xFF;
rtpHeader[10] = (rtpSsrc >> 8) & 0xFF;
rtpHeader[11] = rtpSsrc & 0xFF;
// Combine header and payload
const rtpPacket = new Uint8Array(rtpHeader.length + audioData.length);
rtpPacket.set(rtpHeader);
rtpPacket.set(audioData, rtpHeader.length);
// Send to server
ws.send(JSON.stringify({
type: 'audio_data',
data: Array.from(rtpPacket)
}));
});
}
};
// Start recording
recorder.start(20); // Capture in 20ms chunks for real-time transmission
mediaRecorder = recorder;
addLog('Started audio recording');
} catch (e) {
addLog('Error starting recorder: ' + e.message);
}
}
// Stop recording
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
mediaRecorder = null;
addLog('Stopped audio recording');
}
}
// Toggle mute
function toggleMute() {
muted = !muted;
document.getElementById('muteBtn').textContent = muted ? "Unmute" : "Mute";
addLog(muted ? 'Microphone muted' : 'Microphone unmuted');
}
// Adjust volume
function adjustVolume() {
const volume = document.getElementById('volumeSlider').value;
if (gainNode) {
gainNode.gain.value = volume;
}
addLog('Speaker volume set to: ' + Math.round(volume * 100) + '%');
}
// Add log entry
function addLog(message) {
const log = document.getElementById('log');
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.textContent = new Date().toLocaleTimeString() + ': ' + message;
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
// Register
function register() {
addLog('Registering...');
document.getElementById('registerBtn').disabled = true;
fetch('/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
addLog('Registration failed: ' + data.message);
document.getElementById('registerBtn').disabled = false;
}
})
.catch(error => {
addLog('Error: ' + error.message);
document.getElementById('registerBtn').disabled = false;
});
}
// Make a call
function makeCall() {
const destination = document.getElementById('destination').value;
if (!destination) {
alert('Please enter a destination number');
return;
}
addLog('Making call to ' + destination);
document.getElementById('callBtn').disabled = true;
fetch('/call', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ destination })
})
.then(response => response.json())
.then(data => {
if (!data.success) {
addLog('Call failed: ' + data.message);
document.getElementById('callBtn').disabled = !registered;
} else {
// Start recording if microphone is enabled
if (microphoneEnabled) {
startRecording();
document.getElementById('muteBtn').disabled = false;
}
}
})
.catch(error => {
addLog('Error: ' + error.message);
document.getElementById('callBtn').disabled = !registered;
});
}
// Hang up
function hangup() {
addLog('Hanging up...');
document.getElementById('hangupBtn').disabled = true;
fetch('/hangup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
addLog('Hangup failed: ' + data.message);
document.getElementById('hangupBtn').disabled = false;
} else {
// Stop recording
stopRecording();
document.getElementById('muteBtn').disabled = true;
}
})
.catch(error => {
addLog('Error: ' + error.message);
document.getElementById('hangupBtn').disabled = false;
});
}
</script>
</body>
</html>
`);
});
// Start the server
const PORT = process.env.PORT || 5002;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`Open http://localhost:${PORT} in your browser to use the SIP client`);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment