| | |
| | | import JsSIP from 'jssip' |
| | | import JsSIP from "jssip"; |
| | | |
| | | class SipService { |
| | | constructor() { |
| | | this.ua = null |
| | | this.currentSession = null |
| | | this.onStatusChange = null // 状态变化回调 |
| | | this.ua = null; |
| | | this.currentSession = null; |
| | | this.onStatusChange = null; // 状态变化回调 |
| | | } |
| | | |
| | | // 初始化SIP客户端 |
| | | init(config) { |
| | | try { |
| | | this.updateStatus('connecting', '连接中...') |
| | | this.updateStatus("connecting", "连接中..."); |
| | | |
| | | this.ua = new JsSIP.UA({ |
| | | sockets: [new JsSIP.WebSocketInterface(config.wsUrl)], |
| | | uri: config.sipUri, |
| | | password: config.password, |
| | | display_name: config.displayName, |
| | | iceservers:[], |
| | | // realm: config.realm, |
| | | register: true, |
| | | session_expires:180, |
| | | sessionTimersExpires: 300, // 设置 Session-Expires=120(必须 >= Min-SE) |
| | | extraHeaders: [ |
| | | "Min-SE: 120", // 可选:显式告诉服务器你支持的最小值 |
| | | ], |
| | | register_expires: 300, // 注册有效期(秒) |
| | | connection_recovery_min_interval: 2, // 最小重连间隔 |
| | | connection_recovery_max_interval: 30 // 最大重连间隔 |
| | | }) |
| | | connection_recovery_max_interval: 30, // 最大重连间隔 |
| | | }); |
| | | |
| | | this.ua.start() |
| | | this.ua.start(); |
| | | |
| | | // 注册事件监听 |
| | | this.ua.on('registered', () => { |
| | | this.updateStatus('registered', '已注册') |
| | | }) |
| | | this.ua.on("registered", () => { |
| | | this.updateStatus("registered", "已注册"); |
| | | }); |
| | | |
| | | this.ua.on('registrationFailed', (e) => { |
| | | this.updateStatus('failed', `注册失败: ${e.cause}`) |
| | | }) |
| | | this.ua.on("registrationFailed", (e) => { |
| | | this.updateStatus("failed", `注册失败: ${e.cause}`); |
| | | }); |
| | | |
| | | this.ua.on('disconnected', () => { |
| | | this.updateStatus('disconnected', '连接断开') |
| | | }) |
| | | this.ua.on("disconnected", () => { |
| | | this.updateStatus("disconnected", "连接断开"); |
| | | }); |
| | | |
| | | this.ua.on('connected', () => { |
| | | this.updateStatus('connecting', '重新连接中...') |
| | | }) |
| | | this.ua.on("connected", () => { |
| | | this.updateStatus("connecting", "重新连接中..."); |
| | | }); |
| | | |
| | | // 监听来电 |
| | | this.ua.on('newRTCSession', (data) => { |
| | | this.handleIncomingCall(data.session) |
| | | }) |
| | | |
| | | this.ua.on("newRTCSession", (data) => { |
| | | this.handleIncomingCall(data.session); |
| | | }); |
| | | } catch (error) { |
| | | this.updateStatus('failed', `初始化失败: ${error.message}`) |
| | | console.error('SIP初始化失败:', error) |
| | | this.updateStatus("failed", `初始化失败: ${error.message}`); |
| | | console.error("SIP初始化失败:", error); |
| | | } |
| | | } |
| | | handleIncomingCall(session) { |
| | | if (session.direction === "incoming") { |
| | | console.log("来电:", session.remote_identity.uri.toString()); |
| | | // 可以在这里触发 UI 通知 |
| | | if (this.onIncomingCall) { |
| | | this.onIncomingCall(session); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 更新状态并通知UI |
| | | updateStatus(type, text) { |
| | | console.log(`SIP状态更新: ${type} - ${text}`) |
| | | console.log(`SIP状态更新: ${type} - ${text}`); |
| | | if (this.onStatusChange) { |
| | | this.onStatusChange({ type, text }) |
| | | this.onStatusChange({ type, text }); |
| | | } |
| | | } |
| | | |
| | | // 一键拨号 - 增加注册状态检查 |
| | | makeCall(targetNumber) { |
| | | if (!this.ua) { |
| | | throw new Error('SIP客户端未初始化') |
| | | throw new Error("SIP客户端未初始化"); |
| | | } |
| | | |
| | | if (!this.ua.isRegistered()) { |
| | | throw new Error('SIP未注册,无法呼叫') |
| | | throw new Error("SIP未注册,无法呼叫"); |
| | | } |
| | | |
| | | const options = { |
| | | sessionTimers: true, |
| | | sessionTimersExpires: 300, |
| | | extraHeaders: [ |
| | | "Min-SE: 120", |
| | | "Route: <sip:@192.168.100.6>", |
| | | "Accept: application/sdp", |
| | | "Supported: replaces, timer", |
| | | "Allow: INVITE, ACK, BYE, CANCEL, OPTIONS", |
| | | ], |
| | | eventHandlers: { |
| | | progress: (e) => console.log('呼叫中...'), |
| | | failed: (e) => console.error('呼叫失败:', e), |
| | | ended: (e) => console.log('通话结束'), |
| | | confirmed: (e) => console.log('通话已接通') |
| | | progress: (e) => console.log("呼叫中..."), |
| | | failed: (e) => console.error("呼叫失败:", e), |
| | | ended: (e) => console.log("通话结束"), |
| | | confirmed: (e) => console.log("通话已接通"), |
| | | }, |
| | | mediaConstraints: { audio: true, video: false }, |
| | | rtcOfferConstraints: { offerToReceiveAudio: 1 } |
| | | } |
| | | mediaConstraints: { |
| | | audio: true, |
| | | video: false, |
| | | }, |
| | | rtcOfferConstraints: { |
| | | offerToReceiveAudio: 1, |
| | | offerToReceiveVideo: 0, |
| | | mandatory: { |
| | | OfferToReceiveAudio: true, |
| | | OfferToReceiveVideo: false, |
| | | }, |
| | | }, |
| | | pcConfig: { |
| | | iceServers: [{ urls: "stun:stun.l.google.com:19302" }], |
| | | iceTransportPolicy: "all", |
| | | bundlePolicy: "balanced", |
| | | rtcpMuxPolicy: "require", |
| | | codecs: { |
| | | audio: [ |
| | | { name: "PCMU", clockRate: 8000, payloadType: 0 }, |
| | | { name: "PCMA", clockRate: 8000, payloadType: 8 }, |
| | | ], |
| | | video: [], |
| | | }, |
| | | }, |
| | | }; |
| | | |
| | | this.currentSession = this.ua.call(`sip:${targetNumber}@192.168.100.6`, options) |
| | | this.setupAudio(this.currentSession) |
| | | this.currentSession = this.ua.call( |
| | | `sip:${targetNumber}@192.168.100.6`, |
| | | options |
| | | ); |
| | | // 在会话创建后修改 SDP |
| | | this.currentSession.on("peerconnection", (pc) => { |
| | | pc.createOffer = (offerOptions) => { |
| | | return RTCPeerConnection.prototype.createOffer |
| | | .call(pc, offerOptions) |
| | | .then((offer) => { |
| | | const modifiedSdp = offer.sdp |
| | | .replace(/c=IN IP4 192\.168\.100\.10/g, "c=IN IP4 192.168.100.6") |
| | | .replace(/m=audio \d+ RTP\/AVP.*/, "m=audio 7078 RTP/AVP 0 8"); |
| | | return new RTCSessionDescription({ |
| | | type: "offer", |
| | | sdp: modifiedSdp, |
| | | }); |
| | | }); |
| | | }; |
| | | }); |
| | | this.setupAudio(this.currentSession); |
| | | } |
| | | |
| | | setupAudio(session) { |
| | | session.connection.addEventListener("addstream", (e) => { |
| | | const audioElement = document.getElementById("remoteAudio"); |
| | | if (audioElement) { |
| | | audioElement.srcObject = e.stream; |
| | | } |
| | | }); |
| | | } |
| | | // 挂断当前通话 |
| | | endCall() { |
| | | if (this.currentSession) { |
| | | this.currentSession.terminate() |
| | | this.currentSession = null |
| | | this.currentSession.terminate(); |
| | | this.currentSession = null; |
| | | } |
| | | } |
| | | |
| | | // 处理音频流 |
| | | setupAudio(session) { |
| | | session.connection.addEventListener('addstream', (e) => { |
| | | const audioElement = document.getElementById('remoteAudio') |
| | | if (audioElement) { |
| | | audioElement.srcObject = e.stream |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 处理来电 |
| | | handleIncomingCall(session) { |
| | | if (session.direction === 'incoming') { |
| | | console.log('来电:', session.remote_identity.uri.toString()) |
| | | // 这里可以触发UI通知 |
| | | } |
| | | } |
| | | } |
| | | |
| | | export default new SipService() |
| | | export default new SipService(); |