| | |
| | | props: { |
| | | phoneNumber: { |
| | | type: String, |
| | | default: '' |
| | | } |
| | | default: "", |
| | | }, |
| | | }, |
| | | data() { |
| | | const randomNum = Math.floor(Math.random() * 11) + 1000; // 内部定义 |
| | | return { |
| | | isCalling: false, |
| | | callStatus: 'idle', // idle, calling, connected, ended |
| | | callStatus: "idle", // idle, calling, connected, ended |
| | | sipStatus: "未连接", |
| | | sipStatusClass: "status-disconnected", |
| | | randomNum : Math.floor(Math.random() * 11) + 1000, // 生成 1000-1010 的随机整数 |
| | | sipConfig: { |
| | | wsUrl: "wss://192.168.10.124:7443", |
| | | sipUri: `${randomNum}`+"@192.168.10.124", |
| | | sipUri: `${randomNum}` + "@192.168.10.124", |
| | | password: "Smartor@2023", |
| | | displayName: "Web 小龙", |
| | | }, |
| | |
| | | computed: { |
| | | callStatusText() { |
| | | const statusMap = { |
| | | idle: '准备就绪', |
| | | calling: '呼叫中...', |
| | | connected: '通话中', |
| | | ended: '通话结束' |
| | | idle: "准备就绪", |
| | | calling: "呼叫中...", |
| | | connected: "通话中", |
| | | ended: "通话结束", |
| | | }; |
| | | return statusMap[this.callStatus]; |
| | | }, |
| | |
| | | }, |
| | | callButtonText() { |
| | | return this.isCalling ? "通话中..." : "一键呼叫"; |
| | | } |
| | | }, |
| | | }, |
| | | mounted() { |
| | | sipService.init(this.sipConfig); |
| | |
| | | // 监听通话状态变化 |
| | | sipService.onCallStatusChange = (status) => { |
| | | this.callStatus = status.type; |
| | | this.isCalling = status.type === 'calling' || status.type === 'connected'; |
| | | this.isCalling = status.type === "calling" || status.type === "connected"; |
| | | |
| | | // 通知父组件通话状态变化 |
| | | this.$emit('call-status-change', status); |
| | | this.$emit("call-status-change", status); |
| | | }; |
| | | }, |
| | | methods: { |
| | |
| | | } |
| | | |
| | | try { |
| | | this.callStatus = 'calling'; |
| | | this.callStatus = "calling"; |
| | | this.isCalling = true; |
| | | |
| | | await sipService.makeCall(this.phoneNumber); |
| | | |
| | | } catch (error) { |
| | | console.error("呼叫失败:", error); |
| | | this.callStatus = 'ended'; |
| | | this.callStatus = "ended"; |
| | | this.isCalling = false; |
| | | this.$message.error(`呼叫失败: ${error.message}`); |
| | | } |
| | |
| | | |
| | | endCall() { |
| | | sipService.endCall(); |
| | | this.callStatus = 'ended'; |
| | | this.callStatus = "ended"; |
| | | this.isCalling = false; |
| | | } |
| | | } |
| | | }, |
| | | }, |
| | | }; |
| | | </script> |
| | | |
| | |
| | | |
| | | 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: [{ urls: "stun:stun.l.google.com:19302" }], |
| | | iceServers: [], |
| | | register: true, |
| | | session_expires: 900, |
| | | sessionTimersExpires: 600, |
| | | extraHeaders: ["Min-SE: 120"], |
| | | sessionExpires: 1800, |
| | | minSessionExpires: 90, |
| | | register_expires: 300, |
| | | connection_recovery_min_interval: 2, |
| | | connection_recovery_max_interval: 30, |
| | | pcConfig: { |
| | | iceTransportPolicy: "all", |
| | | rtcpMuxPolicy: "require", |
| | | bundlePolicy: "max-bundle" |
| | | } |
| | | }); |
| | | |
| | | this.ua.start(); |
| | | |
| | | // 事件监听 |
| | | this.ua.on("registered", () => this.updateStatus("registered", "已注册")); |
| | | this.ua.on("registered", () => |
| | | this.updateStatus("registered", "已注册56") |
| | | ); |
| | | this.ua.on("registrationFailed", (e) => |
| | | this.updateStatus("failed", `注册失败: ${e.cause}`)); |
| | | this.updateStatus("failed", `注册失败11: ${e.cause}`) |
| | | ); |
| | | this.ua.on("disconnected", () => |
| | | this.updateStatus("disconnected", "连接断开")); |
| | | this.updateStatus("disconnected", "连接断开") |
| | | ); |
| | | this.ua.on("connected", () => |
| | | this.updateStatus("connecting", "重新连接中...")); |
| | | this.updateStatus("connecting", "重新连接中...") |
| | | ); |
| | | this.ua.on("newRTCSession", (data) => |
| | | this.handleIncomingCall(data.session)); |
| | | |
| | | this.handleIncomingCall(data.session) |
| | | ); |
| | | } catch (error) { |
| | | this.updateStatus("failed", `初始化失败: ${error.message}`); |
| | | console.error("SIP初始化失败:", error); |
| | |
| | | } |
| | | |
| | | const options = { |
| | | sessionTimers: false, // 暂时禁用以减少兼容性问题 |
| | | extraHeaders: [ |
| | | "Min-SE: 120", |
| | | "Accept: application/sdp", |
| | | "Supported: outbound" |
| | | ], |
| | | sessionTimers: true, // 启用会话计时器 |
| | | sessionTimersExpires: 90, |
| | | extraHeaders: ["Accept: application/sdp"], |
| | | mediaConstraints: { audio: true, video: false }, |
| | | rtcOfferConstraints: { |
| | | offerToReceiveAudio: true, |
| | | offerToReceiveVideo: false |
| | | offerToReceiveVideo: false, |
| | | }, |
| | | eventHandlers: { |
| | | progress: () => this.updateCallStatus("calling", "呼叫中..."), |
| | |
| | | confirmed: () => { |
| | | this.updateCallStatus("connected", "通话已接通"); |
| | | resolve(); |
| | | } |
| | | } |
| | | }, |
| | | }, |
| | | }; |
| | | |
| | | this.currentSession = this.ua.call( |
| | |
| | | |
| | | this.setupPeerConnection(this.currentSession); |
| | | this.setupAudio(this.currentSession); |
| | | |
| | | } catch (error) { |
| | | this.updateCallStatus("failed", `呼叫失败1: ${error.message}`); |
| | | this.updateCallStatus("failed", `呼叫失败22: ${error.message}`); |
| | | reject(error); |
| | | } |
| | | }); |
| | |
| | | normalizeSDP(offer) { |
| | | let sdp = offer.sdp; |
| | | |
| | | // 1. 标准化连接行 |
| | | // 标准化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" |
| | | ); |
| | | |
| | | // 2. 标准化音频媒体行 |
| | | 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"; |
| | | |
| | | // 3. 确保包含基本编解码器 |
| | | 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"; |
| | | } |
| | | |
| | | // 4. 添加必要属性 |
| | | // 添加必要属性 |
| | | sdp += "a=rtcp-mux\r\n"; |
| | | sdp += "a=sendrecv\r\n"; |
| | | |
| | | console.log("标准化后的SDP:", sdp); |
| | | return new RTCSessionDescription({ |
| | | type: offer.type, |
| | | sdp: sdp |
| | | 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 && e.response.status_code |
| | | response: e.response && { |
| | | status: e.response.status_code, |
| | | reason: e.response.reason_phrase, |
| | | }, |
| | | }); |
| | | |
| | | let errorMessage = "呼叫失败"; |
| | | switch(e.cause) { |
| | | switch (e.cause) { |
| | | case "Incompatible SDP": |
| | | errorMessage = "媒体协商失败,请检查编解码器配置"; |
| | | break; |
| | |
| | | case "606": |
| | | errorMessage = "对方设备不支持当前媒体配置"; |
| | | break; |
| | | case "422": |
| | | errorMessage = "会话参数不满足服务器要求"; |
| | | break; |
| | | default: |
| | | errorMessage = `呼叫失败: ${e.cause || e.message}`; |
| | | } |
| | | |
| | | this.updateCallStatus("failed2", errorMessage); |
| | | this.updateCallStatus("failed55", errorMessage); |
| | | reject(new Error(errorMessage)); |
| | | } |
| | | |