From a4118ffba17d59f6b2ad1e39d2ab5b03a3a844be Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Mon, 29 Dec 2025 19:41:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(call):=20=E5=B0=86=E9=80=9A=E8=AF=9D?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=E4=BB=8ECallManager?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=88=B0CallController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现语音和视频通话的发起、接听、拒绝、取消和结束功能 - 添加通话状态管理包括等待接通、通话中、未接听、已取消和已拒绝 - 集成音频播放器实现来电铃声的循环播放功能 - 实现通话计时器功能记录通话时长 - 完成通话消息的发送和状态更新机制 - 迁移通话相关枚举和数据模型到CallController - 更新聊天页面和IM管理器使用新的CallController实例 - 移除独立的CallManager文件并整合到CallController中 --- lib/controller/message/call_controller.dart | 540 ++++++++++++++++- lib/im/im_manager.dart | 16 +- lib/pages/message/chat_page.dart | 37 +- lib/pages/message/video_call_page.dart | 26 +- lib/rtc/call_manager.dart | 542 ------------------ .../live/live_room_notice_chat_panel.dart | 10 +- .../message/video_call_overlay_widget.dart | 10 +- 7 files changed, 589 insertions(+), 592 deletions(-) delete mode 100644 lib/rtc/call_manager.dart diff --git a/lib/controller/message/call_controller.dart b/lib/controller/message/call_controller.dart index 4370c6a..1c3bedf 100644 --- a/lib/controller/message/call_controller.dart +++ b/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(); @@ -16,6 +69,36 @@ class CallController extends GetxController { /// 是否正在创建频道 final RxBool isCreatingChannel = false.obs; + // 当前正在进行的通话 + final Rx currentCall = Rx(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] 1为音频,2为视频 Future createOneOnOneRtcChannel({required int type}) async { @@ -59,10 +142,463 @@ class CallController extends GetxController { } } + /// 创建音频通话频道 + Future createAudioChannel() { + return createOneOnOneRtcChannel(type: 1); + } + + /// 创建视频通话频道 + Future createVideoChannel() { + return createOneOnOneRtcChannel(type: 2); + } + + /// 发起通话 + /// [targetUserId] 目标用户ID + /// [callType] 通话类型:语音或视频 + /// [chatController] 聊天控制器,用于发送通话消息 + Future 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; // 1为音频,2为视频 + 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 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 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 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 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 _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 = { + '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 _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 = { + '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.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? _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 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 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(); } } - diff --git a/lib/im/im_manager.dart b/lib/im/im_manager.dart index 7e0079b..4798bbc 100644 --- a/lib/im/im_manager.dart +++ b/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, ); diff --git a/lib/pages/message/chat_page.dart b/lib/pages/message/chat_page.dart index f79fad5..d50df5a 100644 --- a/lib/pages/message/chat_page.dart +++ b/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 { 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 { // 语音通话回调(暂时隐藏) // onVoiceCall: () async { // // 发起语音通话 - // await CallManager.instance.initiateCall( + // await CallController.instance.initiateCall( // targetUserId: widget.userId, // callType: CallType.voice, // chatController: controller, diff --git a/lib/pages/message/video_call_page.dart b/lib/pages/message/video_call_page.dart index 46f2f94..d0d008d 100644 --- a/lib/pages/message/video_call_page.dart +++ b/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 { - final CallManager _callManager = CallManager.instance; + final CallController _callController = CallController.instance; bool _isMicMuted = false; bool _isSpeakerOn = false; @@ -60,11 +60,11 @@ class _VideoCallPageState extends State { /// 初始化通话状态 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 { /// 开始通话时长计时器 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 { }); // 监听通话时长变化(已接通时更新) - _callManager.callDurationSeconds.listen((seconds) { + _callController.callDurationSeconds.listen((seconds) { if (mounted && _isCallConnected) { setState(() { _callDuration = Duration(seconds: seconds); @@ -178,8 +178,8 @@ class _VideoCallPageState extends State { 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 { // await _rtcManager.leaveChannel(); // 结束通话(传递通话时长) - await _callManager.endCall(callDuration: _callDuration.inSeconds); + await _callController.endCall(callDuration: _callDuration.inSeconds); // 返回上一页 Get.back(); diff --git a/lib/rtc/call_manager.dart b/lib/rtc/call_manager.dart deleted file mode 100644 index 304e8ce..0000000 --- a/lib/rtc/call_manager.dart +++ /dev/null @@ -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 currentCall = Rx(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 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 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 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 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 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 _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 = { - '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 _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 = { - '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.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? _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 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 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; - } -} - diff --git a/lib/widget/live/live_room_notice_chat_panel.dart b/lib/widget/live/live_room_notice_chat_panel.dart index 588698b..23261e1 100644 --- a/lib/widget/live/live_room_notice_chat_panel.dart +++ b/lib/widget/live/live_room_notice_chat_panel.dart @@ -92,12 +92,12 @@ class _LiveRoomNoticeChatPanelState extends State { 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, + ); } }); } diff --git a/lib/widget/message/video_call_overlay_widget.dart b/lib/widget/message/video_call_overlay_widget.dart index c965073..395713b 100644 --- a/lib/widget/message/video_call_overlay_widget.dart +++ b/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 { 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 { _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,