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("通话被取消");
});
}
到目前为止前端代码基本已经实现完毕,
具体呼叫流程如下:
拨打呼叫:
呼叫结束
如果外部来电
版权归原作者 常生果 所有, 如有侵权,请联系我们删除。