Vue3使用JsSip+FreeSwitch实现网页接打电话,及一些必踩的坑

前言

最近在做一个语音后台供客服人员使用,想摒弃以前的软电话,在网页实现和软电话一样的功能,所以就开始寻找解决方案,在网页上面使用语音视频就不得不说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库让上手难度降得很低,但其中的原理还要靠自己去了解。

相关推荐
凹凸曼打不赢小怪兽4 分钟前
react 受控组件和非受控组件
前端·javascript·react.js
狂奔solar15 分钟前
分享个好玩的,在k8s上部署web版macos
前端·macos·kubernetes
qiyi.sky17 分钟前
JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)
java·前端·笔记·学习·tomcat
清云随笔39 分钟前
axios 实现 无感刷新方案
前端
鑫宝Code40 分钟前
【React】状态管理之Redux
前端·react.js·前端框架
忠实米线1 小时前
使用pdf-lib.js实现pdf添加自定义水印功能
前端·javascript·pdf
pink大呲花1 小时前
关于番外篇-CSS3新增特性
前端·css·css3
少年维持着烦恼.1 小时前
第八章习题
前端·css·html
我是哈哈hh1 小时前
HTML5和CSS3的进阶_HTML5和CSS3的新增特性
开发语言·前端·css·html·css3·html5·web
田本初1 小时前
如何修改npm包
前端·npm·node.js