feat(phase2e-2): 前端 mediasoup-client 集成 + Pinia meeting Store(Task 9)

- frontend/src/api/meeting.js:12 个 REST 接口封装,统一 unwrap envelope.data
- frontend/src/services/websocket.js:新增 sendWithAck(Promise 化 + 超时 + 序列号)
- frontend/src/utils/mediasoup-client.js:MediaEngine 包装 Device/Transport/Producer/Consumer
- frontend/src/store/meeting.js:Pinia 会议状态机,桥接 14 个 WS 事件 + cleanupStaleMeetings
- frontend/src/constants/meeting.js:状态枚举 + 事件名集中管理
- frontend/src/pages/meeting/debug.vue:临时调试页(H5 原生 video/audio DOM 绕过 uni 组件限制)
- backend:meeting.consume.resume WS 事件 + create/join 响应透传 router_id + rtp_capabilities
- 文档:frontend/meeting.md、websocket.md、CURRENT_STATUS、plan 全部同步 Task 9 落地

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-04-22 11:22:49 +08:00
parent e7f6a32dfe
commit f97fec24a8
22 changed files with 2314 additions and 60 deletions

View File

@@ -18,6 +18,7 @@ const HEARTBEAT_INTERVAL = 30000
const RECONNECT_BASE_DELAY = 1000
const RECONNECT_MAX_DELAY = 30000
const MAX_RECONNECT_ATTEMPTS = Infinity
const ACK_DEFAULT_TIMEOUT_MS = 10000
class WebSocketService {
constructor() {
@@ -28,6 +29,11 @@ class WebSocketService {
this.reconnectAttempts = 0
this.isManualClose = false
this.seq = 0
/**
* 等待 ACK 的请求索引Map<seq, { resolve, reject, timer, event }>
* Task 9 引入,为 meeting.* 事件提供 Promise 化 ACK 等待
*/
this.pendingAcks = new Map()
}
/**
@@ -120,6 +126,41 @@ class WebSocketService {
return this.seq
}
/**
* 发送消息并等待服务端 ACKTask 9 引入)
*
* 服务端对 C→S 事件以相同 event 名 + 对应 seq 返回 ACK 报文:
* { event, seq, data: { code, message, data? } }
*
* 约定:
* - code === 0 表示成功Promise resolve 为 data.data业务 payload
* - code !== 0 表示业务失败Promise reject 为 { code, message }
* - 超时 reject 为 { code: -1, message: 'ACK 超时' }
* - 连接未就绪 reject 为 { code: -1, message: '连接未就绪' }
*
* 典型用法:
* const info = await wsService.sendWithAck('meeting.transport.create', { room_code, direction: 'send' })
*
* @param {string} event - 事件名
* @param {Object} [data] - 业务 payload
* @param {number} [timeoutMS=10000] - 超时时间
* @returns {Promise<any>} ACK 的 data 字段
*/
sendWithAck(event, data = {}, timeoutMS = ACK_DEFAULT_TIMEOUT_MS) {
return new Promise((resolve, reject) => {
const seq = this.send(event, data)
if (seq === -1) {
reject({ code: -1, message: '连接未就绪' })
return
}
const timer = setTimeout(() => {
this.pendingAcks.delete(seq)
reject({ code: -1, message: `ACK 超时 (${event})` })
}, timeoutMS)
this.pendingAcks.set(seq, { resolve, reject, timer, event })
})
}
/**
* 注册事件监听
* @param {string} event - 事件类型
@@ -171,6 +212,7 @@ class WebSocketService {
_onClose(e) {
console.log('[WS] 连接关闭', e)
this._clearTimers()
this._rejectAllPendingAcks('连接已断开')
this._emit('_disconnected')
if (!this.isManualClose) {
@@ -178,6 +220,19 @@ class WebSocketService {
}
}
/**
* 连接断开时统一拒绝所有未完成的 ACK 等待Task 9
* 避免业务层 await sendWithAck() 永久挂起
*/
_rejectAllPendingAcks(reason) {
if (this.pendingAcks.size === 0) return
this.pendingAcks.forEach((pending, seq) => {
clearTimeout(pending.timer)
pending.reject({ code: -1, message: reason, event: pending.event })
})
this.pendingAcks.clear()
}
_onError(e) {
console.error('[WS] 连接错误', e)
}
@@ -185,12 +240,36 @@ class WebSocketService {
_onMessage(data) {
try {
const msg = JSON.parse(data)
// Task 9ACK 报文event 以 ".ack" 结尾)走 pendingAcks 处理
// 后端 ws.NewResponse 固定在原 event 后追加 ".ack"
if (typeof msg.event === 'string' && msg.event.endsWith('.ack')) {
this._handleAck(msg)
return
}
this._emit(msg.event, msg)
} catch (e) {
console.warn('[WS] 消息解析失败', data)
}
}
/**
* 处理 ACK 报文,触发 pendingAcks 中对应 seq 的 resolve/reject
* @param {Object} msg - { event: 'xxx.ack', seq, code, message, data? }
*/
_handleAck(msg) {
const pending = this.pendingAcks.get(msg.seq)
if (!pending) {
return
}
this.pendingAcks.delete(msg.seq)
clearTimeout(pending.timer)
if (msg.code === 0) {
pending.resolve(msg.data)
} else {
pending.reject({ code: msg.code, message: msg.message || 'WS 请求失败', event: pending.event })
}
}
/** 分发事件给注册的监听器 */
_emit(event, data) {
const cbs = this.listeners.get(event)