首页 > 其他分享 >【freeswitch】基于FreeSwitch实现的Webrtc VoIP Phone

【freeswitch】基于FreeSwitch实现的Webrtc VoIP Phone

时间:2022-11-01 09:45:12浏览次数:76  
标签:function pc track localStream var Phone freeswitch FreeSwitch response

最近有一个客户的呼叫中心项目,客户提出了一个强制性需求,要求坐席使用PC+Phone的方式来接听电话,而且最重要的是PC不能安装任何软件或者浏览器插件,研究了半天,似乎只有华山一条路了,那就是使用基于现代浏览器的webrtc音频通信技术了,可喜的是,Freeswtich作为语音服务器,已经天然支持webrtc技术了,而且成熟度很高,基本只要做些配置就好。

对于那些不熟悉webrtc技术的小伙伴,先简单的介绍下,引自百度:

“WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC 包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能。”

典型的webrtc技术栈如下图所示:

 

 

 

由于webrtc是基于浏览器实现的,所以开发使用天然的会会使用javascripts语音,感谢开源项目的贡献,已经多个基于webrtc的js封装库可用了,比较有名的有jssip,sip.js等,这次我们选用了sip.js作为客户端的开发基础库,来实现VoIP的通话控制,整个过程整体还是比较顺利的,当然免不了还是会有一些坑需要填,下面就结合代码来做一个说明。

首先就服务器来说,这块不是本文的重点,而且几乎是零配置的,不过有一点需要简单的说明下,由于现在浏览器对安全性的要求比较高,正常的业务使用,不管是http网页还是webrtc传输信令所要使用的websocket协议,均是要求加密的,否则的是不允许调用音视频资源的。但是部署一套使用ssl证书加密的环境也是一件稍嫌麻烦的事情,能不能在开发阶段免去这个步骤呢?答案是肯定的。本文以谷歌浏览器为例,说明下配置方法:

第一步: 浏览器地址栏输入: chrome://flags/#unsafely-treat-insecure-origin-as-secure
第二步: 如图配置:

 

 

 

第三步:权限配置成功,访问页面相关功能,授权允许麦克风。

然后我们就可以欢乐的使用sip.js的各种功能了,首先,我们包含下sip.js的代码:

<script src="./js/sip-0.13.8.js"></script>

然后,我们初始化并注册一个UAC到服务器:

var extCode = "1001"; //分机号
        var extPass = "1234"; //分机密码
        var config = {
            uri: "sip:" + extCode + '@119.1.2.3:5066',
            authorizationUser: extCode,
            password: extPass,
            displayName: extCode,
            log: {
                builtinEnabled: true,
                level: 3 // log日志级别
            },
            transportOptions: {
                wsServers: ['ws://119.1.2.3:5066'], //wss协议
                traceSip: true  //开启sip日志,用于排查问题
            },
            allowLegacyNotifications: true,
            hackWssInTransport: false, // 设置为true 则注册时 transport=wss; false:transport=ws;
 
            hackIpInContact: "192.168.0.2",
            userAgentString: "smarkdeskclient",
            registerOptions: {
                expires: 300,
                registrar: 'sip:registrar.mydomain.com',
            },
 
            contactName: "1001",
        };
        var ua = new SIP.UA(config);

外拨电话的示例,整个过程中最大的坑就在这里了,通话本身并没有问题,但是对于早期媒体的,默认sip.js并没有很好的支持,查看了官方的文档,sip.js对于早期媒体的支持仅限于100rel的方式,而目前的线路环境是通过183信令来带有早期媒体的sdp的,因此这块就有问题了,本来想用本地的语音媒体来代替,但这样的用户体验实在是太差了,没有办法只能自立更生,有坑填坑,通过查找了一些资料并研究了下sip.js的源码,终于解决了这个问题,核心的代码其实主要就在sessionall.on('progress')中,大家可以参考:

/**
         * 拨打电话
         */
        bindEvent(startCall, 'click', function () {
 
            var number = document.getElementById("number").value;
            //外拨呼叫
            sessionall = ua.invite(number, {
                sessionDescriptionHandlerOptions: {
                    constraints: {
                        audio: true,
                        video: false
                    },
                    alwaysAcquireMediaFirst: true 
                }
            });
 
            var remoteVideo = document.getElementById('remoteVideo');
            var localVideo = document.getElementById('localVideo');
 
            //处理接受183早期媒体
            sessionall.on('trackAdded', function () {
 
                var pc = this.sessionDescriptionHandler.peerConnection;
                var remoteStream;
 
                if (pc.getReceivers) {
                    remoteStream = new window.MediaStream();
                    pc.getReceivers().forEach(function (receiver) {
                        var track = receiver.track;
                        if (track) {
                            remoteStream.addTrack(track);
                        }
                    });
                } else {
                    remoteStream = pc.getRemoteStreams()[0];
                }
                remoteVideo.srcObject = remoteStream;
 
                var localStream_1;
                if (pc.getSenders) {
                    localStream_1 = new window.MediaStream();
                    pc.getSenders().forEach(function (sender) {
                        var track = sender.track;
                        if (track && track.kind === "video") {
                            localStream_1.addTrack(track);
                        }
                    });
                }
                else {
                    localStream_1 = pc.getLocalStreams()[0];
                }
                localVideo.srcObject = localStream_1;
            });
 
            //每次收到成功的最终(200-299)响应时都会触发。
            sessionall.on("accepted", function (response, cause) {
                console.log(response);
                var pc = this.sessionDescriptionHandler.peerConnection;
                var remoteStream;
 
                if (pc.getReceivers) {
                    remoteStream = new window.MediaStream();
                    pc.getReceivers().forEach(function (receiver) {
                        var track = receiver.track;
                        if (track) {
                            remoteStream.addTrack(track);
                        }
                    });
                } else {
                    remoteStream = pc.getRemoteStreams()[0];
                }
                remoteVideo.srcObject = remoteStream;
 
 
                var localStream_1;
                if (pc.getSenders) {
                    localStream_1 = new window.MediaStream();
                    pc.getSenders().forEach(function (sender) {
                        var track = sender.track;
                        if (track && track.kind === "video") {
                            localStream_1.addTrack(track);
                        }
                    });
                }
                else {
                    localStream_1 = pc.getLocalStreams()[0];
                }
                localVideo.srcObject = localStream_1;
            })
 
            //挂机时会触发
            sessionall.on("bye", function (response, cause) {
                console.log(response);
            })
 
            //请求失败时触发,无论是由于最终响应失败,还是由于超时,传输或其他错误。
            sessionall.on("failed", function (response, cause) {
                console.log(response);
            })
 
            /**
             *
             */
            sessionall.on("terminated", function (message, cause) {
            })
 
            /**
             * 对方拒绝
             */
            sessionall.on('rejected', function (response, cause) {
            })
 
            sessionall.on('progress', function (response) {
 
                if (response.statusCode === 183 && response.body && this.hasOffer && !this.dialog) {
                    if (!response.hasHeader('require') || response.getHeader('require').indexOf('100rel') === -1) {
                        if (this.sessionDescriptionHandler.hasDescription(response.getHeader('Content-Type'))) {
 
                            if (!this.createDialog(response, 'UAC')) { // confirm the dialog, eventhough it's a provisional answer
                                return
                            }
 
                            this.hasAnswer = true
 
                        
                            this.dialog.pracked.push(response.getHeader('rseq'))
 
                            
                            this.status = SIP.Session.C.STATUS_EARLY_MEDIA
 
                        
                            this.sessionDescriptionHandler
                                .setDescription(response.body, this.sessionDescriptionHandlerOptions, this.modifiers)
                                .catch((reason) => {
                                    this.logger.warn(reason)
                                    this.failed(response, C.causes.BAD_MEDIA_DESCRIPTION)
                                    this.terminate({ status_code: 488, reason_phrase: 'Bad Media Description' })
                                })
 
 
 
 
                        }
                    }
                }
            });
        })

再后面就是接听电话:

// 接受呼入会话
        ua.on('invite', function (session) {
 
            var url = session.remoteIdentity.uri.toString() + "来电了,是否接听";
 
            var remoteVideo = document.getElementById('remoteVideo');
            var localVideo = document.getElementById('localVideo');
 
 
            session.on("terminated", function (message, cause) {
                console.error(message);
 
            })
 
            /**
             *
             */
            session.on('accepted', function (response, cause) {
                console.error(response);
                console.error(session);
                Ring.stopRingTone();
                // If there is a video track, it will attach the video and audio to the same element
                var pc = this.sessionDescriptionHandler.peerConnection;
                console.error(this.sessionDescriptionHandler);
                var remoteStream;
 
                if (pc.getReceivers) {
                    remoteStream = new window.MediaStream();
                    pc.getReceivers().forEach(function (receiver) {
                        var track = receiver.track;
                        if (track) {
                            remoteStream.addTrack(track);
                        }
                    });
                } else {
                    remoteStream = pc.getRemoteStreams()[0];
                }
                remoteVideo.srcObject = remoteStream;
 
 
                var localStream_1;
                if (pc.getSenders) {
                    localStream_1 = new window.MediaStream();
                    pc.getSenders().forEach(function (sender) {
                        var track = sender.track;
                        if (track && track.kind === "video") {
                            localStream_1.addTrack(track);
                        }
                    });
                }
                else {
                    localStream_1 = pc.getLocalStreams()[0];
                }
                localVideo.srcObject = localStream_1;
                localVideo.volume = 0;
 
 
            })
 
            session.on('bye', function (resp, cause) {
            });
 
            var isaccept = confirm(url);
            if (isaccept) {
                //接受来电
                session.accept({
                    sessionDescriptionHandlerOptions: {
                        constraints: {
                            audio: true,
                            video: false
                        }
                    }
                });
                sessionall = session;
            }
            else {
                //拒绝来电
                session.reject();
            }
 
        });

其他的一些电话功能均再demo中实现,整个项目的demo已经开源到github,地址为webrtc phone,欢迎有需要的朋友参考,希望能对解决您的问题有所帮助!

【参考链接】

https://github.com/shanghaimoon888/webrtcphone/

 

标签:function,pc,track,localStream,var,Phone,freeswitch,FreeSwitch,response
From: https://www.cnblogs.com/opensmarty/p/16846661.html

相关文章

  • 1016 Phone Bills
    Along-distancetelephonecompanychargesitscustomersbythefollowingrules:Makingalong-distancecallcostsacertainamountperminute,dependingonthe......
  • uniapp does not have a method “getPhoneNumber“ to handle event “getphonenumbe
    ##获取手机号从基础库2.21.2开始,对获取手机号的接口进行了安全升级,以下是新版本接口使用指南。(旧版本接口目前可以继续使用,但建议开发者使用新版本接口,以增强小程序安全......
  • 再次“战胜库克”,无卡槽iPhone为何难不倒他们?
    「iPhone消费里的怪需求」近日,一则题为“华强北战胜库克”的视频,在网络上引发了网友们的热议。iPhone14在一个多月前上市时,网友们发现美版机型取消了实体SIM卡槽,采用了eS......
  • fs03 FreeSWITCH中常用概念
    阅读说明文中概念来自FreeSWITCH权威指南,FreeSWITCH官方文档等,仅作为阅读笔记记录,专业知识结构请阅读书籍和FreeSWITCH官网 1.媒体媒体简单来说就是RTP流,在S......
  • fs01 FreeSWITCH中APP和API
    PART1APP和API的区别 简单来说,一个APP是一个程序,它作为一个Channel一端与另一端的UA进行通信,相当于它工作在Channel内部;而一个API则是独立于一个Channel之外的,它只能通......
  • freeswitch-1.10.7 on centos7编译安装
      概述最近由于项目需求,老版本的fs已经不适用,特此升级了freeswitch的版本,使用当前最新的1.10.7版本编译安装。环境centos:CentOS release7.0(Final)或以上版本......
  • 快速启动freeswitch
    文档说明:只记录关键地方;试验环境:linuxdebian11目标:启动容器版freeswitchfreeswitchversion:"3"services:freeswitch:image:docker.io/wenba100xi......
  • 鸡肋的iphone听写功能
    转换准确率一般,有的根本不对,我还是用的普通话;转换时长太短,有次是几秒,有次是2分钟,不知道为啥自动停止了。弃。还是用有道云笔记吧,时间长,转换准。......
  • 在 Windows Phone上使用QQConnect OAuth2
    QQ互联OAuth2.0.NETSDK发布以及网站QQ登陆示例代码这篇文章讲述的普通的ASP.NET站点上使用QQ互联,本篇文章主要介绍在WindowsPhone环境使用QQ互联OAut......
  • 拆解:AFEM-8231和SKY58290-20前端模块 苹果iPhone 14Pro Max
    近期,iFixit对苹果最新iPhone14的拆解终于完成了,认为这次iPhone14最值得点赞的不是更强的处理器,也不是卫星SOS功能和更大的摄像头,而是完全重新设计的内部结构——显示面......