diff --git a/docs/ai-playbook/flows.zh-CN.md b/docs/ai-playbook/flows.zh-CN.md index b6920f9..ececf29 100644 --- a/docs/ai-playbook/flows.zh-CN.md +++ b/docs/ai-playbook/flows.zh-CN.md @@ -634,9 +634,15 @@ async requestWakeLock(): Promise { **网络切换适应**: - **连接检测**:监听 `connectionstatechange` 事件检测网络质量变化 -- **自动重连**:`iceConnectionState: 'disconnected'` 时触发重连流程 +- **自动重连**:`connectionState: 'disconnected' | 'failed' | 'closed'` 时均触发重连流程(统一走 attemptReconnection) - **状态恢复**:重连成功后恢复房间状态和传输进度 +**移动端后台/前台切换补充策略**: + +- **socket 连接恢复自动入房**:`socket.on('connect')` 时,若已持有 `roomId` 且(`lastJoinedSocketId !== socket.id` 或 `!isInRoom`),则强制重新 `joinRoom(roomId, isInitiator, isInitiator)`;发送端会自动广播 `initiator-online`,接收端回复 `recipient-ready`。 +- **身份追踪**:成功 `joinRoom` 后记录 `lastJoinedSocketId = socket.id`,用以检测“后台恢复时 socketId 更换”的情形。 +- **门槛放宽**:`attemptReconnection` 只要满足“`roomId` 存在,且满足任一:P2P 断开 / socket 断开 / socketId 改变”,即可发起重连;不再强依赖“socket 与 P2P 同时断开”。 + ### 重连调试要点 **关键日志点**: diff --git a/frontend/lib/webrtc_base.ts b/frontend/lib/webrtc_base.ts index 9b9bce6..ef4fee6 100644 --- a/frontend/lib/webrtc_base.ts +++ b/frontend/lib/webrtc_base.ts @@ -66,6 +66,8 @@ export default class BaseWebRTC { protected wakeLockManager: WakeLockManager; // Graceful disconnect tracking protected gracefullyDisconnectedPeers: Set; + // Track last socket.id used to successfully join a room + protected lastJoinedSocketId: string | null; constructor(config: WebRTCConfig) { this.iceServers = config.iceServers; @@ -94,6 +96,7 @@ export default class BaseWebRTC { this.isPeerDisconnected = false; this.reconnectionInProgress = false; this.wakeLockManager = new WakeLockManager(); + this.lastJoinedSocketId = null; } // region Logging and Error Handling protected log( @@ -113,9 +116,41 @@ export default class BaseWebRTC { // endregion // Sets up event listeners for the signaling server to handle various signaling messages (connection, ICE candidates, offer, answer, etc.). setupCommonSocketListeners() { - this.socket.on("connect", () => { + this.socket.on("connect", async () => { this.peerId = this.socket.id; // Save own ID + this.isSocketDisconnected = false; this.log("log", `Connected to signaling server, peerId: ${this.peerId}`); + + // Auto re-join if we previously joined a room but socket.id changed + const hasRoom = !!this.roomId; + const currentSocketId = this.socket.id ?? null; + const socketIdChanged = + this.lastJoinedSocketId !== null && + this.lastJoinedSocketId !== currentSocketId; + + if (hasRoom && (socketIdChanged || !this.isInRoom)) { + // Ensure joinRoom does not early-return + if (socketIdChanged) this.isInRoom = false; + + if (!this.reconnectionInProgress) { + this.reconnectionInProgress = true; + try { + const sendInitiatorOnline = this.isInitiator; + await this.joinRoom( + this.roomId as string, + this.isInitiator, + sendInitiatorOnline + ); + // Reset flags after successful auto rejoin + this.isSocketDisconnected = false; + this.isPeerDisconnected = false; + } catch (error) { + this.fireError("Auto rejoin on socket connect failed", { error }); + } finally { + this.reconnectionInProgress = false; + } + } + } }); this.socket.on("error", (error) => { @@ -149,17 +184,25 @@ export default class BaseWebRTC { } protected async attemptReconnection(): Promise { if (this.reconnectionInProgress) return; + if (!this.roomId) return; - if (this.isSocketDisconnected && this.isPeerDisconnected && this.roomId) { - // Start reconnection only after both socket and P2P connections are disconnected + const currentSocketId = this.socket.id ?? null; + const socketIdChanged = + this.lastJoinedSocketId !== null && + this.lastJoinedSocketId !== currentSocketId; + + // Widen condition: if either side disconnected or socketId changed, try to rejoin + if (this.isPeerDisconnected || this.isSocketDisconnected || socketIdChanged) { this.reconnectionInProgress = true; if (developmentEnv === "development") { postLogToBackend( - `Starting reconnection, socket and peer both disconnected. isInitiator:${this.isInitiator}` + `Starting reconnection. socketDisc:${this.isSocketDisconnected}, peerDisc:${this.isPeerDisconnected}, socketIdChanged:${socketIdChanged}, isInitiator:${this.isInitiator}` ); } try { + // Ensure joinRoom does not early-return + if (socketIdChanged) this.isInRoom = false; const sendInitiatorOnline = this.isInitiator; await this.joinRoom(this.roomId, this.isInitiator, sendInitiatorOnline); @@ -323,11 +366,15 @@ export default class BaseWebRTC { failed: async () => { this.cleanupExistingConnection(peerId); this.isPeerDisconnected = true; + // Attempt to reconnect as well when failed + this.attemptReconnection(); await this.wakeLockManager.releaseWakeLock(); }, closed: async () => { this.cleanupExistingConnection(peerId); this.isPeerDisconnected = true; + // Attempt to reconnect when closed + this.attemptReconnection(); await this.wakeLockManager.releaseWakeLock(); }, // The following must be added to prevent errors @@ -437,6 +484,8 @@ export default class BaseWebRTC { if (response.success) { this.roomId = roomId; this.isInRoom = true; + // Record the socket.id used for this successful join + this.lastJoinedSocketId = this.socket.id ?? null; if (sendInitiatorOnline) { this.socket.emit("initiator-online", { roomId: this.roomId,