/** * BackgroundHandler - Mini Program background detection and auto hangup handler * * When mini program goes to background for more than 4s, automatically hang up the call * to avoid issues caused by WeChat disconnecting WebSocket after 5s. */ import { CallStatus, CallRole, StoreName, NAME } from '../const/index'; import TuiStore from '../TUIStore/tuiStore'; import { ITUIStore } from '../interface/index'; const TUIStore: ITUIStore = TuiStore.getInstance(); export interface IBackgroundConfig { enableAutoHangup: boolean; // Enable auto hangup on background (default: true) hangupTimeout: number; // Auto hangup timeout in ms (default: 4000) } const DEFAULT_BACKGROUND_CONFIG: IBackgroundConfig = { enableAutoHangup: true, hangupTimeout: 4000, // 4s auto hangup (before ws disconnects at 5s) }; export default class BackgroundHandler { private _callService: any; private _hangupTimer: ReturnType | null = null; private _backgroundTimestamp: number = 0; private _isInBackground: boolean = false; private _config: IBackgroundConfig = { ...DEFAULT_BACKGROUND_CONFIG }; private _isInitialized: boolean = false; constructor(callService: any) { this._callService = callService; } // Initialize background detection for mini program; Listen to wx.onAppHide/onAppShow events public initBackgroundDetection(): void { if (this._isInitialized) return; try { // @ts-ignore if (typeof wx !== 'undefined' && wx.onAppHide && wx.onAppShow) { // @ts-ignore wx.onAppHide(this._handleAppHide); // @ts-ignore wx.onAppShow(this._handleAppShow); this._isInitialized = true; console.log('[TUICallKit] BackgroundHandler initialized'); } } catch (error) { console.warn('[TUICallKit] BackgroundHandler init failed:', error); } } // Destroy background detection; Remove event listeners and clear timers public destroyBackgroundDetection(): void { if (!this._isInitialized) return; try { // @ts-ignore if (typeof wx !== 'undefined' && wx.offAppHide && wx.offAppShow) { // @ts-ignore wx.offAppHide(this._handleAppHide); // @ts-ignore wx.offAppShow(this._handleAppShow); } this._clearTimers(); this._isInitialized = false; console.log('[TUICallKit] BackgroundHandler destroyed'); } catch (error) { console.warn('[TUICallKit] BackgroundHandler destroy failed:', error); } } // Handle app hide event (going to background) private _handleAppHide = (): void => { const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) || CallStatus.IDLE; const callRole = TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE) || CallRole.UNKNOWN; // Only handle when in connected state (as per requirement: only for connected calls) if ((callStatus !== CallStatus.CONNECTED) || !this._config.enableAutoHangup) return; this._isInBackground = true; this._backgroundTimestamp = Date.now(); this._callService?._tuiCallEngine?.reportLog?.({ name: 'TUICallkit.background.appHide', data: { callStatus, callRole, timestamp: this._backgroundTimestamp, config: this._config }, }); this._hangupTimer = setTimeout(() => { this._handleBackgroundTimeout(); }, this._config.hangupTimeout); }; // Handle app show event (returning from background) private _handleAppShow = (): void => { if (!this._isInBackground) return; const backgroundDuration = Date.now() - this._backgroundTimestamp; const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) || CallStatus.IDLE; const callRole = TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE) || CallRole.UNKNOWN; this._callService?._tuiCallEngine?.reportLog?.({ name: 'TUICallkit.background.appShow', data: { callStatus, callRole, backgroundDuration, timestamp: Date.now() }, }); this._isInBackground = false; this._clearTimers(); // If background time exceeded hangup timeout, show toast to explain why call ended if (backgroundDuration >= this._config.hangupTimeout) { this._showToast('后台超时,通话已结束'); this._checkCallStatusAfterBackground(); } }; // Show toast - compatible with both UniApp and native mini program private _showToast(title: string): void { try { // @ts-ignore - UniApp API if (typeof uni !== 'undefined' && uni?.showToast) { // @ts-ignore uni.showToast({ title, icon: 'none', duration: 2500 }); // @ts-ignore - Native mini program API } else if (typeof wx !== 'undefined' && wx?.showToast) { // @ts-ignore wx.showToast({ title, icon: 'none', duration: 2500 }); } } catch (error) {} } // Handle background timeout - auto hangup private async _handleBackgroundTimeout(): Promise { try { const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) || CallStatus.IDLE; const callRole = TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE) || CallRole.UNKNOWN; const backgroundDuration = Date.now() - this._backgroundTimestamp; // Report timeout event before hangup this._callService?._tuiCallEngine?.reportLog?.({ name: 'TUICallkit.background.timeout', data: { callStatus, callRole, backgroundDuration, hangupTimeout: this._config.hangupTimeout }, }); // Only handle CONNECTED state (CALLING state is filtered in _handleAppHide) if (callStatus === CallStatus.CONNECTED) { this._callService?._tuiCallEngine?.reportLog?.({ name: 'TUICallkit.background.hangup', data: { callStatus, callRole, action: 'hangup' }, }); await this._callService?.hangup?.(); } } catch (error) { this._callService?._tuiCallEngine?.reportLog?.({ name: 'TUICallkit.background.hangup.fail', data: { error: String(error) }, }); } } // Check call status after returning from long background; Handle case where call should have ended private _checkCallStatusAfterBackground(): void { try { const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) || CallStatus.IDLE; // If still in call state after exceeding timeout, something went wrong // The call should have been ended by _handleBackgroundTimeout if (callStatus !== CallStatus.IDLE) { // Force reset if needed this._callService?._resetCallStore?.(); } } catch (error) { console.warn('[TUICallKit] Check call status after background failed:', error); } } // Clear hangup timer private _clearTimers(): void { if (this._hangupTimer) { clearTimeout(this._hangupTimer); this._hangupTimer = null; } } }