在 Web 应用程序中集成通信功能越来越重要。其中一项功能就是开发软电话,让用户可以直接从 Web浏览器拨打语音电话。本教程将指导您使用 SIP.js、Vue.js、WebSocket、WebRTC 和 Asterisk 开发软电话。
简介
什么是 SIP.js?
SIP.js 是一个 JavaScript 库,允许开发人员构建基于 SIP(会话发起协议)的应用程序。SIP 是 VoIP(IP 语音)通信中使用的一种协议,用于发起、维护和终止涉及视频、语音、消息传递和其他通信服务的实时会话。
什么是 Vue.js?
Vue.js 是一个渐进式 JavaScript 框架,用于构建用户界面。它的设计旨在逐步采用,其核心库只关注视图层。Vue.js 易于与其他库或现有项目集成,可支持复杂的单页面应用程序。
什么是 Asterisk?
Asterisk 是一个用于构建通信应用程序的开源框架。它充当 SIP 服务器,提供呼叫路由、语音邮件和会议等功能。
设置 Asterisk
在服务器上安装 Asterisk,并将其配置为处理 SIP 连接。您需要设置 SIP 端点(用户)并配置 WebSocket 支持。以下是 Asterisk 的基本配置示例:
sip.conf:
[general]
transport=udp,ws
context=default
[your-username]
type=friend
host=dynamic
secret=your-password
context=default
http.conf :
[general]
enabled=yes
bindaddr=0.0.0.0
bindport=8088
extensions.conf:
[default]
exten => _X.,1,Dial(SIP/${EXTEN},20)
你可以使用 astrisk 面板,如 issabel https://www.issabel.org/。并确保在测试中使用的账户中启用 rtc-mux。
对于后端,你还需要使用 trurn 和 stun 服务器,比如这个开源项目 https://github.com/coturn/coturn
设置环境:前端
在开始编码之前,请确保您已安装 Node.js 和 npm(Node 包管理器)。您可以从 Node.js 官方网站下载。
可以使用 yarn,也可以使用 npm。
创建新的 Vue.js 项目
使用 Vue CLI 创建一个新的 Vue.js 项目。如果尚未安装 Vue CLI,可运行以下命令安装:
npm install -g @vue/cli
然后创建一个新项目:
vue create softphone
cd softphone
安装 SIP.js
导航到项目目录并安装 SIP.js:
npm install sip.js
构建软电话
设置 Vue.js 组件:
在 src/components 目录中创建一个名为 SoftPhone.vue 的新组件:
<template>
<VDialog v-model="dialog">
<VCard scrollable="true">
<VFlex>
<VCard>
<VCol cols="12">
<VCol
cols="12"
md="3"
>
<label for="displayName">display Name</label>
</VCol>
<VCol
cols="12"
md="12"
>
<VTextField
id="displayName"
v-model="displayName"
placeholder="display Name"
persistent-placeholder
/>
</VCol>
</VCol>
<VCol cols="12">
<VCol
cols="12"
md="3"
>
<label for="server">server</label>
</VCol>
<VCol
cols="12"
md="12"
>
<VTextField
id="server"
v-model="server"
placeholder="server"
persistent-placeholder
/>
</VCol>
</VCol>
<VCol cols="12">
<VCol
cols="12"
md="3"
>
<label for="aor">aor</label>
</VCol>
<VCol
cols="12"
md="12"
>
<VTextField
id="aor"
v-model="aor"
placeholder="aor"
persistent-placeholder
/>
</VCol>
</VCol>
<VCol cols="12">
<VCol
cols="12"
md="3"
>
<label for="authorizationUsername">authorization Username</label>
</VCol>
<VCol
cols="12"
md="12"
>
<VTextField
id="authorizationUsername"
v-model="authorizationUsername"
placeholder="authorization Username"
persistent-placeholder
/>
</VCol>
</VCol>
<VCol cols="12">
<VCol
cols="12"
md="3"
>
<label for="authorizationPassword">authorization Password</label>
</VCol>
<VCol
cols="12"
md="12"
>
<VTextField
id="authorizationPassword"
v-model="authorizationPassword"
placeholder="authorization Password"
persistent-placeholder
/>
</VCol>
</VCol>
<VCol cols="12">
<VCol
cols="12"
md="3"
>
<label for="stun_1">stun url 1</label>
</VCol>
<VCol
cols="12"
md="12"
>
<VTextField
id="stun_1"
v-model="stun_1"
placeholder="stun 1"
persistent-placeholder
/>
</VCol>
</VCol>
<VCol cols="12">
<VCol
cols="12"
md="3"
>
<label for="stun_1">stun url 2</label>
</VCol>
<VCol
cols="12"
md="12"
>
<VTextField
id="stun_2"
v-model="stun_2"
placeholder="stun 2"
persistent-placeholder
/>
</VCol>
</VCol>
<VCol cols="12">
<VCol
cols="12"
md="3"
>
<label for="turn_url">turn url</label>
</VCol>
<VCol
cols="12"
md="12"
>
<VTextField
id="turn_url"
v-model="turn_url"
placeholder="turn_url"
persistent-placeholder
/>
</VCol>
</VCol>
<VCol cols="12">
<VCol
cols="12"
md="3"
>
<label for="turn_username">turn username</label>
</VCol>
<VCol
cols="12"
md="12"
>
<VTextField
id="turn_username"
v-model="turn_username"
placeholder="turn username"
persistent-placeholder
/>
</VCol>
</VCol>
<VCol cols="12">
<VCol
cols="12"
md="3"
>
<label for="turn_password">turn password</label>
</VCol>
<VCol
cols="12"
md="12"
>
<VTextField
id="turn_password"
v-model="turn_password"
placeholder="turn password"
persistent-placeholder
/>
</VCol>
</VCol>
</VCard>
<VCardActions>
<VBtn
color="primary"
block
@click="save"
>
Save
</VBtn>
</VCardActions>
</VFlex>
</VCard>
</VDialog>
<div class="container">
<VRow>
<VCol lg="10">
</VCol>
<VCol lg="2">
<IconBtn @click="dialog = true ; isIneditMode = false">
<VIcon icon="mi-settings" />
setting
</IconBtn>
</VCol>
</VRow>
<VRow>
<VCol
cols="12"
:lg="3"
:md="12"
:sm="12"
:xs="12"
>
<div class="phone">
<div class="call-display">
<div class="row">
<div class="col agent-name">
Phone
<span
v-if="isConnected"
class="conected"
> : online </span> <span
v-else
class="notconected"
> : offline </span>
</div>
<div class="w-100" />
<div class="col agent-ext">
{{ authorizationUsername }}
</div>
<div class="col agent-status text-danger text-right">
<div class="btn-group">
<button
type="button"
class="btn btn-sm btn-outline-danger dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
{{ callStatus }}
</button>
</div>
</div>
</div>
<div
v-if="isInCall"
class="call-info"
>
<span class="call-name"> {{ callStatus }}</span><br>
<span class="call-number">{{ dailed }}</span>
<div
v-if="audioDuration != null"
id="timer"
class="col agent-time text-right"
>
{{ audioDuration }}
</div>
</div>
<!-- /.call-info -->
</div>
<!-- /.call-display -->
<form
id="dialer"
class="dial-display"
>
<input
v-model="dailed"
type="text"
pattern="[0-9 ]+"
autofocus
>
<IconBtn
class="rest"
@click="clearPad"
>
<VIcon
color="#ffffff"
icon="mdi-delete"
/>
</IconBtn>
</form>
<div class="grid">
<button
value="1"
@click="addToPad(1)"
>
1
</button>
<button
value="2"
@click="addToPad(2)"
>
2 <span>ABC</span>
</button>
<button
value="3"
@click="addToPad(3)"
>
3 <span>DEF</span>
</button>
<button
value="4"
@click="addToPad(4)"
>
4 <span>GHI</span>
</button>
<button
value="5"
@click="addToPad(5)"
>
5 <span>JKL</span>
</button>
<button
value="6"
@click="addToPad(6)"
>
6 <span>MNO</span>
</button>
<button
value="7"
@click="addToPad(7)"
>
7 <span>PQRS</span>
</button>
<button
value="8"
@click="addToPad(8)"
>
8 <span>TUV</span>
</button>
<button
value="9"
@click="addToPad(9)"
>
9 <span>WXYZ</span>
</button>
<button
value="*"
@click="addToPad('*')"
>
*
</button>
<button
value="0"
@click="addToPad(0)"
>
0
</button>
<button
value="#"
@click="addToPad('#')"
>
#
</button>
</div>
<!-- /.grid -->
<IconBtn
v-if="isInCall == false"
id="answer-call"
class="ans-call"
@click="call"
>
<VIcon icon="mdi-phone" />
</IconBtn>
<IconBtn
v-if="isInCall == true"
id="end-call"
class="end-call"
@click="hangup"
>
<VIcon icon="mdi-phone" />
</IconBtn>
</div>
</VCol>
<VCol
:lg="8"
:md="12"
:sm="12"
:xs="12"
>
</VCol>
</VRow>
</div>
<audio
id="remoteAudio"
ref="remoteAudio"
style="display: none;"
controls
/>
</template>
<script setup>
//import axiosIns from '@axios'
//import '@vuepic/vue-datepicker/dist/main.css'
import { SimpleUser } from "sip.js/lib/platform/web"
import { onMounted, ref } from 'vue'
let domain = ref("viop.example.com")
let dailed = ref("")
let isConnected = ref(false)
let callStatus = ref("")
let isInCall = ref(false)
let dialog = ref(false)
const audioDuration = ref(null)
function clearPad(){
dailed.value = ""
}
function addToPad( input ){
dailed.value = dailed.value + input
if(isInCall.value === true)
{
simpleUser.sendDTMF(input)
}
}
let target = ref("")
let displayName = ref("test-man")
let server = ref("wss://viop.example.com:8089/ws")
let aor = ref("sip:101@viop.example.com")
let authorizationUsername = ref("101")
let authorizationPassword = ref("**************************")
let stun_1 = ref("stun:stun.l.google.com:19302")
let stun_2 = ref("stun:stun1.l.google.com:19302")
let turn_url = ref("turn:example.com:3478")
let turn_username = ref("user")
let turn_password = ref("Pass")
let dtmf = ref("")
let hold = ref(false)
let mute = ref(false)
let keypad = ref([])
let remoteAudio = ref(null)
let options
let simpleUser
const connect = () => {
simpleUser.connect()
.then(() => {
isConnected.value = simpleUser.isConnected()
})
.catch(error => {
console.error(`[${simpleUser.id}] failed to connect`)
console.error(error)
alert("Failed to connect.\n" + error)
})
}
*/
async function connect(){
simpleUser.connect()
.then(() => {
isConnected.value = simpleUser.isConnected()
})
.catch(error => {
console.error(`[${simpleUser.id}] failed to connect`)
console.error(error)
alert("Failed to connect.\n" + error)
})
}
const call = () => {
let uri = "sip:"+dailed.value+"@"+domain.value
simpleUser
.call(uri, {
inviteWithoutSdp: false,
})
.catch(error => {
console.error(`[${simpleUser.id}] failed to place call`)
console.error(error)
alert("Failed to place call.\n" + error)
})
}
const hangup = () => {
isInCall.value = false
simpleUser.hangup().catch(error => {
console.error(`[${simpleUser.id}] failed to hangup call`)
console.error(error)
alert("Failed to hangup call.\n" + error)
})
}
const disconnect = () => {
simpleUser
.disconnect()
.then(() => {
isInCall.value = false
})
.catch(error => {
console.error(`[${simpleUser.id}] failed to disconnect`)
console.error(error)
alert("Failed to disconnect.\n" + error)
})
}
const pressKey = button => {
// Your pressKey logic here
dtmf.value = button.textContent
}
const toggleHold = () => {
// Your toggleHold logic here
}
const toggleMute = () => {
// Your toggleMute logic here
}
function formatTime(timeInSeconds) {
const minutes = Math.floor(timeInSeconds / 60)
const seconds = Math.floor(timeInSeconds % 60)
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
}
onUpdated(() => {
const intervalId = setInterval(() => {
if (remoteAudio.value.readyState >= 1) {
if(isInCall.value === true){
audioDuration.value = formatTime(remoteAudio.value.currentTime)
}else{
audioDuration.value = null
}
}
}, 1000)
// Clean up the interval to avoid memory leaks
return () => clearInterval(intervalId)
})
onMounted(() => {
remoteAudio.value = document.getElementById('remoteAudio')
setupConnection().then(()=>{
options = {
aor: aor.value,
delegate: {
onCallCreated: () => {
console.log(`[${displayName.value}] Call created`)
callStatus.value = "dailing"
isInCall.value = true
},
onCallAnswered: () => {
console.log(`[${displayName.value}] Call answered`)
callStatus.value = "on call"
isInCall.value = true
},
onCallHangup: () => {
console.log(`[${displayName.value}] Call hangup`)
callStatus.value = "Hangup"
isInCall.value = false
},
onCallHold: held => {
console.log(`[${displayName.value}] Call hold ${held}`)
callStatus.value = "Hold"
isInCall.value = true
},
},
media: {
remote: {
audio: document.getElementById('remoteAudio'),
},
},
userAgentOptions: {
authorizationPassword: authorizationPassword.value,
authorizationUsername: authorizationUsername.value,
sessionDescriptionHandlerFactoryOptions: {
alwaysAcquireMediaFirst: true,
RTCOfferOptions: {
offerToReceiveAudio: true,
offerToReceiveVideo: false,
iceRestart: true,
iceServers: [
{
urls: turn_url.value,
username: turn_username.value,
credential: turn_password.value,
},
{ urls: stun_1.value },
{ urls: stun_2.value },
],
},
peerConnectionConfiguration: {
iceServers: [
{
urls: turn_url.value,
username: turn_username.value,
credential: turn_password.value,
},
{ urls: stun_1.value },
{ urls: stun_2.value },
],
},
},
},
}
simpleUser = new SimpleUser(server.value, options)
connect()
})
})
</script>
<style scoped>
/**
your style
**/
</style>
这是 vue 3 的代码风格,使用了 Vuetify 的 “Vue 组件框架”。https://vuetifyjs.com/en/ 如果要使用相同的 templet 模式,你必须安装它。
在 vuejs 上实现 sip.js 非常麻烦
现在运行这个项目:
npm run serve
在浏览器输入 http://localhost:8080,你将看到软电话界面。在输入框中输入 SIP URI,然后单击 “Call(呼叫)”发起呼叫,单击 “Hang Up “结束通话。确保为该组件设置了路由,并导航到该组件。
结论
通过利用 SIP.js、Vue.js、WebSocket、WebRTC 和 Asterisk,你可以构建一个强大且可扩展的软电话应用程序。SIP.js 提供了强大的 SIP 功能,而 Vue.js 则提供了灵活的反应式用户界面框架。WebSocket 可确保以最小的延迟进行实时通信,WebRTC 可安全地处理点对点通信,而 Asterisk 则为管理呼叫提供了灵活而强大的后台。
作者:mohammed alaa
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/50435.html