使用 SIP.js、Vue.js、WebSocket、WebRTC 和 Asterisk 开发软电话

在 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">
&nbsp;
      </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"
      >
      &nbsp;
      </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

(0)

相关推荐

发表回复

登录后才能评论