JSSIP 通话

174 阅读5分钟

官网:jssip.net/

参考网站:jssip.net/documentati…

一、安装和配置

最近接手的vue项目需要使用jssip通话,jssip官网是全英文,所有看起来比较费劲。完成项目后记录一下jssip。 主要记录了jssip的安装、配置以及一些状态信息传递,包括二次拨号函数、来电铃声开关函数、通话添加媒体流函数、接听挂断函数、初始化函数等

  1. 安装

npm install jssip

  1. jssip文件

在src/utils下创建jssip.js文件

import JsSIP from 'jssip'
import store from '@/store'

let ua = null
let currentSession = ''
let peer = null
let stream = null

// 启用信令相关日志
JsSIP.debug.enable('JsSIP:RTCSession:*');

navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
  navigator.mozGetUserMedia || navigator.msGetUserMedia;

// 初始化配置
const initConfig = (data) => {
  const socket = new JsSIP.WebSocketInterface(process.env.VUE_APP_CALL_WS_URL)
  socket.via_transport = "ws"; // 制定实际传输协议为ws
  const configuration = {
    sockets: [socket],
    uri: `sip:${data.username}@${process.env.VUE_APP_JSSIP_URL}`, // 分机号注册 格式 sip: + 分机号码 + @ + FS注册地址
    authorization_user: data.username, // 授权用户
    password: data.password, // 密码
    register: true, // 自动注册
    register_expires: 1800, // 注册有效期
    connection_recovery_max_interval: 60, // 链接恢复的最大间隔
    connection_recovery_min_interval: 5 // 链接恢复的最小间隔
  }
  return configuration
};

// 初始化jssip
const jssipInit = (data) => {
  JsSIP.C.SESSION_EXPIRES = 120 // FreeSWITCH 默认设置要求 Session Expires 不低于120 低于120打不出电话
  JsSIP.C.MIN_SESSION_EXPIRES = 120
  const configuration = initConfig(data)
  ua = new JsSIP.UA(configuration)
  addRegisterMethod(ua)
  ua.start()
  return ua
}

// jssip监听方法
const addRegisterMethod = (ua) => {
  // 接线中
  ua.on('connecting', (res) => {
    console.log('接线中', res)
  })
  // 连线中
  ua.on('connected', (res) => {
    console.log('连线中', res)
  })
  // 取消连线
  ua.on('disconnected', (res) => {
    console.log('取消连线', res)
  })
  // 注册成功
  ua.on('registered', (res) => {
    console.log('注册成功', res)
    store.commit('setSessionData', { state: 'registered' });
  })
  // 注册失败
  ua.on('registrationFailed', (res) => {
    console.log('注册失败', res)
  })
  // 注册即将失效(重新注册)
  ua.on('registrationExpiring', (res) => {
    console.log('重新注册', res)
  })
  // 注销回调
  ua.on('unregistered', (res) => {
    console.log('解除注册', res)
    store.commit('setSessionData', { state: 'unregistered' });
  })
  // 会话邀请
  ua.on('invite', (res) => {
    console.log('会话邀请', res.remote_identity)
  })

  // 会话
  ua.on('newRTCSession', (data) => {
    const { session, request, originator } = data
    currentSession = session
    // 呼叫中 打电话与接电话的呼叫都走这里
    currentSession.on('progress', (res) => {
      console.log('process事件', originator)
      if (originator === 'remote') {
        console.log('电话过来拉~~~~~~~~~··', data)
        // console.log('我接听了', res)
        store.commit('setSessionData', { state:'progress-remote', data: request });
        // todo playRingMedia() // 这里可以加一个判断,如果是监听的时候不需要播放音乐
      } else {
        console.log('接听事件在progress中触发', res)
        store.commit('setSessionData', { state:'progress' });
        streamingMediaPlayer() // 添加媒体流 这时候开启音频
      }
    })
    // 对等连接事件触发
    currentSession.on('peerconnection', (data) => {
      console.log('对等连接事件触发,peerconnection', data)
      store.commit('setSessionData', { state:'peerconnection' });
    })
    // 交换sdp信令事件触发
    currentSession.on('sdp', (res) => {
      console.log('交换sdp信令事件触发,sdp', res)
      store.commit('setSessionData', { state:'sdp' });
    })
    // 对等连接建立(通话连线时候触发)
    currentSession.on('connecting', (data) => {
      peer = currentSession._connection
      console.log('对等连接建立,connecting', peer, data)
      store.commit('setSessionData', { state:'connecting' });
    })
    // 通话接受时候触发
    currentSession.on('accepted', (res) => {
      console.log('通话接受时候触发', res)
      store.commit('setSessionData', { state:'accepted' });
    })
    // 通话失败事件触发
    currentSession.on('failed', (res) => {
      console.log('通话失败事件触发--- failed', res)
      stopPlayRingMedia() // 关闭来电振铃
      store.commit('setSessionData', { state:'failed' });
    })
    currentSession.on('reinvite', (res) => {
      // openLocalCamera()
      console.log('重新协商事件触发', res)
      if (currentSession._connection.getLocalStreams().length > 0) {
        // 接听后,判断localStream
        console.log('localStream存在');
      }
      if (currentSession._connection.getRemoteStreams().length > 0) {
        console.log('remoteStream存在');
      }
    })
    // 呼叫确认
    currentSession.on('confirmed', (res) => {
      console.log('呼叫确认--设置媒体流到音视频中,confirmed', res)
      store.commit('setSessionData', { state:'confirmed' });
      if (originator === 'remote') {
        streamingMediaPlayer() // 呼入时没有添加媒体流,这时候添加媒体流
      }
    })
    // 通话结束
    currentSession.on('ended', (res) => {
      console.log('通话结束--- ended', res)
      store.commit('setSessionData', { state:'ended' });
    })
  })
}

// 呼叫
const call = (data) => {
  // 这一步也是添加监听,不过已经使用在addRegisterMethod方法中添加了这里为空就行
  const eventHandlers = {
    // 呼叫中
    progress: function (e) {
      console.log('call is in progress')
    },
    // 失败
    failed: function (e) {
      console.log('call failed: ', e)
    },
    // 结束
    ended: function (e) {
      console.log('call ended : ', e)
    },
    // 呼叫确认
    confirmed: function (e) {
      console.log('call confirmed')
    }
  }
  const options = {
    eventHandlers,
    mediaConstraints: {
      audio: true,
      video: false
    }
  }
  ua.call(`sip:${data.phone}@${data.url}`, options)
}

// 接听
const answer = () => {
  const options = {
    mediaConstraints: { audio: true, video: false, mandatory: { maxWidth: 640, maxHeight: 360 } }
  }
  currentSession.answer(options) // 接听来电
  console.log('电话接听')
}

// 挂断
const hangUp = () => {
  stopPlayRingMedia() // 关闭放来电振铃
  ua.terminateSessions()
}

/**
* 二次拨号
* @param number 二次拨号号码
* 注意功能是打通电话后,在按键时对方可接收到
*/
const dtmf = (number) => {
  console.log(number)
  const options = {
    transportType: 'RFC2833' // dtmf类型,默认是info
  }
  currentSession.sendDTMF(number, options)
}

// 添加媒体流播放
const streamingMediaPlayer = () => {
  stopstreamingMediaPlayer() // 先关闭媒体流
  stream = new MediaStream()
  const receivers = currentSession.connection && currentSession.connection.getReceivers()
  if (receivers) {
    receivers.forEach((receiver) => stream.addTrack(receiver.track))
  }
  store.commit('setCurrentSession', currentSession);
  // 这里创建一个audio标签隐藏,添加媒体流,添加到界面id为ringMediaAudioId的标签中
  let ringAudio = document.getElementById('ringMediaAudioId')
  ringAudio = document.createElement('audio')
  ringAudio.id = 'ringMediaAudioId'
  // ringAudio.hidden = true
  ringAudio.srcObject = stream // 添加媒体流
  const box = document.getElementById('ringMediaAudioBox')
  box.appendChild(ringAudio) // 添加到box中
  ringAudio.play() // 播放
}

// 关闭媒体流播放
const stopstreamingMediaPlayer = () => {
  const ringAudio = document.getElementById('ringMediaAudioId')
  if (ringAudio) {
    const box = document.getElementById('ringMediaAudioBox')
    box.removeChild(ringAudio)
  }
}

// 添加来电振铃播放
const playRingMedia = () => {
  stopPlayRingMedia()
  const ringAudio = document.getElementById('cheerMusic')
  ringAudio && ringAudio.play()
}

// 关闭来电振铃播放
const stopPlayRingMedia = () => {
  const cheerMusic = document.getElementById('cheerMusic')
  cheerMusic && cheerMusic.load()
}
const sip = {
  jssipInit: jssipInit, // 初始化
  call: call, // 拨号
  answer: answer, // 接听
  hangUp: hangUp, // 挂断
  dtmf: dtmf // 二次拨号
}
export default sip

二、使用(拨号)

jssip用到的主要是链接sip、断开链接sip、拨打电话、挂断电话、来电监听、来电接听、挂断来电、二次拨号以及电话监听挂和断监听

<script>
import jssip from '@/utils/jssipInit'
export default {
   data() {
        return {
            phone: null, // 电话号
            JsSipURL: 'www.abc.com', // JsSipURL 这里是域名,当然本地的话加端口号即可
            password: '123', // JsSipPassword 这个密码要与后端约定好
            username: '', // 分机号
            phoneState: '', // 用来控制界面拨打的状态
            incomingCall: false // 来电状态 来电弹窗状态
        }
    },
    created() {
        // 监听sip状态()
        this.$bus.$on('sessionData', (data) => { // 监听sip
            switch (data.state) {
                case 'unregistered': // 注销
                   this.$message.warning('sip已注销,签出成功')
                   break
                case 'registered': // 注册成功
                    this.$message.success('sip注册成功,签入成功')
                    break
                case 'ended': // 通话结束
                    // 这里写挂断后逻辑
                    break
                case 'progress': // 呼叫中
                      this.phoneState = 2
                      break
                case 'confirmed': // 呼叫确认
                      this.phoneState = 1
                      break
                case 'failed': // 通话失败
                      this.incomingCall = false
                      this.phoneState = 0
                      break
                case 'progress-remote': // 来电中
                    this.incomingCall = true
                    // 来电后逻辑
                    this.phoneState = 5
                    //响铃逻辑:<audio src="../../../assets/kanong.mp3" id="cheerMusic" loop class="hidden-audio"></audio>
                    const ring = document.getElementById('cheerMusic');
                    ring && ring.play();
                  }
                  break
               default:
                  break
              }
            })
            window.addEventListener('beforeunload', this.onBeforeunload)// 关闭窗口调用事件
        },
        mounted() {
            this.initLink()
          },
          methods: {
            // 初始化链接
            initLink() {
                 // username分机号 password密码 url域名 port端口 
                 jssip.jssipInit({ 
                     username: this.username, 
                     password: this.password, 
                     url: this.JsSipURL, 
                     port: '5072' 
                 })
            },
            // 断开sip
            unregistered() {
              jssip.unregister()
            },
            onBeforeunload(e) {
              jssip.unregister() // 断开jssip
            },
            // 二次拨号号码
            onDTMFFun(dtmfNumber) {
              jssip.dtmf(dtmfNumber)
            },
            // 呼叫
            onClickPhone(phone) {
              this.phone = phone
              const len = this.phone.length
              if (this.phone.length === 11) {
                jssip.call({ phone: this.phone, url: this.JsSipURL })
                layoutAPI.callStatus()
              } else {
                if (this.phone.length=== 0) {
                  this.$message.error('请输入号码!')
                } else {
                  this.$message.error('您输入号码不对,请输入正确的号码!')
                }
              }
            },
            // 点击挂断
            onClickHangUp() {
              this.phoneState = 0
              jssip.hangUp()
            },
            // 点击接听来电
            onAnswer() {
              // 接听来电逻辑
              jssip.answer()
              this.incomingCall = false
            },
            // 点击挂断来电
            onHangUp() {
              // 挂断逻辑
              jssip.hangUp()
              this.incomingCall = false
            },
            // 监听
            onListen() {
              // 监听逻辑
              jssip.answer()
            },
            // 关闭监听
            listenInClose() {
              // 关闭监听逻辑
              jssip.hangUp()
            },
          }
}
</script>
  1. 麦克风静音

 // 麦克风静音
handleCallMute(isMute) {
  // getSenders():这是 WebRTC 中的一个方法,用于获取当前连接的所有发送者(senders)。每个发送者代表一个音频或视频流的发送通道。
  // 返回的 audioSenders 是一个数组,包含所有发送者(即所有音视频流的发送对象)。
  const audioSenders = this.currentSession.connection && this.currentSession.connection.getSenders();
 //sender.track.kind 代表发送者的流类型,kind 可以是 'audio' 或 'video'。audio即找到音频流的发送者。
 //sender.track 是发送者的媒体轨道(audioTrack 或 videoTrack),它代表实际的音视频流。
  const audioTrack = audioSenders.find(sender => sender.track.kind === 'audio').track;
  if (audioTrack) {
    //audioTrack.enabled 用于控制音频是否被启用。enabled 为true 表示音频轨道正常工作,false 表示音频被禁用(即静音)
    audioTrack.enabled = !isMute;
  }
},

2.2 通话保持

 <!-- 通话保持-间奏 -->
 <audio src="../../../assets/bridge.mp3" id="bridgeMusic" loop class="hidden-audio"></audio>
 // 保持通话
handleCallHold(isKeep) {
  const bridge = document.getElementById('bridgeMusic');
  if (isKeep) {
    this.handleCallMute(true);
    bridge.play();
  } else {
    this.handleCallMute(false);
    bridge.load();
  }
}