import JsSIP from "jssip";
|
|
class SipService {
|
constructor() {
|
this.ua = null;
|
this.currentSession = null;
|
this.onStatusChange = null;
|
this.onCallStatusChange = null;
|
this.onIncomingCall = 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: [{ urls: "stun:stun.l.google.com:19302" }],
|
register: true,
|
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.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);
|
throw error;
|
}
|
}
|
|
makeCall(targetNumber) {
|
return new Promise((resolve, reject) => {
|
try {
|
if (!this.ua) {
|
throw new Error("SIP客户端未初始化");
|
}
|
|
if (!this.ua.isRegistered()) {
|
throw new Error("SIP未注册,无法呼叫");
|
}
|
|
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}@9.208.5.18`,
|
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.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();
|