Browse Source

feat(call): 将通话管理功能从CallManager迁移到CallController

- 实现语音和视频通话的发起、接听、拒绝、取消和结束功能
- 添加通话状态管理包括等待接通、通话中、未接听、已取消和已拒绝
- 集成音频播放器实现来电铃声的循环播放功能
- 实现通话计时器功能记录通话时长
- 完成通话消息的发送和状态更新机制
- 迁移通话相关枚举和数据模型到CallController
- 更新聊天页面和IM管理器使用新的CallController实例
- 移除独立的CallManager文件并整合到CallController中
master
Jolie 3 months ago
parent
commit
a4118ffba1
7 changed files with 589 additions and 592 deletions
  1. 540
      lib/controller/message/call_controller.dart
  2. 16
      lib/im/im_manager.dart
  3. 37
      lib/pages/message/chat_page.dart
  4. 26
      lib/pages/message/video_call_page.dart
  5. 542
      lib/rtc/call_manager.dart
  6. 10
      lib/widget/live/live_room_notice_chat_panel.dart
  7. 10
      lib/widget/message/video_call_overlay_widget.dart

540
lib/controller/message/call_controller.dart

@ -1,10 +1,63 @@
import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/im/im_manager.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart';
import 'package:dating_touchme_app/network/network_service.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 'chat_controller.dart';
///
enum CallType {
voice, //
video, //
}
///
enum CallStatus {
waitCalling, //
calling, //
missed, //
cancelled, //
rejected, //
}
///
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;
}
}
///
class CallController extends GetxController {
static CallController? _instance;
static CallController get instance {
_instance ??= Get.put(CallController());
return _instance!;
}
CallController({NetworkService? networkService})
: _networkService = networkService ?? Get.find<NetworkService>();
@ -16,6 +69,36 @@ class CallController extends GetxController {
///
final RxBool isCreatingChannel = false.obs;
//
final Rx<CallSession?> currentCall = Rx<CallSession?>(null);
//
Timer? _callTimer;
int _callDurationSeconds = 0;
final RxInt callDurationSeconds = RxInt(0);
//
final AudioPlayer _callAudioPlayer = AudioPlayer();
bool _isPlayingCallAudio = false;
@override
void onInit() {
super.onInit();
print('📞 [CallController] 通话控制器已初始化');
//
_callAudioPlayer.onPlayerComplete.listen((_) async {
if (_isPlayingCallAudio) {
//
try {
await _callAudioPlayer.play(AssetSource(Assets.audioCall.replaceFirst('assets/', '')));
} catch (e) {
print('❌ [CallController] 循环播放来电铃声失败: $e');
_isPlayingCallAudio = false;
}
}
});
}
/// RTC频道
/// [type] 12
Future<RtcChannelData?> createOneOnOneRtcChannel({required int type}) async {
@ -59,10 +142,463 @@ class CallController extends GetxController {
}
}
///
Future<RtcChannelData?> createAudioChannel() {
return createOneOnOneRtcChannel(type: 1);
}
///
Future<RtcChannelData?> createVideoChannel() {
return createOneOnOneRtcChannel(type: 2);
}
///
/// [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('📞 [CallController] 发起${callType == CallType.video ? "视频" : "语音"}通话,目标用户: $targetUserId');
// RTC
final type = callType == CallType.video ? 2 : 1; // 12
final channelData = await createOneOnOneRtcChannel(type: type);
if (channelData == null) {
print('❌ [CallController] 创建RTC频道失败,无法发起通话');
SmartDialog.showToast('创建通话频道失败');
return false;
}
print('✅ [CallController] 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,
);
startCallAudio();
// TODO: SDK
// await RTCManager.instance.startCall(targetUserId, callType);
return true;
} catch (e) {
print('❌ [CallController] 发起通话失败: $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('📞 [CallController] 接听${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('❌ [CallController] 接听通话失败: $e');
SmartDialog.showToast('接听通话失败: $e');
return false;
}
}
///
/// [message]
/// [chatController]
Future<bool> rejectCall({
required EMMessage message,
ChatController? chatController,
}) async {
try {
print('📞 [CallController] 拒绝通话');
//
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('❌ [CallController] 拒绝通话失败: $e');
SmartDialog.showToast('拒绝通话失败: $e');
return false;
}
}
///
/// [message]
/// [chatController]
Future<bool> cancelCall({
EMMessage? message,
ChatController? chatController,
}) async {
try {
print('📞 [CallController] 取消通话');
//
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('❌ [CallController] 取消通话失败: $e');
SmartDialog.showToast('取消通话失败: $e');
return false;
}
}
///
/// [callDuration]
/// [chatController]
Future<bool> endCall({
required int callDuration,
EMMessage? message,
ChatController? chatController,
}) async {
try {
print('📞 [CallController] 结束通话,时长: ${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('❌ [CallController] 结束通话失败: $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('❌ [CallController] 发送通话消息失败: $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('❌ [CallController] 消息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('✅ [CallController] 消息修改成功: 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('⚠️ [CallController] 更新本地消息列表失败: $e');
}
}
}
return success;
}
//
return false;
} catch (e) {
print('❌ [CallController] 更新通话消息状态失败: $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('🔊 [CallController] 开始播放来电铃声');
await _callAudioPlayer.play(AssetSource(Assets.audioCall.replaceFirst('assets/', '')));
} catch (e) {
print('❌ [CallController] 播放来电铃声失败: $e');
_isPlayingCallAudio = false;
}
}
///
///
Future<void> stopCallAudio() async {
if (!_isPlayingCallAudio) {
return; //
}
try {
_isPlayingCallAudio = false;
print('🔇 [CallController] 停止播放来电铃声');
await _callAudioPlayer.stop();
} catch (e) {
print('❌ [CallController] 停止播放来电铃声失败: $e');
}
}
@override
void onClose() {
super.onClose();
stopCallAudio();
_stopCallTimer();
currentCall.value = null;
rtcChannel.value = null;
_callAudioPlayer.dispose();
super.onClose();
}
}

16
lib/im/im_manager.dart

@ -18,7 +18,7 @@ import '../network/user_api.dart';
import '../widget/message/message_notification_dialog.dart';
import '../widget/message/video_call_invite_dialog.dart';
import '../pages/message/video_call_page.dart';
import '../rtc/call_manager.dart';
import '../controller/message/call_controller.dart';
import '../service/local_notification_service.dart';
//
@ -1494,8 +1494,8 @@ class IMManager {
final finalAvatarUrl = avatarUrl ?? '';
//
final callManager = CallManager.instance;
callManager.startCallAudio();
final callController = CallController.instance;
callController.startCallAudio();
//
SmartDialog.show(
@ -1508,7 +1508,7 @@ class IMManager {
SmartDialog.dismiss();
//
callManager.stopCallAudio();
callController.stopCallAudio();
//
Get.to(() => VideoCallPage(
@ -1521,7 +1521,7 @@ class IMManager {
SmartDialog.dismiss();
// acceptCall
callManager.stopCallAudio();
callController.stopCallAudio();
//
ChatController? chatController;
@ -1536,7 +1536,7 @@ class IMManager {
}
}
final accepted = await callManager.acceptCall(
final accepted = await callController.acceptCall(
message: message,
chatController: chatController,
);
@ -1554,7 +1554,7 @@ class IMManager {
SmartDialog.dismiss();
// rejectCall
callManager.stopCallAudio();
callController.stopCallAudio();
// rejected
ChatController? chatController;
@ -1570,7 +1570,7 @@ class IMManager {
}
// 使 modifyMessage
await callManager.rejectCall(
await callController.rejectCall(
message: message,
chatController: chatController,
);

37
lib/pages/message/chat_page.dart

@ -4,10 +4,9 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import '../../rtc/call_manager.dart';
import '../../controller/message/call_controller.dart';
import '../../controller/message/chat_controller.dart';
import '../../controller/message/voice_player_manager.dart';
// import '../../controller/message/call_manager.dart'; //
import '../../generated/assets.dart';
import '../../model/home/marriage_data.dart';
import '../../../widget/message/chat_input_bar.dart';
@ -145,31 +144,35 @@ class _ChatPageState extends State<ChatPage> {
return CallTypeSelectionDialog(
onVoiceCall: () async {
//
await CallManager.instance.initiateCall(
final success = await CallController.instance.initiateCall(
targetUserId: widget.userId,
callType: CallType.voice,
chatController: controller,
);
//
Get.to(() => VideoCallPage(
targetUserId: widget.userId,
userData: widget.userData ?? controller.userData,
isInitiator: true,
));
//
if (success) {
Get.to(() => VideoCallPage(
targetUserId: widget.userId,
userData: widget.userData ?? controller.userData,
isInitiator: true,
));
}
},
onVideoCall: () async {
//
await CallManager.instance.initiateCall(
final success = await CallController.instance.initiateCall(
targetUserId: widget.userId,
callType: CallType.video,
chatController: controller,
);
//
Get.to(() => VideoCallPage(
targetUserId: widget.userId,
userData: widget.userData ?? controller.userData,
isInitiator: true,
));
//
if (success) {
Get.to(() => VideoCallPage(
targetUserId: widget.userId,
userData: widget.userData ?? controller.userData,
isInitiator: true,
));
}
},
);
},
@ -351,7 +354,7 @@ class _ChatPageState extends State<ChatPage> {
//
// onVoiceCall: () async {
// //
// await CallManager.instance.initiateCall(
// await CallController.instance.initiateCall(
// targetUserId: widget.userId,
// callType: CallType.voice,
// chatController: controller,

26
lib/pages/message/video_call_page.dart

@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../rtc/call_manager.dart';
import '../../controller/message/call_controller.dart';
import '../../controller/message/conversation_controller.dart';
import '../../controller/overlay_controller.dart';
import '../../model/home/marriage_data.dart';
@ -29,7 +29,7 @@ class VideoCallPage extends StatefulWidget {
}
class _VideoCallPageState extends State<VideoCallPage> {
final CallManager _callManager = CallManager.instance;
final CallController _callController = CallController.instance;
bool _isMicMuted = false;
bool _isSpeakerOn = false;
@ -60,11 +60,11 @@ class _VideoCallPageState extends State<VideoCallPage> {
///
void _initCallStatus() {
//
final callSession = _callManager.currentCall.value;
if (callSession != null && _callManager.callDurationSeconds.value > 0) {
final callSession = _callController.currentCall.value;
if (callSession != null && _callController.callDurationSeconds.value > 0) {
//
_isCallConnected = true;
_callDuration = Duration(seconds: _callManager.callDurationSeconds.value);
_callDuration = Duration(seconds: _callController.callDurationSeconds.value);
} else {
//
_isCallConnected = false;
@ -141,16 +141,16 @@ class _VideoCallPageState extends State<VideoCallPage> {
///
void _startDurationTimer() {
// CallManager
_callManager.currentCall.listen((callSession) {
// CallController
_callController.currentCall.listen((callSession) {
if (mounted) {
final wasConnected = _isCallConnected;
//
if (callSession != null && _callManager.callDurationSeconds.value > 0) {
if (callSession != null && _callController.callDurationSeconds.value > 0) {
_isCallConnected = true;
if (!wasConnected) {
//
_callDuration = Duration(seconds: _callManager.callDurationSeconds.value);
_callDuration = Duration(seconds: _callController.callDurationSeconds.value);
}
} else if (callSession == null) {
_isCallConnected = false;
@ -160,7 +160,7 @@ class _VideoCallPageState extends State<VideoCallPage> {
});
//
_callManager.callDurationSeconds.listen((seconds) {
_callController.callDurationSeconds.listen((seconds) {
if (mounted && _isCallConnected) {
setState(() {
_callDuration = Duration(seconds: seconds);
@ -178,8 +178,8 @@ class _VideoCallPageState extends State<VideoCallPage> {
if (!_isCallConnected) {
_durationTimer = Timer.periodic(Duration(seconds: 1), (timer) {
if (mounted) {
final callSession = _callManager.currentCall.value;
final duration = _callManager.callDurationSeconds.value;
final callSession = _callController.currentCall.value;
final duration = _callController.callDurationSeconds.value;
// 0
if (callSession != null && duration > 0) {
@ -226,7 +226,7 @@ class _VideoCallPageState extends State<VideoCallPage> {
// await _rtcManager.leaveChannel();
//
await _callManager.endCall(callDuration: _callDuration.inSeconds);
await _callController.endCall(callDuration: _callDuration.inSeconds);
//
Get.back();

542
lib/rtc/call_manager.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; // 12
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;
}
}

10
lib/widget/live/live_room_notice_chat_panel.dart

@ -92,12 +92,12 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
if (scrollController.hasClients && messages.isNotEmpty) {
//
Future.microtask(() {
if (scrollController.hasClients) {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
if (scrollController.hasClients) {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
curve: Curves.easeOut,
);
}
});
}

10
lib/widget/message/video_call_overlay_widget.dart

@ -1,5 +1,5 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dating_touchme_app/rtc/call_manager.dart';
import 'package:dating_touchme_app/controller/message/call_controller.dart';
import 'package:dating_touchme_app/extension/ex_widget.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/pages/message/video_call_page.dart';
@ -29,7 +29,7 @@ class VideoCallOverlayWidget extends StatefulWidget {
class _VideoCallOverlayWidgetState extends State<VideoCallOverlayWidget> {
Offset _position = Offset.zero;
bool _isDragging = false;
final CallManager _callManager = CallManager.instance;
final CallController _callController = CallController.instance;
@override
void initState() {
@ -102,9 +102,9 @@ class _VideoCallOverlayWidgetState extends State<VideoCallOverlayWidget> {
_snapToEdge(screenSize.width);
},
child: Obx(() {
final callSession = _callManager.currentCall.value;
final isConnected = callSession != null && _callManager.callDurationSeconds.value > 0;
final callDuration = _callManager.callDurationSeconds.value;
final callSession = _callController.currentCall.value;
final isConnected = callSession != null && _callController.callDurationSeconds.value > 0;
final callDuration = _callController.callDurationSeconds.value;
return Container(
width: 100.w,

Loading…
Cancel
Save