前言
最近在做一个语音后台供客服人员使用,想摒弃以前的软电话,在网页实现和软电话一样的功能,所以就开始寻找解决方案,在网页上面使用语音视频就不得不说webrtc了,WebRTC (Web Real-Time Communication ) 是一个可以用在视频聊天,音频聊天或 P2P 文件分享等 Web App 中的 API。
这是小电话的配置,看起来是需要配置sip,sip服务器需要连接freeswitch,用户名和域名可与后端商议自定义,在网上查了一下找到两个库,一个是sipjs,看了下已经很久没更新了,一个是jssip,活跃度依旧很高,所以采用这个库,附上文档地址:https://jssip.net/, jssip里面已经集成了webrtc的相关代码,所以我们也不需要做处理。这样事情就变得简单了。
Jssip使用
此处使用jssip的版本为3.10.1。
注册和绑定事件
注册遇到的坑
Constants.d.ts
// 这是jssip里面的
export const SESSION_EXPIRES = 90
export const MIN_SESSION_EXPIRES = 60
但是FreeSWITCH 默认设置要求 Session Expires 不低于120
js
// 这样改就可以了
JsSIP.C.SESSION_EXPIRES = 120;
JsSIP.C.MIN_SESSION_EXPIRES = 120;
这样就可以连上fs了,但是坑又来了,在注册到fs后,contact出现了乱码的情况,这样就会不知道你的注册信息,也导致电话不能正常接打。
翻了一下源码,在Config.js里发现了这么一行via_host : '${Utils.createRandomToken(12)}.invalid'
,在这里有一个转成随机字符串的操作,你可以在这里将这个去掉,或者就用我下面代码里的方法也能解决。
typescript
class SipEvents {
private currentSession: RTCSession | null = null; // 接听挂断都会用到
private UA: JsSIP.UA | null = null; // 全局变量
private host = '服务地址';
public sipInit() {
// 只读属性,FreeSWITCH 默认设置要求 Session Expires 不低于120
JsSIP.C.SESSION_EXPIRES = 120;
JsSIP.C.MIN_SESSION_EXPIRES = 120;
const ws = 'freeSwitch直连地址';
const port = '端口';
const socket = new JsSIP.WebSocketInterface(ws);
let uri = new URI("sip", '分机号', this.host, port);
const configuration = {
sockets: [socket],
uri: uri.toString(),
password: "",
contact_uri: "",
};
uri.setParam("transport", "ws");
configuration.contact_uri = uri.toString();
this.UA = new JsSIP.UA(configuration);
this.UA.on("connecting", () => {
console.log("sip正在连接");
});
this.UA.on("connected", () => {
console.log("sip连接成功");
});
this.UA.on("unregistered", () => {
console.error("sip取消注册");
});
this.UA.on("registrationFailed", () => {
console.error("sip注册失败");
});
this.UA.on("registered", () => {
console.log("sip注册成功");
});
this.UA.on("newRTCSession", (res: RTCSessionEvent) => {
let { session, originator } = res;
// 远程来电
if (originator === "remote") {
// 处理接听逻辑
this.answerSession(session);
} else if (originator === "local") {
// 处理呼叫逻辑
this.callSession(session);
}
});
this.UA.start();
}
}
接听事件
typescript
private answerSession(session: RTCSession) {
// const { connection } = session;
this.currentSession = session;
// 来电-被接听了
session.on("accepted", () => {
console.log("来电接听");
// 上面解构不生效
this.handleStreamsSrcObject(session.connection);
});
session.on("peerconnection", (data) => {
console.log("peerconnection", data);
});
session.on("progress", () => {
console.log("来电提示");
});
session.on("ended", (data) => {
console.log("来电挂断", data);
});
session.on("failed", () => {
console.error("无法建立通话");
});
}
呼叫事件
typescript
private callSession(session: RTCSession) {
const { connection } = session;
this.currentSession = session;
session.on("progress", () => {
console.log("响铃中");
});
session.on("confirmed", (data: OutgoingAckEvent) => {
console.log("已接听", data);
this.handleStreamsSrcObject(connection);
});
session.on("ended", (data: EndEvent) => {
console.log("通话结束",data);
});
}
处理webrtc媒体流
ini
private handleStreamsSrcObject(connection: RTCPeerConnectionDeprecated) {
const stream = new MediaStream();
const receivers = connection?.getReceivers();
if (receivers){
receivers.forEach((receiver) => stream.addTrack(receiver.track));
}
// audioRef是页面的audio标签节点
if (audioRef.value) {
audioRef.value.srcObject = stream;
audioRef.value.play();
}
}
挂断事件
arduino
public hangUp() {
if (this.currentSession) {
this.currentSession.terminate();
}
}
接听拨打遇到的坑
pcConfig里需要配置ice服务,不然就会秒挂出现488错误
接听事件
php
public answer() {
this.currentSession.answer({
mediaConstraints: { audio: true, video: false },
pcConfig: {
iceServers: [
{
urls: "stun:stun.l.google.com:19302",
}
],
},
});
}
拨打事件
typescript
public call() {
const url = `fs地址`;
const eventHandlers = {
progress: (data: IncomingEvent) => {
console.log("呼叫中");
},
confirmed: () => {
console.log("已接听");
},
failed: (data: EndEvent) => {
console.error("无法建立");
},
ended: (data: EndEvent) => {
console.error("通话结束了",data);
},
};
this.UA?.call(url, {
eventHandlers,
mediaConstraints: {
audio: true,
video: false,
},
pcConfig: {
iceServers: [
{
urls: "stun:stun.l.google.com:19302",
},
],
},
});
}
退出事件
csharp
public out() {
this.UA.stop();
}
说到这里,sip的逻辑就结束了,上述代码就可以满足外呼和接听的功能了,其他的就是和后端对接联调了,然而这时,又有新的坑来了。
freeswitch报错488,音频编码的坑
由于和fs对接的时候需要把相关通话记录下来存储,并且到实际生产线路上是没法直接在页面输入号码调用jssip的外呼进行打电话的,实际情况是不管进线还是外呼都由fs进行分配,所以后端提供了一个外呼的api,在调用这个api的时候也就成了接听状态,这样两个状态就只能被动接听了,这时候诡异的事情出现了,在外呼调用api点击接听后,fs再中转拨出,对方点击接听后出现了秒挂的情况,在排查fs日志后,发现报错488音频编码不匹配,这里暂时的解决办法就是将音频编码固定,让两边统一,就不会出现接起秒挂了。
虽然jssip库让上手难度降得很低,但其中的原理还要靠自己去了解。