|
|
|
|
@ -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}")
|
|
|
|
|
|