当前位置:首页 » 《关注互联网》 » 正文

FreeSwitch通过WebRTC实现语音通话

14 人参与  2024年11月26日 16:01  分类 : 《关注互联网》  评论

点击全文阅读


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);         }      });    }

处理来电应答代码

//接听事件:来电应答sessionfunction 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)--》onConfirmedsession.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("通话被取消");});}

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

具体呼叫流程如下:

拨打呼叫:

呼叫结束

如果外部来电


点击全文阅读


本文链接:http://zhangshiyu.com/post/192371.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1