feat(CallFeature): implement call functionality and integrate WebSocket signaling
- Updated app.json to include microphone permissions and background audio support. - Added call-related dependencies in package.json and package-lock.json. - Enhanced ChatScreen to initiate calls and handle call actions. - Introduced call components in the layout for incoming call handling. - Expanded WebSocket service to manage call signaling messages and events. - Refactored authStore to initialize call state on user authentication.
This commit is contained in:
@@ -30,7 +30,106 @@ export type WSMessageType =
|
||||
| 'pong'
|
||||
| 'message_sent'
|
||||
| 'message_recalled'
|
||||
| 'error';
|
||||
| 'error'
|
||||
| 'call_incoming'
|
||||
| 'call_accepted'
|
||||
| 'call_rejected'
|
||||
| 'call_busy'
|
||||
| 'call_sdp'
|
||||
| 'call_ice'
|
||||
| 'call_ended'
|
||||
| 'call_peer_muted'
|
||||
| 'call_invited'
|
||||
| 'call_answered_elsewhere';
|
||||
|
||||
export interface WSCallIncomingMessage {
|
||||
type: 'call_incoming';
|
||||
call_id: string;
|
||||
conversation_id: string;
|
||||
caller_id: string;
|
||||
call_type: string;
|
||||
created_at: number;
|
||||
lifetime?: number;
|
||||
ice_servers?: ICEServerConfig[];
|
||||
}
|
||||
|
||||
export interface ICEServerConfig {
|
||||
urls: string[];
|
||||
username?: string;
|
||||
credential?: string;
|
||||
}
|
||||
|
||||
export interface WSCallAcceptedMessage {
|
||||
type: 'call_accepted';
|
||||
call_id: string;
|
||||
started_at: number;
|
||||
ice_servers?: ICEServerConfig[];
|
||||
}
|
||||
|
||||
export interface WSCallRejectedMessage {
|
||||
type: 'call_rejected';
|
||||
call_id: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface WSCallBusyMessage {
|
||||
type: 'call_busy';
|
||||
call_id: string;
|
||||
}
|
||||
|
||||
export interface WSCallEndedMessage {
|
||||
type: 'call_ended';
|
||||
call_id: string;
|
||||
ended_by?: string;
|
||||
reason?: string;
|
||||
duration?: number;
|
||||
ended_at?: number;
|
||||
}
|
||||
|
||||
export interface WSCallSDPMessage {
|
||||
type: 'call_sdp';
|
||||
call_id: string;
|
||||
from_id: string;
|
||||
payload: {
|
||||
sdp_type: string;
|
||||
sdp: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WSCallICEMessage {
|
||||
type: 'call_ice';
|
||||
call_id: string;
|
||||
from_id: string;
|
||||
payload: {
|
||||
candidate: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WSCallPeerMutedMessage {
|
||||
type: 'call_peer_muted';
|
||||
call_id: string;
|
||||
user_id: string;
|
||||
muted: boolean;
|
||||
}
|
||||
|
||||
export interface WSCallInvitedMessage {
|
||||
type: 'call_invited';
|
||||
call_id: string;
|
||||
conversation_id: string;
|
||||
callee_id: string;
|
||||
}
|
||||
|
||||
export interface WSCallAnsweredElsewhereMessage {
|
||||
type: 'call_answered_elsewhere';
|
||||
call_id: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface WSErrorMessage {
|
||||
type: 'error';
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WSChatMessage {
|
||||
type: 'chat';
|
||||
@@ -166,7 +265,18 @@ export type WSMessage =
|
||||
| WSGroupNoticeMessage
|
||||
| WSGroupMentionMessage
|
||||
| WSGroupReadMessage
|
||||
| WSGroupRecallMessage;
|
||||
| WSGroupRecallMessage
|
||||
| WSCallIncomingMessage
|
||||
| WSCallAcceptedMessage
|
||||
| WSCallRejectedMessage
|
||||
| WSCallBusyMessage
|
||||
| WSCallEndedMessage
|
||||
| WSCallSDPMessage
|
||||
| WSCallICEMessage
|
||||
| WSCallPeerMutedMessage
|
||||
| WSCallInvitedMessage
|
||||
| WSCallAnsweredElsewhereMessage
|
||||
| WSErrorMessage;
|
||||
|
||||
type MessageHandler<T extends WSMessage = WSMessage> = (message: T) => void;
|
||||
type ConnectionHandler = () => void;
|
||||
@@ -273,17 +383,35 @@ class WebSocketService {
|
||||
}
|
||||
|
||||
private handleIncoming(data: string): void {
|
||||
try {
|
||||
const msg: WSServerMessage = JSON.parse(data);
|
||||
|
||||
// 后端 writePump 会将多条消息以 \n 分隔合并发送,需要逐条解析
|
||||
const lines = data.split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
const msg: WSServerMessage = JSON.parse(trimmed);
|
||||
this.processMessage(msg);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error, 'raw:', trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private processMessage(msg: WSServerMessage): void {
|
||||
// 处理pong响应
|
||||
if (msg.type === 'pong') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理错误消息
|
||||
// 处理错误消息 - emit 到上层处理
|
||||
if (msg.type === 'error') {
|
||||
console.error('WebSocket error:', msg.payload);
|
||||
const err: WSErrorMessage = {
|
||||
type: 'error',
|
||||
code: msg.payload?.code || 'unknown',
|
||||
message: msg.payload?.message || 'Unknown error',
|
||||
};
|
||||
this.emit('error', err);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -306,9 +434,6 @@ class WebSocketService {
|
||||
|
||||
// 分发事件
|
||||
this.dispatchServerMessage(msg);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchServerMessage(msg: WSServerMessage): void {
|
||||
@@ -333,6 +458,37 @@ class WebSocketService {
|
||||
case 'system_notification':
|
||||
this.handleSystemNotification(payload);
|
||||
break;
|
||||
// Call signaling messages
|
||||
case 'call_incoming':
|
||||
this.handleCallIncoming(payload);
|
||||
break;
|
||||
case 'call_accepted':
|
||||
this.handleCallAccepted(payload);
|
||||
break;
|
||||
case 'call_rejected':
|
||||
this.handleCallRejected(payload);
|
||||
break;
|
||||
case 'call_busy':
|
||||
this.handleCallBusy(payload);
|
||||
break;
|
||||
case 'call_sdp':
|
||||
this.handleCallSDP(payload);
|
||||
break;
|
||||
case 'call_ice':
|
||||
this.handleCallICE(payload);
|
||||
break;
|
||||
case 'call_ended':
|
||||
this.handleCallEnded(payload);
|
||||
break;
|
||||
case 'call_peer_muted':
|
||||
this.handleCallPeerMuted(payload);
|
||||
break;
|
||||
case 'call_invited':
|
||||
this.handleCallInvited(payload);
|
||||
break;
|
||||
case 'call_answered_elsewhere':
|
||||
this.handleCallAnsweredElsewhere(payload);
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown message type:', type);
|
||||
}
|
||||
@@ -458,6 +614,152 @@ class WebSocketService {
|
||||
systemNotificationService.handleWSMessage(m as any).catch(() => {});
|
||||
}
|
||||
|
||||
// Call signaling handlers
|
||||
private handleCallIncoming(payload: any): void {
|
||||
const m: WSCallIncomingMessage = {
|
||||
type: 'call_incoming',
|
||||
call_id: payload.call_id,
|
||||
conversation_id: payload.conversation_id,
|
||||
caller_id: payload.caller_id,
|
||||
call_type: payload.call_type,
|
||||
created_at: payload.created_at,
|
||||
lifetime: payload.lifetime,
|
||||
ice_servers: payload.ice_servers,
|
||||
};
|
||||
this.emit('call_incoming', m);
|
||||
}
|
||||
|
||||
private handleCallAccepted(payload: any): void {
|
||||
const m: WSCallAcceptedMessage = {
|
||||
type: 'call_accepted',
|
||||
call_id: payload.call_id,
|
||||
started_at: payload.started_at,
|
||||
ice_servers: payload.ice_servers,
|
||||
};
|
||||
this.emit('call_accepted', m);
|
||||
}
|
||||
|
||||
private handleCallRejected(payload: any): void {
|
||||
const m: WSCallRejectedMessage = {
|
||||
type: 'call_rejected',
|
||||
call_id: payload.call_id,
|
||||
reason: payload.reason,
|
||||
};
|
||||
this.emit('call_rejected', m);
|
||||
}
|
||||
|
||||
private handleCallBusy(payload: any): void {
|
||||
const m: WSCallBusyMessage = {
|
||||
type: 'call_busy',
|
||||
call_id: payload.call_id,
|
||||
};
|
||||
this.emit('call_busy', m);
|
||||
}
|
||||
|
||||
private handleCallSDP(payload: any): void {
|
||||
const m: WSCallSDPMessage = {
|
||||
type: 'call_sdp',
|
||||
call_id: payload.call_id,
|
||||
from_id: payload.from_id,
|
||||
payload: payload.payload,
|
||||
};
|
||||
this.emit('call_sdp', m);
|
||||
}
|
||||
|
||||
private handleCallICE(payload: any): void {
|
||||
const m: WSCallICEMessage = {
|
||||
type: 'call_ice',
|
||||
call_id: payload.call_id,
|
||||
from_id: payload.from_id,
|
||||
payload: payload.payload,
|
||||
};
|
||||
this.emit('call_ice', m);
|
||||
}
|
||||
|
||||
private handleCallEnded(payload: any): void {
|
||||
const m: WSCallEndedMessage = {
|
||||
type: 'call_ended',
|
||||
call_id: payload.call_id,
|
||||
ended_by: payload.ended_by,
|
||||
reason: payload.reason,
|
||||
duration: payload.duration,
|
||||
ended_at: payload.ended_at,
|
||||
};
|
||||
this.emit('call_ended', m);
|
||||
}
|
||||
|
||||
private handleCallPeerMuted(payload: any): void {
|
||||
const m: WSCallPeerMutedMessage = {
|
||||
type: 'call_peer_muted',
|
||||
call_id: payload.call_id,
|
||||
user_id: payload.user_id,
|
||||
muted: payload.muted,
|
||||
};
|
||||
this.emit('call_peer_muted', m);
|
||||
}
|
||||
|
||||
private handleCallInvited(payload: any): void {
|
||||
const m: WSCallInvitedMessage = {
|
||||
type: 'call_invited',
|
||||
call_id: payload.call_id,
|
||||
conversation_id: payload.conversation_id,
|
||||
callee_id: payload.callee_id,
|
||||
};
|
||||
this.emit('call_invited', m);
|
||||
}
|
||||
|
||||
private handleCallAnsweredElsewhere(payload: any): void {
|
||||
const m: WSCallAnsweredElsewhereMessage = {
|
||||
type: 'call_answered_elsewhere',
|
||||
call_id: payload.call_id,
|
||||
reason: payload.reason,
|
||||
};
|
||||
this.emit('call_answered_elsewhere', m);
|
||||
}
|
||||
|
||||
// Call signaling send methods
|
||||
sendCallInvite(conversationId: string, calleeId: string): void {
|
||||
this.sendFireAndForget('call_invite', {
|
||||
conversation_id: conversationId,
|
||||
callee_id: calleeId,
|
||||
});
|
||||
}
|
||||
|
||||
sendCallAnswer(callId: string): void {
|
||||
this.sendFireAndForget('call_answer', { call_id: callId });
|
||||
}
|
||||
|
||||
sendCallReject(callId: string): void {
|
||||
this.sendFireAndForget('call_reject', { call_id: callId });
|
||||
}
|
||||
|
||||
sendCallBusy(callId: string): void {
|
||||
this.sendFireAndForget('call_busy', { call_id: callId });
|
||||
}
|
||||
|
||||
sendCallSDP(callId: string, sdpType: string, sdp: string): void {
|
||||
this.sendFireAndForget('call_sdp', {
|
||||
call_id: callId,
|
||||
sdp_type: sdpType,
|
||||
sdp,
|
||||
});
|
||||
}
|
||||
|
||||
sendCallICE(callId: string, candidate: string): void {
|
||||
this.sendFireAndForget('call_ice', {
|
||||
call_id: callId,
|
||||
candidate,
|
||||
});
|
||||
}
|
||||
|
||||
sendCallEnd(callId: string, reason?: string): void {
|
||||
this.sendFireAndForget('call_end', { call_id: callId, reason });
|
||||
}
|
||||
|
||||
sendCallMute(callId: string, muted: boolean): void {
|
||||
this.sendFireAndForget('call_mute', { call_id: callId, muted });
|
||||
}
|
||||
|
||||
private handleMessageSent(payload: any): void {
|
||||
// 找到对应的发送请求并resolve
|
||||
const pendingMsg = this.pendingMessages.find(p =>
|
||||
@@ -540,6 +842,15 @@ class WebSocketService {
|
||||
return api.post('/messages/delete_msg', { message_id: messageId });
|
||||
}
|
||||
|
||||
// 通过WebSocket发送(fire-and-forget,不等待确认)
|
||||
private sendFireAndForget(type: string, payload: any): void {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
const message = { type, payload };
|
||||
this.ws.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
// 通过WebSocket发送
|
||||
private sendViaWebSocket(type: string, payload: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
Reference in New Issue
Block a user