| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789 |
- // softPhone.js - WebPhone核心逻辑与配置管理(使用 localStorage 存储,密码混淆)
- // 本模块负责 SIP 注册、音频处理、DTMF、保持/接回等,不包含外呼逻辑(由 ccPhoneBarSocket 实现)
- import * as JsSIP from 'jssip';
- // ========== 回铃音 URL(使用本地音频文件) ==========
- export const RINGBACK_AUDIO_URL = '/assets/voice/ringback.wav';
- /**
- * 简单的密码混淆(非加密,仅避免明文暴露)
- * 注意:生产环境应使用服务端认证token或OAuth
- */
- const encodePassword = (pwd) => {
- if (!pwd) return '';
- try {
- const timestamp = Date.now().toString(36);
- const pwdStr = String(pwd);
- const encoded = btoa(unescape(encodeURIComponent(pwdStr)));
- return `${timestamp}:${encoded}`;
- } catch (e) {
- console.error('[密码] 编码失败');
- try {
- return btoa(String(pwd));
- } catch (fallbackError) {
- console.error('[密码] 备选编码失败');
- return '';
- }
- }
- };
- const decodePassword = (encoded) => {
- if (!encoded) return '';
- try {
- let base64Part = encoded;
- if (encoded.includes(':')) {
- base64Part = encoded.split(':')[1];
- }
- return decodeURIComponent(escape(atob(base64Part)));
- } catch (e) {
- console.warn('[密码] 解码失败,可能是旧格式');
- return '';
- }
- };
- // 检查麦克风权限
- export async function checkMicrophonePermission() {
- try {
- const permissionStatus = await navigator.permissions.query({ name: 'microphone' });
- if (permissionStatus.state === 'prompt') {
- await navigator.mediaDevices.getUserMedia({ audio: true });
- return true;
- } else if (permissionStatus.state === 'denied') {
- return false;
- } else if (permissionStatus.state === 'granted') {
- return true;
- }
- } catch (err) {
- return false;
- }
- }
- // ---------- ProfileManager(localStorage 安全存储)----------
- export class ProfileManager {
- constructor() {
- this.profile = null;
- this.load();
- }
- load() {
- try {
- const json = localStorage.getItem('WebPhoneProfile');
- if (!json) {
- this.reset();
- } else {
- this.profile = JSON.parse(json);
- if (this.profile.users) {
- Object.keys(this.profile.users).forEach(uid => {
- const user = this.profile.users[uid];
- if (user.password && typeof user.password === 'string') {
- user.password = decodePassword(user.password);
- }
- });
- }
- }
- } catch (error) {
- console.error('[配置] 加载失败,重置为默认值');
- this.reset();
- }
- }
- save() {
- const toStore = JSON.parse(JSON.stringify(this.profile));
- if (toStore.users) {
- Object.keys(toStore.users).forEach(uid => {
- const user = toStore.users[uid];
- if (user.password && typeof user.password === 'string') {
- user.password = encodePassword(user.password);
- }
- });
- }
- localStorage.setItem('WebPhoneProfile', JSON.stringify(toStore));
- }
- reset() {
- this.profile = {
- 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,
- speaker_paused: false,
- mic_paused: false,
- auto_answer: false,
- stun: false,
- ice_server: ''
- };
- this.save();
- }
- getProfile() {
- return this.profile;
- }
- getSettings() {
- let reconnectInterval = this.profile.reconnect_interval || 15;
- if (reconnectInterval > 1000) {
- reconnectInterval = Math.floor(reconnectInterval / 1000);
- this.profile.reconnect_interval = reconnectInterval;
- this.save();
- }
- 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,
- reconnect: this.profile.reconnect,
- reconnect_interval: reconnectInterval
- };
- return settings;
- }
- updateSettings(settings) {
- Object.assign(this.profile, settings);
- this.save();
- }
- 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.auto_answer = false;
- this.profile.stun = false;
- this.profile.ice_server = '';
- this.save();
- }
- getCurrentUserProfile() {
- if (!this.profile.users || !this.profile.user) return null;
- return this.profile.users[this.profile.user];
- }
- addUser(profile) {
- if (!profile.user || !profile.domain || !profile.password) {
- throw new Error('登录名、域名和密码为必填项');
- }
- const userId = `${profile.user}@${profile.domain}`;
- if (this.profile.users[userId]) {
- this.profile.users[userId] = {
- ...this.profile.users[userId],
- ...profile,
- user: profile.user,
- domain: profile.domain
- };
- this.profile.user = userId;
- this.save();
- return;
- }
- this.profile.user = userId;
- this.profile.users[userId] = profile;
- this.save();
- }
- 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;
- this.save();
- }
- }
- deleteCurrentUser() {
- if (!this.profile.user) return;
- delete this.profile.users[this.profile.user];
- const keys = Object.keys(this.profile.users);
- this.profile.user = keys.length > 0 ? keys[0] : '';
- this.save();
- }
- switchUser(userId) {
- if (this.profile.users[userId]) {
- this.profile.user = userId;
- this.save();
- }
- }
- }
- // ---------- WebPhone 核心类(SIP注册、媒体处理、保持/接回、DTMF、回铃音)----------
- // 外呼功能已移至 Vue 组件中的 ccPhoneBarSocket 实现
- export class WebPhone {
- constructor(profile, settings) {
- this.profile = profile;
- this.settings = settings;
- this.session = null;
- this.ua = null;
- this.call_id = this.randomUUID();
- this.events = {};
- this.sessionCloseTimerId = null;
- this.callTimerId = null;
- this.dtfmTimerId = null;
- this.audioCtx = null;
- this.oscillatorLow = null;
- this.oscillatorHigh = null;
- this.gainNode = null;
- this.reconnectEnabled = settings.reconnect;
- this.reconnectAttempts = 0;
- this.reconnectStartTime = null;
- this.reconnectTotalDuration = 1 * 60 * 1000; // 1分钟
- this.isReconnecting = false;
- this.reconnectTimerId = null;
- this._isHandlingDisconnect = false;
- // 使用 Base64 回铃音
- this.ringbackMedia = new Audio(RINGBACK_AUDIO_URL);
- this.ringbackMedia.loop = true;
- this.remoteMedia = new Audio();
- this.localMedia = new Audio();
- // this.applyWebSocketPatch();
- this.initUA();
- }
- randomUUID() {
- if (crypto && typeof crypto.randomUUID === 'function') {
- return crypto.randomUUID().replace(/-/g, '');
- }
- return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
- const r = Math.random() * 16 | 0;
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
- return v.toString(16);
- });
- }
- applyWebSocketPatch() {
- if (window._webSocketPatchedForSip) return;
- const targetServer = this.profile.server;
- if (!targetServer || !(targetServer.startsWith('ws://') || targetServer.startsWith('wss://'))) {
- console.warn('[WebPhone] WebSocket地址无效:', targetServer);
- return;
- }
- try {
- new URL(targetServer);
- } catch (e) {
- console.error('[WebPhone] WebSocket URL格式错误:', targetServer);
- return;
- }
- const originalWebSocket = window.WebSocket;
- const patchedWebSocket = function(url, protocols) {
- if (url === targetServer && protocols && protocols.length > 0) {
- return new originalWebSocket(url);
- }
- return new originalWebSocket(url, protocols);
- };
- for (let key in originalWebSocket) {
- if (originalWebSocket.hasOwnProperty(key)) {
- patchedWebSocket[key] = originalWebSocket[key];
- }
- }
- window.WebSocket = patchedWebSocket;
- window._webSocketPatchedForSip = true;
- console.log('[WebPhone] WebSocket Patch已应用');
- }
- initUA() {
- if (!this.profile.server || !this.profile.user || !this.profile.domain) {
- console.error('[SIP] 配置不完整');
- this.emit('OnStatusMessage', { type: 'error', text: '配置不完整' });
- return;
- }
- const socket = new JsSIP.WebSocketInterface(this.profile.server, { protocols: [] });
- socket.via_transport = this.profile.transport || 'wss';
- 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');
- if (!user || !domain || !password) {
- console.error('[SIP] 账号配置缺失:', { user, domain, hasPassword: !!password });
- this.emit('OnStatusMessage', { type: 'error', text: '账号配置不完整,请检查登录名、域名和密码' });
- return;
- }
- const uri = new JsSIP.URI('sip', user, domain);
- const contactUriStr = `sip:${user}@${domain};transport=ws`;
- this.configuration = {
- sockets: [socket],
- authorization_user: user,
- hack_ip_in_contact: true,
- user_agent: this.settings.user_agent || 'JsSIP',
- display_name: displayName || undefined,
- session_timers: true,
- no_answer_timeout: 60,
- uri: uri.toAor(),
- contact_uri: contactUriStr,
- };
- if (password && password.length === 32) {
- this.configuration.ha1 = password;
- this.configuration.realm = domain;
- } else {
- this.configuration.password = password;
- }
- console.log(`[SIP] UA配置完成: ${user}@${domain}`);
- }
- createUA() {
- this.ua = new JsSIP.UA(this.configuration);
- this.ua.set('display_name', this.profile.display_name);
- this.ua.on('connecting', this.connecting.bind(this));
- this.ua.on('connected', this.connected.bind(this));
- this.ua.on('disconnected', this.disconnected.bind(this));
- this.ua.on('registered', this.registered.bind(this));
- this.ua.on('unregistered', this.unregistered.bind(this));
- this.ua.on('registrationFailed', this.registrationFailed.bind(this));
- this.ua.on('registrationExpiring', this.registrationExpiring.bind(this));
- this.ua.on('newRTCSession', this.newRTCSession.bind(this));
- this.ua.on('newMessage', this.newMessage.bind(this));
- }
- On(event, callback) {
- this.events[event] = callback;
- }
- emit(event, ...args) {
- if (this.events[event]) {
- try {
- this.events[event](...args);
- } catch (e) {
- console.error(e);
- }
- }
- }
- resetReconnectState() {
- if (this.reconnectTimerId) {
- clearTimeout(this.reconnectTimerId);
- this.reconnectTimerId = null;
- }
- this.isReconnecting = false;
- this.reconnectAttempts = 0;
- this.reconnectStartTime = null;
- this.emit('OnReconnectStatus', { isReconnecting: false, failed: false });
- }
- Start(reconnect, isReconnect = false) {
- console.log(`[SIP] 启动 ${reconnect ? '启用重连' : '禁用重连'} ${isReconnect ? '(重连模式)' : ''}`);
- this.reconnectEnabled = reconnect;
- if (this.ua) {
- try {
- if (this.ua.isRegistered()) this.ua.unregister();
- this.ua.stop();
- } catch (e) {}
- this.ua = null;
- }
- if (!isReconnect) this.resetReconnectState();
- setTimeout(() => {
- this.createUA();
- if (this.ua.isRegistered()) {
- this.SetQueueIn();
- return;
- }
- if (this.ua.isConnected()) {
- this.Register();
- return;
- }
- try {
- this.ua.start();
- } catch (error) {
- console.error('[SIP] UA启动失败:', error.message);
- this.emit('OnStatusMessage', { type: 'error', text: '启动失败: ' + error.message });
- this.scheduleReconnect();
- }
- }, 50);
- }
- Register() {
- if (this.ua) this.ua.register();
- }
- UnRegister() {
- this.reconnectEnabled = false;
- this.resetReconnectState();
- if (this.ua) {
- try {
- if (this.ua.isRegistered()) this.ua.unregister();
- this.ua.stop();
- } catch (e) {}
- this.ua = null;
- }
- if (this.reconnectTimerId) clearTimeout(this.reconnectTimerId);
- if (this.callTimerId) clearInterval(this.callTimerId);
- if (this.sessionCloseTimerId) clearTimeout(this.sessionCloseTimerId);
- if (this.dtfmTimerId) clearTimeout(this.dtfmTimerId);
- }
- scheduleReconnect() {
- if (!this.reconnectEnabled) return;
- if (this.reconnectTimerId) {
- clearTimeout(this.reconnectTimerId);
- this.reconnectTimerId = null;
- }
- if (this.isReconnecting) return;
- const now = Date.now();
- if (this.reconnectStartTime === null) this.reconnectStartTime = now;
- const elapsed = now - this.reconnectStartTime;
- if (elapsed >= this.reconnectTotalDuration) {
- console.error('[SIP] 重连超时(超过1分钟)');
- this.isReconnecting = false;
- this.emit('OnReconnectStatus', { isReconnecting: false, failed: true });
- this.emit('OnStatusMessage', { type: 'error', text: '重连超时' });
- return;
- }
- this.reconnectAttempts++;
- let interval = (this.settings.reconnect_interval || 15) * 1000;
- if (this.reconnectAttempts > 5) interval = 10000;
- if (this.reconnectAttempts > 10) interval = 20000;
- if (elapsed + interval > this.reconnectTotalDuration) {
- const remainingTime = this.reconnectTotalDuration - elapsed;
- if (remainingTime < 1000) {
- this.emit('OnReconnectStatus', { isReconnecting: false, failed: true });
- this.emit('OnStatusMessage', { type: 'error', text: '重连超时' });
- return;
- }
- interval = Math.min(interval, remainingTime);
- }
- console.log(`[SIP] ${Math.ceil(interval/1000)}秒后重连 (第${this.reconnectAttempts}次)`);
- this.isReconnecting = true;
- this.emit('OnReconnectStatus', { isReconnecting: true, failed: false });
- this.reconnectTimerId = setTimeout(() => {
- this.isReconnecting = false;
- this.reconnectTimerId = null;
- this.Start(true, true);
- }, interval);
- }
- // ---- JsSIP 事件处理 ----
- connecting() {
- console.log('[SIP] 连接中...');
- this.emit('OnStatusMessage', { type: 'info', text: '连接中...' });
- }
- connected() {
- console.log('[SIP] 已连接,开始注册');
- this.Register();
- this.emit('OnStatusMessage', { type: 'success', text: '已连接' });
- }
- disconnected(e) {
- if (this._isHandlingDisconnect) return;
- this._isHandlingDisconnect = true;
- console.log('[SIP] 连接断开');
- this.emit('OnRegister', { registered: false });
- this.emit('OnStatusMessage', { type: 'error', text: '连接断开' });
- if (this.ua) {
- try { this.ua.stop(); } catch (err) {}
- this.ua = null;
- }
- if (!this.isReconnecting && this.reconnectEnabled) this.scheduleReconnect();
- setTimeout(() => { this._isHandlingDisconnect = false; }, 1000);
- }
- registered() {
- console.log('[SIP] 连接成功');
- this.resetReconnectState();
- this.emit('OnRegister', { registered: true });
- this.emit('OnStatusMessage', { type: 'success', text: '连接成功' });
- this.SetQueueIn();
- }
- unregistered() {
- console.log('[SIP] 已注销');
- this.emit('OnRegister', { registered: false });
- this.emit('OnStatusMessage', { type: 'info', text: '已注销' });
- }
- registrationFailed(e) {
- console.error('[SIP] 注册失败:', e.cause || '未知原因');
- this.emit('OnRegister', { registered: false });
- this.emit('OnStatusMessage', { type: 'error', text: '注册失败: ' + (e.cause || '未知原因') });
- if (!this.isReconnecting && this.reconnectEnabled) this.scheduleReconnect();
- }
- registrationExpiring() {
- console.log('[SIP] 注册即将过期,重新注册');
- this.Register();
- }
- // 外呼功能由 ccPhoneBarSocket 实现,WebPhone 不再实现 Call 方法
- // 但保留 Answer、Terminate、ToggleHold、ToggleMicPhone 等方法供来电和通话控制
- Answer() {
- if (this.session) this.session.answer({ mediaConstraints: { audio: true, video: false } });
- }
- Terminate(code) {
- if (this.session) {
- if (code) this.session.terminate({ status_code: code });
- else this.session.terminate();
- }
- if (this.sessionCloseTimerId) clearTimeout(this.sessionCloseTimerId);
- this.sessionCloseTimerId = setTimeout(() => this.sessionClosed(true, ''), 1000);
- }
- ToggleHold() {
- if (this.session && this.session.isEstablished()) {
- if (this.session.isOnHold().local) this.session.unhold();
- else this.session.hold();
- }
- }
- ToggleMicPhone() {
- if (this.session) {
- if (this.session.isMuted().audio) this.session.unmute();
- else this.session.mute();
- }
- }
- SetSpeaker(paused, volume) {
- this.remoteMedia.volume = paused ? 0 : volume;
- this.ringbackMedia.volume = paused ? 0 : volume;
- }
- SetMicPhone(paused, volume) {
- this.localMedia.volume = paused ? 0 : volume;
- }
- SetQueueIn() {
- if (this.ua && this.ua.isRegistered()) {
- this.ua.sendMessage('execute_available', `${this.profile.user}@${this.profile.domain}`, {});
- }
- }
- SetQueueOut() {
- if (this.ua && this.ua.isRegistered()) {
- this.ua.sendMessage('execute_logout', `${this.profile.user}@${this.profile.domain}`, {});
- }
- }
- SendDTMF(tone) {
- if (this.session && this.session.isEstablished()) this.session.sendDTMF(tone);
- }
- PlayDtmfTone(key) {
- const DTMF_MAP = {
- '0': [697, 1633], '1': [697, 1209], '2': [697, 1336], '3': [697, 1477],
- '4': [770, 1209], '5': [770, 1336], '6': [770, 1477], '7': [852, 1209],
- '8': [852, 1336], '9': [852, 1477], '*': [697, 1633], '#': [770, 1633]
- };
- const [lowFreq, highFreq] = DTMF_MAP[key] || [697, 1209];
- this.generateDtmfTone(lowFreq, highFreq, 0.2, this.remoteMedia.volume);
- }
- generateDtmfTone(lowFreq, highFreq, duration, volume) {
- if (this.dtfmTimerId) {
- clearTimeout(this.dtfmTimerId);
- if (this.oscillatorLow) {
- try { this.oscillatorLow.stop(); } catch (e) {}
- }
- if (this.oscillatorHigh) {
- try { this.oscillatorHigh.stop(); } catch (e) {}
- }
- }
- if (!this.audioCtx || this.audioCtx.state === 'closed') {
- this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
- }
- this.gainNode = this.audioCtx.createGain();
- this.oscillatorLow = this.audioCtx.createOscillator();
- this.oscillatorHigh = this.audioCtx.createOscillator();
- this.oscillatorLow.type = 'sine';
- this.oscillatorLow.frequency.value = lowFreq;
- this.oscillatorHigh.type = 'sine';
- this.oscillatorHigh.frequency.value = highFreq;
- this.oscillatorLow.connect(this.gainNode);
- this.oscillatorHigh.connect(this.gainNode);
- this.gainNode.connect(this.audioCtx.destination);
- this.gainNode.gain.setValueAtTime(volume, this.audioCtx.currentTime);
- this.gainNode.gain.linearRampToValueAtTime(0, this.audioCtx.currentTime + duration);
- this.oscillatorLow.start();
- this.oscillatorHigh.start();
- this.dtfmTimerId = setTimeout(() => {
- if (this.oscillatorLow) {
- try { this.oscillatorLow.stop(); } catch (e) {}
- }
- if (this.oscillatorHigh) {
- try { this.oscillatorHigh.stop(); } catch (e) {}
- }
- this.dtfmTimerId = null;
- }, duration * 1000);
- }
- IsOnHold() {
- return this.session ? this.session.isOnHold().local : false;
- }
- pauseRingback() {
- if (this.ringbackMedia && !this.ringbackMedia.paused) {
- this.ringbackMedia.pause();
- console.log('[音频] 暂停回铃音');
- }
- }
- playRingback() {
- if (this.ringbackMedia && this.ringbackMedia.paused) {
- this.ringbackMedia.currentTime = 0;
- this.ringbackMedia.play().catch(e => console.warn('[音频] 回铃音播放失败'));
- console.log('[音频] 播放回铃音');
- }
- }
- newRTCSession(event) {
- console.log('[通话] 新会话创建');
- if (this.session) { event.session.terminate({ status_code: 486 }); return; }
- this.session = event.session;
- this.session.on('progress', (e) => {
- this.emit('OnRing', {
- outgoing: this.session.direction === 'outgoing',
- province: e.response?.getHeader('X-Province') || '',
- city: e.response?.getHeader('X-City') || ''
- });
- });
- this.session.on('confirmed', () => {
- this.pauseRingback();
- this.emit('OnAnswered', this.session.direction === 'outgoing');
- this.startCallTimer();
- });
- this.session.on('ended', () => this.sessionClosed(true, ''));
- this.session.on('failed', (e) => this.sessionClosed(false, e.cause));
- this.session.on('peerconnection', (pc) => {
- if (pc && typeof pc.addTrack === 'function') {
- this.registerRemoteMedia(pc);
- } else if (this.session && this.session.connection && typeof this.session.connection.addTrack === 'function') {
- console.log('[通话] 使用session.connection');
- this.registerRemoteMedia(this.session.connection);
- } else {
- console.error('[通话] 未找到有效的RTCPeerConnection');
- }
- });
- const outgoing = this.session.direction === 'outgoing';
- this.emit('OnSessionCreated', {
- outgoing,
- callee: this.session.remote_identity?.uri?.user || '',
- province: event.request?.getHeader('X-Province') || '',
- city: event.request?.getHeader('X-City') || ''
- });
- if (!outgoing && this.settings.auto_answer) this.Answer();
- if (outgoing) this.playRingback();
- }
- newMessage(event) {
- if (event.message.direction === 'incoming') {
- this.emit('OnMessage', { type: 'request', message: event.request.body });
- }
- }
- registerRemoteMedia(connection) {
- if (!connection || typeof connection.addTrack !== 'function') {
- console.error('[通话] 无效的连接对象');
- return;
- }
- // 监听远程流
- connection.ontrack = (e) => {
- if (!this.remoteMedia.srcObject) {
- const stream = new MediaStream();
- e.streams[0].getTracks().forEach(track => stream.addTrack(track));
- this.remoteMedia.srcObject = stream;
- this.remoteMedia.play().catch(err => console.warn('[音频] 远程媒体播放失败'));
- this.pauseRingback();
- }
- };
- // 获取并添加本地流
- navigator.mediaDevices.getUserMedia({ audio: true })
- .then(stream => {
- this.localMedia.srcObject = stream;
- stream.getTracks().forEach(track => {
- try {
- connection.addTrack(track, stream);
- console.log('[音频] 本地轨道添加成功');
- } catch (err) {
- console.error('[音频] 添加本地轨道失败:', err);
- }
- });
- })
- .catch(err => {
- console.error('[音频] 获取麦克风失败:', err);
- this.emit('OnStatusMessage', { type: 'error', text: '无法获取麦克风权限' });
- });
- }
- sessionClosed(succeed, reason) {
- if (!this.session) return;
- 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());
- this.emit('OnSessionClosed', { succeeded: succeed, reason });
- }
- startCallTimer() {
- if (this.callTimerId) clearInterval(this.callTimerId);
- let seconds = 0;
- this.callTimerId = setInterval(() => {
- seconds++;
- const mins = Math.floor(seconds / 60);
- const secs = seconds % 60;
- this.emit('OnCallTimer', `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`);
- }, 1000);
- }
- destroy() {
- console.log('[销毁] WebPhone实例');
- this.reconnectEnabled = false;
- [this.reconnectTimerId, this.callTimerId, this.sessionCloseTimerId, this.dtfmTimerId].forEach(timerId => {
- if (timerId) clearTimeout(timerId);
- });
- this.UnRegister();
- const cleanupAudio = (audio) => {
- if (!audio) return;
- try {
- audio.pause();
- audio.src = '';
- if (audio.srcObject) {
- audio.srcObject.getTracks().forEach(t => t.stop());
- audio.srcObject = null;
- }
- audio.load();
- } catch (e) {}
- };
- cleanupAudio(this.ringbackMedia);
- cleanupAudio(this.remoteMedia);
- cleanupAudio(this.localMedia);
- this.ringbackMedia = null;
- this.remoteMedia = null;
- this.localMedia = null;
- if (this.audioCtx) {
- try { if (this.audioCtx.state !== 'closed') this.audioCtx.close(); } catch (e) {}
- this.audioCtx = null;
- }
- this.oscillatorLow = null;
- this.oscillatorHigh = null;
- this.gainNode = null;
- this.events = {};
- this.configuration = null;
- this.profile = null;
- this.settings = null;
- this.session = null;
- this.ua = null;
- console.log('[销毁] WebPhone清理完成');
- }
- }
- export default { WebPhone, ProfileManager, checkMicrophonePermission, RINGBACK_AUDIO_URL };
|