|
|
|
@ -15,6 +15,7 @@ import 'package:get/get.dart'; |
|
|
|
import 'package:im_flutter_sdk/im_flutter_sdk.dart'; |
|
|
|
import 'package:permission_handler/permission_handler.dart'; |
|
|
|
|
|
|
|
import '../overlay_controller.dart'; |
|
|
|
import 'chat_controller.dart'; |
|
|
|
|
|
|
|
/// 通话类型 |
|
|
|
@ -66,7 +67,7 @@ class CallController extends GetxController { |
|
|
|
} |
|
|
|
|
|
|
|
CallController({NetworkService? networkService}) |
|
|
|
: _networkService = networkService ?? Get.find<NetworkService>(); |
|
|
|
: _networkService = networkService ?? Get.find<NetworkService>(); |
|
|
|
|
|
|
|
final NetworkService _networkService; |
|
|
|
|
|
|
|
@ -78,25 +79,29 @@ class CallController extends GetxController { |
|
|
|
|
|
|
|
// 当前正在进行的通话 |
|
|
|
final Rx<CallSession?> currentCall = Rx<CallSession?>(null); |
|
|
|
|
|
|
|
|
|
|
|
// 通话计时器(用于记录通话时长) |
|
|
|
Timer? _callTimer; |
|
|
|
int _callDurationSeconds = 0; |
|
|
|
final RxInt callDurationSeconds = RxInt(0); |
|
|
|
|
|
|
|
|
|
|
|
// 远端用户UID(用于显示远端视频) |
|
|
|
final Rxn<int> remoteUid = Rxn<int>(); |
|
|
|
|
|
|
|
|
|
|
|
// 麦克风静音状态 |
|
|
|
final RxBool isMicMuted = false.obs; |
|
|
|
|
|
|
|
|
|
|
|
// 扬声器开启状态 |
|
|
|
final RxBool isSpeakerOn = false.obs; |
|
|
|
|
|
|
|
|
|
|
|
// 音频播放器(用于播放来电铃声) |
|
|
|
final AudioPlayer _callAudioPlayer = AudioPlayer(); |
|
|
|
bool _isPlayingCallAudio = false; |
|
|
|
|
|
|
|
// 当前通话的频道ID和UID(用于发送RTM消息) |
|
|
|
String? _callChannelId; |
|
|
|
int? _callUid; |
|
|
|
|
|
|
|
@override |
|
|
|
void onInit() { |
|
|
|
super.onInit(); |
|
|
|
@ -105,14 +110,16 @@ class CallController extends GetxController { |
|
|
|
_callAudioPlayer.onPlayerComplete.listen((_) async { |
|
|
|
if (_isPlayingCallAudio) { |
|
|
|
// 如果还在播放状态,重新播放(循环播放) |
|
|
|
await _callAudioPlayer.play(AssetSource(Assets.audioCall.replaceFirst('assets/', ''))); |
|
|
|
await _callAudioPlayer.play( |
|
|
|
AssetSource(Assets.audioCall.replaceFirst('assets/', '')), |
|
|
|
); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 注册 RTM 消息监听器,用于接收通话相关的 RTM 消息 |
|
|
|
_registerRtmMessageListener(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 注册 RTM 消息监听器 |
|
|
|
void _registerRtmMessageListener() { |
|
|
|
RTMManager.instance.onMessageEvent = (MessageEvent event) { |
|
|
|
@ -120,7 +127,7 @@ class CallController extends GetxController { |
|
|
|
}; |
|
|
|
print('✅ [CallController] RTM 消息监听器已注册'); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 处理 RTM 消息 |
|
|
|
void _handleRtmMessage(MessageEvent event) { |
|
|
|
try { |
|
|
|
@ -133,10 +140,10 @@ class CallController extends GetxController { |
|
|
|
} 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?; |
|
|
|
@ -145,8 +152,14 @@ class CallController extends GetxController { |
|
|
|
final uid = messageData['uid']; |
|
|
|
if (uid != null) { |
|
|
|
remoteUid.value = uid is int ? uid : int.tryParse(uid.toString()); |
|
|
|
print('📞 [CallController] 收到 accept 消息,设置 remoteUid: ${remoteUid.value}'); |
|
|
|
print( |
|
|
|
'📞 [CallController] 收到 accept 消息,设置 remoteUid: ${remoteUid.value}', |
|
|
|
); |
|
|
|
} |
|
|
|
} else if (event == 'hangup') { |
|
|
|
// 收到挂断消息,执行退出逻辑 |
|
|
|
print('📞 [CallController] 收到 hangup 消息,执行退出逻辑'); |
|
|
|
_handleHangupMessage(); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
@ -213,22 +226,26 @@ class CallController extends GetxController { |
|
|
|
SmartDialog.showToast('已有通话正在进行中'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 清空之前的远端用户UID |
|
|
|
|
|
|
|
// 清空之前的远端用户UID和通话信息 |
|
|
|
remoteUid.value = null; |
|
|
|
_callChannelId = null; |
|
|
|
_callUid = null; |
|
|
|
|
|
|
|
print('📞 [CallController] 发起${callType == CallType.video ? "视频" : "语音"}通话,目标用户: $targetUserId'); |
|
|
|
print( |
|
|
|
'📞 [CallController] 发起${callType == CallType.video ? "视频" : "语音"}通话,目标用户: $targetUserId', |
|
|
|
); |
|
|
|
|
|
|
|
// 发起通话前,先创建一对一 RTC 频道 |
|
|
|
final type = callType == CallType.video ? 2 : 1; // 1为音频,2为视频 |
|
|
|
final channelData = await createOneOnOneRtcChannel(type: type); |
|
|
|
|
|
|
|
|
|
|
|
if (channelData == null) { |
|
|
|
print('❌ [CallController] 创建RTC频道失败,无法发起通话'); |
|
|
|
SmartDialog.showToast('创建通话频道失败'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
print('✅ [CallController] RTC频道创建成功: ${channelData.channelId}'); |
|
|
|
|
|
|
|
// 创建通话会话 |
|
|
|
@ -273,11 +290,11 @@ class CallController extends GetxController { |
|
|
|
rtcType: RTCType.call, |
|
|
|
); |
|
|
|
print('✅ [CallController] 已加入 RTC 频道: ${channelData.channelId}'); |
|
|
|
|
|
|
|
|
|
|
|
// 加入 RTC 频道后订阅 RTM 频道 |
|
|
|
await RTMManager.instance.subscribe(channelData.channelId); |
|
|
|
print('✅ [CallController] 已订阅 RTM 频道: ${channelData.channelId}'); |
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
@ -297,10 +314,14 @@ class CallController extends GetxController { |
|
|
|
final callTypeStr = callInfo['callType'] as String?; |
|
|
|
final callType = callTypeStr == 'video' ? CallType.video : CallType.voice; |
|
|
|
|
|
|
|
print('📞 [CallController] 接听${callType == CallType.video ? "视频" : "语音"}通话'); |
|
|
|
|
|
|
|
// 清空之前的远端用户UID |
|
|
|
print( |
|
|
|
'📞 [CallController] 接听${callType == CallType.video ? "视频" : "语音"}通话', |
|
|
|
); |
|
|
|
|
|
|
|
// 清空之前的远端用户UID和通话信息 |
|
|
|
remoteUid.value = null; |
|
|
|
_callChannelId = null; |
|
|
|
_callUid = null; |
|
|
|
|
|
|
|
// 创建通话会话 |
|
|
|
final session = CallSession( |
|
|
|
@ -334,6 +355,9 @@ class CallController extends GetxController { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 保存 channelId 为全局变量 |
|
|
|
_callChannelId = channelId; |
|
|
|
|
|
|
|
// 根据通话类型设置摄像头状态 |
|
|
|
if (callType == CallType.voice) { |
|
|
|
// 语音通话:禁用视频(关闭摄像头) |
|
|
|
@ -443,6 +467,10 @@ class CallController extends GetxController { |
|
|
|
currentCall.value = null; |
|
|
|
remoteUid.value = null; |
|
|
|
|
|
|
|
// 清理保存的 channelId 和 uid |
|
|
|
_callChannelId = null; |
|
|
|
_callUid = null; |
|
|
|
|
|
|
|
// TODO: 这里可以集成实际的通话SDK,结束通话 |
|
|
|
// 例如:await RTCManager.instance.endCall(); |
|
|
|
|
|
|
|
@ -519,10 +547,7 @@ class CallController extends GetxController { |
|
|
|
} |
|
|
|
|
|
|
|
// 创建新的消息体 |
|
|
|
final customBody = EMCustomMessageBody( |
|
|
|
event: 'call', |
|
|
|
params: callParams, |
|
|
|
); |
|
|
|
final customBody = EMCustomMessageBody(event: 'call', params: callParams); |
|
|
|
|
|
|
|
// 使用modifyMessage修改消息 |
|
|
|
final success = await IMManager.instance.modifyMessage( |
|
|
|
@ -532,18 +557,24 @@ class CallController extends GetxController { |
|
|
|
); |
|
|
|
|
|
|
|
if (success) { |
|
|
|
print('✅ [CallController] 消息修改成功: messageId=$messageId, callStatus=$callStatus'); |
|
|
|
|
|
|
|
print( |
|
|
|
'✅ [CallController] 消息修改成功: messageId=$messageId, callStatus=$callStatus', |
|
|
|
); |
|
|
|
|
|
|
|
// 如果提供了chatController,更新本地消息列表 |
|
|
|
if (chatController != null) { |
|
|
|
// 更新消息体中的参数 |
|
|
|
final index = chatController.messages.indexWhere((msg) => msg.msgId == messageId); |
|
|
|
final index = chatController.messages.indexWhere( |
|
|
|
(msg) => msg.msgId == messageId, |
|
|
|
); |
|
|
|
if (index != -1) { |
|
|
|
final updatedMessage = chatController.messages[index]; |
|
|
|
if (updatedMessage.body.type == MessageType.CUSTOM) { |
|
|
|
final customBody = updatedMessage.body as EMCustomMessageBody; |
|
|
|
// 创建新的参数Map并更新 |
|
|
|
final updatedParams = Map<String, String>.from(customBody.params ?? {}); |
|
|
|
final updatedParams = Map<String, String>.from( |
|
|
|
customBody.params ?? {}, |
|
|
|
); |
|
|
|
updatedParams['callType'] = callType; |
|
|
|
updatedParams['callStatus'] = callStatus; |
|
|
|
if (callDuration != null) { |
|
|
|
@ -572,8 +603,8 @@ class CallController extends GetxController { |
|
|
|
return { |
|
|
|
'callType': params['callType'] ?? 'voice', |
|
|
|
'callStatus': params['callStatus'] ?? 'missed', |
|
|
|
'callDuration': params['callDuration'] != null |
|
|
|
? int.tryParse(params['callDuration']!) |
|
|
|
'callDuration': params['callDuration'] != null |
|
|
|
? int.tryParse(params['callDuration']!) |
|
|
|
: null, |
|
|
|
'channelId': params['channelId'], |
|
|
|
}; |
|
|
|
@ -594,10 +625,12 @@ class CallController extends GetxController { |
|
|
|
if (_isPlayingCallAudio) { |
|
|
|
return; // 已经在播放中 |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_isPlayingCallAudio = true; |
|
|
|
print('🔊 [CallController] 开始播放来电铃声'); |
|
|
|
await _callAudioPlayer.play(AssetSource(Assets.audioCall.replaceFirst('assets/', ''))); |
|
|
|
await _callAudioPlayer.play( |
|
|
|
AssetSource(Assets.audioCall.replaceFirst('assets/', '')), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
/// 停止播放来电铃声 |
|
|
|
@ -606,7 +639,7 @@ class CallController extends GetxController { |
|
|
|
if (!_isPlayingCallAudio) { |
|
|
|
return; // 没有在播放 |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_isPlayingCallAudio = false; |
|
|
|
print('🔇 [CallController] 停止播放来电铃声'); |
|
|
|
await _callAudioPlayer.stop(); |
|
|
|
@ -617,13 +650,18 @@ class CallController extends GetxController { |
|
|
|
final base = response.data; |
|
|
|
if (base.isSuccess && base.data != null) { |
|
|
|
rtcChannel.value = base.data; |
|
|
|
|
|
|
|
|
|
|
|
// 保存 UID 为全局变量 |
|
|
|
_callUid = base.data!.uid; |
|
|
|
|
|
|
|
// 订阅 RTM 频道 |
|
|
|
await RTMManager.instance.subscribe(channelName); |
|
|
|
|
|
|
|
|
|
|
|
// 获取当前通话信息 |
|
|
|
final callSession = currentCall.value; |
|
|
|
final callType = callSession?.callType == CallType.video ? 'video' : 'voice'; |
|
|
|
final callType = callSession?.callType == CallType.video |
|
|
|
? 'video' |
|
|
|
: 'voice'; |
|
|
|
|
|
|
|
// 发布 RTM 消息,包含 UID 和通话相关字段 |
|
|
|
await RTMManager.instance.publishChannelMessage( |
|
|
|
@ -635,7 +673,7 @@ class CallController extends GetxController { |
|
|
|
'event': 'accept', |
|
|
|
}), |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
await _joinRtcChannel( |
|
|
|
base.data!.token, |
|
|
|
channelName, |
|
|
|
@ -660,7 +698,7 @@ class CallController extends GetxController { |
|
|
|
role: roleType, |
|
|
|
rtcType: RTCType.call, |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
final data = { |
|
|
|
'channelId': channelName, |
|
|
|
'seatNumber': 1, |
|
|
|
@ -683,7 +721,7 @@ class CallController extends GetxController { |
|
|
|
} |
|
|
|
|
|
|
|
final permanentlyDenied = statuses.values.any( |
|
|
|
(status) => status.isPermanentlyDenied, |
|
|
|
(status) => status.isPermanentlyDenied, |
|
|
|
); |
|
|
|
if (permanentlyDenied) { |
|
|
|
SmartDialog.showToast('请在系统设置中开启摄像头和麦克风权限'); |
|
|
|
@ -710,15 +748,72 @@ class CallController extends GetxController { |
|
|
|
|
|
|
|
/// 挂断通话 |
|
|
|
Future<void> hangUpCall() async { |
|
|
|
// 发送 RTM 挂断消息 |
|
|
|
if (_callChannelId != null && |
|
|
|
_callChannelId!.isNotEmpty && |
|
|
|
_callUid != null) { |
|
|
|
final callSession = currentCall.value; |
|
|
|
final callType = callSession?.callType == CallType.video |
|
|
|
? 'video' |
|
|
|
: 'voice'; |
|
|
|
|
|
|
|
await RTMManager.instance.publishChannelMessage( |
|
|
|
channelName: _callChannelId!, |
|
|
|
message: json.encode({ |
|
|
|
'type': 'call_message', |
|
|
|
'uid': _callUid!, |
|
|
|
'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'); |
|
|
|
} |
|
|
|
|
|
|
|
// 结束通话(传递通话时长) |
|
|
|
await endCall(callDuration: callDurationSeconds.value); |
|
|
|
|
|
|
|
|
|
|
|
print('✅ [CallController] 通话已挂断'); |
|
|
|
} |
|
|
|
|
|
|
|
/// 处理挂断消息(对方挂断时调用) |
|
|
|
Future<void> _handleHangupMessage() async { |
|
|
|
// 离开RTC频道 |
|
|
|
await RTCManager.instance.leaveChannel(); |
|
|
|
|
|
|
|
// 取消订阅 RTM 频道 |
|
|
|
if (_callChannelId != null && _callChannelId!.isNotEmpty) { |
|
|
|
await RTMManager.instance.unsubscribe(_callChannelId!); |
|
|
|
print('✅ [CallController] 已取消订阅 RTM 频道: $_callChannelId'); |
|
|
|
} |
|
|
|
|
|
|
|
// 结束通话 |
|
|
|
await endCall(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'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@override |
|
|
|
void onClose() { |
|
|
|
stopCallAudio(); |
|
|
|
@ -728,6 +823,8 @@ class CallController extends GetxController { |
|
|
|
remoteUid.value = null; |
|
|
|
isMicMuted.value = false; |
|
|
|
isSpeakerOn.value = false; |
|
|
|
_callChannelId = null; |
|
|
|
_callUid = null; |
|
|
|
_callAudioPlayer.dispose(); |
|
|
|
super.onClose(); |
|
|
|
} |
|
|
|
|