| | |
| | | |
| | | 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: 150, |
| | | 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)); |
| | | } |
| | | |