You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
522 lines
15 KiB
522 lines
15 KiB
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 'chat_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');
|
|
|
|
// 创建通话会话
|
|
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', // 初始状态为未接听,等待对方响应
|
|
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,
|
|
ChatController? chatController,
|
|
}) async {
|
|
try {
|
|
// 如果提供了 chatController,使用它发送消息
|
|
if (chatController != null) {
|
|
return await chatController.sendCallMessage(
|
|
callType: callType,
|
|
callStatus: callStatus,
|
|
callDuration: callDuration,
|
|
);
|
|
}
|
|
|
|
// 否则直接通过 IMManager 发送自定义消息
|
|
final callParams = <String, String>{
|
|
'callType': callType,
|
|
'callStatus': callStatus,
|
|
};
|
|
if (callDuration != null) {
|
|
callParams['callDuration'] = callDuration.toString();
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|