import JsSIP from "jssip"; import { Notification, MessageBox, Message, Loading } from "element-ui"; // 医院机构与SIP服务器映射配置 const HOSPITAL_CONFIG = { 丽水市中医院: { wsUrl: "wss://192.168.10.124:7443", domain: "192.168.10.124", }, 龙泉市人民医院: { wsUrl: "wss://10.10.0.220:7443", domain: "10.10.0.220", }, // 可以继续添加其他医院配置 default: { wsUrl: "wss://192.168.10.124:7443", domain: "192.168.10.124", }, }; class SipService { constructor() { this.ua = null; this.currentSession = null; this.onStatusChange = null; this.onCallStatusChange = null; this.onIncomingCall = null; this.isRegistered = false; // 新增注册状态标志 this.registrationTime = null; // 新增注册成功时间戳 this.currentConfig = null; // 存储当前配置 } // 获取医院配置方法 getHospitalConfig() { const orgName = localStorage.getItem("orgname"); return HOSPITAL_CONFIG[orgName] || HOSPITAL_CONFIG.default; } init(baseConfig) { try { // 获取机构名称,如果没有传入则从localStorage读取 const orgName = baseConfig.orgName || localStorage.getItem("orgname"); // 根据机构名称获取对应的服务器配置 const hospitalConfig = this.getHospitalConfig(orgName); console.log(hospitalConfig, "88"); // 合并配置 this.currentConfig = { ...baseConfig, ...hospitalConfig, }; console.log( `当前机构: ${orgName}, 使用服务器: ${this.currentConfig.domain}` ); this.updateStatus("connecting", "连接中..."); console.log(baseConfig.sipUri, "baseConfig.sipUri"); this.ua = new JsSIP.UA({ sockets: [new JsSIP.WebSocketInterface(this.currentConfig.wsUrl)], uri: baseConfig.sipUri, // 这里使用基础的sipUri,domain部分会被动态替换 password: baseConfig.password, display_name: baseConfig.displayName, iceServers: [], register: true, 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("unregistered", () => { this.isRegistered = false; let registrationTime = Date.now(); // 记录注销成功时间 console.log(registrationTime, "注销时间"); this.updateStatus("disconnected", "已注销"); }); 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; } } // 新增方法:检查是否可以呼叫 canMakeCall(minDelay = 2000) { if (!this.isRegistered) { return { canCall: false, reason: "SIP未注册,无法呼叫" }; } 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) { const { canCall, reason } = this.canMakeCall(); if (!canCall) { Message.error(reason); 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未注册,无法呼叫"); } const targetUri = `sip:${targetNumber}@${this.currentConfig.domain}`; console.log(`呼叫目标: ${targetUri}`); 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(targetUri, 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; // console.log("原始SDP:", sdp); // 调试用,捕获原始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, // }); // } // 在 SipService 类中新增方法,用于获取针对特定服务器的SDP处理策略 getSDPNormalizationStrategy(orgName) { const strategies = { 龙泉市人民医院: "conservative", // 保守策略:最小化修改,优先兼容 丽水市中医院: "aggressive", // 激进策略:保持原有强标准化逻辑 // 可以为其他机构添加更多策略 }; return strategies[orgName] || "moderate"; // 默认策略 } /** * 标准化SDP Offer - 修复龙泉市人民医院488错误 * 核心思路:从“强制覆盖”改为“智能修补”,针对不同服务器使用差异化策略 */ normalizeSDP(offer) { const orgName = localStorage.getItem("orgname"); const strategy = this.getSDPNormalizationStrategy(orgName); let sdp = offer.sdp; console.log(`[SDP标准化] 机构: ${orgName}, 策略: ${strategy}`); console.log("[SDP标准化] 原始SDP:", sdp); if (strategy === "conservative") { // ==================== 保守策略:针对龙泉市人民医院等严格服务器 ==================== // 原则:除非必要,否则不修改原有SDP结构,仅添加缺失的关键属性 // 1. 谨慎处理连接地址:仅在地址是明显内网地址时才修改 const privateIPRegex = /c=IN IP4 (192\.168|10\.|172\.(1[6-9]|2[0-9]|3[0-1]))/; if (privateIPRegex.test(sdp)) { sdp = sdp.replace(/c=IN IP4.*\r\n/, "c=IN IP4 0.0.0.0\r\n"); console.log("[SDP标准化] 已修改连接地址为 0.0.0.0"); } // 2. 保持媒体行原样,不强制修改端口和协议 // sdp = sdp.replace(/m=audio \d+.*\r\n/, "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n"); // 3. 智能添加基础编解码器映射(仅在缺失时添加) const codecMappings = [ { pt: 0, name: "PCMU/8000" }, { pt: 8, name: "PCMA/8000" }, ]; codecMappings.forEach((codec) => { const rtpmapPattern = `a=rtpmap:${codec.pt} ${codec.name}`; const payloadPattern = ` ${codec.pt} `; // 只有当SDP中包含该负载类型但缺少详细映射时才添加 if (sdp.includes(payloadPattern) && !sdp.includes(rtpmapPattern)) { sdp += `${rtpmapPattern}\r\n`; console.log(`[SDP标准化] 已添加编解码器映射: ${rtpmapPattern}`); } }); // 4. 条件性添加必要属性(避免重复) const essentialAttributes = [ { attr: "a=rtcp-mux", desc: "RTCP复用" }, { attr: "a=sendrecv", desc: "双向媒体流" }, ]; essentialAttributes.forEach((item) => { if (!sdp.includes(item.attr)) { sdp += `${item.attr}\r\n`; console.log(`[SDP标准化] 已添加属性: ${item.desc}`); } }); } else if (strategy === "aggressive") { // ==================== 激进策略:保持原有逻辑 ==================== 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"; } else { // ==================== 默认策略:平衡方案 ==================== // 适度修改,兼顾兼容性和功能性 sdp = sdp.replace(/c=IN IP4.*\r\n/, "c=IN IP4 0.0.0.0\r\n"); // 仅在媒体行格式明显异常时修改 if (!sdp.match(/m=audio \d+ RTP\/AVP/)) { sdp = sdp.replace( /m=audio \d+.*\r\n/, "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n" ); } // 智能添加缺失的属性 if (!sdp.includes("a=rtcp-mux")) sdp += "a=rtcp-mux\r\n"; if (!sdp.includes("a=sendrecv")) sdp += "a=sendrecv\r\n"; } console.log("[SDP标准化] 标准化后SDP:", sdp); return new RTCSessionDescription({ type: offer.type, sdp: sdp, }); } /** * 增强的SDP调试方法 - 用于对比分析 */ debugSDPComparison(originalOffer, normalizedOffer, context) { console.group(`[SDP调试] ${context}`); console.log( "原始SDP媒体行:", originalOffer.sdp.match(/m=audio.*\r\n/)?.[0] || "未找到" ); console.log( "标准化后媒体行:", normalizedOffer.sdp.match(/m=audio.*\r\n/)?.[0] || "未找到" ); console.log( "原始编解码器列表:", originalOffer.sdp.match(/a=rtpmap:\d+.*\r\n/g) || [] ); console.log( "标准化后编解码器列表:", normalizedOffer.sdp.match(/a=rtpmap:\d+.*\r\n/g) || [] ); console.groupEnd(); } // 在 setupPeerConnection 方法中集成调试功能 setupPeerConnection(session) { session.on("peerconnection", (pc) => { const originalCreateOffer = pc.createOffer.bind(pc); pc.createOffer = async (offerOptions) => { try { const offer = await originalCreateOffer(offerOptions); const normalizedOffer = this.normalizeSDP(offer); // 调试信息输出 this.debugSDPComparison(offer, normalizedOffer, "Offer创建阶段"); return normalizedOffer; } catch (error) { console.error("创建Offer失败:", error); throw error; } }; }); } 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"); 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();