¶Ô±ÈÐÂÎļþ |
| | |
| | | import JsSIP from "jssip"; |
| | | |
| | | class SipService { |
| | | constructor() { |
| | | this.ua = null; |
| | | this.currentSession = null; |
| | | this.onStatusChange = null; |
| | | this.onCallStatusChange = null; |
| | | this.onIncomingCall = null; |
| | | } |
| | | |
| | | init(config) { |
| | | try { |
| | | 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: [], |
| | | register: true, |
| | | sessionExpires: 1800, |
| | | minSessionExpires: 90, |
| | | register_expires: 300, |
| | | }); |
| | | |
| | | this.ua.start(); |
| | | |
| | | // äºä»¶çå¬ |
| | | this.ua.on("registered", () => |
| | | this.updateStatus("registered", "已注å56") |
| | | ); |
| | | this.ua.on("registrationFailed", (e) => |
| | | this.updateStatus("failed", `注å失败11: ${e.cause}`) |
| | | ); |
| | | this.ua.on("disconnected", () => |
| | | this.updateStatus("disconnected", "è¿æ¥æå¼") |
| | | ); |
| | | this.ua.on("connected", () => |
| | | this.updateStatus("connecting", "éæ°è¿æ¥ä¸...") |
| | | ); |
| | | this.ua.on("newRTCSession", (data) => |
| | | this.handleIncomingCall(data.session) |
| | | ); |
| | | } catch (error) { |
| | | this.updateStatus("failed", `åå§å失败: ${error.message}`); |
| | | console.error("SIPåå§å失败:", error); |
| | | throw error; |
| | | } |
| | | } |
| | | |
| | | makeCall(targetNumber) { |
| | | return new Promise((resolve, reject) => { |
| | | try { |
| | | if (!this.ua) { |
| | | throw new Error("SIP客æ·ç«¯æªåå§å"); |
| | | } |
| | | |
| | | if (!this.ua.isRegistered()) { |
| | | throw new Error("SIPæªæ³¨åï¼æ æ³å¼å«"); |
| | | } |
| | | |
| | | const options = { |
| | | sessionTimers: true, // å¯ç¨ä¼è¯è®¡æ¶å¨ |
| | | sessionTimersExpires: 90, |
| | | extraHeaders: ["Accept: application/sdp"], |
| | | mediaConstraints: { audio: true, video: false }, |
| | | rtcOfferConstraints: { |
| | | offerToReceiveAudio: true, |
| | | offerToReceiveVideo: false, |
| | | }, |
| | | eventHandlers: { |
| | | progress: () => this.updateCallStatus("calling", "å¼å«ä¸..."), |
| | | failed: (e) => { |
| | | this.handleCallFailure(e, reject); |
| | | }, |
| | | ended: () => this.updateCallStatus("ended", "éè¯ç»æ"), |
| | | confirmed: () => { |
| | | this.updateCallStatus("connected", "éè¯å·²æ¥é"); |
| | | resolve(); |
| | | }, |
| | | }, |
| | | }; |
| | | |
| | | this.currentSession = this.ua.call( |
| | | `sip:${targetNumber}@192.168.10.124`, |
| | | options |
| | | ); |
| | | |
| | | this.setupPeerConnection(this.currentSession); |
| | | this.setupAudio(this.currentSession); |
| | | } catch (error) { |
| | | this.updateCallStatus("failed", `å¼å«å¤±è´¥22: ${error.message}`); |
| | | reject(error); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | setupPeerConnection(session) { |
| | | session.on("peerconnection", (pc) => { |
| | | const originalCreateOffer = pc.createOffer.bind(pc); |
| | | |
| | | pc.createOffer = async (offerOptions) => { |
| | | try { |
| | | const offer = await originalCreateOffer(offerOptions); |
| | | return this.normalizeSDP(offer); |
| | | } catch (error) { |
| | | console.error("å建Offer失败:", error); |
| | | throw error; |
| | | } |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | normalizeSDP(offer) { |
| | | let sdp = offer.sdp; |
| | | |
| | | // æ ååSDP |
| | | sdp = sdp.replace(/c=IN IP4.*\r\n/, "c=IN IP4 0.0.0.0\r\n"); |
| | | sdp = sdp.replace( |
| | | /m=audio \d+.*\r\n/, |
| | | "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n" |
| | | ); |
| | | |
| | | // ç¡®ä¿å
å«åºæ¬ç¼è§£ç å¨ |
| | | if (!sdp.includes("PCMU/8000")) sdp += "a=rtpmap:0 PCMU/8000\r\n"; |
| | | if (!sdp.includes("PCMA/8000")) sdp += "a=rtpmap:8 PCMA/8000\r\n"; |
| | | |
| | | // æ·»å å¿
è¦å±æ§ |
| | | sdp += "a=rtcp-mux\r\n"; |
| | | sdp += "a=sendrecv\r\n"; |
| | | |
| | | console.log("æ åååçSDP:", sdp); |
| | | return new RTCSessionDescription({ |
| | | type: offer.type, |
| | | sdp: sdp, |
| | | }); |
| | | } |
| | | |
| | | handleCallFailure(e, reject) { |
| | | if (e.response?.status_code === 422) { |
| | | const serverMinSE = e.response.headers["Min-SE"]?.[0]?.raw || "æªç¥"; |
| | | console.error(`æå¡å¨è¦æ± Min-SE ⤠${serverMinSE}ï¼å½å设置: 120`); |
| | | } |
| | | console.error("å¼å«å¤±è´¥è¯¦æ
:", { |
| | | cause: e.cause, |
| | | message: e.message, |
| | | response: e.response && { |
| | | status: e.response.status_code, |
| | | reason: e.response.reason_phrase, |
| | | }, |
| | | }); |
| | | |
| | | let errorMessage = "å¼å«å¤±è´¥"; |
| | | switch (e.cause) { |
| | | case "Incompatible SDP": |
| | | errorMessage = "åªä½åå失败ï¼è¯·æ£æ¥ç¼è§£ç å¨é
ç½®"; |
| | | break; |
| | | case "488": |
| | | case "606": |
| | | errorMessage = "对æ¹è®¾å¤ä¸æ¯æå½ååªä½é
ç½®"; |
| | | break; |
| | | case "422": |
| | | errorMessage = "ä¼è¯åæ°ä¸æ»¡è¶³æå¡å¨è¦æ±"; |
| | | break; |
| | | default: |
| | | errorMessage = `å¼å«å¤±è´¥: ${e.cause || e.message}`; |
| | | } |
| | | |
| | | this.updateCallStatus("failed55", errorMessage); |
| | | reject(new Error(errorMessage)); |
| | | } |
| | | |
| | | 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.updateCallStatus("ended", "éè¯å·²ç»æ"); |
| | | this.currentSession = null; |
| | | } |
| | | } |
| | | |
| | | updateStatus(type, text) { |
| | | console.log(`SIPç¶ææ´æ°: ${type} - ${text}`); |
| | | if (this.onStatusChange) { |
| | | this.onStatusChange({ type, text }); |
| | | } |
| | | } |
| | | |
| | | updateCallStatus(type, text) { |
| | | console.log(`éè¯ç¶ææ´æ°: ${type} - ${text}`); |
| | | if (this.onCallStatusChange) { |
| | | this.onCallStatusChange({ type, text }); |
| | | } |
| | | } |
| | | |
| | | handleIncomingCall(session) { |
| | | if (session.direction === "incoming") { |
| | | console.log("æ¥çµ:", session.remote_identity.uri.toString()); |
| | | if (this.onIncomingCall) { |
| | | this.onIncomingCall(session); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | export default new SipService(); |