| | |
| | | export * from './indicator' |
| | | export * from './Problemspeaking' |
| | | export * from './Followup' |
| | | export * from './general' |
| | | export * from './publicity' |
| | | export * from './Qtemplate' |
| | | export * from './questionnaire' |
| | | export * from './SingleTask' |
| | | export * from './external' |
| | | export * from './patientexternal' |
| | | export * from './EChartsdata' |
| | | export * from './satisfactionse' |
| | | export * from './satisfaction' |
| | | |
| | | export * from "./indicator"; |
| | | export * from "./Problemspeaking"; |
| | | export * from "./Followup"; |
| | | export * from "./general"; |
| | | export * from "./publicity"; |
| | | export * from "./Qtemplate"; |
| | | export * from "./questionnaire"; |
| | | export * from "./SingleTask"; |
| | | export * from "./external"; |
| | | export * from "./patientexternal"; |
| | | export * from "./EChartsdata"; |
| | | export * from "./satisfactionse"; |
| | | export * from "./satisfaction"; |
| | | export * from "./phoneCall"; |
¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | |
| | | // å é¤å¤é¨æ£è
表 |
| | | export function CallgetList() { |
| | | return request({ |
| | | url: "/smartor/ServiceTelInfo/getList", |
| | | method: "get", |
| | | }); |
| | | } |
| | | // æ¥è¯¢å¤é¨æ£è
表 |
| | | export function CallsetState(data) { |
| | | return request({ |
| | | url: "/smartor/ServiceTelInfo/setState", |
| | | method: "get", |
| | | params: data, |
| | | }); |
| | | } |
| | |
| | | |
| | | <script> |
| | | import sipService from "@/utils/sipService"; |
| | | import { CallsetState, CallgetList } from "@/api/AiCentre/index"; |
| | | |
| | | export default { |
| | | props: { |
| | |
| | | }, |
| | | }, |
| | | data() { |
| | | const randomNum = Math.floor(Math.random() * 20) + 1000; // å
é¨å®ä¹ |
| | | const randomNum = Math.floor(Math.random() * 20) + 1000; // å®ä¹éæºåæºå· |
| | | return { |
| | | isCalling: false, |
| | | randomNum: randomNum, |
| | | randomID: null, |
| | | callStatus: "idle", // idle, calling, connected, ended |
| | | sipStatus: "æªè¿æ¥", |
| | | sipStatusClass: "status-disconnected", |
| | | sipConfig: { |
| | | wsUrl: "wss://192.168.100.6:7443", |
| | | sipUri: `${randomNum}` + "@192.168.100.6", |
| | | sipUri: "", |
| | | password: "Smartor@2023", |
| | | displayName: "Web å°é¾", |
| | | // realm: "9.208.5.18:8090", |
| | |
| | | return this.isCalling ? "éè¯ä¸..." : "ä¸é®å¼å«"; |
| | | }, |
| | | }, |
| | | mounted() { |
| | | console.log('å½ååæºå·',this.sipConfig); |
| | | created() { |
| | | // CallgetList(); |
| | | }, |
| | | |
| | | async mounted() { |
| | | await this.CallgetList(); |
| | | sipService.init(this.sipConfig); |
| | | // è®¾ç½®ç¶æåè° |
| | | sipService.onStatusChange = (status) => { |
| | | this.sipStatus = status.text; |
| | | this.sipStatusClass = `status-${status.type}`; |
| | | |
| | | // å¤ç注å失败åæå¼è¿æ¥æ
åµ |
| | | if (status.type === "failed" || status.type === "disconnected") { |
| | | this.overCallsetState(); // éæ¾åæºå· |
| | | } |
| | | }; |
| | | |
| | | // çå¬éè¯ç¶æåå |
| | |
| | | this.isCalling = true; |
| | | console.log("å¼å§å¼å«ï¼", sipService); |
| | | |
| | | await sipService.makeCall("0"+this.phoneNumber); |
| | | await sipService.makeCall("0" + this.phoneNumber); |
| | | } catch (error) { |
| | | let registrationTime = Date.now(); // è®°å½æ³¨éæåæ¶é´ |
| | | console.log(registrationTime, "å¼å«å¤±è´¥æ¶é´"); |
| | |
| | | // this.callStatus = "ended"; |
| | | // this.isCalling = false; |
| | | //this.$message.error(`å¼å«å¤±è´¥: ${error.message}`); |
| | | try { |
| | | try { |
| | | // å
æ£æ¥æ¯å¦å¯ä»¥å¼å« |
| | | const { canCall, reason } = sipService.canMakeCall(); |
| | | if (!canCall) { |
| | |
| | | this.isCalling = true; |
| | | console.log("å¼å§å¼å«ï¼", sipService); |
| | | |
| | | await sipService.makeCall("0"+this.phoneNumber); |
| | | await sipService.makeCall("0" + this.phoneNumber); |
| | | } catch (error) { |
| | | this.callStatus = "ended"; |
| | | this.isCalling = false; |
| | | this.callStatus = "ended"; |
| | | this.isCalling = false; |
| | | } |
| | | } |
| | | }, |
| | | // æ¥è¯¢å¯ç¨åæºå· |
| | | async CallgetList() { |
| | | try { |
| | | const res = await CallgetList(); |
| | | this.randomNum = res.data[0].tel; |
| | | this.randomID = res.data[0].id; |
| | | // æ£ç¡®è®¾ç½® sipUri |
| | | this.sipConfig.sipUri = `${this.randomNum}@192.168.100.6`; |
| | | this.startCallsetState(); |
| | | } catch (error) { |
| | | console.error("è·ååæºå·å¤±è´¥:", error); |
| | | this.updateStatus("failed", "è·ååæºå·å¤±è´¥"); |
| | | } |
| | | }, |
| | | async startCallsetState() { |
| | | try { |
| | | await CallsetState({ id: this.randomID, state: 1 }); |
| | | console.log("åæºå·ç¶ææ´æ°ä¸ºä½¿ç¨ä¸"); |
| | | } catch (error) { |
| | | console.error("æ´æ°åæºå·ç¶æå¤±è´¥:", error); |
| | | } |
| | | }, |
| | | |
| | | async overCallsetState() { |
| | | try { |
| | | if (this.randomID) { |
| | | await CallsetState({ id: this.randomID, state: 0 }); |
| | | console.log("åæºå·ç¶ææ´æ°ä¸ºå¯ç¨"); |
| | | } |
| | | } catch (error) { |
| | | console.error("éæ¾åæºå·å¤±è´¥:", error); |
| | | } |
| | | }, |
| | | endCall() { |
| | | sipService.endCall(); |
| | | this.callStatus = "ended"; |
| | | this.isCalling = false; |
| | | }, |
| | | cleanupResources() { |
| | | // ç»æéè¯ |
| | | if (this.isCalling) { |
| | | sipService.endCall(); |
| | | } |
| | | |
| | | // éæ¾åæºå· |
| | | this.overCallsetState(); |
| | | |
| | | // æå¼ SIP è¿æ¥ |
| | | if (sipService.ua) { |
| | | sipService.ua.stop(); |
| | | } |
| | | }, |
| | | }, |
| | | beforeUnmount() { |
| | | // ç»ä»¶éæ¯æ¶ç¡®ä¿éæ¾èµæº |
| | | this.cleanupResources(); |
| | | }, |
| | | }; |
| | | </script> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | import JsSIP from "jssip"; |
| | | |
| | | class SipService { |
| | | constructor() { |
| | | this.ua = null; |
| | | this.currentSession = null; |
| | | this.onStatusChange = null; // ç¶æåååè° |
| | | this.onCallStatusChange = null; // æ°å¢éè¯ç¶æåè° |
| | | } |
| | | |
| | | // åå§åSIP客æ·ç«¯ |
| | | init(config) { |
| | | try { |
| | | this.updateStatus("connecting", "è¿æ¥ä¸..."); |
| | | console.log(config); |
| | | |
| | | 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, // æå¤§éè¿é´é |
| | | }); |
| | | |
| | | this.ua.start(); |
| | | |
| | | // 注åäºä»¶çå¬ |
| | | this.ua.on("registered", () => { |
| | | this.updateStatus("registered", "已注å"); |
| | | }); |
| | | |
| | | this.ua.on("registrationFailed", (e) => { |
| | | this.updateStatus("failed", `注å失败: ${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); |
| | | } |
| | | } |
| | | 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}`); |
| | | if (this.onStatusChange) { |
| | | this.onStatusChange({ type, text }); |
| | | } |
| | | } |
| | | |
| | | // ä¸é®æ¨å· - å¢å 注åç¶ææ£æ¥ |
| | | makeCall(targetNumber) { |
| | | if (!this.ua) { |
| | | throw new Error("SIP客æ·ç«¯æªåå§å"); |
| | | } |
| | | |
| | | if (!this.ua.isRegistered()) { |
| | | 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) => { |
| | | this.updateCallStatus("calling", "å¼å«ä¸..."); |
| | | }, |
| | | failed: (e) => { |
| | | this.updateCallStatus("ended", `å¼å«å¤±è´¥: ${e.cause}`); |
| | | }, |
| | | ended: (e) => { |
| | | this.updateCallStatus("ended", "éè¯ç»æ"); |
| | | }, |
| | | confirmed: (e) => { |
| | | this.updateCallStatus("connected", "éè¯å·²æ¥é"); |
| | | }, |
| | | }, |
| | | 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 |
| | | ); |
| | | // å¨ä¼è¯å建åä¿®æ¹ SDP |
| | | this.currentSession.on("peerconnection", (pc) => { |
| | | this.updateCallStatus('calling', 'å¼å«ä¸...'); |
| | | 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.currentSession.on('failed', (e) => { |
| | | this.updateCallStatus('failed', `å¼å«å¤±è´¥2: ${e}`); |
| | | }); |
| | | |
| | | this.currentSession.on('ended', () => { |
| | | this.updateCallStatus('ended', 'éè¯å·²ç»æ'); |
| | | }); |
| | | |
| | | this.currentSession.on('confirmed', () => { |
| | | this.updateCallStatus('connected', 'éè¯å·²æ¥é'); |
| | | }); |
| | | 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.updateCallStatus('ended', 'éè¯å·²ç»æ'); |
| | | this.currentSession = null; |
| | | } |
| | | } |
| | | // æ°å¢æ¹æ³ï¼æ´æ°éè¯ç¶æ |
| | | updateCallStatus(type, text) { |
| | | console.log(`éè¯ç¶ææ´æ°: ${type} - ${text}`); |
| | | if (this.onCallStatusChange) { |
| | | this.onCallStatusChange({ type, text }); |
| | | } |
| | | } |
| | | } |
| | | |
| | | export default new SipService(); |
| | |
| | | constructor() { |
| | | this.ua = null; |
| | | this.currentSession = null; |
| | | this.onStatusChange = null; // ç¶æåååè° |
| | | this.onCallStatusChange = null; // æ°å¢éè¯ç¶æåè° |
| | | this.onStatusChange = null; |
| | | this.onCallStatusChange = null; |
| | | this.onIncomingCall = null; |
| | | this.isRegistered = false; // æ°å¢æ³¨åç¶ææ å¿ |
| | | this.registrationTime = 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, |
| | | iceServers: [], |
| | | 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, // æå¤§éè¿é´é |
| | | sessionExpires: 1800, |
| | | minSessionExpires: 90, |
| | | register_expires: 300, |
| | | }); |
| | | |
| | | this.ua.start(); |
| | | |
| | | // 注åäºä»¶çå¬ |
| | | // äºä»¶çå¬ |
| | | this.ua.on("registered", () => { |
| | | this.isRegistered = true; |
| | | this.registrationTime = Date.now(); // è®°å½æ³¨åæåæ¶é´ |
| | | console.log(this.registrationTime, "æ³¨åæ¶é´"); |
| | | |
| | | this.updateStatus("registered", "已注å"); |
| | | }); |
| | | |
| | | this.ua.on("registrationFailed", (e) => { |
| | | this.isRegistered = false; |
| | | this.updateStatus("failed", `注å失败: ${e.cause}`); |
| | | }); |
| | | |
| | | this.ua.on("disconnected", () => { |
| | | this.updateStatus("disconnected", "è¿æ¥æå¼"); |
| | | this.ua.on("unregistered", () => { |
| | | this.isRegistered = false; |
| | | let registrationTime = Date.now(); // è®°å½æ³¨éæåæ¶é´ |
| | | console.log(registrationTime, "æ³¨éæ¶é´"); |
| | | this.updateStatus("disconnected", "已注é"); |
| | | }); |
| | | |
| | | this.ua.on("connected", () => { |
| | | this.updateStatus("connecting", "éæ°è¿æ¥ä¸..."); |
| | | }); |
| | | |
| | | // ç嬿¥çµ |
| | | this.ua.on("newRTCSession", (data) => { |
| | | this.handleIncomingCall(data.session); |
| | | }); |
| | | 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; |
| | | } |
| | | } |
| | | handleIncomingCall(session) { |
| | | if (session.direction === "incoming") { |
| | | console.log("æ¥çµ:", session.remote_identity.uri.toString()); |
| | | // å¯ä»¥å¨è¿é触å UI éç¥ |
| | | if (this.onIncomingCall) { |
| | | this.onIncomingCall(session); |
| | | } |
| | | // æ°å¢æ¹æ³ï¼æ£æ¥æ¯å¦å¯ä»¥å¼å« |
| | | canMakeCall(minDelay = 2000) { |
| | | if (!this.isRegistered) { |
| | | return { canCall: false, reason: "SIPæªæ³¨åï¼æ æ³å¼å«" }; |
| | | } |
| | | } |
| | | |
| | | // æ´æ°ç¶æå¹¶éç¥UI |
| | | updateStatus(type, text) { |
| | | console.log(`SIPç¶ææ´æ°: ${type} - ${text}`); |
| | | if (this.onStatusChange) { |
| | | this.onStatusChange({ type, text }); |
| | | } |
| | | } |
| | | const now = Date.now(); |
| | | const timeSinceRegistration = now - this.registrationTime; |
| | | |
| | | // ä¸é®æ¨å· - å¢å 注åç¶ææ£æ¥ |
| | | if (timeSinceRegistration < minDelay) { |
| | | const remaining = minDelay - timeSinceRegistration; |
| | | return { |
| | | canCall: false, |
| | | reason: `注åæåï¼è¯·çå¾
${Math.ceil(remaining / 1000)} ç§ååå¼å«`, |
| | | }; |
| | | } |
| | | |
| | | return { canCall: true, reason: "" }; |
| | | } |
| | | makeCall(targetNumber) { |
| | | if (!this.ua) { |
| | | throw new Error("SIP客æ·ç«¯æªåå§å"); |
| | | const { canCall, reason } = this.canMakeCall(); |
| | | if (!canCall) { |
| | | return Promise.reject(new Error(reason)); |
| | | } |
| | | return new Promise((resolve, reject) => { |
| | | try { |
| | | if (!this.ua) { |
| | | throw new Error("SIP客æ·ç«¯æªåå§å"); |
| | | } |
| | | |
| | | if (!this.ua.isRegistered()) { |
| | | throw new Error("SIPæªæ³¨åï¼æ æ³å¼å«"); |
| | | } |
| | | if (!this.ua.isRegistered()) { |
| | | 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) => { |
| | | this.updateCallStatus("calling", "å¼å«ä¸..."); |
| | | }, |
| | | failed: (e) => { |
| | | this.updateCallStatus("ended", `å¼å«å¤±è´¥: ${e.cause}`); |
| | | }, |
| | | ended: (e) => { |
| | | this.updateCallStatus("ended", "éè¯ç»æ"); |
| | | }, |
| | | confirmed: (e) => { |
| | | this.updateCallStatus("connected", "éè¯å·²æ¥é"); |
| | | }, |
| | | }, |
| | | 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: [], |
| | | }, |
| | | }, |
| | | }; |
| | | const options = { |
| | | sessionTimers: true, // å¯ç¨ä¼è¯è®¡æ¶å¨ |
| | | sessionTimersExpires: 150, |
| | | 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.100.6`, |
| | | options |
| | | ); |
| | | // å¨ä¼è¯å建åä¿®æ¹ SDP |
| | | this.currentSession.on("peerconnection", (pc) => { |
| | | this.updateCallStatus('calling', 'å¼å«ä¸...'); |
| | | 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.currentSession = this.ua.call( |
| | | `sip:${targetNumber}@1192.170.66.107`, |
| | | 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; |
| | | } |
| | | }; |
| | | }); |
| | | this.currentSession.on('failed', (e) => { |
| | | this.updateCallStatus('failed', `å¼å«å¤±è´¥2: ${e}`); |
| | | }); |
| | | |
| | | this.currentSession.on('ended', () => { |
| | | this.updateCallStatus('ended', 'éè¯å·²ç»æ'); |
| | | }); |
| | | |
| | | this.currentSession.on('confirmed', () => { |
| | | this.updateCallStatus('connected', 'éè¯å·²æ¥é'); |
| | | }); |
| | | this.setupAudio(this.currentSession); |
| | | } |
| | | |
| | | 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 = `å¼å«å¤±è´¥3: ${e.cause || e.message}`; |
| | | } |
| | | |
| | | this.updateCallStatus("failed55", errorMessage); |
| | | reject(new Error(errorMessage)); |
| | | } |
| | | |
| | | setupAudio(session) { |
| | | session.connection.addEventListener("addstream", (e) => { |
| | | const audioElement = document.getElementById("remoteAudio"); |
| | |
| | | } |
| | | }); |
| | | } |
| | | // ææå½åéè¯ |
| | | |
| | | endCall() { |
| | | if (this.currentSession) { |
| | | if (this.currentSession) { |
| | | this.currentSession.terminate(); |
| | | this.updateCallStatus('ended', 'éè¯å·²ç»æ'); |
| | | 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(); |
| | |
| | | // è¿éè°ç¨ä½ ççä¿¡åé API |
| | | // å设 API 为 sendMsgï¼åæ°å¯è½éè¦æ ¹æ®å®é
æ
åµè°æ´ |
| | | sendMsg({ |
| | | phone: this.userform.telcode, // ç¡®ä¿çµè¯å·ç åæ®µæ£ç¡® |
| | | phone: this.userform.telcode, // ç¡®ä¿çµè¯å·ç åæ®µæ£ç¡® |
| | | content: this.smsContent, |
| | | }) |
| | | .then((res) => { |
| | |
| | | this.getTaskservelist(); |
| | | }, |
| | | }, |
| | | // deactivated() { |
| | | // console.log(11); |
| | | // }, |
| | | beforeRouteLeave(to, from, next) { |
| | | this.$refs.callButton.cleanupResources(); |
| | | next(); // ç¡®ä¿è°ç¨ nex |
| | | }, |
| | | // beforeRouteUpdate() { |
| | | // console.log(33); |
| | | // }, |
| | | }; |
| | | </script> |
| | | |
| | |
| | | @media screen and (max-width: 1200px), (min-resolution: 1.1dppx) { |
| | | flex-direction: column; |
| | | |
| | | .call-action, .manual-action { |
| | | .call-action, |
| | | .manual-action { |
| | | width: 100% !important; |
| | | } |
| | | } |
| | |
| | | margin: 10px; |
| | | padding: 15px; |
| | | |
| | | .topic-dev, .scriptTopic-dev { |
| | | .topic-dev, |
| | | .scriptTopic-dev { |
| | | margin-bottom: 15px; |
| | | } |
| | | } |
| | |
| | | |
| | | /* 为移å¨è®¾å¤ä¼åæ»å¨ä½éª */ |
| | | @media screen and (max-width: 768px) { |
| | | .Followuserinfo, .Followuserinfos { |
| | | .Followuserinfo, |
| | | .Followuserinfos { |
| | | padding: 15px; |
| | | margin: 5px; |
| | | } |
| | |
| | | |
| | | /* ç¼©æ¾æ£æµæ ·å¼ */ |
| | | @media screen and (min-resolution: 1.1dppx), |
| | | screen and (-webkit-min-device-pixel-ratio: 1.1), |
| | | screen and (max-width: 1200px) { |
| | | screen and (-webkit-min-device-pixel-ratio: 1.1), |
| | | screen and (max-width: 1200px) { |
| | | .action-container { |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .call-action, .manual-action { |
| | | .call-action, |
| | | .manual-action { |
| | | width: 100%; |
| | | } |
| | | |
| | | /* è°æ´å
é¨å
ç´ é´è· */ |
| | | .call-container, .Followuserinfos { |
| | | .call-container, |
| | | .Followuserinfos { |
| | | margin-bottom: 20px; |
| | | } |
| | | } |