|
|
|
@ -1,19 +1,16 @@ |
|
|
|
import 'dart:async'; |
|
|
|
import 'dart:convert'; |
|
|
|
import 'dart:typed_data'; |
|
|
|
import 'package:agora_rtc_engine/agora_rtc_engine.dart'; |
|
|
|
import 'package:agora_rtm/agora_rtm.dart'; |
|
|
|
import 'package:audioplayers/audioplayers.dart'; |
|
|
|
import 'package:dating_touchme_app/generated/assets.dart'; |
|
|
|
import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart'; |
|
|
|
import 'package:dating_touchme_app/network/network_service.dart'; |
|
|
|
import 'package:dating_touchme_app/rtc/rtc_manager.dart'; |
|
|
|
import 'package:dating_touchme_app/rtc/rtm_manager.dart'; |
|
|
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; |
|
|
|
import 'package:get/get.dart'; |
|
|
|
import 'package:im_flutter_sdk/im_flutter_sdk.dart'; |
|
|
|
import 'package:permission_handler/permission_handler.dart'; |
|
|
|
|
|
|
|
import '../discover/room_controller.dart'; |
|
|
|
import '../overlay_controller.dart'; |
|
|
|
import 'chat_controller.dart'; |
|
|
|
|
|
|
|
@ -118,88 +115,7 @@ class CallController extends GetxController { |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 注册 RTM 消息监听器,用于接收通话相关的 RTM 消息 |
|
|
|
_registerRtmMessageListener(); |
|
|
|
} |
|
|
|
|
|
|
|
/// 注册 RTM 消息监听器 |
|
|
|
void _registerRtmMessageListener() { |
|
|
|
RTMManager.instance.onMessageEvent = (MessageEvent event) { |
|
|
|
_handleRtmMessage(event); |
|
|
|
}; |
|
|
|
print('✅ [CallController] RTM 消息监听器已注册'); |
|
|
|
} |
|
|
|
|
|
|
|
/// 处理 RTM 消息 |
|
|
|
Future<void> _handleRtmMessage(MessageEvent event) async { |
|
|
|
try { |
|
|
|
// 解析消息内容 |
|
|
|
String messageText; |
|
|
|
if (event.message is String) { |
|
|
|
messageText = event.message as String; |
|
|
|
} else if (event.message is Uint8List) { |
|
|
|
messageText = utf8.decode(event.message as Uint8List); |
|
|
|
} else { |
|
|
|
messageText = event.message.toString(); |
|
|
|
} |
|
|
|
|
|
|
|
final messageData = json.decode(messageText) as Map<String, dynamic>; |
|
|
|
print('📥 [CallController] 收到 RTM 消息: $messageData'); |
|
|
|
|
|
|
|
// 处理通话消息 |
|
|
|
if (messageData['type'] == 'call_message') { |
|
|
|
final event = messageData['event'] as String?; |
|
|
|
if (event == 'accept') { |
|
|
|
// 发起方收到 accept 消息,设置远端用户 UID 并启动计时器 |
|
|
|
final uid = messageData['uid']; |
|
|
|
if (uid != null) { |
|
|
|
remoteUid.value = uid is int ? uid : int.tryParse(uid.toString()); |
|
|
|
print( |
|
|
|
'📞 [CallController] 收到 accept 消息,设置 remoteUid: ${remoteUid.value}', |
|
|
|
); |
|
|
|
} |
|
|
|
// 取消超时计时器(对方已接听) |
|
|
|
_stopCallTimeoutTimer(); |
|
|
|
// 发起方收到 accept 消息后,启动通话计时器 |
|
|
|
_startCallTimer(); |
|
|
|
print('📞 [CallController] 收到 accept 消息,已启动通话计时器'); |
|
|
|
} else if (event == 'hangup') { |
|
|
|
// 收到挂断消息,执行退出逻辑 |
|
|
|
print('📞 [CallController] 收到 hangup 消息,执行退出逻辑'); |
|
|
|
final channelId = messageData['channelId'] as String?; |
|
|
|
if (channelId != null && channelId.isNotEmpty) { |
|
|
|
RTMManager.instance.unsubscribe(channelId); |
|
|
|
} |
|
|
|
SmartDialog.dismiss(tag: 'video_call_invite_dialog'); |
|
|
|
_handleHangupMessage(); |
|
|
|
} else if (event == 'reject') { |
|
|
|
// 发起方收到 reject 消息,执行退出逻辑 |
|
|
|
print('📞 [CallController] 收到 reject 消息,执行退出逻辑'); |
|
|
|
// 取消超时计时器(对方已拒绝) |
|
|
|
_stopCallTimeoutTimer(); |
|
|
|
final channelId = messageData['channelId'] as String?; |
|
|
|
if (channelId != null && channelId.isNotEmpty) { |
|
|
|
RTMManager.instance.unsubscribe(channelId); |
|
|
|
print('✅ [CallController] 已取消订阅 RTM 频道: $channelId'); |
|
|
|
} |
|
|
|
// 关闭通话小窗口 |
|
|
|
if (Get.isRegistered<OverlayController>()) { |
|
|
|
final overlayController = Get.find<OverlayController>(); |
|
|
|
overlayController.hideVideoCall(); |
|
|
|
print('✅ [CallController] 已关闭通话小窗口'); |
|
|
|
} |
|
|
|
// 退出 VideoCallPage(如果当前在 VideoCallPage) |
|
|
|
if (Get.currentRoute.contains('VideoCallPage')) { |
|
|
|
Get.back(); |
|
|
|
print('✅ [CallController] 已退出 VideoCallPage'); |
|
|
|
} |
|
|
|
// 结束通话 |
|
|
|
await endCall(callDuration: callDurationSeconds.value); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
print('❌ [CallController] 处理 RTM 消息失败: $e'); |
|
|
|
} |
|
|
|
// RTM 消息监听器已移除,通话相关消息改为通过onMessageContentChanged处理 |
|
|
|
} |
|
|
|
|
|
|
|
/// 创建一对一RTC频道 |
|
|
|
@ -326,10 +242,6 @@ class CallController extends GetxController { |
|
|
|
); |
|
|
|
print('✅ [CallController] 已加入 RTC 频道: ${channelData.channelId}'); |
|
|
|
|
|
|
|
// 加入 RTC 频道后订阅 RTM 频道 |
|
|
|
await RTMManager.instance.subscribe(channelData.channelId); |
|
|
|
print('✅ [CallController] 已订阅 RTM 频道: ${channelData.channelId}'); |
|
|
|
|
|
|
|
// 启动30秒超时计时器(如果30秒内对方未接听,自动取消通话) |
|
|
|
_startCallTimeoutTimer(); |
|
|
|
|
|
|
|
@ -408,15 +320,33 @@ class CallController extends GetxController { |
|
|
|
print('📞 [CallController] 视频通话,已打开摄像头'); |
|
|
|
} |
|
|
|
|
|
|
|
// 加入 RTC 频道,接听通话 |
|
|
|
await joinChannel(channelId); |
|
|
|
print('✅ [CallController] 已加入 RTC 频道: $channelId'); |
|
|
|
// 获取 RTC token 并加入频道 |
|
|
|
final response = await _networkService.rtcApi.getSwRtcToken(channelId); |
|
|
|
final base = response.data; |
|
|
|
if (base.isSuccess && base.data != null) { |
|
|
|
rtcChannel.value = base.data; |
|
|
|
|
|
|
|
// 保存 UID 为全局变量 |
|
|
|
_callUid = base.data!.uid; |
|
|
|
|
|
|
|
await _joinRtcChannel( |
|
|
|
base.data!.token, |
|
|
|
channelId, |
|
|
|
base.data!.uid, |
|
|
|
ClientRoleType.clientRoleBroadcaster, |
|
|
|
); |
|
|
|
|
|
|
|
// 加入 RTC 频道后,从消息中获取发起方的 uid,设置为远端用户 UID |
|
|
|
final initiatorUid = callInfo['uid'] as int?; |
|
|
|
if (initiatorUid != null) { |
|
|
|
remoteUid.value = initiatorUid; |
|
|
|
print('📞 [CallController] 从消息中获取到发起方 UID: $initiatorUid,已设置 remoteUid'); |
|
|
|
// 从消息中获取发起方的 uid,设置为远端用户 UID |
|
|
|
final initiatorUid = callInfo['uid'] as int?; |
|
|
|
if (initiatorUid != null) { |
|
|
|
remoteUid.value = initiatorUid; |
|
|
|
print( |
|
|
|
'📞 [CallController] 从消息中获取到发起方 UID: $initiatorUid,已设置 remoteUid', |
|
|
|
); |
|
|
|
} |
|
|
|
} else { |
|
|
|
SmartDialog.showToast('获取RTC token失败'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
@ -454,22 +384,7 @@ class CallController extends GetxController { |
|
|
|
return false; |
|
|
|
} |
|
|
|
print('✅ [CallController] 已调用拒绝一对一RTC频道接口,channelId: $channelId'); |
|
|
|
await RTMManager.instance.unsubscribe(channelId); |
|
|
|
final callInfo = _parseCallInfo(message); |
|
|
|
final callTypeStr = callInfo?['callType'] as String?; |
|
|
|
final callType = callTypeStr == 'video' ? 'video' : 'voice'; |
|
|
|
|
|
|
|
// 发送拒绝 RTM 消息 |
|
|
|
await RTMManager.instance.publishChannelMessage( |
|
|
|
channelName: channelId, |
|
|
|
message: json.encode({ |
|
|
|
'type': 'call_message', |
|
|
|
'uid': 0, |
|
|
|
'channelId': channelId, |
|
|
|
'callType': callType, |
|
|
|
'event': 'reject', |
|
|
|
}), |
|
|
|
); |
|
|
|
// 服务端会自动修改消息callStatus为'rejected',客户端通过onMessageContentChanged收到通知 |
|
|
|
} |
|
|
|
|
|
|
|
// 清理通话会话 |
|
|
|
@ -631,32 +546,13 @@ class CallController extends GetxController { |
|
|
|
// 保存 UID 为全局变量 |
|
|
|
_callUid = base.data!.uid; |
|
|
|
|
|
|
|
// 订阅 RTM 频道 |
|
|
|
await RTMManager.instance.subscribe(channelName); |
|
|
|
|
|
|
|
// 获取当前通话信息 |
|
|
|
final callSession = currentCall.value; |
|
|
|
final callType = callSession?.callType == CallType.video |
|
|
|
? 'video' |
|
|
|
: 'voice'; |
|
|
|
|
|
|
|
// 发布 RTM 消息,包含 UID 和通话相关字段 |
|
|
|
await RTMManager.instance.publishChannelMessage( |
|
|
|
channelName: channelName, |
|
|
|
message: json.encode({ |
|
|
|
'type': 'call_message', |
|
|
|
'uid': base.data!.uid, |
|
|
|
'callType': callType, |
|
|
|
'event': 'accept', |
|
|
|
}), |
|
|
|
); |
|
|
|
|
|
|
|
await _joinRtcChannel( |
|
|
|
base.data!.token, |
|
|
|
channelName, |
|
|
|
base.data!.uid, |
|
|
|
ClientRoleType.clientRoleBroadcaster, |
|
|
|
); |
|
|
|
// 服务端会自动修改消息callStatus为'calling',客户端通过onMessageContentChanged收到通知 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -675,19 +571,7 @@ class CallController extends GetxController { |
|
|
|
role: roleType, |
|
|
|
rtcType: RTCType.call, |
|
|
|
); |
|
|
|
|
|
|
|
final data = { |
|
|
|
'channelId': channelName, |
|
|
|
'seatNumber': 1, |
|
|
|
'isMicrophoneOn': true, |
|
|
|
'isVideoOn': true, |
|
|
|
}; |
|
|
|
final response = await _networkService.rtcApi.connectRtcChannel(data); |
|
|
|
if (!response.data.isSuccess) { |
|
|
|
SmartDialog.showToast(response.data.message); |
|
|
|
return; |
|
|
|
} |
|
|
|
SmartDialog.showToast('加入通话成功'); |
|
|
|
print('✅ [CallController] 已加入 RTC 频道: $channelName'); |
|
|
|
} |
|
|
|
|
|
|
|
Future<bool> _ensureRtcPermissions() async { |
|
|
|
@ -759,36 +643,10 @@ class CallController extends GetxController { |
|
|
|
print('✅ [CallController] 已调用终止一对一RTC频道接口,channelId: $_callChannelId'); |
|
|
|
} |
|
|
|
|
|
|
|
if (_callChannelId != null && |
|
|
|
_callChannelId!.isNotEmpty && |
|
|
|
_callUid != null) { |
|
|
|
final callType = callSession?.callType == CallType.video |
|
|
|
? 'video' |
|
|
|
: 'voice'; |
|
|
|
|
|
|
|
await RTMManager.instance.publishChannelMessage( |
|
|
|
channelName: _callChannelId!, |
|
|
|
message: json.encode({ |
|
|
|
'type': 'call_message', |
|
|
|
'uid': _callUid!, |
|
|
|
'channelId': _callChannelId, |
|
|
|
'callType': callType, |
|
|
|
'event': 'hangup', |
|
|
|
}), |
|
|
|
); |
|
|
|
print( |
|
|
|
'✅ [CallController] 已发送 RTM 挂断消息,channelId: $_callChannelId, uid: $_callUid', |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 离开RTC频道 |
|
|
|
await RTCManager.instance.leaveChannel(); |
|
|
|
|
|
|
|
// 取消订阅 RTM 频道 |
|
|
|
if (_callChannelId != null && _callChannelId!.isNotEmpty) { |
|
|
|
await RTMManager.instance.unsubscribe(_callChannelId!); |
|
|
|
print('✅ [CallController] 已取消订阅 RTM 频道: $_callChannelId'); |
|
|
|
} |
|
|
|
// 服务端会自动修改消息callStatus为'cancelled'或'terminated',客户端通过onMessageContentChanged收到通知 |
|
|
|
|
|
|
|
// 结束通话(传递通话时长) |
|
|
|
await endCall(callDuration: callDurationSeconds.value); |
|
|
|
@ -812,41 +670,127 @@ class CallController extends GetxController { |
|
|
|
print('✅ [CallController] 通话已挂断'); |
|
|
|
} |
|
|
|
|
|
|
|
/// 处理挂断消息(对方挂断时调用) |
|
|
|
Future<void> _handleHangupMessage() async { |
|
|
|
// 关闭视频通话邀请弹框(如果正在显示) |
|
|
|
SmartDialog.dismiss(); |
|
|
|
print('✅ [CallController] 已关闭视频通话邀请弹框'); |
|
|
|
/// 处理通话消息callStatus变化(通过onMessageContentChanged调用) |
|
|
|
Future<void> handleCallStatusChange({ |
|
|
|
required EMMessage message, |
|
|
|
required String callStatus, |
|
|
|
String? channelId, |
|
|
|
int? uid, |
|
|
|
int? callDuration, |
|
|
|
}) async { |
|
|
|
print( |
|
|
|
'📞 [CallController] 处理callStatus变化: callStatus=$callStatus, channelId=$channelId', |
|
|
|
); |
|
|
|
|
|
|
|
// 取消超时计时器 |
|
|
|
_stopCallTimeoutTimer(); |
|
|
|
final callSession = currentCall.value; |
|
|
|
if (callSession == null) { |
|
|
|
print('⚠️ [CallController] 当前没有进行中的通话,忽略callStatus变化'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 停止播放来电铃声 |
|
|
|
stopCallAudio(); |
|
|
|
// 如果提供了channelId,验证是否匹配当前通话 |
|
|
|
if (channelId != null && |
|
|
|
channelId.isNotEmpty && |
|
|
|
_callChannelId != channelId) { |
|
|
|
print( |
|
|
|
'⚠️ [CallController] channelId不匹配,忽略callStatus变化: 当前=$_callChannelId, 消息=$channelId', |
|
|
|
); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 离开RTC频道 |
|
|
|
await RTCManager.instance.leaveChannel(); |
|
|
|
try { |
|
|
|
if (callStatus == 'calling') { |
|
|
|
// 通话接通 |
|
|
|
print('📞 [CallController] 通话已接通,callStatus=$callStatus'); |
|
|
|
|
|
|
|
// 如果是发起方,设置远端用户UID并启动计时器 |
|
|
|
if (callSession.isInitiator) { |
|
|
|
// 停止播放来电铃声(对方已接听) |
|
|
|
stopCallAudio(); |
|
|
|
// 取消超时计时器(对方已接听) |
|
|
|
_stopCallTimeoutTimer(); |
|
|
|
// 启动通话计时器 |
|
|
|
_startCallTimer(); |
|
|
|
print('📞 [CallController] 已启动通话计时器'); |
|
|
|
// remoteUid 会由 RTCManager 的 onUserJoined 事件自动设置 |
|
|
|
// 如果此时还没有设置,尝试从 RTCManager 的 remoteUsersNotifier 中获取 |
|
|
|
if (remoteUid.value == null) { |
|
|
|
final rtcManager = RTCManager.instance; |
|
|
|
final remoteUsers = rtcManager.remoteUsersNotifier.value; |
|
|
|
if (remoteUsers.isNotEmpty) { |
|
|
|
remoteUid.value = remoteUsers.first; |
|
|
|
print( |
|
|
|
'📞 [CallController] 从 RTCManager.remoteUsersNotifier 获取到 remoteUid: ${remoteUsers.first}', |
|
|
|
); |
|
|
|
} else if (uid != null) { |
|
|
|
// 如果 RTCManager 还没有远端用户,使用消息中的 uid 作为备用 |
|
|
|
remoteUid.value = uid; |
|
|
|
print('📞 [CallController] 使用消息中的 uid 设置 remoteUid: $uid'); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} else if (callStatus == 'rejected') { |
|
|
|
// 通话被拒绝 |
|
|
|
print('📞 [CallController] 通话被拒绝,callStatus=$callStatus'); |
|
|
|
|
|
|
|
// 取消订阅 RTM 频道 |
|
|
|
if (_callChannelId != null && _callChannelId!.isNotEmpty) { |
|
|
|
await RTMManager.instance.unsubscribe(_callChannelId!); |
|
|
|
print('✅ [CallController] 已取消订阅 RTM 频道: $_callChannelId'); |
|
|
|
} |
|
|
|
// 如果是发起方,执行退出逻辑 |
|
|
|
if (callSession.isInitiator) { |
|
|
|
// 取消超时计时器(对方已拒绝) |
|
|
|
_stopCallTimeoutTimer(); |
|
|
|
// 关闭通话小窗口 |
|
|
|
if (Get.isRegistered<OverlayController>()) { |
|
|
|
final overlayController = Get.find<OverlayController>(); |
|
|
|
overlayController.hideVideoCall(); |
|
|
|
print('✅ [CallController] 已关闭通话小窗口'); |
|
|
|
} |
|
|
|
// 退出 VideoCallPage(如果当前在 VideoCallPage) |
|
|
|
if (Get.currentRoute.contains('VideoCallPage')) { |
|
|
|
Get.back(); |
|
|
|
print('✅ [CallController] 已退出 VideoCallPage'); |
|
|
|
} |
|
|
|
// 结束通话 |
|
|
|
await endCall(callDuration: callDurationSeconds.value); |
|
|
|
} |
|
|
|
} else if (callStatus == 'cancelled' || callStatus == 'terminated') { |
|
|
|
// 通话被取消或终止 |
|
|
|
print( |
|
|
|
'📞 [CallController] 通话被取消/终止,callStatus=$callStatus, isInitiator=${callSession.isInitiator}', |
|
|
|
); |
|
|
|
|
|
|
|
// 结束通话 |
|
|
|
await endCall(callDuration: callDurationSeconds.value); |
|
|
|
// 关闭视频通话邀请弹框(如果正在显示) |
|
|
|
SmartDialog.dismiss(); |
|
|
|
print('✅ [CallController] 已关闭视频通话邀请弹框'); |
|
|
|
|
|
|
|
// 关闭通话小窗口 |
|
|
|
if (Get.isRegistered<OverlayController>()) { |
|
|
|
final overlayController = Get.find<OverlayController>(); |
|
|
|
overlayController.hideVideoCall(); |
|
|
|
print('✅ [CallController] 已关闭通话小窗口'); |
|
|
|
} |
|
|
|
// 取消超时计时器 |
|
|
|
_stopCallTimeoutTimer(); |
|
|
|
|
|
|
|
// 退出 VideoCallPage(如果当前在 VideoCallPage) |
|
|
|
if (Get.currentRoute.contains('VideoCallPage')) { |
|
|
|
Get.back(); |
|
|
|
print('✅ [CallController] 已退出 VideoCallPage'); |
|
|
|
// 停止播放来电铃声 |
|
|
|
stopCallAudio(); |
|
|
|
|
|
|
|
// 停止通话计时器 |
|
|
|
_stopCallTimer(); |
|
|
|
|
|
|
|
// 离开RTC频道 |
|
|
|
await RTCManager.instance.leaveChannel(); |
|
|
|
|
|
|
|
// 结束通话 |
|
|
|
await endCall(callDuration: callDuration ?? callDurationSeconds.value); |
|
|
|
|
|
|
|
// 关闭通话小窗口 |
|
|
|
if (Get.isRegistered<OverlayController>()) { |
|
|
|
final overlayController = Get.find<OverlayController>(); |
|
|
|
overlayController.hideVideoCall(); |
|
|
|
print('✅ [CallController] 已关闭通话小窗口'); |
|
|
|
} |
|
|
|
|
|
|
|
// 退出 VideoCallPage(如果当前在 VideoCallPage) |
|
|
|
if (Get.currentRoute.contains('VideoCallPage')) { |
|
|
|
Get.back(); |
|
|
|
print('✅ [CallController] 已退出 VideoCallPage'); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
print('❌ [CallController] 处理callStatus变化失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|