Browse Source
feat(call): 将通话管理功能从CallManager迁移到CallController
feat(call): 将通话管理功能从CallManager迁移到CallController
- 实现语音和视频通话的发起、接听、拒绝、取消和结束功能 - 添加通话状态管理包括等待接通、通话中、未接听、已取消和已拒绝 - 集成音频播放器实现来电铃声的循环播放功能 - 实现通话计时器功能记录通话时长 - 完成通话消息的发送和状态更新机制 - 迁移通话相关枚举和数据模型到CallController - 更新聊天页面和IM管理器使用新的CallController实例 - 移除独立的CallManager文件并整合到CallController中master
7 changed files with 589 additions and 592 deletions
Unified View
Diff Options
-
540lib/controller/message/call_controller.dart
-
16lib/im/im_manager.dart
-
37lib/pages/message/chat_page.dart
-
26lib/pages/message/video_call_page.dart
-
542lib/rtc/call_manager.dart
-
10lib/widget/live/live_room_notice_chat_panel.dart
-
10lib/widget/message/video_call_overlay_widget.dart
@ -1,542 +0,0 @@ |
|||||
import 'dart:async'; |
|
||||
import 'package:audioplayers/audioplayers.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 '../generated/assets.dart'; |
|
||||
import '../im/im_manager.dart'; |
|
||||
import '../controller/message/chat_controller.dart'; |
|
||||
import '../controller/message/call_controller.dart'; |
|
||||
|
|
||||
/// 通话类型 |
|
||||
enum CallType { |
|
||||
voice, // 语音通话 |
|
||||
video, // 视频通话 |
|
||||
} |
|
||||
|
|
||||
/// 通话状态 |
|
||||
enum CallStatus { |
|
||||
waitCalling, // 等待接通 |
|
||||
calling, // 通话中 |
|
||||
missed, // 未接听 |
|
||||
cancelled, // 已取消 |
|
||||
rejected, // 已拒绝 |
|
||||
} |
|
||||
|
|
||||
/// 通话管理器,单例模式,统一管理通话逻辑 |
|
||||
class CallManager extends GetxController { |
|
||||
static CallManager? _instance; |
|
||||
static CallManager get instance { |
|
||||
_instance ??= Get.put(CallManager()); |
|
||||
return _instance!; |
|
||||
} |
|
||||
|
|
||||
// 当前正在进行的通话 |
|
||||
final Rx<CallSession?> currentCall = Rx<CallSession?>(null); |
|
||||
|
|
||||
// 通话计时器(用于记录通话时长) |
|
||||
Timer? _callTimer; |
|
||||
int _callDurationSeconds = 0; |
|
||||
final RxInt callDurationSeconds = RxInt(0); |
|
||||
|
|
||||
// 音频播放器(用于播放来电铃声) |
|
||||
final AudioPlayer _callAudioPlayer = AudioPlayer(); |
|
||||
bool _isPlayingCallAudio = false; |
|
||||
|
|
||||
CallManager() { |
|
||||
print('📞 [CallManager] 通话管理器已初始化'); |
|
||||
// 监听音频播放完成事件,实现循环播放 |
|
||||
_callAudioPlayer.onPlayerComplete.listen((_) async { |
|
||||
if (_isPlayingCallAudio) { |
|
||||
// 如果还在播放状态,重新播放(循环播放) |
|
||||
try { |
|
||||
await _callAudioPlayer.play(AssetSource(Assets.audioCall.replaceFirst('assets/', ''))); |
|
||||
} catch (e) { |
|
||||
print('❌ [CallManager] 循环播放来电铃声失败: $e'); |
|
||||
_isPlayingCallAudio = false; |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
/// 发起通话 |
|
||||
/// [targetUserId] 目标用户ID |
|
||||
/// [callType] 通话类型:语音或视频 |
|
||||
/// [chatController] 聊天控制器,用于发送通话消息 |
|
||||
Future<bool> initiateCall({ |
|
||||
required String targetUserId, |
|
||||
required CallType callType, |
|
||||
ChatController? chatController, |
|
||||
}) async { |
|
||||
try { |
|
||||
if (currentCall.value != null) { |
|
||||
SmartDialog.showToast('已有通话正在进行中'); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
print('📞 [CallManager] 发起${callType == CallType.video ? "视频" : "语音"}通话,目标用户: $targetUserId'); |
|
||||
|
|
||||
// 发起通话前,先调用 CallController 创建一对一 RTC 频道 |
|
||||
final callController = Get.put(CallController()); |
|
||||
final type = callType == CallType.video ? 2 : 1; // 1为音频,2为视频 |
|
||||
final channelData = await callController.createOneOnOneRtcChannel(type: type); |
|
||||
|
|
||||
if (channelData == null) { |
|
||||
print('❌ [CallManager] 创建RTC频道失败,无法发起通话'); |
|
||||
SmartDialog.showToast('创建通话频道失败'); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
print('✅ [CallManager] RTC频道创建成功: ${channelData.channelId}'); |
|
||||
|
|
||||
// 创建通话会话 |
|
||||
final session = CallSession( |
|
||||
targetUserId: targetUserId, |
|
||||
callType: callType, |
|
||||
status: CallStatus.calling, |
|
||||
isInitiator: true, |
|
||||
startTime: DateTime.now(), |
|
||||
); |
|
||||
currentCall.value = session; |
|
||||
|
|
||||
// 发送通话消息(发起) |
|
||||
final callTypeStr = callType == CallType.video ? 'video' : 'voice'; |
|
||||
await _sendCallMessage( |
|
||||
targetUserId: targetUserId, |
|
||||
callType: callTypeStr, |
|
||||
callStatus: 'missed', // 初始状态为未接听,等待对方响应 |
|
||||
channelId: channelData.channelId, // 传递频道ID |
|
||||
chatController: chatController, |
|
||||
); |
|
||||
|
|
||||
// 如果是视频通话,开始循环播放来电铃声 |
|
||||
if (callType == CallType.video) { |
|
||||
startCallAudio(); |
|
||||
} |
|
||||
|
|
||||
// TODO: 这里可以集成实际的通话SDK,发起真正的通话 |
|
||||
// 例如:await RTCManager.instance.startCall(targetUserId, callType); |
|
||||
|
|
||||
return true; |
|
||||
} catch (e) { |
|
||||
print('❌ [CallManager] 发起通话失败: $e'); |
|
||||
SmartDialog.showToast('发起通话失败: $e'); |
|
||||
currentCall.value = null; |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 接听通话 |
|
||||
/// [message] 通话消息 |
|
||||
/// [chatController] 聊天控制器,用于更新通话消息 |
|
||||
Future<bool> acceptCall({ |
|
||||
required EMMessage message, |
|
||||
ChatController? chatController, |
|
||||
}) async { |
|
||||
try { |
|
||||
final callInfo = _parseCallInfo(message); |
|
||||
if (callInfo == null) { |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
final targetUserId = message.from ?? ''; |
|
||||
final callTypeStr = callInfo['callType'] as String?; |
|
||||
final callType = callTypeStr == 'video' ? CallType.video : CallType.voice; |
|
||||
|
|
||||
print('📞 [CallManager] 接听${callType == CallType.video ? "视频" : "语音"}通话'); |
|
||||
|
|
||||
// 创建通话会话 |
|
||||
final session = CallSession( |
|
||||
targetUserId: targetUserId, |
|
||||
callType: callType, |
|
||||
status: CallStatus.calling, |
|
||||
isInitiator: false, |
|
||||
startTime: DateTime.now(), |
|
||||
); |
|
||||
currentCall.value = session; |
|
||||
|
|
||||
// 停止播放来电铃声(已接通) |
|
||||
stopCallAudio(); |
|
||||
|
|
||||
// 开始计时 |
|
||||
_startCallTimer(); |
|
||||
|
|
||||
// 更新通话消息状态为通话中 |
|
||||
await _updateCallMessageStatus( |
|
||||
message: message, |
|
||||
callStatus: 'calling', |
|
||||
callDuration: 0, |
|
||||
chatController: chatController, |
|
||||
); |
|
||||
|
|
||||
// TODO: 这里可以集成实际的通话SDK,接听通话 |
|
||||
// 例如:await RTCManager.instance.acceptCall(targetUserId, callType); |
|
||||
|
|
||||
return true; |
|
||||
} catch (e) { |
|
||||
print('❌ [CallManager] 接听通话失败: $e'); |
|
||||
SmartDialog.showToast('接听通话失败: $e'); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 拒绝通话 |
|
||||
/// [message] 通话消息 |
|
||||
/// [chatController] 聊天控制器,用于更新通话消息 |
|
||||
Future<bool> rejectCall({ |
|
||||
required EMMessage message, |
|
||||
ChatController? chatController, |
|
||||
}) async { |
|
||||
try { |
|
||||
print('📞 [CallManager] 拒绝通话'); |
|
||||
|
|
||||
// 停止播放来电铃声(已拒绝) |
|
||||
stopCallAudio(); |
|
||||
|
|
||||
// 更新通话消息状态为已拒绝 |
|
||||
await _updateCallMessageStatus( |
|
||||
message: message, |
|
||||
callStatus: 'rejected', |
|
||||
chatController: chatController, |
|
||||
); |
|
||||
|
|
||||
// 清理通话会话 |
|
||||
currentCall.value = null; |
|
||||
|
|
||||
// TODO: 这里可以集成实际的通话SDK,拒绝通话 |
|
||||
// 例如:await RTCManager.instance.rejectCall(message.from ?? ''); |
|
||||
|
|
||||
return true; |
|
||||
} catch (e) { |
|
||||
print('❌ [CallManager] 拒绝通话失败: $e'); |
|
||||
SmartDialog.showToast('拒绝通话失败: $e'); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 取消通话 |
|
||||
/// [message] 通话消息(可选,如果是发起方取消) |
|
||||
/// [chatController] 聊天控制器,用于更新通话消息 |
|
||||
Future<bool> cancelCall({ |
|
||||
EMMessage? message, |
|
||||
ChatController? chatController, |
|
||||
}) async { |
|
||||
try { |
|
||||
print('📞 [CallManager] 取消通话'); |
|
||||
|
|
||||
// 如果有消息,更新通话消息状态为已取消 |
|
||||
if (message != null) { |
|
||||
await _updateCallMessageStatus( |
|
||||
message: message, |
|
||||
callStatus: 'cancelled', |
|
||||
chatController: chatController, |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
// 停止播放来电铃声(已取消) |
|
||||
stopCallAudio(); |
|
||||
|
|
||||
// 停止计时 |
|
||||
_stopCallTimer(); |
|
||||
|
|
||||
// 清理通话会话 |
|
||||
currentCall.value = null; |
|
||||
|
|
||||
// TODO: 这里可以集成实际的通话SDK,取消通话 |
|
||||
// 例如:await RTCManager.instance.cancelCall(); |
|
||||
|
|
||||
return true; |
|
||||
} catch (e) { |
|
||||
print('❌ [CallManager] 取消通话失败: $e'); |
|
||||
SmartDialog.showToast('取消通话失败: $e'); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 结束通话(通话完成) |
|
||||
/// [callDuration] 通话时长(秒) |
|
||||
/// [chatController] 聊天控制器,用于更新通话消息 |
|
||||
Future<bool> endCall({ |
|
||||
required int callDuration, |
|
||||
EMMessage? message, |
|
||||
ChatController? chatController, |
|
||||
}) async { |
|
||||
try { |
|
||||
print('📞 [CallManager] 结束通话,时长: ${callDuration}秒'); |
|
||||
|
|
||||
// 停止播放来电铃声(通话结束) |
|
||||
stopCallAudio(); |
|
||||
|
|
||||
// 停止计时 |
|
||||
_stopCallTimer(); |
|
||||
|
|
||||
// 如果有消息,更新通话消息状态为通话中(显示时长) |
|
||||
if (message != null) { |
|
||||
await _updateCallMessageStatus( |
|
||||
message: message, |
|
||||
callStatus: 'calling', |
|
||||
callDuration: callDuration, |
|
||||
chatController: chatController, |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
// 清理通话会话 |
|
||||
currentCall.value = null; |
|
||||
|
|
||||
// TODO: 这里可以集成实际的通话SDK,结束通话 |
|
||||
// 例如:await RTCManager.instance.endCall(); |
|
||||
|
|
||||
return true; |
|
||||
} catch (e) { |
|
||||
print('❌ [CallManager] 结束通话失败: $e'); |
|
||||
SmartDialog.showToast('结束通话失败: $e'); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 开始通话计时 |
|
||||
void _startCallTimer() { |
|
||||
_callDurationSeconds = 0; |
|
||||
callDurationSeconds.value = 0; |
|
||||
_callTimer?.cancel(); |
|
||||
_callTimer = Timer.periodic(Duration(seconds: 1), (timer) { |
|
||||
_callDurationSeconds++; |
|
||||
callDurationSeconds.value = _callDurationSeconds; |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
/// 停止通话计时 |
|
||||
void _stopCallTimer() { |
|
||||
_callTimer?.cancel(); |
|
||||
_callTimer = null; |
|
||||
_callDurationSeconds = 0; |
|
||||
callDurationSeconds.value = 0; |
|
||||
} |
|
||||
|
|
||||
/// 发送通话消息 |
|
||||
Future<bool> _sendCallMessage({ |
|
||||
required String targetUserId, |
|
||||
required String callType, |
|
||||
required String callStatus, |
|
||||
int? callDuration, |
|
||||
String? channelId, |
|
||||
ChatController? chatController, |
|
||||
}) async { |
|
||||
try { |
|
||||
// 如果提供了 chatController,使用它发送消息 |
|
||||
if (chatController != null) { |
|
||||
return await chatController.sendCallMessage( |
|
||||
callType: callType, |
|
||||
callStatus: callStatus, |
|
||||
callDuration: callDuration, |
|
||||
channelId: channelId, |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
// 否则直接通过 IMManager 发送自定义消息 |
|
||||
final callParams = <String, String>{ |
|
||||
'callType': callType, |
|
||||
'callStatus': callStatus, |
|
||||
}; |
|
||||
if (callDuration != null) { |
|
||||
callParams['callDuration'] = callDuration.toString(); |
|
||||
} |
|
||||
if (channelId != null && channelId.isNotEmpty) { |
|
||||
callParams['channelId'] = channelId; |
|
||||
} |
|
||||
|
|
||||
final message = await IMManager.instance.sendCustomMessage( |
|
||||
targetUserId, |
|
||||
'call', |
|
||||
callParams, |
|
||||
); |
|
||||
return message != null; |
|
||||
} catch (e) { |
|
||||
print('❌ [CallManager] 发送通话消息失败: $e'); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 更新通话消息状态(使用modifyMessage修改现有消息) |
|
||||
Future<bool> _updateCallMessageStatus({ |
|
||||
required EMMessage message, |
|
||||
required String callStatus, |
|
||||
int? callDuration, |
|
||||
ChatController? chatController, |
|
||||
}) async { |
|
||||
try { |
|
||||
// 解析现有通话信息 |
|
||||
final callInfo = _parseCallInfo(message); |
|
||||
if (callInfo == null) { |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
final callType = callInfo['callType'] as String? ?? 'voice'; |
|
||||
final messageId = message.msgId; |
|
||||
|
|
||||
if (messageId.isEmpty) { |
|
||||
print('❌ [CallManager] 消息ID为空,无法修改消息'); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
// 如果是自定义消息,使用modifyMessage修改 |
|
||||
if (message.body.type == MessageType.CUSTOM) { |
|
||||
// 构建新的参数 |
|
||||
final callParams = <String, String>{ |
|
||||
'callType': callType, |
|
||||
'callStatus': callStatus, |
|
||||
}; |
|
||||
if (callDuration != null) { |
|
||||
callParams['callDuration'] = callDuration.toString(); |
|
||||
} |
|
||||
|
|
||||
// 创建新的消息体 |
|
||||
final customBody = EMCustomMessageBody( |
|
||||
event: 'call', |
|
||||
params: callParams, |
|
||||
); |
|
||||
|
|
||||
// 使用modifyMessage修改消息 |
|
||||
final success = await IMManager.instance.modifyMessage( |
|
||||
messageId: messageId, |
|
||||
msgBody: customBody, |
|
||||
attributes: null, // 不修改扩展属性 |
|
||||
); |
|
||||
|
|
||||
if (success) { |
|
||||
print('✅ [CallManager] 消息修改成功: messageId=$messageId, callStatus=$callStatus'); |
|
||||
|
|
||||
// 如果提供了chatController,更新本地消息列表 |
|
||||
if (chatController != null) { |
|
||||
// 更新消息体中的参数 |
|
||||
try { |
|
||||
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 ?? {}); |
|
||||
updatedParams['callType'] = callType; |
|
||||
updatedParams['callStatus'] = callStatus; |
|
||||
if (callDuration != null) { |
|
||||
updatedParams['callDuration'] = callDuration.toString(); |
|
||||
} |
|
||||
// 注意:EMCustomMessageBody的params可能是只读的,这里可能需要重新创建消息 |
|
||||
// 暂时先通知UI更新,实际的消息体更新会在收到onMessageContentChanged回调时处理 |
|
||||
chatController.update(); |
|
||||
} |
|
||||
} |
|
||||
} catch (e) { |
|
||||
print('⚠️ [CallManager] 更新本地消息列表失败: $e'); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return success; |
|
||||
} |
|
||||
// 如果不是自定义消息,返回失败 |
|
||||
return false; |
|
||||
} catch (e) { |
|
||||
print('❌ [CallManager] 更新通话消息状态失败: $e'); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 从自定义消息中解析通话信息 |
|
||||
Map<String, dynamic>? _parseCallInfo(EMMessage message) { |
|
||||
try { |
|
||||
if (message.body.type == MessageType.CUSTOM) { |
|
||||
final customBody = message.body as EMCustomMessageBody; |
|
||||
if (customBody.event == 'call' && customBody.params != null) { |
|
||||
final params = customBody.params!; |
|
||||
return { |
|
||||
'callType': params['callType'] ?? 'voice', |
|
||||
'callStatus': params['callStatus'] ?? 'missed', |
|
||||
'callDuration': params['callDuration'] != null |
|
||||
? int.tryParse(params['callDuration']!) |
|
||||
: null, |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
} catch (e) { |
|
||||
print('解析通话信息失败: $e'); |
|
||||
} |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
/// 检查是否有正在进行的通话 |
|
||||
bool get isInCall => currentCall.value != null; |
|
||||
|
|
||||
/// 获取当前通话时长(秒) |
|
||||
int get currentCallDuration => callDurationSeconds.value; |
|
||||
|
|
||||
/// 开始播放来电铃声(循环播放) |
|
||||
/// 可以是发起方或接收方调用 |
|
||||
Future<void> startCallAudio() async { |
|
||||
if (_isPlayingCallAudio) { |
|
||||
return; // 已经在播放中 |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
_isPlayingCallAudio = true; |
|
||||
print('🔊 [CallManager] 开始播放来电铃声'); |
|
||||
await _callAudioPlayer.play(AssetSource(Assets.audioCall.replaceFirst('assets/', ''))); |
|
||||
} catch (e) { |
|
||||
print('❌ [CallManager] 播放来电铃声失败: $e'); |
|
||||
_isPlayingCallAudio = false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 停止播放来电铃声 |
|
||||
/// 可以是发起方或接收方调用 |
|
||||
Future<void> stopCallAudio() async { |
|
||||
if (!_isPlayingCallAudio) { |
|
||||
return; // 没有在播放 |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
_isPlayingCallAudio = false; |
|
||||
print('🔇 [CallManager] 停止播放来电铃声'); |
|
||||
await _callAudioPlayer.stop(); |
|
||||
} catch (e) { |
|
||||
print('❌ [CallManager] 停止播放来电铃声失败: $e'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@override |
|
||||
void onClose() { |
|
||||
stopCallAudio(); |
|
||||
_stopCallTimer(); |
|
||||
currentCall.value = null; |
|
||||
_callAudioPlayer.dispose(); |
|
||||
super.onClose(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 通话会话信息 |
|
||||
class CallSession { |
|
||||
final String targetUserId; |
|
||||
final CallType callType; |
|
||||
final CallStatus status; |
|
||||
final bool isInitiator; // 是否是发起方 |
|
||||
final DateTime startTime; |
|
||||
DateTime? endTime; |
|
||||
|
|
||||
CallSession({ |
|
||||
required this.targetUserId, |
|
||||
required this.callType, |
|
||||
required this.status, |
|
||||
required this.isInitiator, |
|
||||
required this.startTime, |
|
||||
this.endTime, |
|
||||
}); |
|
||||
|
|
||||
/// 获取通话时长(秒) |
|
||||
int get duration { |
|
||||
final end = endTime ?? DateTime.now(); |
|
||||
return end.difference(startTime).inSeconds; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Write
Preview
Loading…
Cancel
Save