|
|
@@ -6,6 +6,48 @@ import * as JsSIP from 'jssip';
|
|
|
// ========== 回铃音 URL(使用本地音频文件) ==========
|
|
|
export const RINGBACK_AUDIO_URL = '/assets/voice/ringback.wav';
|
|
|
|
|
|
+// ========== 默认配置常量(IPCC 与 JsSIP 严格分离) ==========
|
|
|
+
|
|
|
+/** IPCC(呼叫中心)服务器默认配置 */
|
|
|
+export const IPCC_DEFAULTS = {
|
|
|
+ SERVER_PROD: 'sip.ylrzcloud.com',
|
|
|
+ SERVER_LOCAL: '129.28.164.235',
|
|
|
+ PORT_LOCAL: 1081,
|
|
|
+ CONNECT_TIMEOUT: 15000,
|
|
|
+ HEARTBEAT_INTERVAL: 16
|
|
|
+};
|
|
|
+
|
|
|
+/** JsSIP(软电话)SIP 默认配置 */
|
|
|
+export const JS_SIP_DEFAULTS = {
|
|
|
+ SERVER: 'wss://sip.ylrzcloud.com:8443', // 线上环境
|
|
|
+ DOMAIN: 'sip.ylrzcloud.com',
|
|
|
+ TRANSPORT: 'wss',
|
|
|
+ USER_AGENT: 'JsSIP',
|
|
|
+ SESSION_EXPIRES: 180,
|
|
|
+ MIN_SESSION_EXPIRES: 90,
|
|
|
+ SPEAKER_VOLUME: 0.8,
|
|
|
+ MIC_VOLUME: 0.8,
|
|
|
+ RECONNECT_INTERVAL: 15,
|
|
|
+ RECONNECT_TOTAL_DURATION: 60000
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 将字符串转换为 Base64(安全,使用 TextEncoder 替代已弃用的 unescape)
|
|
|
+ */
|
|
|
+const toBase64 = (str) => {
|
|
|
+ const bytes = new TextEncoder().encode(str);
|
|
|
+ return btoa(String.fromCharCode(...bytes));
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 从 Base64 解码为字符串(安全,使用 TextDecoder 替代已弃用的 escape)
|
|
|
+ */
|
|
|
+const fromBase64 = (b64) => {
|
|
|
+ const binary = atob(b64);
|
|
|
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
|
+ return new TextDecoder().decode(bytes);
|
|
|
+};
|
|
|
+
|
|
|
/**
|
|
|
* 简单的密码混淆(非加密,仅避免明文暴露)
|
|
|
* 注意:生产环境应使用服务端认证token或OAuth
|
|
|
@@ -15,14 +57,12 @@ const encodePassword = (pwd) => {
|
|
|
try {
|
|
|
const timestamp = Date.now().toString(36);
|
|
|
const pwdStr = String(pwd);
|
|
|
- const encoded = btoa(unescape(encodeURIComponent(pwdStr)));
|
|
|
+ const encoded = toBase64(pwdStr);
|
|
|
return `${timestamp}:${encoded}`;
|
|
|
} catch (e) {
|
|
|
- console.error('[密码] 编码失败');
|
|
|
try {
|
|
|
return btoa(String(pwd));
|
|
|
} catch (fallbackError) {
|
|
|
- console.error('[密码] 备选编码失败');
|
|
|
return '';
|
|
|
}
|
|
|
}
|
|
|
@@ -35,9 +75,8 @@ const decodePassword = (encoded) => {
|
|
|
if (encoded.includes(':')) {
|
|
|
base64Part = encoded.split(':')[1];
|
|
|
}
|
|
|
- return decodeURIComponent(escape(atob(base64Part)));
|
|
|
+ return fromBase64(base64Part);
|
|
|
} catch (e) {
|
|
|
- console.warn('[密码] 解码失败,可能是旧格式');
|
|
|
return '';
|
|
|
}
|
|
|
};
|
|
|
@@ -106,12 +145,12 @@ export class ProfileManager {
|
|
|
users: {},
|
|
|
user: '',
|
|
|
reconnect: true,
|
|
|
- reconnect_interval: 15,
|
|
|
- user_agent: 'JsSIP',
|
|
|
- session_expires: 180,
|
|
|
- min_session_expires: 120,
|
|
|
- speaker_volume: 0.8,
|
|
|
- mic_volume: 0.8,
|
|
|
+ reconnect_interval: JS_SIP_DEFAULTS.RECONNECT_INTERVAL,
|
|
|
+ user_agent: JS_SIP_DEFAULTS.USER_AGENT,
|
|
|
+ session_expires: JS_SIP_DEFAULTS.SESSION_EXPIRES,
|
|
|
+ min_session_expires: JS_SIP_DEFAULTS.MIN_SESSION_EXPIRES,
|
|
|
+ speaker_volume: JS_SIP_DEFAULTS.SPEAKER_VOLUME,
|
|
|
+ mic_volume: JS_SIP_DEFAULTS.MIC_VOLUME,
|
|
|
speaker_paused: false,
|
|
|
mic_paused: false,
|
|
|
auto_answer: false,
|
|
|
@@ -132,13 +171,14 @@ export class ProfileManager {
|
|
|
this.profile.reconnect_interval = reconnectInterval;
|
|
|
this.save();
|
|
|
}
|
|
|
+ const currentUser = this.getCurrentUserProfile() || {};
|
|
|
const settings = {
|
|
|
user_agent: this.profile.user_agent,
|
|
|
session_expires: this.profile.session_expires,
|
|
|
min_session_expires: this.profile.min_session_expires,
|
|
|
stun: this.profile.stun,
|
|
|
ice_server: this.profile.ice_server,
|
|
|
- auto_answer: this.profile.auto_answer,
|
|
|
+ auto_answer: currentUser.auto_answer !== undefined ? currentUser.auto_answer : this.profile.auto_answer,
|
|
|
reconnect: this.profile.reconnect,
|
|
|
reconnect_interval: reconnectInterval
|
|
|
};
|
|
|
@@ -152,10 +192,10 @@ export class ProfileManager {
|
|
|
|
|
|
resetSettings() {
|
|
|
this.profile.reconnect = true;
|
|
|
- this.profile.reconnect_interval = 15;
|
|
|
- this.profile.user_agent = 'JsSIP';
|
|
|
- this.profile.session_expires = 180;
|
|
|
- this.profile.min_session_expires = 120;
|
|
|
+ this.profile.reconnect_interval = JS_SIP_DEFAULTS.RECONNECT_INTERVAL;
|
|
|
+ this.profile.user_agent = JS_SIP_DEFAULTS.USER_AGENT;
|
|
|
+ this.profile.session_expires = JS_SIP_DEFAULTS.SESSION_EXPIRES;
|
|
|
+ this.profile.min_session_expires = JS_SIP_DEFAULTS.MIN_SESSION_EXPIRES;
|
|
|
this.profile.auto_answer = false;
|
|
|
this.profile.stun = false;
|
|
|
this.profile.ice_server = '';
|
|
|
@@ -190,14 +230,31 @@ export class ProfileManager {
|
|
|
|
|
|
updateUser(userId, updatedProfile) {
|
|
|
if (this.profile.users[userId]) {
|
|
|
- const existing = this.profile.users[userId];
|
|
|
- const merged = {
|
|
|
- ...existing,
|
|
|
- ...updatedProfile,
|
|
|
- user: existing.user,
|
|
|
- domain: existing.domain
|
|
|
- };
|
|
|
- this.profile.users[userId] = merged;
|
|
|
+ const newUser = updatedProfile.user;
|
|
|
+ const newDomain = updatedProfile.domain;
|
|
|
+ const newUserId = `${newUser}@${newDomain}`;
|
|
|
+
|
|
|
+ if (newUserId !== userId) {
|
|
|
+ // user 或 domain 改变了,删除旧条目并用新 key 创建
|
|
|
+ const merged = {
|
|
|
+ ...this.profile.users[userId],
|
|
|
+ ...updatedProfile,
|
|
|
+ user: newUser,
|
|
|
+ domain: newDomain
|
|
|
+ };
|
|
|
+ delete this.profile.users[userId];
|
|
|
+ this.profile.users[newUserId] = merged;
|
|
|
+ if (this.profile.user === userId) {
|
|
|
+ this.profile.user = newUserId;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.profile.users[userId] = {
|
|
|
+ ...this.profile.users[userId],
|
|
|
+ ...updatedProfile,
|
|
|
+ user: newUser,
|
|
|
+ domain: newDomain
|
|
|
+ };
|
|
|
+ }
|
|
|
this.save();
|
|
|
}
|
|
|
}
|
|
|
@@ -239,7 +296,7 @@ export class WebPhone {
|
|
|
this.reconnectEnabled = settings.reconnect;
|
|
|
this.reconnectAttempts = 0;
|
|
|
this.reconnectStartTime = null;
|
|
|
- this.reconnectTotalDuration = 1 * 60 * 1000; // 1分钟
|
|
|
+ this.reconnectTotalDuration = JS_SIP_DEFAULTS.RECONNECT_TOTAL_DURATION;
|
|
|
this.isReconnecting = false;
|
|
|
this.reconnectTimerId = null;
|
|
|
this._isHandlingDisconnect = false;
|
|
|
@@ -249,8 +306,9 @@ export class WebPhone {
|
|
|
this.ringbackMedia.loop = true;
|
|
|
this.remoteMedia = new Audio();
|
|
|
this.localMedia = new Audio();
|
|
|
+ this.peerConnection = null;
|
|
|
+ this.localStream = null;
|
|
|
|
|
|
- // this.applyWebSocketPatch();
|
|
|
this.initUA();
|
|
|
}
|
|
|
|
|
|
@@ -297,49 +355,64 @@ export class WebPhone {
|
|
|
|
|
|
initUA() {
|
|
|
if (!this.profile.server || !this.profile.user || !this.profile.domain) {
|
|
|
- console.error('[SIP] 配置不完整');
|
|
|
+ console.error('[jsSip] 配置不完整');
|
|
|
this.emit('OnStatusMessage', { type: 'error', text: '配置不完整' });
|
|
|
return;
|
|
|
}
|
|
|
- const socket = new JsSIP.WebSocketInterface(this.profile.server, { protocols: [] });
|
|
|
- socket.via_transport = this.profile.transport || 'wss';
|
|
|
+ // JsSIP 3.x 底层创建 WebSocket 时已自动使用 'sip' 子协议(RFC 7118)
|
|
|
+ const socket = new JsSIP.WebSocketInterface(this.profile.server);
|
|
|
+
|
|
|
+ // 修复 JsSIP 3.x 的 via_transport 问题:
|
|
|
+ // 对于 WSS 连接,JsSIP 设置 via_transport = 'WSS'(WebSocketInterface.js:23)
|
|
|
+ // 但 RFC 3261/7118 中 Via header transport 应用 'WS'(不分是否 TLS)
|
|
|
+ // 若服务器不识别 'WSS' 会静默丢弃 REGISTER 请求,导致超时
|
|
|
+ if (String(this.profile.server || '').startsWith('wss://')) {
|
|
|
+ socket.via_transport = 'WS';
|
|
|
+ console.log('[jsSip] WSS 连接: 修正 via_transport WSS → WS(RFC 7118)');
|
|
|
+ }
|
|
|
|
|
|
const user = String(this.profile.user || '');
|
|
|
- const domain = String(this.profile.domain || '');
|
|
|
const displayName = this.profile.display_name ? String(this.profile.display_name) : '';
|
|
|
const password = this.profile.password ? String(this.profile.password) : '';
|
|
|
const server = String(this.profile.server || '');
|
|
|
- const transport = String(this.profile.transport || 'wss');
|
|
|
+ // SIP over WebSocket 的 transport 始终为 'ws'(RFC 7118),与是否 WSS/TLS 无关
|
|
|
+ const transport = 'ws';
|
|
|
|
|
|
+ const domain = String(this.profile.domain || '');
|
|
|
if (!user || !domain || !password) {
|
|
|
- console.error('[SIP] 账号配置缺失:', { user, domain, hasPassword: !!password });
|
|
|
+ console.error('[jsSip] 账号配置缺失:', { user, domain, hasPassword: !!password });
|
|
|
this.emit('OnStatusMessage', { type: 'error', text: '账号配置不完整,请检查登录名、域名和密码' });
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // 检测混合内容问题:HTTP 页面使用 WSS 连接会被浏览器阻止
|
|
|
+ if (server.startsWith('wss://') && window.location.protocol === 'http:') {
|
|
|
+ console.warn('[jsSip] 警告: 页面通过 HTTP 加载,但 SIP 服务器使用 WSS 协议。浏览器会阻止混合内容,请使用 HTTPS 或改用 WS 协议');
|
|
|
+ this.emit('OnStatusMessage', { type: 'warn', text: 'HTTP页面使用WSS会被浏览器阻止' });
|
|
|
+ }
|
|
|
+
|
|
|
const uri = new JsSIP.URI('sip', user, domain);
|
|
|
- const contactUriStr = `sip:${user}@${domain};transport=ws`;
|
|
|
+ const contactUriStr = `sip:${user}@${domain};transport=${transport}`;
|
|
|
|
|
|
this.configuration = {
|
|
|
sockets: [socket],
|
|
|
authorization_user: user,
|
|
|
- hack_ip_in_contact: true,
|
|
|
- user_agent: this.settings.user_agent || 'JsSIP',
|
|
|
+ user_agent: this.settings.user_agent || JS_SIP_DEFAULTS.USER_AGENT,
|
|
|
display_name: displayName || undefined,
|
|
|
+ // 启用会话定时器,防止长时间通话被中间代理断开
|
|
|
session_timers: true,
|
|
|
+ session_timers_expires: this.settings.session_expires || JS_SIP_DEFAULTS.SESSION_EXPIRES,
|
|
|
+ session_timers_min_se: this.settings.min_session_expires || JS_SIP_DEFAULTS.MIN_SESSION_EXPIRES,
|
|
|
no_answer_timeout: 60,
|
|
|
+ register: true,
|
|
|
uri: uri.toAor(),
|
|
|
contact_uri: contactUriStr,
|
|
|
+ // 始终使用 password 模式进行认证
|
|
|
+ // 移除旧的 ha1 启发式判断(password.length === 32),避免错误地将32位密码当成HA1
|
|
|
+ password: password
|
|
|
};
|
|
|
|
|
|
- if (password && password.length === 32) {
|
|
|
- this.configuration.ha1 = password;
|
|
|
- this.configuration.realm = domain;
|
|
|
- } else {
|
|
|
- this.configuration.password = password;
|
|
|
- }
|
|
|
-
|
|
|
- console.log(`[SIP] UA配置完成: ${user}@${domain}`);
|
|
|
+ console.log(`[jsSip] UA配置完成: ${user}@${domain}, 服务器: ${server}, 认证方式: password`);
|
|
|
}
|
|
|
|
|
|
createUA() {
|
|
|
@@ -354,12 +427,19 @@ export class WebPhone {
|
|
|
this.ua.on('registrationExpiring', this.registrationExpiring.bind(this));
|
|
|
this.ua.on('newRTCSession', this.newRTCSession.bind(this));
|
|
|
this.ua.on('newMessage', this.newMessage.bind(this));
|
|
|
+ this.ua.on('transportError', this.transportError.bind(this));
|
|
|
}
|
|
|
|
|
|
On(event, callback) {
|
|
|
this.events[event] = callback;
|
|
|
}
|
|
|
|
|
|
+ Off(event) {
|
|
|
+ if (this.events[event]) {
|
|
|
+ delete this.events[event];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
emit(event, ...args) {
|
|
|
if (this.events[event]) {
|
|
|
try {
|
|
|
@@ -474,20 +554,31 @@ export class WebPhone {
|
|
|
|
|
|
// ---- JsSIP 事件处理 ----
|
|
|
connecting() {
|
|
|
- console.log('[SIP] 连接中...');
|
|
|
- this.emit('OnStatusMessage', { type: 'info', text: '连接中...' });
|
|
|
+ console.log('[jsSip] 连接中...');
|
|
|
+ this.emit('OnStatusMessage', { type: 'info', text: 'jsSip连接中...' });
|
|
|
}
|
|
|
connected() {
|
|
|
- console.log('[SIP] 已连接,开始注册');
|
|
|
+ console.log('[jsSip] 已连接,开始注册');
|
|
|
this.Register();
|
|
|
- this.emit('OnStatusMessage', { type: 'success', text: '已连接' });
|
|
|
+ this.emit('OnStatusMessage', { type: 'success', text: 'jsSip开始注册' });
|
|
|
}
|
|
|
disconnected(e) {
|
|
|
if (this._isHandlingDisconnect) return;
|
|
|
this._isHandlingDisconnect = true;
|
|
|
- console.log('[SIP] 连接断开');
|
|
|
+ const reason = e?.cause || e?.message || '未知原因';
|
|
|
+ const code = e?.code || '';
|
|
|
+ if (code !== '') {
|
|
|
+ console.error(`[SIP] 连接断开: ${reason}`, {
|
|
|
+ code,
|
|
|
+ cause: e?.cause,
|
|
|
+ message: e?.message,
|
|
|
+ server: this.profile?.server || 'unknown',
|
|
|
+ fullEvent: e
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
this.emit('OnRegister', { registered: false });
|
|
|
- this.emit('OnStatusMessage', { type: 'error', text: '连接断开' });
|
|
|
+ this.emit('OnStatusMessage', { type: 'error', text: 'WSS断开: ' + reason });
|
|
|
if (this.ua) {
|
|
|
try { this.ua.stop(); } catch (err) {}
|
|
|
this.ua = null;
|
|
|
@@ -496,27 +587,74 @@ export class WebPhone {
|
|
|
setTimeout(() => { this._isHandlingDisconnect = false; }, 1000);
|
|
|
}
|
|
|
registered() {
|
|
|
- console.log('[SIP] 连接成功');
|
|
|
this.resetReconnectState();
|
|
|
this.emit('OnRegister', { registered: true });
|
|
|
- this.emit('OnStatusMessage', { type: 'success', text: '连接成功' });
|
|
|
+ this.emit('OnStatusMessage', { type: 'success', text: '已连接' });
|
|
|
this.SetQueueIn();
|
|
|
}
|
|
|
unregistered() {
|
|
|
- console.log('[SIP] 已注销');
|
|
|
this.emit('OnRegister', { registered: false });
|
|
|
- this.emit('OnStatusMessage', { type: 'info', text: '已注销' });
|
|
|
+ this.emit('OnStatusMessage', { type: 'info', text: 'jsSip已注销' });
|
|
|
}
|
|
|
registrationFailed(e) {
|
|
|
- console.error('[SIP] 注册失败:', e.cause || '未知原因');
|
|
|
+ const cause = e?.cause || e?.message || '未知原因';
|
|
|
+ const statusCode = e?.response?.status_code || '';
|
|
|
+ console.error('[jsSip] 注册失败:', {
|
|
|
+ cause,
|
|
|
+ status_code: statusCode,
|
|
|
+ response: e?.response,
|
|
|
+ server: this.profile?.server || 'unknown',
|
|
|
+ user: this.profile?.user || 'unknown',
|
|
|
+ domain: this.profile?.domain || 'unknown'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 根据失败原因给出更明确的中文提示
|
|
|
+ let errorText = '注册失败: ' + cause;
|
|
|
+ if (cause === 'Connection Error') {
|
|
|
+ errorText = '注册失败: 无法连接到SIP服务器,请检查服务器地址 ' + (this.profile?.server || '') + ' 是否可访问';
|
|
|
+ } else if (cause.includes('403') || cause.includes('Forbidden') || cause.includes('401') || cause.includes('Unauthorized')) {
|
|
|
+ errorText = '注册失败: 认证失败(code:' + statusCode + '),请检查分机号和密码是否正确';
|
|
|
+ } else if (cause.includes('404') || cause.includes('Not Found')) {
|
|
|
+ errorText = '注册失败: 用户不存在(code:404),请检查分机号是否正确';
|
|
|
+ } else if (cause.includes('408') || cause.includes('Timeout') || cause.includes('timeout')) {
|
|
|
+ errorText = '注册失败: 注册请求超时,服务器无响应';
|
|
|
+ }
|
|
|
+
|
|
|
this.emit('OnRegister', { registered: false });
|
|
|
- this.emit('OnStatusMessage', { type: 'error', text: '注册失败: ' + (e.cause || '未知原因') });
|
|
|
+ this.emit('OnStatusMessage', { type: 'error', text: errorText });
|
|
|
if (!this.isReconnecting && this.reconnectEnabled) this.scheduleReconnect();
|
|
|
}
|
|
|
registrationExpiring() {
|
|
|
- console.log('[SIP] 注册即将过期,重新注册');
|
|
|
+ console.log('[jsSip] 注册即将过期,重新注册');
|
|
|
this.Register();
|
|
|
}
|
|
|
+ transportError(err) {
|
|
|
+ // 详细输出 WebSocket 错误信息,帮助排查连接问题
|
|
|
+ const errorCode = err?.code || '';
|
|
|
+ const errorMessage = err?.message || '';
|
|
|
+ const errorReason = err?.reason || '';
|
|
|
+ console.error('[SIP] WebSocket传输错误:', {
|
|
|
+ code: errorCode,
|
|
|
+ message: errorMessage,
|
|
|
+ reason: errorReason,
|
|
|
+ server: this.profile?.server || 'unknown',
|
|
|
+ fullError: err
|
|
|
+ });
|
|
|
+
|
|
|
+ let detail = errorMessage || errorReason || JSON.stringify(err);
|
|
|
+ let errorText = 'WSS连接失败: ' + detail;
|
|
|
+
|
|
|
+ if (errorMessage.includes('SecurityError') || errorMessage.includes('mixed content') || errorMessage.includes('Mixed Content')) {
|
|
|
+ errorText = 'WSS连接被浏览器阻止: 页面是HTTP协议,无法连接安全的WSS服务。请使用HTTPS访问页面或改用WS协议';
|
|
|
+ } else if (errorCode === 1006 || errorMessage.includes('close with code 1006')) {
|
|
|
+ errorText = 'WSS连接异常关闭(code 1006): 服务器无响应或SSL证书错误, 服务地址:' + (this.profile?.server || 'unknown');
|
|
|
+ } else if (errorCode === 1005 || errorMessage.includes('close with code 1005')) {
|
|
|
+ errorText = 'WSS连接被拒绝(code 1005): 服务器不接受WebSocket连接, 请检查服务地址和端口';
|
|
|
+ }
|
|
|
+
|
|
|
+ this.emit('OnStatusMessage', { type: 'error', text: errorText });
|
|
|
+ if (!this.isReconnecting && this.reconnectEnabled) this.scheduleReconnect();
|
|
|
+ }
|
|
|
|
|
|
// 外呼功能由 ccPhoneBarSocket 实现,WebPhone 不再实现 Call 方法
|
|
|
// 但保留 Answer、Terminate、ToggleHold、ToggleMicPhone 等方法供来电和通话控制
|
|
|
@@ -558,6 +696,11 @@ export class WebPhone {
|
|
|
|
|
|
SetMicPhone(paused, volume) {
|
|
|
this.localMedia.volume = paused ? 0 : volume;
|
|
|
+ if (this.localStream) {
|
|
|
+ this.localStream.getAudioTracks().forEach((track) => {
|
|
|
+ track.enabled = !paused;
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
SetQueueIn() {
|
|
|
@@ -704,6 +847,9 @@ export class WebPhone {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // 保存 peerConnection 引用
|
|
|
+ this.peerConnection = connection;
|
|
|
+
|
|
|
// 监听远程流
|
|
|
connection.ontrack = (e) => {
|
|
|
if (!this.remoteMedia.srcObject) {
|
|
|
@@ -715,9 +861,31 @@ export class WebPhone {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ // 如果已有本地流,先清理
|
|
|
+ if (this.localStream) {
|
|
|
+ console.log('[音频] 清理旧的本地流');
|
|
|
+ this.localStream.getTracks().forEach(track => {
|
|
|
+ track.stop();
|
|
|
+ if (this.peerConnection) {
|
|
|
+ try {
|
|
|
+ this.peerConnection.removeTrack(track, this.localStream);
|
|
|
+ } catch (err) {
|
|
|
+ // ignore
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ this.localStream = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理 localMedia 的旧 srcObject
|
|
|
+ if (this.localMedia.srcObject) {
|
|
|
+ this.localMedia.srcObject = null;
|
|
|
+ }
|
|
|
+
|
|
|
// 获取并添加本地流
|
|
|
navigator.mediaDevices.getUserMedia({ audio: true })
|
|
|
.then(stream => {
|
|
|
+ this.localStream = stream;
|
|
|
this.localMedia.srcObject = stream;
|
|
|
stream.getTracks().forEach(track => {
|
|
|
try {
|
|
|
@@ -736,12 +904,40 @@ export class WebPhone {
|
|
|
|
|
|
sessionClosed(succeed, reason) {
|
|
|
if (!this.session) return;
|
|
|
+
|
|
|
+ console.log('[通话] 会话关闭,开始清理资源');
|
|
|
this.session = null;
|
|
|
if (this.callTimerId) clearInterval(this.callTimerId);
|
|
|
this.pauseRingback();
|
|
|
|
|
|
- if (this.remoteMedia.srcObject) this.remoteMedia.srcObject.getTracks().forEach(t => t.stop());
|
|
|
- if (this.localMedia.srcObject) this.localMedia.srcObject.getTracks().forEach(t => t.stop());
|
|
|
+ // 清理远程媒体
|
|
|
+ if (this.remoteMedia.srcObject) {
|
|
|
+ this.remoteMedia.srcObject.getTracks().forEach(t => t.stop());
|
|
|
+ this.remoteMedia.srcObject = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理本地媒体
|
|
|
+ if (this.localMedia.srcObject) {
|
|
|
+ this.localMedia.srcObject.getTracks().forEach(t => t.stop());
|
|
|
+ this.localMedia.srcObject = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理本地流引用
|
|
|
+ if (this.localStream) {
|
|
|
+ this.localStream.getTracks().forEach(t => t.stop());
|
|
|
+ this.localStream = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理 peerConnection
|
|
|
+ if (this.peerConnection) {
|
|
|
+ try {
|
|
|
+ this.peerConnection.close();
|
|
|
+ } catch (err) {
|
|
|
+ // ignore
|
|
|
+ }
|
|
|
+ this.peerConnection = null;
|
|
|
+ }
|
|
|
+
|
|
|
this.emit('OnSessionClosed', { succeeded: succeed, reason });
|
|
|
}
|
|
|
|
|
|
@@ -798,4 +994,4 @@ export class WebPhone {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-export default { WebPhone, ProfileManager, checkMicrophonePermission, RINGBACK_AUDIO_URL };
|
|
|
+export default { WebPhone, ProfileManager, checkMicrophonePermission, RINGBACK_AUDIO_URL, IPCC_DEFAULTS, JS_SIP_DEFAULTS };
|