diff --git a/src/views/im/home/index.vue b/src/views/im/home/index.vue
index ff9bff657..a852ddde0 100644
--- a/src/views/im/home/index.vue
+++ b/src/views/im/home/index.vue
@@ -22,6 +22,9 @@
+ * 单一 dispatcher,单次 JSON 解析,mirror handleFriendNotification 的结构 + */ + handleRtcSignaling(websocketMessage: ImPrivateMessageDTO) { + const rtcStore = useRtcStore() + switch (websocketMessage.type) { + case ImMessageType.RTC_CALL: { + const payload = this.safeParse(websocketMessage.content) as ImRtcCallNotification | null + if (!payload) { + return + } + switch (payload.status) { + case ImRtcParticipantStatus.INVITING: + // 当前已在通话中:忽略新来电;后端层面也会拒绝,这里是兜底 + if (!rtcStore.isActive) { + rtcStore.showIncoming(payload) + } + break + case ImRtcParticipantStatus.REJECTED: + // 群通话单人拒绝;把拒绝者从 pending 占位移除(私聊拒绝走 RTC_CALL_END 入消息流,不走本通道) + if (payload.operatorUserId) { + rtcStore.markUserLeft(payload.operatorUserId) + } + break + case ImRtcParticipantStatus.JOINED: + case ImRtcParticipantStatus.NO_ANSWER: + case ImRtcParticipantStatus.LEFT: + // ACCEPT / CANCEL / HUNGUP 暂不需要本端额外响应;rtcStore 状态由 1602/1603 + END 维护 + break + default: + console.warn('[IM WS] 未识别的 RTC_CALL status', payload) + } + return + } + case ImMessageType.RTC_PARTICIPANT_CONNECTED: { + const payload = this.safeParse( + websocketMessage.content + ) as ImRtcParticipantConnectedNotification | null + if (payload?.room && payload.userId) { + rtcStore.applyParticipantConnected(payload) + } + return + } + case ImMessageType.RTC_PARTICIPANT_DISCONNECTED: { + const payload = this.safeParse( + websocketMessage.content + ) as ImRtcParticipantDisconnectedNotification | null + if (payload?.room && payload.userId) { + rtcStore.applyParticipantDisconnected(payload) + } + } + } + }, + + /** + * RTC_CALL_END 通话结束;私聊 + 群聊都走这一条;payload 携带 conversationType 区分 + *
+ * 私聊:关闭当前通话窗 + * 群聊:移除胶囊条;如本端在该群通话内则关闭通话窗 + */ + handleRtcCallEnd(websocketMessage: ImPrivateMessageDTO | ImGroupMessageDTO) { + const payload = this.safeParse(websocketMessage.content) as ImRtcCallEndNotification | null + if (!payload?.room) { + return + } + const rtcStore = useRtcStore() + const isGroup = payload.conversationType === ImConversationType.GROUP + // 群通话:移除胶囊条(按外层 groupId 取,不依赖 payload) + const groupId = (websocketMessage as ImGroupMessageDTO).groupId + if (isGroup && groupId) { + rtcStore.removeGroupCall(groupId) + } + // 通话窗 / 来电窗指向同一 room 时关闭: + // RUNNING / INVITING 阶段对比 session.room;INCOMING 阶段对比 incomingPayload.room + const matchSession = rtcStore.session?.room === payload.room + const matchIncoming = rtcStore.incomingPayload?.room === payload.room + if (rtcStore.isActive && (matchSession || matchIncoming)) { + const reasonText = resolveCallEndReasonText(payload.endReason) + console.info('[Call] end:', reasonText) + rtcStore.reset() + } } } }) diff --git a/src/views/im/utils/constants.ts b/src/views/im/utils/constants.ts index 09e94a8e0..af1c5b2e3 100644 --- a/src/views/im/utils/constants.ts +++ b/src/views/im/utils/constants.ts @@ -165,20 +165,20 @@ export function isGroupConversation(type: number | undefined): boolean { } /** IM 通话媒体类型(对齐后端 ImRtcCallMediaTypeEnum) */ -export const ImCallMediaType = { +export const ImRtcCallMediaType = { VOICE: 1, VIDEO: 2 } as const /** IM 通话状态(对齐后端 ImRtcCallStatusEnum) */ -export const ImCallStatus = { +export const ImRtcCallStatus = { CREATED: 10, // 创建:私聊等被叫接听;群聊发起人已进房等其他人加入 RUNNING: 20, // 进行中:第一个非发起人接通后进入 ENDED: 30 // 已结束 } as const /** IM 通话结束原因(对齐后端 ImRtcCallEndReasonEnum) */ -export const ImCallEndReason = { +export const ImRtcCallEndReason = { HANGUP: 1, // 接通后任一方主动挂断 REJECT: 2, // 被叫接通前点拒接 CANCEL: 3, // 主叫接通前主动取消 @@ -186,11 +186,11 @@ export const ImCallEndReason = { ERROR: 9 // 网络中断 / 设备失败 } as const -/** ImCallEndReason 取值类型 */ -export type ImCallEndReasonValue = (typeof ImCallEndReason)[keyof typeof ImCallEndReason] +/** ImRtcCallEndReason 取值类型 */ +export type ImRtcCallEndReasonValue = (typeof ImRtcCallEndReason)[keyof typeof ImRtcCallEndReason] /** IM 通话参与者状态(对齐后端 ImRtcParticipantStatusEnum);同时作为 RTC_CALL 信令 status 字段取值 */ -export const ImCallParticipantStatus = { +export const ImRtcParticipantStatus = { INVITING: 10, // 来电邀请 JOINED: 20, // 接听 / 已加入 REJECTED: 30, // 拒接 @@ -198,9 +198,23 @@ export const ImCallParticipantStatus = { LEFT: 50 // 挂断离开 } as const -/** ImCallParticipantStatus 取值类型 */ -export type ImCallParticipantStatusValue = - (typeof ImCallParticipantStatus)[keyof typeof ImCallParticipantStatus] +/** ImRtcParticipantStatus 取值类型 */ +export type ImRtcParticipantStatusValue = + (typeof ImRtcParticipantStatus)[keyof typeof ImRtcParticipantStatus] + +/** + * IM 通话 UI 阶段;前端独有,用于驱动 inviting / incoming / running 三种弹窗切换; + * 跟后端 ImRtcCallStatus 不是 1:1 映射,stage 多了「自己是主叫还是被叫」的角色维度 + */ +export const ImRtcCallStage = { + IDLE: 'idle', // 空闲;后端无对应(本端无 session) + INVITING: 'inviting', // 主叫等待对方接受;对应后端 ImRtcCallStatus.CREATED(自己是主叫) + INCOMING: 'incoming', // 被叫来电响铃;对应后端 ImRtcCallStatus.CREATED(自己是被叫) + RUNNING: 'running' // 通话中;对应后端 ImRtcCallStatus.RUNNING +} as const + +/** ImRtcCallStage 取值类型 */ +export type ImRtcCallStageValue = (typeof ImRtcCallStage)[keyof typeof ImRtcCallStage] /** IM WebSocket 外层帧类型(对齐后端 ImPrivateMessageDTO.TYPE / ImGroupMessageDTO.TYPE) */ export const ImWebSocketMessageType = { diff --git a/src/views/im/utils/message.ts b/src/views/im/utils/message.ts index 0b9f3a043..ff618f182 100644 --- a/src/views/im/utils/message.ts +++ b/src/views/im/utils/message.ts @@ -1,7 +1,7 @@ import { generateUUID } from '@/utils' import { useUserStore } from '@/store/modules/user' import { - ImCallEndReason, + ImRtcCallEndReason, ImConversationType, ImMessageType, type ImConversationTypeValue @@ -905,15 +905,15 @@ export function resolveRtcCallPrivateBubbleText(payload: RtcCallEndPayload | nul const hasDuration = duration > 0 const isOperator = payload.operatorUserId === getCurrentUserId() switch (payload.endReason) { - case ImCallEndReason.HANGUP: + case ImRtcCallEndReason.HANGUP: return hasDuration ? `通话时长 ${formatCallDuration(duration)}` : '通话中断' - case ImCallEndReason.CANCEL: + case ImRtcCallEndReason.CANCEL: return isOperator ? '已取消' : '对方已取消' - case ImCallEndReason.REJECT: + case ImRtcCallEndReason.REJECT: return isOperator ? '已拒绝' : '对方已拒绝' - case ImCallEndReason.BUSY: + case ImRtcCallEndReason.BUSY: return isOperator ? '忙线未接听' : '对方忙线中' - case ImCallEndReason.ERROR: + case ImRtcCallEndReason.ERROR: return hasDuration ? `通话中断 ${formatCallDuration(duration)}` : '通话中断' default: return hasDuration ? `通话时长 ${formatCallDuration(duration)}` : '通话已结束' @@ -936,15 +936,15 @@ export function resolveRtcCallTipText(message: { */ export function resolveCallEndReasonText(reason: number | undefined): string { switch (reason) { - case ImCallEndReason.REJECT: + case ImRtcCallEndReason.REJECT: return '对方已拒绝' - case ImCallEndReason.CANCEL: + case ImRtcCallEndReason.CANCEL: return '对方已取消' - case ImCallEndReason.BUSY: + case ImRtcCallEndReason.BUSY: return '对方忙线中' - case ImCallEndReason.HANGUP: + case ImRtcCallEndReason.HANGUP: return '通话已结束' - case ImCallEndReason.ERROR: + case ImRtcCallEndReason.ERROR: return '通话异常' default: return '通话已断开'