先把封装好的地址安上(非本人封装):webrtc-webphone: 基于JsSIP开发的webrtc软电话
jssip中文文档:jssip中文开发文档(完整版) - 简书
jssip使用文档:(我没有运行过,但是他写的很清楚,反正比我好)jssip+webrtc+freeswitch实现电话网页及遇到的488状态码问题_freeswitch 488_weixin_39715323的博客-CSDN博客
正常使用由于web限制应该在https上使用,但是http也不是不可以,我放到下一篇文章了,这就说怎么使用
我这用的是webrtc+jssip
webrtc-webphone已经实现了我的需求,所以我没有使用原生jssip
特主要实现的功能有:注册,拨叫,接听,保持,恢复
我的项目中需要静音(指我不能说话,通话人可以讲话)
所以我将hold(保持)和unhold(恢复)改成了mute和unmute
下面是完整代码和方法说明:
1.init(注册citbar)config中需要使用ip地址、端口号、extNo拨号人、extPwd密码
2.handleAgentBarBtnClick(通话状态更改)
makecall(拨号)
hangup(挂断)
hold(静音)
unhold(取消静音)
3.onbeforeunload (通话中刷新对讲群组中没有退出,导致群组中有多个同一个人)
4.beforeDestroy(切换页面后没有退出群组,刷新不走这个方法)
注意:
1.不要重复拨号,状态卡住后就能同一个设备对话了bug
2.如果想切换页面还能通话就不要beforeDestroy,但是在其他页面在回来时会导致状态不一致,而且容易出现卡状态,所以我将通话操作放在index最顶层里面
<template>
<div class="top">
<div style="padding: 0 30px;">
<div style="display: flex;padding-top: 3px;">
<!-- 设备树插件 -->
<organizationTree ref="organizationTree"></organizationTree>
<div style="width:77vw;height:80vh; margin-top: 5px;">
<div class="photo_date" style="position:relative">
<div style="display:flex;min-width:30%">
<div class="text_type photo_deviceName " v-show="groupName" style="text-align: center;">
{{groupName}}</div>
<div class="text_type" :class="meetingStatus==2?' meetingName1':' meetingName'"
style="width: 125px;text-align: center;">
{{meetingStatus==2?meetingName:meetingName1}}</div>
</div>
<div style="position: absolute;right: 0;" v-show="createBy==userName">
<div v-show="meetingStatus==2" class="btn_type photo_meeting_btn" @click="meetingAll"
style="position:absolute; right:135px;width: 140px;height: 35px;line-height: 35px;">
邀请全部成员</div>
<div class="bg_btn_type photo_meeting_btn" @click="meeting"
style="position:absolute; right:0;width: 125px;height: 35px;line-height: 35px;">
{{meetingStatus==1?'开始会议':'结束会议'}}</div>
</div>
</div>
<div class="">
<div class="deviceList_title">
<div class="deviceList_title_text" style="width:15%">名称</div>
<div class="deviceList_title_text" style="width:15%">imei</div>
<div class="deviceList_title_text" style="width:15%">类型</div>
<div class="deviceList_title_text" style="width:15%">状态</div>
<div class="deviceList_title_text" style="width:15%">会议状态</div>
<div class="deviceList_title_text" style="width:24%">操作</div>
<!-- <div class="deviceList_title_text">操作</div> -->
</div>
<div v-show="deviceList" class="deviceList">
<div class="deviceList_list" :class="item.id==itemId?'deviceList_list1':''"
v-for="(item,index) in deviceList" :key="item.id" @mouseover="mouseover(item.id)"
@mouseleave="mouseout()">
<!-- <el-tooltip :content="item.name" placement="bottom" effect="light"> -->
<div class="deviceList_title_text" style="width:15%">{{item.devName}}</div>
<!-- </el-tooltip> -->
<div class="deviceList_title_text" style="width:15%">{{item.imei}}</div>
<div class="deviceList_title_text" style="width:15%">{{item.devTypeName}}
</div>
<div class="deviceList_title_text" style="width:15%">
{{dictionary(item.devStatus,'dev_status',item.imei)}}
</div>
<div class="deviceList_title_text" style="width:15%">
{{dictionary(item.memberStatus,'meeting_member_status')}}
</div>
<div style="width:24%;">
<div v-if="meetingStatus==2">
<div v-if="item.imei == createBy&&item.imei == userName">
<div v-if="item.devStatus==3||item.devStatus==4||item.devStatus==5||item.devStatus==1">
<div style="width:100%;justify-content: center;" class="deviceList_operate">
<div class="deviceList_title_text1" @click="speak('request')"
v-if="item.memberStatus!=3&&item.memberStatus!=1&&item.memberStatus!=4&&item.memberStatus!=5">
<div>开始发言</div>
</div>
<div class="deviceList_title_text1" @click="speak('request')" v-if="item.memberStatus==3">
<div>结束发言</div>
</div>
<div class="deviceList_title_text1" @click="outMeeting" v-if="item.memberStatus==2">离开会议
</div>
<div class="deviceList_title_text1" @click="inMeeting" v-if="item.memberStatus==1">进入会议
</div>
</div>
</div>
</div>
<div v-if="item.imei != createBy">
<div v-if="item.devStatus==3||item.devStatus==4||item.devStatus==5||item.devStatus==1">
<div style="width:100%;justify-content: center;" class="deviceList_operate"
v-show="createBy==userName&&isInOrOutMeeting&&deviceList[0].memberStatus==2">
<div class="deviceList_title_text1" @click="speak('call',item.imei,'start')"
v-if="item.memberStatus!=3&&item.memberStatus!=1&&item.memberStatus!=4&&item.memberStatus!=5">
<div>点名发言</div>
</div>
<div class="deviceList_title_text1" @click="speak('call',item.imei,'end')"
v-if="item.memberStatus==3">
<div>结束发言</div>
</div>
<div class="deviceList_title_text1" @click="inOrOut('out',item.imei)"
v-if="item.memberStatus==2">请离会议</div>
<div class="deviceList_title_text1" @click="inOrOut('in',item.imei)"
v-if="item.memberStatus==1">拉入会议</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Header from "../home/header/index";
import Footer from "../home/footer/index";
import webSocketClass from "@/utils/webSocket";
import { postWarnStatus } from "@/api/AlarmRecord";
import organizationTree from "./deviceTree/organizationTree"
import { devicetree } from "@/api/system/deviceTree";
import { timestampToTime } from "../../../utils/time.js"
import { addGroup, delGroup, getGroup, listGroup, updateGroup, updateMeetingStatus, selectDeviceGroupDetailList, deviceRequestTalking, inOrOutMeeting } from "@/api/system/group";
import { listData } from "@/api/system/dict/data";
import Ctibar from './AgentBar/ctibar.js';
var audio = document.getElementById('audio');
var constraints = {
audio: true,
video: true,
mandatory: {
maxWidth: 640,
maxHeight: 360
}
};
URL = window.URL || window.webkitURL;
var 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');
}
};
export default {
dicts: ["warn_type"],
components: {
Header,
Footer,
organizationTree
},
data() {
return {
// 以下群组
isGroup: false,
createBy: '',
isInOrOutMeeting: false,
isSpeak: false,
itemId: '',
meetingStatus: '',
groupDetail: {},
isMike: false,
deviceList: null,
meetingName1: "会议未开始",
meetingName: "会议中",
groupName: "",
dataList: null,
userId: JSON.parse(sessionStorage.getItem("userInfo")).userId,
userName: JSON.parse(sessionStorage.getItem("userInfo")).userName,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
deptId: null,
planName: null,
status: null,
},
total: 0,
deviceTreeList: [],
title: "",
num: 1,
groupId: '',
dictionaryList: [],
websocket: null,
//初始化SDK所需要的配置
config: {
host: '39.152.2.103',
port: '5066',
proto: false,
extNo: '',
extPwd: '20181231',
autoRegister: true,
debug: true,
//stunServer: 'stun.1.google.com', 可自行修改使用stun服务器地址
stateEventListener: this.stateEventListener
},
//坐席分机号
agentNo: '',
//客户号码
customerNo: '',
//拨号弹窗
showDial: false,
//转接弹窗
showTransferDial: false,
//转接号码
transNum: '',
numList: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#',],
agentStatus: 'DISCONNECTED',
statusMap: {
CONNECTED: '已连接',
DISCONNECTED: '网络断开',
REGISTERED: '已注册',
UNREGISTERED: '未注册',
REGISTER_FAILED: '注册失败',
IN_CALL: '通话中',
INCOMING_CALL: '来电振铃',
OUTGOING_CALL: '外呼振铃',
HOLD: '保持中',
CALL_END: '通话结束'
},
timer: null,
timerString: '00:00:00',
outNum: '',
isHold: true
};
},
computed: {
classObject() {
const bool1 = this.alarmArr.length > 1;
const bool2 = this.alarmArr.length === 1;
return {
tanchuangbox: true,
"tanchuangbox-height-multi": bool1,
"tanchuangbox-height-single": bool2,
};
},
},
mounted() {
this.init()
},
methods: {
// 以下群组
inMeeting() {
this.loadingFun()
var body = {
devImei: this.userName,
flag: 'in',
groupId: this.groupId,
}
inOrOutMeeting(body).then(response => {
if (response.data.success) {
} else {
this.$modal.msgError(response.data.message);
}
this.loading.close();
},
error => {
this.loading.close();
});
},
outMeeting() {
this.loadingFun()
var body = {
devImei: this.userName,
flag: 'out',
groupId: this.groupId,
}
inOrOutMeeting(body).then(response => {
if (response.data.message == '已离开!') {
} else {
this.$modal.msgError(response.data.message);
}
this.loading.close();
},
error => {
this.loading.close();
});
},
inOrOut(flag, devImei) {
this.loadingFun()
var body = {
devImei: devImei,
flag: flag,
groupId: this.groupId,
}
inOrOutMeeting(body).then(response => {
console.log("123123123", response)
if (response.data.success) {
this.$modal.msgSuccess(response.data.message);
} else {
this.$modal.msgError(response.data.message);
}
this.loading.close();
},
error => {
this.loading.close();
});
},
loadingFun() {
this.loading = this.$loading({
lock: true,
text: 'Loading',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
},
speak(type, devImei, flag) {
this.loadingFun()
var flagg = ''
if (type == 'request') {
flagg = !this.isSpeak ? 'start' : 'end'
devImei = this.userName
} else {
flagg = flag
}
var body = {
devImei: devImei,
flag: flagg,
groupId: this.groupId,
type: type,
}
deviceRequestTalking(body).then(response => {
if (response.data.success) {
if (type == 'request') {
}
this.$modal.msgSuccess(response.data.message);
//恢复发言
} else {
this.$modal.msgError(response.data.message);
this.hold()
}
this.loading.close();
},
error => {
this.loading.close();
});
},
// 1、进入元素
mouseover(index) {
this.itemId = index
},
// 4、离开元素
mouseout() {
this.itemId = ''
},
ws() {
console.log('{"groupId":' + this.groupId + '}')
this.websocket.webSocketSendMsg('{"groupId":' + this.groupId + '}')
},
devStatus(devStatus) {
for (const i in this.deviceList) {
if (this.deviceList[i].devStatus == devStatus) {
this.deviceList[i].devStatus = devStatus
}
}
},
dictionary(e, type, imei) {
for (const i in this.dictionaryList) {
if (this.createBy == imei && e == 4) {
return '在线'
}
if (this.dictionaryList[i].dictValue == e && this.dictionaryList[i].dictType == type) {
return this.dictionaryList[i].dictLabel
}
}
},
handleAdd() {
// this.reset();
// this.open = true;
this.title = "添加群组";
},
tableRowClassName({ row, rowIndex }) {
return 'photo';
},
rowClass({ row, rowIndex }) {
return 'text-align: center;background-color: #1A1D30;color: #fff'
},
meetingAll() {
var meetingStatusAll = 2
var params = {
groupId: this.groupId,
meetingStatus: meetingStatusAll
}
updateMeetingStatus(params).then(response => {
if (response.code == 200) {
this.$modal.msgSuccess(response.msg);
} else {
this.$modal.msgError(response.msg);
}
});
},
meeting() {
this.loadingFun()
if (this.deviceList != null) {
console.log('meetingStatusmeetingStatus', this.meetingStatus)
var params = {
groupId: this.groupId,
meetingStatus: this.meetingStatus == 1 ? 2 : 1
}
updateMeetingStatus(params).then(response => {
setTimeout(() => {
if (response.code == 200) {
if (this.meetingStatus == 1) {
if (sessionStorage.getItem('groupId') == this.groupId) {
this.handleAgentBarBtnClick('hangup')
sessionStorage.setItem('groupId', '')
}
this.isInOrOutMeeting = false
this.isHold = true
this.loading.close()
}
}
}, 5000);
});
}
},
//孙组件向父组件传递数据
wsMeetingStatus(isInterface) {
console.log('isMeeting状态:', isInterface)
this.meetingStatus = isInterface
},
groupDeviceItemClick(item) {
// this.handleAgentBarBtnClick('hangup')
this.isSpeak = false
this.deviceList = []
this.groupId = item.id
this.meetingStatus = item.meetingStatus
this.groupName = item.groupName
this.createBy = item.createBy
this.item = item
var params = { groupInfoId: item.id, pageNum: 0, pageSize: 0, }
selectDeviceGroupDetailList(params).then(response => {
this.deviceList = response.rows
this.total = this.deviceList.length
this.ws()
if (this.deviceList[0].memberStatus == 3 &&
this.deviceList[0].imei == this.userName) {
this.isSpeak = true;
}
if (
this.deviceList[0].memberStatus == 2 &&
this.deviceList[0].imei == this.userName
) {
if (sessionStorage.getItem("groupId") == this.groupId) {
this.isInOrOutMeeting = true
return
}
if (sessionStorage.getItem("groupId")) {
} else {
this.call()
}
}
});
},
onClickDialOutside(event) {
console.log(event)
this.showDial = false
},
onClickTransDialOutside(event) {
this.showTransferDial = false;
},
handleAgentBarBtnClick(name) {
console.log(name + '当前')
if (name === 'login') {
this.login();
} else if (name === 'logout') {
this.logout();
} else if (name === 'answer') {
this.answer();
} else if (name === 'hangup') {
this.hangup();
} else if (name === 'makecall') {
this.makeCall('9*' + this.groupId)
} else if (name === 'hold') {
this.hold();
} else if (name === 'unhold') {
this.unhold();
} else if (name === 'transfer') {
this.transfer(this.transNum)
}
},
login() {
this.init()
Ctibar.register()
},
logout() {
Ctibar.unregister()
},
makeCall(phone) {
if (phone === "" || phone === undefined) {
console.error('无效的号码,请重新输入!');
return
}
Ctibar.makecall(phone);
},
hold() {
Ctibar.hold();
},
unhold() {
Ctibar.unhold();
},
answer() {
Ctibar.answer();
},
hangup() {
Ctibar.hangup();
},
transfer(phone) {
console.info("触发转接", phone)
Ctibar.transfer(phone);
},
//外呼拨号盘
handleDialBtnClick(val) {
this.outNum += val;
this.$refs.outNumInput.focus();
},
//转接拨号盘
handleTransDialBtnClick(val) {
this.transNum += val;
this.$refs.transNumInput.focus();
},
//参数为时间差秒数,返回这两个时间差并格式化
computeTimeDiff(diff) {
diff = Math.round(diff / 1000);
let hour = Math.floor(diff / 3600).toString().padStart(2, '0');
let min = Math.floor((diff - hour * 3600) / 60).toString().padStart(2, '0');
let sec = (diff % 60).toString().padStart(2, '0');
return hour + ':' + min + ':' + sec;
},
//重置时间
restoreTime(origin) {
clearInterval(this.timer);
this.timerString = '00:00:00';
this.timer = setInterval(() => {
this.timerString = this.computeTimeDiff(new Date().getTime() - origin);
}, 1000);
},
//状态变更回调
stateEventListener(event, data) {
//debug使用
console.log('当前event为: ' + event + ', 当前data为: ' + JSON.stringify(data))
this.agentStatus = event
let origin = new Date().getTime();
switch (event) {
case "CONNECTED":
this.agentNo = data.localAgent
this.restoreTime(origin);
break;
case "DISCONNECTED":
this.restoreTime(origin);
break;
case "UNREGISTERED":
this.restoreTime(origin);
break;
case "OUTGOING_CALL":
this.customerNo = data.otherLegNumber;
this.restoreTime(origin);
break;
case "INCOMING_CALL":
this.customerNo = data.otherLegNumber;
//播放来电振铃音
this.playRingMedia();
this.restoreTime(origin);
break;
case "IN_CALL":
this.stopPlayRingMedia();
this.restoreTime(origin);
// this.timer = setInterval(() => {
// }, 1000);
console.log('当前是否hold', this.isHold)
if (this.isHold) {
setTimeout(() => {
this.handleAgentBarBtnClick('hold')
this.isHold = false
// 方法区
}, 500);
}
break;
case "CALL_END":
this.stopPlayRingMedia();
//挂机铃声
this.playHangupMedia()
this.restoreTime(origin);
break;
default:
}
},
//播放挂机铃声
playHangupMedia() {
const _this = this;
var hangupAudio = document.getElementById("hangupMediaAudioId")
if (!hangupAudio) {
hangupAudio = document.createElement('audio');
hangupAudio.id = 'hangupMediaAudioId';
hangupAudio.hidden = true;
hangupAudio.src = 'wav/hangup.wav'
document.body.appendChild(hangupAudio);
}
hangupAudio.play();
},
//播放来电振铃
playRingMedia() {
const _this = this;
_this.stopPlayRingMedia();
var ringAudio = document.getElementById("ringMediaAudioId")
if (!ringAudio) {
ringAudio = document.createElement('audio');
ringAudio.id = 'ringMediaAudioId';
ringAudio.hidden = true;
ringAudio.src = 'wav/ring.wav';
ringAudio.loop = 'loop';
document.body.appendChild(ringAudio);
}
ringAudio.play();
},
//停止播放来电振铃
stopPlayRingMedia() {
const _this = this;
var ringAudio = document.getElementById("ringMediaAudioId");
if (ringAudio) {
document.body.removeChild(ringAudio);
}
},
//初始化方法
init() {
this.config.extNo = this.userName
if (sessionStorage.getItem("freeSwitchWs") != null) {
this.config.host = sessionStorage.getItem("freeSwitchWs").split(":")[0]
this.config.port = sessionStorage.getItem("freeSwitchWs").split(":")[1]
}
Ctibar.initSDK(this.config)
let url = `/ws/` + this.userId + '/group/device/push';
this.websocket = new webSocketClass(url)
this.websocket.getWebSocketMsg(evt => {
// 客户端接收服务端返回的数据
var data = JSON.parse(evt.data);
console.log("websocket返回的数据123:", data);
switch (data.flag) {
case "group"://会议状态
this.$refs.organizationTree.setGroupList(data, this.groupId)
console.log("websocket返回的数据123:", data);
break
case "memberStatus"://成员状态
for (const i in this.deviceList) {
console.log('123123132', data.member);
if (this.deviceList[i].imei == data.member) {
this.deviceList[i].devStatus = data.memberStatus;
}
}
break
case "memberMeetingStatus"://成员会议状态
for (const i in this.deviceList) {
if (this.deviceList[i].imei == data.member) {
this.deviceList[i].memberStatus = data.memberMeetingStatus;
}
}
if (this.groupId == data.groupId && this.userName == data.member && this.createBy == data.member) {
if (data.memberMeetingStatus == 3) {
this.isSpeak = true;
this.unhold()
console.log('数据this.isInOrOutMeeting1111', this.isSpeak)
} else {
this.isSpeak = false;
this.hold()
}
}
if (data.memberMeetingStatus == 4) {
this.loading.close()
}
if (!this.isInOrOutMeeting && this.groupId == data.groupId && this.userName == data.member && this.createBy == data.member && data.memberMeetingStatus == 2) {
this.hold()
if (sessionStorage.getItem("groupId") &&
sessionStorage.getItem("groupId") !== this.groupId) {
} else {
console.log('this.isInOrOutMeeting1111', data.memberMeetingStatus)
this.call()
}
}
if (this.createBy == data.member && data.memberMeetingStatus == 1 && this.userName == data.member) {
this.handleAgentBarBtnClick('hangup')
sessionStorage.setItem("groupId", '')
this.isInOrOutMeeting = false
this.isSpeak = false
this.isHold = true
}
break
}
})
},
setGroup(group) {
this.isGroup = group
},
call() {
this.handleAgentBarBtnClick('makecall')
sessionStorage.setItem("groupId", this.groupId)
this.isInOrOutMeeting = true
if (this.deviceList[0].imei != this.createBy) {
// this.groupDeviceItemClick(this.item)
}
var that = this
window.onbeforeunload = (e) => {
console.log('this.isInOrOutMeeting', that.isInOrOutMeeting)
if (that.isInOrOutMeeting) {
that.handleAgentBarBtnClick('hangup')
sessionStorage.setItem("groupId", '')
that.websocket.closeSocket()
window.onbeforeunload = null
}
}
this.loading.close()
this.isInOrOutMeeting = true
this.isHold = true
}
},
beforeDestroy() { //进行监听销毁
console.log('1231232131232132131,', this.isInOrOutMeeting)
if (this.isInOrOutMeeting) {
this.handleAgentBarBtnClick('hangup')
sessionStorage.setItem("groupId", '')
this.websocket.closeSocket()
window.onbeforeunload = null
}
},
};
</script>