jssip(呼叫,接听,重新协商等功能实现)
<template><div class="head"><a-button type="primary" @click="call">call</a-button><a-button type="primary">answer</a-button><a-button type="primary" @click="renegotiate">renegotiate</a-button><a-button type="primary" @click="terminate">terminate</a-button><a-button type="primary" @click="peerBtn">对等连接</a-button></div><div class="main"><div><video src="" id="selfVideo" controls></video><video src="" id="remoteVideo" controls></video><audio src="" id="audioElement" controls></audio></div><div class="message"></div></div>
</template><script setup>
import JsSIP from "jssip";
import { onMounted } from "vue";
let currentSession;
let userAgent;
let peer;
onMounted(() => {sip_init();
});
var msg_log;
function sip_init() {msg_log = {el: document.querySelector(".message"),log(msg) {console.log("msg", msg);this.el.innerHTML += `<span class="success">${new Date().toLocaleTimeString()}:${msg}</span></br>`;},error(msg) {console.log("error", msg);this.el.innerHTML += `<span class="error">${new Date().toLocaleTimeString()}:${msg}</span></br>`;},};const name = location.search.slice(6);if (!name) {return msg_log.error("location.search获取不到信息");}const selfVideo = document.querySelector("#selfVideo");const remoteVideo = document.querySelector("#remoteVideo");// 本地加载完成 对端加载完成const socket = new JsSIP.WebSocketInterface("ws://101.200.183.204:5062");const configuration = {sockets: [socket],uri: `sip:${name === "offer" ? 2001 : 2002}@172.24.66.100;transport=ws`,password: "67975111",register: true,session_timers: false,};var ua = new JsSIP.UA(configuration);ua.on("connected", () => msg_log.log("连线中"));ua.on("connecting", () => msg_log.log("接线中"));ua.on("disconnected", () => msg_log.error("取消连线"));ua.on("registered", () =>msg_log.log(`--${name === "offer" ? 2001 : 2002}注册成功`));ua.on("registrationExpiring", () => msg_log.log("注册即将到期,重新注册"));ua.on("registrationFailed", () => msg_log.error("注册失败"));ua.on("unregistered", () => msg_log.log("取消注册"));ua.on("sipEvent", () => msg_log.log("sipEvent"));ua.on("newRTCSession", function (data) {const { session, request, originator } = data;if (originator === "remote") {msg_log.log("对方打电话过来了~~~");} else {msg_log.log("拨打电话中~~~");}currentSession = session;session.on("accepted", () => msg_log.log("通话接受时候触发"));session.on("connecting", () => msg_log.log("通话连线时候触发"));session.on("sdp", () => msg_log.log("交换sdp信令事件触发"));session.on("failed", () => msg_log.log("通话失败事件触发"));session.on("reinvite", () => {openLocalCamera();msg_log.log("重新协商事件触发");audioElement.srcObject = null;if (session._connection.getLocalStreams().length > 0) {// 接听后,判断localStreamselfVideo.srcObject = session?._connection.getLocalStreams()[0];selfVideo.play();}if (session?._connection.getRemoteStreams().length > 0) {remoteVideo.srcObject = session?._connection.getRemoteStreams()[0];remoteVideo.play();}});session.on("progress", () => {if (originator === "remote") {msg_log.log("电话过来拉~~~~~~~~~··");// var flag = confirm("是否接听?");// if (!flag) {// ua?.terminateSessions();// return;// }session.answer({mediaConstraints: { audio: true, video: true },// mediaStream: localStream,});msg_log.log("我接听了");}msg_log.log("接听事件在progress中触发");});session.on("confirmed", () => {msg_log.log("呼叫确认--设置媒体流到音视频中");selfVideo.srcObject = null;remoteVideo.srcObject = null;const stream = new MediaStream();const receivers = currentSession.connection?.getReceivers();if (receivers)receivers.forEach((receiver) => stream.addTrack(receiver.track));audioElement.srcObject = stream;// 最后都要播放audioElement.oncanplay = () => {audioElement.play();};});session.on("peerconnection", (data) => {msg_log.log("对等连接事件触发");});session.on("connecting", (data) => {peer = session._connection;console.log(peer, data, "wewewewewewewew");msg_log.log("对等连接建立,connecting");});session.on("ended", () => msg_log.log("通话结束"));});userAgent = ua;ua.start();
}
const eventHandlers = {progress: function (e) {console.log("call is in progress");},failed: function (e) {console.log("call failed: ", e);},ended: function (e) {console.log("call ended : ", e);},confirmed: function (e) {console.log("call confirmed");},
};
function call() {const opt = {mediaConstraints: {audio: true,video: false,},eventHandlers,};msg_log.log("1001 呼叫");userAgent.call("sip:2002@101.200.183.204", opt);
}
// 重新协商
function renegotiate() {// openLocalCamera();var options = {useUpdate: false,rtcOfferConstraints: {mandatory: {OfferToReceiveAudio: true,OfferToReceiveVideo: true,},},pcConfig: {rtcpMuxPolicy: "negotiate",iceServers: [{ urls: ["stun:stun.l.google.com:19302"] }],},};currentSession.renegotiate(options, () => {audioElement.srcObject = null;if (currentSession._connection.getLocalStreams().length > 0) {// 接听后,判断localStreamselfVideo.srcObject = currentSession?._connection.getLocalStreams()[0];selfVideo.play();}if (currentSession?._connection.getRemoteStreams().length > 0) {remoteVideo.srcObject = currentSession?._connection.getRemoteStreams()[0];remoteVideo.play();}console.log("我重新协商成功了~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");});
}
function terminate() {currentSession.terminate();
}
//开启本地摄像头,并把当前的媒体流添加到peerConnection中
function openLocalCamera() {// 1.获取本地音视频流// 调用 getUserMedia API 获取音视频流let constraints = {video: true,// audio: true,// audio: {// // 设置回音消除// noiseSuppression: true,// // 设置降噪// echoCancellation: true,// }};navigator.mediaDevices.getUserMedia(constraints).then(gotLocalMediaStream).catch((err) => {console.log("getUserMedia 错误", err);});
}
async function gotLocalMediaStream(mediaStream) {selfVideo.srcObject = mediaStream;selfVideo.play();const mediaStreamTrack = mediaStream.getTracks()[0];const localStream = mediaStream;localStream.getTracks().forEach((track) => {peer.addTrack(track, mediaStream);});// 这里每次开启本地视频流都要重新发送offer到对端// await peer.setLocalDescription(offer);
}
//关闭本地的视频流
function closeLocalMedia() {peerConnection.removeTrack(this.rtpSender);
}
async function peerBtn() {console.log(peer, "peer");// const await navigator.const stream = await navigator.mediaDevices.getUserMedia({ video: true });stream.getTracks().forEach((track) => {peer.addTrack(track, stream);});selfVideo.srcObject = stream;selfVideo.play();// openLocalCamera();
}
// 在关闭视频流之后,video标签会显示最后一帧的画面,如果想使video标签展示初始状态,可以调用video.load()
// video.load(); //视频回到初始状态
</script><style lang="less">
.head {height: 50px;background-color: #ccc;display: flex;align-items: center;margin-bottom: 20px;button {margin: 0 10px;}
}
.main {display: flex;.message {margin-left: 100px;background-color: rgba(0, 0, 0, 0.729);width: 500px;border: 2px solid #000;span.success {color: green;}span.error {color: red;}}
}
</style>
http://localhost:8080/?type=offer 发送方
http://localhost:8080/?type=answer 接听方