WXL
2025-08-07 209b2f7b9a0b2abc4043f22bed9b06ab2650ba59
src/utils/sipService.js
@@ -1,118 +1,217 @@
import JsSIP from 'jssip'
import JsSIP from "jssip";
class SipService {
  constructor() {
    this.ua = null
    this.currentSession = null
    this.onStatusChange = null // 状态变化回调
    this.ua = null;
    this.currentSession = null;
    this.onStatusChange = null;
    this.onCallStatusChange = null;
    this.onIncomingCall = null;
  }
  // 初始化SIP客户端
  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,
        // realm: config.realm,
        iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
        register: true,
        session_expires:180,
        register_expires: 300, // 注册有效期(秒)
        connection_recovery_min_interval: 2, // 最小重连间隔
        connection_recovery_max_interval: 30 // 最大重连间隔
      })
        session_expires: 180,
        sessionTimersExpires: 300,
        extraHeaders: ["Min-SE: 120"],
        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.start();
      // 注册事件监听
      this.ua.on('registered', () => {
        this.updateStatus('registered', '已注册')
      })
      this.ua.on('registrationFailed', (e) => {
        this.updateStatus('failed', `注册失败: ${e.cause}`)
      })
      this.ua.on('disconnected', () => {
        this.updateStatus('disconnected', '连接断开')
      })
      this.ua.on('connected', () => {
        this.updateStatus('connecting', '重新连接中...')
      })
      // 监听来电
      this.ua.on('newRTCSession', (data) => {
        this.handleIncomingCall(data.session)
      })
      // 事件监听
      this.ua.on("registered", () => this.updateStatus("registered", "已注册"));
      this.ua.on("registrationFailed", (e) =>
        this.updateStatus("failed", `注册失败: ${e.cause}`));
      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)
      this.updateStatus("failed", `初始化失败: ${error.message}`);
      console.error("SIP初始化失败:", error);
      throw error;
    }
  }
  // 更新状态并通知UI
  updateStatus(type, text) {
    console.log(`SIP状态更新: ${type} - ${text}`)
    if (this.onStatusChange) {
      this.onStatusChange({ type, text })
    }
  }
  // 一键拨号 - 增加注册状态检查
  makeCall(targetNumber) {
    if (!this.ua) {
      throw new Error('SIP客户端未初始化')
    }
    return new Promise((resolve, reject) => {
      try {
        if (!this.ua) {
          throw new Error("SIP客户端未初始化");
        }
    if (!this.ua.isRegistered()) {
      throw new Error('SIP未注册,无法呼叫')
    }
        if (!this.ua.isRegistered()) {
          throw new Error("SIP未注册,无法呼叫");
        }
    const options = {
      eventHandlers: {
        progress: (e) => console.log('呼叫中...'),
        failed: (e) => console.error('呼叫失败:', e),
        ended: (e) => console.log('通话结束'),
        confirmed: (e) => console.log('通话已接通')
      },
      mediaConstraints: { audio: true, video: false },
      rtcOfferConstraints: { offerToReceiveAudio: 1 }
    }
        const options = {
          sessionTimers: false, // 暂时禁用以减少兼容性问题
          extraHeaders: [
            "Min-SE: 120",
            "Accept: application/sdp",
            "Supported: outbound"
          ],
          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(`sip:${targetNumber}@192.168.100.6`, options)
    this.setupAudio(this.currentSession)
        this.currentSession = this.ua.call(
          `sip:${targetNumber}@192.168.10.124`,
          options
        );
        this.setupPeerConnection(this.currentSession);
        this.setupAudio(this.currentSession);
      } catch (error) {
        this.updateCallStatus("failed", `呼叫失败: ${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;
    // 1. 标准化连接行
    sdp = sdp.replace(/c=IN IP4.*\r\n/, "c=IN IP4 0.0.0.0\r\n");
    // 2. 标准化音频媒体行
    sdp = sdp.replace(/m=audio \d+.*\r\n/,
      "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\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
    });
  }
  handleCallFailure(e, reject) {
    console.error("呼叫失败详情:", {
      cause: e.cause,
      message: e.message,
      response: e.response && e.response.status_code
    });
    let errorMessage = "呼叫失败";
    switch(e.cause) {
      case "Incompatible SDP":
        errorMessage = "媒体协商失败,请检查编解码器配置";
        break;
      case "488":
      case "606":
        errorMessage = "对方设备不支持当前媒体配置";
        break;
      default:
        errorMessage = `呼叫失败: ${e.cause || e.message}`;
    }
    this.updateCallStatus("failed", 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.currentSession = null
      this.currentSession.terminate();
      this.updateCallStatus("ended", "通话已结束");
      this.currentSession = null;
    }
  }
  // 处理音频流
  setupAudio(session) {
    session.connection.addEventListener('addstream', (e) => {
      const audioElement = document.getElementById('remoteAudio')
      if (audioElement) {
        audioElement.srcObject = e.stream
      }
    })
  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())
      // 这里可以触发UI通知
    if (session.direction === "incoming") {
      console.log("来电:", session.remote_identity.uri.toString());
      if (this.onIncomingCall) {
        this.onIncomingCall(session);
      }
    }
  }
}
export default new SipService()
export default new SipService();