6f1e7a63fa6afc261ba2247bf1d56f59dbb9f4c9..337fb928600d4975206c3f44c41afa6439dc703e
2025-08-08 WXL
测试完成
337fb9 对比 | 目录
2025-08-08 WXL
测试完成
a7a20b 对比 | 目录
已添加1个文件
已修改2个文件
126 ■■■■ 文件已修改
dist.zip 补丁 | 查看 | 原始文档 | blame | 历史
src/components/CallButton/index.vue 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/sipService.js 91 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dist.zip
Binary files differ
src/components/CallButton/index.vue
@@ -33,19 +33,19 @@
  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 小龙",
      },
@@ -54,10 +54,10 @@
  computed: {
    callStatusText() {
      const statusMap = {
        idle: '准备就绪',
        calling: '呼叫中...',
        connected: '通话中',
        ended: '通话结束'
        idle: "准备就绪",
        calling: "呼叫中...",
        connected: "通话中",
        ended: "通话结束",
      };
      return statusMap[this.callStatus];
    },
@@ -66,7 +66,7 @@
    },
    callButtonText() {
      return this.isCalling ? "通话中..." : "一键呼叫";
    }
    },
  },
  mounted() {
    sipService.init(this.sipConfig);
@@ -78,10 +78,10 @@
    // 监听通话状态变化
    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: {
@@ -92,14 +92,13 @@
      }
      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}`);
      }
@@ -107,10 +106,10 @@
    endCall() {
      sipService.endCall();
      this.callStatus = 'ended';
      this.callStatus = "ended";
      this.isCalling = false;
    }
  }
    },
  },
};
</script>
src/utils/sipService.js
@@ -11,41 +11,38 @@
  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);
@@ -65,16 +62,13 @@
        }
        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", "呼叫中..."),
@@ -85,8 +79,8 @@
            confirmed: () => {
              this.updateCallStatus("connected", "通话已接通");
              resolve();
            }
          }
            },
          },
        };
        this.currentSession = this.ua.call(
@@ -96,9 +90,8 @@
        this.setupPeerConnection(this.currentSession);
        this.setupAudio(this.currentSession);
      } catch (error) {
        this.updateCallStatus("failed", `呼叫失败1: ${error.message}`);
        this.updateCallStatus("failed", `呼叫失败22: ${error.message}`);
        reject(error);
      }
    });
@@ -123,41 +116,44 @@
  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;
@@ -165,11 +161,14 @@
      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));
  }