From 17efc8b0fb7d3fa66eb8e22f32b81e3e14bcd7f6 Mon Sep 17 00:00:00 2001
From: WXL (wul) <wl_5969728@163.com>
Date: 星期二, 06 一月 2026 17:37:57 +0800
Subject: [PATCH] 测试完成

---
 src/utils/sipService-cs.js |  301 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 301 insertions(+), 0 deletions(-)

diff --git a/src/utils/sipService-cs.js b/src/utils/sipService-cs.js
new file mode 100644
index 0000000..0cfc572
--- /dev/null
+++ b/src/utils/sipService-cs.js
@@ -0,0 +1,301 @@
+import JsSIP from "jssip";
+
+class SipService {
+  constructor() {
+    this.ua = null;
+    this.currentSession = null;
+    this.onStatusChange = null;
+    this.onCallStatusChange = null;
+    this.onIncomingCall = null;
+    this.isRegistered = false; // 鏂板娉ㄥ唽鐘舵�佹爣蹇�
+    this.registrationTime = 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.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) {
+      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 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",
+          ],
+          mediaConstraints: {
+            audio: true,
+            video: false,
+          },
+          rtcOfferConstraints: {
+            offerToReceiveAudio: 1,
+            offerToReceiveVideo: 0,
+            mandatory: {
+              OfferToReceiveAudio: true,
+              OfferToReceiveVideo: false,
+            },
+          },
+          eventHandlers: {
+            progress: () => this.updateCallStatus("calling", "鍛煎彨涓�..."),
+            failed: (e) => {
+              this.handleCallFailure(e, reject);
+            },
+            ended: () => this.updateCallStatus("ended", "閫氳瘽缁撴潫"),
+            confirmed: () => {
+              this.updateCallStatus("connected", "閫氳瘽宸叉帴閫�");
+              resolve();
+            },
+          },
+          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.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;
+
+    // 鏍囧噯鍖朣DP
+    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("鏍囧噯鍖栧悗鐨凷DP:", 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 = "濯掍綋鍗忓晢澶辫触锛岃妫�鏌ョ紪瑙g爜鍣ㄩ厤缃�";
+        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();

--
Gitblit v1.9.3