0


FreeSwitch通过WebRTC实现语音通话

WebRTC 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能。随着WebRTC技术的日益成熟,越来越多的开发者开始将其应用于项目中,以实现浏览器间的实时语音、视频通信。而FreeSwitch,作为一款开源的电话交换软件平台,为开发者提供了强大的通信功能。

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

javascripts语音实现WebRTC的开源库主要有JSSIP和SIP,引用这些库实现与FreeSwitch的通讯非常简单。webrtc传输信令依赖websocket协议,需要加密的,否则的是不允许调用音视频资源的。但是部署一套使用ssl证书加密的环境也是一件稍嫌复杂的事情,今后再分享实现。

本章主要使用ws而非wss实现开发环境下的简易环境配置和部署,主要的浏览器为谷歌浏览器。

那么,如何在FreeSwitch中启用WebRTC呢?本文将带您一步步完成配置:

第一步:浏览器设置允许http协议正常执行

第二步:FreeSwitch基本配置

conf/vars.xml 有两个开关关闭,本文未实现SSL

<X-PRE-PROCESS cmd="set" data="internal_ssl_enable=false" />
<X-PRE-PROCESS cmd="set" data="external_ssl_enable=false"/>

conf/sip_profiles/internal.xml 中确保下面两个配置打开

<param name="ws-binding" value=":5066"/>
<param name="wss-binding" value=":7443" /> 

修改/conf/vars.xml

 <X-PRE-PROCESS cmd="set" data="external_sip_ip=stun:stun.freeswitch.org"/> 
 <X-PRE-PROCESS cmd="set" data="external_rtp_ip=stun:stun.freeswitch.org"/>
 换成
 <X-PRE-PROCESS cmd="set" data="external_sip_ip=*.*.*.*"/>
 <X-PRE-PROCESS cmd="set" data="external_rtp_ip=xxx.xxx.xxx.xxx"/>

修改conf/sip_profiles/external.xml

  <param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
  <param name="ext-sip-ip" value="$${external_sip_ip}"/>

修改/conf/sip_profiles/internal.xml

<param name="ext-rtp-ip" value="$${external_rtp_ip}"/>
<param name="ext-sip-ip" value="$${external_sip_ip}"/>

延迟呼叫调整

NAT穿墙的调整

配置sip_profiles/internal.xm

<param name="apply-nat-acl" value="nat.auto"/>

注释掉sip_profiles/internal.xml中inbound-bypass-media配置

<!--<param name="inbound-bypass-media" value="true"/>-->

<param name="NDLB-force-rport" value="true"/>

到目前为止FreeSwitch基本配置完成,接下来写前端代码实现

目前本文主要使用JSSIP第三方包实现软电话呼叫,基本页面如下:

相关前端代码:

绑定事件

 // 绑定userAgent事件
    function setUAEvent() {
      // ws 开始尝试连接
      userAgent.on('connecting', (args) => {
        console.log('ws尝试连接');
      });

      // ws 连接完毕
      userAgent.on('connected', () => {
        console.log('ws连接完毕');
      });

      // ws 连接失败
      userAgent.on('disconnected', () => {
        console.log('ws连接失败');
      })

      // SIP 注册成功,data:Response JsSIP.IncomingResponse收到的SIP 2XX响应的实例
      userAgent.on('registered', e => {
        console.log('SIP注册成功')
      });

      //  SIP 注册失败,,data:Response JsSIP.IncomingResponse接收到的SIP否定响应的实例,如果失败是由这样的响应的接收产生的,否则为空
      userAgent.on('registrationFailed', e => {
        console.log('SIP注册失败',e)
      });
      
      //1.在注册到期之前发射几秒钟。如果应用程序没有为这个事件设置任何监听器,JsSIP将像往常一样重新注册。
      // 2.如果应用程序订阅了这个事件,它负责ua.register()在registrationExpiring事件中调用(否则注册将过期)。
      // 3.此事件使应用程序有机会在重新注册之前执行异步操作。对于那些在REGISTER请求中的自定义SIP头中使用外部获得的“令牌”的环境很有用。
      userAgent.on('registrationExpiring', function(){
         console.warn("registrationExpiring");
       });

      // SIP 取消注册
      userAgent.on('unregistered', e => {
        console.log('SIP主动取消注册或注册后定期重新注册失败')
      });

      userAgent.on('newRTCSession', function(data){
        console.log('onNewRTCSession: ', data);
                
        console.log(`新的${data.originator === 'local' ? '外呼' : '来电'}`, data);
         
        currentSession = data.session;        
        if(data.originator == 'remote'){ //incoming call
          console.log("incomingSession, answer the call");
          timeAudio.src = './public/ring.wav';
          timeAudio.play();
          
          const caller = data.request.from._uri.user;//
          const caller_dom = document.getElementById('caller');
          caller_dom.innerHTML="---"+caller+" 来电!---";
          
          const myPhone = document.getElementById('myPhone');
          myPhone.classList.add('shake-animation');
          
          answerSession(currentSession);//应答处理
        }else{//拨出去的来电
          timeAudio.src = './public/ringback.ogg';
          timeAudio.play();
          const calling = data.request.to._uri.user;//
          const caller_dom = document.getElementById('caller');
          caller_dom.innerHTML="---正在呼叫:"+calling+"---";
         
          callSession(currentSession);
        }
        
      });
      userAgent.on('newMessage', function(data){
         if(data.originator == 'local'){
            console.info('onNewMessage , OutgoingRequest - ', data.request);
         }else{
            console.info('onNewMessage , IncomingRequest - ', data.request);
         }
      });
    }
    

处理来电应答代码

//接听事件:来电应答session
    function answerSession(session) {
        session.on("progress", function(data) {
          console.log("来电提示");
        });
        session.on("ended", (data) => {
          console.log("来电挂断", data);
          var caller_dom = document.getElementById('caller');
          caller_dom.innerHTML="------";
          
          if(audio!=null){
              audio.pause();
          }
          timeAudio.pause();
          currentSession = null;//当前回话也初始化
          
          //只有来电会震动
          const myPhone = document.getElementById('myPhone');
          myPhone.classList.remove('shake-animation');    
          
        });
        session.on("failed", (data) => {
          console.log("无法建立通话");
          if(audio!=null){
              audio.pause();
          }
          timeAudio.pause();
          currentSession = null;
          
          //只有来电会震动
          const myPhone = document.getElementById('myPhone');
          myPhone.classList.remove('shake-animation');    
          
        });
        //实际工作中是没有任何意义的
        session.on('sdp', function(data){
            console.log('onSDP, type - ', data.type, ' sdp - ', data.sdp);
        });
        // 接听成功
        session.on('accepted', function(data){//接受
            console.log('onAccepted - ', data);
           if(data.originator == 'remote'){//去掉对方接受
              console.log('对方接听!');
           }else if(data.originator == 'local'){//来电--自己接受
              console.log('来电自己接听!');
           }
        });
          
        //确认呼叫后激发
        //顺序:对方接听(accepted)--》对方接受(handle onConfirmed)--》onConfirmed
        session.on('confirmed', function(data){//确认
            console.log('onConfirmed - ', data);
            if(data.originator == 'remote'){
            } 
            timeAudio.pause();
            
            //只有来电会震动
            const myPhone = document.getElementById('myPhone');
            myPhone.classList.remove('shake-animation');            
        });
        
        
        // 通话被挂起
        session.on('hold', (data) => {
          const org = data.originator;
          if (org === 'local') {
            console.log('通话被本地挂起:', org);
          } else {
            console.log('通话被远程挂起:', org);
          }
        });
        
        // 通话被继续
        session.on('unhold', (data) => {
          const org = data.originator;
          if (org === 'local') {
            console.log('通话被本地继续:', org)
          } else {
            console.log('通话被远程继续:', org);
          }
        });        
        //绑定通话取消事件
        session.on("canceled", () => {
            console.log("通话被取消");
            //只有来电会震动
            const myPhone = document.getElementById('myPhone');
            myPhone.classList.remove('shake-animation');    
        });    
    }
    

呼叫代码处理:

//接听事件:呼叫处理
    function callSession(session) {
        session.on("progress", () => {
            console.log("响铃中");
        });
        // 接听成功
        session.on('accepted', function(data){//接受
            console.log('onAccepted - ', data);
           if(data.originator == 'remote'){//去掉对方接受
              console.log('对方接听!');
           }else if(data.originator == 'local'){//来电--自己接受
              console.log('来电自己接听!');
           }
           if(timeAudio!=null){
               timeAudio.pause();
           }
        });
        
        /**
         * 这种方式的音频加载有问题
         * */
        session.on("confirmed", (data) => {
             console.log("已接听", data);
             const remoteStream = new MediaStream();         
             const receivers = session.connection.getReceivers();
             if (receivers){
                 receivers.forEach((receiver) =>{
                      if (receiver.track.kind === 'audio') {
                        remoteStream.addTrack(receiver.track)
                      }
                     
                 });
             }
             try {
                 audio.srcObject = remoteStream;
             } catch (error) {
                 audio.src = URL.createObjectURL(remoteStream);
             }
            audio.play();
        });
        
        session.on("failed", (data) => {
          console.log("无法建立通话");
          if(audio!=null){
              audio.pause();
          }
          timeAudio.pause();
          currentSession = null;
        });
        
        session.on("ended", (data) => {
          console.log("通话结束",data);
          
          var caller_dom = document.getElementById('caller');
          caller_dom.innerHTML="------";
          
          if(audio!=null){
              audio.pause();
          }
          timeAudio.pause();
          currentSession = null;//当前回话也初始化
        });
        
        
        // 通话被挂起
        session.on('hold', (data) => {
          const org = data.originator;
          if (org === 'local') {
            console.log('通话被本地挂起:', org);
          } else {
            console.log('通话被远程挂起:', org);
          }
        });
        
        // 通话被继续
        session.on('unhold', (data) => {
          const org = data.originator;
          if (org === 'local') {
            console.log('通话被本地继续:', org)
          } else {
            console.log('通话被远程继续:', org);
          }
        });
        
        //绑定通话取消事件
        session.on("canceled", () => {
            console.log("通话被取消");
        });    
        
    }

到目前为止前端代码基本已经实现完毕,

具体呼叫流程如下:

拨打呼叫:

呼叫结束

如果外部来电


本文转载自: https://blog.csdn.net/zhyooo123/article/details/141605044
版权归原作者 常生果 所有, 如有侵权,请联系我们删除。

“FreeSwitch通过WebRTC实现语音通话”的评论:

还没有评论