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.
 
 
 
 
 

734 lines
22 KiB

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:agora_rtm/agora_rtm.dart';
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:dating_touchme_app/rtc/rtc_manager.dart';
import 'package:dating_touchme_app/rtc/rtm_manager.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 'package:permission_handler/permission_handler.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>();
final NetworkService _networkService;
/// 当前频道信息
final Rxn<RtcChannelData> rtcChannel = Rxn<RtcChannelData>();
/// 是否正在创建频道
final RxBool isCreatingChannel = false.obs;
// 当前正在进行的通话
final Rx<CallSession?> currentCall = Rx<CallSession?>(null);
// 通话计时器(用于记录通话时长)
Timer? _callTimer;
int _callDurationSeconds = 0;
final RxInt callDurationSeconds = RxInt(0);
// 远端用户UID(用于显示远端视频)
final Rxn<int> remoteUid = Rxn<int>();
// 麦克风静音状态
final RxBool isMicMuted = false.obs;
// 扬声器开启状态
final RxBool isSpeakerOn = false.obs;
// 音频播放器(用于播放来电铃声)
final AudioPlayer _callAudioPlayer = AudioPlayer();
bool _isPlayingCallAudio = false;
@override
void onInit() {
super.onInit();
print('📞 [CallController] 通话控制器已初始化');
// 监听音频播放完成事件,实现循环播放
_callAudioPlayer.onPlayerComplete.listen((_) async {
if (_isPlayingCallAudio) {
// 如果还在播放状态,重新播放(循环播放)
await _callAudioPlayer.play(AssetSource(Assets.audioCall.replaceFirst('assets/', '')));
}
});
// 注册 RTM 消息监听器,用于接收通话相关的 RTM 消息
_registerRtmMessageListener();
}
/// 注册 RTM 消息监听器
void _registerRtmMessageListener() {
RTMManager.instance.onMessageEvent = (MessageEvent event) {
_handleRtmMessage(event);
};
print('✅ [CallController] RTM 消息监听器已注册');
}
/// 处理 RTM 消息
void _handleRtmMessage(MessageEvent event) {
try {
// 解析消息内容
String messageText;
if (event.message is String) {
messageText = event.message as String;
} else if (event.message is Uint8List) {
messageText = utf8.decode(event.message as Uint8List);
} else {
messageText = event.message.toString();
}
final messageData = json.decode(messageText) as Map<String, dynamic>;
print('📥 [CallController] 收到 RTM 消息: $messageData');
// 处理通话消息
if (messageData['type'] == 'call_message') {
final event = messageData['event'] as String?;
if (event == 'accept') {
// 发起方收到 accept 消息,设置远端用户 UID
final uid = messageData['uid'];
if (uid != null) {
remoteUid.value = uid is int ? uid : int.tryParse(uid.toString());
print('📞 [CallController] 收到 accept 消息,设置 remoteUid: ${remoteUid.value}');
}
}
}
} catch (e) {
print('❌ [CallController] 处理 RTM 消息失败: $e');
}
}
/// 创建一对一RTC频道
/// [type] 1为音频,2为视频
Future<RtcChannelData?> createOneOnOneRtcChannel({required int type}) async {
if (isCreatingChannel.value) {
print('⚠️ 正在创建频道,请稍候');
return null;
}
// 验证 type 参数
if (type != 1 && type != 2) {
SmartDialog.showToast('类型参数错误:1为音频,2为视频');
return null;
}
isCreatingChannel.value = true;
final response = await _networkService.rtcApi.createOneOnOneRtcChannel({
'type': type,
});
if (response.data.isSuccess && response.data.data != null) {
rtcChannel.value = response.data.data;
print('✅ 创建一对一RTC频道成功: ${response.data.data?.channelId}');
isCreatingChannel.value = false;
return response.data.data;
} else {
final message = response.data.message.isNotEmpty
? response.data.message
: '创建频道失败';
SmartDialog.showToast(message);
print('❌ 创建一对一RTC频道失败: $message');
isCreatingChannel.value = false;
return null;
}
}
/// 创建音频通话频道
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 {
if (currentCall.value != null) {
SmartDialog.showToast('已有通话正在进行中');
return false;
}
// 清空之前的远端用户UID
remoteUid.value = null;
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: 'waitCalling', // 初始状态为未接听,等待对方响应
channelId: channelData.channelId, // 传递频道ID
chatController: chatController,
);
startCallAudio();
// 根据通话类型设置摄像头状态
if (callType == CallType.voice) {
// 语音通话:禁用视频(关闭摄像头)
await RTCManager.instance.disableVideo();
print('📞 [CallController] 语音通话,已关闭摄像头');
} else {
// 视频通话:启用视频(打开摄像头)
await RTCManager.instance.enableVideo();
print('📞 [CallController] 视频通话,已打开摄像头');
}
// 加入 RTC 频道,发起真正的通话
await RTCManager.instance.joinChannel(
token: channelData.token,
channelId: channelData.channelId,
uid: channelData.uid,
role: ClientRoleType.clientRoleBroadcaster,
rtcType: RTCType.call,
);
print('✅ [CallController] 已加入 RTC 频道: ${channelData.channelId}');
// 加入 RTC 频道后订阅 RTM 频道
await RTMManager.instance.subscribe(channelData.channelId);
print('✅ [CallController] 已订阅 RTM 频道: ${channelData.channelId}');
return true;
}
/// 接听通话
/// [message] 通话消息
/// [chatController] 聊天控制器,用于更新通话消息
Future<bool> acceptCall({
required EMMessage message,
ChatController? chatController,
}) async {
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 ? "视频" : "语音"}通话');
// 清空之前的远端用户UID
remoteUid.value = null;
// 创建通话会话
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,
);
// 从通话信息中获取 channelId
final channelId = callInfo['channelId'] as String?;
if (channelId == null || channelId.isEmpty) {
print('❌ [CallController] channelId 为空,无法加入 RTC 频道');
SmartDialog.showToast('频道ID不存在');
return false;
}
// 根据通话类型设置摄像头状态
if (callType == CallType.voice) {
// 语音通话:禁用视频(关闭摄像头)
await RTCManager.instance.disableVideo();
print('📞 [CallController] 语音通话,已关闭摄像头');
} else {
// 视频通话:启用视频(打开摄像头)
await RTCManager.instance.enableVideo();
print('📞 [CallController] 视频通话,已打开摄像头');
}
// 加入 RTC 频道,接听通话
await joinChannel(channelId);
print('✅ [CallController] 已加入 RTC 频道: $channelId');
return true;
}
/// 拒绝通话
/// [message] 通话消息
/// [chatController] 聊天控制器,用于更新通话消息
Future<bool> rejectCall({
required EMMessage message,
ChatController? chatController,
}) async {
print('📞 [CallController] 拒绝通话');
// 停止播放来电铃声(已拒绝)
stopCallAudio();
// 更新通话消息状态为已拒绝
await _updateCallMessageStatus(
message: message,
callStatus: 'rejected',
chatController: chatController,
);
// 清理通话会话
currentCall.value = null;
// TODO: 这里可以集成实际的通话SDK,拒绝通话
// 例如:await RTCManager.instance.rejectCall(message.from ?? '');
return true;
}
/// 取消通话
/// [message] 通话消息(可选,如果是发起方取消)
/// [chatController] 聊天控制器,用于更新通话消息
Future<bool> cancelCall({
EMMessage? message,
ChatController? chatController,
}) async {
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;
}
/// 结束通话(通话完成)
/// [callDuration] 通话时长(秒)
/// [chatController] 聊天控制器,用于更新通话消息
Future<bool> endCall({
required int callDuration,
EMMessage? message,
ChatController? chatController,
}) async {
print('📞 [CallController] 结束通话,时长: ${callDuration}');
// 停止播放来电铃声(通话结束)
stopCallAudio();
// 停止计时
_stopCallTimer();
// 如果有消息,更新通话消息状态为通话中(显示时长)
if (message != null) {
await _updateCallMessageStatus(
message: message,
callStatus: 'calling',
callDuration: callDuration,
chatController: chatController,
);
}
// 清理通话会话和远端用户UID
currentCall.value = null;
remoteUid.value = null;
// TODO: 这里可以集成实际的通话SDK,结束通话
// 例如:await RTCManager.instance.endCall();
return true;
}
/// 开始通话计时
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 {
// 如果提供了 chatController,使用它发送消息
return await chatController!.sendCallMessage(
callType: callType,
callStatus: callStatus,
callDuration: callDuration,
channelId: channelId,
);
}
/// 更新通话消息状态(使用modifyMessage修改现有消息)
Future<bool> _updateCallMessageStatus({
required EMMessage message,
required String callStatus,
int? callDuration,
ChatController? chatController,
}) async {
// 解析现有通话信息
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) {
// 更新消息体中的参数
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();
}
}
}
}
return success;
}
// 如果不是自定义消息,返回失败
return false;
}
/// 从自定义消息中解析通话信息
Map<String, dynamic>? _parseCallInfo(EMMessage message) {
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,
'channelId': params['channelId'],
};
}
}
return null;
}
/// 检查是否有正在进行的通话
bool get isInCall => currentCall.value != null;
/// 获取当前通话时长(秒)
int get currentCallDuration => callDurationSeconds.value;
/// 开始播放来电铃声(循环播放)
/// 可以是发起方或接收方调用
Future<void> startCallAudio() async {
if (_isPlayingCallAudio) {
return; // 已经在播放中
}
_isPlayingCallAudio = true;
print('🔊 [CallController] 开始播放来电铃声');
await _callAudioPlayer.play(AssetSource(Assets.audioCall.replaceFirst('assets/', '')));
}
/// 停止播放来电铃声
/// 可以是发起方或接收方调用
Future<void> stopCallAudio() async {
if (!_isPlayingCallAudio) {
return; // 没有在播放
}
_isPlayingCallAudio = false;
print('🔇 [CallController] 停止播放来电铃声');
await _callAudioPlayer.stop();
}
Future<void> joinChannel(String channelName) async {
final response = await _networkService.rtcApi.getSwRtcToken(channelName);
final base = response.data;
if (base.isSuccess && base.data != null) {
rtcChannel.value = base.data;
// 订阅 RTM 频道
await RTMManager.instance.subscribe(channelName);
// 获取当前通话信息
final callSession = currentCall.value;
final callType = callSession?.callType == CallType.video ? 'video' : 'voice';
// 发布 RTM 消息,包含 UID 和通话相关字段
await RTMManager.instance.publishChannelMessage(
channelName: channelName,
message: json.encode({
'type': 'call_message',
'uid': base.data!.uid,
'callType': callType,
'event': 'accept',
}),
);
await _joinRtcChannel(
base.data!.token,
channelName,
base.data!.uid,
ClientRoleType.clientRoleBroadcaster,
);
}
}
Future<void> _joinRtcChannel(
String token,
String channelName,
int uid,
ClientRoleType roleType,
) async {
final granted = await _ensureRtcPermissions();
if (!granted) return;
await RTCManager.instance.joinChannel(
token: token,
channelId: channelName,
uid: uid,
role: roleType,
rtcType: RTCType.call,
);
final data = {
'channelId': channelName,
'seatNumber': 1,
'isMicrophoneOn': true,
'isVideoOn': true,
};
final response = await _networkService.rtcApi.connectRtcChannel(data);
if (!response.data.isSuccess) {
SmartDialog.showToast(response.data.message);
return;
}
SmartDialog.showToast('加入通话成功');
}
Future<bool> _ensureRtcPermissions() async {
final statuses = await [Permission.camera, Permission.microphone].request();
final allGranted = statuses.values.every((status) => status.isGranted);
if (allGranted) {
return true;
}
final permanentlyDenied = statuses.values.any(
(status) => status.isPermanentlyDenied,
);
if (permanentlyDenied) {
SmartDialog.showToast('请在系统设置中开启摄像头和麦克风权限');
await openAppSettings();
} else {
SmartDialog.showToast('请允许摄像头和麦克风权限以进入房间');
}
return false;
}
/// 切换麦克风状态
Future<void> toggleMic() async {
isMicMuted.value = !isMicMuted.value;
await RTCManager.instance.muteLocalAudio(isMicMuted.value);
print('📞 [CallController] 麦克风${isMicMuted.value ? "已静音" : "已取消静音"}');
}
/// 切换扬声器状态
Future<void> toggleSpeaker() async {
isSpeakerOn.value = !isSpeakerOn.value;
await RTCManager.instance.setEnableSpeakerphone(isSpeakerOn.value);
print('📞 [CallController] 扬声器${isSpeakerOn.value ? "已开启" : "已关闭"}');
}
/// 挂断通话
Future<void> hangUpCall() async {
// 离开RTC频道
await RTCManager.instance.leaveChannel();
// 结束通话(传递通话时长)
await endCall(callDuration: callDurationSeconds.value);
print('✅ [CallController] 通话已挂断');
}
@override
void onClose() {
stopCallAudio();
_stopCallTimer();
currentCall.value = null;
rtcChannel.value = null;
remoteUid.value = null;
isMicMuted.value = false;
isSpeakerOn.value = false;
_callAudioPlayer.dispose();
super.onClose();
}
}