修复语音通话

main
杨博文 4 months ago
parent 0b7ad39d94
commit 79597ff6b0

@ -112,7 +112,9 @@ class P2PClientApp:
# 初始化语音聊天模块
self._voice_chat = VoiceChatModule(self.config)
self._voice_chat.set_user_info(user_info.user_id, user_info.username)
self._voice_chat.set_send_message_callback(self._send_message_async)
logger.info(f"Voice chat module initialized for user: {user_info.user_id}")
# 请求在线用户列表
await self._request_online_users()
@ -482,25 +484,25 @@ class P2PClientApp:
def _handle_voice_call_request(self, message: Message) -> None:
"""处理语音通话请求"""
if self._voice_chat:
self._voice_chat._handle_call_request(message)
asyncio.create_task(self._voice_chat._handle_call_request(message))
logger.info(f"Voice call request from {message.sender_id}")
def _handle_voice_call_accept(self, message: Message) -> None:
"""处理语音通话接受"""
if self._voice_chat:
self._voice_chat._handle_call_accept(message)
asyncio.create_task(self._voice_chat._handle_call_accept(message))
logger.info(f"Voice call accepted by {message.sender_id}")
def _handle_voice_call_reject(self, message: Message) -> None:
"""处理语音通话拒绝"""
if self._voice_chat:
self._voice_chat._handle_call_reject(message)
asyncio.create_task(self._voice_chat._handle_call_reject(message))
logger.info(f"Voice call rejected by {message.sender_id}")
def _handle_voice_call_end(self, message: Message) -> None:
"""处理语音通话结束"""
if self._voice_chat:
self._voice_chat._handle_call_end(message)
asyncio.create_task(self._voice_chat._handle_call_end(message))
logger.info(f"Voice call ended by {message.sender_id}")
def _handle_voice_data(self, message: Message) -> None:
@ -550,7 +552,7 @@ class P2PClientApp:
file_name: 文件名
file_path: 保存路径
"""
logger.info(f"File received from {sender_id}: {file_name} -> {file_path}")
logger.info(f"P2PClientApp: File received from {sender_id}: {file_name} -> {file_path}")
# 判断是否是图片
image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
@ -571,6 +573,7 @@ class P2PClientApp:
self._add_to_history(sender_id, chat_msg)
# 通知回调
logger.info(f"P2PClientApp: Notifying {len(self._file_received_callbacks)} callbacks")
for callback in self._file_received_callbacks:
try:
callback(sender_id, file_name, file_path)

@ -775,8 +775,10 @@ class FileTransferModule:
logger.info(f"File received and verified: {state.file_name} -> {state.save_path}")
# 通知回调
logger.info(f"Notifying {len(self._file_received_callbacks)} file received callbacks")
for callback in self._file_received_callbacks:
try:
logger.info(f"Calling callback with: {state.sender_id}, {state.file_name}, {state.save_path}")
callback(state.sender_id, state.file_name, state.save_path)
except Exception as e:
logger.error(f"File received callback error: {e}")

@ -890,10 +890,15 @@ class VoiceChatModule:
logger.error("No message callback set")
return False
if not self._user_id:
logger.error("User ID not set. Call set_user_info() first.")
return False
async with self._lock:
try:
# 初始化UDP
if not await self._init_udp_socket():
logger.error("Failed to initialize UDP socket")
return False
# 创建通话信息
@ -903,7 +908,8 @@ class VoiceChatModule:
is_outgoing=True
)
# 发送通话请求
# 发送通话请求 - 使用 JSON 格式而不是 str()
import json
call_data = {
"caller_id": self._user_id,
"caller_name": self._username,
@ -915,16 +921,21 @@ class VoiceChatModule:
sender_id=self._user_id,
receiver_id=peer_id,
timestamp=time.time(),
payload=str(call_data).encode('utf-8')
payload=json.dumps(call_data).encode('utf-8')
)
logger.info(f"Sending call request to {peer_id}, caller: {self._user_id}")
success = await self._send_message_callback(peer_id, message)
if success:
self._set_state(CallState.CALLING, f"Calling {peer_name or peer_id}")
logger.info(f"Call request sent to {peer_id}")
# 启动呼叫超时任务
asyncio.create_task(self._call_timeout(peer_id))
return True
else:
logger.error(f"Failed to send call request to {peer_id}")
self._close_udp_socket()
self._call_info = None
return False
@ -935,6 +946,21 @@ class VoiceChatModule:
self._call_info = None
return False
async def _call_timeout(self, peer_id: str, timeout: float = 30.0) -> None:
"""
呼叫超时处理
Args:
peer_id: 目标用户ID
timeout: 超时时间
"""
await asyncio.sleep(timeout)
# 如果仍在呼叫状态,则超时
if self._state == CallState.CALLING and self._call_info and self._call_info.peer_id == peer_id:
logger.info(f"Call to {peer_id} timed out")
self._cleanup_call()
async def accept_call(self, peer_id: str) -> bool:
"""
接听语音通话
@ -955,9 +981,14 @@ class VoiceChatModule:
logger.warning(f"No incoming call from {peer_id}")
return False
if not self._user_id:
logger.error("User ID not set. Call set_user_info() first.")
return False
async with self._lock:
try:
# 发送接听响应
# 发送接听响应 - 使用 JSON 格式
import json
accept_data = {
"callee_id": self._user_id,
"callee_name": self._username,
@ -969,16 +1000,19 @@ class VoiceChatModule:
sender_id=self._user_id,
receiver_id=peer_id,
timestamp=time.time(),
payload=str(accept_data).encode('utf-8')
payload=json.dumps(accept_data).encode('utf-8')
)
logger.info(f"Sending call accept to {peer_id}")
success = await self._send_message_callback(peer_id, message)
if success:
# 开始通话
await self._start_audio_session()
logger.info(f"Call accepted, audio session started with {peer_id}")
return True
else:
logger.error(f"Failed to send call accept to {peer_id}")
return False
except Exception as e:
@ -995,9 +1029,11 @@ class VoiceChatModule:
peer_id: 来电用户ID
"""
if self._state != CallState.RINGING:
logger.warning(f"Cannot reject call: current state is {self._state.value}")
return
if not self._call_info or self._call_info.peer_id != peer_id:
logger.warning(f"No incoming call from {peer_id} to reject")
return
try:
@ -1008,7 +1044,7 @@ class VoiceChatModule:
sender_id=self._user_id,
receiver_id=peer_id,
timestamp=time.time(),
payload=b""
payload=b"rejected"
)
# 同步发送(不等待结果)
@ -1017,7 +1053,7 @@ class VoiceChatModule:
logger.info(f"Call from {peer_id} rejected")
finally:
self._cleanup_call()
self._cleanup_call("Call rejected")
def end_call(self) -> None:
"""
@ -1026,6 +1062,7 @@ class VoiceChatModule:
释放音频资源并关闭连接 (需求 7.6)
"""
if self._state == CallState.IDLE:
logger.debug("No active call to end")
return
self._set_state(CallState.ENDING, "Ending call")
@ -1044,14 +1081,17 @@ class VoiceChatModule:
asyncio.create_task(self._send_message_callback(
self._call_info.peer_id, message
))
logger.info(f"Call end notification sent to {self._call_info.peer_id}")
logger.info("Call ended")
logger.info("Call ended by user")
finally:
self._cleanup_call()
self._cleanup_call("Call ended by user")
def _cleanup_call(self) -> None:
def _cleanup_call(self, reason: str = "Call ended") -> None:
"""清理通话资源"""
logger.info(f"Cleaning up call: {reason}")
# 停止音频
self._stop_audio_session()
@ -1065,7 +1105,7 @@ class VoiceChatModule:
self._sequence_number = 0
self._network_stats = NetworkStats()
self._set_state(CallState.IDLE, "Call ended")
self._set_state(CallState.IDLE, reason)
# ==================== 音频会话管理 ====================
@ -1132,9 +1172,8 @@ class VoiceChatModule:
音频发送循环
实时采集和传输音频数据 (需求 7.3)
使用TCP消息通道传输音频数据
"""
loop = asyncio.get_event_loop()
while self._state == CallState.CONNECTED:
try:
# 获取采集到的音频
@ -1148,11 +1187,11 @@ class VoiceChatModule:
else:
encoded_data = audio_data
# 发送音频数据
await self._send_audio_packet(encoded_data)
# 通过TCP消息通道发送音频数据
await self._send_voice_data(encoded_data)
# 短暂休眠避免CPU占用过高
await asyncio.sleep(0.001)
await asyncio.sleep(0.02) # 20ms与音频块时长匹配
except asyncio.CancelledError:
break
@ -1160,37 +1199,49 @@ class VoiceChatModule:
logger.error(f"Audio send error: {e}")
await asyncio.sleep(0.01)
async def _send_voice_data(self, audio_data: bytes) -> None:
"""通过TCP消息通道发送语音数据"""
if not self._call_info or not self._send_message_callback:
return
try:
self._sequence_number += 1
# 构建语音数据消息
voice_payload = {
"seq": self._sequence_number,
"ts": time.time(),
"data": audio_data.hex()
}
message = Message(
msg_type=MessageType.VOICE_DATA,
sender_id=self._user_id,
receiver_id=self._call_info.peer_id,
timestamp=time.time(),
payload=str(voice_payload).encode('utf-8')
)
await self._send_message_callback(self._call_info.peer_id, message)
self._network_stats.packets_sent += 1
except Exception as e:
logger.error(f"Failed to send voice data: {e}")
async def _audio_receive_loop(self) -> None:
"""
音频接收循环
接收和播放远端音频数据 (需求 7.3, 7.4)
语音数据通过_handle_voice_data方法接收
"""
loop = asyncio.get_event_loop()
# 语音数据通过TCP消息通道接收由_handle_voice_data处理
# 这个循环只是保持任务运行
while self._state == CallState.CONNECTED:
try:
if self._udp_socket:
try:
# 非阻塞接收
data, addr = await asyncio.wait_for(
loop.sock_recvfrom(self._udp_socket, 4096),
timeout=0.1
)
# 处理接收到的音频数据
await self._handle_audio_packet(data, addr)
except asyncio.TimeoutError:
continue
else:
await asyncio.sleep(0.01)
await asyncio.sleep(0.1)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Audio receive error: {e}")
await asyncio.sleep(0.01)
async def _send_audio_packet(self, audio_data: bytes) -> None:
"""
@ -1386,6 +1437,7 @@ class VoiceChatModule:
"""处理来电请求"""
if self._state != CallState.IDLE:
# 正忙,自动拒绝
logger.info(f"Rejecting call from {message.sender_id}: busy (state={self._state.value})")
if self._send_message_callback:
reject_msg = Message(
msg_type=MessageType.VOICE_CALL_REJECT,
@ -1398,14 +1450,23 @@ class VoiceChatModule:
return
try:
# 解析来电信息
call_data = eval(message.payload.decode('utf-8'))
# 解析来电信息 - 使用 JSON 格式
import json
try:
call_data = json.loads(message.payload.decode('utf-8'))
except json.JSONDecodeError:
# 兼容旧格式
call_data = eval(message.payload.decode('utf-8'))
caller_id = call_data.get("caller_id", message.sender_id)
caller_name = call_data.get("caller_name", "")
peer_udp_port = call_data.get("udp_port", 0)
logger.info(f"Incoming call from {caller_id} ({caller_name}), UDP port: {peer_udp_port}")
# 初始化UDP
if not await self._init_udp_socket():
logger.error("Failed to initialize UDP socket for incoming call")
return
# 创建通话信息
@ -1427,7 +1488,7 @@ class VoiceChatModule:
except Exception as e:
logger.error(f"Error in incoming call callback: {e}")
logger.info(f"Incoming call from {caller_id}")
logger.info(f"Incoming call from {caller_id}, state set to RINGING")
except Exception as e:
logger.error(f"Failed to handle call request: {e}")
@ -1436,24 +1497,28 @@ class VoiceChatModule:
async def _handle_call_accept(self, message: Message) -> None:
"""处理通话接受响应"""
if self._state != CallState.CALLING:
logger.warning(f"Received call accept but state is {self._state.value}")
return
if not self._call_info or self._call_info.peer_id != message.sender_id:
logger.warning(f"Received call accept from unexpected peer: {message.sender_id}")
return
try:
# 解析响应信息
accept_data = eval(message.payload.decode('utf-8'))
peer_udp_port = accept_data.get("udp_port", 0)
# 解析响应信息 - 使用 JSON 格式
import json
try:
accept_data = json.loads(message.payload.decode('utf-8'))
except json.JSONDecodeError:
# 兼容旧格式
accept_data = eval(message.payload.decode('utf-8'))
# 设置对端地址需要从消息中获取IP
# 这里需要实际的IP地址暂时使用占位符
# self._peer_address = (peer_ip, peer_udp_port)
logger.info(f"Call accepted by {message.sender_id}, accept_data: {accept_data}")
# 开始音频会话
# 开始音频会话使用TCP消息通道传输音频
await self._start_audio_session()
logger.info(f"Call accepted by {message.sender_id}")
logger.info(f"Call accepted by {message.sender_id}, audio session started")
except Exception as e:
logger.error(f"Failed to handle call accept: {e}")
@ -1462,17 +1527,56 @@ class VoiceChatModule:
async def _handle_call_reject(self, message: Message) -> None:
"""处理通话拒绝响应"""
if self._state != CallState.CALLING:
logger.warning(f"Received call reject but state is {self._state.value}")
return
reason = message.payload.decode('utf-8') if message.payload else "rejected"
logger.info(f"Call rejected by {message.sender_id}: {reason}")
self._cleanup_call()
self._cleanup_call(f"Call rejected: {reason}")
async def _handle_call_end(self, message: Message) -> None:
"""处理通话结束消息"""
if self._state == CallState.IDLE:
logger.debug("Received call end but already idle")
return
logger.info(f"Call ended by {message.sender_id}")
self._cleanup_call()
self._cleanup_call(f"Call ended by {message.sender_id}")
def _handle_voice_data(self, message: Message) -> None:
"""
处理接收到的语音数据
Args:
message: 语音数据消息
"""
if self._state != CallState.CONNECTED:
return
try:
# 解析语音数据
voice_payload = eval(message.payload.decode('utf-8'))
seq = voice_payload.get("seq", 0)
ts = voice_payload.get("ts", 0)
audio_hex = voice_payload.get("data", "")
if not audio_hex:
return
audio_data = bytes.fromhex(audio_hex)
# 解码音频
if self._decoder:
decoded_data = self._decoder.decode(audio_data)
else:
decoded_data = audio_data
# 放入播放缓冲区
if self._playback:
self._playback.push_audio(seq, decoded_data, ts)
self._network_stats.packets_received += 1
except Exception as e:
logger.error(f"Failed to handle voice data: {e}")

@ -176,6 +176,25 @@ class AsyncWorker(QThread):
if self._loop and self._running:
self.client.end_voice_call()
def accept_voice_call(self, peer_id: str) -> bool:
"""接听语音通话(线程安全)"""
if self._loop and self._running:
future = asyncio.run_coroutine_threadsafe(
self.client.accept_voice_call(peer_id),
self._loop
)
try:
return future.result(timeout=10.0)
except Exception as e:
logger.error(f"Accept voice call error: {e}")
return False
return False
def reject_voice_call(self, peer_id: str):
"""拒绝语音通话(线程安全)"""
if self._loop and self._running:
self.client.reject_voice_call(peer_id)
def refresh_users(self):
"""刷新用户列表(线程安全)"""
if self._loop and self._running:
@ -188,6 +207,9 @@ class AsyncWorker(QThread):
class P2PChatGUI(QObject):
"""P2P 聊天 GUI 应用程序"""
# 信号定义 - 用于跨线程通信
file_received_signal = pyqtSignal(str, str, str) # sender_id, file_name, file_path
def __init__(self, config: ClientConfig):
super().__init__()
self.config = config
@ -197,6 +219,9 @@ class P2PChatGUI(QObject):
self.worker: Optional[AsyncWorker] = None
self._current_user: Optional[UserInfo] = None
self._current_chat_peer: Optional[str] = None
# 连接信号到槽
self.file_received_signal.connect(self._handle_file_received_ui)
def run(self) -> int:
"""运行 GUI 应用程序"""
@ -395,12 +420,32 @@ class P2PChatGUI(QObject):
elif message.msg_type == MessageType.VOICE_CALL_REQUEST:
sender = message.sender_id
logger.info(f"Voice call request from {sender}")
self.main_window.show_notification(
"来电",
f"{sender} 正在呼叫你"
)
# 显示来电对话框
self._show_incoming_call_dialog(sender)
elif message.msg_type == MessageType.VOICE_CALL_ACCEPT:
sender = message.sender_id
logger.info(f"Voice call accepted by {sender}")
self.main_window._statusbar.showMessage(f"通话已接通 - {sender}", 5000)
self.main_window.show_notification("通话已接通", f"{sender} 的通话已建立")
elif message.msg_type == MessageType.VOICE_CALL_REJECT:
sender = message.sender_id
reason = message.payload.decode('utf-8') if message.payload else "对方拒绝"
logger.info(f"Voice call rejected by {sender}: {reason}")
self.main_window._statusbar.showMessage(f"呼叫被拒绝: {reason}", 5000)
self.main_window.show_notification("呼叫被拒绝", f"{sender} 拒绝了你的通话请求")
elif message.msg_type == MessageType.VOICE_CALL_END:
sender = message.sender_id
logger.info(f"Voice call ended by {sender}")
self.main_window._statusbar.showMessage("通话已结束", 3000)
self.main_window.show_notification("通话结束", f"{sender} 的通话已结束")
def _on_user_list_updated(self, users: List[UserInfo]):
"""用户列表更新回调"""
@ -438,6 +483,7 @@ class P2PChatGUI(QObject):
def _on_send_file(self):
"""发送文件"""
from PyQt6.QtWidgets import QFileDialog, QMessageBox
import os
if not self._current_chat_peer:
QMessageBox.warning(self.main_window, "提示", "请先选择一个联系人")
@ -456,12 +502,15 @@ class P2PChatGUI(QObject):
success = self.worker.send_file(self._current_chat_peer, file_path)
if success:
self.main_window._statusbar.showMessage("文件发送成功", 3000)
# 在聊天窗口显示发送的文件
self._add_sent_file_message(file_path, is_image=False)
else:
self.main_window._statusbar.showMessage("文件发送失败", 3000)
def _on_send_image(self):
"""发送图片"""
from PyQt6.QtWidgets import QFileDialog, QMessageBox
import os
if not self._current_chat_peer:
QMessageBox.warning(self.main_window, "提示", "请先选择一个联系人")
@ -480,9 +529,32 @@ class P2PChatGUI(QObject):
success = self.worker.send_image(self._current_chat_peer, file_path)
if success:
self.main_window._statusbar.showMessage("图片发送成功", 3000)
# 在聊天窗口显示发送的图片
self._add_sent_file_message(file_path, is_image=True)
else:
self.main_window._statusbar.showMessage("图片发送失败", 3000)
def _add_sent_file_message(self, file_path: str, is_image: bool):
"""在聊天窗口添加发送的文件/图片消息"""
from datetime import datetime
from shared.models import ChatMessage
content_type = MessageType.IMAGE if is_image else MessageType.FILE_REQUEST
msg = ChatMessage(
message_id=f"sent_file_{datetime.now().timestamp()}",
sender_id=self._current_user.user_id if self._current_user else "",
receiver_id=self._current_chat_peer,
content_type=content_type,
content=file_path,
timestamp=datetime.now(),
is_sent=True,
is_read=False
)
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.add_message(msg, is_self=True)
def _on_voice_call(self):
"""发起语音通话"""
from PyQt6.QtWidgets import QMessageBox
@ -491,27 +563,32 @@ class P2PChatGUI(QObject):
QMessageBox.warning(self.main_window, "提示", "请先选择一个联系人")
return
self.main_window._statusbar.showMessage(f"正在呼叫...")
self.main_window._statusbar.showMessage(f"正在呼叫 {self._current_chat_peer}...")
if self.worker:
success = self.worker.start_voice_call(self._current_chat_peer)
if success:
self.main_window._statusbar.showMessage("通话已建立", 3000)
self.main_window._statusbar.showMessage("正在等待对方接听...", 30000)
else:
self.main_window._statusbar.showMessage("呼叫失败", 3000)
QMessageBox.warning(self.main_window, "呼叫失败", "无法发起语音通话,请检查网络连接")
def _show_incoming_call_dialog(self, caller_id: str):
"""显示来电对话框"""
from PyQt6.QtWidgets import QMessageBox
logger.info(f"Showing incoming call dialog for {caller_id}")
reply = QMessageBox.question(
self.main_window,
"来电",
f"{caller_id} 正在呼叫你,是否接听?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.Yes # 默认选择接听
)
if reply == QMessageBox.StandardButton.Yes:
# 接听
logger.info(f"Accepting call from {caller_id}")
if self.worker and self.worker._loop and self.worker._running:
future = asyncio.run_coroutine_threadsafe(
self.client.accept_voice_call(caller_id),
@ -521,24 +598,33 @@ class P2PChatGUI(QObject):
success = future.result(timeout=10.0)
if success:
self.main_window._statusbar.showMessage("通话已接通", 3000)
logger.info(f"Call accepted successfully with {caller_id}")
else:
self.main_window._statusbar.showMessage("接听失败", 3000)
logger.error(f"Failed to accept call from {caller_id}")
except Exception as e:
logger.error(f"Accept call error: {e}")
self.main_window._statusbar.showMessage(f"接听失败: {e}", 3000)
else:
# 拒绝
logger.info(f"Rejecting call from {caller_id}")
if self.client:
self.client.reject_voice_call(caller_id)
self.main_window._statusbar.showMessage("已拒绝来电", 3000)
def _on_file_received(self, sender_id: str, file_name: str, file_path: str):
"""文件接收完成回调 - 使用信号在主线程中更新GUI"""
# 使用 QTimer 在主线程中执行 GUI 更新
QTimer.singleShot(0, lambda: self._handle_file_received_ui(sender_id, file_name, file_path))
logger.info(f"GUI: File received callback triggered - {sender_id}, {file_name}, {file_path}")
# 使用信号在主线程中执行 GUI 更新
self.file_received_signal.emit(sender_id, file_name, file_path)
def _handle_file_received_ui(self, sender_id: str, file_name: str, file_path: str):
"""在主线程中处理文件接收的UI更新"""
import os
from datetime import datetime
from shared.models import ChatMessage
logger.info(f"GUI: _handle_file_received_ui called - sender: {sender_id}, current_peer: {self._current_chat_peer}")
# 判断是否是图片
image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
@ -553,26 +639,36 @@ class P2PChatGUI(QObject):
self.main_window._statusbar.showMessage(f"收到文件: {file_name},已保存到 {file_path}", 5000)
# 如果当前正在和发送者聊天,在聊天窗口显示
if self._current_chat_peer == sender_id:
from datetime import datetime
from shared.models import ChatMessage
content_type = MessageType.IMAGE if is_image else MessageType.FILE_REQUEST
msg = ChatMessage(
message_id=f"file_{datetime.now().timestamp()}",
sender_id=sender_id,
receiver_id=self._current_user.user_id if self._current_user else "",
content_type=content_type,
content=file_path,
timestamp=datetime.now(),
is_sent=True,
is_read=False
)
# 创建消息对象
content_type = MessageType.IMAGE if is_image else MessageType.FILE_REQUEST
msg = ChatMessage(
message_id=f"file_{datetime.now().timestamp()}",
sender_id=sender_id,
receiver_id=self._current_user.user_id if self._current_user else "",
content_type=content_type,
content=file_path,
timestamp=datetime.now(),
is_sent=True,
is_read=False
)
# 如果当前没有打开聊天窗口或不是和发送者聊天,自动切换到发送者
if self._current_chat_peer != sender_id:
logger.info(f"GUI: Switching to chat with {sender_id}")
self._current_chat_peer = sender_id
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.add_message(msg, is_self=False)
# 获取发送者信息
peer_info = None
if hasattr(self, '_contact_list_widget') and self._contact_list_widget:
peer_info = self._contact_list_widget.get_contact(sender_id)
self._chat_widget.set_peer(sender_id, peer_info)
self.main_window.set_current_chat_peer(sender_id)
# 在聊天窗口显示
if hasattr(self, '_chat_widget') and self._chat_widget:
logger.info(f"GUI: Adding file message to chat widget")
self._chat_widget.add_message(msg, is_self=False)
def _on_contact_selected(self, user_id: str):
"""联系人选中回调 - 打开聊天窗口"""

@ -678,6 +678,17 @@ class RelayServer:
receiver_id = message.receiver_id
sender_id = message.sender_id
# 记录语音通话相关消息的日志
voice_types = {
MessageType.VOICE_CALL_REQUEST,
MessageType.VOICE_CALL_ACCEPT,
MessageType.VOICE_CALL_REJECT,
MessageType.VOICE_CALL_END,
MessageType.VOICE_DATA,
}
if message.msg_type in voice_types:
logger.info(f"Voice message: {message.msg_type.value} from {sender_id} to {receiver_id}")
# 检查目标用户是否在线
if self.is_user_online(receiver_id):
# 在线,直接转发

@ -173,12 +173,19 @@ class MessageHandler:
if not isinstance(message.msg_type, MessageType):
errors.append(f"Invalid message type: {message.msg_type}")
# 对于文件传输相关消息跳过校验和验证因为payload在序列化过程中会变化
# 对于文件传输和语音通话相关消息跳过校验和验证因为payload在序列化过程中会变化
skip_checksum_types = {
MessageType.FILE_REQUEST,
MessageType.FILE_CHUNK,
MessageType.FILE_COMPLETE,
MessageType.IMAGE,
MessageType.VOICE_CALL_REQUEST,
MessageType.VOICE_CALL_ACCEPT,
MessageType.VOICE_CALL_REJECT,
MessageType.VOICE_CALL_END,
MessageType.VOICE_DATA,
MessageType.AUDIO_STREAM,
MessageType.VIDEO_STREAM,
}
# 验证校验和(跳过文件传输消息)

Loading…
Cancel
Save