摘要:最近在做一个freeSwitch项目,前端需要通过sip协议完成音视频通话,把一些关键的核心api记录一下;因为网上找的一部分资料不一定准确,这个是实际操作过得具有一定的参考性;基本复制粘贴可快速完成直连freeSwitch的目的;更新日期2022-10-19;
前端sip这块使用的是jssip(版本3.9.1),详细api可以去官网查看;freeSwitch(1.10.7)这个我们只做参考;
注册
let ws = '你的freeSwitch直连地址';
let socket = new JsSIP.WebSocketInterface(ws);
let configuration = {
sockets: [socket],
uri: `sip:${username}@${serverip}`,
password: '',
authorizationUser: '',
sessionTimersExpires: 5000
}
let UA = new JsSIP.UA(configuration);
// 存储当前session和connection
let currentSession = null;
let currentConnection = null;
绑定事件
// 开始尝试连接
UA.on('connecting', () => {});
// 连接完毕
UA.on('connected', () => {});
// 主动取消注册或注册后定期重新注册失败
UA.on('unregistered', () => {});
// 注册失败
UA.on('registrationFailed', () => {});
// 注册成功
UA.on('registered', () => {});
// websocket 连接失败
UA.on('disconnected', () => {});
// 新消息接收和发送消息
UA.on('newMessage', (res) => {
console.log('接收和发送消息', res, '消息字段', res.data);
if (res.originator === 'remote') { // 远程消息
} else if ( res.originator === 'local') { // 本地消息
};
});
// 这一块处理webRTC音视频逻辑
UA.on('newRTCSession', (res) => {
let { session, originator } = res;
// 远程来电
if (originator === 'remote') { // 处理接听逻辑
handleAnswerWebRTCSession(session);
} else if (originator === 'local') { // 处理呼叫逻辑
handleCallWebRTCSession(session);
};
});
newRTCSession处理
// 处理接听newRTCSession
function handleAnswerWebRTCSession(session) {
/** session 要单独存下,后面接听挂断需要
挂断: session.terminate();
接听:session.answer({'mediaConstraints': { 'audio': true, 'video': true }})
*/
let {connection} = session;
let currentSession = session;
let currentConnection = connection;
// 来电-被接听了
session.on("accepted", () => {
handleStreamsSrcObject(connection);
});
session.on("peerconnection", () => {});
// 来电=>自定义来电弹窗,让用户选择接听和挂断
session.on("progress", () =>{});
// 挂断-来电已挂断
session.on("ended", () => {});
// 当会话无法建立时触发
session.on("failed", () => {});
}
// 处理呼叫newRTCSession
function handleCallWebRTCSession(session){
/** session 要单独存下挂断需要
挂断: session.terminate();
connection 如果后面音视频做禁音,或者解绑轨道,或共享桌面需要
*/
let {connection} = session;
let currentSession = session;
let currentConnection = connection;
session.on('progress', () => { // 呼叫中,响铃中
});
session.on('confirmed', () => {
handleStreamsSrcObject(connection);
});
}
// 处理媒体流
handleStreamsSrcObject(connection) {
if (connection.getRemoteStreams().length > 0) {
// 获取远程媒体流
let srcObject = connection.getRemoteStreams()[0];
};
if (connection.getLocalStreams().length > 0) {
// 获取本地媒体流
let srcObject = connection.getLocalStreams()[0];
};
}
判断当前session是否连接
judgeSessionState() { // 判断当前会话状态
return new Promise((resolve, reject) => {
let curSession = this.session;
if (curSession === null) {
reject({code: 0, message: '会话未建立'});
return;
};
if (curSession.isEnded()) {
reject({code: 0, message: '会话已结束'});
return;
};
// isEstablished_如果会话已建立; isInProgress_会话处于进行中状态;
if (curSession.isEstablished() || curSession.isInProgress()) {
resolve(curSession);
return;
};
// 保底情况
reject({code: 0, message: '会话超时或已被销毁'});
});
}
挂断/接听/呼叫
hangUp() {
// terminate({status_code: 603}); 可以指定status_code为603,收到来电拒绝可以发603
currentSession.terminate();
},
answer(){
currentSession.answer({'mediaConstraints': { 'audio': true, 'video': true }})
}
call(target, options = {isAudio: true, isVideo: true}, callback) {
let url = `sip:${target}@${serverip}`;
var eventHandlers = {
// 在接收或生成对INVITE请求的1XX SIP类响应(> 100)时触发
'progress': function(data){
callback('呼叫中');
// 如果成功发起会触发2次
const statusCode = data.response.status_code;
// 表示一个呼叫请求已经被接收,正在被处理,并且即将被传递给被叫方。
if (statusCode === 180) {
}
// 是一种会话进展消息,表示会话已经建立,但媒体尚未准备好。
if (statusCode === 183) {
}
},
// 会议无法建立时触发
'failed': function(data){
callback('无法建立', data);
},
// 通话确认(ACK收/发)时触发
'confirmed': function(data){
callback('已接听', data);
},
// 当已建立的通话结束时触发
'ended': function(){
callback('通话结束了');
}
}
this.UA.call(url, {
'eventHandlers': eventHandlers,
'mediaConstraints': { 'audio': options.isAudio, 'video': options.isVideo},
});
}
会话无法建立归纳
/*呼叫篇*/
'failed': function(data){ // 会议无法建立时触发
const {originator, cause, message } = data;
let errorText = '呼叫失败';
let errorCode = 'CallFailed';
// 本地错误
if (originator === 'local') {
if (cause === 'Canceled') {
errorText = '呼叫已取消';
errorCode = 'Canceled';
} if (cause === 'User Denied Media Access') {
errorText = '媒体设备权限被禁';
errorCode = 'NoMediaPower';
}
}
// 远端错误
if (originator === 'remote') {
const statusCode = message.status_code || '';
/* 如果是别的端拒接状态码都不一致,所以做了个范围都算是对方拒绝了邀请;
安卓_Linphone: Rejected; SIP 603
// 挂断指定code可以解决:terminate({status_code: 603});
WEB_JsSip:Unavailable; SIP 480
软电话拒接接听:Busy; SIP 487 对方忙
*/
if (statusCode === 603 || cause === 'Rejected') {
errorText = '对方拒绝了你的通话请求';
errorCode = 'Rejected';
} else if (statusCode === 486) {
errorText = '呼叫失败,对方在忙';
} else if (statusCode === 480) {
errorText = '呼叫失败,对方不在线';
}
}
}
/*接听篇*/
'failed': function(data){ // 会议无法建立时触发
const {originator, cause, message } = data;
let errorText = '接听失败';
let errorCode = 'AnswerFailed';
if (originator === 'local') {
if (cause === 'Rejected') {
errorCode = 'Rejected';
errorText = '自己拒绝接听';
} else if (cause === 'User Denied Media Access') {
errorText = '媒体设备权限被禁';
errorCode = 'NoMediaPower';
}
}
if (data.originator === 'remote') {
if (cause === 'Canceled') {
errorText = '对方已取消';
errorCode = 'Canceled';
}
}
}
屏幕共享-结束共享后切换原摄像头
// 共享桌面
desktopSharing(){
let peerConnection = currentConnection;
navigator.mediaDevices.getDisplayMedia({video: true, audio: true}).then((disStream) => {
// 把disStream存起来手动结束时候用得到
this.desktopMediaStream = disStream;
let srcObject = disStream;
peerConnection.getSenders().forEach((sender) => {
if (sender.track.kind == 'video') {
var res = sender.replaceTrack(disStream.getVideoTracks()[0]);
console.log(res);
};
});
/*
注意`onended`事件仅适用于轨道对象(例如视频轨道),而`oninactive`事件适用于整个媒体流对象
*/
// 1.监听屏幕结束方案1,也可以写成disStream.getVideoTracks()[0].onended = () => {}
disStream.getVideoTracks()[0].addEventListener('ended', () => {
// '屏幕共享结束, 准备切换为本地摄像头
this.switchLocalCamera(peerConnection);
});
// 2.监听屏幕共享结束方案2,项目中我们用的是这种,因为有时候需要手动结束屏幕共享;
disStream.oninactive = () => {
}
}).catch((error) => {
console.error('屏幕共享失败,失败原因:', error);
});
}
// 手动结束屏幕共享
endDesktopSharing() {
const desktopMediaStream = this.desktopMediaStream;
if (desktopMediaStream === null) {
return;
}
desktopMediaStream.getTracks().forEach((track) => { track.stop(); })
this.desktopMediaStream = null;
}
// 切换为本地摄像头
switchLocalCamera() {
let peerConnection = currentConnection;
let localStreams = curSession.connection.getLocalStreams();
if (localStreams.length) {
let localVideoTracks = localStreams[0].getVideoTracks();
let localAudioTracks = localStreams[0].getAudioTracks();
peerConnection.getSenders().forEach((sender) => {
if (sender.track && sender.track.kind == 'video') {
if (localVideoTracks.length) {
sender.replaceTrack(localVideoTracks[0]);
}
};
if (sender.track && sender.track.kind == 'audio') {
if (localAudioTracks) {
sender.replaceTrack(localAudioTracks[0]);
}
};
});
};
}
视频上传分辨率
// 视频上传分辨率
settingsScreenSize(constraints = { width: {exact: 50}, height: {exact: 50}}) {
let tmpLocal = currentConnection.getLocalStreams()[0];
const videoTrack = tmpLocal.getVideoTracks()[0];
videoTrack.applyConstraints(constraints).then(() => {
console.log('动态改变分辨率');
});
}
开启/关闭音频
// 禁音
mute(){
let pc = currentConnection;
if (pc.getSenders) {
pc.getSenders().forEach((sender) => {
// 如果是视频的话 kind === 'video'
if (sender.track.kind === 'audio') {
sender.track.enabled = false
}
})
} else {
pc.getLocalStreams().forEach((stream) => {
stream.getAudioTracks().forEach((track) => {
// 如果是视频的话 kind === 'video'
if (track.kind === 'audio') {
track.enabled = false;
}
})
});
}
}
// 解除禁音
unmute(){
var pc = currentConnection;
if (pc.getSenders) {
pc.getSenders().forEach(function (sender) {
// 如果是视频的话 kind === 'video'
if (sender.track.kind === 'audio') {
sender.track.enabled = true;
}
});
} else {
pc.getLocalStreams().forEach(function (stream) {
stream.getAudioTracks().forEach(function (track) {
// 如果是视频的话 kind === 'video'
if (track.kind === 'audio') {
track.enabled = true;
}
});
});
}
}
保持取消保持
// 接听
hold() { // 会话保持
return new Promise((resolve, reject) => {
if (!this.session) {
reject(false);
};
let isHold = this.session.hold({useUpdate: false}, () => {
resolve(true);
});
if (isHold) { // true 表示保持成功,你可以停止桌面共享,切换未本地摄像头
// this.switchLocalCamera();
return;
};
return reject({code: 0, message: '保持失败'});
});
}
unhold() { // 取消保持
return new Promise((resolve, reject) => {
if (!this.session) {
reject(false);
};
let isUnHold = this.session.unhold({useUpdate: false}, () => {
resolve(true);
});
if (isUnHold) {
return;
};
return reject({code: 0, message: '取消保持失败'});
});
}
结束是解除摄像头或麦克风占用
let peerConnection = currentConnection;
// 获取本地MediaStream对象
let localStreams = curSession.connection.getLocalStreams();
// 获取本地所有轨道
const curTracks = localStreams[0].getTracks();
// 解除轨道占用
curTracks.forEach(track => {
track.stop();
});